From eb02ca95f9135f0a5b6c1462403677d0ba38bc65 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 18 Jun 2024 14:29:29 -0400 Subject: [PATCH] debugger basics rough --- .../extensions/quick-reply/html/qrEditor.html | 12 ++ .../extensions/quick-reply/src/QuickReply.js | 108 ++++++++++- .../quick-reply/src/QuickReplySet.js | 26 +++ .../scripts/extensions/quick-reply/style.css | 31 +++- .../scripts/extensions/quick-reply/style.less | 20 ++ public/scripts/slash-commands.js | 4 + .../slash-commands/SlashCommandBreakPoint.js | 3 + .../slash-commands/SlashCommandClosure.js | 175 +++++++++++++++++- .../SlashCommandDebugController.js | 41 ++++ .../slash-commands/SlashCommandParser.js | 23 ++- 10 files changed, 431 insertions(+), 12 deletions(-) create mode 100644 public/scripts/slash-commands/SlashCommandBreakPoint.js create mode 100644 public/scripts/slash-commands/SlashCommandDebugController.js diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index b9ce236d6..d7dd85b00 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -116,6 +116,17 @@ +
+ + + +
+
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index f4a09906f..09894ae36 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,6 +1,10 @@ import { POPUP_TYPE, Popup } from '../../../popup.js'; import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; +import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; +import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js'; +import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; +import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js'; import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { debounce, getSortableDelay } from '../../../utils.js'; @@ -38,6 +42,7 @@ export class QuickReply { /**@type {String}*/ automationId = ''; /**@type {Function}*/ onExecute; + /**@type {(qr:QuickReply)=>AsyncGenerator}*/ onDebug; /**@type {Function}*/ onDelete; /**@type {Function}*/ onUpdate; @@ -56,9 +61,11 @@ export class QuickReply { /**@type {HTMLElement}*/ editorExecuteProgress; /**@type {HTMLElement}*/ editorExecuteErrors; /**@type {HTMLElement}*/ editorExecuteResult; + /**@type {HTMLElement}*/ editorDebugState; /**@type {HTMLInputElement}*/ editorExecuteHide; /**@type {Promise}*/ editorExecutePromise; /**@type {SlashCommandAbortController}*/ abortController; + /**@type {SlashCommandDebugController}*/ debugController; get hasContext() { @@ -298,6 +305,7 @@ export class QuickReply { }); /**@type {HTMLTextAreaElement}*/ const message = dom.querySelector('#qr--modal-message'); + this.editorMessage = message; message.value = this.message; message.addEventListener('input', () => { updateSyntax(); @@ -506,6 +514,9 @@ export class QuickReply { /**@type {HTMLElement}*/ const executeResult = dom.querySelector('#qr--modal-executeResult'); this.editorExecuteResult = executeResult; + /**@type {HTMLElement}*/ + const debugState = dom.querySelector('#qr--modal-debugState'); + this.editorDebugState = debugState; /**@type {HTMLInputElement}*/ const executeHide = dom.querySelector('#qr--modal-executeHide'); this.editorExecuteHide = executeHide; @@ -536,6 +547,22 @@ export class QuickReply { this.abortController?.abort('Stop button clicked'); }); + /**@type {HTMLElement}*/ + const resumeBtn = dom.querySelector('#qr--modal-resume'); + resumeBtn.addEventListener('click', ()=>{ + this.debugController?.resume(); + }); + /**@type {HTMLElement}*/ + const stepBtn = dom.querySelector('#qr--modal-step'); + stepBtn.addEventListener('click', ()=>{ + this.debugController?.step(); + }); + /**@type {HTMLElement}*/ + const stepIntoBtn = dom.querySelector('#qr--modal-stepInto'); + stepIntoBtn.addEventListener('click', ()=>{ + this.debugController?.stepInto(); + }); + await popupResult; window.removeEventListener('resize', resizeListener); @@ -544,6 +571,47 @@ export class QuickReply { } } + getEditorPosition(start, end) { + const inputRect = this.editorMessage.getBoundingClientRect(); + const style = window.getComputedStyle(this.editorMessage); + if (!this.clone) { + this.clone = document.createElement('div'); + for (const key of style) { + this.clone.style[key] = style[key]; + } + this.clone.style.position = 'fixed'; + this.clone.style.visibility = 'hidden'; + document.body.append(this.clone); + const mo = new MutationObserver(muts=>{ + if (muts.find(it=>Array.from(it.removedNodes).includes(this.editorMessage))) { + this.clone.remove(); + } + }); + mo.observe(this.editorMessage.parentElement, { childList:true }); + } + this.clone.style.height = `${inputRect.height}px`; + this.clone.style.left = `${inputRect.left}px`; + this.clone.style.top = `${inputRect.top}px`; + this.clone.style.whiteSpace = style.whiteSpace; + this.clone.style.tabSize = style.tabSize; + const text = this.editorMessage.value; + const before = text.slice(0, start); + this.clone.textContent = before; + const locator = document.createElement('span'); + locator.textContent = text.slice(start, end); + this.clone.append(locator); + this.clone.append(text.slice(end)); + this.clone.scrollTop = this.editorMessage.scrollTop; + this.clone.scrollLeft = this.editorMessage.scrollLeft; + const locatorRect = locator.getBoundingClientRect(); + const location = { + left: locatorRect.left, + right: locatorRect.right, + top: locatorRect.top, + bottom: locatorRect.bottom, + }; + return location; + } async executeFromEditor() { if (this.editorExecutePromise) return; this.editorExecuteBtn.classList.add('qr--busy'); @@ -560,8 +628,44 @@ export class QuickReply { this.editorPopup.dom.classList.add('qr--hide'); } try { - this.editorExecutePromise = this.execute({}, true); - const result = await this.editorExecutePromise; + // this.editorExecutePromise = this.execute({}, true); + // const result = await this.editorExecutePromise; + this.abortController = new SlashCommandAbortController(); + this.debugController = new SlashCommandDebugController(); + this.debugController.onBreakPoint = async(closure, executor)=>{ + const vars = closure.scope.variables; + vars['#pipe'] = closure.scope.pipe; + let v = vars; + let s = closure.scope.parent; + while (s) { + v['#parent'] = s.variables; + v = v['#parent']; + v['#pipe'] = s.pipe; + s = s.parent; + } + this.editorDebugState.textContent = JSON.stringify(closure.scope.variables, (key, val)=>{ + if (val instanceof SlashCommandClosure) return val.toString(); + return val; + }, 2); + this.editorDebugState.classList.add('qr--active'); + const loc = this.getEditorPosition(executor.start - 1, executor.end); + const hi = document.createElement('div'); + hi.style.position = 'fixed'; + hi.style.left = `${loc.left}px`; + hi.style.width = `${loc.right - loc.left}px`; + hi.style.top = `${loc.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)'; + document.body.append(hi); + const isStepping = await this.debugController.awaitContinue(); + hi.remove(); + this.editorDebugState.textContent = ''; + this.editorDebugState.classList.remove('qr--active'); + return isStepping; + }; + const result = await this.onDebug(this); if (this.abortController?.signal?.aborted) { this.editorExecuteProgress.classList.add('qr--aborted'); } else { diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 106084081..55bea527c 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -1,5 +1,6 @@ import { getRequestHeaders, substituteParams } from '../../../../script.js'; import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js'; +import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { debounceAsync, warn } from '../index.js'; import { QuickReply } from './QuickReply.js'; @@ -100,6 +101,26 @@ export class QuickReplySet { + /** + * + * @param {QuickReply} qr + */ + async debug(qr) { + const parser = new SlashCommandParser(); + const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController); + closure.onProgress = (done, total) => qr.updateEditorProgress(done, total); + // closure.abortController = qr.abortController; + // closure.debugController = qr.debugController; + // const stepper = closure.executeGenerator(); + // let step; + // let isStepping = false; + // while (!step?.done) { + // step = await stepper.next(isStepping); + // isStepping = yield(step.value); + // } + // return step.value; + return (await closure.execute())?.pipe; + } /** * * @param {QuickReply} qr The QR to execute. @@ -195,7 +216,12 @@ export class QuickReplySet { return qr; } + /** + * + * @param {QuickReply} qr + */ hookQuickReply(qr) { + qr.onDebug = ()=>this.debug(qr); qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); qr.onDelete = ()=>this.removeQuickReply(qr); qr.onUpdate = ()=>this.save(); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index dfab049d8..9ea971f8e 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -301,19 +301,19 @@ text-align: left; overflow: hidden; } -.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-messageSyntax { display: none; } -.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message { background-color: var(--ac-style-color-background); color: var(--ac-style-color-text); } -.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { color: unset; background-color: rgba(108 171 251 / 0.25); } @supports (color: rgb(from white r g b / 0.25)) { - .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { + .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection { background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); } } @@ -343,12 +343,12 @@ visibility: hidden; cursor: default; } -.dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { color: transparent; background-color: rgba(108 171 251 / 0.25); } @supports (color: rgb(from white r g b / 0.25)) { - .dialogue_popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { + .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::selection { background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25); } } @@ -410,6 +410,10 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { border-color: #d78872; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons { + display: flex; + gap: 1em; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress { --prog: 0; --progColor: #92befc; @@ -469,6 +473,7 @@ overflow: auto; min-width: 100%; width: 0; + white-space: pre-wrap; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult { display: block; @@ -476,6 +481,20 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before { content: 'Result: '; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState { + display: none; + text-align: left; + font-size: smaller; + color: white; + padding: 0.5em; + overflow: auto; + min-width: 100%; + width: 0; + white-space: pre-wrap; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active { + display: block; +} @keyframes qr--progressPulse { 0%, 100% { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 0d6b92a04..221eecb99 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -431,6 +431,10 @@ border-color: rgb(215, 136, 114); } } + #qr--modal-debugButtons { + display: flex; + gap: 1em; + } #qr--modal-executeProgress { --prog: 0; --progColor: rgb(146, 190, 252); @@ -494,6 +498,22 @@ overflow: auto; min-width: 100%; width: 0; + white-space: pre-wrap; + } + #qr--modal-debugState { + display: none; + &.qr--active { + display: block; + } + text-align: left; + font-size: smaller; + // background-color: rgb(146, 190, 252); + color: white; + padding: 0.5em; + overflow: auto; + min-width: 100%; + width: 0; + white-space: pre-wrap; } } } diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 019ab0d45..57b6488d5 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -62,6 +62,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js'; import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; +import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -3004,6 +3005,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress); * @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply * @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution + * @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution * @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events */ @@ -3096,6 +3098,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) { handleExecutionErrors: false, parserFlags: null, abortController: null, + debugController: null, onProgress: null, }, options); @@ -3104,6 +3107,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) { closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController()); closure.scope.parent = options.scope; closure.onProgress = options.onProgress; + closure.debugController = options.debugController; } catch (e) { if (options.handleParserErrors && e instanceof SlashCommandParserError) { /**@type {SlashCommandParserError}*/ diff --git a/public/scripts/slash-commands/SlashCommandBreakPoint.js b/public/scripts/slash-commands/SlashCommandBreakPoint.js new file mode 100644 index 000000000..e29d15838 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandBreakPoint.js @@ -0,0 +1,3 @@ +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; + +export class SlashCommandBreakPoint extends SlashCommandExecutor {} diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 89eda369a..0b0826c05 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -2,8 +2,10 @@ import { substituteParams } from '../../script.js'; import { delay, escapeRegex } from '../utils.js'; import { SlashCommand } from './SlashCommand.js'; import { SlashCommandAbortController } from './SlashCommandAbortController.js'; +import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js'; import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; import { SlashCommandScope } from './SlashCommandScope.js'; @@ -17,6 +19,7 @@ export class SlashCommandClosure { /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = []; /**@type {SlashCommandExecutor[]}*/ executorList = []; /**@type {SlashCommandAbortController}*/ abortController; + /**@type {SlashCommandDebugController}*/ debugController; /**@type {(done:number, total:number)=>void}*/ onProgress; /**@type {string}*/ rawText; @@ -87,6 +90,7 @@ export class SlashCommandClosure { closure.providedArgumentList = this.providedArgumentList; closure.executorList = this.executorList; closure.abortController = this.abortController; + closure.debugController = this.debugController; closure.onProgress = this.onProgress; return closure; } @@ -97,10 +101,29 @@ export class SlashCommandClosure { */ async execute() { const closure = this.getCopy(); - return await closure.executeDirect(); + const gen = closure.executeDirect(); + let step; + while (!step?.done) { + step = await gen.next(this.debugController?.isStepping ?? false); + if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) { + this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor); + } + } + return step.value; } - async executeDirect() { + async * executeGenerator() { + const closure = this.getCopy(); + const gen = closure.executeDirect(); + let step; + while (!step?.done) { + step = await gen.next(this.debugController?.isStepping); + this.debugController.isStepping = yield step.value; + } + return step.value; + } + + async * executeDirect() { // closure arguments for (const arg of this.argumentList) { let v = arg.value; @@ -153,7 +176,7 @@ export class SlashCommandClosure { if (this.executorList.length == 0) { this.scope.pipe = ''; } - for (const executor of this.executorList) { + for (const executor of [] ?? this.executorList) { this.onProgress?.(done, this.commandCount); if (executor instanceof SlashCommandClosureExecutor) { const closure = this.scope.getVariable(executor.name); @@ -258,10 +281,156 @@ export class SlashCommandClosure { } } } + const stepper = this.executeStep(); + let step; + while (!step?.done) { + // get executor before execution + step = await stepper.next(); + if (step.value instanceof SlashCommandBreakPoint) { + console.log('encountered SlashCommandBreakPoint'); + if (this.debugController) { + // "execute" breakpoint + step = await stepper.next(); + // get next executor + step = await stepper.next(); + this.debugController.isStepping = yield { closure:this, executor:step.value }; + } + } else if (!step.done && this.debugController?.isStepping) { + this.debugController.isSteppingInto = false; + this.debugController.isStepping = yield { closure:this, executor:step.value }; + } + // execute executor + step = await stepper.next(); + } + + // if execution has returned a closure result, return that (should only happen on abort) + if (step.value instanceof SlashCommandClosureResult) { + return step.value; + } /**@type {SlashCommandClosureResult} */ const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe }); return result; } + async * executeStep() { + let done = 0; + for (const executor of this.executorList) { + this.onProgress?.(done, this.commandCount); + yield executor; + if (executor instanceof SlashCommandClosureExecutor) { + const closure = this.scope.getVariable(executor.name); + if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`); + closure.scope.parent = this.scope; + closure.providedArgumentList = executor.providedArgumentList; + const result = await closure.execute(); + this.scope.pipe = result.pipe; + } else if (executor instanceof SlashCommandBreakPoint) { + // no execution for breakpoints, just raise counter + done++; + } else { + /**@type {import('./SlashCommand.js').NamedArguments} */ + let args = { + _scope: this.scope, + _parserFlags: executor.parserFlags, + _abortController: this.abortController, + _hasUnnamedArgument: executor.unnamedArgumentList.length > 0, + }; + let value; + // substitute named arguments + for (const arg of executor.namedArgumentList) { + if (arg.value instanceof SlashCommandClosure) { + /**@type {SlashCommandClosure}*/ + const closure = arg.value; + closure.scope.parent = this.scope; + if (closure.executeNow) { + args[arg.name] = (await closure.execute())?.pipe; + } else { + args[arg.name] = closure; + } + } else { + args[arg.name] = this.substituteParams(arg.value); + } + // unescape named argument + if (typeof args[arg.name] == 'string') { + args[arg.name] = args[arg.name] + ?.replace(/\\\{/g, '{') + ?.replace(/\\\}/g, '}') + ; + } + } + + // substitute unnamed argument + if (executor.unnamedArgumentList.length == 0) { + if (executor.injectPipe) { + value = this.scope.pipe; + args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined; + } + } else { + value = []; + for (let i = 0; i < executor.unnamedArgumentList.length; i++) { + let v = executor.unnamedArgumentList[i].value; + if (v instanceof SlashCommandClosure) { + /**@type {SlashCommandClosure}*/ + const closure = v; + closure.scope.parent = this.scope; + if (closure.executeNow) { + v = (await closure.execute())?.pipe; + } else { + v = closure; + } + } else { + v = this.substituteParams(v); + } + value[i] = v; + } + if (!executor.command.splitUnnamedArgument) { + if (value.length == 1) { + value = value[0]; + } else if (!value.find(it=>it instanceof SlashCommandClosure)) { + value = value.join(''); + } + } + } + // unescape unnamed argument + if (typeof value == 'string') { + value = value + ?.replace(/\\\{/g, '{') + ?.replace(/\\\}/g, '}') + ; + } else if (Array.isArray(value)) { + value = value.map(v=>{ + if (typeof v == 'string') { + return v + ?.replace(/\\\{/g, '{') + ?.replace(/\\\}/g, '}'); + } + return v; + }); + } + + let abortResult = await this.testAbortController(); + if (abortResult) { + return abortResult; + } + executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); + const isStepping = this.debugController?.isStepping; + if (this.debugController) { + this.debugController.isStepping = false || this.debugController.isSteppingInto; + } + this.scope.pipe = await executor.command.callback(args, value ?? ''); + if (this.debugController) { + this.debugController.isStepping = isStepping; + } + this.#lintPipe(executor.command); + done += executor.commandCount; + this.onProgress?.(done, this.commandCount); + abortResult = await this.testAbortController(); + if (abortResult) { + return abortResult; + } + } + yield executor; + } + } async testPaused() { while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) { diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js new file mode 100644 index 000000000..54d778ee4 --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -0,0 +1,41 @@ +import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; + +export class SlashCommandDebugController { + /**@type {boolean} */ isStepping = false; + /**@type {boolean} */ isSteppingInto = false; + + /**@type {Promise} */ continuePromise; + /**@type {(boolean)=>void} */ continueResolver; + + /**@type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise} */ onBreakPoint; + + + + resume() { + this.continueResolver?.(false); + this.continuePromise = null; + } + step() { + this.continueResolver?.(true); + this.continuePromise = null; + } + stepInto() { + this.isSteppingInto = true; + this.continueResolver?.(true); + this.continuePromise = null; + } + + async awaitContinue() { + this.continuePromise ??= new Promise(resolve=>{ + this.continueResolver = resolve; + }); + this.isStepping = await this.continuePromise; + return this.isStepping; + } + + async awaitBreakPoint(closure, executor) { + this.isStepping = await this.onBreakPoint(closure, executor); + return this.isStepping; + } +} diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 0e75afebe..a895b028e 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -17,6 +17,8 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js'; +import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; /**@readonly*/ /**@enum {Number}*/ @@ -85,6 +87,7 @@ export class SlashCommandParser { /**@type {string}*/ text; /**@type {number}*/ index; /**@type {SlashCommandAbortController}*/ abortController; + /**@type {SlashCommandDebugController}*/ debugController; /**@type {SlashCommandScope}*/ scope; /**@type {SlashCommandClosure}*/ closure; @@ -560,12 +563,13 @@ export class SlashCommandParser { } - parse(text, verifyCommandNames = true, flags = null, abortController = null) { + parse(text, verifyCommandNames = true, flags = null, abortController = null, debugController = null) { this.verifyCommandNames = verifyCommandNames; for (const key of Object.keys(PARSER_FLAG)) { this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false; } this.abortController = abortController; + this.debugController = debugController; this.text = text; this.index = 0; this.scope = null; @@ -601,6 +605,7 @@ export class SlashCommandParser { const textStart = this.index; let closure = new SlashCommandClosure(this.scope); closure.abortController = this.abortController; + closure.debugController = this.debugController; this.scope = closure.scope; this.closure = closure; this.discardWhitespace(); @@ -619,6 +624,11 @@ export class SlashCommandParser { const cmd = this.parseRunShorthand(); closure.executorList.push(cmd); injectPipe = true; + } else if (this.testBreakPoint()) { + const bp = this.parseBreakPoint(); + if (this.debugController) { + closure.executorList.push(bp); + } } else if (this.testCommand()) { const cmd = this.parseCommand(); cmd.injectPipe = injectPipe; @@ -650,6 +660,17 @@ export class SlashCommandParser { return closure; } + testBreakPoint() { + return this.testSymbol(/\/breakpoint\s*\|/); + } + parseBreakPoint() { + const bp = new SlashCommandBreakPoint(); + bp.start = this.index; + this.take('/breakpoint'.length); + bp.end = this.index; + return bp; + } + testComment() { return this.testSymbol(/\/[/#]/); }