diff --git a/src/linecomps.tsx b/src/linecomps.tsx index 5ee077bc..ea28390b 100644 --- a/src/linecomps.tsx +++ b/src/linecomps.tsx @@ -600,6 +600,33 @@ class LineCmd extends React.Component< }; } + scrollToBringIntoViewport = () => { + const container = document.getElementsByClassName("lines")[0]; + const targetDiv = this.lineRef.current; + const targetPosition = targetDiv.getBoundingClientRect(); + const containerPosition = container.getBoundingClientRect(); + + // Check if the top of the targetDiv is above the container's visible area + if (targetPosition.top < containerPosition.top) { + // Scroll up to make the top of the targetDiv visible + const scrollAmount = container.scrollTop + targetPosition.top - containerPosition.top; + container.scrollTo({ + top: scrollAmount, + behavior: "smooth", + }); + } + // Check if the bottom of the targetDiv is below the container's visible area + else if (targetPosition.bottom > containerPosition.bottom) { + // Scroll down to make the bottom of the targetDiv visible + const scrollAmount = container.scrollTop + targetPosition.bottom - containerPosition.bottom; + container.scrollTo({ + top: scrollAmount, + behavior: "smooth", + }); + } + // If both conditions are false, then targetDiv is already fully visible, no scrolling needed + }; + render() { let { screen, line, width, staticRender, visible, topBorder, renderMode } = this.props; let model = GlobalModel; @@ -734,6 +761,7 @@ class LineCmd extends React.Component< plugin={rendererPlugin} onHeightChange={this.handleHeightChange} initParams={this.makeRendererModelInitializeParams()} + scrollToBringIntoViewport={this.scrollToBringIntoViewport} /> diff --git a/src/prompt.less b/src/prompt.less index 001a66a3..0d150046 100644 --- a/src/prompt.less +++ b/src/prompt.less @@ -239,12 +239,11 @@ input[type="checkbox"] { } .dropdown { - background: rgb(180, 180, 180); + background: #dbdbdb; color: black; - border-radius: 4px 4px 0 0; + border-radius: 6px 6px 0 0; font-size: 10px; - font-family: system-ui; - padding: 1px 0 3px 3px; + padding: 2px 0 5px 5px; outline: none; } } @@ -254,6 +253,37 @@ input[type="checkbox"] { .monaco-editor .monaco-editor-background { background-color: rgba(255, 255, 255, 0.075) !important; } + .cmd-hints { + display: inline-block; + position: relative; + margin-right: 26px; + } + .hint-item { + border-radius: 4px 4px 0 0; + padding: 3px 9px 2px 8px; + line-height: 15px; + text-align: center; + } + section { + transition: height 0.3s ease-in-out; + } + .save-enabled { + color: white; + background-color: #4e9a06; + } + .save-disabled { + color: rgb(52, 52, 52); + background-color: #aaaea7; + cursor: default !important; + } + .error { + background-color: red; + color: white; + border-radius: 6px; + margin-bottom: 1rem; + padding: 4px 1rem; + max-width: 16rem; + } } .renderer-container.json-renderer { diff --git a/src/simplerenderer.tsx b/src/simplerenderer.tsx index c7136319..5c65c30e 100644 --- a/src/simplerenderer.tsx +++ b/src/simplerenderer.tsx @@ -176,6 +176,7 @@ class SimpleBlobRenderer extends React.Component< plugin: RendererPluginType; onHeightChange: () => void; initParams: RendererModelInitializeParams; + scrollToBringIntoViewport: () => void; }, {} > { @@ -262,17 +263,19 @@ class SimpleBlobRenderer extends React.Component<
(no component found in plugin)
; } let simpleModel = model as SimpleBlobRendererModel; - let { festate, cmdstr } = this.props.initParams.rawCmd; + let { festate, cmdstr, exitcode } = this.props.initParams.rawCmd; return (
); diff --git a/src/view/code.tsx b/src/view/code.tsx index b07b943d..42eb01bb 100644 --- a/src/view/code.tsx +++ b/src/view/code.tsx @@ -1,90 +1,153 @@ import * as React from "react"; -import * as mobx from "mobx"; -import * as mobxReact from "mobx-react"; import { RendererContext, RendererOpts, LineStateType } from "../types"; import Editor from "@monaco-editor/react"; import { GlobalModel } from "../model"; -type OV = mobx.IObservableValue; - -@mobxReact.observer class SourceCodeRenderer extends React.Component< { data: Blob; cmdstr: String; cwd: String; + exitcode: Number; context: RendererContext; opts: RendererOpts; savedHeight: number; + scrollToBringIntoViewport: () => void; lineState: LineStateType; }, {} > { - code: OV = mobx.observable.box(""); - language: OV = mobx.observable.box(""); - languages: OV = mobx.observable.box([]); - selectedLanguage: OV = mobx.observable.box(""); + /** + * codeCache is a Hashmap with key=filepath and value=code + * Editor should never read the code directly from the filesystem. it should read from the cache. + * Upon loading a file (props.data contains the file-contents) FOR THE FIRST TIME, + * we will put it in the cache, and will update the contents of the cache upon every onChange(). + * ALl this is to ensure that the file contents doesnt get reloaded when the line scrolls out of the viewport + * (and hence the react component gets destroyed) + */ + static codeCache = new Map(); - editorRef; + filePath; constructor(props) { super(props); this.editorRef = React.createRef(); + this.state = { + code: "", + language: "", + languages: [], + selectedLanguage: "", + isFullWindow: false, + isSave: false, + editorHeight: props.savedHeight, + errorMessage: null, + }; } - componentDidMount() { - let prtn = this.props.data.text(); - prtn.then((text) => this.code.set(text)); + componentDidMount(): void { + // DANGEROUS ... I AM ASSUMING THE COMMAND IS IN FORMAT cat prompt_samples/sample.java + // filePath should be saved in the new lineOpts field that Mike is working on :) + this.filePath = `${this.props.cwd}/${this.props.cmdstr.split(" ")[1]}`; + const code = SourceCodeRenderer.codeCache.get(this.filePath); + if (code) { + this.setState({ code }); + } else + this.props.data.text().then((code) => { + this.setState({ code }); + SourceCodeRenderer.codeCache.set(this.filePath, code); + }); } handleEditorDidMount = (editor, monaco) => { - // Use a regular expression to match a filename with an extension const extension = this.props.cmdstr.match(/(?:[^\\\/:*?"<>|\r\n]+\.)([a-zA-Z0-9]+)\b/)?.[1] || ""; const detectedLanguage = monaco.languages .getLanguages() - .find((lang) => lang.extensions && lang.extensions.includes("." + extension)); + .find((lang) => lang.extensions?.includes("." + extension)); const languages = monaco.languages.getLanguages().map((lang) => lang.id); - this.languages.set(languages); + this.setState({ languages }); if (detectedLanguage) { - this.selectedLanguage.set(detectedLanguage.id); this.editorRef.current = editor; const model = editor.getModel(); if (model) { monaco.editor.setModelLanguage(model, detectedLanguage.id); - this.language.set(detectedLanguage.id); + this.setState({ selectedLanguage: detectedLanguage.id, language: detectedLanguage.id }); } } + this.setEditorHeight(); }; handleLanguageChange = (event) => { const selectedLanguage = event.target.value; - this.selectedLanguage.set(selectedLanguage); + this.setState({ selectedLanguage }); if (this.editorRef.current) { const model = this.editorRef.current.getModel(); if (model) { monaco.editor.setModelLanguage(model, selectedLanguage); - this.language.set(selectedLanguage); + this.setState({ language: selectedLanguage }); } } }; - render() { - let opts = this.props.opts; - let lang = this.language.get(); - let code = this.code.get(); - if (!code) { - return
; + toggleFit = () => { + const isFullWindow = !this.state.isFullWindow; + this.setState({ isFullWindow }); + this.setEditorHeight(); + setTimeout(() => this.props.scrollToBringIntoViewport(), 350); + }; + + doSave = () => { + // call the function that would save the file to filesystem. would likely be async + // as a result of the save operation, the entire component should get reloaded (** HOW **) + // once its reloaded, this.props.data.text() should contain the latest code + // in which case, the cache will get refilled and we can consider the transaction "committed" + this.setState({ errorMessage: "File could not be saved" }); + setTimeout(() => this.setState({ errorMessage: null }), 3000); + }; + + handleEditorChange = (code) => { + this.setState({ isFullWindow: true, code }); + SourceCodeRenderer.codeCache.set(this.filePath, code); + this.setEditorHeight(); + setTimeout(() => this.props.scrollToBringIntoViewport(), 350); + this.props.data.text().then((originalCode) => this.setState({ isSave: code !== originalCode })); + }; + + setEditorHeight = () => { + const fullWindowHeight = parseInt(this.props.opts.maxSize.height); + let _editorHeight = fullWindowHeight; + if (!this.state.isFullWindow) { + const noOfLines = this.state.code.split("\n").length; + _editorHeight = Math.min(noOfLines * GlobalModel.termFontSize.get() * 1.5 + 10, fullWindowHeight); } - const noOfLines = code.split("\n").length; - const editorHeight = Math.min( - noOfLines * GlobalModel.termFontSize.get() * 1.5 + 10, - parseInt(opts.maxSize.height) - ); + this.setState({ editorHeight: _editorHeight }); + }; + + render() { + const { opts, exitcode } = this.props; + const { lang, code, isSave } = this.state; + + if (!code) + return
; + + if (exitcode === 1) + return ( +
+ {code} +
+ ); + return (
+
+
+ {this.state.isFullWindow ? `shrink` : `expand`} +
+
+ {!this.props.opts.readOnly && ( +
+
+ {"save"} +
+
+ )}
+ {this.state.errorMessage && ( +
+
+ {this.state.errorMessage} +
+
+ )}
); } diff --git a/src/view/code_mobx.tsx b/src/view/code_mobx.tsx new file mode 100644 index 00000000..41a0c350 --- /dev/null +++ b/src/view/code_mobx.tsx @@ -0,0 +1,149 @@ +import * as React from "react"; +import * as mobx from "mobx"; +import * as mobxReact from "mobx-react"; +import { RendererContext, RendererOpts } from "../types"; +import Editor from "@monaco-editor/react"; +import { GlobalModel } from "../model"; + +type OV = mobx.IObservableValue; + +@mobxReact.observer +class SourceCodeRenderer extends React.Component< + { + data: Blob; + cmdstr: String; + cwd: String; + context: RendererContext; + opts: RendererOpts; + savedHeight: number; + scrollToBringIntoViewport: () => void; + }, + {} +> { + code: OV = mobx.observable.box(""); + language: OV = mobx.observable.box(""); + languages: OV = mobx.observable.box([]); + selectedLanguage: OV = mobx.observable.box(""); + isFullWindow: OV = mobx.observable.box(false); // load this from opts + editorHeight: OV = mobx.observable.box(this.props.savedHeight); // load this from opts + editorRef; + resizeObserver; + constructor(props) { + super(props); + this.editorRef = React.createRef(); + console.log(`resetting the code`); + this.props.data.text().then((text) => this.code.set(text)); + } + + componentDidMount() {} + + componentWillUnmount(): void { + console.log(`will unmount`); + } + + handleEditorDidMount = (editor, monaco) => { + // Use a regular expression to match a filename with an extension + const extension = this.props.cmdstr.match(/(?:[^\\\/:*?"<>|\r\n]+\.)([a-zA-Z0-9]+)\b/)?.[1] || ""; + const detectedLanguage = monaco.languages + .getLanguages() + .find((lang) => lang.extensions && lang.extensions.includes("." + extension)); + const languages = monaco.languages.getLanguages().map((lang) => lang.id); + this.languages.set(languages); + if (detectedLanguage) { + this.selectedLanguage.set(detectedLanguage.id); + this.editorRef.current = editor; + const model = editor.getModel(); + if (model) { + monaco.editor.setModelLanguage(model, detectedLanguage.id); + this.language.set(detectedLanguage.id); + } + } + this.setEditorHeight(); + }; + + handleLanguageChange = (event) => { + const selectedLanguage = event.target.value; + this.selectedLanguage.set(selectedLanguage); + if (this.editorRef.current) { + const model = this.editorRef.current.getModel(); + if (model) { + monaco.editor.setModelLanguage(model, selectedLanguage); + this.language.set(selectedLanguage); + } + } + }; + + toggleFit = () => { + this.isFullWindow.set(!this.isFullWindow.get()); + this.setEditorHeight(); + setTimeout(() => this.props.scrollToBringIntoViewport(), 350); + }; + + handleEditorChange = (value, event) => { + // editing will always be in fullscreen + this.isFullWindow.set(true); + this.code.set(value); + this.setEditorHeight(); + setTimeout(() => this.props.scrollToBringIntoViewport(), 350); + }; + + setEditorHeight = () => { + const fullWindowHeight = parseInt(this.props.opts.maxSize.height); + let _editorHeight = fullWindowHeight; + if (!this.isFullWindow.get()) { + const noOfLines = this.code.get().split("\n").length; + _editorHeight = Math.min(noOfLines * GlobalModel.termFontSize.get() * 1.5 + 10, fullWindowHeight); + } + this.editorHeight.set(_editorHeight); + }; + + render() { + let opts = this.props.opts; + let lang = this.language.get(); + let code = this.code.get(); + if (!code) { + return
; + } + return ( +
+
+ +
+
+ +
+
+ {this.isFullWindow.get() ? `shrink` : `expand`} +
+
+
+
+ ); + } +} + +export { SourceCodeRenderer };