Skip to content

Add Auto-Close Brackets plugin and brackets functionality for indenta… #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion code-input.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down
10 changes: 9 additions & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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)

Expand Down
54 changes: 54 additions & 0 deletions plugins/auto-close-brackets.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
8 changes: 6 additions & 2 deletions plugins/go-to-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
63 changes: 61 additions & 2 deletions plugins/indent.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
/**
* 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;

/**
* 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++) {
Expand All @@ -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 */
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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, "");
}
}
}
}
}