From db1094e391c3f46dad673836d9483b1e761deba1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 18:58:13 -0400 Subject: [PATCH] add block comments with shortcut and breakpoint shortcut --- .../extensions/quick-reply/html/qrEditor.html | 3 +- .../extensions/quick-reply/src/QuickReply.js | 122 +++++++++++++----- .../scripts/extensions/quick-reply/style.css | 3 +- .../scripts/extensions/quick-reply/style.less | 3 +- .../slash-commands/SlashCommandParser.js | 39 +++++- 5 files changed, 135 insertions(+), 35 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 814da1b81..24c6270a3 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -43,7 +43,8 @@ Syntax highlight - Ctrl+Alt+Click to set / remove breakpoints + Ctrl+Alt+Click (or F9) to set / remove breakpoints + Ctrl+ to toggle block comments
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 558dcc64e..2a8a51a0b 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -501,6 +501,7 @@ export class QuickReply { localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); updateSyntaxEnabled(); }); + navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]'); /**@type {HTMLTextAreaElement}*/ const message = dom.querySelector('#qr--modal-message'); @@ -515,6 +516,7 @@ export class QuickReply { message.addEventListener('keydown', async(evt) => { if (this.isExecuting) return; if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { + // increase indent evt.preventDefault(); const start = message.selectionStart; const end = message.selectionEnd; @@ -536,6 +538,7 @@ export class QuickReply { message.dispatchEvent(new Event('input', { bubbles:true })); } } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { + // decrease indent evt.preventDefault(); evt.stopImmediatePropagation(); evt.stopPropagation(); @@ -548,6 +551,7 @@ export class QuickReply { message.selectionEnd = end - count; message.dispatchEvent(new Event('input', { bubbles:true })); } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { + // new line, keep indent evt.stopImmediatePropagation(); evt.stopPropagation(); evt.preventDefault(); @@ -560,10 +564,11 @@ export class QuickReply { message.selectionEnd = message.selectionStart; message.dispatchEvent(new Event('input', { bubbles:true })); } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { - evt.stopImmediatePropagation(); - evt.stopPropagation(); - evt.preventDefault(); if (executeShortcut.checked) { + // execute QR + evt.stopImmediatePropagation(); + evt.stopPropagation(); + evt.preventDefault(); const selectionStart = message.selectionStart; const selectionEnd = message.selectionEnd; message.blur(); @@ -574,6 +579,45 @@ export class QuickReply { message.selectionEnd = selectionEnd; } } + } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + // toggle breakpoint + evt.stopImmediatePropagation(); + evt.stopPropagation(); + evt.preventDefault(); + preBreakPointStart = message.selectionStart; + preBreakPointEnd = message.selectionEnd; + toggleBreakpoint(); + } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + // toggle comment + evt.stopImmediatePropagation(); + evt.stopPropagation(); + evt.preventDefault(); + // check if we are inside a comment -> uncomment + const parser = new SlashCommandParser(); + parser.parse(message.value, false); + const start = message.selectionStart; + const end = message.selectionEnd; + const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end)); + if (comment) { + let content = message.value.slice(comment.start + 1, comment.end - 1); + let len = content.length; + content = content.replace(/^ /, ''); + const offsetStart = len - content.length; + len = content.length; + content = content.replace(/ $/, ''); + const offsetEnd = len - content.length; + message.value = `${message.value.slice(0, comment.start - 1)}${content}${message.value.slice(comment.end + 1)}`; + message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0); + message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0); + } else { + const lineStart = message.value.lastIndexOf('\n', start - 1) + 1; + const lineEnd = message.value.indexOf('\n', end); + const lines = message.value.slice(lineStart, lineEnd).split('\n'); + message.value = `${message.value.slice(0, lineStart)}/* ${message.value.slice(lineStart, lineEnd)} *|${message.value.slice(lineEnd)}`; + message.selectionStart = start + 3; + message.selectionEnd = end + 3; + } + message.dispatchEvent(new Event('input', { bubbles:true })); } }); const ac = await setSlashCommandAutoComplete(message, true); @@ -583,6 +627,11 @@ export class QuickReply { message.addEventListener('scroll', (evt)=>{ updateScrollDebounced(); }); + let preBreakPointStart; + let preBreakPointEnd; + /** + * @param {SlashCommandBreakPoint} bp + */ const removeBreakpoint = (bp)=>{ // start at -1 because "/" is not included in start-end let start = bp.start - 1; @@ -623,21 +672,38 @@ export class QuickReply { } return { start:postStart, end:postEnd }; }; - let preBreakPointStart; - let preBreakPointEnd; - message.addEventListener('pointerdown', (evt)=>{ - if (!evt.ctrlKey || !evt.altKey) return; - preBreakPointStart = message.selectionStart; - preBreakPointEnd = message.selectionEnd; - }); - message.addEventListener('pointerup', async(evt)=>{ - if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; + /** + * @param {SlashCommandExecutor} cmd + */ + const addBreakpoint = (cmd)=>{ + // start at -1 because "/" is not included in start-end + let start = cmd.start - 1; + let indent = ''; + // step left until forward slash "/" + while (message.value[start] != '/') start--; + // step left while whitespace (except newline) before start, collect the whitespace to help build indentation + while (/[^\S\n]/.test(message.value[start - 1])) { + start--; + indent += message.value[start]; + } + // if newline before indent, include the newline + if (message.value[start - 1] == '\n') { + start--; + indent = `\n${indent}`; + } + const breakpointText = `${indent}/breakpoint |`; + const v = `${message.value.slice(0, start)}${breakpointText}${message.value.slice(start)}`; + message.value = v; + message.dispatchEvent(new Event('input', { bubbles:true })); + return breakpointText.length; + }; + const toggleBreakpoint = ()=>{ const idx = message.selectionStart; let postStart = preBreakPointStart; let postEnd = preBreakPointEnd; const parser = new SlashCommandParser(); parser.parse(message.value, false); - const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx && it.end >= idx); + const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx); if (cmdIdx > -1) { const cmd = parser.commandIndex[cmdIdx]; if (cmd instanceof SlashCommandBreakPoint) { @@ -651,28 +717,22 @@ export class QuickReply { postStart = start; postEnd = end; } else { - // start at -1 because "/" is not included in start-end - let start = cmd.start - 1; - let indent = ''; - // step left until forward slash "/" - while (message.value[start] != '/') start--; - // step left while whitespace (except newline) before start, collect the whitespace to help build indentation - while (/[^\S\n]/.test(message.value[start - 1])) { - start--; - indent += message.value[start]; - } - // if newline before indent, include the newline - if (message.value[start - 1] == '\n') { - start--; - indent = `\n${indent}`; - } - const v = `${message.value.slice(0, start)}${indent}/breakpoint |${message.value.slice(start)}`; - message.value = v; - message.dispatchEvent(new Event('input', { bubbles:true })); + const len = addBreakpoint(cmd); + postStart += len; + postEnd += len; } message.selectionStart = postStart; message.selectionEnd = postEnd; } + }; + message.addEventListener('pointerdown', (evt)=>{ + if (!evt.ctrlKey || !evt.altKey) return; + preBreakPointStart = message.selectionStart; + preBreakPointEnd = message.selectionEnd; + }); + message.addEventListener('pointerup', async(evt)=>{ + if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; + toggleBreakpoint(); }); /** @type {any} */ const resizeListener = debounce((evt) => { diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 065d1667b..013c31839 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -489,8 +489,9 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { display: flex; + flex-wrap: wrap; flex-direction: row; - gap: 1em; + column-gap: 1em; color: var(--grey70); font-size: smaller; align-items: baseline; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 8bd1c6ac7..604c1938c 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -560,8 +560,9 @@ overflow: hidden; > .qr--modal-editorSettings { display: flex; + flex-wrap: wrap; flex-direction: row; - gap: 1em; + column-gap: 1em; color: var(--grey70); font-size: smaller; align-items: baseline; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 83194e02f..9b4ad67dc 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -231,6 +231,12 @@ export class SlashCommandParser { } } + const BLOCK_COMMENT = { + scope: 'comment', + begin: /\/\*/, + end: /\*\|/, + contains: [], + }; const COMMENT = { scope: 'comment', begin: /\/[/#]/, @@ -312,6 +318,9 @@ export class SlashCommandParser { begin: /{{/, end: /}}/, }; + BLOCK_COMMENT.contains.push( + BLOCK_COMMENT, + ); RUN.contains.push( hljs.BACKSLASH_ESCAPE, NAMED_ARG, @@ -362,6 +371,7 @@ export class SlashCommandParser { ); CLOSURE.contains.push( hljs.BACKSLASH_ESCAPE, + BLOCK_COMMENT, COMMENT, ABORT, KEYWORD, @@ -381,6 +391,7 @@ export class SlashCommandParser { keywords: ['|'], contains: [ hljs.BACKSLASH_ESCAPE, + BLOCK_COMMENT, COMMENT, ABORT, KEYWORD, @@ -678,7 +689,9 @@ export class SlashCommandParser { this.discardWhitespace(); } while (!this.testClosureEnd()) { - if (this.testComment()) { + if (this.testBlockComment()) { + this.parseBlockComment(); + } else if (this.testComment()) { this.parseComment(); } else if (this.testParserFlag()) { this.parseParserFlag(); @@ -760,6 +773,30 @@ export class SlashCommandParser { return cmd; } + testBlockComment() { + return this.testSymbol(/\/\*/); + } + testBlockCommentEnd() { + return this.testSymbol(/\*\|/); + } + parseBlockComment() { + const start = this.index + 1; + const cmd = new SlashCommandExecutor(start); + cmd.command = this.commands['*']; + this.commandIndex.push(cmd); + this.scopeIndex.push(this.scope.getCopy()); + this.take(); // discard "/" + cmd.name = this.take(); //set "*" as name + while (!this.testBlockCommentEnd()) { + if (this.testBlockComment()) { + this.parseBlockComment(); + } + this.take(); + } + this.take(2); // take closing "*|" + cmd.end = this.index - 1; + } + testComment() { return this.testSymbol(/\/[/#]/); }