From 11c3227cb344733c29453efaef69f1861a935a2b Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Fri, 27 Jun 2025 17:46:54 +0100 Subject: [PATCH 1/4] Fix infinite recursion (bug unfixed - see #131) --- plugins/auto-close-brackets.js | 12 ++++++++++-- plugins/indent.js | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/plugins/auto-close-brackets.js b/plugins/auto-close-brackets.js index a30a813..77ce6de 100644 --- a/plugins/auto-close-brackets.js +++ b/plugins/auto-close-brackets.js @@ -19,13 +19,15 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { /* Add keystroke events */ afterElementsAdded(codeInput) { - codeInput.textareaElement.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) }); + codeInput.pluginData.autoCloseBrackets = { automatedKeypresses: false}; + codeInput.textareaElement.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event); }); codeInput.textareaElement.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); }); } /* Deal with the automatic creation of closing bracket when opening brackets are typed, and the ability to "retype" a closing bracket where one has already been placed. */ checkBrackets(codeInput, event) { + if(codeInput.pluginData.autoCloseBrackets.automatedKeypresses) return; if(event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) { // Check if a closing bracket is typed for(let openingBracket in this.bracketPairs) { @@ -41,7 +43,12 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { // Opening bracket typed; Create bracket pair let closingBracket = this.bracketPairs[event.data]; // Insert the closing bracket + // automatedKeypresses property to prevent keypresses being captured + // by this plugin during automated input as some browsers + // (e.g. GNOME Web) do. + codeInput.pluginData.autoCloseBrackets.automatedKeypresses = true; document.execCommand("insertText", false, closingBracket); + codeInput.pluginData.autoCloseBrackets.automatedKeypresses = false; // Move caret before the inserted closing bracket codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1; } @@ -49,6 +56,7 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { /* Deal with cases where a backspace deleting an opening bracket deletes the closing bracket straight after it as well */ checkBackspace(codeInput, event) { + if(codeInput.pluginData.autoCloseBrackets.automatedKeypresses) return; if(event.key == "Backspace" && codeInput.textareaElement.selectionStart == codeInput.textareaElement.selectionEnd) { let closingBracket = this.bracketPairs[codeInput.textareaElement.value[codeInput.textareaElement.selectionStart-1]]; if(closingBracket != undefined && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == closingBracket) { @@ -58,4 +66,4 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { } } } -} \ No newline at end of file +} diff --git a/plugins/indent.js b/plugins/indent.js index 7ddc2fe..5031e9a 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -81,11 +81,12 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { let indentationWidthPx = testIndentationWidthSpan.offsetWidth; codeInput.removeChild(testIndentationWidthPre); - codeInput.pluginData.indent = {indentationWidthPx: indentationWidthPx}; + codeInput.pluginData.indent = {automatedKeypresses: false, indentationWidthPx: indentationWidthPx}; } /* Deal with the Tab key causing indentation, and Tab+Selection indenting / Shift+Tab+Selection unindenting lines, and the mechanism through which Tab can be used to switch focus instead (accessibility). */ checkTab(codeInput, event) { + if(codeInput.pluginData.indent.automatedKeypresses) return; if(!this.tabIndentationEnabled) return; if(this.escTabToChangeFocus) { // Accessibility - allow Tab for keyboard navigation when Esc pressed right before it. @@ -116,7 +117,12 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { if(!event.shiftKey && inputElement.selectionStart == inputElement.selectionEnd) { // Just place a tab/spaces here. + // automatedKeypresses property to prevent keypresses being captured + // by this plugin during automated input as some browsers + // (e.g. GNOME Web) do. + codeInput.pluginData.indent.automatedKeypresses = true; document.execCommand("insertText", false, this.indentation); + codeInput.pluginData.indent.automatedKeypresses = false; } else { let lines = inputElement.value.split("\n"); @@ -147,7 +153,12 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { // Add tab at start inputElement.selectionStart = letterI; inputElement.selectionEnd = letterI; + // automatedKeypresses property to prevent keypresses being captured + // by this plugin during automated input as some browsers + // (e.g. GNOME Web) do. + codeInput.pluginData.indent.f = true; document.execCommand("insertText", false, this.indentation); + codeInput.pluginData.indent.automatedKeypresses = false; // Change selection if(selectionStartI > letterI) { // Indented outside selection @@ -191,6 +202,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { /* Deal with new lines retaining indentation */ checkEnter(codeInput, event) { + if(codeInput.pluginData.indent.automatedKeypresses) return; if(event.key != "Enter") { return; } @@ -270,11 +282,16 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { // save the current cursor position let selectionStartI = inputElement.selectionStart; + // automatedKeypresses property to prevent keypresses being captured + // by this plugin during automated input as some browsers + // (e.g. GNOME Web) do. + codeInput.pluginData.indent.automatedKeypresses = true; if(bracketThreeLinesTriggered) { document.execCommand("insertText", false, "\n" + furtherIndentation); // Write indented line numberIndents += 1; // Reflects the new indent } document.execCommand("insertText", false, "\n" + newLine); // Write new line, including auto-indentation + codeInput.pluginData.indent.automatedKeypresses = false; // move cursor to new position inputElement.selectionStart = selectionStartI + numberIndents*this.indentationNumChars + 1; // count the indent level and the newline character @@ -294,6 +311,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { /* Deal with one 'tab' of spaces-based-indentation being deleted by each backspace, rather than one space */ checkBackspace(codeInput, event) { + if(codeInput.pluginData.indent.automatedKeypresses) return; if(event.key != "Backspace" || this.indentationNumChars == 1) { return; // Normal backspace when indentation of 1 } From caa95691bb7b31f5cbee4b0bfa3c1f0cdc1df908 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Tue, 15 Jul 2025 14:47:52 +0100 Subject: [PATCH 2/4] Fix AutoCloseBrackets and Indent across WebKit and others (Fixes #131, Fixes #138) --- plugins/auto-close-brackets.js | 18 +++++++++++++----- plugins/indent.js | 7 ++++++- tests/tester.js | 14 ++++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/plugins/auto-close-brackets.js b/plugins/auto-close-brackets.js index 77ce6de..d8f7473 100644 --- a/plugins/auto-close-brackets.js +++ b/plugins/auto-close-brackets.js @@ -21,12 +21,14 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { afterElementsAdded(codeInput) { codeInput.pluginData.autoCloseBrackets = { automatedKeypresses: false}; codeInput.textareaElement.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event); }); - codeInput.textareaElement.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); }); + codeInput.textareaElement.addEventListener('beforeinput', (event) => { this.checkClosingBracket(codeInput, event); }); + codeInput.textareaElement.addEventListener('input', (event) => { this.checkOpeningBracket(codeInput, event); }); } - /* Deal with the automatic creation of closing bracket when opening brackets are typed, and the ability to "retype" a closing - bracket where one has already been placed. */ - checkBrackets(codeInput, event) { + /* Deal with the ability to "retype" a closing bracket where one has already + been placed. Runs before input so newly typing a closing bracket can be + prevented.*/ + checkClosingBracket(codeInput, event) { if(codeInput.pluginData.autoCloseBrackets.automatedKeypresses) return; if(event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) { // Check if a closing bracket is typed @@ -39,7 +41,13 @@ codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { break; } } - } else if(event.data in this.bracketPairs) { + } + } + + /* Deal with the automatic creation of closing bracket when opening brackets are typed. Runs after input for consistency between browsers. */ + checkOpeningBracket(codeInput, event) { + if(codeInput.pluginData.autoCloseBrackets.automatedKeypresses) return; + if(event.data in this.bracketPairs) { // Opening bracket typed; Create bracket pair let closingBracket = this.bracketPairs[event.data]; // Insert the closing bracket diff --git a/plugins/indent.js b/plugins/indent.js index 5031e9a..2e67ac6 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -275,6 +275,10 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { } // insert our indents and any text from the previous line that might have been after the line break + // negative indents shouldn't exist and would only break future calculations. + if(numberIndents < 0) { + numberIndents = 0; + } for (let i = 0; i < numberIndents; i++) { newLine += this.indentation; } @@ -339,7 +343,8 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { if(codeInput.value.substring(codeInput.textareaElement.selectionStart - this.indentationNumChars, codeInput.textareaElement.selectionStart) == this.indentation) { // Indentation before cursor = delete it codeInput.textareaElement.selectionStart -= this.indentationNumChars; - document.execCommand("delete", false, ""); + // document.execCommand("delete", false, ""); + // event.preventDefault(); } } } diff --git a/tests/tester.js b/tests/tester.js index 5b0899e..e304f74 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -59,6 +59,7 @@ function testAddingText(group, textarea, action, correctOutput, correctLengthToS /* Assuming the textarea is focused, add the given text to it, emitting 'input' and 'beforeinput' keyboard events (and 'keydown'/'keyup' Enter on newlines, if enterEvents is true) which plugins can handle */ function addText(textarea, text, enterEvents=false) { for(let i = 0; i < text.length; i++) { + const selectionStartBefore = textarea.selectionStart; if(enterEvents && text[i] == "\n") { textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" })); @@ -162,12 +163,17 @@ function startLoad(codeInputElem, isHLJS) { } /* Make input events work and be trusted in the inputElement - thanks for this SO answer: https://stackoverflow.com/a/49519772/21785620 */ -function allowInputEvents(inputElement) { +function allowInputEvents(inputElement, codeInputElement=undefined) { inputElement.addEventListener('input', function(e){ if(!e.isTrusted){ e.preventDefault(); // Manually trigger + // Prevent auto-close-brackets plugin recapturing the event + // Needed because this interception is hacky. + // TODO: Potentially plugin-agnostic way, probably automatedKeypresses var in core, won't be needed much but may be helpful extra feature. + if(codeInputElement !== undefined) codeInputElement.pluginData.autoCloseBrackets.automatedKeypresses = true; document.execCommand("insertText", false, e.data); + if(codeInputElement !== undefined) codeInputElement.pluginData.autoCloseBrackets.automatedKeypresses = false; } }, false); } @@ -175,9 +181,9 @@ function allowInputEvents(inputElement) { /* Start the tests using the textarea inside the code-input element and whether highlight.js is being used (as the Autodetect plugin only works with highlight.js, for example) */ async function startTests(textarea, isHLJS) { textarea.focus(); - allowInputEvents(textarea); codeInputElement = textarea.parentElement; + allowInputEvents(textarea, codeInputElement); /*--- Tests for core functionality ---*/ @@ -438,7 +444,7 @@ console.log("I've got another line!", 2 < 3, "should be true."); findInput.focus(); allowInputEvents(findInput); addText(findInput, "hello"); - await waitAsync(150); // Wait for highlighting so matches update + await waitAsync(200); // Wait for highlighting so matches update replaceInput.value = "hi"; replaceAllButton.click(); @@ -590,4 +596,4 @@ console.log("I've got another line!", 2 < 3, "should be true."); document.querySelector("h2").style.backgroundColor = "lightgreen"; document.querySelector("h2").textContent = "All Tests have Passed."; } -} \ No newline at end of file +} From 40aaf388c09836303ede6465be7932aee9ab3511 Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Tue, 15 Jul 2025 15:23:46 +0100 Subject: [PATCH 3/4] Add test for automatic unindent --- tests/tester.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/tester.js b/tests/tester.js index e304f74..d22ce9e 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -531,8 +531,10 @@ console.log("I've got another line!", 2 < 3, "should be true."); backspace(textarea); testAddingText("Indent-AutoCloseBrackets", textarea, function(textarea) { - addText(textarea, `function printTriples(max) {\nfor(let i = 0; i < max-2; i++) {\nfor(let j = 0; j < max-1; j++) {\nfor(let k = 0; k < max; k++) {\nconsole.log(i,j,k);\n}\n//Hmmm...`, true); - }, 'function printTriples(max) {\n for(let i = 0; i < max-2; i++) {\n for(let j = 0; j < max-1; j++) {\n for(let k = 0; k < max; k++) {\n console.log(i,j,k);\n }\n //Hmmm...\n }\n }\n }\n}', 189, 189); + addText(textarea, `function printTriples(max) {\nfor(let i = 0; i < max-2; i++) {\nfor(let j = 0; j < max-1; j++) {\nfor(let k = 0; k < max; k++) {\nconsole.log(i,j,k);\n}\n//Hmmm...\n}//Test auto-unindent\n{`, true); + move(textarea, 1); // Move after created closing bracket + backspace(textarea); // Remove created closing bracket + }, 'function printTriples(max) {\n for(let i = 0; i < max-2; i++) {\n for(let j = 0; j < max-1; j++) {\n for(let k = 0; k < max; k++) {\n console.log(i,j,k);\n }\n //Hmmm...\n }//Test auto-unindent\n {\n }\n }\n }\n}', 221, 211); // SelectTokenCallbacks if(isHLJS) { From 47008949f166767143300b2e54815d4edc5c1fde Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Tue, 15 Jul 2025 15:25:53 +0100 Subject: [PATCH 4/4] Remove unnecessary line of testing code --- tests/tester.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tester.js b/tests/tester.js index d22ce9e..a408ce3 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -59,7 +59,6 @@ function testAddingText(group, textarea, action, correctOutput, correctLengthToS /* Assuming the textarea is focused, add the given text to it, emitting 'input' and 'beforeinput' keyboard events (and 'keydown'/'keyup' Enter on newlines, if enterEvents is true) which plugins can handle */ function addText(textarea, text, enterEvents=false) { for(let i = 0; i < text.length; i++) { - const selectionStartBefore = textarea.selectionStart; if(enterEvents && text[i] == "\n") { textarea.dispatchEvent(new KeyboardEvent("keydown", { "key": "Enter" })); textarea.dispatchEvent(new KeyboardEvent("keyup", { "key": "Enter" }));