From 7ebf23e9e6deeaa0507da1e97ae57bc5ee60a27c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 08:41:05 -0400 Subject: [PATCH 001/388] don't hide secondary autocomplete values on select or fully typed --- public/scripts/autocomplete/AutoComplete.js | 2 +- .../slash-commands/SlashCommandAutoCompleteNameResult.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index ba401c68e..24cfd4e59 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -398,7 +398,7 @@ export class AutoComplete { , ); this.result.push(option); - } else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) { + } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) { // only one result that is exactly the current value? just show hint, no autocomplete this.isReplaceable = false; this.isShowingDetails = false; diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index d2c7852e3..d12750491 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -177,7 +177,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { ); const isCompleteValue = enumList.find(it=>it.value == value); const isSelectedValue = isSelect && isCompleteValue; - result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue; + result.isRequired = cmdArg.isRequired && !isSelectedValue; result.forceMatch = cmdArg.forceEnum; return result; } From 8d8a41d91221b2a22980c7bf388f65df00ec584e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 08:43:50 -0400 Subject: [PATCH 002/388] add scope to enumProvider --- .../scripts/slash-commands/SlashCommandArgument.js | 11 ++++++----- .../SlashCommandAutoCompleteNameResult.js | 12 ++++++++---- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandArgument.js b/public/scripts/slash-commands/SlashCommandArgument.js index 324d5b9d6..bdccbe714 100644 --- a/public/scripts/slash-commands/SlashCommandArgument.js +++ b/public/scripts/slash-commands/SlashCommandArgument.js @@ -1,6 +1,7 @@ import { SlashCommandClosure } from './SlashCommandClosure.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; +import { SlashCommandScope } from './SlashCommandScope.js'; @@ -30,7 +31,7 @@ export class SlashCommandArgument { * @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values * @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options + * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options * @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values */ static fromProps(props) { @@ -55,7 +56,7 @@ export class SlashCommandArgument { /**@type {boolean}*/ acceptsMultiple = false; /**@type {string|SlashCommandClosure}*/ defaultValue; /**@type {SlashCommandEnumValue[]}*/ enumList = []; - /**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null; + /**@type {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]}*/ enumProvider = null; /**@type {boolean}*/ forceEnum = true; @@ -64,7 +65,7 @@ export class SlashCommandArgument { * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types * @param {string|SlashCommandClosure} defaultValue * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options + * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options */ constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) { this.description = description; @@ -95,7 +96,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { * @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values * @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options + * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider] function that returns auto complete options * @param {boolean} [props.forceEnum] default: true - whether the input must match one of the enum values */ static fromProps(props) { @@ -126,7 +127,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument { * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types * @param {string|SlashCommandClosure} defaultValue * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums - * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options + * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options * @param {boolean} forceEnum */ constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) { diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index d12750491..cdd7f1a77 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -8,15 +8,18 @@ import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAuto import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js'; +import { SlashCommandScope } from './SlashCommandScope.js'; export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { /**@type {SlashCommandExecutor}*/ executor; + /**@type {SlashCommandScope}*/ scope; /** * @param {SlashCommandExecutor} executor + * @param {SlashCommandScope} scope * @param {Object.} commands */ - constructor(executor, commands) { + constructor(executor, scope, commands) { super( executor.name, executor.start, @@ -29,6 +32,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { ()=>'No slash commands found!', ); this.executor = executor; + this.scope = scope; } getSecondaryNameAt(text, index, isSelect) { @@ -103,7 +107,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { if (name.includes('=') && cmdArg) { // if cursor is already behind "=" check for enums - const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList; + const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; if (cmdArg && enumList?.length) { if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) { return null; @@ -150,7 +154,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { if (idx > -1) { argAssign = this.executor.unnamedArgumentList[idx]; cmdArg = this.executor.command.unnamedArgumentList[idx]; - const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList; + const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; if (cmdArg && enumList.length > 0) { value = argAssign.value.toString().slice(0, index - argAssign.start); start = argAssign.start; @@ -166,7 +170,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { return null; } - const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList; + const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; if (cmdArg == null || enumList.length == 0) return null; const result = new AutoCompleteSecondaryNameResult( diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 8dffa666e..52796e063 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -411,7 +411,7 @@ export class SlashCommandParser { ); return result; } - const result = new SlashCommandAutoCompleteNameResult(executor, this.commands); + const result = new SlashCommandAutoCompleteNameResult(executor, this.scopeIndex[this.commandIndex.indexOf(executor)], this.commands); return result; } return null; From cefb9a10dca95bf46bf47b63fd3291092cb0dd94 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 08:45:04 -0400 Subject: [PATCH 003/388] add autocomplete for multiple unnamed args --- .../SlashCommandAutoCompleteNameResult.js | 10 ++++++++-- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index cdd7f1a77..56dd7535c 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -90,8 +90,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { } } else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) { // cursor is somewhere within the unnamed arguments - //TODO if index is in first array item and that is a string, treat it as an unfinished named arg - if (typeof this.executor.unnamedArgumentList[0].value == 'string') { + // if index is in first array item and that is a string, treat it as an unfinished named arg + if (typeof this.executor.unnamedArgumentList[0]?.value == 'string') { if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) { name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs); start = this.executor.startUnnamedArgs; @@ -154,6 +154,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { if (idx > -1) { argAssign = this.executor.unnamedArgumentList[idx]; cmdArg = this.executor.command.unnamedArgumentList[idx]; + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0].acceptsMultiple) { + cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; + } const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; if (cmdArg && enumList.length > 0) { value = argAssign.value.toString().slice(0, index - argAssign.start); @@ -165,6 +168,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { value = ''; start = index; cmdArg = notProvidedArguments[0]; + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0].acceptsMultiple) { + cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; + } } } else { return null; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 52796e063..fecd695f2 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -757,7 +757,7 @@ export class SlashCommandParser { this.discardWhitespace(); } this.discardWhitespace(); - cmd.startUnnamedArgs = this.index; + cmd.startUnnamedArgs = this.index - /\s(\s*)$/s.exec(this.behind)[1]?.length ?? 0; cmd.endUnnamedArgs = this.index; if (this.testUnnamedArgument()) { cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument); From a9b54672048047e3e09966c97cc94f63b6e1865f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 08:45:18 -0400 Subject: [PATCH 004/388] add enum provider to /add --- public/scripts/variables.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index de8ff390b..145f3f7fc 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1345,13 +1345,23 @@ export function registerVariableCommands() { `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'add', - callback: addValuesCallback, + callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')), returns: 'sum of the provided values', unnamedArgumentList: [ - new SlashCommandArgument( - 'values', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], true, true, - ), + SlashCommandArgument.fromProps({ description: 'values', + typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], + isRequired: true, + acceptsMultiple: true, + enumProvider: (executor, scope)=>[ + ...scope.allVariableNames.map(it=>new SlashCommandEnumValue(it, 'scope', 'variable', 'S')), + ...Object.keys(chat_metadata.variables).map(it=>new SlashCommandEnumValue(it, 'chat', 'qr', 'C')), + ...Object.keys(extension_settings.variables.global).map(it=>new SlashCommandEnumValue(it, 'global', 'enum', 'G')), + new SlashCommandEnumValue('', 'any number or variable name', 'macro', '?'), + ].filter((value, idx, list)=>idx == list.findIndex(it=>it.value == value.value)), + forceEnum: false, + }), ], + splitUnnamedArgument: true, helpString: `
Performs an addition of the set of values and passes the result down the pipe. From 4463a20b35185b7a1e469cdc83b8521a125b3514 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 09:06:53 -0400 Subject: [PATCH 005/388] fix whitespace check --- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index fecd695f2..0e75afebe 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -757,7 +757,7 @@ export class SlashCommandParser { this.discardWhitespace(); } this.discardWhitespace(); - cmd.startUnnamedArgs = this.index - /\s(\s*)$/s.exec(this.behind)[1]?.length ?? 0; + cmd.startUnnamedArgs = this.index - /\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0; cmd.endUnnamedArgs = this.index; if (this.testUnnamedArgument()) { cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument); From 7c98517c27b4d560b508579c8e50db1624dfa858 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 23:15:44 -0400 Subject: [PATCH 006/388] add base class for name results --- .../autocomplete/AutoCompleteNameResult.js | 33 ++----------------- .../AutoCompleteNameResultBase.js | 31 +++++++++++++++++ .../AutoCompleteSecondaryNameResult.js | 4 +-- 3 files changed, 36 insertions(+), 32 deletions(-) create mode 100644 public/scripts/autocomplete/AutoCompleteNameResultBase.js diff --git a/public/scripts/autocomplete/AutoCompleteNameResult.js b/public/scripts/autocomplete/AutoCompleteNameResult.js index 41c19cf9f..f048d6383 100644 --- a/public/scripts/autocomplete/AutoCompleteNameResult.js +++ b/public/scripts/autocomplete/AutoCompleteNameResult.js @@ -1,36 +1,9 @@ -import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js'; -import { AutoCompleteOption } from './AutoCompleteOption.js'; -// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; +import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js'; +import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js'; -export class AutoCompleteNameResult { - /**@type {string} */ name; - /**@type {number} */ start; - /**@type {AutoCompleteOption[]} */ optionList = []; - /**@type {boolean} */ canBeQuoted = false; - /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; - /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; - - - /** - * @param {string} name Name (potentially partial) of the name at the requested index. - * @param {number} start Index where the name starts. - * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. - * @param {boolean} canBeQuoted Whether the name can be inside quotes. - * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. - * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. - */ - constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { - this.name = name; - this.start = start; - this.optionList = optionList; - this.canBeQuoted = canBeQuoted; - this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; - this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; - } - - +export class AutoCompleteNameResult extends AutoCompleteNameResultBase { /** * * @param {string} text The whole text diff --git a/public/scripts/autocomplete/AutoCompleteNameResultBase.js b/public/scripts/autocomplete/AutoCompleteNameResultBase.js new file mode 100644 index 000000000..150ee68c5 --- /dev/null +++ b/public/scripts/autocomplete/AutoCompleteNameResultBase.js @@ -0,0 +1,31 @@ +import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js'; +import { AutoCompleteOption } from './AutoCompleteOption.js'; + + + +export class AutoCompleteNameResultBase { + /**@type {string} */ name; + /**@type {number} */ start; + /**@type {AutoCompleteOption[]} */ optionList = []; + /**@type {boolean} */ canBeQuoted = false; + /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`; + /**@type {()=>string} */ makeNoOptionsText = ()=>'No options'; + + + /** + * @param {string} name Name (potentially partial) of the name at the requested index. + * @param {number} start Index where the name starts. + * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope. + * @param {boolean} canBeQuoted Whether the name can be inside quotes. + * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found. + * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against. + */ + constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) { + this.name = name; + this.start = start; + this.optionList = optionList; + this.canBeQuoted = canBeQuoted; + this.noMatchText = makeNoMatchText ?? this.makeNoMatchText; + this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText; + } +} diff --git a/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js b/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js index 63eccf99f..e0e65fc7c 100644 --- a/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js +++ b/public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js @@ -1,6 +1,6 @@ -import { AutoCompleteNameResult } from './AutoCompleteNameResult.js'; +import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js'; -export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult { +export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase { /**@type {boolean}*/ isRequired = false; /**@type {boolean}*/ forceMatch = true; } From eb02ca95f9135f0a5b6c1462403677d0ba38bc65 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 18 Jun 2024 14:29:29 -0400 Subject: [PATCH 007/388] 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(/\/[/#]/); } From 05c24f6d31c61a919b61d7368affaeca8ace05d7 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 18 Jun 2024 21:51:08 -0400 Subject: [PATCH 008/388] add default value to unnamed args --- public/scripts/slash-commands/SlashCommand.js | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index d04934924..48ae78ee1 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -298,40 +298,52 @@ export class SlashCommand { for (const arg of unnamedArguments) { const listItem = document.createElement('li'); { listItem.classList.add('argumentItem'); - const argItem = document.createElement('div'); { - argItem.classList.add('argument'); - argItem.classList.add('unnamedArgument'); - argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`; - if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional'); - if (arg.acceptsMultiple) argItem.classList.add('multiple'); - if (arg.enumList.length > 0) { - const enums = document.createElement('span'); { - enums.classList.add('argument-enums'); - enums.title = `${argItem.title} - accepted values`; - for (const e of arg.enumList) { - const enumItem = document.createElement('span'); { - enumItem.classList.add('argument-enum'); - enumItem.textContent = e.value; - enums.append(enumItem); + const argSpec = document.createElement('div'); { + argSpec.classList.add('argumentSpec'); + const argItem = document.createElement('div'); { + argItem.classList.add('argument'); + argItem.classList.add('unnamedArgument'); + argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`; + if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional'); + if (arg.acceptsMultiple) argItem.classList.add('multiple'); + if (arg.enumList.length > 0) { + const enums = document.createElement('span'); { + enums.classList.add('argument-enums'); + enums.title = `${argItem.title} - accepted values`; + for (const e of arg.enumList) { + const enumItem = document.createElement('span'); { + enumItem.classList.add('argument-enum'); + enumItem.textContent = e.value; + enums.append(enumItem); + } } + argItem.append(enums); + } + } else { + const types = document.createElement('span'); { + types.classList.add('argument-types'); + types.title = `${argItem.title} - accepted types`; + for (const t of arg.typeList) { + const type = document.createElement('span'); { + type.classList.add('argument-type'); + type.textContent = t; + types.append(type); + } + } + argItem.append(types); } - argItem.append(enums); } - } else { - const types = document.createElement('span'); { - types.classList.add('argument-types'); - types.title = `${argItem.title} - accepted types`; - for (const t of arg.typeList) { - const type = document.createElement('span'); { - type.classList.add('argument-type'); - type.textContent = t; - types.append(type); - } - } - argItem.append(types); + argSpec.append(argItem); + } + if (arg.defaultValue !== null) { + const argDefault = document.createElement('div'); { + argDefault.classList.add('argument-default'); + argDefault.title = 'default value'; + argDefault.textContent = arg.defaultValue.toString(); + argSpec.append(argDefault); } } - listItem.append(argItem); + listItem.append(argSpec); } const desc = document.createElement('div'); { desc.classList.add('argument-description'); From 76bacfe219d29ef234cd41710fdf6a1f89f2ea37 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 19 Jun 2024 09:45:22 -0400 Subject: [PATCH 009/388] fix for no arg --- .../slash-commands/SlashCommandAutoCompleteNameResult.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index 56dd7535c..6c00c275c 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -154,7 +154,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { if (idx > -1) { argAssign = this.executor.unnamedArgumentList[idx]; cmdArg = this.executor.command.unnamedArgumentList[idx]; - if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0].acceptsMultiple) { + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) { cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; } const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList; @@ -168,7 +168,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { value = ''; start = index; cmdArg = notProvidedArguments[0]; - if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0].acceptsMultiple) { + if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) { cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0]; } } From d6ee84dd6be5ea63d8b00f0f4347dbf82d7529db Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 19 Jun 2024 09:45:30 -0400 Subject: [PATCH 010/388] fixes --- public/scripts/extensions/quick-reply/src/QuickReply.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 09894ae36..fc3d90319 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -645,20 +645,22 @@ export class QuickReply { } this.editorDebugState.textContent = JSON.stringify(closure.scope.variables, (key, val)=>{ if (val instanceof SlashCommandClosure) return val.toString(); + if (val === undefined) return null; return val; }, 2); this.editorDebugState.classList.add('qr--active'); const loc = this.getEditorPosition(executor.start - 1, executor.end); + const layer = this.editorPopup.dlg.getBoundingClientRect(); const hi = document.createElement('div'); hi.style.position = 'fixed'; - hi.style.left = `${loc.left}px`; + hi.style.left = `${loc.left - layer.left}px`; hi.style.width = `${loc.right - loc.left}px`; - hi.style.top = `${loc.top}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)'; - document.body.append(hi); + this.editorPopup.dlg.append(hi); const isStepping = await this.debugController.awaitContinue(); hi.remove(); this.editorDebugState.textContent = ''; From 6ff1d6a9b008ba9c5493320bf73b3cb4029127f3 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 08:54:37 -0400 Subject: [PATCH 011/388] fix firefox and selectionchange issues --- public/scripts/autocomplete/AutoComplete.js | 45 +++++++++------------ 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 57f348edf..1d3da947f 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -102,10 +102,15 @@ export class AutoComplete { this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10); this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10); - textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced)); + textarea.addEventListener('input', ()=>{ + this.selectionStart = this.textarea.selectionStart; + if (this.text != this.textarea.value) this.show(true, this.wasForced); + }); textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt)); - textarea.addEventListener('click', ()=>this.isActive ? this.show() : null); - textarea.addEventListener('selectionchange', ()=>this.show()); + textarea.addEventListener('click', ()=>{ + this.selectionStart = this.textarea.selectionStart; + if (this.isActive) this.show(); + }); textarea.addEventListener('blur', ()=>this.hide()); if (isFloating) { textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced()); @@ -768,30 +773,16 @@ export class AutoComplete { // ignore keydown on modifier keys return; } - switch (evt.key) { - case 'ArrowUp': - case 'ArrowDown': - case 'ArrowRight': - case 'ArrowLeft': { - if (this.isActive) { - // keyboard navigation, wait for keyup to complete cursor move - const oldText = this.textarea.value; - await new Promise(resolve=>{ - window.addEventListener('keyup', resolve, { once:true }); - }); - if (this.selectionStart != this.textarea.selectionStart) { - this.selectionStart = this.textarea.selectionStart; - this.show(this.isReplaceable || oldText != this.textarea.value); - } - } - break; - } - default: { - if (this.isActive) { - this.text != this.textarea.value && this.show(this.isReplaceable); - } - break; - } + // await keyup to see if cursor position or text has changed + const oldText = this.textarea.value; + await new Promise(resolve=>{ + window.addEventListener('keyup', resolve, { once:true }); + }); + if (this.selectionStart != this.textarea.selectionStart) { + this.selectionStart = this.textarea.selectionStart; + this.show(this.isReplaceable || oldText != this.textarea.value); + } else if (this.isActive) { + this.text != this.textarea.value && this.show(this.isReplaceable); } } } From 996268e6b3b851fdf68b31cacd66a5e43d4a0eee Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 09:12:43 -0400 Subject: [PATCH 012/388] add matchProvider and valueProvider to autocomplete options --- public/scripts/autocomplete/AutoComplete.js | 12 +++++++++--- public/scripts/autocomplete/AutoCompleteOption.js | 6 +++++- .../SlashCommandEnumAutoCompleteOption.js | 2 +- .../scripts/slash-commands/SlashCommandEnumValue.js | 9 ++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 1d3da947f..34d1a452e 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -194,6 +194,11 @@ export class AutoComplete { * @returns The option. */ fuzzyScore(option) { + // might have been matched by the options matchProvider function instead + if (!this.fuzzyRegex.test(option.name)) { + option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1); + return option; + } const parts = this.fuzzyRegex.exec(option.name).slice(1, -1); let start = null; let consecutive = []; @@ -344,7 +349,7 @@ export class AutoComplete { this.result = this.effectiveParserResult.optionList // filter the list of options by the partial name according to the matching type - .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name) + .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name) // remove aliases .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx); @@ -362,10 +367,11 @@ export class AutoComplete { // build element option.dom = this.makeItem(option); // update replacer and add quotes if necessary + const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name; if (this.effectiveParserResult.canBeQuoted) { - option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`; + option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName}"` : `${optionName}`; } else { - option.replacer = option.name; + option.replacer = optionName; } // calculate fuzzy score if matching is fuzzy if (this.matchType == 'fuzzy') this.fuzzyScore(option); diff --git a/public/scripts/autocomplete/AutoCompleteOption.js b/public/scripts/autocomplete/AutoCompleteOption.js index 7a12d74b2..e371b0951 100644 --- a/public/scripts/autocomplete/AutoCompleteOption.js +++ b/public/scripts/autocomplete/AutoCompleteOption.js @@ -11,6 +11,8 @@ export class AutoCompleteOption { /**@type {AutoCompleteFuzzyScore}*/ score; /**@type {string}*/ replacer; /**@type {HTMLElement}*/ dom; + /**@type {(input:string)=>boolean}*/ matchProvider; + /**@type {(input:string)=>string}*/ valueProvider; /** @@ -25,10 +27,12 @@ export class AutoCompleteOption { /** * @param {string} name */ - constructor(name, typeIcon = ' ', type = '') { + constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null) { this.name = name; this.typeIcon = typeIcon; this.type = type; + this.matchProvider = matchProvider; + this.valueProvider = valueProvider; } diff --git a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js index 748454b11..fe387e8ca 100644 --- a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js +++ b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js @@ -13,7 +13,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption { * @param {SlashCommandEnumValue} enumValue */ constructor(cmd, enumValue) { - super(enumValue.value, enumValue.typeIcon, enumValue.type); + super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider); this.cmd = cmd; this.enumValue = enumValue; } diff --git a/public/scripts/slash-commands/SlashCommandEnumValue.js b/public/scripts/slash-commands/SlashCommandEnumValue.js index 1fa610c56..dd658069a 100644 --- a/public/scripts/slash-commands/SlashCommandEnumValue.js +++ b/public/scripts/slash-commands/SlashCommandEnumValue.js @@ -1,14 +1,21 @@ +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; +import { SlashCommandScope } from './SlashCommandScope.js'; + export class SlashCommandEnumValue { /**@type {string}*/ value; /**@type {string}*/ description; /**@type {string}*/ type = 'enum'; /**@type {string}*/ typeIcon = '◊'; + /**@type {(input:string)=>boolean}*/ matchProvider; + /**@type {(input:string)=>string}*/ valueProvider; - constructor(value, description = null, type = 'enum', typeIcon = '◊') { + constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider, valueProvider) { this.value = value; this.description = description; this.type = type; this.typeIcon = typeIcon; + this.matchProvider = matchProvider; + this.valueProvider = valueProvider; } toString() { From 2b3627bb00ae1b26ee3bfbbff86b86e1dd85b5a2 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 09:13:07 -0400 Subject: [PATCH 013/388] coalesce vars in enumProvider --- public/scripts/variables.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 145f3f7fc..51a12c5a9 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1354,9 +1354,8 @@ export function registerVariableCommands() { acceptsMultiple: true, enumProvider: (executor, scope)=>[ ...scope.allVariableNames.map(it=>new SlashCommandEnumValue(it, 'scope', 'variable', 'S')), - ...Object.keys(chat_metadata.variables).map(it=>new SlashCommandEnumValue(it, 'chat', 'qr', 'C')), - ...Object.keys(extension_settings.variables.global).map(it=>new SlashCommandEnumValue(it, 'global', 'enum', 'G')), - new SlashCommandEnumValue('', 'any number or variable name', 'macro', '?'), + ...Object.keys(chat_metadata.variables ?? {}).map(it=>new SlashCommandEnumValue(it, 'chat', 'qr', 'C')), + ...Object.keys(extension_settings.variables?.global ?? {}).map(it=>new SlashCommandEnumValue(it, 'global', 'enum', 'G')), ].filter((value, idx, list)=>idx == list.findIndex(it=>it.value == value.value)), forceEnum: false, }), From 02e1ef76062586820891c792571035fe1fe09f17 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 09:13:30 -0400 Subject: [PATCH 014/388] use matchProvider and valueProvider in /add arguments --- public/scripts/variables.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 51a12c5a9..796012cf6 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1356,6 +1356,8 @@ export function registerVariableCommands() { ...scope.allVariableNames.map(it=>new SlashCommandEnumValue(it, 'scope', 'variable', 'S')), ...Object.keys(chat_metadata.variables ?? {}).map(it=>new SlashCommandEnumValue(it, 'chat', 'qr', 'C')), ...Object.keys(extension_settings.variables?.global ?? {}).map(it=>new SlashCommandEnumValue(it, 'global', 'enum', 'G')), + new SlashCommandEnumValue('', 'any number', 'macro', '?', (input)=>/^\d*$/.test(input), (input)=>input), + new SlashCommandEnumValue(' ', 'any variable name', 'macro', '?', (input)=>/^\w*$/.test(input), (input)=>input), ].filter((value, idx, list)=>idx == list.findIndex(it=>it.value == value.value)), forceEnum: false, }), From 538724739be3df0c4de91e7c2680f3a9ceb6f0fa Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 13:06:58 -0400 Subject: [PATCH 015/388] debugger stuff --- .../extensions/quick-reply/src/QuickReply.js | 145 ++++++++++++++++-- .../scripts/extensions/quick-reply/style.css | 62 +++++++- .../scripts/extensions/quick-reply/style.less | 61 +++++++- .../slash-commands/SlashCommandClosure.js | 5 +- .../SlashCommandDebugController.js | 11 ++ 5 files changed, 264 insertions(+), 20 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 0b034d385..83fa7acce 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -54,6 +54,7 @@ export class QuickReply { /**@type {HTMLTextAreaElement}*/ settingsDomMessage; /**@type {Popup}*/ editorPopup; + /**@type {HTMLElement}*/ editorDom; /**@type {HTMLElement}*/ editorExecuteBtn; /**@type {HTMLElement}*/ editorExecuteBtnPause; @@ -215,6 +216,7 @@ export class QuickReply { /**@type {HTMLElement} */ // @ts-ignore const dom = this.template.cloneNode(true); + this.editorDom = dom; this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); const popupResult = this.editorPopup.show(); @@ -614,6 +616,7 @@ export class QuickReply { } async executeFromEditor() { if (this.editorExecutePromise) return; + this.editorDom.classList.add('qr--isExecuting'); this.editorExecuteBtn.classList.add('qr--busy'); this.editorExecuteProgress.style.setProperty('--prog', '0'); this.editorExecuteErrors.classList.remove('qr--hasErrors'); @@ -628,26 +631,133 @@ export class QuickReply { this.editorPopup.dlg.classList.add('qr--hide'); } try { - // 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(); - if (val === undefined) return null; - return val; - }, 2); + this.editorDebugState.innerHTML = ''; + let ci = -1; + const varNames = []; + const macroNames = []; + /** + * @param {SlashCommandScope} scope + */ + const buildVars = (scope, isCurrent = false)=>{ + if (!isCurrent) { + ci--; + } + const c = this.debugController.stack.slice(ci)[0]; + const wrap = document.createElement('div'); { + wrap.classList.add('qr--scope'); + 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(c.executorList[0].start, c.executorList.slice(-1)[0].end); + const layer = this.editorPopup.dlg.getBoundingClientRect(); + hi = document.createElement('div'); + hi.style.position = 'fixed'; + 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()); + wrap.append(title); + } + for (const key of Object.keys(scope.variables)) { + const isHidden = varNames.includes(key); + if (!isHidden) varNames.push(key); + const item = document.createElement('div'); { + item.classList.add('qr--var'); + if (isHidden) item.classList.add('qr--isHidden'); + const k = document.createElement('div'); { + k.classList.add('qr--key'); + k.textContent = key; + item.append(k); + } + const v = document.createElement('div'); { + v.classList.add('qr--val'); + const val = scope.variables[key]; + if (val instanceof SlashCommandClosure) { + v.classList.add('qr--closure'); + v.title = val.rawText; + v.textContent = val.toString(); + } else if (val === undefined) { + v.classList.add('qr--undefined'); + v.textContent = 'undefined'; + } else { + v.textContent = val; + } + item.append(v); + } + wrap.append(item); + } + } + for (const key of Object.keys(scope.macros)) { + const isHidden = macroNames.includes(key); + if (!isHidden) macroNames.push(key); + const item = document.createElement('div'); { + item.classList.add('qr--macro'); + if (isHidden) item.classList.add('qr--isHidden'); + const k = document.createElement('div'); { + k.classList.add('qr--key'); + k.textContent = key; + item.append(k); + } + const v = document.createElement('div'); { + v.classList.add('qr--val'); + const val = scope.macros[key]; + if (val instanceof SlashCommandClosure) { + v.classList.add('qr--closure'); + v.title = val.rawText; + v.textContent = val.toString(); + } else if (val === undefined) { + v.classList.add('qr--undefined'); + v.textContent = 'undefined'; + } else { + v.textContent = val; + } + item.append(v); + } + wrap.append(item); + } + } + const pipeItem = document.createElement('div'); { + pipeItem.classList.add('qr--pipe'); + const k = document.createElement('div'); { + k.classList.add('qr--key'); + k.textContent = 'pipe'; + pipeItem.append(k); + } + const v = document.createElement('div'); { + v.classList.add('qr--val'); + const val = scope.pipe; + if (val instanceof SlashCommandClosure) { + v.classList.add('qr--closure'); + v.title = val.rawText; + v.textContent = val.toString(); + } else if (val === undefined) { + v.classList.add('qr--undefined'); + v.textContent = 'undefined'; + } else { + v.textContent = val; + } + pipeItem.append(v); + } + wrap.append(pipeItem); + } + if (scope.parent) { + wrap.append(buildVars(scope.parent)); + } + } + return wrap; + }; + this.editorDebugState.append(buildVars(closure.scope, true)); this.editorDebugState.classList.add('qr--active'); const loc = this.getEditorPosition(executor.start - 1, executor.end); const layer = this.editorPopup.dlg.getBoundingClientRect(); @@ -695,6 +805,7 @@ export class QuickReply { this.editorExecutePromise = null; this.editorExecuteBtn.classList.remove('qr--busy'); this.editorPopup.dlg.classList.remove('qr--hide'); + this.editorDom.classList.remove('qr--isExecuting'); } updateEditorProgress(done, total) { diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 95f81396e..fa6e423de 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -238,6 +238,9 @@ .popup:has(#qr--modalEditor) { aspect-ratio: unset; } +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls { + display: none; +} .popup:has(#qr--modalEditor) .popup-content { display: flex; flex-direction: column; @@ -249,6 +252,16 @@ gap: 1em; overflow: hidden; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3, +.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--main { flex: 1 1 auto; display: flex; @@ -485,8 +498,9 @@ display: none; text-align: left; font-size: smaller; + font-family: var(--monoFontFamily); color: white; - padding: 0.5em; + padding: 0.5em 0; overflow: auto; min-width: 100%; width: 0; @@ -495,6 +509,52 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active { display: block; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope { + display: grid; + grid-template-columns: 0fr 1fr; + column-gap: 1em; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title { + grid-column: 1 / 3; + font-weight: bold; + background-color: var(--black50a); + padding: 0.25em; + margin-top: 0.5em; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro, +.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.qr--isHidden .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val { + opacity: 0.5; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key { + margin-left: 0.5em; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after { + content: ": "; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before { + content: "{{"; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after { + content: "}}: "; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope { + grid-column: 1 / 3; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val { + opacity: 0.5; +} @keyframes qr--progressPulse { 0%, 100% { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 8c7a78ec3..527dca2d6 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -262,6 +262,10 @@ .popup:has(#qr--modalEditor) { aspect-ratio: unset; + &:has(.qr--isExecuting) .popup-controls { + display: none; + } + .popup-content { display: flex; flex-direction: column; @@ -273,6 +277,21 @@ gap: 1em; overflow: hidden; + &.qr--isExecuting { + #qr--main > h3:first-child, + #qr--main > .qr--labels, + #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings, + #qr--qrOptions > h3:first-child, + #qr--qrOptions > #qr--ctxEditor, + #qr--qrOptions > .qr--ctxEditorActions, + #qr--qrOptions > .qr--ctxEditorActions + h3, + #qr--qrOptions > .qr--ctxEditorActions + h3 + div + { + display: none; + } + + } + > #qr--main { flex: 1 1 auto; display: flex; @@ -507,13 +526,53 @@ } text-align: left; font-size: smaller; + font-family: var(--monoFontFamily); // background-color: rgb(146, 190, 252); color: white; - padding: 0.5em; + padding: 0.5em 0; overflow: auto; min-width: 100%; width: 0; white-space: pre-wrap; + + .qr--scope { + display: grid; + grid-template-columns: 0fr 1fr; + column-gap: 1em; + .qr--title { + grid-column: 1 / 3; + font-weight: bold; + background-color: var(--black50a); + padding: 0.25em; + margin-top: 0.5em; + } + .qr--var, .qr--macro, .qr--pipe { + display: contents; + &.qr--isHidden { + .qr--key, .qr--val { + opacity: 0.5; + } + } + } + .qr--key { + margin-left: 0.5em; + &:after { content: ": "; } + } + .qr--pipe, .qr--macro { + > .qr--key { + &:before { content: "{{"; } + &:after { content: "}}: "; } + } + } + .qr--scope { + grid-column: 1 / 3; + .qr--pipe { + .qr--key, .qr--val { + opacity: 0.5; + } + } + } + } } } } diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 0b0826c05..4a342076f 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -97,7 +97,7 @@ export class SlashCommandClosure { /** * - * @returns Promise + * @returns {Promise} */ async execute() { const closure = this.getCopy(); @@ -124,6 +124,7 @@ export class SlashCommandClosure { } async * executeDirect() { + this.debugController?.down(this); // closure arguments for (const arg of this.argumentList) { let v = arg.value; @@ -305,10 +306,12 @@ export class SlashCommandClosure { // if execution has returned a closure result, return that (should only happen on abort) if (step.value instanceof SlashCommandClosureResult) { + this.debugController?.up(); return step.value; } /**@type {SlashCommandClosureResult} */ const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe }); + this.debugController?.up(); return result; } async * executeStep() { diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js index 54d778ee4..af4df2614 100644 --- a/public/scripts/slash-commands/SlashCommandDebugController.js +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -2,6 +2,7 @@ import { SlashCommandClosure } from './SlashCommandClosure.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; export class SlashCommandDebugController { + /**@type {SlashCommandClosure[]} */ stack = []; /**@type {boolean} */ isStepping = false; /**@type {boolean} */ isSteppingInto = false; @@ -12,6 +13,16 @@ export class SlashCommandDebugController { + + down(closure) { + this.stack.push(closure); + } + up() { + this.stack.pop(); + } + + + resume() { this.continueResolver?.(false); this.continuePromise = null; From 9b3cd719d789d45b6c666dce6ddb9982dcaff62e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 15:52:08 -0400 Subject: [PATCH 016/388] track index in getvar replacement --- .../scripts/slash-commands/SlashCommandParser.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index a895b028e..2b90c2625 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -514,11 +514,14 @@ export class SlashCommandParser { } replaceGetvar(value) { - return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => { + return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (match, cmd, name, idx) => { name = name.trim(); + const startIdx = this.index - value.length + idx; + const endIdx = this.index - value.length + idx + match.length; // store pipe const pipeName = `_PARSER_${uuidv4()}`; - const storePipe = new SlashCommandExecutor(null); { + const storePipe = new SlashCommandExecutor(startIdx); { + storePipe.end = endIdx; storePipe.command = this.commands['let']; storePipe.name = 'let'; const nameAss = new SlashCommandUnnamedArgumentAssignment(); @@ -529,7 +532,8 @@ export class SlashCommandParser { this.closure.executorList.push(storePipe); } // getvar / getglobalvar - const getvar = new SlashCommandExecutor(null); { + const getvar = new SlashCommandExecutor(startIdx); { + getvar.end = endIdx; getvar.command = this.commands[cmd]; getvar.name = 'cmd'; const nameAss = new SlashCommandUnnamedArgumentAssignment(); @@ -539,7 +543,8 @@ export class SlashCommandParser { } // set to temp scoped var const varName = `_PARSER_${uuidv4()}`; - const setvar = new SlashCommandExecutor(null); { + const setvar = new SlashCommandExecutor(startIdx); { + setvar.end = endIdx; setvar.command = this.commands['let']; setvar.name = 'let'; const nameAss = new SlashCommandUnnamedArgumentAssignment(); @@ -550,7 +555,8 @@ export class SlashCommandParser { this.closure.executorList.push(setvar); } // return pipe - const returnPipe = new SlashCommandExecutor(null); { + const returnPipe = new SlashCommandExecutor(startIdx); { + returnPipe.end = endIdx; returnPipe.command = this.commands['return']; returnPipe.name = 'return'; const varAss = new SlashCommandUnnamedArgumentAssignment(); From ed8f923b7af0d0bcd8237d44401e8a97f3b85dd4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 15:52:26 -0400 Subject: [PATCH 017/388] add /breakpoint to command list and block it --- public/scripts/slash-commands/SlashCommandParser.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 2b90c2625..ad176b80f 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -51,7 +51,7 @@ export class SlashCommandParser { * @param {SlashCommand} command */ static addCommandObject(command) { - const reserved = ['/', '#', ':', 'parser-flag']; + const reserved = ['/', '#', ':', 'parser-flag', 'breakpoint']; for (const start of reserved) { if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) { throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`); @@ -153,6 +153,11 @@ export class SlashCommandParser { helpString: 'Write a comment.', })); } + if (!Object.keys(this.commands).includes('breakpoint')) { + SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'breakpoint', + helpString: 'Set a breakpoint for debugging in the QR Editor.', + })); + } //TODO should not be re-registered from every instance this.registerLanguage(); From 3b6f4dee2c020d7b436fb7a3adc11aff3896b175 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 20 Jun 2024 15:53:30 -0400 Subject: [PATCH 018/388] debugger --- .../extensions/quick-reply/html/qrEditor.html | 26 ++-- .../extensions/quick-reply/src/QuickReply.js | 7 +- .../scripts/extensions/quick-reply/style.css | 31 ++++- .../scripts/extensions/quick-reply/style.less | 30 ++++- .../slash-commands/SlashCommandClosure.js | 113 +----------------- .../SlashCommandDebugController.js | 22 ++++ 6 files changed, 103 insertions(+), 126 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index d7dd85b00..80aab2386 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -116,17 +116,6 @@ -
- - - -
+ +
+ + + + +
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 83fa7acce..746e98af6 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -564,6 +564,11 @@ export class QuickReply { stepIntoBtn.addEventListener('click', ()=>{ this.debugController?.stepInto(); }); + /**@type {HTMLElement}*/ + const stepOutBtn = dom.querySelector('#qr--modal-stepOut'); + stepOutBtn.addEventListener('click', ()=>{ + this.debugController?.stepOut(); + }); await popupResult; @@ -653,7 +658,7 @@ export class QuickReply { title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; let hi; title.addEventListener('pointerenter', ()=>{ - const loc = this.getEditorPosition(c.executorList[0].start, c.executorList.slice(-1)[0].end); + const loc = this.getEditorPosition(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'; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index fa6e423de..9098bcbf9 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -262,6 +262,9 @@ .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--modal-debugButtons { + display: flex; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main { flex: 1 1 auto; display: flex; @@ -376,6 +379,9 @@ border: 1px solid var(--SmartThemeBorderColor); border-radius: 5px; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor label:has(#qr--modal-executeHide) { + display: none; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons { display: flex; gap: 1em; @@ -424,9 +430,21 @@ border-color: #d78872; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons { - display: flex; + display: none; gap: 1em; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton { + aspect-ratio: 1.25 / 1; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton.qr--glyph-combo { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton.qr--glyph-combo .qr--glyph { + grid-column: 1; + line-height: 0.8; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress { --prog: 0; --progColor: #92befc; @@ -512,7 +530,7 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope { display: grid; grid-template-columns: 0fr 1fr; - column-gap: 1em; + column-gap: 0em; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title { grid-column: 1 / 3; @@ -526,6 +544,14 @@ .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 { + 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, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key, @@ -536,6 +562,7 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key { margin-left: 0.5em; + padding-right: 1em; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after { content: ": "; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 527dca2d6..a63fb4229 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -289,7 +289,9 @@ { display: none; } - + #qr--modal-debugButtons { + display: flex; + } } > #qr--main { @@ -403,6 +405,10 @@ } } + label:has(#qr--modal-executeHide) { + // hide editor is not working anyways + display: none; + } #qr--modal-executeButtons { display: flex; gap: 1em; @@ -451,8 +457,20 @@ } } #qr--modal-debugButtons { - display: flex; + display: none; gap: 1em; + .qr--modal-debugButton { + aspect-ratio: 1.25 / 1; + &.qr--glyph-combo { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + .qr--glyph { + grid-column: 1; + line-height: 0.8; + } + } + } } #qr--modal-executeProgress { --prog: 0; @@ -538,7 +556,7 @@ .qr--scope { display: grid; grid-template-columns: 0fr 1fr; - column-gap: 1em; + column-gap: 0em; .qr--title { grid-column: 1 / 3; font-weight: bold; @@ -548,6 +566,11 @@ } .qr--var, .qr--macro, .qr--pipe { display: contents; + &:nth-child(2n + 2) { + .qr--key, .qr--val { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); + } + } &.qr--isHidden { .qr--key, .qr--val { opacity: 0.5; @@ -556,6 +579,7 @@ } .qr--key { margin-left: 0.5em; + padding-right: 1em; &:after { content: ": "; } } .qr--pipe, .qr--macro { diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 4a342076f..80902e60c 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -104,7 +104,7 @@ export class SlashCommandClosure { const gen = closure.executeDirect(); let step; while (!step?.done) { - step = await gen.next(this.debugController?.isStepping ?? false); + step = await gen.next(this.debugController?.testStepping(this) ?? false); if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) { this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor); } @@ -117,7 +117,7 @@ export class SlashCommandClosure { const gen = closure.executeDirect(); let step; while (!step?.done) { - step = await gen.next(this.debugController?.isStepping); + step = await gen.next(this.debugController?.testStepping(this)); this.debugController.isStepping = yield step.value; } return step.value; @@ -177,111 +177,6 @@ export class SlashCommandClosure { if (this.executorList.length == 0) { this.scope.pipe = ''; } - for (const executor of [] ?? this.executorList) { - this.onProgress?.(done, this.commandCount); - 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 { - /**@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); - this.scope.pipe = await executor.command.callback(args, value ?? ''); - this.#lintPipe(executor.command); - done += executor.commandCount; - this.onProgress?.(done, this.commandCount); - abortResult = await this.testAbortController(); - if (abortResult) { - return abortResult; - } - } - } const stepper = this.executeStep(); let step; while (!step?.done) { @@ -296,7 +191,7 @@ export class SlashCommandClosure { step = await stepper.next(); this.debugController.isStepping = yield { closure:this, executor:step.value }; } - } else if (!step.done && this.debugController?.isStepping) { + } else if (!step.done && this.debugController?.testStepping(this)) { this.debugController.isSteppingInto = false; this.debugController.isStepping = yield { closure:this, executor:step.value }; } @@ -415,7 +310,7 @@ export class SlashCommandClosure { return abortResult; } executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); - const isStepping = this.debugController?.isStepping; + const isStepping = this.debugController?.testStepping(this); if (this.debugController) { this.debugController.isStepping = false || this.debugController.isSteppingInto; } diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js index af4df2614..fa47e3b9b 100644 --- a/public/scripts/slash-commands/SlashCommandDebugController.js +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -3,8 +3,10 @@ import { SlashCommandExecutor } from './SlashCommandExecutor.js'; export class SlashCommandDebugController { /**@type {SlashCommandClosure[]} */ stack = []; + /**@type {boolean[]} */ stepStack = []; /**@type {boolean} */ isStepping = false; /**@type {boolean} */ isSteppingInto = false; + /**@type {boolean} */ isSteppingOut = false; /**@type {Promise} */ continuePromise; /**@type {(boolean)=>void} */ continueResolver; @@ -14,11 +16,22 @@ export class SlashCommandDebugController { + testStepping(closure) { + return this.stepStack[this.stack.indexOf(closure)]; + } + + + + down(closure) { this.stack.push(closure); + if (this.stepStack.length < this.stack.length) { + this.stepStack.push(this.isSteppingInto); + } } up() { this.stack.pop(); + this.stepStack.pop(); } @@ -26,16 +39,25 @@ export class SlashCommandDebugController { resume() { this.continueResolver?.(false); this.continuePromise = null; + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = false); } step() { + this.stepStack[this.stepStack.length - 1] = true; this.continueResolver?.(true); this.continuePromise = null; } stepInto() { this.isSteppingInto = true; + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true); this.continueResolver?.(true); this.continuePromise = null; } + stepOut() { + this.isSteppingOut = true; + this.stepStack[this.stepStack.length - 1] = false; + this.continueResolver?.(false); + this.continuePromise = null; + } async awaitContinue() { this.continuePromise ??= new Promise(resolve=>{ From e964a106121529c84e3056b7737d9edcd5caff3c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 22 Jun 2024 10:44:34 -0400 Subject: [PATCH 019/388] 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() { From 9bcfb9ab2650f9710a949cd0135ff1eb4997e69e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 22 Jun 2024 15:22:51 -0400 Subject: [PATCH 020/388] debugger --- public/scripts/extensions/quick-reply/style.css | 4 +++- public/scripts/extensions/quick-reply/style.less | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index f122dd927..eb8abc9d3 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -565,6 +565,7 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title { grid-column: 1 / 3; font-weight: bold; + font-family: var(--mainFontFamily); background-color: var(--black50a); padding: 0.25em; margin-top: 0.5em; @@ -606,7 +607,7 @@ content: "}}: "; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope { - grid-column: 1 / 3; + display: contents; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val { @@ -615,6 +616,7 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title { grid-column: 1 / 3; font-weight: bold; + font-family: var(--mainFontFamily); background-color: var(--black50a); padding: 0.25em; margin-top: 1em; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index a44e42b1d..09ad2a00e 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -593,6 +593,7 @@ .qr--title { grid-column: 1 / 3; font-weight: bold; + font-family: var(--mainFontFamily); background-color: var(--black50a); padding: 0.25em; margin-top: 0.5em; @@ -622,7 +623,7 @@ } } .qr--scope { - grid-column: 1 / 3; + display: contents; .qr--pipe { .qr--key, .qr--val { opacity: 0.5; @@ -635,6 +636,7 @@ .qr--title { grid-column: 1 / 3; font-weight: bold; + font-family: var(--mainFontFamily); background-color: var(--black50a); padding: 0.25em; margin-top: 1em; From 9ae0591e3f2c6fdf6b296454644153b96ee752ae Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:30:34 -0400 Subject: [PATCH 021/388] indicate pipe, var and getvar command in replaceGetvar --- public/scripts/slash-commands/SlashCommandParser.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index ad176b80f..b07a4f2bf 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -524,7 +524,7 @@ export class SlashCommandParser { const startIdx = this.index - value.length + idx; const endIdx = this.index - value.length + idx + match.length; // store pipe - const pipeName = `_PARSER_${uuidv4()}`; + const pipeName = `_PARSER_PIPE_${uuidv4()}`; const storePipe = new SlashCommandExecutor(startIdx); { storePipe.end = endIdx; storePipe.command = this.commands['let']; @@ -540,14 +540,14 @@ export class SlashCommandParser { const getvar = new SlashCommandExecutor(startIdx); { getvar.end = endIdx; getvar.command = this.commands[cmd]; - getvar.name = 'cmd'; + getvar.name = cmd; const nameAss = new SlashCommandUnnamedArgumentAssignment(); nameAss.value = name; getvar.unnamedArgumentList = [nameAss]; this.closure.executorList.push(getvar); } // set to temp scoped var - const varName = `_PARSER_${uuidv4()}`; + const varName = `_PARSER_VAR_${uuidv4()}`; const setvar = new SlashCommandExecutor(startIdx); { setvar.end = endIdx; setvar.command = this.commands['let']; From 5c5c4ae91adc3eac0d6858b64a34534e75df99f4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:30:54 -0400 Subject: [PATCH 022/388] indicate immediate closure in toString --- public/scripts/slash-commands/SlashCommandClosure.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index d624e69c0..c79be25cb 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -33,7 +33,7 @@ export class SlashCommandClosure { } toString() { - return '[Closure]'; + return `[Closure]${this.executeNow ? '()' : ''}`; } /** From ef5d4e394b93621c0e022d21314e309ad1826ab4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:31:07 -0400 Subject: [PATCH 023/388] debugger --- .../extensions/quick-reply/src/QuickReply.js | 165 +++++++++++++++++- .../scripts/extensions/quick-reply/style.css | 53 +++++- .../scripts/extensions/quick-reply/style.less | 46 ++++- .../slash-commands/SlashCommandClosure.js | 28 ++- .../SlashCommandDebugController.js | 3 + 5 files changed, 286 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index fe6da8bc4..70f3f1fbf 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -618,14 +618,15 @@ export class QuickReply { } 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))) { + if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) { this.clone.remove(); + this.clone = null; } }); - mo.observe(this.editorMessage.parentElement, { childList:true }); + mo.observe(document.body, { childList:true }); } + document.body.append(this.clone); this.clone.style.width = `${inputRect.width}px`; this.clone.style.height = `${inputRect.height}px`; this.clone.style.left = `${inputRect.left}px`; @@ -648,6 +649,7 @@ export class QuickReply { top: locatorRect.top, bottom: locatorRect.bottom, }; + // this.clone.remove(); return location; } async executeFromEditor() { @@ -689,6 +691,157 @@ export class QuickReply { const c = this.debugController.stack.slice(ci)[0]; const wrap = document.createElement('div'); { wrap.classList.add('qr--scope'); + if (isCurrent) { + const executor = this.debugController.cmdStack.slice(-1)[0]; + { + const namedTitle = document.createElement('div'); { + namedTitle.classList.add('qr--title'); + namedTitle.textContent = `Named Args - /${executor.name}`; + if (executor.command.name == 'run') { + namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; + } + wrap.append(namedTitle); + } + const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]); + for (const key of keys) { + if (key[0] == '_') continue; + const item = document.createElement('div'); { + item.classList.add('qr--var'); + const k = document.createElement('div'); { + k.classList.add('qr--key'); + k.textContent = key; + item.append(k); + } + const vUnresolved = document.createElement('div'); { + vUnresolved.classList.add('qr--val'); + vUnresolved.classList.add('qr--singleCol'); + const val = executor.namedArgumentList.find(it=>it.name == key)?.value; + if (val instanceof SlashCommandClosure) { + vUnresolved.classList.add('qr--closure'); + vUnresolved.title = val.rawText; + vUnresolved.textContent = val.toString(); + } else if (val === undefined) { + vUnresolved.classList.add('qr--undefined'); + vUnresolved.textContent = 'undefined'; + } else { + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); + } else { + vUnresolved.textContent = val; + vUnresolved.classList.add('qr--simple'); + } + } + item.append(vUnresolved); + } + const vResolved = document.createElement('div'); { + vResolved.classList.add('qr--val'); + vResolved.classList.add('qr--singleCol'); + const val = this.debugController.namedArguments[key]; + if (val instanceof SlashCommandClosure) { + vResolved.classList.add('qr--closure'); + vResolved.title = val.rawText; + vResolved.textContent = val.toString(); + } else if (val === undefined) { + vResolved.classList.add('qr--undefined'); + vResolved.textContent = 'undefined'; + } else { + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + vResolved.textContent = JSON.stringify(jsonVal, null, 2); + } else { + vResolved.textContent = val; + vResolved.classList.add('qr--simple'); + } + } + item.append(vResolved); + } + wrap.append(item); + } + } + } + { + const unnamedTitle = document.createElement('div'); { + unnamedTitle.classList.add('qr--title'); + unnamedTitle.textContent = `Unnamed Args - /${executor.name}`; + if (executor.command.name == 'run') { + unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; + } + wrap.append(unnamedTitle); + } + let i = 0; + let unnamed = this.debugController.unnamedArguments ?? []; + if (!Array.isArray(unnamed)) unnamed = [unnamed]; + while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); + unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); + for (const arg of unnamed) { + i++; + const item = document.createElement('div'); { + item.classList.add('qr--var'); + const k = document.createElement('div'); { + k.classList.add('qr--key'); + k.textContent = i.toString(); + item.append(k); + } + const vUnresolved = document.createElement('div'); { + vUnresolved.classList.add('qr--val'); + vUnresolved.classList.add('qr--singleCol'); + const val = arg[0]?.value; + if (val instanceof SlashCommandClosure) { + vUnresolved.classList.add('qr--closure'); + vUnresolved.title = val.rawText; + vUnresolved.textContent = val.toString(); + } else if (val === undefined) { + vUnresolved.classList.add('qr--undefined'); + vUnresolved.textContent = 'undefined'; + } else { + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); + } else { + vUnresolved.textContent = val; + vUnresolved.classList.add('qr--simple'); + } + } + item.append(vUnresolved); + } + const vResolved = document.createElement('div'); { + vResolved.classList.add('qr--val'); + vResolved.classList.add('qr--singleCol'); + if (this.debugController.unnamedArguments === undefined) { + vResolved.classList.add('qr--unresolved'); + } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) { + // do nothing + } else { + const val = arg[1]; + if (val instanceof SlashCommandClosure) { + vResolved.classList.add('qr--closure'); + vResolved.title = val.rawText; + vResolved.textContent = val.toString(); + } else if (val === undefined) { + vResolved.classList.add('qr--undefined'); + vResolved.textContent = 'undefined'; + } else { + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + vResolved.textContent = JSON.stringify(jsonVal, null, 2); + } else { + vResolved.textContent = val; + vResolved.classList.add('qr--simple'); + } + } + } + item.append(vResolved); + } + wrap.append(item); + } + } + } + } const title = document.createElement('div'); { title.classList.add('qr--title'); title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; @@ -735,6 +888,7 @@ export class QuickReply { v.textContent = JSON.stringify(jsonVal, null, 2); } else { v.textContent = val; + v.classList.add('qr--simple'); } } item.append(v); @@ -770,6 +924,7 @@ export class QuickReply { v.textContent = JSON.stringify(jsonVal, null, 2); } else { v.textContent = val; + v.classList.add('qr--simple'); } } item.append(v); @@ -801,6 +956,7 @@ export class QuickReply { v.textContent = JSON.stringify(jsonVal, null, 2); } else { v.textContent = val; + v.classList.add('qr--simple'); } } pipeItem.append(v); @@ -854,6 +1010,9 @@ export class QuickReply { const layer = this.editorPopup.dlg.getBoundingClientRect(); const hi = document.createElement('div'); hi.classList.add('qr--highlight'); + if (this.debugController.namedArguments === undefined) { + hi.classList.add('qr--unresolved'); + } hi.style.left = `${loc.left - layer.left}px`; hi.style.width = `${loc.right - loc.left}px`; hi.style.top = `${loc.top - layer.top}px`; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index eb8abc9d3..7a3455fd3 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -245,6 +245,9 @@ position: fixed; z-index: 50000; pointer-events: none; + background-color: rgba(47, 150, 180, 0.5); +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved { background-color: rgba(255, 255, 0, 0.5); } .popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary { @@ -559,11 +562,11 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope { display: grid; - grid-template-columns: 0fr 1fr; + grid-template-columns: 0fr 1fr 1fr; column-gap: 0em; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title { - grid-column: 1 / 3; + grid-column: 1 / 4; font-weight: bold; font-family: var(--mainFontFamily); background-color: var(--black50a); @@ -583,6 +586,26 @@ .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:nth-child(2n + 1) .qr--val:nth-child(2n), +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n), +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n), +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n), +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key, .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key, @@ -591,6 +614,32 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val { opacity: 0.5; } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val { + grid-column: 2 / 4; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol { + grid-column: unset; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after { + content: '"'; + color: var(--SmartThemeQuoteColor); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after { + content: '-UNRESOLVED-'; + font-style: italic; + color: var(--SmartThemeQuoteColor); +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key { margin-left: 0.5em; padding-right: 1em; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 09ad2a00e..f66206265 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -271,7 +271,10 @@ position: fixed; z-index: 50000; pointer-events: none; - background-color: rgba(255, 255, 0, 0.5); + background-color: rgb(47 150 180 / 0.5); + &.qr--unresolved { + background-color: rgb(255 255 0 / 0.5); + } } .qr--highlight-secondary { position: fixed; @@ -588,10 +591,10 @@ .qr--scope { display: grid; - grid-template-columns: 0fr 1fr; + grid-template-columns: 0fr 1fr 1fr; column-gap: 0em; .qr--title { - grid-column: 1 / 3; + grid-column: 1 / 4; font-weight: bold; font-family: var(--mainFontFamily); background-color: var(--black50a); @@ -604,12 +607,49 @@ .qr--key, .qr--val { background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25); } + .qr--val { + &:nth-child(2n) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125); + } + &:hover { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); + } + } + } + &:nth-child(2n) { + .qr--val { + &:nth-child(2n) { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625); + } + &:hover { + background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5); + } + } } &.qr--isHidden { .qr--key, .qr--val { opacity: 0.5; } } + .qr--val { + grid-column: 2 / 4; + &.qr--singleCol { + grid-column: unset; + } + &.qr--simple { + &:before, &:after { + content: '"'; + color: var(--SmartThemeQuoteColor); + } + } + &.qr--unresolved { + &:after { + content: '-UNRESOLVED-'; + font-style: italic; + color: var(--SmartThemeQuoteColor); + } + } + } } .qr--key { margin-left: 0.5em; diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index c79be25cb..fff437d1d 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -187,11 +187,29 @@ export class SlashCommandClosure { if (this.debugController) { // "execute" breakpoint step = await stepper.next(); + step = await stepper.next(); // get next executor step = await stepper.next(); - this.debugController.isStepping = yield { closure:this, executor:step.value }; + const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.executeNow); + const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.executeNow); + if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { + this.debugController.isStepping = yield { closure:this, executor:step.value }; + } else { + this.debugController.isStepping = true; + this.debugController.stepStack[this.debugController.stepStack.length - 1] = true; + } } } else if (!step.done && this.debugController?.testStepping(this)) { + this.debugController.isSteppingInto = false; + const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { + this.debugController.isStepping = yield { closure:this, executor:step.value }; + } + } + // resolve args + step = await stepper.next(); + if (!step.done && this.debugController?.testStepping(this)) { this.debugController.isSteppingInto = false; this.debugController.isStepping = yield { closure:this, executor:step.value }; } @@ -225,6 +243,7 @@ export class SlashCommandClosure { } else if (executor instanceof SlashCommandBreakPoint) { // no execution for breakpoints, just raise counter done++; + yield executor; } else { /**@type {import('./SlashCommand.js').NamedArguments} */ let args = { @@ -310,6 +329,11 @@ export class SlashCommandClosure { if (abortResult) { return abortResult; } + if (this.debugController) { + this.debugController.namedArguments = args; + this.debugController.unnamedArguments = value ?? ''; + } + yield executor; executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount); const isStepping = this.debugController?.testStepping(this); if (this.debugController) { @@ -317,6 +341,8 @@ export class SlashCommandClosure { } this.scope.pipe = await executor.command.callback(args, value ?? ''); if (this.debugController) { + this.debugController.namedArguments = undefined; + this.debugController.unnamedArguments = undefined; this.debugController.isStepping = isStepping; } this.#lintPipe(executor.command); diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js index f6ae407c2..11754a8a6 100644 --- a/public/scripts/slash-commands/SlashCommandDebugController.js +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -9,6 +9,9 @@ export class SlashCommandDebugController { /**@type {boolean} */ isSteppingInto = false; /**@type {boolean} */ isSteppingOut = false; + /**@type {object} */ namedArguments; + /**@type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments; + /**@type {Promise} */ continuePromise; /**@type {(boolean)=>void} */ continueResolver; From 0506451ee6413aaabf3d6c9d253531108bf08bc7 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:41:45 -0400 Subject: [PATCH 024/388] add missing semicolon --- .../scripts/slash-commands/SlashCommandCommonEnumsProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index 683f28ffa..ef2b62e6b 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -143,7 +143,7 @@ export const commonEnumProviders = { ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny - ] + ]; }, /** From d61fbc39925dcaec2822a3c7c3243e43a7dc173c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:51:12 -0400 Subject: [PATCH 025/388] add scoped vars and fix hiding (scope->local->global) --- .../slash-commands/SlashCommandCommonEnumsProvider.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js index ef2b62e6b..7305a5c82 100644 --- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js +++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js @@ -6,6 +6,7 @@ import { searchCharByName, getTagsList, tags } from "../tags.js"; import { SlashCommandClosure } from "./SlashCommandClosure.js"; import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js"; import { SlashCommandExecutor } from "./SlashCommandExecutor.js"; +import { SlashCommandScope } from "./SlashCommandScope.js"; /** * A collection of regularly used enum icons @@ -134,16 +135,16 @@ export const commonEnumProviders = { * Can be filtered by `type` to only show global or local variables * * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'. - * @returns {() => SlashCommandEnumValue[]} + * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]} */ - variables: (...type) => () => { + variables: (...type) => (executor, scope) => { const types = type.flat(); const isAll = types.includes('all'); return [ - ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], + ...isAll || types.includes('scope') ? scope.allVariableNames.map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], - ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny - ]; + ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], + ].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value)); }, /** From ab5a6b1c6196c4214a3d94609a6ad042f254658d Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 11:51:26 -0400 Subject: [PATCH 026/388] fix /add multi unnamed --- public/scripts/variables.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 3117f07ec..c0de0f4f1 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1448,7 +1448,7 @@ export function registerVariableCommands() { })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'add', - callback: addValuesCallback, + callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')), returns: 'sum of the provided values', unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -1460,6 +1460,7 @@ export function registerVariableCommands() { forceEnum: false, }), ], + splitUnnamedArgument: true, helpString: `
Performs an addition of the set of values and passes the result down the pipe. From 5862c7ea91208cb8845a3a0226710b2042e24020 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 12:07:24 -0400 Subject: [PATCH 027/388] don't block enter/tab for items with valueProvider --- public/scripts/autocomplete/AutoComplete.js | 2 ++ public/scripts/autocomplete/AutoCompleteOption.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index bd5ae9da9..521a8c7e1 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -721,6 +721,7 @@ export class AutoComplete { // pick the selected item to autocomplete if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; if (this.selectedItem.name == this.name) break; + if (!this.selectedItem.isSelectable) break; evt.preventDefault(); evt.stopImmediatePropagation(); this.select(); @@ -729,6 +730,7 @@ export class AutoComplete { case 'Tab': { // pick the selected item to autocomplete if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; + if (!this.selectedItem.isSelectable) break; evt.preventDefault(); evt.stopImmediatePropagation(); this.select(); diff --git a/public/scripts/autocomplete/AutoCompleteOption.js b/public/scripts/autocomplete/AutoCompleteOption.js index 621fe368a..a946a462f 100644 --- a/public/scripts/autocomplete/AutoCompleteOption.js +++ b/public/scripts/autocomplete/AutoCompleteOption.js @@ -23,6 +23,10 @@ export class AutoCompleteOption { return this.name; } + get isSelectable() { + return !this.valueProvider; + } + /** * @param {string} name From a7f74f038736b722694182e4fb0bb31b1c278269 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 12:07:45 -0400 Subject: [PATCH 028/388] add any var / any number to /add enumProvider --- public/scripts/variables.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index c0de0f4f1..7be0d99aa 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -6,8 +6,8 @@ import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortC import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; -import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; -import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; +import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; import { isFalseBoolean } from './utils.js'; @@ -1456,7 +1456,28 @@ export function registerVariableCommands() { typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME], isRequired: true, acceptsMultiple: true, - enumProvider: commonEnumProviders.variables('all'), + enumProvider: (executor, scope)=>{ + const vars = commonEnumProviders.variables('all')(executor, scope); + vars.push( + new SlashCommandEnumValue( + 'any variable name', + null, + enumTypes.variable, + enumIcons.variable, + (input)=>/^\w*$/.test(input), + (input)=>input, + ), + new SlashCommandEnumValue( + 'any number', + null, + enumTypes.number, + enumIcons.number, + (input)=>input == '' || !Number.isNaN(Number(input)), + (input)=>input, + ), + ); + return vars; + }, forceEnum: false, }), ], From bc40ee084d4c1e68118e0c95c153e915c15bd787 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 14:19:10 -0400 Subject: [PATCH 029/388] debugger --- .../extensions/quick-reply/src/QuickReply.js | 44 +++++++++++++++---- .../scripts/extensions/quick-reply/style.css | 4 +- .../scripts/extensions/quick-reply/style.less | 4 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 70f3f1fbf..5396efbf0 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -673,6 +673,33 @@ export class QuickReply { if (this.editorExecuteHide.checked) { this.editorPopup.dlg.classList.add('qr--hide'); } + const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner'); + const updateScroll = (evt) => { + let left = syntax.scrollLeft; + let top = syntax.scrollTop; + if (evt) { + evt.preventDefault(); + left = syntax.scrollLeft + evt.deltaX; + top = syntax.scrollTop + evt.deltaY; + syntax.scrollTo({ + behavior: 'instant', + left, + top, + }); + } + this.editorMessage.scrollTo({ + behavior: 'instant', + left, + top, + }); + }; + const updateScrollDebounced = updateScroll; + syntax.addEventListener('wheel', (evt)=>{ + updateScrollDebounced(evt); + }); + syntax.addEventListener('scroll', (evt)=>{ + updateScrollDebounced(); + }); try { this.abortController = new SlashCommandAbortController(); this.debugController = new SlashCommandDebugController(); @@ -693,7 +720,7 @@ export class QuickReply { wrap.classList.add('qr--scope'); if (isCurrent) { const executor = this.debugController.cmdStack.slice(-1)[0]; - { + { // named args const namedTitle = document.createElement('div'); { namedTitle.classList.add('qr--title'); namedTitle.textContent = `Named Args - /${executor.name}`; @@ -762,7 +789,7 @@ export class QuickReply { } } } - { + { // unnamed args const unnamedTitle = document.createElement('div'); { unnamedTitle.classList.add('qr--title'); unnamedTitle.textContent = `Unnamed Args - /${executor.name}`; @@ -842,20 +869,21 @@ export class QuickReply { } } } + // current scope 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 = this.editorPopup.dlg.getBoundingClientRect(); + 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`; - this.editorPopup.dlg.append(hi); + syntax.append(hi); }); title.addEventListener('pointerleave', ()=>hi?.remove()); wrap.append(title); @@ -987,14 +1015,14 @@ export class QuickReply { let hi; item.addEventListener('pointerenter', ()=>{ const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end); - const layer = this.editorPopup.dlg.getBoundingClientRect(); + 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`; - this.editorPopup.dlg.append(hi); + syntax.append(hi); }); item.addEventListener('pointerleave', ()=>hi?.remove()); wrap.append(item); @@ -1007,7 +1035,7 @@ export class QuickReply { this.editorDebugState.append(buildStack()); this.editorDebugState.classList.add('qr--active'); const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end); - const layer = this.editorPopup.dlg.getBoundingClientRect(); + const layer = syntax.getBoundingClientRect(); const hi = document.createElement('div'); hi.classList.add('qr--highlight'); if (this.debugController.namedArguments === undefined) { @@ -1017,7 +1045,7 @@ export class QuickReply { 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); + syntax.append(hi); const isStepping = await this.debugController.awaitContinue(); hi.remove(); this.editorDebugState.textContent = ''; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 7a3455fd3..caa7e1e66 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -242,7 +242,7 @@ display: none; } .popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight { - position: fixed; + position: absolute; z-index: 50000; pointer-events: none; background-color: rgba(47, 150, 180, 0.5); @@ -251,7 +251,7 @@ background-color: rgba(255, 255, 0, 0.5); } .popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary { - position: fixed; + position: absolute; z-index: 50000; pointer-events: none; border: 3px solid red; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index f66206265..d75c7a8e8 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -268,7 +268,7 @@ } .qr--highlight { - position: fixed; + position: absolute; z-index: 50000; pointer-events: none; background-color: rgb(47 150 180 / 0.5); @@ -277,7 +277,7 @@ } } .qr--highlight-secondary { - position: fixed; + position: absolute; z-index: 50000; pointer-events: none; border: 3px solid red; From 13496cfb3ae4d4b083680db99be5b09829988dea Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 14:26:57 -0400 Subject: [PATCH 030/388] trim start of first and end of lat value part in unsplit unnamed arg (analog to pure text arg) --- public/scripts/slash-commands/SlashCommandParser.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index e76a6598a..fd780a716 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -904,6 +904,14 @@ export class SlashCommandParser { listValues.push(assignment); } if (isList) { + const firstVal = listValues[0]; + if (typeof firstVal.value == 'string') { + firstVal.value = firstVal.value.trimStart(); + } + const lastVal = listValues.slice(-1)[0]; + if (typeof lastVal.value == 'string') { + lastVal.value = lastVal.value.trimEnd(); + } return listValues; } this.indexMacros(this.index - value.length, value); From 31a67a973a0d262be9802359dee2fe06e6e08d4e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 14:27:10 -0400 Subject: [PATCH 031/388] only remove if exists --- public/scripts/extensions/quick-reply/src/QuickReply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 5396efbf0..e9a65e3ac 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -620,7 +620,7 @@ export class QuickReply { this.clone.style.visibility = 'hidden'; const mo = new MutationObserver(muts=>{ if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) { - this.clone.remove(); + this.clone?.remove(); this.clone = null; } }); From 7cdc4c5713939e5e025bc618a148c946cb1f4711 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 14:27:18 -0400 Subject: [PATCH 032/388] debugger --- public/scripts/extensions/quick-reply/style.css | 2 +- public/scripts/extensions/quick-reply/style.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index caa7e1e66..0ec2504e0 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -405,11 +405,11 @@ font-family: var(--monoFontFamily); padding: 0.75em; margin: 0; - border: none; resize: none; line-height: 1.2; border: 1px solid var(--SmartThemeBorderColor); border-radius: 5px; + position: relative; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor label:has(#qr--modal-executeHide) { display: none; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index d75c7a8e8..5b33e350e 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -430,11 +430,11 @@ font-family: var(--monoFontFamily); padding: 0.75em; margin: 0; - border: none; resize: none; line-height: 1.2; border: 1px solid var(--SmartThemeBorderColor); border-radius: 5px; + position: relative; } } } From 00652cce0a445f004d67c5c12a3901a11dbdff26 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 15:16:55 -0400 Subject: [PATCH 033/388] add ctrl+click breakpoints --- .../extensions/quick-reply/html/qrEditor.html | 1 + .../extensions/quick-reply/src/QuickReply.js | 68 +++++++++++++++++++ .../slash-commands/SlashCommandParser.js | 3 +- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index c92a12487..4e780c6af 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -33,6 +33,7 @@ Syntax highlight + Ctrl+Click to set / remove breakpoints
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index e9a65e3ac..0995dab82 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,10 +1,12 @@ import { POPUP_TYPE, Popup } from '../../../popup.js'; import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; +import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.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 { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { debounce, getSortableDelay } from '../../../utils.js'; @@ -374,6 +376,72 @@ export class QuickReply { message.addEventListener('scroll', (evt)=>{ updateScrollDebounced(); }); + message.addEventListener('pointerup', async(evt)=>{ + if (!evt.ctrlKey) return; + const selIdx = message.selectionStart; + const parser = new SlashCommandParser(); + parser.parse(message.value, false); + const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= message.selectionStart && it.end >= message.selectionStart); + if (cmdIdx > -1) { + const cmd = parser.commandIndex[cmdIdx]; + if (cmd instanceof SlashCommandBreakPoint) { + const bp = cmd; + // start at -1 because "/" is not included in start-end + let start = bp.start - 1; + // step left until forward slash "/" + while (message.value[start] != '/') start--; + // step left while whitespace (except newline) before start + while (/[^\S\n]/.test(message.value[start - 1])) start--; + // if newline before indent, include the newline for removal + if (message.value[start - 1] == '\n') start--; + let end = bp.end; + // step right while whitespace + while (/\s/.test(message.value[end])) end++; + // if pipe after whitepace, include pipe for removal + if (message.value[end ] == '|') end++; + const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; + message.value = v; + message.dispatchEvent(new Event('input', { bubbles:true })); + } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { + const bp = parser.commandIndex[cmdIdx - 1]; + // start at -1 because "/" is not included in start-end + let start = bp.start - 1; + // step left until forward slash "/" + while (message.value[start] != '/') start--; + // step left while whitespace (except newline) before start + while (/[^\S\n]/.test(message.value[start - 1])) start--; + // if newline before indent, include the newline for removal + if (message.value[start - 1] == '\n') start--; + let end = bp.end; + // step right while whitespace + while (/\s/.test(message.value[end])) end++; + // if pipe after whitepace, include pipe for removal + if (message.value[end] == '|') end++; + const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; + message.value = v; + message.dispatchEvent(new Event('input', { bubbles:true })); + } 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 })); + } + } + }); /** @type {any} */ const resizeListener = debounce((evt) => { updateSyntax(); diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index fd780a716..5e91c6881 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -680,9 +680,10 @@ export class SlashCommandParser { } parseBreakPoint() { const bp = new SlashCommandBreakPoint(); - bp.start = this.index; + bp.start = this.index + 1; this.take('/breakpoint'.length); bp.end = this.index; + this.commandIndex.push(bp); return bp; } From ca703042486d0f5f9a9af852d91645274850bbf4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 15:21:13 -0400 Subject: [PATCH 034/388] fix step while inside subscope --- public/scripts/slash-commands/SlashCommandDebugController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandDebugController.js b/public/scripts/slash-commands/SlashCommandDebugController.js index 11754a8a6..16969746a 100644 --- a/public/scripts/slash-commands/SlashCommandDebugController.js +++ b/public/scripts/slash-commands/SlashCommandDebugController.js @@ -51,7 +51,7 @@ export class SlashCommandDebugController { this.stepStack.forEach((_,idx)=>this.stepStack[idx] = false); } step() { - this.stepStack[this.stepStack.length - 1] = true; + this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true); this.continueResolver?.(true); this.continuePromise = null; } From ca0843152cf0a5ab131feb989367c2caeadb0896 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 15:21:20 -0400 Subject: [PATCH 035/388] highlight /breakpoint --- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 5e91c6881..3110dc4cd 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -214,7 +214,7 @@ export class SlashCommandParser { }; const ABORT = { scope: 'abort', - begin: /\/abort/, + begin: /\/(abort|breakpoint)/, end: /\||$|:}/, contains: [], }; From 7c7fa08d02f4a9bb325c4dc9dcced39caae93004 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 15:31:39 -0400 Subject: [PATCH 036/388] fix immediate closure check --- public/scripts/slash-commands/SlashCommandClosure.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index fff437d1d..219d1e817 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -190,8 +190,8 @@ export class SlashCommandClosure { step = await stepper.next(); // get next executor step = await stepper.next(); - const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.executeNow); - const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.executeNow); + const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { this.debugController.isStepping = yield { closure:this, executor:step.value }; } else { From a69d4147cbb9bb075208dbf8eca52affd51e43b4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 17:11:13 -0400 Subject: [PATCH 037/388] debugger --- .../extensions/quick-reply/src/QuickReply.js | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 0995dab82..d6a507a39 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -376,9 +376,26 @@ export class QuickReply { message.addEventListener('scroll', (evt)=>{ updateScrollDebounced(); }); + const removeBreakpoint = (bp)=>{ + // start at -1 because "/" is not included in start-end + let start = bp.start - 1; + // step left until forward slash "/" + while (message.value[start] != '/') start--; + // step left while whitespace (except newline) before start + while (/[^\S\n]/.test(message.value[start - 1])) start--; + // if newline before indent, include the newline for removal + if (message.value[start - 1] == '\n') start--; + let end = bp.end; + // step right while whitespace + while (/\s/.test(message.value[end])) end++; + // if pipe after whitepace, include pipe for removal + if (message.value[end] == '|') end++; + const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; + message.value = v; + message.dispatchEvent(new Event('input', { bubbles:true })); + }; message.addEventListener('pointerup', async(evt)=>{ if (!evt.ctrlKey) return; - const selIdx = message.selectionStart; const parser = new SlashCommandParser(); parser.parse(message.value, false); const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= message.selectionStart && it.end >= message.selectionStart); @@ -386,40 +403,10 @@ export class QuickReply { const cmd = parser.commandIndex[cmdIdx]; if (cmd instanceof SlashCommandBreakPoint) { const bp = cmd; - // start at -1 because "/" is not included in start-end - let start = bp.start - 1; - // step left until forward slash "/" - while (message.value[start] != '/') start--; - // step left while whitespace (except newline) before start - while (/[^\S\n]/.test(message.value[start - 1])) start--; - // if newline before indent, include the newline for removal - if (message.value[start - 1] == '\n') start--; - let end = bp.end; - // step right while whitespace - while (/\s/.test(message.value[end])) end++; - // if pipe after whitepace, include pipe for removal - if (message.value[end ] == '|') end++; - const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; - message.value = v; - message.dispatchEvent(new Event('input', { bubbles:true })); + removeBreakpoint(bp); } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { const bp = parser.commandIndex[cmdIdx - 1]; - // start at -1 because "/" is not included in start-end - let start = bp.start - 1; - // step left until forward slash "/" - while (message.value[start] != '/') start--; - // step left while whitespace (except newline) before start - while (/[^\S\n]/.test(message.value[start - 1])) start--; - // if newline before indent, include the newline for removal - if (message.value[start - 1] == '\n') start--; - let end = bp.end; - // step right while whitespace - while (/\s/.test(message.value[end])) end++; - // if pipe after whitepace, include pipe for removal - if (message.value[end] == '|') end++; - const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; - message.value = v; - message.dispatchEvent(new Event('input', { bubbles:true })); + removeBreakpoint(bp); } else { // start at -1 because "/" is not included in start-end let start = cmd.start - 1; From b6da9fecf956c3fdc1138c3d95d8b6ecc499dba1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 18:17:01 -0400 Subject: [PATCH 038/388] add count to split unnamed args --- public/scripts/slash-commands/SlashCommand.js | 2 ++ public/scripts/slash-commands/SlashCommandParser.js | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index 209e685fc..d83a157de 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -36,6 +36,7 @@ export class SlashCommand { * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise} [props.callback] * @param {string} [props.helpString] * @param {boolean} [props.splitUnnamedArgument] + * @param {Number} [props.splitUnnamedArgumentCount] * @param {string[]} [props.aliases] * @param {string} [props.returns] * @param {SlashCommandNamedArgument[]} [props.namedArgumentList] @@ -53,6 +54,7 @@ export class SlashCommand { /**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise}*/ callback; /**@type {string}*/ helpString; /**@type {boolean}*/ splitUnnamedArgument = false; + /**@type {Number}*/ splitUnnamedArgumentCount; /**@type {string[]}*/ aliases = []; /**@type {string}*/ returns; /**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = []; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 3110dc4cd..84a37b914 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -797,7 +797,7 @@ export class SlashCommandParser { cmd.startUnnamedArgs = this.index - /\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0; cmd.endUnnamedArgs = this.index; if (this.testUnnamedArgument()) { - cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument); + cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount); cmd.endUnnamedArgs = this.index; if (cmd.name == 'let') { const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key'); @@ -846,7 +846,7 @@ export class SlashCommandParser { testUnnamedArgumentEnd() { return this.testCommandEnd(); } - parseUnnamedArgument(split) { + parseUnnamedArgument(split, splitCount = null) { /**@type {SlashCommandClosure|String}*/ let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one let isList = split; @@ -855,6 +855,7 @@ export class SlashCommandParser { let assignment = new SlashCommandUnnamedArgumentAssignment(); assignment.start = this.index; while (!this.testUnnamedArgumentEnd()) { + if (split && splitCount && listValues.length >= splitCount) split = false; if (this.testClosure()) { isList = true; if (value.length > 0) { From 965c15fa44f795c6659385c2463384788c992635 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 23 Jun 2024 18:18:44 -0400 Subject: [PATCH 039/388] add split count to /times, /let, and /var --- public/scripts/variables.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 7be0d99aa..31d9d4af0 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1372,6 +1372,7 @@ export function registerVariableCommands() { ), ], splitUnnamedArgument: true, + splitUnnamedArgumentCount: 1, helpString: `
Execute any valid slash command enclosed in quotes repeats number of times. @@ -2001,6 +2002,7 @@ export function registerVariableCommands() { ), ], splitUnnamedArgument: true, + splitUnnamedArgumentCount: 1, helpString: `
Get or set a variable. @@ -2043,6 +2045,7 @@ export function registerVariableCommands() { ), ], splitUnnamedArgument: true, + splitUnnamedArgumentCount: 1, helpString: `
Declares a new variable in the current scope. From 3a60b4525352adce17822cb08156655fc369d7ac Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 07:29:20 -0400 Subject: [PATCH 040/388] add unresolved note --- .../extensions/quick-reply/src/QuickReply.js | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index d6a507a39..27338e228 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -820,22 +820,26 @@ export class QuickReply { const vResolved = document.createElement('div'); { vResolved.classList.add('qr--val'); vResolved.classList.add('qr--singleCol'); - const val = this.debugController.namedArguments[key]; - if (val instanceof SlashCommandClosure) { - vResolved.classList.add('qr--closure'); - vResolved.title = val.rawText; - vResolved.textContent = val.toString(); - } else if (val === undefined) { - vResolved.classList.add('qr--undefined'); - vResolved.textContent = 'undefined'; + if (this.debugController.namedArguments === undefined) { + vResolved.classList.add('qr--unresolved'); } else { - let jsonVal; - try { jsonVal = JSON.parse(val); } catch { /* empty */ } - if (jsonVal && typeof jsonVal == 'object') { - vResolved.textContent = JSON.stringify(jsonVal, null, 2); + const val = this.debugController.namedArguments?.[key]; + if (val instanceof SlashCommandClosure) { + vResolved.classList.add('qr--closure'); + vResolved.title = val.rawText; + vResolved.textContent = val.toString(); + } else if (val === undefined) { + vResolved.classList.add('qr--undefined'); + vResolved.textContent = 'undefined'; } else { - vResolved.textContent = val; - vResolved.classList.add('qr--simple'); + let jsonVal; + try { jsonVal = JSON.parse(val); } catch { /* empty */ } + if (jsonVal && typeof jsonVal == 'object') { + vResolved.textContent = JSON.stringify(jsonVal, null, 2); + } else { + vResolved.textContent = val; + vResolved.classList.add('qr--simple'); + } } } item.append(vResolved); From 2a742db63e15463c295ce6e01c7425e775e3e830 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 07:44:10 -0400 Subject: [PATCH 041/388] fix type checks for evalBoolean and export - numeric comparison only when both types are numbers - otherwise use case-insensitive string comparison and JSON-stringify and non-strings --- public/scripts/variables.js | 59 ++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 31d9d4af0..1712a97a4 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -510,36 +510,15 @@ function parseBooleanOperands(args) { * @param {string|number} b The right operand * @returns {boolean} True if the rule yields true, false otherwise */ -function evalBoolean(rule, a, b) { +export function evalBoolean(rule, a, b) { if (!rule) { toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command'); throw new Error('Invalid command.'); } let result = false; - - if (typeof a === 'string' && typeof b !== 'number') { - const aString = String(a).toLowerCase(); - const bString = String(b).toLowerCase(); - - switch (rule) { - case 'in': - result = aString.includes(bString); - break; - case 'nin': - result = !aString.includes(bString); - break; - case 'eq': - result = aString === bString; - break; - case 'neq': - result = aString !== bString; - break; - default: - toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command'); - throw new Error('Invalid command.'); - } - } else if (typeof a === 'number') { + if (typeof a === 'number' && typeof b === 'number') { + // only do numeric comparison if both operands are numbers const aNumber = Number(a); const bNumber = Number(b); @@ -569,6 +548,38 @@ function evalBoolean(rule, a, b) { toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command'); throw new Error('Invalid command.'); } + } else { + // otherwise do case-insensitive string comparsion, stringify non-strings + let aString; + let bString; + if (typeof a == 'string') { + aString = a.toLowerCase(); + } else { + aString = JSON.stringify(a).toLowerCase(); + } + if (typeof b == 'string') { + bString = b.toLowerCase(); + } else { + bString = JSON.stringify(b).toLowerCase(); + } + + switch (rule) { + case 'in': + result = aString.includes(bString); + break; + case 'nin': + result = !aString.includes(bString); + break; + case 'eq': + result = aString === bString; + break; + case 'neq': + result = aString !== bString; + break; + default: + toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command'); + throw new Error('Invalid command.'); + } } return result; From 45eeb63a0d43745cea03b86ed6837f1150366018 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 07:44:27 -0400 Subject: [PATCH 042/388] export parseBooleanOperands --- public/scripts/variables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 1712a97a4..c149ca504 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -461,7 +461,7 @@ function existsGlobalVariable(name) { * @param {object} args Command arguments * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands */ -function parseBooleanOperands(args) { +export function parseBooleanOperands(args) { // Resolution order: numeric literal, local variable, global variable, string literal /** * @param {string} operand Boolean operand candidate From 914e8eb4cf5d945cd5df86b75a855a3f9e4f0157 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 07:51:44 -0400 Subject: [PATCH 043/388] remove SlashCommandClosureExecutor has long since been replaced with /: --- public/scripts/slash-commands/SlashCommandClosure.js | 10 +--------- .../slash-commands/SlashCommandClosureExecutor.js | 7 ------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 public/scripts/slash-commands/SlashCommandClosureExecutor.js diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 219d1e817..abfee8ff9 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -3,7 +3,6 @@ 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'; @@ -233,14 +232,7 @@ export class SlashCommandClosure { this.onProgress?.(done, this.commandCount); this.debugController?.setExecutor(executor); 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) { + if (executor instanceof SlashCommandBreakPoint) { // no execution for breakpoints, just raise counter done++; yield executor; diff --git a/public/scripts/slash-commands/SlashCommandClosureExecutor.js b/public/scripts/slash-commands/SlashCommandClosureExecutor.js deleted file mode 100644 index 50dc2e33c..000000000 --- a/public/scripts/slash-commands/SlashCommandClosureExecutor.js +++ /dev/null @@ -1,7 +0,0 @@ -import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; - -export class SlashCommandClosureExecutor { - /**@type {String}*/ name = ''; - // @ts-ignore - /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = []; -} From c4c3218424ee80a161ad982510b372b7b678a8ff Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 08:36:39 -0400 Subject: [PATCH 044/388] add /break to break out of loops --- .../slash-commands/SlashCommandBreak.js | 3 ++ .../SlashCommandBreakController.js | 7 +++++ .../slash-commands/SlashCommandClosure.js | 30 ++++++++++++++----- .../SlashCommandClosureResult.js | 1 + .../slash-commands/SlashCommandParser.js | 21 +++++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 public/scripts/slash-commands/SlashCommandBreak.js create mode 100644 public/scripts/slash-commands/SlashCommandBreakController.js diff --git a/public/scripts/slash-commands/SlashCommandBreak.js b/public/scripts/slash-commands/SlashCommandBreak.js new file mode 100644 index 000000000..bb3586c2f --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandBreak.js @@ -0,0 +1,3 @@ +import { SlashCommandExecutor } from './SlashCommandExecutor.js'; + +export class SlashCommandBreak extends SlashCommandExecutor {} diff --git a/public/scripts/slash-commands/SlashCommandBreakController.js b/public/scripts/slash-commands/SlashCommandBreakController.js new file mode 100644 index 000000000..c63cf491f --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandBreakController.js @@ -0,0 +1,7 @@ +export class SlashCommandBreakController { + /**@type {boolean} */ isBreak = false; + + break() { + this.isBreak = true; + } +} diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index abfee8ff9..ee3d72df6 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -2,6 +2,8 @@ import { substituteParams } from '../../script.js'; import { delay, escapeRegex } from '../utils.js'; import { SlashCommand } from './SlashCommand.js'; import { SlashCommandAbortController } from './SlashCommandAbortController.js'; +import { SlashCommandBreak } from './SlashCommandBreak.js'; +import { SlashCommandBreakController } from './SlashCommandBreakController.js'; import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; import { SlashCommandDebugController } from './SlashCommandDebugController.js'; @@ -18,6 +20,7 @@ export class SlashCommandClosure { /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = []; /**@type {SlashCommandExecutor[]}*/ executorList = []; /**@type {SlashCommandAbortController}*/ abortController; + /**@type {SlashCommandBreakController}*/ breakController; /**@type {SlashCommandDebugController}*/ debugController; /**@type {(done:number, total:number)=>void}*/ onProgress; /**@type {string}*/ rawText; @@ -89,6 +92,7 @@ export class SlashCommandClosure { closure.providedArgumentList = this.providedArgumentList; closure.executorList = this.executorList; closure.abortController = this.abortController; + closure.breakController = this.breakController; closure.debugController = this.debugController; closure.onProgress = this.onProgress; return closure; @@ -131,6 +135,7 @@ export class SlashCommandClosure { /**@type {SlashCommandClosure}*/ const closure = v; closure.scope.parent = this.scope; + closure.breakController = this.breakController; if (closure.executeNow) { v = (await closure.execute())?.pipe; } else { @@ -154,6 +159,7 @@ export class SlashCommandClosure { /**@type {SlashCommandClosure}*/ const closure = v; closure.scope.parent = this.scope; + closure.breakController = this.breakController; if (closure.executeNow) { v = (await closure.execute())?.pipe; } else { @@ -172,13 +178,12 @@ export class SlashCommandClosure { this.scope.setVariable(arg.name, v); } - let done = 0; if (this.executorList.length == 0) { this.scope.pipe = ''; } const stepper = this.executeStep(); let step; - while (!step?.done) { + while (!step?.done && !this.breakController?.isBreak) { // get executor before execution step = await stepper.next(); if (step.value instanceof SlashCommandBreakPoint) { @@ -189,8 +194,8 @@ export class SlashCommandClosure { step = await stepper.next(); // get next executor step = await stepper.next(); - const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); - const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInNamedArgs = step.value?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInUnnamedArgs = step.value?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { this.debugController.isStepping = yield { closure:this, executor:step.value }; } else { @@ -198,10 +203,16 @@ export class SlashCommandClosure { this.debugController.stepStack[this.debugController.stepStack.length - 1] = true; } } + } else if (step.value instanceof SlashCommandBreak) { + console.log('encountered SlashCommandBreak'); + if (this.breakController) { + this.breakController?.break(); + break; + } } else if (!step.done && this.debugController?.testStepping(this)) { this.debugController.isSteppingInto = false; - const hasImmediateClosureInNamedArgs = step.value.namedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); - const hasImmediateClosureInUnnamedArgs = step.value.unnamedArgumentList.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInNamedArgs = step.value?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); + const hasImmediateClosureInUnnamedArgs = step.value?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow); if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) { this.debugController.isStepping = yield { closure:this, executor:step.value }; } @@ -222,7 +233,7 @@ export class SlashCommandClosure { return step.value; } /**@type {SlashCommandClosureResult} */ - const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe }); + const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe, isBreak: this.breakController?.isBreak ?? false }); this.debugController?.up(); return result; } @@ -236,6 +247,9 @@ export class SlashCommandClosure { // no execution for breakpoints, just raise counter done++; yield executor; + } else if (executor instanceof SlashCommandBreak) { + done += this.executorList.length - this.executorList.indexOf(executor); + yield executor; } else { /**@type {import('./SlashCommand.js').NamedArguments} */ let args = { @@ -251,6 +265,7 @@ export class SlashCommandClosure { /**@type {SlashCommandClosure}*/ const closure = arg.value; closure.scope.parent = this.scope; + closure.breakController = this.breakController; if (closure.executeNow) { args[arg.name] = (await closure.execute())?.pipe; } else { @@ -282,6 +297,7 @@ export class SlashCommandClosure { /**@type {SlashCommandClosure}*/ const closure = v; closure.scope.parent = this.scope; + closure.breakController = this.breakController; if (closure.executeNow) { v = (await closure.execute())?.pipe; } else { diff --git a/public/scripts/slash-commands/SlashCommandClosureResult.js b/public/scripts/slash-commands/SlashCommandClosureResult.js index 740d09a9d..4ea0a6ed6 100644 --- a/public/scripts/slash-commands/SlashCommandClosureResult.js +++ b/public/scripts/slash-commands/SlashCommandClosureResult.js @@ -1,6 +1,7 @@ export class SlashCommandClosureResult { /**@type {boolean}*/ interrupt = false; /**@type {string}*/ pipe; + /**@type {boolean}*/ isBreak = false; /**@type {boolean}*/ isAborted = false; /**@type {boolean}*/ isQuietlyAborted = false; /**@type {string}*/ abortReason; diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 84a37b914..a3688554b 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -20,6 +20,7 @@ import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; import { SlashCommandDebugController } from './SlashCommandDebugController.js'; import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js'; +import { SlashCommandBreak } from './SlashCommandBreak.js'; /** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */ /** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */ @@ -162,6 +163,11 @@ export class SlashCommandParser { helpString: 'Set a breakpoint for debugging in the QR Editor.', })); } + if (!Object.keys(this.commands).includes('break')) { + SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break', + helpString: 'Break out of a loop.', + })); + } //TODO should not be re-registered from every instance this.registerLanguage(); @@ -644,6 +650,9 @@ export class SlashCommandParser { if (this.debugController) { closure.executorList.push(bp); } + } else if (this.testBreak()) { + const b = this.parseBreak(); + closure.executorList.push(b); } else if (this.testCommand()) { const cmd = this.parseCommand(); cmd.injectPipe = injectPipe; @@ -687,6 +696,18 @@ export class SlashCommandParser { return bp; } + testBreak() { + return this.testSymbol(/\/break(\s|\||$)/); + } + parseBreak() { + const b = new SlashCommandBreak(); + b.start = this.index + 1; + this.take('/break'.length); + b.end = this.index; + this.commandIndex.push(b); + return b; + } + testComment() { return this.testSymbol(/\/[/#]/); } From 1de96ce11f0cc1982fb6acce66810842d2df736e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 08:42:33 -0400 Subject: [PATCH 045/388] add /break support in /times and /while --- public/scripts/variables.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index c149ca504..90814da70 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -4,6 +4,7 @@ import { executeSlashCommandsWithOptions } from './slash-commands.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; +import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; @@ -348,11 +349,13 @@ async function whileCallback(args, value) { if (result && command) { if (command instanceof SlashCommandClosure) { + command.breakController = new SlashCommandBreakController(); commandResult = await command.execute(); } else { commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController); } if (commandResult.isAborted) break; + if (commandResult.isBreak) break; } else { break; } @@ -390,8 +393,8 @@ async function timesCallback(args, value) { const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS); let result; for (let i = 0; i < iterations; i++) { - /**@type {SlashCommandClosureResult}*/ if (command instanceof SlashCommandClosure) { + command.breakController = new SlashCommandBreakController(); command.scope.setMacro('timesIndex', i); result = await command.execute(); } @@ -399,6 +402,7 @@ async function timesCallback(args, value) { result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController); } if (result.isAborted) break; + if (result.isBreak) break; } return result?.pipe ?? ''; From 916c7f1738d835aa2a955bc043c62100064fa649 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 16:44:51 -0400 Subject: [PATCH 046/388] add command source indicator --- public/scripts/slash-commands/SlashCommand.js | 39 ++++++++++++++++--- .../slash-commands/SlashCommandParser.js | 12 ++++++ public/style.css | 38 +++++++++++++++++- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index d83a157de..974e92fc0 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -63,6 +63,10 @@ export class SlashCommand { /**@type {Object.}*/ helpCache = {}; /**@type {Object.}*/ helpDetailsCache = {}; + /**@type {boolean}*/ isExtension = false; + /**@type {boolean}*/ isThirdParty = false; + /**@type {string}*/ source; + renderHelpItem(key = null) { key = key ?? this.name; if (!this.helpCache[key]) { @@ -227,12 +231,35 @@ export class SlashCommand { const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key); const specs = document.createElement('div'); { specs.classList.add('specs'); - const name = document.createElement('div'); { - name.classList.add('name'); - name.classList.add('monospace'); - name.title = 'command name'; - name.textContent = `/${key}`; - specs.append(name); + const head = document.createElement('div'); { + head.classList.add('head'); + const name = document.createElement('div'); { + name.classList.add('name'); + name.classList.add('monospace'); + name.title = 'command name'; + name.textContent = `/${key}`; + head.append(name); + } + const src = document.createElement('div'); { + src.classList.add('source'); + src.classList.add('fa-solid'); + if (this.isExtension) { + src.classList.add('isExtension'); + src.classList.add('fa-cubes'); + if (this.isThirdParty) src.classList.add('isThirdParty'); + else src.classList.add('isCore'); + } else { + src.classList.add('isCore'); + src.classList.add('fa-star-of-life'); + } + src.title = [ + this.isExtension ? 'Extension' : 'Core', + this.isThirdParty ? 'Third Party' : (this.isExtension ? 'Core' : null), + this.source, + ].filter(it=>it).join('\n'); + head.append(src); + } + specs.append(head); } const body = document.createElement('div'); { body.classList.add('body'); diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index a3688554b..333278c5c 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -73,6 +73,18 @@ export class SlashCommandParser { console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]); } + const stack = new Error().stack.split('\n').map(it=>it.trim()); + command.isExtension = stack.find(it=>it.includes('/scripts/extensions/')) != null; + command.isThirdParty = stack.find(it=>it.includes('/scripts/extensions/third-party/')) != null; + if (command.isThirdParty) { + command.source = stack.find(it=>it.includes('/scripts/extensions/third-party/')).replace(/^.*?\/scripts\/extensions\/third-party\/([^/]+)\/.*$/, '$1'); + } else if (command.isExtension) { + command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1'); + } else { + const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser\.')) + 1; + command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1'); + } + this.commands[command.name] = command; if (Array.isArray(command.aliases)) { diff --git a/public/style.css b/public/style.css index c7bc6023c..d3679dc3b 100644 --- a/public/style.css +++ b/public/style.css @@ -1744,7 +1744,12 @@ body[data-stscript-style] .hljs.language-stscript { padding: 0.25em 0.25em 0.5em 0.25em; border-bottom: 1px solid var(--ac-color-border); - >.name { + > .head { + display: flex; + gap: 0.5em; + } + > .head > .name, > .name { + flex: 1 1 auto; font-weight: bold; color: var(--ac-color-text); cursor: help; @@ -1753,6 +1758,35 @@ body[data-stscript-style] .hljs.language-stscript { text-decoration: 1px dotted underline; } } + > .head > .source { + padding: 0 0.5em; + cursor: help; + display: flex; + align-items: center; + gap: 0.5em; + &.isThirdParty.isExtension { + color: #F89406; + } + &.isCore { + color: transparent; + &.isExtension { + color: #51A351; + } + &:after { + content: ''; + order: -1; + height: 14px; + aspect-ratio: 1 / 1; + background-image: url('/favicon.ico'); + background-size: contain; + background-repeat: no-repeat; + } + } + + &:hover { + text-decoration: 1px dotted underline; + } + } >.body { flex-direction: column; @@ -1840,7 +1874,7 @@ body[data-stscript-style] .hljs.language-stscript { >code { display: block; - padding: 0; + padding: 1px; } } } From 0994de63b7a5e77129773e32398cec91eae5f1ba Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 16:54:40 -0400 Subject: [PATCH 047/388] remove unnecessary escape --- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 333278c5c..e8960b3ff 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -81,7 +81,7 @@ export class SlashCommandParser { } else if (command.isExtension) { command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1'); } else { - const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser\.')) + 1; + const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser.')) + 1; command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1'); } From b730aac8f72a6de6e20efa2315cccf8819c10334 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 17:18:00 -0400 Subject: [PATCH 048/388] make /break and /breakpoint show up properly with autocomplete details --- .../slash-commands/SlashCommandParser.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index e8960b3ff..55eea8152 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -700,24 +700,28 @@ export class SlashCommandParser { return this.testSymbol(/\/breakpoint\s*\|/); } parseBreakPoint() { - const bp = new SlashCommandBreakPoint(); - bp.start = this.index + 1; + const cmd = new SlashCommandBreakPoint(); + cmd.name = 'breakpoint'; + cmd.command = this.commands['breakpoint']; + cmd.start = this.index + 1; this.take('/breakpoint'.length); - bp.end = this.index; - this.commandIndex.push(bp); - return bp; + cmd.end = this.index; + this.commandIndex.push(cmd); + return cmd; } testBreak() { return this.testSymbol(/\/break(\s|\||$)/); } parseBreak() { - const b = new SlashCommandBreak(); - b.start = this.index + 1; + const cmd = new SlashCommandBreak(); + cmd.name = 'break'; + cmd.command = this.commands['break']; + cmd.start = this.index + 1; this.take('/break'.length); - b.end = this.index; - this.commandIndex.push(b); - return b; + cmd.end = this.index; + this.commandIndex.push(cmd); + return cmd; } testComment() { From 7851c974d17a8c0b354220e878b2984eed1671a8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 24 Jun 2024 18:28:26 -0400 Subject: [PATCH 049/388] can't stand this shit any longer --- public/scripts/autocomplete/AutoComplete.js | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 521a8c7e1..9b9e93eaf 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -538,32 +538,34 @@ export class AutoComplete { updateFloatingPosition() { const location = this.getCursorPosition(); const rect = this.textarea.getBoundingClientRect(); + const layerRect = this.textarea.closest('dialog, body').getBoundingClientRect(); // cursor is out of view -> hide if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { return this.hide(); } - const left = Math.max(rect.left, location.left); + const left = Math.max(rect.left, location.left) - layerRect.left; this.domWrap.style.setProperty('--targetOffset', `${left}`); if (location.top <= window.innerHeight / 2) { // if cursor is in lower half of window, show list above line - this.domWrap.style.top = `${location.bottom}px`; + this.domWrap.style.top = `${location.bottom - layerRect.top}px`; this.domWrap.style.bottom = 'auto'; - this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`; + this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } else { // if cursor is in upper half of window, show list below line this.domWrap.style.top = 'auto'; - this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`; - this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`; + this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`; + this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } } updateFloatingDetailsPosition(location = null) { if (!location) location = this.getCursorPosition(); const rect = this.textarea.getBoundingClientRect(); + const layerRect = this.textarea.closest('dialog, body').getBoundingClientRect(); if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) { return this.hide(); } - const left = Math.max(rect.left, location.left); + const left = Math.max(rect.left, location.left) - layerRect.left; this.detailsWrap.style.setProperty('--targetOffset', `${left}`); if (this.isReplaceable) { this.detailsWrap.classList.remove('full'); @@ -583,14 +585,14 @@ export class AutoComplete { } if (location.top <= window.innerHeight / 2) { // if cursor is in lower half of window, show list above line - this.detailsWrap.style.top = `${location.bottom}px`; + this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`; this.detailsWrap.style.bottom = 'auto'; - this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`; + this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } else { // if cursor is in upper half of window, show list below line this.detailsWrap.style.top = 'auto'; - this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`; - this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`; + this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`; + this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`; } } @@ -608,7 +610,7 @@ export class AutoComplete { } this.clone.style.position = 'fixed'; this.clone.style.visibility = 'hidden'; - getTopmostModalLayer().append(this.clone); + document.body.append(this.clone); const mo = new MutationObserver(muts=>{ if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) { this.clone.remove(); From d8dc16d6c133697c729ebe966c94b754cb29afc7 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 08:20:05 -0400 Subject: [PATCH 050/388] show scrollbars with noSyntax --- public/scripts/extensions/quick-reply/style.css | 5 +++++ public/scripts/extensions/quick-reply/style.less | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index acdef3b94..7d16db9f3 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -356,6 +356,11 @@ background-color: var(--ac-style-color-background); color: var(--ac-style-color-text); } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar, +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb { + visibility: visible; + cursor: unset; +} .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); diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 7fd299f24..a7e0adae5 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -429,6 +429,10 @@ > #qr--modal-message { background-color: var(--ac-style-color-background); color: var(--ac-style-color-text); + &::-webkit-scrollbar, &::-webkit-scrollbar-thumb { + visibility: visible; + cursor: unset; + } &::selection { color: unset; background-color: rgba(108 171 251 / 0.25); From c988f6f762f103f795453448f5c8b09b61150fad Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 08:30:13 -0400 Subject: [PATCH 051/388] restore completeAffirmative, completeNegative, completeCancelled --- public/scripts/popup.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/scripts/popup.js b/public/scripts/popup.js index 527552cde..92bc0f5bd 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -358,6 +358,15 @@ export class Popup { Popup.util.lastResult = { value, result }; this.hide(); } + completeAffirmative() { + return this.complete(POPUP_RESULT.AFFIRMATIVE); + } + completeNegative() { + return this.complete(POPUP_RESULT.NEGATIVE); + } + completeCancelled() { + return this.complete(POPUP_RESULT.CANCELLED); + } /** * Hides the popup, using the internal resolver to return the value to the original show promise From 17e794b718755e3eacb1059a07e4b0923efd40a6 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 16:56:19 -0400 Subject: [PATCH 052/388] use ctrl+alt+click for breakpoints --- public/scripts/extensions/quick-reply/html/qrEditor.html | 2 +- public/scripts/extensions/quick-reply/src/QuickReply.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 4e780c6af..919f9804d 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -33,7 +33,7 @@ Syntax highlight - Ctrl+Click to set / remove breakpoints + Ctrl+Alt+Click to set / remove breakpoints
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 27338e228..04f12cc15 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -395,7 +395,7 @@ export class QuickReply { message.dispatchEvent(new Event('input', { bubbles:true })); }; message.addEventListener('pointerup', async(evt)=>{ - if (!evt.ctrlKey) return; + if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; const parser = new SlashCommandParser(); parser.parse(message.value, false); const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= message.selectionStart && it.end >= message.selectionStart); From adc54e7f226c46a325903fdc1512cab0bb25c854 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 16:56:28 -0400 Subject: [PATCH 053/388] restore caret after breakpoint click --- .../extensions/quick-reply/src/QuickReply.js | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 04f12cc15..031d295be 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -393,20 +393,56 @@ export class QuickReply { const v = `${message.value.slice(0, start)}${message.value.slice(end)}`; message.value = v; message.dispatchEvent(new Event('input', { bubbles:true })); + let postStart = preBreakPointStart; + let postEnd = preBreakPointEnd; + // set caret back to where it was + if (preBreakPointStart <= start) { + // do nothing + } else if (preBreakPointStart > start && preBreakPointEnd < end) { + // selection start was inside breakpoint: move to index before breakpoint + postStart = start; + } else if (preBreakPointStart >= end) { + // selection was behind breakpoint: move back by length of removed string + postStart = preBreakPointStart - (end - start); + } + if (preBreakPointEnd <= start) { + // do nothing + } else if (preBreakPointEnd > start && preBreakPointEnd < end) { + // selection start was inside breakpoint: move to index before breakpoint + postEnd = start; + } else if (preBreakPointEnd >= end) { + // selection was behind breakpoint: move back by length of removed string + postEnd = preBreakPointEnd - (end - start); + } + 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; + 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 <= message.selectionStart && it.end >= message.selectionStart); + const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx && it.end >= idx); if (cmdIdx > -1) { const cmd = parser.commandIndex[cmdIdx]; if (cmd instanceof SlashCommandBreakPoint) { const bp = cmd; - removeBreakpoint(bp); + const { start, end } = removeBreakpoint(bp); + postStart = start; + postEnd = end; } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { const bp = parser.commandIndex[cmdIdx - 1]; - removeBreakpoint(bp); + const { start, end } = removeBreakpoint(bp); + postStart = start; + postEnd = end; } else { // start at -1 because "/" is not included in start-end let start = cmd.start - 1; @@ -421,12 +457,14 @@ export class QuickReply { // if newline before indent, include the newline if (message.value[start - 1] == '\n') { start--; - indent += `\n${indent}`; + 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 })); } + message.selectionStart = postStart; + message.selectionEnd = postEnd; } }); /** @type {any} */ From 1fc34bd387f2a7ed0746536bd624fd3eed2d3348 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 21:06:27 -0400 Subject: [PATCH 054/388] remove empty first string and last string from unsplit list-parsed unnamed argument --- public/scripts/slash-commands/SlashCommandParser.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 55eea8152..dc97991c1 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -946,10 +946,16 @@ export class SlashCommandParser { const firstVal = listValues[0]; if (typeof firstVal.value == 'string') { firstVal.value = firstVal.value.trimStart(); + if (firstVal.value.length == 0) { + listValues.shift(); + } } const lastVal = listValues.slice(-1)[0]; if (typeof lastVal.value == 'string') { lastVal.value = lastVal.value.trimEnd(); + if (lastVal.value.length == 0) { + listValues.pop(); + } } return listValues; } From ccbc78ed41f699fcfc5ba9fac98a742b524c8a27 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 22:54:03 -0400 Subject: [PATCH 055/388] add missing scopeIndex entries fixes missing scoped vars in /: auto complete --- public/scripts/slash-commands/SlashCommandParser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index dc97991c1..2a59ddbf9 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -707,6 +707,7 @@ export class SlashCommandParser { this.take('/breakpoint'.length); cmd.end = this.index; this.commandIndex.push(cmd); + this.scopeIndex.push(this.scope.getCopy()); return cmd; } @@ -721,6 +722,7 @@ export class SlashCommandParser { this.take('/break'.length); cmd.end = this.index; this.commandIndex.push(cmd); + this.scopeIndex.push(this.scope.getCopy()); return cmd; } From 676472f13d16e79dc38132e8c25647363dc24854 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 25 Jun 2024 23:18:48 -0400 Subject: [PATCH 056/388] fix run syntax highlight --- public/scripts/slash-commands/SlashCommandParser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 2a59ddbf9..4a85c7be6 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -217,10 +217,10 @@ export class SlashCommandParser { function getQuotedRunRegex() { try { - return new RegExp('(".+?(? Date: Tue, 25 Jun 2024 23:20:24 -0400 Subject: [PATCH 057/388] escape quotes inside quoted option value --- public/scripts/autocomplete/AutoComplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 9b9e93eaf..09e9f288c 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -369,7 +369,7 @@ export class AutoComplete { // update replacer and add quotes if necessary const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name; if (this.effectiveParserResult.canBeQuoted) { - option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName}"` : `${optionName}`; + option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`; } else { option.replacer = optionName; } From 173c5ef53ec94532f124fca5c0886fefaaba4740 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 27 Jun 2024 11:49:12 -0400 Subject: [PATCH 058/388] step into closures from elsewhere (draft) --- .../extensions/quick-reply/src/QuickReply.js | 9 +++++++++ public/scripts/slash-commands.js | 4 ++++ public/scripts/slash-commands/SlashCommand.js | 2 ++ .../slash-commands/SlashCommandClosure.js | 19 +++++++++++++++++++ .../slash-commands/SlashCommandParser.js | 5 +++++ 5 files changed, 39 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 031d295be..78ce9296e 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -747,6 +747,7 @@ export class QuickReply { } async executeFromEditor() { if (this.isExecuting) return; + const oText = this.message; this.isExecuting = true; this.editorDom.classList.add('qr--isExecuting'); const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax'); @@ -797,6 +798,12 @@ export class QuickReply { this.abortController = new SlashCommandAbortController(); this.debugController = new SlashCommandDebugController(); this.debugController.onBreakPoint = async(closure, executor)=>{ + //TODO move debug code into its own element, separate from the QR + //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.editorDebugState.innerHTML = ''; let ci = -1; const varNames = []; @@ -1177,6 +1184,8 @@ export class QuickReply { if (noSyntax) { this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); } + this.editorMessage.value = oText; + this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); this.editorExecutePromise = null; this.editorExecuteBtn.classList.remove('qr--busy'); this.editorPopup.dlg.classList.remove('qr--hide'); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 22f2e9ee4..9d0247e80 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1737,6 +1737,10 @@ async function runCallback(args, name) { throw new Error(`"${name}" is not callable.`); } closure.scope.parent = scope; + if (args._debugController && !closure.debugController) { + closure.debugController = args._debugController; + } + while (closure.providedArgumentList.pop()); closure.argumentList.forEach(arg => { if (Object.keys(args).includes(arg.name)) { const providedArg = new SlashCommandNamedArgumentAssignment(); diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index 974e92fc0..81019fcef 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -1,6 +1,7 @@ import { SlashCommandAbortController } from './SlashCommandAbortController.js'; import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js'; import { SlashCommandClosure } from './SlashCommandClosure.js'; +import { SlashCommandDebugController } from './SlashCommandDebugController.js'; import { PARSER_FLAG } from './SlashCommandParser.js'; import { SlashCommandScope } from './SlashCommandScope.js'; @@ -12,6 +13,7 @@ import { SlashCommandScope } from './SlashCommandScope.js'; * _scope:SlashCommandScope, * _parserFlags:{[id:PARSER_FLAG]:boolean}, * _abortController:SlashCommandAbortController, + * _debugController:SlashCommandDebugController, * _hasUnnamedArgument:boolean, * [id:string]:string|SlashCommandClosure, * }} NamedArguments diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index ee3d72df6..dc7c33e6c 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -24,6 +24,8 @@ export class SlashCommandClosure { /**@type {SlashCommandDebugController}*/ debugController; /**@type {(done:number, total:number)=>void}*/ onProgress; /**@type {string}*/ rawText; + /**@type {string}*/ fullText; + /**@type {string}*/ parserContext; /**@type {number}*/ get commandCount() { @@ -58,6 +60,12 @@ export class SlashCommandClosure { const after = remaining.slice(match.index + match[0].length); const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value; if (replacer instanceof SlashCommandClosure) { + replacer.abortController = this.abortController; + replacer.breakController = this.breakController; + replacer.scope.parent = this.scope; + if (this.debugController && !replacer.debugController) { + replacer.debugController = this.debugController; + } isList = true; if (match.index > 0) { listValues.push(before); @@ -94,6 +102,9 @@ export class SlashCommandClosure { closure.abortController = this.abortController; closure.breakController = this.breakController; closure.debugController = this.debugController; + closure.rawText = this.rawText; + closure.fullText = this.fullText; + closure.parserContext = this.parserContext; closure.onProgress = this.onProgress; return closure; } @@ -256,6 +267,7 @@ export class SlashCommandClosure { _scope: this.scope, _parserFlags: executor.parserFlags, _abortController: this.abortController, + _debugController: this.debugController, _hasUnnamedArgument: executor.unnamedArgumentList.length > 0, }; let value; @@ -266,6 +278,9 @@ export class SlashCommandClosure { const closure = arg.value; closure.scope.parent = this.scope; closure.breakController = this.breakController; + if (this.debugController && !closure.debugController) { + closure.debugController = this.debugController; + } if (closure.executeNow) { args[arg.name] = (await closure.execute())?.pipe; } else { @@ -285,6 +300,7 @@ export class SlashCommandClosure { // substitute unnamed argument if (executor.unnamedArgumentList.length == 0) { + //TODO no pipe injection on first executor in a closure? if (executor.injectPipe) { value = this.scope.pipe; args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined; @@ -298,6 +314,9 @@ export class SlashCommandClosure { const closure = v; closure.scope.parent = this.scope; closure.breakController = this.breakController; + if (this.debugController && !closure.debugController) { + closure.debugController = this.debugController; + } if (closure.executeNow) { v = (await closure.execute())?.pipe; } else { diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 4a85c7be6..d7f32990c 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -117,6 +117,8 @@ export class SlashCommandParser { /**@type {SlashCommandExecutor[]}*/ commandIndex; /**@type {SlashCommandScope[]}*/ scopeIndex; + /**@type {string}*/ parserContext; + get userIndex() { return this.index; } get ahead() { @@ -610,6 +612,7 @@ export class SlashCommandParser { this.commandIndex = []; this.scopeIndex = []; this.macroIndex = []; + this.parserContext = uuidv4(); const closure = this.parseClosure(true); return closure; } @@ -637,6 +640,8 @@ export class SlashCommandParser { if (!isRoot) this.take(2); // discard opening {: const textStart = this.index; let closure = new SlashCommandClosure(this.scope); + closure.parserContext = this.parserContext; + closure.fullText = this.text; closure.abortController = this.abortController; closure.debugController = this.debugController; this.scope = closure.scope; From afb849cfb67b70bdd642b073d4f1031741c34297 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 11:48:31 -0400 Subject: [PATCH 059/388] fix REPLACE_GETVAR nesting issues --- public/scripts/slash-commands/SlashCommandParser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index d7f32990c..0f3c6a85c 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -645,6 +645,7 @@ export class SlashCommandParser { closure.abortController = this.abortController; closure.debugController = this.debugController; this.scope = closure.scope; + const oldClosure = this.closure; this.closure = closure; this.discardWhitespace(); while (this.testNamedArgument()) { @@ -698,6 +699,7 @@ export class SlashCommandParser { } closureIndexEntry.end = this.index - 1; this.scope = closure.scope.parent; + this.closure = oldClosure ?? closure; return closure; } From fcf1830887fcb35b8f5cf6261f37282bc7b9015b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 11:50:00 -0400 Subject: [PATCH 060/388] make matchProvider and valueProvider optional --- public/scripts/slash-commands/SlashCommandEnumValue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandEnumValue.js b/public/scripts/slash-commands/SlashCommandEnumValue.js index 50ac3733a..4ff32d515 100644 --- a/public/scripts/slash-commands/SlashCommandEnumValue.js +++ b/public/scripts/slash-commands/SlashCommandEnumValue.js @@ -57,9 +57,9 @@ export class SlashCommandEnumValue { * @param {string} value - The value * @param {string?} description - Optional description, displayed in a second line * @param {EnumType?} type - type of the enum (defining its color) - * @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) + * @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) */ - constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider, valueProvider) { + constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null) { this.value = value; this.description = description; this.type = type ?? 'enum'; From 92f4402b63601fd26ff2a401f3f3d4ae5ccbe8ac Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:20:58 -0400 Subject: [PATCH 061/388] keep indent on enter --- .../extensions/quick-reply/src/QuickReply.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 78ce9296e..9a2f61953 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -354,6 +354,21 @@ export class QuickReply { message.selectionStart = start - 1; message.selectionEnd = end - count; updateSyntax(); + } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + evt.stopImmediatePropagation(); + evt.stopPropagation(); + evt.preventDefault(); + const start = message.selectionStart; + const end = message.selectionEnd; + const lineStart = message.value.lastIndexOf('\n', start - 1); + const indent = /^(\s*)/.exec(message.value.slice(lineStart).replace(/^\n*/, ''))[1] ?? ''; + const x = message.value.slice(0, start); + const y = message.value.slice(end); + const z = message.value.slice(lineStart); + message.value = `${message.value.slice(0, start)}\n${indent}${message.value.slice(end)}`; + message.selectionStart = start + 1 + indent.length; + message.selectionEnd = message.selectionStart; + updateSyntax(); } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { evt.stopPropagation(); evt.preventDefault(); From a1341fbcab97642ca099b9b0c9d3b65719898ab2 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:21:15 -0400 Subject: [PATCH 062/388] fix tab indent line detection --- public/scripts/extensions/quick-reply/src/QuickReply.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 9a2f61953..f9c6564c9 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -332,7 +332,7 @@ export class QuickReply { const start = message.selectionStart; const end = message.selectionEnd; if (end - start > 0 && message.value.substring(start, end).includes('\n')) { - const lineStart = message.value.lastIndexOf('\n', start); + const lineStart = message.value.lastIndexOf('\n', start - 1); const count = message.value.substring(lineStart, end).split('\n').length - 1; message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`; message.selectionStart = start + 1; @@ -348,7 +348,7 @@ export class QuickReply { evt.preventDefault(); const start = message.selectionStart; const end = message.selectionEnd; - const lineStart = message.value.lastIndexOf('\n', start); + const lineStart = message.value.lastIndexOf('\n', start - 1); const count = message.value.substring(lineStart, end).split('\n\t').length - 1; message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`; message.selectionStart = start - 1; From 8e90e2a0e441245ecced9ab498353544b5028a48 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:21:31 -0400 Subject: [PATCH 063/388] fix editor hotkeys and autocomplete interfering --- public/scripts/extensions/quick-reply/src/QuickReply.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index f9c6564c9..491ec6106 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -323,12 +323,13 @@ export class QuickReply { this.updateMessage(message.value); updateScrollDebounced(); }); - 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(); + evt.stopImmediatePropagation(); + evt.stopPropagation(); const start = message.selectionStart; const end = message.selectionEnd; if (end - start > 0 && message.value.substring(start, end).includes('\n')) { @@ -346,6 +347,8 @@ export class QuickReply { } } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { evt.preventDefault(); + evt.stopImmediatePropagation(); + evt.stopPropagation(); const start = message.selectionStart; const end = message.selectionEnd; const lineStart = message.value.lastIndexOf('\n', start - 1); @@ -370,6 +373,7 @@ export class QuickReply { message.selectionEnd = message.selectionStart; updateSyntax(); } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + evt.stopImmediatePropagation(); evt.stopPropagation(); evt.preventDefault(); if (executeShortcut.checked) { @@ -385,6 +389,7 @@ export class QuickReply { } } }); + setSlashCommandAutoComplete(message, true); message.addEventListener('wheel', (evt)=>{ updateScrollDebounced(evt); }); From 6193b6590e050a4a1d96310b40fd1c686a2abb4e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:26:58 -0400 Subject: [PATCH 064/388] add /break value --- public/scripts/slash-commands/SlashCommandBreak.js | 6 +++++- public/scripts/slash-commands/SlashCommandClosure.js | 2 ++ public/scripts/slash-commands/SlashCommandParser.js | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandBreak.js b/public/scripts/slash-commands/SlashCommandBreak.js index bb3586c2f..689bd9453 100644 --- a/public/scripts/slash-commands/SlashCommandBreak.js +++ b/public/scripts/slash-commands/SlashCommandBreak.js @@ -1,3 +1,7 @@ import { SlashCommandExecutor } from './SlashCommandExecutor.js'; -export class SlashCommandBreak extends SlashCommandExecutor {} +export class SlashCommandBreak extends SlashCommandExecutor { + get value() { + return this.unnamedArgumentList[0]?.value; + } +} diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index dc7c33e6c..12da36cd9 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -218,6 +218,7 @@ export class SlashCommandClosure { console.log('encountered SlashCommandBreak'); if (this.breakController) { this.breakController?.break(); + this.scope.pipe = step.value.value ?? this.scope.pipe; break; } } else if (!step.done && this.debugController?.testStepping(this)) { @@ -260,6 +261,7 @@ export class SlashCommandClosure { yield executor; } else if (executor instanceof SlashCommandBreak) { done += this.executorList.length - this.executorList.indexOf(executor); + this.scope.pipe = executor.value ?? this.scope.pipe; yield executor; } else { /**@type {import('./SlashCommand.js').NamedArguments} */ diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 0f3c6a85c..a75899d24 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -727,6 +727,10 @@ export class SlashCommandParser { cmd.command = this.commands['break']; cmd.start = this.index + 1; this.take('/break'.length); + this.discardWhitespace(); + if (this.testUnnamedArgument()) { + cmd.unnamedArgumentList.push(...this.parseUnnamedArgument()); + } cmd.end = this.index; this.commandIndex.push(cmd); this.scopeIndex.push(this.scope.getCopy()); From e4ab5d7d02fb2b56c458478d540a214ef5fac89a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:31:27 -0400 Subject: [PATCH 065/388] use /break as return statement in /run --- public/scripts/slash-commands.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 335d28fe5..1c1a45414 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -65,6 +65,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; +import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -1782,6 +1783,7 @@ async function runCallback(args, name) { throw new Error(`"${name}" is not callable.`); } closure.scope.parent = scope; + closure.breakController = new SlashCommandBreakController(); if (args._debugController && !closure.debugController) { closure.debugController = args._debugController; } From c47db9e7299066b81861a04439fbfaa3a88e54e8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 12:35:56 -0400 Subject: [PATCH 066/388] no pipe inject in first executor of closure --- public/scripts/slash-commands/SlashCommandClosure.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 12da36cd9..faf7440c1 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -251,6 +251,7 @@ export class SlashCommandClosure { } async * executeStep() { let done = 0; + let isFirst = true; for (const executor of this.executorList) { this.onProgress?.(done, this.commandCount); this.debugController?.setExecutor(executor); @@ -259,10 +260,12 @@ export class SlashCommandClosure { // no execution for breakpoints, just raise counter done++; yield executor; + isFirst = false; } else if (executor instanceof SlashCommandBreak) { done += this.executorList.length - this.executorList.indexOf(executor); this.scope.pipe = executor.value ?? this.scope.pipe; yield executor; + isFirst = false; } else { /**@type {import('./SlashCommand.js').NamedArguments} */ let args = { @@ -302,8 +305,7 @@ export class SlashCommandClosure { // substitute unnamed argument if (executor.unnamedArgumentList.length == 0) { - //TODO no pipe injection on first executor in a closure? - if (executor.injectPipe) { + if (!isFirst && executor.injectPipe) { value = this.scope.pipe; args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined; } @@ -383,6 +385,7 @@ export class SlashCommandClosure { } } yield executor; + isFirst = false; } } From 490b2004b74b5b5cb946292bcce5b013a6594a37 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 16:52:58 -0400 Subject: [PATCH 067/388] update /break help --- public/scripts/slash-commands/SlashCommandParser.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index a75899d24..06bffd642 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -179,7 +179,12 @@ export class SlashCommandParser { } if (!Object.keys(this.commands).includes('break')) { SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break', - helpString: 'Break out of a loop.', + helpString: 'Break out of a loop or closure executed through /run or /:', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ description: 'value to pass down the pipe instead of the current pipe value', + typeList: Object.values(ARGUMENT_TYPE), + }), + ], })); } From 0fc9b11adfaac552eaa4d309ef587354ad1f866e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 16:53:39 -0400 Subject: [PATCH 068/388] fix key conflicts in QR editor --- .../scripts/extensions/quick-reply/src/QuickReply.js | 12 +++++++----- public/scripts/slash-commands.js | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 491ec6106..095204af7 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -328,18 +328,20 @@ export class QuickReply { if (this.isExecuting) return; if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { evt.preventDefault(); - evt.stopImmediatePropagation(); - evt.stopPropagation(); const start = message.selectionStart; const end = message.selectionEnd; if (end - start > 0 && message.value.substring(start, end).includes('\n')) { + evt.stopImmediatePropagation(); + evt.stopPropagation(); const lineStart = message.value.lastIndexOf('\n', start - 1); const count = message.value.substring(lineStart, end).split('\n').length - 1; message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`; message.selectionStart = start + 1; message.selectionEnd = end + count; updateSyntax(); - } else { + } else if (!(ac.isReplaceable && ac.isActive)) { + evt.stopImmediatePropagation(); + evt.stopPropagation(); message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`; message.selectionStart = start + 1; message.selectionEnd = end + 1; @@ -357,7 +359,7 @@ export class QuickReply { message.selectionStart = start - 1; message.selectionEnd = end - count; updateSyntax(); - } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { evt.stopImmediatePropagation(); evt.stopPropagation(); evt.preventDefault(); @@ -389,7 +391,7 @@ export class QuickReply { } } }); - setSlashCommandAutoComplete(message, true); + const ac = await setSlashCommandAutoComplete(message, true); message.addEventListener('wheel', (evt)=>{ updateScrollDebounced(evt); }); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 1c1a45414..f852d75ec 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -3507,6 +3507,7 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul * * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor) + * @returns {Promise} */ export async function setSlashCommandAutoComplete(textarea, isFloating = false) { function canUseNegativeLookbehind() { @@ -3530,6 +3531,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false) async (text, index) => await parser.getNameAt(text, index), isFloating, ); + return ac; } /**@type {HTMLTextAreaElement} */ const sendTextarea = document.querySelector('#send_textarea'); From 438d6600bb1ced9480864073ee4171a7ad477ac8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 16:54:04 -0400 Subject: [PATCH 069/388] allow options with valueProvider to be selectable --- public/scripts/autocomplete/AutoCompleteOption.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/scripts/autocomplete/AutoCompleteOption.js b/public/scripts/autocomplete/AutoCompleteOption.js index a946a462f..24822750b 100644 --- a/public/scripts/autocomplete/AutoCompleteOption.js +++ b/public/scripts/autocomplete/AutoCompleteOption.js @@ -13,6 +13,7 @@ export class AutoCompleteOption { /**@type {HTMLElement}*/ dom; /**@type {(input:string)=>boolean}*/ matchProvider; /**@type {(input:string)=>string}*/ valueProvider; + /**@type {boolean}*/ makeSelectable = false; /** @@ -24,19 +25,20 @@ export class AutoCompleteOption { } get isSelectable() { - return !this.valueProvider; + return this.makeSelectable || !this.valueProvider; } /** * @param {string} name */ - constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null) { + constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) { this.name = name; this.typeIcon = typeIcon; this.type = type; this.matchProvider = matchProvider; this.valueProvider = valueProvider; + this.makeSelectable = makeSelectable; } From acf414bedb32d6f919ba974fef1f8cc3747a5770 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 16:54:13 -0400 Subject: [PATCH 070/388] add onSelect callback --- public/scripts/autocomplete/AutoComplete.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 09e9f288c..9a7ef6f6a 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -56,6 +56,8 @@ export class AutoComplete { /**@type {function}*/ updateDetailsPositionDebounced; /**@type {function}*/ updateFloatingPositionDebounced; + /**@type {(item:AutoCompleteOption)=>any}*/ onSelect; + get matchType() { return power_user.stscript.matching ?? 'fuzzy'; } @@ -669,6 +671,7 @@ export class AutoComplete { } this.wasForced = false; this.textarea.dispatchEvent(new Event('input', { bubbles:true })); + this.onSelect?.(this.selectedItem); } From db1cf54929ac360ba489c9ce8662f6f21c0d432b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 4 Jul 2024 16:54:37 -0400 Subject: [PATCH 071/388] cleanup --- public/scripts/extensions/quick-reply/src/QuickReply.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 095204af7..a142b1c3c 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -367,9 +367,6 @@ export class QuickReply { const end = message.selectionEnd; const lineStart = message.value.lastIndexOf('\n', start - 1); const indent = /^(\s*)/.exec(message.value.slice(lineStart).replace(/^\n*/, ''))[1] ?? ''; - const x = message.value.slice(0, start); - const y = message.value.slice(end); - const z = message.value.slice(lineStart); message.value = `${message.value.slice(0, start)}\n${indent}${message.value.slice(end)}`; message.selectionStart = start + 1 + indent.length; message.selectionEnd = message.selectionStart; From 1ab11cf85f30b96d3a673794fbd30b2ce99095ef Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 14:00:07 -0400 Subject: [PATCH 072/388] allow autocomplete on input element (jsdoc) --- public/scripts/autocomplete/AutoComplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 9a7ef6f6a..39ff0fae5 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -17,7 +17,7 @@ export const AUTOCOMPLETE_WIDTH = { }; export class AutoComplete { - /**@type {HTMLTextAreaElement}*/ textarea; + /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea; /**@type {boolean}*/ isFloating = false; /**@type {()=>boolean}*/ checkIfActivate; /**@type {(text:string, index:number) => Promise}*/ getNameAt; From 12e30bde99ee35e915d12504e6b043303a098a9f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:04:33 -0400 Subject: [PATCH 073/388] add closure support to /run --- public/scripts/slash-commands.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index f852d75ec..26bbedd95 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1075,7 +1075,7 @@ export function initDefaultSlashCommands() { unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'scoped variable or qr label', - typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING], + typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE], isRequired: true, enumProvider: () => [ ...commonEnumProviders.variables('scope')(), @@ -1775,6 +1775,10 @@ async function runCallback(args, name) { throw new Error('No name provided for /run command'); } + if (name instanceof SlashCommandClosure) { + return await name.execute(); + } + /**@type {SlashCommandScope} */ const scope = args._scope; if (scope.existsVariable(name)) { From b1412d3bce08239fbefa52b353c242a340d17fc0 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:04:50 -0400 Subject: [PATCH 074/388] what? --- public/scripts/slash-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 26bbedd95..fcdf7afc2 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1812,7 +1812,7 @@ async function runCallback(args, name) { name = name.trim(); return await window['executeQuickReplyByName'](name, args); } catch (error) { - throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error'); + throw new Error(`Error running Quick Reply "${name}": ${error.message}`); } } From 91ffd141ef5305a4c68efbced613382af5f11292 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:05:22 -0400 Subject: [PATCH 075/388] add a little more details to execution exceptions --- public/scripts/slash-commands.js | 37 +++++++++++- .../slash-commands/SlashCommandClosure.js | 7 ++- .../SlashCommandExecutionError.js | 60 +++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 public/scripts/slash-commands/SlashCommandExecutionError.js diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fcdf7afc2..37a392ecb 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -66,6 +66,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; +import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; export { executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, }; @@ -3405,7 +3406,23 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) { result.isError = true; result.errorMessage = e.message || 'An unknown error occurred'; if (e.cause !== 'abort') { - toastr.error(result.errorMessage); + if (e instanceof SlashCommandExecutionError) { + /**@type {SlashCommandExecutionError}*/ + const ex = e; + const toast = ` +
${ex.message}
+
Line: ${ex.line} Column: ${ex.column}
+
${ex.hint}
+ `; + const clickHint = '

Click to see details

'; + toastr.error( + `${toast}${clickHint}`, + 'SlashCommandExecutionError', + { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') }, + ); + } else { + toastr.error(result.errorMessage); + } } } finally { delay(1000).then(() => clearCommandProgressDebounced()); @@ -3474,7 +3491,23 @@ async function executeSlashCommandsWithOptions(text, options = {}) { return result; } catch (e) { if (options.handleExecutionErrors) { - toastr.error(e.message); + if (e instanceof SlashCommandExecutionError) { + /**@type {SlashCommandExecutionError}*/ + const ex = e; + const toast = ` +
${ex.message}
+
Line: ${ex.line} Column: ${ex.column}
+
${ex.hint}
+ `; + const clickHint = '

Click to see details

'; + toastr.error( + `${toast}${clickHint}`, + 'SlashCommandExecutionError', + { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') }, + ); + } else { + toastr.error(e.message); + } const result = new SlashCommandClosureResult(); result.isError = true; result.errorMessage = e.message; diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index faf7440c1..6be5ac204 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -7,6 +7,7 @@ import { SlashCommandBreakController } from './SlashCommandBreakController.js'; import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js'; import { SlashCommandClosureResult } from './SlashCommandClosureResult.js'; import { SlashCommandDebugController } from './SlashCommandDebugController.js'; +import { SlashCommandExecutionError } from './SlashCommandExecutionError.js'; import { SlashCommandExecutor } from './SlashCommandExecutor.js'; import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js'; import { SlashCommandScope } from './SlashCommandScope.js'; @@ -370,7 +371,11 @@ export class SlashCommandClosure { if (this.debugController) { this.debugController.isStepping = false || this.debugController.isSteppingInto; } - this.scope.pipe = await executor.command.callback(args, value ?? ''); + try { + this.scope.pipe = await executor.command.callback(args, value ?? ''); + } catch (ex) { + throw new SlashCommandExecutionError(ex, ex.message, executor.name, executor.start, executor.end, this.fullText.slice(executor.start, executor.end), this.fullText); + } if (this.debugController) { this.debugController.namedArguments = undefined; this.debugController.unnamedArguments = undefined; diff --git a/public/scripts/slash-commands/SlashCommandExecutionError.js b/public/scripts/slash-commands/SlashCommandExecutionError.js new file mode 100644 index 000000000..a8f9df0eb --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandExecutionError.js @@ -0,0 +1,60 @@ +export class SlashCommandExecutionError extends Error { + /**@type {string} */ commandName; + /**@type {number} */ start; + /**@type {number} */ end; + /**@type {string} */ commandText; + + /**@type {string} */ text; + get index() { return this.start; } + + get line() { + return this.text.slice(0, this.index).replace(/[^\n]/g, '').length; + } + get column() { + return this.text.slice(0, this.index).split('\n').pop().length; + } + get hint() { + let lineOffset = this.line.toString().length; + let lineStart = this.index; + let start = this.index; + let end = this.index; + let offset = 0; + let lineCount = 0; + while (offset < 10000 && lineCount < 3 && start >= 0) { + if (this.text[start] == '\n') lineCount++; + if (lineCount == 0) lineStart--; + offset++; + start--; + } + if (this.text[start + 1] == '\n') start++; + offset = 0; + while (offset < 10000 && this.text[end] != '\n') { + offset++; + end++; + } + let hint = []; + let lines = this.text.slice(start + 1, end - 1).split('\n'); + let lineNum = this.line - lines.length + 1; + let tabOffset = 0; + for (const line of lines) { + const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`; + lineNum++; + const untabbedLine = line.replace(/\t/g, ' '.repeat(4)); + tabOffset = untabbedLine.length - line.length; + hint.push(`${num}: ${untabbedLine}`); + } + hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1 + tabOffset)}^^^^^`); + return hint.join('\n'); + } + + + + constructor(cause, message, commandName, start, end, commandText, fullText) { + super(message, { cause }); + this.commandName = commandName; + this.start = start; + this.end = end; + this.commandText = commandText; + this.text = fullText; + } +} From c213a64340745d6dd38c0c62f9ed2a8fa6d68868 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:09:24 -0400 Subject: [PATCH 076/388] fix enumProvider not getting scope --- public/scripts/slash-commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 37a392ecb..e12dddaa8 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1078,8 +1078,8 @@ export function initDefaultSlashCommands() { description: 'scoped variable or qr label', typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE], isRequired: true, - enumProvider: () => [ - ...commonEnumProviders.variables('scope')(), + enumProvider: (executor, scope) => [ + ...commonEnumProviders.variables('scope')(executor, scope), ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [], ], }), From ba0f5427cf28f424a6c8fb6c7497a11f37a5432a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:53:55 -0400 Subject: [PATCH 077/388] add missing semicolon --- .../scripts/extensions/quick-reply/src/SlashCommandHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index a2c54e9ee..015ccf2ba 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -63,7 +63,7 @@ export class SlashCommandHandler { ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)), ]; }, - } + }; window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables; From 8785a0a5a39f99d30ca0d5789e0783ed8926b9b3 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 19:13:11 -0400 Subject: [PATCH 078/388] add setting macro if it not already exists somewhere in scope hierarchy --- public/scripts/slash-commands/SlashCommandScope.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandScope.js b/public/scripts/slash-commands/SlashCommandScope.js index 7db2572c5..30e545429 100644 --- a/public/scripts/slash-commands/SlashCommandScope.js +++ b/public/scripts/slash-commands/SlashCommandScope.js @@ -38,8 +38,10 @@ export class SlashCommandScope { } - setMacro(key, value) { - this.macros[key] = value; + setMacro(key, value, overwrite = true) { + if (overwrite || !this.macroList.find(it=>it.key == key)) { + this.macros[key] = value; + } } From 83b4df9cd3c14df8f4cfcafa71abe25e7c81b405 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 19:13:37 -0400 Subject: [PATCH 079/388] add simple wildcards to scope macros --- .../scripts/slash-commands/SlashCommandClosure.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 6be5ac204..f7056bfc4 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -51,15 +51,23 @@ export class SlashCommandClosure { let isList = false; let listValues = []; scope = scope ?? this.scope; - const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|'); - const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`); + const escapeMacro = (it)=>escapeRegex(it.key.replace(/\*/g, '~~~WILDCARD~~~')) + .replaceAll('~~~WILDCARD~~~', '(?:(?:(?!(?:::|}})).)*)') + ; + const macroList = scope.macroList.toSorted((a,b)=>{ + if (a.key.includes('*') && !b.key.includes('*')) return 1; + if (!a.key.includes('*') && b.key.includes('*')) return -1; + return 0; + }); + const macros = macroList.map(it=>escapeMacro(it)).join('|'); + const re = new RegExp(`(?{{pipe}})|(?:{{var::(?[^\\s]+?)(?:::(?(?!}}).+))?}})|(?:{{(?${macros})}})`); let done = ''; let remaining = text; while (re.test(remaining)) { const match = re.exec(remaining); const before = substituteParams(remaining.slice(0, match.index)); const after = remaining.slice(match.index + match[0].length); - const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value; + const replacer = match.groups.pipe ? scope.pipe : match.groups.var ? scope.getVariable(match.groups.var, match.groups.index) : macroList.find(it=>it.key == match.groups.macro || new RegExp(escapeMacro(it)).test(match.groups.macro))?.value; if (replacer instanceof SlashCommandClosure) { replacer.abortController = this.abortController; replacer.breakController = this.breakController; From 88718d89bc9ae9d896383c43661b3eeea3b9c4b6 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 19:14:00 -0400 Subject: [PATCH 080/388] add /qr-arg for {{arg::...}} fallbacks --- .../quick-reply/src/SlashCommandHandler.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 015ccf2ba..fe3bcf0e3 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -526,6 +526,34 @@ export class SlashCommandHandler {
`, })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg', + callback: ({ _scope }, [key, value]) => { + _scope.setMacro(`arg::${key}`, value, false); + return ''; + }, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ description: 'argument name', + typeList: ARGUMENT_TYPE.STRING, + isRequired: true, + }), + SlashCommandArgument.fromProps({ description: 'argument value', + typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY], + isRequired: true, + }), + ], + splitUnnamedArgument: true, + splitUnnamedArgumentCount: 2, + helpString: ` +
+ Set a fallback value for a Quick Reply argument. +
+
+ Example: +
/qr-arg x foo |\n/echo {{arg::x}}
+
+ `, + })); } From 3144c219fafb192396257381b6e2217a9dd53acd Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 19:14:30 -0400 Subject: [PATCH 081/388] add {{arg::*}} to replace missing args with empty string --- public/scripts/extensions/quick-reply/src/QuickReply.js | 1 + public/scripts/extensions/quick-reply/src/QuickReplySet.js | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index a142b1c3c..93f93d5eb 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1297,6 +1297,7 @@ export class QuickReply { for (const key of Object.keys(args)) { scope.setMacro(`arg::${key}`, args[key]); } + scope.setMacro('arg::*', ''); if (isEditor) { this.abortController = new SlashCommandAbortController(); } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index caf298c39..9e12f233a 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -109,6 +109,7 @@ export class QuickReplySet { const parser = new SlashCommandParser(); const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController); closure.onProgress = (done, total) => qr.updateEditorProgress(done, total); + closure.scope.setMacro('arg::*', ''); // closure.abortController = qr.abortController; // closure.debugController = qr.debugController; // const stepper = closure.executeGenerator(); From 42cad6dd1a5b23164ffc069c564eb1279c8c3ad1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 19:14:45 -0400 Subject: [PATCH 082/388] don't add args._scope etc to macros --- public/scripts/extensions/quick-reply/src/QuickReply.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 93f93d5eb..d169f361d 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1295,6 +1295,7 @@ export class QuickReply { if (this.message?.length > 0 && this.onExecute) { const scope = new SlashCommandScope(); for (const key of Object.keys(args)) { + if (key[0] == '_') continue; scope.setMacro(`arg::${key}`, args[key]); } scope.setMacro('arg::*', ''); From 75317f3eb47b7479f83653056e1a80e218aa8ff4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 8 Jul 2024 18:07:37 -0400 Subject: [PATCH 083/388] better stepping into other scripts, with source indicator --- .../quick-reply/api/QuickReplyApi.js | 5 +- .../scripts/extensions/quick-reply/index.js | 4 +- .../extensions/quick-reply/src/QuickReply.js | 108 +++++++++++------- .../quick-reply/src/QuickReplySet.js | 29 +++-- .../scripts/extensions/quick-reply/style.css | 15 ++- .../scripts/extensions/quick-reply/style.less | 14 ++- public/scripts/slash-commands.js | 13 ++- .../slash-commands/SlashCommandClosure.js | 11 +- .../slash-commands/SlashCommandExecutor.js | 12 ++ 9 files changed, 143 insertions(+), 68 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 15ff1d4da..85ff2da73 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -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); } diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index b0f496126..5c5cf7d7e 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -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}".`); } diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index d169f361d..4388d2790 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -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, }); } } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 9e12f233a..32b3761c0 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -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 : ''; } diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 7d16db9f3..b0ed59cd0 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -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%, diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index a7e0adae5..cbb8605f6 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -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; + } } } } diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 988f0a62a..c39b2d9a2 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -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}*/ diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index f7056bfc4..663776acb 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -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; } diff --git a/public/scripts/slash-commands/SlashCommandExecutor.js b/public/scripts/slash-commands/SlashCommandExecutor.js index f64c37aff..cfbcf6cf1 100644 --- a/public/scripts/slash-commands/SlashCommandExecutor.js +++ b/public/scripts/slash-commands/SlashCommandExecutor.js @@ -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 = []; From 6cc523b8052217b75d623961d9dc2a4b19906588 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 8 Jul 2024 18:21:07 -0400 Subject: [PATCH 084/388] show source in message label --- public/scripts/extensions/quick-reply/src/QuickReply.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 4388d2790..80d476095 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -828,6 +828,7 @@ export class QuickReply { // 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; + this.editorDom.querySelector('label[for="qr--modal-message"]').textContent = closure.source; const source = closure.source; this.editorDebugState.innerHTML = ''; let ci = -1; @@ -1226,6 +1227,7 @@ export class QuickReply { if (noSyntax) { this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); } + this.editorDom.querySelector('label[for="qr--modal-message"]').textContent = 'Message / Command: '; this.editorMessage.value = oText; this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); this.editorExecutePromise = null; From 60275e3dce8ad23ba56d099f53aff4a73a8db6a0 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jul 2024 08:21:26 -0400 Subject: [PATCH 085/388] better handling of anonyous source --- .../extensions/quick-reply/src/QuickReply.js | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 80d476095..7c660ce54 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -59,6 +59,7 @@ export class QuickReply { /**@type {HTMLElement}*/ editorDom; /**@type {HTMLTextAreaElement}*/ editorMessage; + /**@type {HTMLTextAreaElement}*/ editorMessageLabel; /**@type {HTMLElement}*/ editorSyntax; /**@type {HTMLElement}*/ editorExecuteBtn; /**@type {HTMLElement}*/ editorExecuteBtnPause; @@ -316,6 +317,7 @@ export class QuickReply { localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); updateSyntaxEnabled(); }); + this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]'); /**@type {HTMLTextAreaElement}*/ const message = dom.querySelector('#qr--modal-message'); this.editorMessage = message; @@ -770,6 +772,7 @@ export class QuickReply { } async executeFromEditor() { if (this.isExecuting) return; + const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/; const oText = this.message; this.isExecuting = true; this.editorDom.classList.add('qr--isExecuting'); @@ -828,7 +831,27 @@ export class QuickReply { // 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; - this.editorDom.querySelector('label[for="qr--modal-message"]').textContent = closure.source; + this.editorMessageLabel.innerHTML = ''; + if (uuidCheck.test(closure.source)) { + const p0 = document.createElement('span'); { + p0.textContent = 'anonymous: '; + this.editorMessageLabel.append(p0); + } + const p1 = document.createElement('strong'); { + p1.textContent = executor.source.slice(0,5); + this.editorMessageLabel.append(p1); + } + const p2 = document.createElement('span'); { + p2.textContent = executor.source.slice(5, -5); + this.editorMessageLabel.append(p2); + } + const p3 = document.createElement('strong'); { + p3.textContent = executor.source.slice(-5); + this.editorMessageLabel.append(p3); + } + } else { + this.editorMessageLabel.textContent = executor.source; + } const source = closure.source; this.editorDebugState.innerHTML = ''; let ci = -1; @@ -1169,7 +1192,26 @@ export class QuickReply { 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}`; + if (uuidCheck.test(executor.source)) { + const p1 = document.createElement('span'); { + p1.classList.add('qr--fixed'); + p1.textContent = executor.source.slice(0,5); + src.append(p1); + } + const p2 = document.createElement('span'); { + p2.classList.add('qr--truncated'); + p2.textContent = '…'; + src.append(p2); + } + const p3 = document.createElement('span'); { + p3.classList.add('qr--fixed'); + p3.textContent = `${executor.source.slice(-5)}:${line}`; + src.append(p3); + } + src.title = `anonymous: ${executor.source}`; + } else { + src.textContent = `${executor.source}:${line}`; + } item.append(src); } wrap.append(item); @@ -1227,7 +1269,8 @@ export class QuickReply { if (noSyntax) { this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); } - this.editorDom.querySelector('label[for="qr--modal-message"]').textContent = 'Message / Command: '; + this.editorMessageLabel.innerHTML = ''; + this.editorMessageLabel.textContent = 'Message / Command: '; this.editorMessage.value = oText; this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); this.editorExecutePromise = null; From aed6952a374171cb409b7425b77e3351b39cf158 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jul 2024 08:21:39 -0400 Subject: [PATCH 086/388] align source right --- public/scripts/extensions/quick-reply/style.css | 1 + public/scripts/extensions/quick-reply/style.less | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index b0ed59cd0..83087aadf 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -691,6 +691,7 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source { opacity: 0.5; + text-align: right; } @keyframes qr--progressPulse { 0%, diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index cbb8605f6..76b2243fd 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -770,6 +770,7 @@ } .qr--source { opacity: 0.5; + text-align: right; } } } From 98dfd25ee7dce18ea58e6c266c48c2cb15410e73 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jul 2024 08:21:57 -0400 Subject: [PATCH 087/388] force pipe to stringified JSON if not string or closure --- public/scripts/slash-commands/SlashCommandClosure.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js index 663776acb..e5a9415b8 100644 --- a/public/scripts/slash-commands/SlashCommandClosure.js +++ b/public/scripts/slash-commands/SlashCommandClosure.js @@ -433,8 +433,11 @@ export class SlashCommandClosure { */ #lintPipe(command) { if (this.scope.pipe === undefined || this.scope.pipe === null) { - console.warn(`${command.name} returned undefined or null. Auto-fixing to empty string.`); + console.warn(`/${command.name} returned undefined or null. Auto-fixing to empty string.`); this.scope.pipe = ''; + } else if (!(typeof this.scope.pipe == 'string' || this.scope.pipe instanceof SlashCommandClosure)) { + console.warn(`/${command.name} returned illegal type (${typeof this.scope.pipe} - ${this.scope.pipe.constructor?.name ?? ''}). Auto-fixing to stringified JSON.`); + this.scope.pipe = JSON.stringify(this.scope.pipe) ?? ''; } } } From 25c8002e9ecea3e471012db25197532007d2dc02 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jul 2024 13:28:06 -0400 Subject: [PATCH 088/388] add font-awesome picker popup --- public/scripts/utils.js | 67 ++++++++++++++++++++++++++++++++++++++++- public/style.css | 20 ++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 83207891e..33bf25a78 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -3,7 +3,7 @@ import { getRequestHeaders } from '../script.js'; import { isMobile } from './RossAscends-mods.js'; import { collapseNewlines } from './power-user.js'; import { debounce_timeout } from './constants.js'; -import { Popup } from './popup.js'; +import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; /** * Pagination status string template. @@ -1882,3 +1882,68 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) { } return `${name}${numberFormatter(counter)}`; } + +export async function showFontAwesomePicker() { + const fetchFa = async(name)=>{ + const style = document.createElement('style'); + style.innerHTML = await (await fetch(`/css/${name}`)).text(); + document.head.append(style); + const sheet = style.sheet; + style.remove(); + return [...sheet.cssRules].filter(it=>it.style?.content).map(it=>it.selectorText.split('::').shift().slice(1)); + }; + const faList = [...new Set((await Promise.all([ + fetchFa('fontawesome.min.css'), + ])).flat())]; + const fas = {}; + const dom = document.createElement('div'); { + const search = document.createElement('div'); { + const qry = document.createElement('input'); { + qry.classList.add('text_pole'); + qry.classList.add('faQuery'); + qry.type = 'search'; + qry.placeholder = 'Filter icons'; + qry.autofocus = true; + const qryDebounced = debounce(()=>{ + const result = faList.filter(it=>it.includes(qry.value)); + for (const fa of faList) { + if (!result.includes(fa)) { + fas[fa].classList.add('hidden'); + } else { + fas[fa].classList.remove('hidden'); + } + } + }); + qry.addEventListener('input', qryDebounced); + search.append(qry); + } + dom.append(search); + } + const grid = document.createElement('div'); { + grid.classList.add('faPicker'); + for (const fa of faList) { + const opt = document.createElement('div'); { + fas[fa] = opt; + opt.classList.add('menu_button'); + opt.classList.add('fa-solid'); + opt.classList.add(fa); + opt.title = fa.slice(3); + opt.dataset.result = POPUP_RESULT.AFFIRMATIVE.toString(); + opt.addEventListener('click', ()=>{ + value = fa; + picker.completeAffirmative(); + }); + grid.append(opt); + } + } + dom.append(grid); + } + } + let value; + const picker = new Popup(dom, POPUP_TYPE.CONFIRM, null, { allowVerticalScrolling:true }); + await picker.show(); + if (picker.result == POPUP_RESULT.AFFIRMATIVE) { + return value; + } + return null; +} diff --git a/public/style.css b/public/style.css index b7afd23c9..016ed20f9 100644 --- a/public/style.css +++ b/public/style.css @@ -5322,3 +5322,23 @@ body:not(.movingUI) .drawer-content.maximized { .regex-highlight { color: #FAF8F6; } + +.faPicker { + display: flex; + gap: 1em; + flex-wrap: wrap; + justify-content: space-between; + + .menu_button { + aspect-ratio: 1 / 1; + font-size: 2em; + height: 1lh; + line-height: 1.2; + padding: 0.25em; + width: unset; + box-sizing: content-box; + &.stcdx--hidden { + display: none; + } + } +} From ffd44b622f5043c76f16cd9947e6380b8b67d6a8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jul 2024 13:28:21 -0400 Subject: [PATCH 089/388] add font-awesome icons to QR buttons --- .../extensions/quick-reply/html/qrEditor.html | 12 +- .../extensions/quick-reply/src/QuickReply.js | 141 +++++++++++++++--- .../scripts/extensions/quick-reply/style.css | 32 +++- .../scripts/extensions/quick-reply/style.less | 32 +++- 4 files changed, 193 insertions(+), 24 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 919f9804d..c7a8d6573 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -2,9 +2,19 @@

Labels and Message

+
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 7b69b6bc0..eb948bebc 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -9,7 +9,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; -import { debounce, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; +import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; import { log, warn } from '../index.js'; import { QuickReplyContextLink } from './QuickReplyContextLink.js'; import { QuickReplySet } from './QuickReplySet.js'; @@ -49,6 +49,8 @@ export class QuickReply { /**@type {(qr:QuickReply)=>AsyncGenerator}*/ onDebug; /**@type {function}*/ onDelete; /**@type {function}*/ onUpdate; + /**@type {function}*/ onInsertBefore; + /**@type {function}*/ onTransfer; /**@type {HTMLElement}*/ dom; @@ -173,38 +175,100 @@ export class QuickReply { item.classList.add('qr--set-item'); item.setAttribute('data-order', String(idx)); item.setAttribute('data-id', String(this.id)); - const drag = document.createElement('div'); { - drag.classList.add('drag-handle'); - drag.classList.add('ui-sortable-handle'); - drag.textContent = '☰'; - item.append(drag); - } - const lblContainer = document.createElement('div'); { - lblContainer.classList.add('qr--set-itemLabelContainer'); - const icon = document.createElement('div'); { - this.settingsDomIcon = icon; - icon.title = 'Click to change icon'; - icon.classList.add('qr--set-itemIcon'); - icon.classList.add('menu_button'); - if (this.icon) { - icon.classList.add('fa-solid'); - icon.classList.add(this.icon); + const adder = document.createElement('div'); { + adder.classList.add('qr--set-itemAdder'); + const actions = document.createElement('div'); { + actions.classList.add('qr--actions'); + const addNew = document.createElement('div'); { + addNew.classList.add('qr--action'); + addNew.classList.add('qr--add'); + addNew.classList.add('menu_button'); + addNew.classList.add('menu_button_icon'); + addNew.classList.add('fa-solid'); + addNew.classList.add('fa-plus'); + addNew.title = 'Add quick reply'; + addNew.addEventListener('click', ()=>this.onInsertBefore()); + actions.append(addNew); } - icon.addEventListener('click', async()=>{ - let value = await showFontAwesomePicker(); - this.updateIcon(value); - }); - lblContainer.append(icon); + const paste = document.createElement('div'); { + paste.classList.add('qr--action'); + paste.classList.add('qr--paste'); + paste.classList.add('menu_button'); + paste.classList.add('menu_button_icon'); + paste.classList.add('fa-solid'); + paste.classList.add('fa-paste'); + paste.title = 'Add quick reply from clipboard'; + paste.addEventListener('click', async()=>{ + const text = await navigator.clipboard.readText(); + this.onInsertBefore(text); + }); + actions.append(paste); + } + const importFile = document.createElement('div'); { + importFile.classList.add('qr--action'); + importFile.classList.add('qr--importFile'); + importFile.classList.add('menu_button'); + importFile.classList.add('menu_button_icon'); + importFile.classList.add('fa-solid'); + importFile.classList.add('fa-file-import'); + importFile.title = 'Add quick reply from JSON file'; + importFile.addEventListener('click', async()=>{ + const inp = document.createElement('input'); { + inp.type = 'file'; + inp.accept = '.json'; + inp.addEventListener('change', async()=>{ + if (inp.files.length > 0) { + for (const file of inp.files) { + const text = await file.text(); + this.onInsertBefore(text); + } + } + }); + inp.click(); + } + }); + actions.append(importFile); + } + adder.append(actions); } - const lbl = document.createElement('input'); { - this.settingsDomLabel = lbl; - lbl.classList.add('qr--set-itemLabel'); - lbl.classList.add('text_pole'); - lbl.value = this.label; - lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); - lblContainer.append(lbl); + item.append(adder); + } + const itemContent = document.createElement('div'); { + itemContent.classList.add('qr--content'); + const drag = document.createElement('div'); { + drag.classList.add('drag-handle'); + drag.classList.add('ui-sortable-handle'); + drag.textContent = '☰'; + itemContent.append(drag); } - item.append(lblContainer); + const lblContainer = document.createElement('div'); { + lblContainer.classList.add('qr--set-itemLabelContainer'); + const icon = document.createElement('div'); { + this.settingsDomIcon = icon; + icon.title = 'Click to change icon'; + icon.classList.add('qr--set-itemIcon'); + icon.classList.add('menu_button'); + if (this.icon) { + icon.classList.add('fa-solid'); + icon.classList.add(this.icon); + } + icon.addEventListener('click', async()=>{ + let value = await showFontAwesomePicker(); + this.updateIcon(value); + }); + lblContainer.append(icon); + } + const lbl = document.createElement('input'); { + this.settingsDomLabel = lbl; + lbl.classList.add('qr--set-itemLabel'); + lbl.classList.add('text_pole'); + lbl.value = this.label; + lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); + lblContainer.append(lbl); + } + itemContent.append(lblContainer); + } + item.append(itemContent); } const optContainer = document.createElement('div'); { optContainer.classList.add('qr--set-optionsContainer'); @@ -217,7 +281,7 @@ export class QuickReply { opt.addEventListener('click', ()=>this.showEditor()); optContainer.append(opt); } - item.append(optContainer); + itemContent.append(optContainer); } const mes = document.createElement('textarea'); { this.settingsDomMessage = mes; @@ -226,10 +290,66 @@ export class QuickReply { mes.value = this.message; //HACK need to use jQuery to catch the triggered event from the expanded editor $(mes).on('input', ()=>this.updateMessage(mes.value)); - item.append(mes); + itemContent.append(mes); } const actions = document.createElement('div'); { actions.classList.add('qr--actions'); + const move = document.createElement('div'); { + move.classList.add('qr--action'); + move.classList.add('menu_button'); + move.classList.add('menu_button_icon'); + move.classList.add('fa-solid'); + move.classList.add('fa-truck-arrow-right'); + move.title = 'Move quick reply to other set'; + move.addEventListener('click', ()=>this.onTransfer(this)); + actions.append(move); + } + const copy = document.createElement('div'); { + copy.classList.add('qr--action'); + copy.classList.add('menu_button'); + copy.classList.add('menu_button_icon'); + copy.classList.add('fa-solid'); + copy.classList.add('fa-copy'); + copy.title = 'Copy quick reply to clipboard'; + copy.addEventListener('click', async()=>{ + await navigator.clipboard.writeText(JSON.stringify(this)); + copy.classList.add('qr--success'); + await delay(3010); + copy.classList.remove('qr--success'); + }); + actions.append(copy); + } + const cut = document.createElement('div'); { + cut.classList.add('qr--action'); + cut.classList.add('menu_button'); + cut.classList.add('menu_button_icon'); + cut.classList.add('fa-solid'); + cut.classList.add('fa-cut'); + cut.title = 'Cut quick reply to clipboard (copy and remove)'; + cut.addEventListener('click', async()=>{ + await navigator.clipboard.writeText(JSON.stringify(this)); + this.delete(); + }); + actions.append(cut); + } + const exp = document.createElement('div'); { + exp.classList.add('qr--action'); + exp.classList.add('menu_button'); + exp.classList.add('menu_button_icon'); + exp.classList.add('fa-solid'); + exp.classList.add('fa-file-export'); + exp.title = 'Export quick reply as file'; + exp.addEventListener('click', ()=>{ + const blob = new Blob([JSON.stringify(this)], { type:'text' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); { + a.href = url; + a.download = `${this.label}.qr.json`; + a.click(); + } + }); + actions.append(exp); + } const del = document.createElement('div'); { del.classList.add('qr--action'); del.classList.add('menu_button'); @@ -241,7 +361,7 @@ export class QuickReply { del.addEventListener('click', ()=>this.delete()); actions.append(del); } - item.append(actions); + itemContent.append(actions); } } } @@ -1469,6 +1589,7 @@ export class QuickReply { const scope = new SlashCommandScope(); for (const key of Object.keys(args)) { if (key[0] == '_') continue; + if (key == 'isAutoExecute') continue; scope.setMacro(`arg::${key}`, args[key]); } scope.setMacro('arg::*', ''); diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 32b3761c0..2c803dc8d 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -1,8 +1,9 @@ import { getRequestHeaders, substituteParams } from '../../../../script.js'; +import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.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 { debounceAsync, log, warn } from '../index.js'; import { QuickReply } from './QuickReply.js'; export class QuickReplySet { @@ -94,6 +95,11 @@ export class QuickReplySet { } return this.settingsDom; } + /** + * + * @param {QuickReply} qr + * @param {number} idx + */ renderSettingsItem(qr, idx) { this.settingsDom.append(qr.renderSettings(idx)); } @@ -198,10 +204,11 @@ export class QuickReplySet { - addQuickReply() { + addQuickReply(data = {}) { const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1; + data.id = this.idIndex = id + 1; - const qr = QuickReply.from({ id }); + const qr = QuickReply.from(data); this.qrList.push(qr); this.hookQuickReply(qr); if (this.settingsDom) { @@ -223,6 +230,98 @@ export class QuickReplySet { qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); qr.onDelete = ()=>this.removeQuickReply(qr); qr.onUpdate = ()=>this.save(); + qr.onInsertBefore = (qrJson)=>{ + const data = JSON.parse(qrJson ?? '{}'); + delete data.id; + log('onInsertBefore', data); + const newQr = this.addQuickReply(data); + this.qrList.pop(); + this.qrList.splice(this.qrList.indexOf(qr), 0, newQr); + if (qr.settingsDom) { + qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom); + } + this.save(); + }; + qr.onTransfer = async()=>{ + /**@type {HTMLSelectElement} */ + let sel; + let isCopy = false; + const dom = document.createElement('div'); { + dom.classList.add('qr--transferModal'); + const title = document.createElement('h3'); { + title.textContent = 'Transfer Quick Reply'; + dom.append(title); + } + const subTitle = document.createElement('h4'); { + const entryName = qr.label; + const bookName = this.name; + subTitle.textContent = `${bookName}: ${entryName}`; + dom.append(subTitle); + } + sel = document.createElement('select'); { + sel.classList.add('qr--transferSelect'); + sel.setAttribute('autofocus', '1'); + const noOpt = document.createElement('option'); { + noOpt.value = ''; + noOpt.textContent = '-- Select QR Set --'; + sel.append(noOpt); + } + for (const qrs of QuickReplySet.list) { + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + sel.append(opt); + } + } + sel.addEventListener('keyup', (evt)=>{ + if (evt.key == 'Shift') { + (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); + return; + } + }); + sel.addEventListener('keydown', (evt)=>{ + if (evt.key == 'Shift') { + (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); + return; + } + if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') { + evt.preventDefault(); + if (evt.shiftKey) isCopy = true; + dlg.completeAffirmative(); + } + }); + dom.append(sel); + } + const hintP = document.createElement('p'); { + const hint = document.createElement('small'); { + hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.'; + hintP.append(hint); + } + dom.append(hintP); + } + } + const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' }); + const copyBtn = document.createElement('div'); { + copyBtn.classList.add('qr--copy'); + copyBtn.classList.add('menu_button'); + copyBtn.textContent = 'Copy'; + copyBtn.addEventListener('click', ()=>{ + isCopy = true; + dlg.completeAffirmative(); + }); + (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); + } + const prom = dlg.show(); + sel.focus(); + await prom; + if (dlg.result == POPUP_RESULT.AFFIRMATIVE) { + const qrs = QuickReplySet.list.find(it=>it.name == sel.value); + qrs.addQuickReply(qr.toJSON()); + if (!isCopy) { + qr.delete(); + } + } + }; } removeQuickReply(qr) { diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 96d1208ce..3bcf73d66 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -117,6 +117,25 @@ export class SettingsUi { this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ this.currentQrSet.addQuickReply(); }); + this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{ + const text = await navigator.clipboard.readText(); + this.currentQrSet.addQuickReply(JSON.parse(text)); + }); + this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{ + const inp = document.createElement('input'); { + inp.type = 'file'; + inp.accept = '.json'; + inp.addEventListener('change', async()=>{ + if (inp.files.length > 0) { + for (const file of inp.files) { + const text = await file.text(); + this.currentQrSet.addQuickReply(JSON.parse(text)); + } + } + }); + inp.click(); + } + }); this.qrList = this.dom.querySelector('#qr--set-qrList'); this.currentSet = this.dom.querySelector('#qr--set'); this.currentSet.addEventListener('change', ()=>this.onQrSetChange()); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index da49a3746..8fbed0e99 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -1,3 +1,20 @@ +@keyframes qr--success { + 0%, + 100% { + color: var(--SmartThemeBodyColor); + } + 25%, + 75% { + color: #51a351; + } +} +.qr--success { + animation-name: qr--success; + animation-duration: 3s; + animation-timing-function: linear; + animation-delay: 0s; + animation-iteration-count: 1; +} #qr--bar { outline: none; margin: 0; @@ -178,43 +195,75 @@ #qr--settings #qr--set-qrList .qr--set-qrListContents { padding: 0 0.5em; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder { + display: flex; + align-items: center; + opacity: 0; + transition: 100ms; + margin: -3px 0 -12px 0; + position: relative; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions { + display: flex; + gap: 0.25em; + flex: 0 0 auto; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action { + margin: 0; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before, +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after { + content: ""; + display: block; + flex: 1 1 auto; + border: 1px solid; + margin: 0 1em; + height: 0; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover { + opacity: 1; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content { display: flex; flex-direction: row; gap: 0.5em; align-items: baseline; padding: 0.25em 0; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) { flex: 0 0 auto; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) { flex: 1 1 25%; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) { flex: 0 0 auto; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) { flex: 1 1 75%; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) { - flex: 0 0 auto; +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) { + flex: 0 1 auto; + display: flex; + gap: 0.25em; + justify-content: flex-end; + flex-wrap: wrap; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle { padding: 0.75em; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabelContainer { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer { display: flex; align-items: center; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) { display: none; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel, -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel, +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action { margin: 0; } -#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage { +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage { font-size: smaller; } #qr--settings .qr--set-qrListActions { @@ -737,3 +786,45 @@ .popup.qr--hide::backdrop { opacity: 0 !important; } +.popup:has(.qr--transferModal) .popup-button-ok { + display: flex; + align-items: center; + flex-direction: column; + white-space: pre; + font-weight: normal; + box-shadow: 0 0 0; + transition: 200ms; +} +.popup:has(.qr--transferModal) .popup-button-ok:after { + content: 'Transfer'; + height: 0; + overflow: hidden; + font-weight: bold; +} +.popup:has(.qr--transferModal) .qr--copy { + display: flex; + align-items: center; + flex-direction: column; + white-space: pre; + font-weight: normal; + box-shadow: 0 0 0; + transition: 200ms; +} +.popup:has(.qr--transferModal) .qr--copy:after { + content: 'Copy'; + height: 0; + overflow: hidden; + font-weight: bold; +} +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok { + font-weight: bold; + box-shadow: 0 0 10px; +} +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok { + font-weight: normal; + box-shadow: 0 0 0; +} +.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy { + font-weight: bold; + box-shadow: 0 0 10px; +} diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 740987bac..a321abd63 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -1,3 +1,18 @@ +@keyframes qr--success { + 0%, 100% { + color: var(--SmartThemeBodyColor); + } + 25%, 75% { + color: rgb(81, 163, 81); + } +} +&.qr--success { + animation-name: qr--success; + animation-duration: 3s; + animation-timing-function: linear; + animation-delay: 0s; + animation-iteration-count: 1; +} #qr--bar { outline: none; margin: 0; @@ -218,14 +233,41 @@ .qr--set-qrListContents> { padding: 0 0.5em; - >.qr--set-item { + >.qr--set-item .qr--set-itemAdder { + display: flex; + align-items: center; + opacity: 0; + transition: 100ms; + margin: -3px 0 -12px 0; + position: relative; + .qr--actions { + display: flex; + gap: 0.25em; + flex: 0 0 auto; + .qr--action { + margin: 0; + } + } + &:before, &:after { + content: ""; + display: block; + flex: 1 1 auto; + border: 1px solid; + margin: 0 1em; + height: 0; + } + &:hover { + opacity: 1; + } + } + >.qr--set-item .qr--content { display: flex; flex-direction: row; gap: 0.5em; align-items: baseline; padding: 0.25em 0; - > :nth-child(1) { + > :nth-child(2) { flex: 0 0 auto; } @@ -242,7 +284,11 @@ } > :nth-child(5) { - flex: 0 0 auto; + flex: 0 1 auto; + display: flex; + gap: 0.25em; + justify-content: flex-end; + flex-wrap: wrap; } >.drag-handle { @@ -266,6 +312,8 @@ font-size: smaller; } } + + } } @@ -827,3 +875,54 @@ .popup.qr--hide::backdrop { opacity: 0 !important; } + + + +.popup:has(.qr--transferModal) { + .popup-button-ok { + &:after { + content: 'Transfer'; + height: 0; + overflow: hidden; + font-weight: bold; + } + display: flex; + align-items: center; + flex-direction: column; + white-space: pre; + font-weight: normal; + box-shadow: 0 0 0; + transition: 200ms; + } + .qr--copy { + &:after { + content: 'Copy'; + height: 0; + overflow: hidden; + font-weight: bold; + } + display: flex; + align-items: center; + flex-direction: column; + white-space: pre; + font-weight: normal; + box-shadow: 0 0 0; + transition: 200ms; + } + &:has(.qr--transferSelect:focus) { + .popup-button-ok { + font-weight: bold; + box-shadow: 0 0 10px; + } + &.qr--isCopy { + .popup-button-ok { + font-weight: normal; + box-shadow: 0 0 0; + } + .qr--copy { + font-weight: bold; + box-shadow: 0 0 10px; + } + } + } +} From 4ecfa53b3e80df1ba59062b36f5097754c58e3d8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 17:56:34 -0400 Subject: [PATCH 091/388] fix no op when adding QR set to global/chat and first set already included --- .../scripts/extensions/quick-reply/src/QuickReplyConfig.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index ace66ec47..5c23a6443 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -60,7 +60,12 @@ export class QuickReplyConfig { /**@type {HTMLElement}*/ this.setListDom = root.querySelector('.qr--setList'); root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{ - this.addSet(QuickReplySet.list[0]); + const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr)); + if (newSet) { + this.addSet(newSet); + } else { + toastr.warning('All existing QR Sets have already been added.'); + } }); this.updateSetListDom(); } From 5df932a76d6fe3dbd2db7acac788e23fe8788f0f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 18:44:58 -0400 Subject: [PATCH 092/388] add icon support to QR context menu --- .../quick-reply/src/ui/ctx/ContextMenu.js | 4 ++ .../quick-reply/src/ui/ctx/MenuHeader.js | 2 +- .../quick-reply/src/ui/ctx/MenuItem.js | 41 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js index fa695e672..f1e0b99ff 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js @@ -33,6 +33,8 @@ export class ContextMenu { */ build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) { const tree = { + icon: qr.icon, + showLabel: qr.showLabel, label: qr.label, message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message, children: [], @@ -45,6 +47,8 @@ export class ContextMenu { cl.set.qrList.forEach(subQr => { const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy); tree.children.push(new MenuItem( + subTree.icon, + subTree.showLabel, subTree.label, subTree.message, (evt) => { diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js index f1f38c83c..2fe6f7a68 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js @@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js'; export class MenuHeader extends MenuItem { constructor(/**@type {String}*/label) { - super(label, null, null); + super(null, null, label, null, null); } diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js index d72fad310..2b4daba99 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js @@ -1,21 +1,34 @@ import { SubMenu } from './SubMenu.js'; export class MenuItem { - /**@type {String}*/ label; - /**@type {Object}*/ value; - /**@type {Function}*/ callback; + /**@type {string}*/ icon; + /**@type {boolean}*/ showLabel; + /**@type {string}*/ label; + /**@type {object}*/ value; + /**@type {function}*/ callback; /**@type {MenuItem[]}*/ childList = []; /**@type {SubMenu}*/ subMenu; - /**@type {Boolean}*/ isForceExpanded = false; + /**@type {boolean}*/ isForceExpanded = false; /**@type {HTMLElement}*/ root; - /**@type {Function}*/ onExpand; + /**@type {function}*/ onExpand; - constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) { + /** + * + * @param {string} icon + * @param {boolean} showLabel + * @param {string} label + * @param {object} value + * @param {function} callback + * @param {MenuItem[]} children + */ + constructor(icon, showLabel, label, value, callback, children = []) { + this.icon = icon; + this.showLabel = showLabel; this.label = label; this.value = value; this.callback = callback; @@ -33,7 +46,21 @@ export class MenuItem { if (this.callback) { item.addEventListener('click', (evt) => this.callback(evt, this)); } - item.append(this.label); + const icon = document.createElement('div'); { + this.domIcon = icon; + icon.classList.add('qr--button-icon'); + icon.classList.add('fa-solid'); + if (!this.icon) icon.classList.add('qr--hidden'); + else icon.classList.add(this.icon); + item.append(icon); + } + const lbl = document.createElement('div'); { + this.domLabel = lbl; + lbl.classList.add('qr--button-label'); + if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); + lbl.textContent = this.label; + item.append(lbl); + } if (this.childList.length > 0) { item.classList.add('ctx-has-children'); const sub = new SubMenu(this.childList); From 977d98e7e8a12c5ee7eb995a59223259c177cfc7 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 19:52:13 -0400 Subject: [PATCH 093/388] add /import to import closures from other QRs --- .../quick-reply/src/SlashCommandHandler.js | 103 ++++++++++++++++++ .../slash-commands/SlashCommandParser.js | 11 ++ public/style.css | 1 + 3 files changed, 115 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index fe3bcf0e3..16c3607c4 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -1,8 +1,11 @@ import { SlashCommand } from '../../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js'; +import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js'; +import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; +import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { isTrueBoolean } from '../../../utils.js'; // eslint-disable-next-line no-unused-vars import { QuickReplyApi } from '../api/QuickReplyApi.js'; @@ -554,6 +557,106 @@ export class SlashCommandHandler {
`, })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import', + /** + * + * @param {{_scope:SlashCommandScope, _debugController:SlashCommandDebugController, from:string}} args + * @param {string} value + */ + callback: (args, value) => { + if (!args.from) throw new Error('/import requires from= to be set.'); + if (!value) throw new Error('/import requires the unnamed argument to be set.'); + let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()] + .map(it=>this.api.getSetByName(it)?.qrList ?? []) + .flat() + .find(it=>it.label == args.from) + ; + if (!qr) { + let [setName, ...qrNameParts] = args.from.split('.'); + let qrName = qrNameParts.join('.'); + let qrs = QuickReplySet.get(setName); + if (qrs) { + qr = qrs.qrList.find(it=>it.label == qrName); + } + } + if (qr) { + const parser = new SlashCommandParser(); + const closure = parser.parse(qr.message, true, [], null, args._debugController); + if (args._debugController) { + closure.source = args.from; + } + const testCandidates = (executor)=>{ + return ( + executor.namedArgumentList.find(arg=>arg.name == 'key') + && executor.unnamedArgumentList.length > 0 + && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure + ) || ( + !executor.namedArgumentList.find(arg=>arg.name == 'key') + && executor.unnamedArgumentList.length > 1 + && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure + ); + }; + const candidates = closure.executorList + .filter(executor=>['let', 'var'].includes(executor.command.name)) + .filter(testCandidates) + .map(executor=>({ + key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value, + value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value, + })) + ; + for (let i = 0; i < value.length; i++) { + const srcName = value[i]; + let dstName = srcName; + if (i + 2 < value.length && value[i + 1] == 'as') { + dstName = value[i + 2]; + i += 2; + } + const pick = candidates.find(it=>it.key == srcName); + if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`); + if (args._scope.existsVariableInScope(dstName)) { + args._scope.setVariable(dstName, pick.value); + } else { + args._scope.letVariable(dstName, pick.value); + } + } + } else { + throw new Error(`No Quick Reply found for "${name}".`); + } + return ''; + }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ name: 'from', + description: 'Quick Reply to import from (QRSet.QRLabel)', + typeList: ARGUMENT_TYPE.STRING, + isRequired: true, + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)', + acceptsMultiple: true, + typeList: ARGUMENT_TYPE.STRING, + isRequired: true, + }), + ], + splitUnnamedArgument: true, + helpString: ` +
+ Import one or more closures from another Quick Reply. +
+
+ Only imports closures that are directly assigned a scoped variable via /let or /var. +
+
+ Examples: +
    +
  • /import from=LibraryQrSet.FooBar foo |\n/:foo
  • +
  • /import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar
  • +
  • /import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y
  • +
+
+ `, + })); } diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 06bffd642..90243203d 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -861,6 +861,17 @@ export class SlashCommandParser { } else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') { this.scope.variableNames.push(cmd.unnamedArgumentList[0].value); } + } else if (cmd.name == 'import') { + const value = /**@type {string[]}*/(cmd.unnamedArgumentList.map(it=>it.value)); + for (let i = 0; i < value.length; i++) { + const srcName = value[i]; + let dstName = srcName; + if (i + 2 < value.length && value[i + 1] == 'as') { + dstName = value[i + 2]; + i += 2; + } + this.scope.variableNames.push(dstName); + } } } if (this.testCommandEnd()) { diff --git a/public/style.css b/public/style.css index 7725c2fa4..f28d0d3a2 100644 --- a/public/style.css +++ b/public/style.css @@ -1872,6 +1872,7 @@ body[data-stscript-style] .hljs.language-stscript { >code { display: block; padding: 1px; + tab-size: 4; } } } From 4396d31d094582f3f488791f0a18e64f34823a4b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 20:53:09 -0400 Subject: [PATCH 094/388] better quoted unnamed arg handling --- .../slash-commands/SlashCommandParser.js | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 90243203d..8e40c94d0 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -913,24 +913,59 @@ export class SlashCommandParser { return this.testCommandEnd(); } parseUnnamedArgument(split, splitCount = null) { + const wasSplit = split; /**@type {SlashCommandClosure|String}*/ let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one let isList = split; let listValues = []; + let listQuoted = []; // keep track of which listValues were quoted /**@type {SlashCommandUnnamedArgumentAssignment}*/ let assignment = new SlashCommandUnnamedArgumentAssignment(); assignment.start = this.index; + if (!split && this.testQuotedValue()) { + // if the next bit is a quoted value, take the whole value and gather contents as a list + assignment.value = this.parseQuotedValue(); + assignment.end = this.index; + isList = true; + listValues.push(assignment); + listQuoted.push(true); + assignment = new SlashCommandUnnamedArgumentAssignment(); + assignment.start = this.index; + } while (!this.testUnnamedArgumentEnd()) { - if (split && splitCount && listValues.length >= splitCount) split = false; + if (split && splitCount && listValues.length >= splitCount) { + // the split count has just been reached: stop splitting, the rest is one singular value + split = false; + if (this.testQuotedValue()) { + // if the next bit is a quoted value, take the whole value + assignment.value = this.parseQuotedValue(); + assignment.end = this.index; + listValues.push(assignment); + listQuoted.push(true); + assignment = new SlashCommandUnnamedArgumentAssignment(); + assignment.start = this.index; + } + } if (this.testClosure()) { isList = true; if (value.length > 0) { this.indexMacros(this.index - value.length, value); assignment.value = value; listValues.push(assignment); + listQuoted.push(false); assignment = new SlashCommandUnnamedArgumentAssignment(); assignment.start = this.index; - value = ''; + if (!split && this.testQuotedValue()) { + // if where currently not splitting and the next bit is a quoted value, take the whole value + assignment.value = this.parseQuotedValue(); + assignment.end = this.index; + listValues.push(assignment); + listQuoted.push(true); + assignment = new SlashCommandUnnamedArgumentAssignment(); + assignment.start = this.index; + } else { + value = ''; + } } assignment.start = this.index; assignment.value = this.parseClosure(); @@ -945,18 +980,21 @@ export class SlashCommandParser { assignment.value = this.parseQuotedValue(); assignment.end = this.index; listValues.push(assignment); + listQuoted.push(true); assignment = new SlashCommandUnnamedArgumentAssignment(); } else if (this.testListValue()) { assignment.start = this.index; assignment.value = this.parseListValue(); assignment.end = this.index; listValues.push(assignment); + listQuoted.push(false); assignment = new SlashCommandUnnamedArgumentAssignment(); } else if (this.testValue()) { assignment.start = this.index; assignment.value = this.parseValue(); assignment.end = this.index; listValues.push(assignment); + listQuoted.push(false); assignment = new SlashCommandUnnamedArgumentAssignment(); } else { throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`); @@ -970,22 +1008,48 @@ export class SlashCommandParser { if (isList && value.length > 0) { assignment.value = value; listValues.push(assignment); + listQuoted.push(false); } if (isList) { const firstVal = listValues[0]; if (typeof firstVal.value == 'string') { - firstVal.value = firstVal.value.trimStart(); + if (!listQuoted[0]) { + // only trim the first part if it wasn't quoted + firstVal.value = firstVal.value.trimStart(); + } if (firstVal.value.length == 0) { listValues.shift(); + listQuoted.shift(); } } const lastVal = listValues.slice(-1)[0]; if (typeof lastVal.value == 'string') { - lastVal.value = lastVal.value.trimEnd(); + if (!listQuoted.slice(-1)[0]) { + // only trim the last part if it wasn't quoted + lastVal.value = lastVal.value.trimEnd(); + } if (lastVal.value.length == 0) { listValues.pop(); + listQuoted.pop(); } } + if (wasSplit && splitCount && splitCount + 1 < listValues.length) { + // if split with a split count and there are more values than expected + // -> should be result of quoting + additional (non-whitespace) text + // -> join the parts into one and restore quotes + const joined = new SlashCommandUnnamedArgumentAssignment(); + joined.start = listValues[splitCount].start; + joined.end = listValues.slice(-1)[0].end; + joined.value = ''; + for (let i = splitCount; i < listValues.length; i++) { + if (listQuoted[i]) joined.value += `"${listValues[i].value}"`; + else joined.value += listValues[i].value; + } + listValues = [ + ...listValues.slice(0, splitCount), + joined, + ]; + } return listValues; } this.indexMacros(this.index - value.length, value); From ec140f4a9754219a267bd9bcaff60f1676722529 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 22:38:37 -0400 Subject: [PATCH 095/388] add makeSelectable --- public/scripts/slash-commands/SlashCommandEnumValue.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandEnumValue.js b/public/scripts/slash-commands/SlashCommandEnumValue.js index 4ff32d515..a13399f32 100644 --- a/public/scripts/slash-commands/SlashCommandEnumValue.js +++ b/public/scripts/slash-commands/SlashCommandEnumValue.js @@ -40,8 +40,8 @@ export const enumTypes = { getBasedOnIndex(index) { const keys = Object.keys(this); return this[keys[(index ?? 0) % keys.length]]; - } -} + }, +}; export class SlashCommandEnumValue { /**@type {string}*/ value; @@ -50,6 +50,7 @@ export class SlashCommandEnumValue { /**@type {string}*/ typeIcon = '◊'; /**@type {(input:string)=>boolean}*/ matchProvider; /**@type {(input:string)=>string}*/ valueProvider; + /**@type {boolean}*/ makeSelectable = false; /** * A constructor for creating a SlashCommandEnumValue instance. @@ -59,13 +60,14 @@ export class SlashCommandEnumValue { * @param {EnumType?} type - type of the enum (defining its color) * @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) */ - constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null) { + constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null, makeSelectable = false) { this.value = value; this.description = description; this.type = type ?? 'enum'; this.typeIcon = typeIcon; this.matchProvider = matchProvider; this.valueProvider = valueProvider; + this.makeSelectable = makeSelectable; } toString() { From 182da4c466f2fe177b91645cc9d0ccdb5b8ef1a1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 22:38:50 -0400 Subject: [PATCH 096/388] fix startUnnamedArgs --- public/scripts/slash-commands/SlashCommandParser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 8e40c94d0..a79bcdafa 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -849,7 +849,7 @@ export class SlashCommandParser { this.discardWhitespace(); } this.discardWhitespace(); - cmd.startUnnamedArgs = this.index - /\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0; + cmd.startUnnamedArgs = this.index - (/\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0); cmd.endUnnamedArgs = this.index; if (this.testUnnamedArgument()) { cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount); From 36265579a28cc627d2e52921a68e3fd594aaa2d1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 22:39:04 -0400 Subject: [PATCH 097/388] add makeSelectable --- .../slash-commands/SlashCommandEnumAutoCompleteOption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js index fe387e8ca..0c04d9c62 100644 --- a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js +++ b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js @@ -13,7 +13,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption { * @param {SlashCommandEnumValue} enumValue */ constructor(cmd, enumValue) { - super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider); + super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider, enumValue.makeSelectable); this.cmd = cmd; this.enumValue = enumValue; } From 1a18b5b180168ec002bf68914e91f62cb9f0ddfb Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 10 Jul 2024 22:39:21 -0400 Subject: [PATCH 098/388] add referencing QRs by ID --- .../quick-reply/api/QuickReplyApi.js | 129 +++++++++--------- .../quick-reply/src/SlashCommandHandler.js | 99 +++++++++++++- 2 files changed, 161 insertions(+), 67 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 85ff2da73..2e4260147 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -26,7 +26,7 @@ export class QuickReplyApi { /** * Finds and returns an existing Quick Reply Set by its name. * - * @param {String} name name of the quick reply set + * @param {string} name name of the quick reply set * @returns the quick reply set, or undefined if not found */ getSetByName(name) { @@ -36,13 +36,14 @@ export class QuickReplyApi { /** * Finds and returns an existing Quick Reply by its set's name and its label. * - * @param {String} setName name of the quick reply set - * @param {String} label label of the quick reply + * @param {string} setName name of the quick reply set + * @param {string|number} label label or numeric ID of the quick reply * @returns the quick reply, or undefined if not found */ getQrByLabel(setName, label) { const set = this.getSetByName(setName); if (!set) return; + if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label); return set.qrList.find(it=>it.label == label); } @@ -70,9 +71,9 @@ export class QuickReplyApi { /** * Executes an existing quick reply. * - * @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 {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID + * @param {object} [args] optional arguments * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options */ async executeQuickReply(setName, label, args = {}, options = {}) { @@ -87,8 +88,8 @@ export class QuickReplyApi { /** * Adds or removes a quick reply set to the list of globally active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ toggleGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); @@ -105,8 +106,8 @@ export class QuickReplyApi { /** * Adds a quick reply set to the list of globally active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ addGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); @@ -119,7 +120,7 @@ export class QuickReplyApi { /** * Removes a quick reply set from the list of globally active quick reply sets. * - * @param {String} name the name of the set + * @param {string} name the name of the set */ removeGlobalSet(name) { const set = this.getSetByName(name); @@ -133,8 +134,8 @@ export class QuickReplyApi { /** * Adds or removes a quick reply set to the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ toggleChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; @@ -152,8 +153,8 @@ export class QuickReplyApi { /** * Adds a quick reply set to the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set - * @param {Boolean} isVisible whether to show the set's buttons or not + * @param {string} name the name of the set + * @param {boolean} isVisible whether to show the set's buttons or not */ addChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; @@ -167,7 +168,7 @@ export class QuickReplyApi { /** * Removes a quick reply set from the list of the current chat's active quick reply sets. * - * @param {String} name the name of the set + * @param {string} name the name of the set */ removeChatSet(name) { if (!this.settings.chatConfig) return; @@ -182,18 +183,18 @@ export class QuickReplyApi { /** * Creates a new quick reply in an existing quick reply set. * - * @param {String} setName name of the quick reply set to insert the new quick reply into - * @param {String} label label for the new quick reply (text on the button) - * @param {Object} [props] - * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply - * @param {String} [props.title] the title / tooltip to be shown on the quick reply button - * @param {Boolean} [props.isHidden] whether to hide or show the button - * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts - * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message - * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message - * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded - * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected - * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated + * @param {string} setName name of the quick reply set to insert the new quick reply into + * @param {string} label label for the new quick reply (text on the button) + * @param {object} [props] + * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply + * @param {string} [props.title] the title / tooltip to be shown on the quick reply button + * @param {boolean} [props.isHidden] whether to hide or show the button + * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated * @returns {QuickReply} the new quick reply */ createQuickReply(setName, label, { @@ -229,19 +230,19 @@ export class QuickReplyApi { /** * Updates an existing quick reply. * - * @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} [props] - * @param {String} [props.newLabel] new label for quick reply (text on the button) - * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply - * @param {String} [props.title] the title / tooltip to be shown on the quick reply button - * @param {Boolean} [props.isHidden] whether to hide or show the button - * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts - * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message - * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message - * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded - * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected - * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated + * @param {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID + * @param {object} [props] + * @param {string} [props.newLabel] new label for quick reply (text on the button) + * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply + * @param {string} [props.title] the title / tooltip to be shown on the quick reply button + * @param {boolean} [props.isHidden] whether to hide or show the button + * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected + * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated * @returns {QuickReply} the altered quick reply */ updateQuickReply(setName, label, { @@ -277,8 +278,8 @@ export class QuickReplyApi { /** * Deletes an existing quick reply. * - * @param {String} setName name of the existing quick reply set - * @param {String} label label of the existing quick reply (text on the button) + * @param {string} setName name of the existing quick reply set + * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID */ deleteQuickReply(setName, label) { const qr = this.getQrByLabel(setName, label); @@ -292,10 +293,10 @@ export class QuickReplyApi { /** * Adds an existing quick reply set as a context menu to an existing quick reply. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply - * @param {String} contextSetName name of the existing quick reply set to be used as a context menu - * @param {Boolean} isChained whether or not to chain the context menu quick replies + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID + * @param {string} contextSetName name of the existing quick reply set to be used as a context menu + * @param {boolean} isChained whether or not to chain the context menu quick replies */ createContextItem(setName, label, contextSetName, isChained = false) { const qr = this.getQrByLabel(setName, label); @@ -315,9 +316,9 @@ export class QuickReplyApi { /** * Removes a quick reply set from a quick reply's context menu. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply - * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID + * @param {string} contextSetName name of the existing quick reply set to be used as a context menu */ deleteContextItem(setName, label, contextSetName) { const qr = this.getQrByLabel(setName, label); @@ -334,8 +335,8 @@ export class QuickReplyApi { /** * Removes all entries from a quick reply's context menu. * - * @param {String} setName name of the existing quick reply set containing the quick reply - * @param {String} label label of the existing quick reply + * @param {string} setName name of the existing quick reply set containing the quick reply + * @param {string|number} label label of the existing quick reply or its numeric ID */ clearContextMenu(setName, label) { const qr = this.getQrByLabel(setName, label); @@ -349,11 +350,11 @@ export class QuickReplyApi { /** * Create a new quick reply set. * - * @param {String} name name of the new quick reply set - * @param {Object} [props] - * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box - * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input - * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @param {string} name name of the new quick reply set + * @param {object} [props] + * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply * @returns {Promise} the new quick reply set */ async createSet(name, { @@ -385,11 +386,11 @@ export class QuickReplyApi { /** * Update an existing quick reply set. * - * @param {String} name name of the existing quick reply set - * @param {Object} [props] - * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box - * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input - * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @param {string} name name of the existing quick reply set + * @param {object} [props] + * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply * @returns {Promise} the altered quick reply set */ async updateSet(name, { @@ -412,7 +413,7 @@ export class QuickReplyApi { /** * Delete an existing quick reply set. * - * @param {String} name name of the existing quick reply set + * @param {string} name name of the existing quick reply set */ async deleteSet(name) { const set = this.getSetByName(name); @@ -452,7 +453,7 @@ export class QuickReplyApi { /** * Gets a list of all quick replies in the quick reply set. * - * @param {String} setName name of the existing quick reply set + * @param {string} setName name of the existing quick reply set * @returns array with the labels of this set's quick replies */ listQuickReplies(setName) { diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 16c3607c4..4f90464ea 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -50,6 +50,13 @@ export class SlashCommandHandler { return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr); }) ?? [], + /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */ + qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => { + const icons = getExecutionIcons(qr); + const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim(); + return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true); + }) ?? [], + /** All QRs as a set.name string, to be able to execute, for example via the /run command */ qrExecutables: () => { const globalSetList = this.api.settings.config.setList; @@ -237,8 +244,8 @@ export class SlashCommandHandler { name: 'label', description: 'text on the button, e.g., label=MyButton', typeList: [ARGUMENT_TYPE.STRING], - isRequired: true, - enumProvider: localEnumProviders.qrLabels, + isRequired: false, + enumProvider: localEnumProviders.qrEntries, }), new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), @@ -250,6 +257,13 @@ export class SlashCommandHandler { ]; const qrUpdateArgs = [ new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + isRequired: false, + enumProvider: localEnumProviders.qrIds, + }), ]; SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create', @@ -275,13 +289,61 @@ export class SlashCommandHandler {
`, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get', + callback: (args, _) => { + return this.getQuickReply(args); + }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'set', + description: 'name of the QR set, e.g., set=PresetName1', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: localEnumProviders.qrSets, + }), + SlashCommandNamedArgument.fromProps({ + name: 'label', + description: 'text on the button, e.g., label=MyButton', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + enumProvider: localEnumProviders.qrEntries, + }), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + isRequired: false, + enumProvider: localEnumProviders.qrIds, + }), + ], + returns: 'a dictionary with all the QR\'s properties', + helpString: ` +
Get a Quick Reply's properties.
+
+ Examples: +
    +
  • +
    /qr-get set=MyPreset label=MyButton | /echo
    +
    /qr-get set=MyPreset id=42 | /echo
    +
  • +
+
+ `, + })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update', callback: (args, message) => { this.updateQuickReply(args, message); return ''; }, returns: 'updated quick reply', - namedArgumentList: [...qrUpdateArgs, ...qrArgs], + namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it=>{ + if (it.name == 'label') { + const clone = SlashCommandNamedArgument.fromProps(it); + clone.isRequired = false; + return clone; + } + return it; + })], unnamedArgumentList: [ new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]), ], @@ -318,6 +380,12 @@ export class SlashCommandHandler { typeList: [ARGUMENT_TYPE.STRING], enumProvider: localEnumProviders.qrEntries, }), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: localEnumProviders.qrIds, + }), ], helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.', })); @@ -340,6 +408,12 @@ export class SlashCommandHandler { typeList: [ARGUMENT_TYPE.STRING], enumProvider: localEnumProviders.qrEntries, }), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: localEnumProviders.qrIds, + }), new SlashCommandNamedArgument( 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), @@ -385,6 +459,12 @@ export class SlashCommandHandler { typeList: [ARGUMENT_TYPE.STRING], enumProvider: localEnumProviders.qrEntries, }), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: localEnumProviders.qrIds, + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -421,6 +501,12 @@ export class SlashCommandHandler { isRequired: true, enumProvider: localEnumProviders.qrSets, }), + SlashCommandNamedArgument.fromProps({ + name: 'id', + description: 'numeric ID of the QR, e.g., id=42', + typeList: [ARGUMENT_TYPE.NUMBER], + enumProvider: localEnumProviders.qrIds, + }), ], unnamedArgumentList: [ SlashCommandArgument.fromProps({ @@ -757,6 +843,13 @@ export class SlashCommandHandler { toastr.error(ex.message); } } + getQuickReply(args) { + try { + return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label)); + } catch (ex) { + toastr.error(ex.message); + } + } updateQuickReply(args, message) { try { this.api.updateQuickReply( From 9446c487e9eaca4ff85d6000907fa223828e3bd5 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 13:33:02 -0400 Subject: [PATCH 099/388] add debugger icons --- public/img/step-into.svg | 149 ++++++++++++ public/img/step-out.svg | 149 ++++++++++++ public/img/step-over.svg | 149 ++++++++++++ public/img/step-resume.svg | 218 ++++++++++++++++++ .../extensions/quick-reply/html/qrEditor.html | 8 +- .../scripts/extensions/quick-reply/style.css | 26 ++- .../scripts/extensions/quick-reply/style.less | 39 +++- 7 files changed, 718 insertions(+), 20 deletions(-) create mode 100644 public/img/step-into.svg create mode 100644 public/img/step-out.svg create mode 100644 public/img/step-over.svg create mode 100644 public/img/step-resume.svg diff --git a/public/img/step-into.svg b/public/img/step-into.svg new file mode 100644 index 000000000..fcfa7ef16 --- /dev/null +++ b/public/img/step-into.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-out.svg b/public/img/step-out.svg new file mode 100644 index 000000000..aa7dd3ea2 --- /dev/null +++ b/public/img/step-out.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-over.svg b/public/img/step-over.svg new file mode 100644 index 000000000..6f23ff22a --- /dev/null +++ b/public/img/step-over.svg @@ -0,0 +1,149 @@ + + + + diff --git a/public/img/step-resume.svg b/public/img/step-resume.svg new file mode 100644 index 000000000..bf3e0647f --- /dev/null +++ b/public/img/step-resume.svg @@ -0,0 +1,218 @@ + + + + diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index c7a8d6573..fc1eafe84 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -141,16 +141,16 @@
diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 8fbed0e99..58acdaa4a 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -548,15 +548,27 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton { aspect-ratio: 1.25 / 1; width: 2.25em; + position: relative; } -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton.qr--glyph-combo { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:after { + content: ''; + position: absolute; + inset: 3px; + background-color: var(--SmartThemeBodyColor); + mask-size: contain; + mask-position: center; } -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton.qr--glyph-combo .qr--glyph { - grid-column: 1; - line-height: 0.8; +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after { + mask-image: url('/img/step-resume.svg'); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after { + mask-image: url('/img/step-over.svg'); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after { + mask-image: url('/img/step-into.svg'); +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after { + mask-image: url('/img/step-out.svg'); } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress { --prog: 0; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index a321abd63..b11836cce 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -631,15 +631,36 @@ .qr--modal-debugButton { aspect-ratio: 1.25 / 1; width: 2.25em; - &.qr--glyph-combo { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; - .qr--glyph { - grid-column: 1; - line-height: 0.8; - } - } + position: relative; + &:after { + content: ''; + position: absolute; + inset: 3px; + background-color: var(--SmartThemeBodyColor); + mask-size: contain; + mask-position: center; + } + &#qr--modal-resume:after { + mask-image: url('/img/step-resume.svg'); + } + &#qr--modal-step:after { + mask-image: url('/img/step-over.svg'); + } + &#qr--modal-stepInto:after { + mask-image: url('/img/step-into.svg'); + } + &#qr--modal-stepOut:after { + mask-image: url('/img/step-out.svg'); + } + // &.qr--glyph-combo { + // display: grid; + // grid-template-columns: 1fr; + // grid-template-rows: 1fr 1fr; + // .qr--glyph { + // grid-column: 1; + // line-height: 0.8; + // } + // } } } From 968340c024818e72344b916e6e4204f38e64a2ab Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 13:42:22 -0400 Subject: [PATCH 100/388] fix key listener not updating message --- public/scripts/extensions/quick-reply/src/QuickReply.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index eb948bebc..6718667ee 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -527,14 +527,14 @@ export class QuickReply { message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`; message.selectionStart = start + 1; message.selectionEnd = end + count; - updateSyntax(); + message.dispatchEvent(new Event('input', { bubbles:true })); } else if (!(ac.isReplaceable && ac.isActive)) { evt.stopImmediatePropagation(); evt.stopPropagation(); message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`; message.selectionStart = start + 1; message.selectionEnd = end + 1; - updateSyntax(); + message.dispatchEvent(new Event('input', { bubbles:true })); } } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { evt.preventDefault(); @@ -547,7 +547,7 @@ export class QuickReply { message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`; message.selectionStart = start - 1; message.selectionEnd = end - count; - updateSyntax(); + message.dispatchEvent(new Event('input', { bubbles:true })); } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { evt.stopImmediatePropagation(); evt.stopPropagation(); @@ -559,7 +559,7 @@ export class QuickReply { message.value = `${message.value.slice(0, start)}\n${indent}${message.value.slice(end)}`; message.selectionStart = start + 1 + indent.length; message.selectionEnd = message.selectionStart; - updateSyntax(); + message.dispatchEvent(new Event('input', { bubbles:true })); } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { evt.stopImmediatePropagation(); evt.stopPropagation(); From 685e31b214bcd48305d48a57ef318b9f68df7362 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 13:54:41 -0400 Subject: [PATCH 101/388] jsdoc --- public/scripts/slash-commands/SlashCommandEnumValue.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandEnumValue.js b/public/scripts/slash-commands/SlashCommandEnumValue.js index a13399f32..077af5e86 100644 --- a/public/scripts/slash-commands/SlashCommandEnumValue.js +++ b/public/scripts/slash-commands/SlashCommandEnumValue.js @@ -59,6 +59,9 @@ export class SlashCommandEnumValue { * @param {string?} description - Optional description, displayed in a second line * @param {EnumType?} type - type of the enum (defining its color) * @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones) + * @param {(input:string)=>boolean?} matchProvider - A custom function to match autocomplete input instead of startsWith/includes/fuzzy. Should only be used for generic options like "any number" or "any string". "input" is the part of the text that is getting auto completed. + * @param {(input:string)=>string?} valueProvider - A function returning a value to be used in autocomplete instead of the enum value. "input" is the part of the text that is getting auto completed. By default, values with a valueProvider will not be selectable in the autocomplete (with tab/enter). + * @param {boolean?} makeSelectable - Set to true to make the value selectable (through tab/enter) even though a valueProvider exists. */ constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null, makeSelectable = false) { this.value = value; From b74600605cc831bb21c2210c489ab2c30c4fa93d Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 14:09:05 -0400 Subject: [PATCH 102/388] add syntax highlight for /import --- .../slash-commands/SlashCommandParser.js | 18 ++++++++++++++++++ public/style.css | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index a79bcdafa..83194e02f 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -243,6 +243,14 @@ export class SlashCommandParser { end: /\||$|:}/, contains: [], }; + const KEYWORD = { + scope: 'command', + begin: /\/(import)/, + beginScope: 'keyword', + end: /\||$|(?=:})/, + excludeEnd: true, + contains: [], + }; const LET = { begin: [ /\/(let|var)\s+/, @@ -312,6 +320,14 @@ export class SlashCommandParser { MACRO, CLOSURE, ); + KEYWORD.contains.push( + hljs.BACKSLASH_ESCAPE, + NAMED_ARG, + NUMBER, + MACRO, + CLOSURE, + hljs.QUOTE_STRING_MODE, + ); LET.contains.push( hljs.BACKSLASH_ESCAPE, NAMED_ARG, @@ -348,6 +364,7 @@ export class SlashCommandParser { hljs.BACKSLASH_ESCAPE, COMMENT, ABORT, + KEYWORD, NAMED_ARG, NUMBER, MACRO, @@ -366,6 +383,7 @@ export class SlashCommandParser { hljs.BACKSLASH_ESCAPE, COMMENT, ABORT, + KEYWORD, RUN, LET, GETVAR, diff --git a/public/style.css b/public/style.css index f28d0d3a2..3dcc3ea05 100644 --- a/public/style.css +++ b/public/style.css @@ -1392,6 +1392,7 @@ body[data-stscript-style="dark"] { --ac-style-color-punctuationL2: rgba(98 160 251 / 1); --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); --ac-style-color-comment: rgba(122 151 90 / 1); + --ac-style-color-keyword: rgba(182 137 190 / 1); } body[data-stscript-style="light"] { @@ -1413,6 +1414,7 @@ body[data-stscript-style="light"] { --ac-style-color-variable: rgba(16 24 125 / 1); --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); --ac-style-color-comment: rgba(70 126 26 / 1); + --ac-style-color-keyword: rgba(182 137 190 / 1); } body[data-stscript-style="theme"] { @@ -1433,6 +1435,7 @@ body[data-stscript-style="theme"] { --ac-style-color-variable: rgba(131 193 252 / 1); --ac-style-color-currentParenthesis: rgba(195 118 210 / 1); --ac-style-color-comment: rgba(122 151 90 / 1); + --ac-style-color-keyword: rgba(182 137 190 / 1); } body { @@ -1463,6 +1466,7 @@ body { --ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1)); --ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1)); --ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1)); + --ac-color-keyword: var(--ac-style-color-keyword, rgba(182 137 190 / 1)); font-size: calc(var(--ac-font-scale) * 1em); @@ -1575,6 +1579,10 @@ body[data-stscript-style] .hljs.language-stscript { color: var(--ac-style-color-abort, #e38e23); } + .hljs-keyword { + color: var(--ac-style-color-keyword); + } + .hljs-closure { >.hljs-punctuation { color: var(--ac-style-color-punctuation); From 956a676390941a868a141b7e6a180dca2efc7f6a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 15:05:39 -0400 Subject: [PATCH 103/388] fix return for /run with closure --- public/scripts/slash-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 46b862cc2..c4990e4be 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1819,7 +1819,7 @@ async function runCallback(args, name) { } if (name instanceof SlashCommandClosure) { - return await name.execute(); + return (await name.execute())?.pipe; } /**@type {SlashCommandScope} */ From 40c5430b148a4fe4541408c1cd3651f1e1c276b8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 15:05:57 -0400 Subject: [PATCH 104/388] fix debug button mask repeat --- public/scripts/extensions/quick-reply/style.css | 1 + public/scripts/extensions/quick-reply/style.less | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 58acdaa4a..95edb2e1c 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -557,6 +557,7 @@ background-color: var(--SmartThemeBodyColor); mask-size: contain; mask-position: center; + mask-repeat: no-repeat; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after { mask-image: url('/img/step-resume.svg'); diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index b11836cce..f2c439e16 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -639,6 +639,7 @@ background-color: var(--SmartThemeBodyColor); mask-size: contain; mask-position: center; + mask-repeat: no-repeat; } &#qr--modal-resume:after { mask-image: url('/img/step-resume.svg'); From 4a4218f49a1558dc3f60c44fd9b887165690940e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 13 Jul 2024 14:15:41 -0400 Subject: [PATCH 105/388] prevent popup close while debugging breaks shit, but that's a popup problem --- public/scripts/extensions/quick-reply/src/QuickReply.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 6718667ee..f993138b9 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -957,6 +957,7 @@ export class QuickReply { } async executeFromEditor() { if (this.isExecuting) return; + this.editorPopup.onClosing = ()=>false; const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/; const oText = this.message; this.isExecuting = true; @@ -1463,6 +1464,7 @@ export class QuickReply { this.editorPopup.dlg.classList.remove('qr--hide'); this.editorDom.classList.remove('qr--isExecuting'); this.isExecuting = false; + this.editorPopup.onClosing = null; } updateEditorProgress(done, total) { From 387ef83b7232ffb9f2781f9c2e0c440b6212289f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 13 Jul 2024 14:15:55 -0400 Subject: [PATCH 106/388] nowrap in debugger call stack source --- public/scripts/extensions/quick-reply/style.css | 1 + public/scripts/extensions/quick-reply/style.less | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 95edb2e1c..e77e1720f 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -780,6 +780,7 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source { opacity: 0.5; text-align: right; + white-space: nowrap; } @keyframes qr--progressPulse { 0%, diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index f2c439e16..6da9e6381 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -867,6 +867,7 @@ .qr--source { opacity: 0.5; text-align: right; + white-space: nowrap; } } } From 8621fdbfa332cee0cdd29d0a1132af1f00e53daf Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 13 Jul 2024 14:23:49 -0400 Subject: [PATCH 107/388] jsdoc type casing --- .../quick-reply/src/QuickReplySet.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 2c803dc8d..28b2025e4 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -18,7 +18,7 @@ export class QuickReplySet { } /** - * @param {String} name - name of the QuickReplySet + * @param {string} name - name of the QuickReplySet */ static get(name) { return this.list.find(it=>it.name == name); @@ -27,17 +27,17 @@ export class QuickReplySet { - /**@type {String}*/ name; - /**@type {Boolean}*/ disableSend = false; - /**@type {Boolean}*/ placeBeforeInput = false; - /**@type {Boolean}*/ injectInput = false; + /**@type {string}*/ name; + /**@type {boolean}*/ disableSend = false; + /**@type {boolean}*/ placeBeforeInput = false; + /**@type {boolean}*/ injectInput = false; /**@type {QuickReply[]}*/ qrList = []; - /**@type {Number}*/ idIndex = 0; + /**@type {number}*/ idIndex = 0; - /**@type {Boolean}*/ isDeleted = false; + /**@type {boolean}*/ isDeleted = false; - /**@type {Function}*/ save; + /**@type {function}*/ save; /**@type {HTMLElement}*/ dom; /**@type {HTMLElement}*/ settingsDom; @@ -190,7 +190,7 @@ export class QuickReplySet { } /** * @param {QuickReply} qr - * @param {String} [message] - optional altered message to be used + * @param {string} [message] - optional altered message to be used * @param {SlashCommandScope} [scope] - optional scope to be used when running the command */ async execute(qr, message = null, isAutoExecute = false, scope = null) { From 9f9553db44504cd1287847f9bdb4012f29702a8f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 13 Jul 2024 14:45:35 -0400 Subject: [PATCH 108/388] add QR set colors --- .../extensions/quick-reply/html/settings.html | 7 +++++++ .../quick-reply/src/QuickReplySet.js | 18 ++++++++++++++++++ .../quick-reply/src/ui/SettingsUi.js | 18 ++++++++++++++++++ .../scripts/extensions/quick-reply/style.css | 8 ++++++++ .../scripts/extensions/quick-reply/style.less | 7 +++++++ 5 files changed, 58 insertions(+) diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html index ebf7c73c6..7d186478d 100644 --- a/public/scripts/extensions/quick-reply/html/settings.html +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -60,6 +60,13 @@ +
+ + Color +
+
diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 28b2025e4..3a3d47958 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -31,6 +31,8 @@ export class QuickReplySet { /**@type {boolean}*/ disableSend = false; /**@type {boolean}*/ placeBeforeInput = false; /**@type {boolean}*/ injectInput = false; + /**@type {string}*/ color = 'transparent'; + /**@type {boolean}*/ onlyBorderColor = false; /**@type {QuickReply[]}*/ qrList = []; /**@type {number}*/ idIndex = 0; @@ -66,6 +68,7 @@ export class QuickReplySet { const root = document.createElement('div'); { this.dom = root; root.classList.add('qr--buttons'); + this.updateColor(); this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ root.append(qr.render()); }); @@ -80,6 +83,19 @@ export class QuickReplySet { this.dom.append(qr.render()); }); } + updateColor() { + if (this.color) { + this.dom.style.setProperty('--qr--color', this.color); + if (this.onlyBorderColor) { + this.dom.classList.add('qr--borderColor'); + } else { + this.dom.classList.remove('qr--borderColor'); + } + } else { + this.dom.style.setProperty('--qr--color', 'transparent'); + this.dom.classList.remove('qr--borderColor'); + } + } @@ -337,6 +353,8 @@ export class QuickReplySet { disableSend: this.disableSend, placeBeforeInput: this.placeBeforeInput, injectInput: this.injectInput, + color: this.color, + onlyBorderColor: this.onlyBorderColor, qrList: this.qrList, idIndex: this.idIndex, }; diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 3bcf73d66..e16cf3ed4 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -23,6 +23,8 @@ export class SettingsUi { /**@type {HTMLInputElement}*/ disableSend; /**@type {HTMLInputElement}*/ placeBeforeInput; /**@type {HTMLInputElement}*/ injectInput; + /**@type {HTMLInputElement}*/ color; + /**@type {HTMLInputElement}*/ onlyBorderColor; /**@type {HTMLSelectElement}*/ currentSet; @@ -164,6 +166,20 @@ export class SettingsUi { qrs.injectInput = this.injectInput.checked; qrs.save(); }); + this.color = this.dom.querySelector('#qr--color'); + this.color.addEventListener('change', (evt)=>{ + const qrs = this.currentQrSet; + qrs.color = evt.detail.rgb; + qrs.save(); + this.currentQrSet.updateColor(); + }); + this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor'); + this.onlyBorderColor.addEventListener('click', ()=>{ + const qrs = this.currentQrSet; + qrs.onlyBorderColor = this.onlyBorderColor.checked; + qrs.save(); + this.currentQrSet.updateColor(); + }); this.onQrSetChange(); } onQrSetChange() { @@ -171,6 +187,8 @@ export class SettingsUi { this.disableSend.checked = this.currentQrSet.disableSend; this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; this.injectInput.checked = this.currentQrSet.injectInput; + this.color.color = this.currentQrSet.color; + this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; this.qrList.innerHTML = ''; const qrsDom = this.currentQrSet.renderSettings(); this.qrList.append(qrsDom); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index e77e1720f..4bb95ca94 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -58,6 +58,8 @@ } #qr--bar > .qr--buttons, #qr--popout > .qr--body > .qr--buttons { + --qr--color: transparent; + background-color: var(--qr--color); margin: 0; padding: 0; display: flex; @@ -66,6 +68,12 @@ gap: 5px; width: 100%; } +#qr--bar > .qr--buttons.qr--borderColor, +#qr--popout > .qr--body > .qr--buttons.qr--borderColor { + background-color: transparent; + border-left: 5px solid var(--qr--color); + border-right: 5px solid var(--qr--color); +} #qr--bar > .qr--buttons > .qr--buttons, #qr--popout > .qr--body > .qr--buttons > .qr--buttons { display: contents; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 6da9e6381..2eba16b6e 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -65,6 +65,13 @@ #qr--bar, #qr--popout>.qr--body { >.qr--buttons { + --qr--color: transparent; + background-color: var(--qr--color); + &.qr--borderColor { + background-color: transparent; + border-left: 5px solid var(--qr--color); + border-right: 5px solid var(--qr--color); + } margin: 0; padding: 0; display: flex; From a8d4e1241993bf97fe64fc2046aa9f26cb196336 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 13 Jul 2024 15:03:52 -0400 Subject: [PATCH 109/388] adjust QR set colors for combined buttons --- public/scripts/extensions/quick-reply/style.css | 9 +++++++++ public/scripts/extensions/quick-reply/style.less | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 4bb95ca94..98b267bea 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -77,6 +77,15 @@ #qr--bar > .qr--buttons > .qr--buttons, #qr--popout > .qr--body > .qr--buttons > .qr--buttons { display: contents; + background-color: transparent; +} +#qr--bar > .qr--buttons > .qr--buttons:before, +#qr--popout > .qr--body > .qr--buttons > .qr--buttons:before, +#qr--bar > .qr--buttons > .qr--buttons:after, +#qr--popout > .qr--body > .qr--buttons > .qr--buttons:after { + content: ''; + width: 5px; + background-color: var(--qr--color); } #qr--bar > .qr--buttons .qr--button, #qr--popout > .qr--body > .qr--buttons .qr--button { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 2eba16b6e..d21c6bb3d 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -82,6 +82,12 @@ >.qr--buttons { display: contents; + background-color: transparent; + &:before, &:after { + content: ''; + width: 5px; + background-color: var(--qr--color); + } } .qr--button { From ddce6c4e8969ad5a69feb582cc6ab9a59274a07b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 14:13:57 -0400 Subject: [PATCH 110/388] clear QR Set color and bg adjustments --- .../extensions/quick-reply/html/settings.html | 3 +- .../quick-reply/src/QuickReplySet.js | 4 ++- .../quick-reply/src/ui/SettingsUi.js | 7 ++++ .../scripts/extensions/quick-reply/style.css | 34 ++++++++++++++----- .../scripts/extensions/quick-reply/style.less | 34 ++++++++++++++----- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html index 7d186478d..793841bc8 100644 --- a/public/scripts/extensions/quick-reply/html/settings.html +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -60,8 +60,9 @@ -
+
+ Color
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index f993138b9..9e8ad68a7 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -879,6 +879,15 @@ export class QuickReply { stepOutBtn.addEventListener('click', ()=>{ this.debugController?.stepOut(); }); + /**@type {HTMLElement}*/ + const minimizeBtn = dom.querySelector('#qr--modal-minimize'); + minimizeBtn.addEventListener('click', ()=>{ + this.editorDom.classList.add('qr--minimized'); + }); + const maximizeBtn = dom.querySelector('#qr--modal-maximize'); + maximizeBtn.addEventListener('click', ()=>{ + this.editorDom.classList.remove('qr--minimized'); + }); /**@type {boolean}*/ let isResizing = false; let resizeStart; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 579a16fe6..5321f6aaa 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -337,6 +337,54 @@ .popup:has(#qr--modalEditor) { aspect-ratio: unset; } +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) { + min-width: unset; + min-height: unset; + height: auto !important; + width: min-content !important; + position: absolute; + right: 1em; + top: 1em; + left: unset; + bottom: unset; + margin: unset; + padding: 0; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop { + backdrop-filter: unset; + background-color: transparent; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body { + flex: 0 0 auto; + height: min-content; + width: min-content; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content { + flex: 0 0 auto; + margin-top: 0; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor { + max-height: 50vh; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main, +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle, +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3, +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons, +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress { + display: none; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions { + width: auto; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { + display: flex; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize { + display: none; +} +.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState { + padding-top: 0; +} .popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls { display: none; } @@ -585,7 +633,7 @@ width: 2.25em; position: relative; } -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:after { +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after { content: ''; position: absolute; inset: 3px; @@ -606,6 +654,9 @@ .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after { mask-image: url('/img/step-out.svg'); } +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { + display: none; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress { --prog: 0; --progColor: #92befc; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 42ea579ef..f9e3c9a3f 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -400,6 +400,56 @@ .popup:has(#qr--modalEditor) { aspect-ratio: unset; + &:has(.qr--isExecuting.qr--minimized) { + min-width: unset; + min-height: unset; + height: auto !important; + width: min-content !important; + position: absolute; + right: 1em; + top: 1em; + left: unset; + bottom: unset; + margin: unset; + padding: 0; + &::backdrop { + backdrop-filter: unset; + background-color: transparent; + } + .popup-body { + flex: 0 0 auto; + height: min-content; + width: min-content; + } + .popup-content { + flex: 0 0 auto; + margin-top: 0; + + > #qr--modalEditor { + max-height: 50vh; + > #qr--main, + > #qr--resizeHandle, + > #qr--qrOptions > h3, + > #qr--qrOptions > #qr--modal-executeButtons, + > #qr--qrOptions > #qr--modal-executeProgress + { + display: none; + } + #qr--qrOptions { + width: auto; + } + #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize { + display: flex; + } + #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize { + display: none; + } + #qr--modal-debugState { + padding-top: 0; + } + } + } + } &:has(.qr--isExecuting) { .popup-controls { display: none; @@ -663,7 +713,7 @@ aspect-ratio: 1.25 / 1; width: 2.25em; position: relative; - &:after { + &:not(.fa-solid):after { content: ''; position: absolute; inset: 3px; @@ -684,15 +734,9 @@ &#qr--modal-stepOut:after { mask-image: url('/img/step-out.svg'); } - // &.qr--glyph-combo { - // display: grid; - // grid-template-columns: 1fr; - // grid-template-rows: 1fr 1fr; - // .qr--glyph { - // grid-column: 1; - // line-height: 0.8; - // } - // } + &#qr--modal-maximize { + display: none; + } } } From 9a9befeb63c2794c70d5525d86146ca58ad7ab2f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 16:59:34 -0400 Subject: [PATCH 112/388] remove second step-out button? --- public/scripts/extensions/quick-reply/html/qrEditor.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index f7f26e259..7febea1b5 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -144,7 +144,6 @@ -
From e256e552687856a061d979c5e5e70af0a54d2871 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 17:01:08 -0400 Subject: [PATCH 113/388] remove traces of execute on hide --- public/scripts/extensions/quick-reply/html/qrEditor.html | 4 ---- public/scripts/extensions/quick-reply/src/QuickReply.js | 8 -------- public/scripts/extensions/quick-reply/style.css | 3 --- public/scripts/extensions/quick-reply/style.less | 5 ----- 4 files changed, 20 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 7febea1b5..814da1b81 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -132,10 +132,6 @@
-
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 9e8ad68a7..558dcc64e 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -74,7 +74,6 @@ export class QuickReply { /**@type {HTMLElement}*/ editorExecuteErrors; /**@type {HTMLElement}*/ editorExecuteResult; /**@type {HTMLElement}*/ editorDebugState; - /**@type {HTMLInputElement}*/ editorExecuteHide; /**@type {Promise}*/ editorExecutePromise; /**@type {boolean}*/ isExecuting; /**@type {SlashCommandAbortController}*/ abortController; @@ -829,9 +828,6 @@ export class QuickReply { /**@type {HTMLElement}*/ const debugState = dom.querySelector('#qr--modal-debugState'); this.editorDebugState = debugState; - /**@type {HTMLInputElement}*/ - const executeHide = dom.querySelector('#qr--modal-executeHide'); - this.editorExecuteHide = executeHide; /**@type {HTMLElement}*/ const executeBtn = dom.querySelector('#qr--modal-execute'); this.editorExecuteBtn = executeBtn; @@ -985,9 +981,6 @@ export class QuickReply { this.editorExecuteProgress.classList.remove('qr--aborted'); this.editorExecuteErrors.innerHTML = ''; this.editorExecuteResult.innerHTML = ''; - if (this.editorExecuteHide.checked) { - this.editorPopup.dlg.classList.add('qr--hide'); - } const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner'); const updateScroll = (evt) => { let left = syntax.scrollLeft; @@ -1470,7 +1463,6 @@ export class QuickReply { this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); 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; this.editorPopup.onClosing = null; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 5321f6aaa..065d1667b 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -574,9 +574,6 @@ border-radius: 5px; position: relative; } -.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor label:has(#qr--modal-executeHide) { - display: none; -} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons { display: flex; gap: 1em; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index f9e3c9a3f..8bd1c6ac7 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -642,11 +642,6 @@ } } } - - label:has(#qr--modal-executeHide) { - // hide editor is not working anyways - display: none; - } #qr--modal-executeButtons { display: flex; gap: 1em; From 82dd53f166b820213f5e8bcc0512aa7df7604d7e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 17:02:24 -0400 Subject: [PATCH 114/388] fix color without dom --- public/scripts/extensions/quick-reply/src/QuickReplySet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 3a9defc4a..0e1fb7839 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -84,6 +84,7 @@ export class QuickReplySet { }); } updateColor() { + if (!this.dom) return; if (this.color && this.color != 'transparent') { this.dom.style.setProperty('--qr--color', this.color); this.dom.classList.add('qr--color'); From db1094e391c3f46dad673836d9483b1e761deba1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 18:58:13 -0400 Subject: [PATCH 115/388] 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(/\/[/#]/); } From 696c24051f6fe49ef29bd478663052292a5d3b25 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 18:07:36 -0400 Subject: [PATCH 116/388] fix color picker widget defaulting to black instead of its initial value and firing change event when added to DOM --- .../scripts/extensions/quick-reply/src/ui/SettingsUi.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index eb6f61940..df26e54ff 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -166,9 +166,15 @@ export class SettingsUi { qrs.injectInput = this.injectInput.checked; qrs.save(); }); + let initialColorChange = true; this.color = this.dom.querySelector('#qr--color'); this.color.addEventListener('change', (evt)=>{ const qrs = this.currentQrSet; + if (initialColorChange) { + initialColorChange = false; + this.color.color = qrs.color; + return; + } qrs.color = evt.detail.rgb; qrs.save(); this.currentQrSet.updateColor(); @@ -194,7 +200,7 @@ export class SettingsUi { this.disableSend.checked = this.currentQrSet.disableSend; this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; this.injectInput.checked = this.currentQrSet.injectInput; - this.color.color = this.currentQrSet.color; + // this.color.color = this.currentQrSet.color ?? 'transparent'; this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; this.qrList.innerHTML = ''; const qrsDom = this.currentQrSet.renderSettings(); From ff39902f120afc0c5b43bfb4b035bf40701459e8 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 18:08:44 -0400 Subject: [PATCH 117/388] align show label checkbox on baseline --- public/scripts/extensions/quick-reply/style.css | 1 + public/scripts/extensions/quick-reply/style.less | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 013c31839..fb8d0e3e6 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -467,6 +467,7 @@ } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup { display: flex; + align-items: baseline; gap: 0.5em; } .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 604c1938c..83d96ebe0 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -537,6 +537,7 @@ } .qr--inputGroup { display: flex; + align-items: baseline; gap: 0.5em; input { flex: 1 1 auto; From a0720343f37ea29f00418c355823ccc2f9951230 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 18:16:32 -0400 Subject: [PATCH 118/388] use POPUP_TYPE text for fontawesome popup --- public/scripts/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 668e8b0f0..68a0f2028 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1956,7 +1956,7 @@ export async function showFontAwesomePicker() { } } let value; - const picker = new Popup(dom, POPUP_TYPE.CONFIRM, null, { allowVerticalScrolling:true }); + const picker = new Popup(dom, POPUP_TYPE.TEXT, null, { allowVerticalScrolling:true, okButton: 'Cancel' }); await picker.show(); if (picker.result == POPUP_RESULT.AFFIRMATIVE) { return value; From e68504d6c37273fb5e28b1c4034c9ac3fc9d5228 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 18:16:51 -0400 Subject: [PATCH 119/388] properly hide filtered fontawesime icons in picker --- public/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/style.css b/public/style.css index 6b22c7728..9efe766f0 100644 --- a/public/style.css +++ b/public/style.css @@ -5348,8 +5348,8 @@ body:not(.movingUI) .drawer-content.maximized { padding: 0.25em; width: unset; box-sizing: content-box; - &.stcdx--hidden { - display: none; - } + &.hidden { + display: none; + } } } From d773174bad17d395fcd941681603a7d3482d7129 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 18:25:48 -0400 Subject: [PATCH 120/388] there was a reason I wanted to use confirm, dammit --- public/scripts/extensions/quick-reply/src/QuickReply.js | 1 + public/scripts/utils.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 2a8a51a0b..7508eb1b9 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1569,6 +1569,7 @@ export class QuickReply { if (this.icon) { this.settingsDomIcon.classList.remove(this.icon); } + this.settingsDomIcon.textContent = '…'; this.settingsDomIcon.classList.remove('fa-solid'); } else { if (this.icon) { diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 68a0f2028..a630fec3d 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1955,8 +1955,8 @@ export async function showFontAwesomePicker() { dom.append(grid); } } - let value; - const picker = new Popup(dom, POPUP_TYPE.TEXT, null, { allowVerticalScrolling:true, okButton: 'Cancel' }); + let value = ''; + const picker = new Popup(dom, POPUP_TYPE.CONFIRM, null, { allowVerticalScrolling:true, okButton: 'No Icon', cancelButton: 'Cancel' }); await picker.show(); if (picker.result == POPUP_RESULT.AFFIRMATIVE) { return value; From b51bf8e38ebaf9056367fcb0b3f7eb6fe75dc33f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 15 Jul 2024 19:42:36 -0400 Subject: [PATCH 121/388] fix QR update/delete by ID --- .../scripts/extensions/quick-reply/src/SlashCommandHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 4f90464ea..143d86bd4 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -854,7 +854,7 @@ export class SlashCommandHandler { try { this.api.updateQuickReply( args.set ?? '', - args.label ?? '', + args.id !== undefined ? Number(args.id) : (args.label ?? ''), { newLabel: args.newlabel, message: (message ?? '').trim().length > 0 ? message : undefined, @@ -874,7 +874,7 @@ export class SlashCommandHandler { } deleteQuickReply(args, label) { try { - this.api.deleteQuickReply(args.set, args.label ?? label); + this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label)); } catch (ex) { toastr.error(ex.message); } From 4b5704896d690c2a4bd8357ac2dc58664b35c39f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 09:26:37 -0400 Subject: [PATCH 122/388] more flexibililty for enums custom mapping from enum value class to enum option class --- .../SlashCommandAutoCompleteNameResult.js | 4 ++-- .../SlashCommandEnumAutoCompleteOption.js | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js index 6c00c275c..e72a20e6a 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js @@ -115,7 +115,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { const result = new AutoCompleteSecondaryNameResult( value, start + name.length, - enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)), + enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)), true, ); result.isRequired = true; @@ -182,7 +182,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult { const result = new AutoCompleteSecondaryNameResult( value, start, - enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)), + enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)), false, ); const isCompleteValue = enumList.find(it=>it.value == value); diff --git a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js index 0c04d9c62..01f8188cc 100644 --- a/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js +++ b/public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js @@ -3,6 +3,17 @@ import { SlashCommand } from './SlashCommand.js'; import { SlashCommandEnumValue } from './SlashCommandEnumValue.js'; export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption { + /** + * @param {SlashCommand} cmd + * @param {SlashCommandEnumValue} enumValue + * @returns {SlashCommandEnumAutoCompleteOption} + */ + static from(cmd, enumValue) { + const mapped = this.valueToOptionMap.find(it=>enumValue instanceof it.value)?.option ?? this; + return new mapped(cmd, enumValue); + } + /**@type {{value:(typeof SlashCommandEnumValue), option:(typeof SlashCommandEnumAutoCompleteOption)}[]} */ + static valueToOptionMap = []; /**@type {SlashCommand}*/ cmd; /**@type {SlashCommandEnumValue}*/ enumValue; From 6af46a34fd7477fb86f03484fc764fec7f6f543f Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 09:28:03 -0400 Subject: [PATCH 123/388] add jsDoc and allow custom icon lists --- public/scripts/utils.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index a630fec3d..bf0953c50 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1899,18 +1899,26 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) { return `${name}${numberFormatter(counter)}`; } -export async function showFontAwesomePicker() { - const fetchFa = async(name)=>{ - const style = document.createElement('style'); - style.innerHTML = await (await fetch(`/css/${name}`)).text(); - document.head.append(style); - const sheet = style.sheet; - style.remove(); - return [...sheet.cssRules].filter(it=>it.style?.content).map(it=>it.selectorText.split('::').shift().slice(1)); - }; - const faList = [...new Set((await Promise.all([ - fetchFa('fontawesome.min.css'), +export async function fetchFaFile(name) { + const style = document.createElement('style'); + style.innerHTML = await (await fetch(`/css/${name}`)).text(); + document.head.append(style); + const sheet = style.sheet; + style.remove(); + return [...sheet.cssRules].filter(it=>it.style?.content).map(it=>it.selectorText.split('::').shift().slice(1)); +} +export async function fetchFa() { + return [...new Set((await Promise.all([ + fetchFaFile('fontawesome.min.css'), ])).flat())]; +} +/** + * Opens a popup with all the available Font Awesome icons and returns the selected icon's name. + * @prop {string[]} customList A custom list of Font Awesome icons to use instead of all available icons. + * @returns {Promise} The icon name (fa-pencil) or null if cancelled. + */ +export async function showFontAwesomePicker(customList = null) { + const faList = customList ?? await fetchFa(); const fas = {}; const dom = document.createElement('div'); { const search = document.createElement('div'); { From 10b9fdd06d41ae52a46b82d1fe72fc2c53fd04de Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 09:28:33 -0400 Subject: [PATCH 124/388] add /pick-icon to show Font Awesome icon picker --- public/scripts/slash-commands.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index c4990e4be..569ce2808 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -51,7 +51,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; -import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; +import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; import { registerVariableCommands, resolveVariable } from './variables.js'; import { background_settings } from './backgrounds.js'; import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; @@ -1475,6 +1475,21 @@ export function initDefaultSlashCommands() { ], helpString: 'Sets the specified prompt manager entry/entries on or off.', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon', + callback: async()=>((await showFontAwesomePicker()) ?? false).toString(), + returns: 'The chosen icon name or false if cancelled.', + helpString: ` +
Opens a popup with all the available Font Awesome icons and returns the selected icon's name.
+
+ Example: +
    +
  • +
    /pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|
    +
  • +
+
+ `, + })); registerVariableCommands(); } From 5478b692539568032748b24420519847dbe4d441 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 09:29:31 -0400 Subject: [PATCH 125/388] add icon= and showLabel= to /qr-create and /qr-update --- .../extensions/quick-reply/api/QuickReplyApi.js | 12 ++++++++++++ .../quick-reply/src/SlashCommandHandler.js | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 2e4260147..8bcccb1bd 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -186,6 +186,8 @@ export class QuickReplyApi { * @param {string} setName name of the quick reply set to insert the new quick reply into * @param {string} label label for the new quick reply (text on the button) * @param {object} [props] + * @param {string} [props.icon] the icon to show on the QR button + * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply * @param {string} [props.title] the title / tooltip to be shown on the quick reply button * @param {boolean} [props.isHidden] whether to hide or show the button @@ -198,6 +200,8 @@ export class QuickReplyApi { * @returns {QuickReply} the new quick reply */ createQuickReply(setName, label, { + icon, + showLabel, message, title, isHidden, @@ -214,6 +218,8 @@ export class QuickReplyApi { } const qr = set.addQuickReply(); qr.label = label ?? ''; + qr.icon = icon ?? ''; + qr.showLabel = showLabel ?? false; qr.message = message ?? ''; qr.title = title ?? ''; qr.isHidden = isHidden ?? false; @@ -233,6 +239,8 @@ export class QuickReplyApi { * @param {string} setName name of the existing quick reply set * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID * @param {object} [props] + * @param {string} [props.icon] the icon to show on the QR button + * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned * @param {string} [props.newLabel] new label for quick reply (text on the button) * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply * @param {string} [props.title] the title / tooltip to be shown on the quick reply button @@ -246,6 +254,8 @@ export class QuickReplyApi { * @returns {QuickReply} the altered quick reply */ updateQuickReply(setName, label, { + icon, + showLabel, newLabel, message, title, @@ -261,6 +271,8 @@ export class QuickReplyApi { if (!qr) { throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); } + qr.updateIcon(icon ?? qr.icon); + qr.updateShowLabel(showLabel ?? qr.showLabel); qr.updateLabel(newLabel ?? qr.label); qr.updateMessage(message ?? qr.message); qr.updateTitle(title ?? qr.title); diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 143d86bd4..a86274be7 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -247,6 +247,18 @@ export class SlashCommandHandler { isRequired: false, enumProvider: localEnumProviders.qrEntries, }), + SlashCommandNamedArgument.fromProps({ + name: 'icon', + description: 'icon to show on the button, e.g., icon=fa-pencil', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: false, + }), + SlashCommandNamedArgument.fromProps({ + name: 'showlabel', + description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true', + typeList: [ARGUMENT_TYPE.BOOLEAN], + isRequired: false, + }), new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'), @@ -828,6 +840,8 @@ export class SlashCommandHandler { args.set ?? '', args.label ?? '', { + icon: args.icon, + showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel), message: message ?? '', title: args.title, isHidden: isTrueBoolean(args.hidden), @@ -856,6 +870,8 @@ export class SlashCommandHandler { args.set ?? '', args.id !== undefined ? Number(args.id) : (args.label ?? ''), { + icon: args.icon, + showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel), newLabel: args.newlabel, message: (message ?? '').trim().length > 0 ? message : undefined, title: args.title, From ea84eddc3e7e3669df120171f4e618722b33c66b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 10:41:56 -0400 Subject: [PATCH 126/388] add getSetByQr --- .../scripts/extensions/quick-reply/api/QuickReplyApi.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 8bcccb1bd..fedb6d0e6 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -23,6 +23,14 @@ export class QuickReplyApi { + /** + * @param {QuickReply} qr + * @returns {QuickReplySet} + */ + getSetByQr(qr) { + return QuickReplySet.list.find(it=>it.qrList.includes(qr)); + } + /** * Finds and returns an existing Quick Reply Set by its name. * From 25c86b65ac8c9537ac2c899a93b0662b7d93a222 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 10:42:25 -0400 Subject: [PATCH 127/388] add quick QR switcher to editor --- .../extensions/quick-reply/html/qrEditor.html | 5 +- .../extensions/quick-reply/src/QuickReply.js | 71 ++++++++++++++- .../scripts/extensions/quick-reply/style.css | 88 +++++++++++++++++-- .../scripts/extensions/quick-reply/style.less | 53 ++++++++++- 4 files changed, 206 insertions(+), 11 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 24c6270a3..5a697800f 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -6,7 +6,7 @@ Icon -
+
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 489283fe6..ef158b86f 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -984,6 +984,24 @@ export class QuickReply { this.abortController?.abort('Stop button clicked'); }); + /**@type {HTMLTextAreaElement} */ + const inputOg = document.querySelector('#send_textarea'); + const inputMirror = dom.querySelector('#qr--modal-send_textarea'); + inputMirror.value = inputOg.value; + const inputOgMo = new MutationObserver(muts=>{ + if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { + inputOg.removeEventListener('input', inputOgListener); + } + }); + inputOgMo.observe(document.body, { childList:true }); + const inputOgListener = ()=>{ + inputMirror.value = inputOg.value; + }; + inputOg.addEventListener('input', inputOgListener); + inputMirror.addEventListener('input', ()=>{ + inputOg.value = inputMirror.value; + }); + /**@type {HTMLElement}*/ const resumeBtn = dom.querySelector('#qr--modal-resume'); resumeBtn.addEventListener('click', ()=>{ From df19c98e9fc723eabe2ec09b150a69c86de04633 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 16 Jul 2024 16:27:51 -0400 Subject: [PATCH 131/388] add syntax highlight for pipes and pipe breaks --- .../slash-commands/SlashCommandParser.js | 38 ++++++++++++++----- public/style.css | 7 ++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 9b4ad67dc..75e8f5f1a 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -249,12 +249,13 @@ export class SlashCommandParser { end: /\||$|:}/, contains: [], }; - const KEYWORD = { + const IMPORT = { scope: 'command', begin: /\/(import)/, beginScope: 'keyword', end: /\||$|(?=:})/, - excludeEnd: true, + excludeEnd: false, + returnEnd: true, contains: [], }; const LET = { @@ -265,20 +266,24 @@ export class SlashCommandParser { 1: 'variable', }, end: /\||$|:}/, + excludeEnd: false, + returnEnd: true, contains: [], }; const SETVAR = { begin: /\/(setvar|setglobalvar)\s+/, beginScope: 'variable', end: /\||$|:}/, - excludeEnd: true, + excludeEnd: false, + returnEnd: true, contains: [], }; const GETVAR = { begin: /\/(getvar|getglobalvar)\s+/, beginScope: 'variable', end: /\||$|:}/, - excludeEnd: true, + excludeEnd: false, + returnEnd: true, contains: [], }; const RUN = { @@ -297,7 +302,8 @@ export class SlashCommandParser { begin: /\/\S+/, beginScope: 'title.function', end: /\||$|(?=:})/, - excludeEnd: true, + excludeEnd: false, + returnEnd: true, contains: [], // defined later }; const CLOSURE = { @@ -318,6 +324,16 @@ export class SlashCommandParser { begin: /{{/, end: /}}/, }; + const PIPEBREAK = { + beginScope: 'pipebreak', + begin: /\|\|/, + end: '', + }; + const PIPE = { + beginScope: 'pipe', + begin: /\|/, + end: '', + }; BLOCK_COMMENT.contains.push( BLOCK_COMMENT, ); @@ -329,7 +345,7 @@ export class SlashCommandParser { MACRO, CLOSURE, ); - KEYWORD.contains.push( + IMPORT.contains.push( hljs.BACKSLASH_ESCAPE, NAMED_ARG, NUMBER, @@ -374,7 +390,7 @@ export class SlashCommandParser { BLOCK_COMMENT, COMMENT, ABORT, - KEYWORD, + IMPORT, NAMED_ARG, NUMBER, MACRO, @@ -385,22 +401,26 @@ export class SlashCommandParser { COMMAND, 'self', hljs.QUOTE_STRING_MODE, + PIPEBREAK, + PIPE, ); hljs.registerLanguage('stscript', ()=>({ case_insensitive: false, - keywords: ['|'], + keywords: [], contains: [ hljs.BACKSLASH_ESCAPE, BLOCK_COMMENT, COMMENT, ABORT, - KEYWORD, + IMPORT, RUN, LET, GETVAR, SETVAR, COMMAND, CLOSURE, + PIPEBREAK, + PIPE, ], })); } diff --git a/public/style.css b/public/style.css index 9efe766f0..89d716a56 100644 --- a/public/style.css +++ b/public/style.css @@ -1583,6 +1583,13 @@ body[data-stscript-style] .hljs.language-stscript { color: var(--ac-style-color-keyword); } + .hljs-pipe { + color: var(--ac-style-color-punctuation); + } + .hljs-pipebreak { + color: var(--ac-style-color-type); + } + .hljs-closure { >.hljs-punctuation { color: var(--ac-style-color-punctuation); From b291014a95fd7a7af240781ac9a63d778a8629d9 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 17 Jul 2024 19:20:38 +0200 Subject: [PATCH 132/388] Resize FA icon in QR editor, add label caption --- public/scripts/extensions/quick-reply/html/qrEditor.html | 6 ++++-- public/scripts/extensions/quick-reply/style.css | 5 +++++ public/scripts/extensions/quick-reply/style.less | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 8db458d32..a5540b8b5 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -4,17 +4,19 @@
Label + (label of the button, if no icon is chosen)
- +
From 896d43ade72a7af606534e2e0d960b481ef4dc4e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 18 Jul 2024 18:39:32 -0400 Subject: [PATCH 148/388] add QR delete confirm --- .../extensions/quick-reply/src/QuickReply.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 47933d6a9..f65c49c45 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,4 +1,4 @@ -import { POPUP_TYPE, Popup } from '../../../popup.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js'; import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js'; @@ -358,8 +358,19 @@ export class QuickReply { del.classList.add('fa-solid'); del.classList.add('fa-trash-can'); del.classList.add('redWarningBG'); - del.title = 'Remove quick reply'; - del.addEventListener('click', ()=>this.delete()); + del.title = 'Remove Quick Reply\n---\nShit+Click to skip confirmation'; + del.addEventListener('click', async(evt)=>{ + if (!evt.shiftKey) { + const result = await Popup.show.confirm( + 'Remove Quick Reply', + 'Are you sure you want to remove this Quick Reply?', + ); + if (result != POPUP_RESULT.AFFIRMATIVE) { + return; + } + } + this.delete(); + }); actions.append(del); } itemContent.append(actions); From 03eb04e8f9c490f89eae0a1dd3946f725f7d00ab Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 18 Jul 2024 19:47:35 -0400 Subject: [PATCH 149/388] verify QR paste JSON and allow non-JSON pastes --- .../quick-reply/src/QuickReplySet.js | 29 +++++++++++++++---- .../quick-reply/src/ui/SettingsUi.js | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 0e1fb7839..dddd0a8bb 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -239,6 +239,28 @@ export class QuickReplySet { this.save(); return qr; } + addQuickReplyFromText(qrJson) { + let data; + try { + data = JSON.parse(qrJson ?? '{}'); + delete data.id; + } catch { + // not JSON data + } + if (data) { + // JSON data + if (data.label === undefined || data.message === undefined) { + // not a QR + toastr.error('Not a QR.'); + return; + } + } else { + // no JSON, use plaintext as QR message + data = { message: qrJson }; + } + const newQr = this.addQuickReply(data); + return newQr; + } /** * @@ -250,11 +272,8 @@ export class QuickReplySet { qr.onDelete = ()=>this.removeQuickReply(qr); qr.onUpdate = ()=>this.save(); qr.onInsertBefore = (qrJson)=>{ - const data = JSON.parse(qrJson ?? '{}'); - delete data.id; - log('onInsertBefore', data); - const newQr = this.addQuickReply(data); - this.qrList.pop(); + this.addQuickReplyFromText(qrJson); + const newQr = this.qrList.pop(); this.qrList.splice(this.qrList.indexOf(qr), 0, newQr); if (qr.settingsDom) { qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom); diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index b46e0538f..544f21836 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -121,7 +121,7 @@ export class SettingsUi { }); this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{ const text = await navigator.clipboard.readText(); - this.currentQrSet.addQuickReply(JSON.parse(text)); + this.currentQrSet.addQuickReplyFromText(text); }); this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{ const inp = document.createElement('input'); { From ae90966f434396e67f13a9cce9c0e3c309c7c8d3 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 18 Jul 2024 19:57:05 -0400 Subject: [PATCH 150/388] add debugger button states --- .../extensions/quick-reply/src/QuickReply.js | 8 ++------ public/scripts/extensions/quick-reply/style.css | 11 +++++++++++ public/scripts/extensions/quick-reply/style.less | 13 +++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index f65c49c45..d75c1c9de 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1204,12 +1204,7 @@ export class QuickReply { this.abortController = new SlashCommandAbortController(); this.debugController = new SlashCommandDebugController(); this.debugController.onBreakPoint = async(closure, executor)=>{ - //TODO move debug code into its own element, separate from the QR - //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.editorDom.classList.add('qr--isPaused'); syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; this.editorMessageLabel.innerHTML = ''; if (uuidCheck.test(closure.source)) { @@ -1619,6 +1614,7 @@ export class QuickReply { hi.remove(); this.editorDebugState.textContent = ''; this.editorDebugState.classList.remove('qr--active'); + this.editorDom.classList.remove('qr--isPaused'); return isStepping; }; const result = await this.onDebug(this); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 6fd7e8368..034bc7a1f 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -435,6 +435,17 @@ .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--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; + transition: 200ms; +} +.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { + cursor: pointer; + opacity: 1; + pointer-events: all; +} .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle { width: 6px; background-color: var(--SmartThemeBorderColor); diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 0547cf1d6..79552f5ee 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -504,7 +504,20 @@ } #qr--modal-debugButtons { display: flex; + .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; + transition: 200ms; + } } + &.qr--isPaused #qr--modal-debugButtons { + .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) { + cursor: pointer; + opacity: 1; + pointer-events: all; + } + } #qr--resizeHandle { width: 6px; background-color: var(--SmartThemeBorderColor); From 9b93dbf80b903b018bfa2d3f06d04b2a66367e8a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 19 Jul 2024 07:41:57 -0400 Subject: [PATCH 151/388] add autocomplete select key setting (enter/tab) --- public/index.html | 10 ++++++++++ public/scripts/autocomplete/AutoComplete.js | 9 +++++++++ public/scripts/power-user.js | 20 +++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index cca1fefba..8ee8a394d 100644 --- a/public/index.html +++ b/public/index.html @@ -4230,6 +4230,16 @@ +
+ + +
diff --git a/public/scripts/autocomplete/AutoComplete.js b/public/scripts/autocomplete/AutoComplete.js index 1526a9937..268342869 100644 --- a/public/scripts/autocomplete/AutoComplete.js +++ b/public/scripts/autocomplete/AutoComplete.js @@ -16,6 +16,13 @@ export const AUTOCOMPLETE_WIDTH = { 'FULL': 2, }; +/**@readonly*/ +/**@enum {Number}*/ +export const AUTOCOMPLETE_SELECT_KEY = { + 'TAB': 1, // 2^0 + 'ENTER': 2, // 2^1 +}; + export class AutoComplete { /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea; /**@type {boolean}*/ isFloating = false; @@ -724,6 +731,7 @@ export class AutoComplete { } case 'Enter': { // pick the selected item to autocomplete + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break; if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; if (this.selectedItem.name == this.name) break; if (!this.selectedItem.isSelectable) break; @@ -734,6 +742,7 @@ export class AutoComplete { } case 'Tab': { // pick the selected item to autocomplete + if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break; if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break; evt.preventDefault(); evt.stopImmediatePropagation(); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 08fbfb504..6c4c53440 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -45,7 +45,7 @@ import { FILTER_TYPES } from './filters.js'; import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommand } from './slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; -import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; +import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js'; import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; import { POPUP_TYPE, callGenericPopup } from './popup.js'; @@ -276,6 +276,7 @@ let power_user = { left: AUTOCOMPLETE_WIDTH.CHAT, right: AUTOCOMPLETE_WIDTH.CHAT, }, + select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER, }, parser: { /**@type {Object.} */ @@ -1492,6 +1493,9 @@ function loadPowerUserSettings(settings, data) { if (power_user.stscript.autocomplete.style === undefined) { power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style; } + if (power_user.stscript.autocomplete.select === undefined) { + power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select; + } } if (power_user.stscript.parser === undefined) { power_user.stscript.parser = defaultStscript.parser; @@ -1656,6 +1660,14 @@ function loadPowerUserSettings(settings, data) { $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme'); document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style); + $('#stscript_autocomplete_select').val(power_user.stscript.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); + $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false); + $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false); + $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale); + $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); + $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme'); + document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style); + $('#stscript_autocomplete_select').val(power_user.stscript.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false); $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false); $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale); @@ -3838,6 +3850,12 @@ $(document).ready(() => { saveSettingsDebounced(); }); + $('#stscript_autocomplete_select').on('change', function () { + const value = $(this).find(':selected').val(); + power_user.stscript.autocomplete.select = parseInt(String(value)); + saveSettingsDebounced(); + }); + $('#stscript_autocomplete_font_scale').on('input', function () { const value = $(this).val(); $('#stscript_autocomplete_font_scale_counter').val(value); From 6e7e57518eed5d068486775a6a361e151b2c084b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 19 Jul 2024 16:42:20 -0400 Subject: [PATCH 152/388] fix autocomplete setting state on load and duplicated lines --- public/scripts/power-user.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 6c4c53440..ecce0bfdc 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1660,14 +1660,7 @@ function loadPowerUserSettings(settings, data) { $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme'); document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style); - $('#stscript_autocomplete_select').val(power_user.stscript.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); - $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false); - $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false); - $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale); - $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy'); - $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme'); - document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style); - $('#stscript_autocomplete_select').val(power_user.stscript.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); + $('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER)); $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false); $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false); $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale); From 4336253b2fccc7acbca29f72d63ff2170229d31a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 20 Jul 2024 11:59:52 -0400 Subject: [PATCH 153/388] fix whitespace variable to "0" --- public/scripts/slash-commands/SlashCommandScope.js | 2 +- public/scripts/variables.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandScope.js b/public/scripts/slash-commands/SlashCommandScope.js index 30e545429..78edb78ae 100644 --- a/public/scripts/slash-commands/SlashCommandScope.js +++ b/public/scripts/slash-commands/SlashCommandScope.js @@ -97,7 +97,7 @@ export class SlashCommandScope { return v ?? ''; } else { const value = this.variables[key]; - return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value); + return (value?.trim?.() === '' || isNaN(Number(value))) ? (value || '') : Number(value); } } if (this.parent) { diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 4e968293f..709dfb9b1 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -41,7 +41,7 @@ function getLocalVariable(name, args = {}) { } } - return (localVariable === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable); + return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable); } function setLocalVariable(name, value, args = {}) { @@ -94,7 +94,7 @@ function getGlobalVariable(name, args = {}) { } } - return (globalVariable === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable); + return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable); } function setGlobalVariable(name, value, args = {}) { From 4191e3fa09e8ab47035cc14b30da6275c76dacc0 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 20 Jul 2024 12:00:22 -0400 Subject: [PATCH 154/388] fix /let key= not given priority --- public/scripts/variables.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 709dfb9b1..a17cbc01a 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -798,24 +798,29 @@ function randValuesCallback(from, to, args) { * @returns The variable's value */ function letCallback(args, value) { - if (Array.isArray(value)) { - args._scope.letVariable(value[0], typeof value[1] == 'string' ? value.slice(1).join(' ') : value[1]); - return value[1]; - } + if (!Array.isArray(value)) value = [value]; if (args.key !== undefined) { const key = args.key; - const val = value; + if (typeof key != 'string') throw new Error('Key must be a string'); + if (args._hasUnnamedArgument) { + const val = value.join(' '); + args._scope.letVariable(key, val); + return val; + } else { + args._scope.letVariable(key); + return ''; + } + } + const key = value.shift(); + if (typeof key != 'string') throw new Error('Key must be a string'); + if (value.length > 0) { + const val = value.join(' '); args._scope.letVariable(key, val); return val; + } else { + args._scope.letVariable(key); + return ''; } - if (value instanceof SlashCommandClosure) throw new Error('/let unnamed argument does not support closures if no key is provided'); - if (value.includes(' ')) { - const key = value.split(' ')[0]; - const val = value.split(' ').slice(1).join(' '); - args._scope.letVariable(key, val); - return val; - } - args._scope.letVariable(value); } /** @@ -828,6 +833,7 @@ function varCallback(args, value) { if (!Array.isArray(value)) value = [value]; if (args.key !== undefined) { const key = args.key; + if (typeof key != 'string') throw new Error('Key must be a string'); if (args._hasUnnamedArgument) { const val = value.join(' '); args._scope.setVariable(key, val, args.index); @@ -837,6 +843,7 @@ function varCallback(args, value) { } } const key = value.shift(); + if (typeof key != 'string') throw new Error('Key must be a string'); if (value.length > 0) { const val = value.join(' '); args._scope.setVariable(key, val, args.index); From 7ab09c64325ef61f33c931f52180b353bdce25eb Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 20 Jul 2024 12:00:50 -0400 Subject: [PATCH 155/388] fix unclosed block comment infinite loop --- public/scripts/slash-commands/SlashCommandParser.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 75e8f5f1a..892ac2c26 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -797,7 +797,12 @@ export class SlashCommandParser { return this.testSymbol(/\/\*/); } testBlockCommentEnd() { - return this.testSymbol(/\*\|/); + if (!this.verifyCommandNames) { + if (this.index >= this.text.length) return true; + } else { + if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed block comment at position ${this.userIndex}`, this.text, this.index); + } + return this.testSymbol('*|'); } parseBlockComment() { const start = this.index + 1; From bff5977f0294ae8e5cfcefc0c0ac9f5cf4831adf Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 20 Jul 2024 12:01:00 -0400 Subject: [PATCH 156/388] don't need regex symbol here --- public/scripts/slash-commands/SlashCommandParser.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 892ac2c26..899970930 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -794,7 +794,7 @@ export class SlashCommandParser { } testBlockComment() { - return this.testSymbol(/\/\*/); + return this.testSymbol('/*'); } testBlockCommentEnd() { if (!this.verifyCommandNames) { @@ -826,7 +826,12 @@ export class SlashCommandParser { return this.testSymbol(/\/[/#]/); } testCommentEnd() { - return this.testCommandEnd(); + if (!this.verifyCommandNames) { + if (this.index >= this.text.length) return true; + } else { + if (this.endOfText) throw new SlashCommandParserError(`Unclosed comment at position ${this.userIndex}`, this.text, this.index); + } + return this.testSymbol('|'); } parseComment() { const start = this.index + 1; From 5712128ac06c5c85acdc00faea02f9d2fa515107 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 20 Jul 2024 12:16:47 -0400 Subject: [PATCH 157/388] improve QR editor performance - only run hljs with syntax enabled - only check localStorage once, then rely on the checkbox - run hljs on a 30fps loop instead of event-based - use morphdom to update syntax dom instead of innerHTML --- .../quick-reply/lib/morphdom-esm.js | 769 ++++++++++++++++++ .../quick-reply/lib/morphdom.LICENSE.txt | 21 + .../extensions/quick-reply/src/QuickReply.js | 34 +- 3 files changed, 819 insertions(+), 5 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/lib/morphdom-esm.js create mode 100644 public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt diff --git a/public/scripts/extensions/quick-reply/lib/morphdom-esm.js b/public/scripts/extensions/quick-reply/lib/morphdom-esm.js new file mode 100644 index 000000000..7a13a27fc --- /dev/null +++ b/public/scripts/extensions/quick-reply/lib/morphdom-esm.js @@ -0,0 +1,769 @@ +var DOCUMENT_FRAGMENT_NODE = 11; + +function morphAttrs(fromNode, toNode) { + var toNodeAttrs = toNode.attributes; + var attr; + var attrName; + var attrNamespaceURI; + var attrValue; + var fromValue; + + // document-fragments dont have attributes so lets not do anything + if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { + return; + } + + // update attributes on original DOM element + for (var i = toNodeAttrs.length - 1; i >= 0; i--) { + attr = toNodeAttrs[i]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + attrValue = attr.value; + + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); + + if (fromValue !== attrValue) { + if (attr.prefix === 'xmlns'){ + attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix + } + fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); + } + } else { + fromValue = fromNode.getAttribute(attrName); + + if (fromValue !== attrValue) { + fromNode.setAttribute(attrName, attrValue); + } + } + } + + // Remove any extra attributes found on the original DOM element that + // weren't found on the target element. + var fromNodeAttrs = fromNode.attributes; + + for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { + attr = fromNodeAttrs[d]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + + if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { + fromNode.removeAttributeNS(attrNamespaceURI, attrName); + } + } else { + if (!toNode.hasAttribute(attrName)) { + fromNode.removeAttribute(attrName); + } + } + } +} + +var range; // Create a range object for efficently rendering strings to elements. +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; + +var doc = typeof document === 'undefined' ? undefined : document; +var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template'); +var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange(); + +function createFragmentFromTemplate(str) { + var template = doc.createElement('template'); + template.innerHTML = str; + return template.content.childNodes[0]; +} + +function createFragmentFromRange(str) { + if (!range) { + range = doc.createRange(); + range.selectNode(doc.body); + } + + var fragment = range.createContextualFragment(str); + return fragment.childNodes[0]; +} + +function createFragmentFromWrap(str) { + var fragment = doc.createElement('body'); + fragment.innerHTML = str; + return fragment.childNodes[0]; +} + +/** + * This is about the same + * var html = new DOMParser().parseFromString(str, 'text/html'); + * return html.body.firstChild; + * + * @method toElement + * @param {String} str + */ +function toElement(str) { + str = str.trim(); + if (HAS_TEMPLATE_SUPPORT) { + // avoid restrictions on content for things like `Hi` which + // createContextualFragment doesn't support + //