PE-44 md viewer (#21)

* md does render. need cleanups

* simul scrolling works

* preview button works

* md previewer works

* use Markdown component from elements, fix null ptr with unset ref

* add scroller div back in

* scrollers should overscroll contain

---------

Co-authored-by: sawka
This commit is contained in:
anandamarsh 2023-09-17 13:58:12 -07:00 committed by GitHub
parent ca13c28a3f
commit 9a578c04d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 345 additions and 172 deletions

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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 = () => (
<div style={{ maxHeight: this.props.opts.maxSize.height }}>
<Editor
theme="hc-black"
height={this.state.editorHeight}
defaultLanguage={this.state.selectedLanguage}
defaultValue={this.state.code}
onMount={this.handleEditorDidMount}
options={{
scrollBeyondLastLine: false,
fontSize: GlobalModel.termFontSize.get(),
fontFamily: "JetBrains Mono",
readOnly: !this.getAllowEditing(),
}}
onChange={this.handleEditorChange}
/>
</div>
);
getPreviewer = () => {
return (
<div
className="scroller"
style={{ maxHeight: this.props.opts.maxSize.height }}
ref={this.markdownRef}
onScroll={() => this.handleDivScroll()}
>
<Markdown text={this.state.code} style={{width: "100%", padding: "1rem"}}/>
</div>
);
};
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 (
<div style={{ position: "absolute", bottom: "-3px", right: "8px" }}>
{isPreviewerAvailable && (
<div className="cmd-hints" style={{ minWidth: "8rem", maxWidth: "8rem" }}>
<div onClick={this.togglePreview} className={`hint-item preview`}>
{`${showPreview ? "hide" : "show"} preview (`}
{renderCmdText("P")}
{`)`}
</div>
</div>
)}
<select className="dropdown" value={selectedLanguage} onChange={this.handleLanguageChange}>
{languages.map((lang, index) => (
<option key={index} value={lang}>
{lang}
</option>
))}
</select>
{allowEditing && (
<div className="cmd-hints">
<div onClick={this.doSave} className={`hint-item ${isSave ? "save-enabled" : "save-disabled"}`}>
{`save (`}
{renderCmdText("S")}
{`)`}
</div>
</div>
)}
{allowEditing && (
<div className="cmd-hints">
<div
onClick={this.doClose}
className={`hint-item ${!isSave ? "close-enabled" : "close-disabled"}`}
>
{`close (`}
{renderCmdText("D")}
{`)`}
</div>
</div>
)}
</div>
);
};
getMessage = () => (
<div style={{ position: "absolute", bottom: "-3px", left: "14px" }}>
<div
className="message"
style={{
fontSize: GlobalModel.termFontSize.get(),
fontFamily: "JetBrains Mono",
background: `${this.state.message.status === "error" ? "red" : "#4e9a06"}`,
}}
>
{this.state.message.text}
</div>
</div>
);
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 <div className="renderer-container code-renderer" style={{ height: this.props.savedHeight }} />;
@ -272,77 +431,14 @@ class SourceCodeRenderer extends React.Component<
</div>
);
let allowEditing = this.getAllowEditing();
return (
<div className="renderer-container code-renderer">
<div className="scroller" style={{ maxHeight: opts.maxSize.height }}>
<Editor
theme="hc-black"
height={this.state.editorHeight}
defaultLanguage={selectedLanguage}
defaultValue={code}
onMount={this.handleEditorDidMount}
options={{
scrollBeyondLastLine: false,
fontSize: GlobalModel.termFontSize.get(),
fontFamily: "JetBrains Mono",
readOnly: !allowEditing,
}}
onChange={this.handleEditorChange}
/>
</div>
<div style={{ position: "absolute", bottom: "-3px", right: 0 }}>
<select
className="dropdown"
value={this.state.selectedLanguage}
onChange={this.handleLanguageChange}
style={{ minWidth: "6rem", maxWidth: "6rem", marginRight: "26px" }}
>
{this.state.languages.map((lang, index) => (
<option key={index} value={lang}>
{lang}
</option>
))}
</select>
{allowEditing && (
<div className="cmd-hints" style={{ minWidth: "6rem", maxWidth: "6rem", marginLeft: "-18px" }}>
<div
onClick={this.doSave}
className={`hint-item ${isSave ? "save-enabled" : "save-disabled"}`}
>
{`save (`}
{renderCmdText("S")}
{`)`}
</div>
</div>
)}
{allowEditing && (
<div className="cmd-hints" style={{ minWidth: "6rem", maxWidth: "6rem", marginLeft: "-18px" }}>
<div
onClick={this.doClose}
className={`hint-item ${!isSave ? "close-enabled" : "close-disabled"}`}
>
{`close (`}
{renderCmdText("D")}
{`)`}
</div>
</div>
)}
</div>
{this.state.message && (
<div style={{ position: "absolute", bottom: "-3px", left: "14px" }}>
<div
className="message"
style={{
fontSize: GlobalModel.termFontSize.get(),
fontFamily: "JetBrains Mono",
background: `${this.state.message.status === "error" ? "red" : "#4e9a06"}`,
}}
>
{this.state.message.text}
</div>
</div>
)}
<Split sizes={[editorFraction, 1 - editorFraction]} onSetSizes={this.setSizes}>
{this.getCodeEditor()}
{isPreviewerAvailable && showPreview && this.getPreviewer()}
</Split>
{this.getEditorControls()}
{message && this.getMessage()}
</div>
);
}

50
src/view/split.css Normal file
View File

@ -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;
}

View File

@ -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"