better stepping into other scripts, with source indicator

This commit is contained in:
LenAnderson 2024-07-08 18:07:37 -04:00
parent 67dfe7354b
commit 75317f3eb4
9 changed files with 143 additions and 68 deletions

View File

@ -73,13 +73,14 @@ export class QuickReplyApi {
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {Object} [args] optional arguments
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
*/
async executeQuickReply(setName, label, args = {}) {
async executeQuickReply(setName, label, args = {}, options = {}) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
return await qr.execute(args);
return await qr.execute(args, false, false, options);
}

View File

@ -176,7 +176,7 @@ const init = async () => {
buttons.show();
settings.onSave = ()=>buttons.refresh();
window['executeQuickReplyByName'] = async(name, args = {}) => {
window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
.map(it=>it.set.qrList)
.flat()
@ -191,7 +191,7 @@ const init = async () => {
}
}
if (qr && qr.onExecute) {
return await qr.execute(args, false, true);
return await qr.execute(args, false, true, options);
} else {
throw new Error(`No Quick Reply found for "${name}".`);
}

View File

@ -58,6 +58,8 @@ export class QuickReply {
/**@type {Popup}*/ editorPopup;
/**@type {HTMLElement}*/ editorDom;
/**@type {HTMLTextAreaElement}*/ editorMessage;
/**@type {HTMLElement}*/ editorSyntax;
/**@type {HTMLElement}*/ editorExecuteBtn;
/**@type {HTMLElement}*/ editorExecuteBtnPause;
/**@type {HTMLElement}*/ editorExecuteBtnStop;
@ -500,6 +502,7 @@ export class QuickReply {
message.style.setProperty('text-shadow', 'none', 'important');
/**@type {HTMLElement}*/
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
this.editorSyntax = messageSyntaxInner;
updateSyntax();
updateWrap();
updateTabSize();
@ -720,7 +723,7 @@ export class QuickReply {
}
}
getEditorPosition(start, end) {
getEditorPosition(start, end, message = null) {
const inputRect = this.editorMessage.getBoundingClientRect();
const style = window.getComputedStyle(this.editorMessage);
if (!this.clone) {
@ -745,21 +748,22 @@ export class QuickReply {
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 text = message ?? 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;
this.clone.scrollTop = this.editorSyntax.scrollTop;
this.clone.scrollLeft = this.editorSyntax.scrollLeft;
const locatorRect = locator.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const location = {
left: locatorRect.left,
right: locatorRect.right,
top: locatorRect.top,
bottom: locatorRect.bottom,
left: locatorRect.left - bodyRect.left,
right: locatorRect.right - bodyRect.left,
top: locatorRect.top - bodyRect.top,
bottom: locatorRect.bottom - bodyRect.top,
};
// this.clone.remove();
return location;
@ -821,8 +825,10 @@ export class QuickReply {
//TODO populate debug code from closure.fullText and get locations, highlights, etc. from that
//TODO keep some kind of reference (human identifier) *where* the closure code comes from?
//TODO QR name, chat input, deserialized closure, ... ?
this.editorMessage.value = closure.fullText;
this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
// this.editorMessage.value = closure.fullText;
// this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
const source = closure.source;
this.editorDebugState.innerHTML = '';
let ci = -1;
const varNames = [];
@ -996,19 +1002,21 @@ export class QuickReply {
const title = document.createElement('div'); {
title.classList.add('qr--title');
title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
let hi;
title.addEventListener('pointerenter', ()=>{
const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end);
const layer = syntax.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`;
syntax.append(hi);
});
title.addEventListener('pointerleave', ()=>hi?.remove());
if (c.source == source) {
let hi;
title.addEventListener('pointerenter', ()=>{
const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
const layer = syntax.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`;
syntax.append(hi);
});
title.addEventListener('pointerleave', ()=>hi?.remove());
}
wrap.append(title);
}
for (const key of Object.keys(scope.variables)) {
@ -1128,26 +1136,41 @@ export class QuickReply {
title.textContent = 'Call Stack';
wrap.append(title);
}
let ei = -1;
for (const executor of this.debugController.cmdStack.toReversed()) {
ei++;
const c = this.debugController.stack.toReversed()[ei];
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}`;
if (executor.source == source) {
let hi;
item.addEventListener('pointerenter', ()=>{
const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
const layer = syntax.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`;
syntax.append(hi);
});
item.addEventListener('pointerleave', ()=>hi?.remove());
}
const cmd = document.createElement('div'); {
cmd.classList.add('qr--cmd');
cmd.textContent = `/${executor.name}`;
if (executor.command.name == 'run') {
cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
}
item.append(cmd);
}
const src = document.createElement('div'); {
src.classList.add('qr--source');
const line = closure.fullText.slice(0, executor.start).split('\n').length;
src.textContent = `${executor.source}:${line}`;
item.append(src);
}
let hi;
item.addEventListener('pointerenter', ()=>{
const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end);
const layer = syntax.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`;
syntax.append(hi);
});
item.addEventListener('pointerleave', ()=>hi?.remove());
wrap.append(item);
}
}
@ -1157,7 +1180,7 @@ export class QuickReply {
this.editorDebugState.append(buildVars(closure.scope, true));
this.editorDebugState.append(buildStack());
this.editorDebugState.classList.add('qr--active');
const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end);
const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
const layer = syntax.getBoundingClientRect();
const hi = document.createElement('div');
hi.classList.add('qr--highlight');
@ -1291,7 +1314,7 @@ export class QuickReply {
}
async execute(args = {}, isEditor = false, isRun = false) {
async execute(args = {}, isEditor = false, isRun = false, options = {}) {
if (this.message?.length > 0 && this.onExecute) {
const scope = new SlashCommandScope();
for (const key of Object.keys(args)) {
@ -1303,11 +1326,12 @@ export class QuickReply {
this.abortController = new SlashCommandAbortController();
}
return await this.onExecute(this, {
message:this.message,
message: this.message,
isAutoExecute: args.isAutoExecute ?? false,
isEditor,
isRun,
scope,
executionOptions: options,
});
}
}

View File

@ -108,18 +108,9 @@ export class QuickReplySet {
async debug(qr) {
const parser = new SlashCommandParser();
const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
closure.source = `${this.name}.${qr.label}`;
closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
closure.scope.setMacro('arg::*', '');
// 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;
}
/**
@ -131,6 +122,7 @@ export class QuickReplySet {
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
* @returns
*/
async executeWithOptions(qr, options = {}) {
@ -140,7 +132,9 @@ export class QuickReplySet {
isEditor:false,
isRun:false,
scope:null,
executionOptions:{},
}, options);
const execOptions = options.executionOptions;
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
const finalMessage = options.message ?? qr.message;
@ -158,21 +152,24 @@ export class QuickReplySet {
if (input[0] == '/' && !this.disableSend) {
let result;
if (options.isAutoExecute || options.isRun) {
result = await executeSlashCommandsWithOptions(input, {
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
handleParserErrors: true,
scope: options.scope,
});
source: `${this.name}.${qr.label}`,
}));
} else if (options.isEditor) {
result = await executeSlashCommandsWithOptions(input, {
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
handleParserErrors: false,
scope: options.scope,
abortController: qr.abortController,
source: `${this.name}.${qr.label}`,
onProgress: (done, total) => qr.updateEditorProgress(done, total),
});
}));
} else {
result = await executeSlashCommandsOnChatInput(input, {
result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
scope: options.scope,
});
source: `${this.name}.${qr.label}`,
}));
}
return typeof result === 'object' ? result?.pipe : '';
}

View File

@ -667,6 +667,10 @@
.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 {
display: grid;
grid-template-columns: 1fr 0fr;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
grid-column: 1 / 3;
font-weight: bold;
@ -676,10 +680,17 @@
margin-top: 1em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
display: contents;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
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);
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
opacity: 0.5;
}
@keyframes qr--progressPulse {
0%,

View File

@ -748,6 +748,8 @@
}
.qr--stack {
display: grid;
grid-template-columns: 1fr 0fr;
.qr--title {
grid-column: 1 / 3;
font-weight: bold;
@ -757,10 +759,18 @@
margin-top: 1em;
}
.qr--item {
margin-left: 0.5em;
display: contents;
&:nth-child(2n + 1) {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
.qr--name, .qr--source {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
}
.qr--name {
margin-left: 0.5em;
}
.qr--source {
opacity: 0.5;
}
}
}
}

View File

@ -1824,7 +1824,12 @@ async function runCallback(args, name) {
try {
name = name.trim();
return await window['executeQuickReplyByName'](name, args);
/**@type {ExecuteSlashCommandsOptions} */
const options = {
abortController: args._abortController,
debugController: args._debugController,
};
return await window['executeQuickReplyByName'](name, args, options);
} catch (error) {
throw new Error(`Error running Quick Reply "${name}": ${error.message}`);
}
@ -3372,6 +3377,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
* @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
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
*/
/**
@ -3379,6 +3385,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
*/
/**
@ -3394,6 +3401,7 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
scope: null,
parserFlags: null,
clearChatInput: false,
source: null,
}, options);
isExecutingCommandsFromChatInput = true;
@ -3423,6 +3431,7 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
onProgress: (done, total) => ta.style.setProperty('--prog', `${done / total * 100}%`),
parserFlags: options.parserFlags,
scope: options.scope,
source: options.source,
});
if (commandsFromChatInputAbortController.signal.aborted) {
document.querySelector('#form_sheld').classList.add('script_aborted');
@ -3481,6 +3490,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
abortController: null,
debugController: null,
onProgress: null,
source: null,
}, options);
let closure;
@ -3489,6 +3499,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
closure.scope.parent = options.scope;
closure.onProgress = options.onProgress;
closure.debugController = options.debugController;
closure.source = options.source;
} catch (e) {
if (options.handleParserErrors && e instanceof SlashCommandParserError) {
/**@type {SlashCommandParserError}*/

View File

@ -1,5 +1,5 @@
import { substituteParams } from '../../script.js';
import { delay, escapeRegex } from '../utils.js';
import { delay, escapeRegex, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
@ -27,6 +27,14 @@ export class SlashCommandClosure {
/**@type {string}*/ rawText;
/**@type {string}*/ fullText;
/**@type {string}*/ parserContext;
/**@type {string}*/ #source = uuidv4();
get source() { return this.#source; }
set source(value) {
this.#source = value;
for (const executor of this.executorList) {
executor.source = value;
}
}
/**@type {number}*/
get commandCount() {
@ -114,6 +122,7 @@ export class SlashCommandClosure {
closure.rawText = this.rawText;
closure.fullText = this.fullText;
closure.parserContext = this.parserContext;
closure.source = this.source;
closure.onProgress = this.onProgress;
return closure;
}

View File

@ -1,4 +1,5 @@
// eslint-disable-next-line no-unused-vars
import { uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandClosure } from './SlashCommandClosure.js';
@ -16,6 +17,17 @@ export class SlashCommandExecutor {
/**@type {Number}*/ startUnnamedArgs;
/**@type {Number}*/ endUnnamedArgs;
/**@type {String}*/ name = '';
/**@type {String}*/ #source = uuidv4();
get source() { return this.#source; }
set source(value) {
this.#source = value;
for (const arg of this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
arg.value.source = value;
}
for (const arg of this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
arg.value.source = value;
}
}
/**@type {SlashCommand}*/ command;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];