From a2c4c3d048e06f927999803585d045c90f42f9c4 Mon Sep 17 00:00:00 2001 From: WebCoder49 Date: Thu, 14 Dec 2023 12:39:31 +0100 Subject: [PATCH] Add Auto-Close Brackets plugin and brackets functionality for indentation; Fix Ctrl+G for Go To Line plugin --- code-input.d.ts | 16 ++++++++- plugins/README.md | 10 +++++- plugins/auto-close-brackets.js | 54 +++++++++++++++++++++++++++++ plugins/go-to-line.js | 8 +++-- plugins/indent.js | 63 ++++++++++++++++++++++++++++++++-- 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 plugins/auto-close-brackets.js diff --git a/code-input.d.ts b/code-input.d.ts index c31bd85..22b8eb9 100644 --- a/code-input.d.ts +++ b/code-input.d.ts @@ -76,6 +76,19 @@ export namespace plugins { constructor(); } + /** + * Automatically closes pairs of brackets/quotes/other syntaxes in code, but also lets you choose the brackets this + * is activated for. + * Files: auto-close-brackets.js + */ + class AutoCloseBrackets extends Plugin { + /** + * Create an auto-close brackets plugin to pass into a template + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}", '"': '"'}. All brackets must only be one character. + */ + constructor(bracketPairs: Object); + } + /** * Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions. * Files: autocomplete.js / autocomplete.css @@ -137,8 +150,9 @@ export namespace plugins { * Create an indentation plugin to pass into a template * @param {Boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false. * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4. + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}"}. All brackets must only be one character, and this can be left as null to remove bracket-based indentation behaviour. */ - constructor(defaultSpaces?: boolean, numSpaces?: Number); + constructor(defaultSpaces?: boolean, numSpaces?: Number, bracketPairs?: Object); } /** diff --git a/plugins/README.md b/plugins/README.md index b841b2a..9c27dd8 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -7,6 +7,14 @@ --- +### Auto-Close Brackets +Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this +is activated for. + +Files: [auto-close-brackets.js](./auto-close-brackets.js) + +[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/qBgGGKR) + ### Autocomplete Display a popup under the caret using the text in the code-input element. This works well with autocomplete suggestions. @@ -36,7 +44,7 @@ Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css) [🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/YzBMOXP) ### Indent -Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation.** +Add indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation, as well as (optionally) brackets typed affecting indentation.** Files: [indent.js](./indent.js) diff --git a/plugins/auto-close-brackets.js b/plugins/auto-close-brackets.js new file mode 100644 index 0000000..54e20f5 --- /dev/null +++ b/plugins/auto-close-brackets.js @@ -0,0 +1,54 @@ +/** + * Automatically close pairs of brackets/quotes/other syntaxes in code, but also optionally choose the brackets this + * is activated for. + * Files: auto-close-brackets.js + */ +codeInput.plugins.AutoCloseBrackets = class extends codeInput.Plugin { + bracketPairs = []; + bracketsOpenedStack = []; // Each item [closing bracket string, opening bracket location] Innermost at right so can know which brackets should be ignored when retyped + + /** + * Create an auto-close brackets plugin to pass into a template + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}", '"': '"'}. All brackets must only be one character. + */ + constructor(bracketPairs={"(": ")", "[": "]", "{": "}", '"': '"'}) { + super([]); // No observed attributes + + this.bracketPairs = bracketPairs; + } + + /* Add keystroke events */ + afterElementsAdded(codeInput) { + let textarea = codeInput.textareaElement; + textarea.addEventListener('keydown', (event) => { this.checkBackspace(codeInput, event) }); + textarea.addEventListener('beforeinput', (event) => { this.checkBrackets(codeInput, event); }); + + } + + /* Event handlers */ + checkBrackets(codeInput, event) { + if(this.bracketsOpenedStack.length > 0 && event.data == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0] && event.data == codeInput.textareaElement.value[codeInput.textareaElement.selectionStart]) { + // "Retype" bracket, i.e. just move caret + codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd += 1; + this.bracketsOpenedStack.pop(); + event.preventDefault(); + } else if(event.data in this.bracketPairs) { + // Create bracket pair + let closingBracket = this.bracketPairs[event.data]; + this.bracketsOpenedStack.push([closingBracket, codeInput.textareaElement.selectionStart]); + document.execCommand("insertText", false, closingBracket); + codeInput.textareaElement.selectionStart = codeInput.textareaElement.selectionEnd -= 1; + } + } + + checkBackspace(codeInput, event) { + if(event.key == "Backspace" && codeInput.textareaElement.selectionStart == codeInput.textareaElement.selectionEnd) { + if(this.bracketsOpenedStack.length > 0 && this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][1]+1 == codeInput.textareaElement.selectionStart && codeInput.textareaElement.value[codeInput.textareaElement.selectionStart] == this.bracketsOpenedStack[this.bracketsOpenedStack.length-1][0]) { + // Delete closing bracket as well + codeInput.textareaElement.selectionEnd = codeInput.textareaElement.selectionStart + 1; + codeInput.textareaElement.selectionStart -= 1; + this.bracketsOpenedStack.pop(); + } + } + } +} \ No newline at end of file diff --git a/plugins/go-to-line.js b/plugins/go-to-line.js index f83acb6..8cd6b74 100644 --- a/plugins/go-to-line.js +++ b/plugins/go-to-line.js @@ -3,19 +3,23 @@ * Files: go-to-line.js / go-to-line.css */ codeInput.plugins.GoToLine = class extends codeInput.Plugin { + useCtrlG = false; /** * Create a go-to-line command plugin to pass into a template * @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? If not, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`. */ - constructor(useCtrlG) { + constructor(useCtrlG = true) { super([]); // No observed attributes + this.useCtrlG = useCtrlG; } /* Add keystroke events */ afterElementsAdded(codeInput) { const textarea = codeInput.textareaElement; - textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); }); + if(this.useCtrlG) { + textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); }); + } } blockSearch(dialog, event) { diff --git a/plugins/indent.js b/plugins/indent.js index 466eb4e..5b515ef 100644 --- a/plugins/indent.js +++ b/plugins/indent.js @@ -1,11 +1,12 @@ /** - * Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it + * Add indentation using the `Tab` key, and auto-indents after a newline, as well as making it * possible to indent/unindent multiple lines using Tab/Shift+Tab * Files: indent.js */ codeInput.plugins.Indent = class extends codeInput.Plugin { numSpaces; + bracketPairs = null; // No bracket-auto-indentation used indentation = "\t"; indentationNumChars = 1; @@ -13,11 +14,13 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { * Create an indentation plugin to pass into a template * @param {Boolean} defaultSpaces Should the Tab key enter spaces rather than tabs? Defaults to false. * @param {Number} numSpaces How many spaces is each tab character worth? Defaults to 4. + * @param {Object} bracketPairs Opening brackets mapped to closing brackets, default and example {"(": ")", "[": "]", "{": "}"}. All brackets must only be one character, and this can be left as null to remove bracket-based indentation behaviour. */ - constructor(defaultSpaces=false, numSpaces=4) { + constructor(defaultSpaces=false, numSpaces=4, bracketPairs={"(": ")", "[": "]", "{": "}"}) { super([]); // No observed attributes this.numSpaces = numSpaces; + this.bracketPairs = bracketPairs; if(defaultSpaces) { this.indentation = ""; for(let i = 0; i < numSpaces; i++) { @@ -31,6 +34,7 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { afterElementsAdded(codeInput) { let textarea = codeInput.textareaElement; textarea.addEventListener('keydown', (event) => { this.checkTab(codeInput, event); this.checkEnter(codeInput, event); this.checkBackspace(codeInput, event); }); + textarea.addEventListener('beforeinput', (event) => { this.checkCloseBracket(codeInput, event); }); } /* Event handlers */ @@ -135,6 +139,39 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { lines[currentLineI] = lines[currentLineI].substring(0, cursorPosInLine); } + let bracketThreeLinesTriggered = false; + let furtherIndentation = ""; + if(this.bracketPairs != null) { + for(let openingBracket in this.bracketPairs) { + if(lines[currentLineI][lines[currentLineI].length-1] == openingBracket) { + let closingBracket = this.bracketPairs[openingBracket]; + if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) { + // Create new line and then put textAfterCursor on yet another line: + // { + // |CARET| + // } + bracketThreeLinesTriggered = true; + for (let i = 0; i < numberIndents+1; i++) { + furtherIndentation += this.indentation; + } + } else { + // Just create new line: + // { + // |CARET| + numberIndents++; + } + break; + } else { + // Check whether brackets cause unindent + let closingBracket = this.bracketPairs[openingBracket]; + if(textAfterCursor.length > 0 && textAfterCursor[0] == closingBracket) { + numberIndents--; + break; + } + } + } + } + // insert our indents and any text from the previous line that might have been after the line break for (let i = 0; i < numberIndents; i++) { newLine += this.indentation; @@ -143,6 +180,10 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { // save the current cursor position let selectionStartI = inputElement.selectionStart; + 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 // move cursor to new position @@ -175,4 +216,22 @@ codeInput.plugins.Indent = class extends codeInput.Plugin { document.execCommand("delete", false, ""); } } + + checkCloseBracket(codeInput, event) { + if(codeInput.textareaElement.selectionStart != codeInput.textareaElement.selectionEnd) { + return; + } + + for(let openingBracket in this.bracketPairs) { + let closingBracket = this.bracketPairs[openingBracket]; + if(event.data == closingBracket) { + // Closing bracket unindents line + 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, ""); + } + } + } + } } \ No newline at end of file