diff --git a/addon/apex-runner.js b/addon/apex-runner.js index 42b37ec2..6318a0d7 100644 --- a/addon/apex-runner.js +++ b/addon/apex-runner.js @@ -103,6 +103,12 @@ class Model { this.suggestionTop = 0; this.suggestionLeft = 0; this.disableSuggestionOverText = localStorage.getItem("disableSuggestionOverText") === "true"; + this.activeSuggestion = -1; + if (history.disableSuggestionOverText) { + this.displaySuggestion = true; + } else { + this.displaySuggestion = false; + } this.clientId = localStorage.getItem(sfHost + "_clientId") ? localStorage.getItem(sfHost + "_clientId") : ""; let scriptTemplatesRawValue = localStorage.getItem("scriptTemplates"); if (scriptTemplatesRawValue && scriptTemplatesRawValue != "[]") { @@ -296,12 +302,7 @@ class Model { selStart = selEnd - searchTerm.length; if (ctrlSpace) { - if (vm.autocompleteResults && vm.autocompleteResults.results && vm.autocompleteResults.results.length > 0) { - let ar = vm.autocompleteResults.results; - vm.editor.focus(); - vm.editor.setRangeText(ar[0].value + ar[0].suffix, selStart, selEnd, "end"); - vm.editorAutocompleteHandler(); - } + this.selectSuggestion(); return; } let contextPath; @@ -497,6 +498,49 @@ class Model { console.error(error); }); } + showSuggestion() { + this.displaySuggestion = true; + this.didUpdate(); + } + hideSuggestion() { + this.displaySuggestion = false; + this.didUpdate(); + } + nextSuggestion() { + if (this.activeSuggestion < this.autocompleteResults.results.length - 1) { + this.activeSuggestion++; + } else { + this.activeSuggestion = 0; + } + this.didUpdate(); + } + previousSuggestion() { + if (this.activeSuggestion > 0) { + this.activeSuggestion--; + } else { + this.activeSuggestion = this.autocompleteResults.results.length - 1; + } + this.didUpdate(); + } + selectSuggestion() { + if (!this.autocompleteResults || !this.autocompleteResults.results || this.autocompleteResults.results.length == 0) { + return; + } + //by default auto complete the first item + let idx = this.activeSuggestion > -1 ? this.activeSuggestion : 0; + let ar = this.autocompleteResults.results; + let selStart = this.editor.selectionStart; + let selEnd = this.editor.selectionEnd; + let searchTerm = selStart != selEnd + ? this.editor.value.substring(selStart, selEnd) + : this.editor.value.substring(0, selStart).match(/[a-zA-Z0-9_.]*$/)[0]; + selStart = selEnd - searchTerm.length; + + this.editor.focus(); + this.editor.setRangeText(ar[idx].value + ar[idx].suffix, selStart, selEnd, "end"); + this.activeSuggestion = -1; + this.editorAutocompleteHandler(); + } parseClass(source, clsName){ //todo build hierarchy of block List with startPosition, endPosition and context //for moment simple list @@ -1218,9 +1262,9 @@ 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", style: model.disableSuggestionOverText ? {} : {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), " ") + h("div", {className: "autocomplete-results" + (model.disableSuggestionOverText ? " autocomplete-results-under" : " autocomplete-results-over"), hidden: !model.displaySuggestion, style: model.disableSuggestionOverText ? {} : {top: model.suggestionTop + "px", left: model.suggestionLeft + "px"}}, + model.autocompleteResults.results.map((r, ri) => + h("div", {className: "autocomplete-result" + (ri == model.activeSuggestion ? " active" : ""), 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 043a35a3..9f00648d 100644 --- a/addon/data-export.css +++ b/addon/data-export.css @@ -120,8 +120,8 @@ textarea { border: 1px solid #DDDBDA; } -textarea[hidden] { - display: none; +textarea[hidden], .autocomplete-results[hidden] { + display: none !important; } .help-text { @@ -561,7 +561,9 @@ button.toggle .button-icon { } .autocomplete-results a:hover, -.autocomplete-results a:active { +.autocomplete-results a:active, +.autocomplete-results .active a + { background-color: #eff1f5; color: #005fb2; } diff --git a/addon/data-export.js b/addon/data-export.js index a805819f..75eb1ddf 100644 --- a/addon/data-export.js +++ b/addon/data-export.js @@ -108,6 +108,12 @@ class Model { this.suggestionLeft = 0; this.columnIndex = {fields: []}; this.disableSuggestionOverText = localStorage.getItem("disableSuggestionOverText") === "true"; + this.activeSuggestion = -1; + if (history.disableSuggestionOverText) { + this.displaySuggestion = true; + } else { + this.displaySuggestion = false; + } this.clientId = localStorage.getItem(sfHost + "_clientId") ? localStorage.getItem(sfHost + "_clientId") : ""; let queryTemplatesRawValue = localStorage.getItem("queryTemplates"); if (queryTemplatesRawValue && queryTemplatesRawValue != "[]") { @@ -352,6 +358,65 @@ class Model { }) .catch(err => console.log("error handling failed", err)); } + + showSuggestion() { + this.displaySuggestion = true; + this.didUpdate(); + } + hideSuggestion() { + this.displaySuggestion = false; + this.didUpdate(); + } + nextSuggestion() { + if (this.activeSuggestion < this.autocompleteResults.results.length - 1) { + this.activeSuggestion++; + } else { + this.activeSuggestion = 0; + } + this.didUpdate(); + } + previousSuggestion() { + if (this.activeSuggestion > 0) { + this.activeSuggestion--; + } else { + this.activeSuggestion = this.autocompleteResults.results.length - 1; + } + this.didUpdate(); + } + selectSuggestion() { + if (!this.autocompleteResults || !this.autocompleteResults.results || this.autocompleteResults.results.length == 0) { + return; + } + let selStart = this.editor.selectionStart; + let selEnd = this.editor.selectionEnd; + let searchTerm = selStart != selEnd + ? this.editor.value.substring(selStart, selEnd) + : this.editor.value.substring(0, selStart).match(/[a-zA-Z0-9_.]*$/)[0]; + selStart = selEnd - searchTerm.length; + let ar = this.autocompleteResults.results; + if (this.autocompleteResults.isField && this.activeSuggestion == -1) { + ar = ar + .filter(r => r.autocompleteType == "fieldName") + .map((r, i, l) => this.autocompleteResults.contextPath + r.value + (i != l.length - 1 ? r.suffix : "")); + if (ar.length > 0) { + this.editor.focus(); + this.editor.setRangeText(ar.join(""), selStart - this.autocompleteResults.contextPath.length, selEnd, "end"); + } + return; + } + if (this.autocompleteResults.isFieldValue && this.activeSuggestion == -1) { + this.suggestFieldValues(); + return; + } + + //by default auto complete the first item + let idx = this.activeSuggestion > -1 ? this.activeSuggestion : 0; + + this.editor.focus(); + this.editor.setRangeText(ar[idx].value + ar[idx].suffix, selStart, selEnd, "end"); + this.activeSuggestion = -1; + this.editorAutocompleteHandler(); + } setSuggestionPosition(top, left){ if (this.suggestionTop == top && this.suggestionLeft == left) { return; @@ -643,6 +708,63 @@ class Model { }; } + suggestFieldValues(){ + //previous object suggestion + let sobjectName = this.autocompleteResults.sobjectName; + let useToolingApi = this.queryTooling; + let selStart = this.editor.selectionStart; + let selEnd = this.editor.selectionEnd; + let query = this.editor.value; + let searchTerm = selStart != selEnd + ? query.substring(selStart, selEnd) + : query.substring(0, selStart).match(/[a-zA-Z0-9_]*$/)[0]; + selStart = selEnd - searchTerm.length; + let contextValueFields = this.autocompleteResults.contextValueFields; + let fieldNames = contextValueFields.map(contextValueField => contextValueField.sobjectDescribe.name + "." + contextValueField.field.name).join(", "); + if (contextValueFields.length > 1) { + this.autocompleteResults = { + sobjectName, + title: "Multiple possible fields: " + fieldNames, + results: [] + }; + return; + } + let contextValueField = contextValueFields[0]; + let queryMethod = useToolingApi ? "tooling/query" : this.queryAll ? "queryAll" : "query"; + let acQuery = "select " + contextValueField.field.name + " from " + contextValueField.sobjectDescribe.name + " where " + contextValueField.field.name + " like '%" + searchTerm.replace(/'/g, "\\'") + "%' group by " + contextValueField.field.name + " limit 100"; + this.spinFor(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(acQuery), {progressHandler: this.autocompleteProgress}) + .catch(err => { + if (err.name != "AbortError") { + this.autocompleteResults = { + sobjectName, + title: "Error: " + err.message, + results: [] + }; + } + return null; + }) + .then(data => { + this.autocompleteProgress = {}; + if (!data) { + return; + } + this.autocompleteResults = { + sobjectName, + title: fieldNames + " values suggestions:", + results: new Enumerable(data.records) + .map(record => record[contextValueField.field.name]) + .filter(value => value) + .map(value => ({value: "'" + value + "'", title: value, suffix: " ", rank: 1, autocompleteType: "fieldValue"})) + .toArray() + .sort(this.resultsSort(searchTerm)) + }; + })); + this.autocompleteResults = { + sobjectName, + title: "Loading " + fieldNames + " values...", + results: [] + }; + } autocompleteField(vm, ctrlSpace, sobjectName, isAfterWhere) { let useToolingApi = vm.queryTooling; let selStart = vm.editor.selectionStart; @@ -811,49 +933,7 @@ class Model { let fieldNames = contextValueFields.map(contextValueField => contextValueField.sobjectDescribe.name + "." + contextValueField.field.name).join(", "); if (ctrlSpace) { // Since this performs a Salesforce API call, we ask the user to opt in by pressing Ctrl+Space - if (contextValueFields.length > 1) { - vm.autocompleteResults = { - sobjectName, - title: "Multiple possible fields: " + fieldNames, - results: [] - }; - return; - } - let contextValueField = contextValueFields[0]; - let queryMethod = useToolingApi ? "tooling/query" : vm.queryAll ? "queryAll" : "query"; - let acQuery = "select " + contextValueField.field.name + " from " + contextValueField.sobjectDescribe.name + " where " + contextValueField.field.name + " like '%" + searchTerm.replace(/'/g, "\\'") + "%' group by " + contextValueField.field.name + " limit 100"; - vm.spinFor(sfConn.rest("/services/data/v" + apiVersion + "/" + queryMethod + "/?q=" + encodeURIComponent(acQuery), {progressHandler: vm.autocompleteProgress}) - .catch(err => { - if (err.name != "AbortError") { - vm.autocompleteResults = { - sobjectName, - title: "Error: " + err.message, - results: [] - }; - } - return null; - }) - .then(data => { - vm.autocompleteProgress = {}; - if (!data) { - return; - } - vm.autocompleteResults = { - sobjectName, - title: fieldNames + " values suggestions:", - results: new Enumerable(data.records) - .map(record => record[contextValueField.field.name]) - .filter(value => value) - .map(value => ({value: "'" + value + "'", title: value, suffix: " ", rank: 1, autocompleteType: "fieldValue"})) - .toArray() - .sort(this.resultsSort(searchTerm)) - }; - })); - vm.autocompleteResults = { - sobjectName, - title: "Loading " + fieldNames + " values...", - results: [] - }; + vm.suggestFieldValues(); return; } let ar = new Enumerable(contextValueFields).flatMap(function* ({field}) { @@ -934,6 +1014,8 @@ class Model { .sort(this.resultsSort(searchTerm)); vm.autocompleteResults = { sobjectName, + isFieldValue, + contextValueFields, title: fieldNames + (ar.length == 0 ? " values (Press Ctrl+Space to load suggestions):" : " values:"), results: ar }; @@ -955,6 +1037,8 @@ class Model { } vm.autocompleteResults = { sobjectName, + isField: true, + contextPath, title: contextSobjectDescribes.map(sobjectDescribe => sobjectDescribe.name).toArray().join(", ") + " fields suggestions:", results: contextSobjectDescribes .flatMap(sobjectDescribe => sobjectDescribe.fields) @@ -1036,12 +1120,10 @@ class Model { .sort(this.resultsSort(searchTerm)); if (ctx.ctrlSpace) { if (ar.length > 0) { - let rel = ar.shift(); + let rel = ar[0]; ctx.sobjectName = rel.dataType; - ctx.vm.editor.focus(); - ctx.vm.editor.setRangeText(rel.value, selStart, selEnd, "end"); } - ctx.vm.editorAutocompleteHandler(); + this.selectSuggestion(); return; } if (suggestRelation) { @@ -1986,9 +2068,9 @@ class App extends React.Component { h("div", {className: "button-toggle-icon"})) : "" ), ), - h("div", {className: "autocomplete-results " + (model.disableSuggestionOverText ? "autocomplete-results-under" : "autocomplete-results-over"), style: model.disableSuggestionOverText ? {} : {top: model.suggestionTop + "px", left: model.suggestionLeft + "px"}}, - model.autocompleteResults.results.map(r => - h("div", {className: "autocomplete-result", 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.value), " ") + h("div", {className: "autocomplete-results" + (model.disableSuggestionOverText ? " autocomplete-results-under" : " autocomplete-results-over"), hidden: !model.displaySuggestion, style: model.disableSuggestionOverText ? {} : {top: model.suggestionTop + "px", left: model.suggestionLeft + "px"}}, + model.autocompleteResults.results.map((r, ri) => + h("div", {className: "autocomplete-result" + (ri == model.activeSuggestion ? " active" : ""), 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.value), " ") ) ), ), diff --git a/addon/data-load.js b/addon/data-load.js index d9d990b4..cf1aeac8 100644 --- a/addon/data-load.js +++ b/addon/data-load.js @@ -776,6 +776,7 @@ 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.handleMouseUp = this.handleMouseUp.bind(this); this.numberOfLines = 1; this.state = {scrolltop: 0, lineHeight: 0}; } @@ -862,99 +863,165 @@ export class Editor extends React.Component { // We do not want to perform Salesforce API calls for autocomplete on every keystroke, so we only perform these when the user pressed Ctrl+Space // Chrome on Linux does not fire keypress when the Ctrl key is down, so we listen for keydown. Might be https://code.google.com/p/chromium/issues/detail?id=13891#c50 let {model} = this.props; - if (e.ctrlKey && e.key == " ") { - e.preventDefault(); - model.editorAutocompleteHandler({ctrlSpace: true}); - model.didUpdate(); - return; - } const {value, selectionStart, selectionEnd} = e.currentTarget; const tabChar = " ";//default is 2 spaces - if (e.key === "Tab") { - //TODO option to select 2 spaces, 4 spaces or tab \t - let selectedText = value.substring(selectionStart, selectionEnd); - let mod = 0; - if (e.shiftKey) { - e.preventDefault(); - //unindent - let lineStart = value.substring(0, selectionStart + 1).lastIndexOf("\n") + 1; - if (value.substring(lineStart).startsWith(tabChar)) { - model.editor.setRangeText("", lineStart, lineStart + 2, "preserve"); - mod -= tabChar.length; + if (!model.displaySuggestion) { + model.displaySuggestion = true; + } + switch (e.key) { + case " ": + if (e.ctrlKey) { + e.preventDefault(); + if ((model.disableSuggestionOverText)) { + model.editorAutocompleteHandler({ctrlSpace: true}); + model.didUpdate(); + } if (model.displaySuggestion) { + model.selectSuggestion(); + } else { + model.showSuggestion(); + } + } + break; + case "ArrowRight": + case "ArrowLeft": + //naviguation reset active suggestion + if (model.displaySuggestion && !model.disableSuggestionOverText && model.activeSuggestion != -1) { + model.activeSuggestion = -1; + } + break; + case "ArrowDown": + if (model.displaySuggestion && !model.disableSuggestionOverText) { + e.preventDefault(); + model.nextSuggestion(); + } + break; + case "ArrowUp": + if (model.displaySuggestion && !model.disableSuggestionOverText) { + e.preventDefault(); + model.previousSuggestion(); + } + break; + case "Enter": + if (model.displaySuggestion && !model.disableSuggestionOverText && model.activeSuggestion != -1) { + e.preventDefault(); + model.selectSuggestion(); } - let breakLineRegEx = /\n/gmi; - let breakLineMatch; - while ((breakLineMatch = breakLineRegEx.exec(selectedText)) !== null) { - lineStart = selectionStart + breakLineMatch.index + breakLineMatch[0].length; + break; + case "Escape": + if (!model.disableSuggestionOverText) { + e.preventDefault(); + model.activeSuggestion = -1; + model.hideSuggestion(); + } + break; + case "Tab": { + //TODO option to select 2 spaces, 4 spaces or tab \t + let selectedText = value.substring(selectionStart, selectionEnd); + let mod = 0; + e.preventDefault(); + if (e.shiftKey) { + //unindent + let lineStart = value.substring(0, selectionStart + 1).lastIndexOf("\n") + 1; if (value.substring(lineStart).startsWith(tabChar)) { - model.editor.setRangeText("", lineStart + mod, lineStart + 2 + mod, "preserve"); + model.editor.setRangeText("", lineStart, lineStart + 2, "preserve"); mod -= tabChar.length; } - } - } else if (selectionStart !== selectionEnd) { - e.preventDefault(); - //indent - let lineStart = value.substring(0, selectionStart + 1).lastIndexOf("\n") + 1; - model.editor.setRangeText(tabChar, lineStart, lineStart, "preserve"); - mod += tabChar.length; - let breakLineRegEx = /\n/gmi; - let breakLineMatch; - while ((breakLineMatch = breakLineRegEx.exec(selectedText)) !== null) { - lineStart = selectionStart + breakLineMatch.index + breakLineMatch[0].length; - model.editor.setRangeText(tabChar, lineStart + mod, lineStart + mod, "preserve"); + let breakLineRegEx = /\n/gmi; + let breakLineMatch; + while ((breakLineMatch = breakLineRegEx.exec(selectedText)) !== null) { + lineStart = selectionStart + breakLineMatch.index + breakLineMatch[0].length; + if (value.substring(lineStart).startsWith(tabChar)) { + model.editor.setRangeText("", lineStart + mod, lineStart + 2 + mod, "preserve"); + mod -= tabChar.length; + } + } + } else if (selectionStart !== selectionEnd) { + //indent + let lineStart = value.substring(0, selectionStart + 1).lastIndexOf("\n") + 1; + model.editor.setRangeText(tabChar, lineStart, lineStart, "preserve"); mod += tabChar.length; + let breakLineRegEx = /\n/gmi; + let breakLineMatch; + while ((breakLineMatch = breakLineRegEx.exec(selectedText)) !== null) { + lineStart = selectionStart + breakLineMatch.index + breakLineMatch[0].length; + model.editor.setRangeText(tabChar, lineStart + mod, lineStart + mod, "preserve"); + mod += tabChar.length; + } + } else if (model.displaySuggestion && !model.disableSuggestionOverText && model.activeSuggestion) { + model.selectSuggestion(); + } else { + model.editor.setRangeText(tabChar, selectionStart, selectionStart, "preserve"); } - } else { - e.preventDefault(); - model.editor.setRangeText(tabChar, selectionStart, selectionStart, "preserve"); + break; } - } else if (e.key == "[" || e.key == "(" || e.key == "{" || e.key == "'" || e.key == "\"") { - e.preventDefault(); - const openToCloseChar = new Map([ - ["[", "]"], - ["(", ")"], - ["{", "}"], - ["'", "'"], - ["\"", "\""], - ]); - const closeChar = openToCloseChar.get(e.key); - // if quote (or any other start char) before and quote (or any other corresponding end char) right after it do not add quote (or the corresponding end char) but just move cursor after the it - if ((e.key === "'" || e.key === "\"") && selectionStart > 0 && selectionEnd < model.editor.value.length && selectionStart === selectionEnd && model.editor.value.substring(selectionStart - 1, selectionStart) == e.key && model.editor.value.substring(selectionEnd, selectionEnd + 1) == closeChar) { - model.editor.setRangeText("", selectionEnd + 1, selectionEnd + 1, "end"); - } else { - model.editor.setRangeText(e.key, selectionStart, selectionStart, "end"); - // add of close quote after open quote happend only if nxt character is space, break line, close parenthesis, close bracket... maybe just if next charactere is not a-z or 0-9 - // look for char at + 1 because start char is already inserted - 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) { - model.editor.setRangeText(closeChar, selectionEnd + 1, selectionEnd + 1, "preserve"); + case "[": + case "(": + case "{": + case "'": + case "\"": { + e.preventDefault(); + const openToCloseChar = new Map([ + ["[", "]"], + ["(", ")"], + ["{", "}"], + ["'", "'"], + ["\"", "\""], + ]); + const closeChar = openToCloseChar.get(e.key); + // if quote (or any other start char) before and quote (or any other corresponding end char) right after it do not add quote (or the corresponding end char) but just move cursor after the it + if ((e.key === "'" || e.key === "\"") && selectionStart > 0 && selectionEnd < model.editor.value.length && selectionStart === selectionEnd && model.editor.value.substring(selectionStart - 1, selectionStart) == e.key && model.editor.value.substring(selectionEnd, selectionEnd + 1) == closeChar) { + model.editor.setRangeText("", selectionEnd + 1, selectionEnd + 1, "end"); + } else { + model.editor.setRangeText(e.key, selectionStart, selectionStart, "end"); + // add of close quote after open quote happend only if nxt character is space, break line, close parenthesis, close bracket... maybe just if next charactere is not a-z or 0-9 + // look for char at + 1 because start char is already inserted + 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) { + model.editor.setRangeText(closeChar, selectionEnd + 1, selectionEnd + 1, "preserve"); + } } + break; } - } else if (e.key == "]" || e.key == ")" || e.key == "}") { - // if quote (or any other start char) before and quote (or any other corresponding end char) right after it do not add quote (or the corresponding end char) but just move cursor after the it - const closeToOpenChar = new Map([ - ["]", "["], - [")", "("], - ["}", "{"], - ]); - const openChar = closeToOpenChar.get(e.key); - // if start char before and corresponding end char right after it do not add the corresponding end char but just move cursor after the it - if (selectionStart === selectionEnd && model.editor.value.substring(selectionStart - 1, selectionStart) == openChar && model.editor.value.substring(selectionEnd, selectionEnd + 1) == e.key) { - e.preventDefault(); - model.editor.setRangeText("", selectionEnd + 1, selectionEnd + 1, "end"); + case "]": + case ")": + case "}": { + // if quote (or any other start char) before and quote (or any other corresponding end char) right after it do not add quote (or the corresponding end char) but just move cursor after the it + const closeToOpenChar = new Map([ + ["]", "["], + [")", "("], + ["}", "{"], + ]); + const openChar = closeToOpenChar.get(e.key); + // if start char before and corresponding end char right after it do not add the corresponding end char but just move cursor after the it + if (selectionStart === selectionEnd && model.editor.value.substring(selectionStart - 1, selectionStart) == openChar && model.editor.value.substring(selectionEnd, selectionEnd + 1) == e.key) { + e.preventDefault(); + model.editor.setRangeText("", selectionEnd + 1, selectionEnd + 1, "end"); + } + break; } - } else if (e.key === "Backspace") { - const textBeforeCaret = value.substring(0, selectionStart); - let indentRgEx = new RegExp("\n(" + tabChar + ")+$", "g"); - if (selectionStart == selectionEnd && textBeforeCaret.match(indentRgEx)) { - e.preventDefault(); - model.editor.setRangeText("", selectionStart, selectionStart - tabChar.length, "preserve"); + case "Backspace": { + const textBeforeCaret = value.substring(0, selectionStart); + let indentRgEx = new RegExp("\n(" + tabChar + ")+$", "g"); + if (selectionStart == selectionEnd && textBeforeCaret.match(indentRgEx)) { + e.preventDefault(); + model.editor.setRangeText("", selectionStart, selectionStart - tabChar.length, "preserve"); + } + //TODO if previous input without other keydown (even move)is openChar then delete open and closeChar + break; } - //TODO if previous input without other keydown (even move)is openChar then delete open and closeChar + } + } + handleMouseUp(e) { + let {model} = this.props; + if (model.disableSuggestionOverText) { + this.editorAutocompleteEvent(e); + } else if (!model.displaySuggestion) { + model.activeSuggestion = -1; + model.showSuggestion(); } } componentWillUnmount() { @@ -964,10 +1031,16 @@ export class Editor extends React.Component { componentDidUpdate() { let {model} = this.props; + if (model.disableSuggestionOverText) { + return; + } let caretEle = model.editorMirror.getElementsByClassName("editor_caret")[0]; - - const rect = caretEle.getBoundingClientRect(); - model.setSuggestionPosition(rect.top + rect.height, rect.left); + if (caretEle) { + const rect = caretEle.getBoundingClientRect(); + model.setSuggestionPosition(rect.top + rect.height, rect.left); + } else { + model.displaySuggestion = false; + } } processText(src) { let {keywordColor, keywordCaseSensitive, model} = this.props; @@ -1018,8 +1091,12 @@ export class Editor extends React.Component { sentence = keywordMatch[0]; bracketIndex++; } else if (keywordMatch[0] == ")" || keywordMatch[0] == "]" || keywordMatch[0] == "}") { - bracketIndex--; - color = colorBrackets[bracketIndex % 3]; + if (bracketIndex == 0) { + color = "red";//error + } else { + bracketIndex--; + color = colorBrackets[bracketIndex % 3]; + } sentence = keywordMatch[0]; } else { color = keywordColor.get(keywordMatch[1].toLocaleLowerCase()); @@ -1076,7 +1153,7 @@ export class Editor extends React.Component { ), 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}) + 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.handleMouseUp, onSelect: this.editorAutocompleteEvent, onInput: this.editorAutocompleteEvent, onKeyDown: this.handlekeyDown}) ) ) );