From ed3a2ec91dff4c3dac6ab1a482cd95b095934670 Mon Sep 17 00:00:00 2001 From: olivier dufour Date: Thu, 9 May 2024 20:58:19 +0200 Subject: [PATCH] Editorapex (#87) editor: bracket, line number and use on apex runner --- addon/apex-runner.css | 19 ----------------- addon/apex-runner.js | 19 +++-------------- addon/data-export.css | 30 ++++++++++++++++++++++++++- addon/data-load.js | 47 +++++++++++++++++++++++++++++++++---------- 4 files changed, 68 insertions(+), 47 deletions(-) diff --git a/addon/apex-runner.css b/addon/apex-runner.css index 3509659b..19511334 100644 --- a/addon/apex-runner.css +++ b/addon/apex-runner.css @@ -1,22 +1,3 @@ - .line-numbers { - text-align: right; - padding: 8px 10px; - } - - .line-numbers span { - counter-increment: linenumber; - } - - .line-numbers span::before { - content: counter(linenumber); - display: block; - color: #506882; - } - .editor { - display: flex; - } - - .class .autocomplete-icon { -webkit-mask-image: url('chrome-extension://__MSG_@@extension_id__/images/relate.svg'); background-color: #04844B; diff --git a/addon/apex-runner.js b/addon/apex-runner.js index 3a058af7..1057d073 100644 --- a/addon/apex-runner.js +++ b/addon/apex-runner.js @@ -79,7 +79,6 @@ class Model { this.sfLink = "https://" + sfHost; this.spinnerCount = 0; - this.numberOfLines = 1; this.showHelp = false; this.userInfo = "..."; this.userId = null; @@ -141,7 +140,6 @@ class Model { setEditor(editor) { this.editor = editor; editor.value = this.initialScript; - this.numberOfLines = this.initialScript.split("\n").length; this.initialScript = null; } toggleHelp() { @@ -530,12 +528,7 @@ class Model { let selStart = vm.editor.selectionStart; let selEnd = vm.editor.selectionEnd; let ctrlSpace = e.ctrlSpace; - let numberOfLines = script.split("\n").length; this.parseAnonApex(script); - if (vm.numberOfLines != numberOfLines) { - vm.numberOfLines = numberOfLines; - vm.didUpdate(); - } //TODO place suggestion over the text area with miroring text with span //advantage is that we can provide color highlight thanks to that. /* @@ -1145,8 +1138,7 @@ class App extends React.Component { render() { let {model} = this.props; - let keywordColor = new Map([["{", "violet"], ["}", "violet"], ["[", "violet"], ["]", "violet"], ["(", "violet"], ["do", "violet"], - [")", "violet"], ["public", "blue"], ["private", "blue"], ["global", "blue"], ["class", "blue"], ["static", "blue"], + let keywordColor = new Map([["do", "violet"], ["public", "blue"], ["private", "blue"], ["global", "blue"], ["class", "blue"], ["static", "blue"], ["interface", "blue"], ["extends", "blue"], ["while", "violet"], ["for", "violet"], ["try", "violet"], ["catch", "violet"], ["finally", "violet"], ["extends", "violet"], ["throw", "violet"], ["new", "violet"], ["if", "violet"], ["else", "violet"]]); return h("div", {onClick: this.onClick}, @@ -1204,12 +1196,7 @@ class App extends React.Component { ), ), ), - h("div", {className: "editor"}, - h("div", {className: "line-numbers"}, - Array(model.numberOfLines).fill(null).map((e, i) => h("span", {key: "LineNumber" + i})) - ), - h(Editor, {model, keywordColor, keywordCaseSensitive: true}), - ), + h(Editor, {model, keywordColor, keywordCaseSensitive: true}), h("div", {className: "autocomplete-box"}, h("div", {className: "autocomplete-header"}, h("span", {}, model.autocompleteResults.title), @@ -1218,7 +1205,7 @@ class App extends React.Component { h("button", {tabIndex: 2, onClick: this.onCopyScript, title: "Copy script url", className: "copy-id"}, "Export Script") ), ), - h("div", {className: "autocomplete-results"}, + h("div", {className: "autocomplete-results", style: {top: model.suggestionTop + "px", left: model.suggestionLeft + "px"}}, model.autocompleteResults.results.map(r => h("div", {className: "autocomplete-result", key: r.key ? r.key : r.value}, h("a", {tabIndex: 0, title: r.title, onClick: e => { e.preventDefault(); model.autocompleteClick(r); model.didUpdate(); }, href: "#", className: r.autocompleteType + " " + r.dataType}, h("div", {className: "autocomplete-icon"}), r.title), " ") ) diff --git a/addon/data-export.css b/addon/data-export.css index 6ebe8b70..642ad036 100644 --- a/addon/data-export.css +++ b/addon/data-export.css @@ -696,6 +696,12 @@ button.toggle .button-icon { overflow: auto; height: 200px; } +.editor-container { + display: flex; +} +.editor-wrapper { + flex-grow: 1; +} .editor_container { position: relative; text-align: 'left'; @@ -720,7 +726,6 @@ button.toggle .button-icon { border: 1px solid #DDDBDA; vertical-align: top; top: 0; - left: 0; height: 100%; width: 100%; resize: vertical; @@ -760,4 +765,27 @@ button.toggle .button-icon { 50% { background-color: rgb(15 23 42); } +} +.line-numbers-wrapper { + position: relative; + width: 44px; + overflow: hidden; +} + +.line-numbers { + text-align: right; + padding: 8px 10px; + position: absolute; + width: 44px; +} + +.line-numbers span { + counter-increment: linenumber; +} + +.line-numbers span::before { + content: counter(linenumber); + display: block; + color: #506882; + font-size: .8125rem; } \ No newline at end of file diff --git a/addon/data-load.js b/addon/data-load.js index ed4cd9f1..d9d990b4 100644 --- a/addon/data-load.js +++ b/addon/data-load.js @@ -776,6 +776,8 @@ export class Editor extends React.Component { this.editorAutocompleteEvent = this.editorAutocompleteEvent.bind(this); this.onScroll = this.onScroll.bind(this); this.processText = this.processText.bind(this); + this.numberOfLines = 1; + this.state = {scrolltop: 0, lineHeight: 0}; } componentDidMount() { @@ -803,6 +805,7 @@ export class Editor extends React.Component { ].forEach((property) => { editorMirror.style[property] = textareaStyles[property]; }); + this.setState({lineHeight: textareaStyles.lineHeight}); editorMirror.style.borderColor = "transparent"; //const parseValue = (v) => v.endsWith("px") ? parseInt(v.slice(0, -2), 10) : 0; @@ -816,6 +819,7 @@ export class Editor extends React.Component { const ro = new ResizeObserver(() => { editorInput.getBoundingClientRect().height; editorMirror.style.height = `${editorInput.getBoundingClientRect().height}px`; + editorMirror.style.width = `${editorInput.getBoundingClientRect().width}px`; recalculateHeight(); }); ro.observe(editorInput); @@ -847,6 +851,7 @@ export class Editor extends React.Component { if (model.editorMirror && model.editor) { model.editorMirror.scrollTop = model.editor.scrollTop; } + this.setState({scrolltop: model.editor.scrollTop}); } editorAutocompleteEvent(e) { let {model} = this.props; @@ -923,9 +928,9 @@ export class Editor extends React.Component { if (selectionStart != selectionEnd) { model.editor.setRangeText(closeChar, selectionEnd + 1, selectionEnd + 1, "preserve"); } else if ( - (e.key !== "'" && e.key !== "\"") || - (selectionEnd + 1 < model.editor.value.length && /[\w|\s]/.test(model.editor.value.substring(selectionEnd + 1, selectionEnd + 2))) || - selectionEnd + 1 === model.editor.value.length) { + (e.key !== "'" && e.key !== "\"") + || (selectionEnd + 1 < model.editor.value.length && /[\w|\s]/.test(model.editor.value.substring(selectionEnd + 1, selectionEnd + 2))) + || selectionEnd + 1 === model.editor.value.length) { model.editor.setRangeText(closeChar, selectionEnd + 1, selectionEnd + 1, "preserve"); } } @@ -963,20 +968,23 @@ export class Editor extends React.Component { const rect = caretEle.getBoundingClientRect(); model.setSuggestionPosition(rect.top + rect.height, rect.left); - console.log("top: " + rect.top); } processText(src) { let {keywordColor, keywordCaseSensitive, model} = this.props; let remaining = src; let keywordMatch; let highlighted = []; + let numberOfLines = src ? src.split("\n").length : 1; let selStart = model.editor ? model.editor.selectionStart : 0; //let endIndex; let keywords = []; for (let keyword of keywordColor.keys()) { keywords.push(keyword); } - let keywordRegEx = new RegExp("\\b(" + keywords.join("|") + ")\\b|(\\/\\/|\\/\\*|')", "g" + (keywordCaseSensitive ? "" : "i")); + + let keywordRegEx = new RegExp("\\b(" + keywords.join("|") + ")\\b|(\\/\\/|\\/\\*|'|{|\\[|\\(|}|\\]|\\))", "g" + (keywordCaseSensitive ? "" : "i")); + const colorBrackets = ["gold", "purple", "deepskyblue"]; + let bracketIndex = 0; //yellow for function while ((keywordMatch = keywordRegEx.exec(remaining)) !== null) { let color = "blue"; @@ -1005,6 +1013,14 @@ export class Editor extends React.Component { } else { sentence = remaining.substring(keywordMatch.index); } + } else if (keywordMatch[0] == "(" || keywordMatch[0] == "[" || keywordMatch[0] == "{") { + color = colorBrackets[bracketIndex % 3]; + sentence = keywordMatch[0]; + bracketIndex++; + } else if (keywordMatch[0] == ")" || keywordMatch[0] == "]" || keywordMatch[0] == "}") { + bracketIndex--; + color = colorBrackets[bracketIndex % 3]; + sentence = keywordMatch[0]; } else { color = keywordColor.get(keywordMatch[1].toLocaleLowerCase()); } @@ -1036,7 +1052,7 @@ export class Editor extends React.Component { } remaining = remaining.substring(keywordMatch.index + sentence.length); selStart -= keywordMatch.index + sentence.length; - keywordRegEx = new RegExp("\\b(" + keywords.join("|") + ")\\b|(\\/\\/|\\/\\*|')", "g" + (keywordCaseSensitive ? "" : "i")); + keywordRegEx = new RegExp("\\b(" + keywords.join("|") + ")\\b|(\\/\\/|\\/\\*|'|{|\\[|\\(|}|\\]|\\))", "g" + (keywordCaseSensitive ? "" : "i")); } if (selStart > 0) { highlighted.push({value: remaining.substring(0, selStart), attributes: {style: {color: "black"}, key: "hl" + highlighted.length}}); @@ -1046,14 +1062,23 @@ export class Editor extends React.Component { if (remaining) { highlighted.push({value: remaining, attributes: {style: {color: "black"}, key: "hl" + highlighted.length}}); } - return highlighted; + return {highlighted, numberOfLines}; } render() { let {model} = this.props; - let highlighted = this.processText(model.editor ? model.editor.value : ""); - return h("div", {className: "editor_container"}, - h("div", {ref: "editorMirror", className: "editor_container_mirror"}, highlighted.map(s => h("span", s.attributes, s.value))), - h("textarea", {id: "editor", autoComplete: "off", autoCorrect: "off", spellCheck: "false", autoCapitalize: "off", className: "editor_textarea", ref: "editor", onScroll: this.onScroll, onKeyUp: this.editorAutocompleteEvent, onMouseUp: this.editorAutocompleteEvent, onSelect: this.editorAutocompleteEvent, onInput: this.editorAutocompleteEvent, onKeyDown: this.handlekeyDown, style: {maxHeight: (model.winInnerHeight - 200) + "px"}}) + let {highlighted, numberOfLines} = this.processText(model.editor ? model.editor.value : ""); + return h("div", {className: "editor_container", style: {maxHeight: (model.winInnerHeight - 200) + "px"}}, + h("div", {className: "editor-container"}, + h("div", {className: "line-numbers-wrapper", style: {lineHeight: this.state.lineHeight}}, + h("div", {className: "line-numbers", style: {top: -this.state.scrolltop + "px"}}, + Array(numberOfLines).fill(null).map((e, i) => h("span", {key: "LineNumber" + i})) + ) + ), + h("div", {className: "editor-wrapper"}, + h("div", {ref: "editorMirror", className: "editor_container_mirror"}, highlighted.map(s => h("span", s.attributes, s.value))), + h("textarea", {id: "editor", autoComplete: "off", autoCorrect: "off", spellCheck: "false", autoCapitalize: "off", className: "editor_textarea", ref: "editor", onScroll: this.onScroll, onKeyUp: this.editorAutocompleteEvent, onMouseUp: this.editorAutocompleteEvent, onSelect: this.editorAutocompleteEvent, onInput: this.editorAutocompleteEvent, onKeyDown: this.handlekeyDown}) + ) + ) ); } }