diff --git a/package.json b/package.json index bdd5a935..f4d4d436 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,78 @@ { - "name": "Prompt", - "version": "0.3.0", - "main": "dist/emain.js", - "license": "Proprietary", - "dependencies": { - "@monaco-editor/react": "^4.5.1", - "autobind-decorator": "^2.4.0", - "classnames": "^2.3.1", - "dayjs": "^1.11.3", - "dompurify": "^3.0.2", - "electron-squirrel-startup": "^1.0.0", - "mobx": "^6.6.0", - "mobx-react": "^7.5.0", - "monaco-editor": "^0.41.0", - "node-fetch": "^3.2.10", - "react": "^18.1.0", - "react-dom": "^18.1.0", - "react-json-view": "^1.21.3", - "react-markdown": "^8.0.5", - "remark": "^14.0.2", - "remark-gfm": "^3.0.1", - "sprintf-js": "^1.1.2", - "throttle-debounce": "^5.0.0", - "tsx-control-statements": "^4.1.1", - "uuid": "^9.0.0", - "winston": "^3.8.2", - "xterm": "^5.0.0" - }, - "devDependencies": { - "@babel/cli": "^7.17.10", - "@babel/core": "^7.18.2", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-decorators": "^7.18.2", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@babel/plugin-transform-react-jsx": "^7.17.12", - "@babel/plugin-transform-runtime": "^7.18.2", - "@babel/preset-env": "^7.18.2", - "@babel/preset-react": "^7.17.12", - "@babel/preset-typescript": "^7.17.12", - "@electron-forge/cli": "^6.0.0-beta.70", - "@electron-forge/maker-deb": "^6.0.0-beta.70", - "@electron-forge/maker-rpm": "^6.0.0-beta.70", - "@electron-forge/maker-squirrel": "^6.0.0-beta.70", - "@electron-forge/maker-zip": "^6.0.0-beta.70", - "@types/classnames": "^2.3.1", - "@types/electron": "^1.6.10", - "@types/node": "^18.0.3", - "@types/react": "^18.0.12", - "@types/uuid": "9.0.0", - "babel-loader": "^9.1.3", - "babel-plugin-jsx-control-statements": "^4.1.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.1", - "electron": "25.4.0", - "electron-rebuild": "^3.2.8", - "http-server": "^14.1.1", - "less": "^4.1.2", - "less-loader": "^11.0.0", - "lodash-webpack-plugin": "^0.11.6", - "mini-css-extract-plugin": "^2.6.0", - "prettier": "^2.8.8", - "style-loader": "^3.3.1", - "typescript": "^4.7.3", - "webpack": "^5.73.0", - "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.9.1", - "webpack-merge": "^5.8.0" - }, - "scripts": { - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make" - } + "name": "Prompt", + "version": "0.3.0", + "main": "dist/emain.js", + "license": "Proprietary", + "dependencies": { + "@monaco-editor/react": "^4.5.1", + "autobind-decorator": "^2.4.0", + "classnames": "^2.3.1", + "dayjs": "^1.11.3", + "dompurify": "^3.0.2", + "electron-squirrel-startup": "^1.0.0", + "mobx": "^6.6.0", + "mobx-react": "^7.5.0", + "monaco-editor": "^0.41.0", + "node-fetch": "^3.2.10", + "react": "^18.1.0", + "react-dom": "^18.1.0", + "react-json-view": "^1.21.3", + "react-markdown": "^8.0.5", + "remark": "^14.0.2", + "remark-gfm": "^3.0.1", + "sprintf-js": "^1.1.2", + "throttle-debounce": "^5.0.0", + "tsx-control-statements": "^4.1.1", + "uuid": "^9.0.0", + "winston": "^3.8.2", + "xterm": "^5.0.0" + }, + "devDependencies": { + "@babel/cli": "^7.17.10", + "@babel/core": "^7.18.2", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.18.2", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/plugin-transform-react-jsx": "^7.17.12", + "@babel/plugin-transform-runtime": "^7.18.2", + "@babel/preset-env": "^7.18.2", + "@babel/preset-react": "^7.17.12", + "@babel/preset-typescript": "^7.17.12", + "@electron-forge/cli": "^6.0.0-beta.70", + "@electron-forge/maker-deb": "^6.0.0-beta.70", + "@electron-forge/maker-rpm": "^6.0.0-beta.70", + "@electron-forge/maker-squirrel": "^6.0.0-beta.70", + "@electron-forge/maker-zip": "^6.0.0-beta.70", + "@types/classnames": "^2.3.1", + "@types/electron": "^1.6.10", + "@types/node": "^18.0.3", + "@types/react": "^18.0.12", + "@types/uuid": "9.0.0", + "babel-loader": "^9.1.3", + "babel-plugin-jsx-control-statements": "^4.1.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.7.1", + "electron": "25.4.0", + "electron-rebuild": "^3.2.8", + "http-server": "^14.1.1", + "less": "^4.1.2", + "less-loader": "^11.0.0", + "lodash-webpack-plugin": "^0.11.6", + "mini-css-extract-plugin": "^2.6.0", + "prettier": "^2.8.8", + "style-loader": "^3.3.1", + "typescript": "^4.7.3", + "webpack": "^5.73.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.9.1", + "webpack-merge": "^5.8.0", + "react-split-it": "^2.0.0" + }, + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make" + } } diff --git a/src/prompt.less b/src/prompt.less index cf350907..36581b78 100644 --- a/src/prompt.less +++ b/src/prompt.less @@ -236,6 +236,7 @@ input[type="checkbox"] { overflow: auto; display: flex; flex-direction: row; + overscroll-behavior: contain; } .dropdown { @@ -253,24 +254,42 @@ input[type="checkbox"] { padding-top: 10px; padding-bottom: 15px; } - + .monaco-editor .monaco-editor-background { background-color: rgba(255, 255, 255, 0.075) !important; } - .cmd-hints { + .monaco-editor .scrollbar { + height: 4px !important; + width: 4px !important; + } + .monaco-editor .scrollbar .slider { + background-color: rgba(255, 255, 255) !important; + } + + .cmd-hints, + .dropdown { display: inline-block; position: relative; - margin-right: 26px; + min-width: 6rem; + max-width: 6rem; + margin-right: 8px; } .hint-item { border-radius: 4px 4px 0 0; padding: 3px 9px 2px 8px; - line-height: 15px; + line-height: 19px; text-align: center; } section { transition: height 0.3s ease-in-out; } + .preview { + color: #000; + background-color: rgb(200, 200, 200); + } + .preview:hover { + background-color: white !important; + } .save-enabled { color: white; background-color: #4e9a06; @@ -309,7 +328,7 @@ input[type="checkbox"] { font-size: 12px; } -.markdown-renderer .markdown { +.renderer-container .markdown { padding: 5px; line-height: 1.5; width: fit-content; diff --git a/src/view/code.tsx b/src/view/code.tsx index 96bc63cb..518c60a2 100644 --- a/src/view/code.tsx +++ b/src/view/code.tsx @@ -1,8 +1,13 @@ import * as React from "react"; import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../types"; import Editor from "@monaco-editor/react"; +import cn from "classnames"; +import { Markdown } from "../elements"; import { GlobalModel, GlobalCommandRunner } from "../model"; +import Split from "react-split-it"; +import "./split.css"; import loader from "@monaco-editor/loader"; +import { editor } from "monaco-editor"; loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } }); function renderCmdText(text: string): any { @@ -37,6 +42,9 @@ class SourceCodeRenderer extends React.Component< isClosed: boolean; editorHeight: number; message: { status: string; text: string }; + isPreviewerAvailable: boolean; + showPreview: boolean; + editorFraction: number; } > { /** @@ -45,23 +53,32 @@ class SourceCodeRenderer extends React.Component< */ static codeCache = new Map(); + // which languages have preview options + languagesWithPreviewer = ["markdown"]; filePath; cacheKey; originalData; monacoEditor: any; // reference to mounted monaco editor. TODO need the correct type + markdownRef; + syncing; constructor(props) { super(props); this.monacoEditor = null; const editorHeight = Math.max(props.savedHeight - 25, 0); // must subtract the padding/margin to get the real editorHeight + this.markdownRef = React.createRef(); + this.syncing = false; // to avoid recursive calls between the two scroll listeners this.state = { code: null, languages: [], selectedLanguage: "", isSave: false, isClosed: false, - editorHeight: editorHeight, + editorHeight, message: null, + isPreviewerAvailable: false, + showPreview: this.props.lineState["showPreview"], + editorFraction: this.props.lineState["editorFraction"] || 0.5, }; } @@ -89,8 +106,12 @@ class SourceCodeRenderer extends React.Component< } } - setInitialLanguage = (editor) => { + saveLineState = (kvp) => { const { screenId, lineId } = this.props.context; + GlobalCommandRunner.setLineState(screenId, lineId, { ...this.props.lineState, ...kvp }, false); + }; + + setInitialLanguage = (editor) => { // set all languages const languages = monaco.languages.getLanguages().map((lang) => lang.id); this.setState({ languages }); @@ -105,19 +126,17 @@ class SourceCodeRenderer extends React.Component< .find((lang) => lang.extensions?.includes("." + extension)); if (detectedLanguageObj) { detectedLanguage = detectedLanguageObj.id; - GlobalCommandRunner.setLineState( - screenId, - lineId, - { ...this.props.lineState, lang: detectedLanguage }, - false - ); + this.saveLineState({ lang: detectedLanguage }); } } if (detectedLanguage) { const model = editor.getModel(); if (model) { monaco.editor.setModelLanguage(model, detectedLanguage); - this.setState({ selectedLanguage: detectedLanguage }); + this.setState({ + selectedLanguage: detectedLanguage, + isPreviewerAvailable: this.languagesWithPreviewer.includes(detectedLanguage), + }); } } }; @@ -135,6 +154,17 @@ class SourceCodeRenderer extends React.Component< e.preventDefault(); this.doClose(); } + if (e.code === "KeyP" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.togglePreview(); + } + }); + editor.onDidScrollChange((e) => { + if (!this.syncing && e.scrollTopChanged) { + this.syncing = true; + this.handleEditorScrollChange(e); + this.syncing = false; + } }); if (this.props.shouldFocus) { this.monacoEditor.focus(); @@ -150,23 +180,49 @@ class SourceCodeRenderer extends React.Component< } }; + handleEditorScrollChange(e) { + // Get the maximum scrollable height for the editor + const scrollableHeightEditor = this.monacoEditor.getScrollHeight() - this.monacoEditor.getLayoutInfo().height; + + // Calculate the scroll percentage + const verticalScrollPercentage = e.scrollTop / scrollableHeightEditor; + + // Apply the same percentage to the markdown div + const markdownDiv = this.markdownRef.current; + if (markdownDiv) { + const scrollableHeightMarkdown = markdownDiv.scrollHeight - markdownDiv.clientHeight; + markdownDiv.scrollTop = verticalScrollPercentage * scrollableHeightMarkdown; + } + } + + handleDivScroll() { + if (!this.syncing) { + this.syncing = true; + // Calculate the scroll percentage for the markdown div + const markdownDiv = this.markdownRef.current; + const scrollableHeightMarkdown = markdownDiv.scrollHeight - markdownDiv.clientHeight; + const verticalScrollPercentage = markdownDiv.scrollTop / scrollableHeightMarkdown; + + // Apply the same percentage to the editor + const scrollableHeightEditor = + this.monacoEditor.getScrollHeight() - this.monacoEditor.getLayoutInfo().height; + this.monacoEditor.setScrollTop(verticalScrollPercentage * scrollableHeightEditor); + + this.syncing = false; + } + } + handleLanguageChange = (event) => { - const { screenId, lineId } = this.props.context; const selectedLanguage = event.target.value; - this.setState({ selectedLanguage }); + this.setState({ + selectedLanguage, + isPreviewerAvailable: this.languagesWithPreviewer.includes(selectedLanguage), + }); if (this.monacoEditor) { const model = this.monacoEditor.getModel(); if (model) { monaco.editor.setModelLanguage(model, selectedLanguage); - GlobalCommandRunner.setLineState( - screenId, - lineId, - { - ...this.props.lineState, - lang: selectedLanguage, - }, - false - ); + this.saveLineState({ lang: selectedLanguage }); } } }; @@ -251,9 +307,112 @@ class SourceCodeRenderer extends React.Component< return !(this.props.readOnly || this.state.isClosed); } + getCodeEditor = () => ( +
+ +
+ ); + + getPreviewer = () => { + return ( +
this.handleDivScroll()} + > + +
+ ); + }; + + togglePreview = () => { + this.saveLineState({ showPreview: !this.state.showPreview }); + this.setState({ showPreview: !this.state.showPreview }); + }; + + getEditorControls = () => { + const { selectedLanguage, isSave, languages, isPreviewerAvailable, showPreview } = this.state; + let allowEditing = this.getAllowEditing(); + return ( +
+ {isPreviewerAvailable && ( +
+
+ {`${showPreview ? "hide" : "show"} preview (`} + {renderCmdText("P")} + {`)`} +
+
+ )} + + {allowEditing && ( +
+
+ {`save (`} + {renderCmdText("S")} + {`)`} +
+
+ )} + {allowEditing && ( +
+
+ {`close (`} + {renderCmdText("D")} + {`)`} +
+
+ )} +
+ ); + }; + + getMessage = () => ( +
+
+ {this.state.message.text} +
+
+ ); + + setSizes = (sizes) => { + this.setState({ editorFraction: sizes[0] }); + this.saveLineState({ editorFraction: sizes[0] }); + }; + render() { - const { opts, exitcode } = this.props; - const { selectedLanguage, code, isSave } = this.state; + const { exitcode } = this.props; + const { code, message, isPreviewerAvailable, showPreview, editorFraction } = this.state; if (code == null) return
; @@ -272,77 +431,14 @@ class SourceCodeRenderer extends React.Component<
); - let allowEditing = this.getAllowEditing(); return (
-
- -
-
- - {allowEditing && ( -
-
- {`save (`} - {renderCmdText("S")} - {`)`} -
-
- )} - {allowEditing && ( -
-
- {`close (`} - {renderCmdText("D")} - {`)`} -
-
- )} -
- {this.state.message && ( -
-
- {this.state.message.text} -
-
- )} + + {this.getCodeEditor()} + {isPreviewerAvailable && showPreview && this.getPreviewer()} + + {this.getEditorControls()} + {message && this.getMessage()}
); } diff --git a/src/view/split.css b/src/view/split.css new file mode 100644 index 00000000..783d1d16 --- /dev/null +++ b/src/view/split.css @@ -0,0 +1,50 @@ +/* example-split.css */ +.jsoneditor { + border: none !important; +} + +.split-horizontal { + display: flex; + width: 100%; + height: 100%; +} +.split-vertical { + display: flex; + flex-direction: column; + height: 100%; +} + +.gutter { + flex-shrink: 0; + flex-grow: 0; + background: rgb(100, 100, 100); + max-width: 4px; +} +.gutter-horizontal { + cursor: col-resize; +} +.gutter-vertical { + cursor: row-resize; +} +.gutter:hover { + background: rgb(155, 155, 155); +} +.gutter-dragging:hover { + background: rgb(155, 155, 155); +} + +.pane { + flex-shrink: 1; + flex-grow: 1; + position: relative; + min-width: 20rem; +} +.pane-dragging { + overflow: hidden; +} + +.scrollable { + height: 100vh; + max-height: 100vh; + overflow: scroll; +} diff --git a/yarn.lock b/yarn.lock index 8da1dca0..1953574f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6217,7 +6217,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.0.0: +prop-types@^15.0.0, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6377,6 +6377,13 @@ react-markdown@^8.0.5: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-split-it@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-split-it/-/react-split-it-2.0.0.tgz#827a79becec8eea63df0866fb6db2071c3e37307" + integrity sha512-MRb8Aez8c7243Nc2y/LoxPea2aW+GCCeSa7GgapChvAB52rFksOB7uiGmFJPfj5DfIt73e1+DGZZCx6zVkm0rA== + dependencies: + prop-types "^15.8.1" + react-textarea-autosize@^8.3.2: version "8.5.2" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.2.tgz#6421df2b5b50b9ca8c5e96fd31be688ea7fa2f9d"