From e964a106121529c84e3056b7737d9edcd5caff3c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 22 Jun 2024 10:44:34 -0400 Subject: [PATCH] debugger --- .../extensions/quick-reply/html/qrEditor.html | 4 + .../extensions/quick-reply/src/QuickReply.js | 115 +++++++++++++++--- .../scripts/extensions/quick-reply/style.css | 55 ++++++++- .../scripts/extensions/quick-reply/style.less | 55 ++++++++- .../slash-commands/SlashCommandClosure.js | 1 + .../SlashCommandDebugController.js | 6 + 6 files changed, 213 insertions(+), 23 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 80aab2386..c92a12487 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -43,6 +43,10 @@ +
+ + +

Context Menu

diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 746e98af6..fe6da8bc4 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -65,6 +65,7 @@ export class QuickReply { /**@type {HTMLElement}*/ editorDebugState; /**@type {HTMLInputElement}*/ editorExecuteHide; /**@type {Promise}*/ editorExecutePromise; + /**@type {boolean}*/ isExecuting; /**@type {SlashCommandAbortController}*/ abortController; /**@type {SlashCommandDebugController}*/ debugController; @@ -244,9 +245,15 @@ export class QuickReply { if (wrap.checked) { message.style.whiteSpace = 'pre-wrap'; messageSyntaxInner.style.whiteSpace = 'pre-wrap'; + if (this.clone) { + this.clone.style.whiteSpace = 'pre-wrap'; + } } else { message.style.whiteSpace = 'pre'; messageSyntaxInner.style.whiteSpace = 'pre'; + if (this.clone) { + this.clone.style.whiteSpace = 'pre'; + } } updateScrollDebounced(); }; @@ -317,6 +324,7 @@ export class QuickReply { setSlashCommandAutoComplete(message, true); //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize message.addEventListener('keydown', async(evt) => { + if (this.isExecuting) return; if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { evt.preventDefault(); const start = message.selectionStart; @@ -569,6 +577,28 @@ export class QuickReply { stepOutBtn.addEventListener('click', ()=>{ this.debugController?.stepOut(); }); + /**@type {boolean}*/ + let isResizing = false; + let resizeStart; + let wStart; + /**@type {HTMLElement}*/ + const resizeHandle = dom.querySelector('#qr--resizeHandle'); + resizeHandle.addEventListener('pointerdown', (evt)=>{ + if (isResizing) return; + isResizing = true; + evt.preventDefault(); + resizeStart = evt.x; + wStart = dom.querySelector('#qr--qrOptions').offsetWidth; + const dragListener = debounce((evt)=>{ + const w = wStart + resizeStart - evt.x; + dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); + }, 5); + window.addEventListener('pointerup', ()=>{ + window.removeEventListener('pointermove', dragListener); + isResizing = false; + }, { once:true }); + window.addEventListener('pointermove', dragListener); + }); await popupResult; @@ -596,6 +626,7 @@ export class QuickReply { }); mo.observe(this.editorMessage.parentElement, { childList:true }); } + this.clone.style.width = `${inputRect.width}px`; this.clone.style.height = `${inputRect.height}px`; this.clone.style.left = `${inputRect.left}px`; this.clone.style.top = `${inputRect.top}px`; @@ -620,8 +651,13 @@ export class QuickReply { return location; } async executeFromEditor() { - if (this.editorExecutePromise) return; + if (this.isExecuting) return; + this.isExecuting = true; this.editorDom.classList.add('qr--isExecuting'); + const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax'); + if (noSyntax) { + this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); + } this.editorExecuteBtn.classList.add('qr--busy'); this.editorExecuteProgress.style.setProperty('--prog', '0'); this.editorExecuteErrors.classList.remove('qr--hasErrors'); @@ -658,17 +694,14 @@ export class QuickReply { title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; let hi; title.addEventListener('pointerenter', ()=>{ - const loc = this.getEditorPosition(c.executorList[0].start - 1, c.executorList.slice(-1)[0].end); + const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end); const layer = this.editorPopup.dlg.getBoundingClientRect(); hi = document.createElement('div'); - hi.style.position = 'fixed'; + hi.classList.add('qr--highlight-secondary'); hi.style.left = `${loc.left - layer.left}px`; hi.style.width = `${loc.right - loc.left}px`; hi.style.top = `${loc.top - layer.top}px`; hi.style.height = `${loc.bottom - loc.top}px`; - hi.style.zIndex = '50000'; - hi.style.pointerEvents = 'none'; - hi.style.border = '3px solid red'; this.editorPopup.dlg.append(hi); }); title.addEventListener('pointerleave', ()=>hi?.remove()); @@ -696,7 +729,13 @@ export class QuickReply { v.classList.add('qr--undefined'); v.textContent = 'undefined'; } else { - v.textContent = val; + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + v.textContent = JSON.stringify(jsonVal, null, 2); + } else { + v.textContent = val; + } } item.append(v); } @@ -725,7 +764,13 @@ export class QuickReply { v.classList.add('qr--undefined'); v.textContent = 'undefined'; } else { - v.textContent = val; + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + v.textContent = JSON.stringify(jsonVal, null, 2); + } else { + v.textContent = val; + } } item.append(v); } @@ -750,7 +795,13 @@ export class QuickReply { v.classList.add('qr--undefined'); v.textContent = 'undefined'; } else { - v.textContent = val; + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + v.textContent = JSON.stringify(jsonVal, null, 2); + } else { + v.textContent = val; + } } pipeItem.append(v); } @@ -762,19 +813,51 @@ export class QuickReply { } return wrap; }; + const buildStack = ()=>{ + const wrap = document.createElement('div'); { + wrap.classList.add('qr--stack'); + const title = document.createElement('div'); { + title.classList.add('qr--title'); + title.textContent = 'Call Stack'; + wrap.append(title); + } + for (const executor of this.debugController.cmdStack.toReversed()) { + const item = document.createElement('div'); { + item.classList.add('qr--item'); + item.textContent = `/${executor.name}`; + if (executor.command.name == 'run') { + item.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; + } + let hi; + item.addEventListener('pointerenter', ()=>{ + const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end); + const layer = this.editorPopup.dlg.getBoundingClientRect(); + hi = document.createElement('div'); + hi.classList.add('qr--highlight-secondary'); + hi.style.left = `${loc.left - layer.left}px`; + hi.style.width = `${loc.right - loc.left}px`; + hi.style.top = `${loc.top - layer.top}px`; + hi.style.height = `${loc.bottom - loc.top}px`; + this.editorPopup.dlg.append(hi); + }); + item.addEventListener('pointerleave', ()=>hi?.remove()); + wrap.append(item); + } + } + } + return wrap; + }; this.editorDebugState.append(buildVars(closure.scope, true)); + this.editorDebugState.append(buildStack()); this.editorDebugState.classList.add('qr--active'); - const loc = this.getEditorPosition(executor.start - 1, executor.end); + const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end); const layer = this.editorPopup.dlg.getBoundingClientRect(); const hi = document.createElement('div'); - hi.style.position = 'fixed'; + hi.classList.add('qr--highlight'); hi.style.left = `${loc.left - layer.left}px`; hi.style.width = `${loc.right - loc.left}px`; hi.style.top = `${loc.top - layer.top}px`; hi.style.height = `${loc.bottom - loc.top}px`; - hi.style.zIndex = '50000'; - hi.style.pointerEvents = 'none'; - hi.style.backgroundColor = 'rgb(255 255 0 / 0.5)'; this.editorPopup.dlg.append(hi); const isStepping = await this.debugController.awaitContinue(); hi.remove(); @@ -807,10 +890,14 @@ export class QuickReply { `; } } + if (noSyntax) { + this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); + } this.editorExecutePromise = null; this.editorExecuteBtn.classList.remove('qr--busy'); this.editorPopup.dlg.classList.remove('qr--hide'); this.editorDom.classList.remove('qr--isExecuting'); + this.isExecuting = false; } updateEditorProgress(done, total) { diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 9098bcbf9..f122dd927 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -241,6 +241,18 @@ .popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls { display: none; } +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight { + position: fixed; + z-index: 50000; + pointer-events: none; + background-color: rgba(255, 255, 0, 0.5); +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary { + position: fixed; + z-index: 50000; + pointer-events: none; + border: 3px solid red; +} .popup:has(#qr--modalEditor) .popup-content { display: flex; flex-direction: column; @@ -262,9 +274,26 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div { display: none; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { + visibility: hidden; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons { display: flex; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle { + width: 6px; + background-color: var(--SmartThemeBorderColor); + border: 2px solid var(--SmartThemeBlurTintColor); + transition: border-color 200ms, background-color 200ms; + cursor: w-resize; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover { + background-color: var(--SmartThemeQuoteColor); + border-color: var(--SmartThemeQuoteColor); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions { + width: var(--width, auto); +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main { flex: 1 1 auto; display: flex; @@ -435,6 +464,7 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton { aspect-ratio: 1.25 / 1; + width: 2.25em; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton.qr--glyph-combo { display: grid; @@ -544,12 +574,12 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe { display: contents; } -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 2) .qr--key, -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 2) .qr--key, -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 2) .qr--key, -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 2) .qr--val, -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 2) .qr--val, -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 2) .qr--val { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val { background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key, @@ -582,6 +612,19 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val { opacity: 0.5; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title { + grid-column: 1 / 3; + font-weight: bold; + background-color: var(--black50a); + padding: 0.25em; + margin-top: 1em; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item { + margin-left: 0.5em; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); +} @keyframes qr--progressPulse { 0%, 100% { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index a63fb4229..a44e42b1d 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -262,8 +262,23 @@ .popup:has(#qr--modalEditor) { aspect-ratio: unset; - &:has(.qr--isExecuting) .popup-controls { - display: none; + &:has(.qr--isExecuting) { + .popup-controls { + display: none; + } + + .qr--highlight { + position: fixed; + z-index: 50000; + pointer-events: none; + background-color: rgba(255, 255, 0, 0.5); + } + .qr--highlight-secondary { + position: fixed; + z-index: 50000; + pointer-events: none; + border: 3px solid red; + } } .popup-content { @@ -289,9 +304,26 @@ { display: none; } + #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message { + visibility: hidden; + } #qr--modal-debugButtons { display: flex; } + #qr--resizeHandle { + width: 6px; + background-color: var(--SmartThemeBorderColor); + border: 2px solid var(--SmartThemeBlurTintColor); + transition: border-color 200ms, background-color 200ms; + cursor: w-resize; + &:hover { + background-color: var(--SmartThemeQuoteColor); + border-color: var(--SmartThemeQuoteColor); + } + } + #qr--qrOptions { + width: var(--width, auto); + } } > #qr--main { @@ -461,6 +493,7 @@ gap: 1em; .qr--modal-debugButton { aspect-ratio: 1.25 / 1; + width: 2.25em; &.qr--glyph-combo { display: grid; grid-template-columns: 1fr; @@ -566,7 +599,7 @@ } .qr--var, .qr--macro, .qr--pipe { display: contents; - &:nth-child(2n + 2) { + &:nth-child(2n + 1) { .qr--key, .qr--val { background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); } @@ -597,6 +630,22 @@ } } } + + .qr--stack { + .qr--title { + grid-column: 1 / 3; + font-weight: bold; + background-color: var(--black50a); + padding: 0.25em; + margin-top: 1em; + } + .qr--item { + margin-left: 0.5em; + &:nth-child(2n + 1) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); + } + } + } } } } diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 80902e60c..d624e69c0 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -213,6 +213,7 @@ export class SlashCommandClosure { let done = 0; for (const executor of this.executorList) { this.onProgress?.(done, this.commandCount); + this.debugController?.setExecutor(executor); yield executor; if (executor instanceof SlashCommandClosureExecutor) { const closure = this.scope.getVariable(executor.name); diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js index fa47e3b9b..f6ae407c2 100644 --- a/public/scripts/slash-commands/SlashCommandDebugController.js +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -3,6 +3,7 @@ import { SlashCommandExecutor } from './SlashCommandExecutor.js'; export class SlashCommandDebugController { /**@type {SlashCommandClosure[]} */ stack = []; + /**@type {SlashCommandExecutor[]} */ cmdStack = []; /**@type {boolean[]} */ stepStack = []; /**@type {boolean} */ isStepping = false; /**@type {boolean} */ isSteppingInto = false; @@ -31,9 +32,14 @@ export class SlashCommandDebugController { } up() { this.stack.pop(); + while (this.cmdStack.length > this.stack.length) this.cmdStack.pop(); this.stepStack.pop(); } + setExecutor(executor) { + this.cmdStack[this.stack.length - 1] = executor; + } + resume() {