From 7ebf23e9e6deeaa0507da1e97ae57bc5ee60a27c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 16 Jun 2024 08:41:05 -0400 Subject: [PATCH 001/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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/393] 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 a410c63333069100aee6d4e4b54196cd6accfa24 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 6 Jul 2024 01:14:47 +0300 Subject: [PATCH 077/393] Fix ComfyUI workflow not saving --- public/scripts/extensions/stable-diffusion/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index b7f0a6496..592d2f146 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -30,7 +30,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { debounce_timeout } from '../../constants.js'; import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; -import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; +import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; export { MODULE_NAME }; const MODULE_NAME = 'sd'; @@ -2976,7 +2976,12 @@ async function onComfyOpenWorkflowEditorClick() { }), })).json(); const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html')); - const popupResult = callGenericPopup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Save', cancelButton: 'Cancel', wide: true, large: true }); + const saveValue = (/** @type {Popup} */ _popup) => { + workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); + return true; + }; + const popup = new Popup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Save', cancelButton: 'Cancel', wide: true, large: true, onClosing: saveValue }); + const popupResult = popup.show(); const checkPlaceholders = () => { workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); $('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function (idx) { @@ -3045,7 +3050,7 @@ async function onComfyOpenWorkflowEditorClick() { headers: getRequestHeaders(), body: JSON.stringify({ file_name: extension_settings.sd.comfy_workflow, - workflow: $('#sd_comfy_workflow_editor_workflow').val().toString(), + workflow: workflow, }), }); if (!response.ok) { From ba0f5427cf28f424a6c8fb6c7497a11f37a5432a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 5 Jul 2024 18:53:55 -0400 Subject: [PATCH 078/393] 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 079/393] 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 080/393] 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 081/393] 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 082/393] 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 083/393] 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 084/393] 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 085/393] 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 086/393] 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 087/393] 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 088/393] 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 089/393] 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 090/393] 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 092/393] 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 093/393] 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 094/393] 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 095/393] 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 096/393] 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 097/393] 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 098/393] 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 099/393] 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 100/393] 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 101/393] 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 102/393] 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 103/393] 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 3c1d639ce511be7d33b961854db5fc423779c99a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:28:42 +0300 Subject: [PATCH 104/393] Update CONTRIBUTING.md --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84253f028..833bf62dc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,4 +29,8 @@ - Updating GitHub Actions. - Hotfixing a critical bug. 4. Project maintainers will test and can change your code before merging. -5. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer. +5. Write at least somewhat meaningful PR descriptions. There's no "right" way to do it, but the following may help with outlining a general structure: + - What is the reason for a change? + - What did you do to achieve this? + - How would a reviewer test the change? +6. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer. From 956a676390941a868a141b7e6a180dca2efc7f6a Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 12 Jul 2024 15:05:39 -0400 Subject: [PATCH 105/393] 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 106/393] 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 107/393] 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 108/393] 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 109/393] 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 110/393] 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 111/393] 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 112/393] 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 114/393] 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 115/393] 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 116/393] 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 b7a1474d7badd8da71504c5c9c0d5a78649909d3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 15 Jul 2024 00:42:16 +0300 Subject: [PATCH 117/393] Switch to dynamic viewport units --- public/css/character-group-overlay.css | 2 +- public/css/loader.css | 4 +- public/css/logprobs.css | 6 +- public/css/mobile-styles.css | 54 ++++++------ public/css/popup-safari-fix.css | 2 +- public/css/popup.css | 8 +- public/css/toggle-dependent.css | 2 +- .../scripts/extensions/quick-reply/style.css | 4 +- .../scripts/extensions/quick-reply/style.less | 4 +- public/style.css | 85 +++++++++---------- 10 files changed, 83 insertions(+), 88 deletions(-) diff --git a/public/css/character-group-overlay.css b/public/css/character-group-overlay.css index 5ccbefd9b..b0d4b4a14 100644 --- a/public/css/character-group-overlay.css +++ b/public/css/character-group-overlay.css @@ -89,7 +89,7 @@ position: absolute; width: 100%; height: 100vh; - height: 100svh; + height: 100dvh; z-index: 9998; top: 0; } diff --git a/public/css/loader.css b/public/css/loader.css index dea0eb5a3..acf82613d 100644 --- a/public/css/loader.css +++ b/public/css/loader.css @@ -7,8 +7,8 @@ z-index: 999999; width: 100vw; height: 100vh; - width: 100svw; - height: 100svh; + width: 100dvw; + height: 100dvh; background-color: var(--SmartThemeBlurTintColor); color: var(--SmartThemeBodyColor); /*for some reason the full screen blur does not work on iOS*/ diff --git a/public/css/logprobs.css b/public/css/logprobs.css index 4f129932a..0cea4f67f 100644 --- a/public/css/logprobs.css +++ b/public/css/logprobs.css @@ -1,7 +1,7 @@ #logprobsViewer { overflow-y: auto; - max-width: 90svw; - max-height: 90svh; + max-width: 90dvw; + max-height: 90dvh; min-width: 100px; min-height: 50px; border-radius: 10px; @@ -16,7 +16,7 @@ top: 0; margin: 0; right: unset; - width: calc(((100svw - var(--sheldWidth)) / 2) - 1px); + width: calc(((100dvw - var(--sheldWidth)) / 2) - 1px); } .logprobs_panel_header { diff --git a/public/css/mobile-styles.css b/public/css/mobile-styles.css index dca2c8fe8..4280e8150 100644 --- a/public/css/mobile-styles.css +++ b/public/css/mobile-styles.css @@ -1,6 +1,8 @@ /*will apply to anything 1000px or less. this catches ipads, horizontal phones, and vertical phones)*/ @media screen and (max-width: 1000px) { - #send_form.compact #leftSendForm, #send_form.compact #rightSendForm { + + #send_form.compact #leftSendForm, + #send_form.compact #rightSendForm { flex-wrap: nowrap; width: unset; } @@ -30,9 +32,9 @@ right: 0; width: fit-content; max-height: calc(60vh - 60px); - max-height: calc(60svh - 60px); + max-height: calc(60dvh - 60px); max-width: 90vw; - max-width: 90svw; + max-width: 90dvw; left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); @@ -98,7 +100,7 @@ min-width: unset; width: 100%; max-height: calc(100vh - 45px); - max-height: calc(100svh - 45px); + max-height: calc(100dvh - 45px); position: fixed; left: 0; top: 5px; @@ -126,15 +128,15 @@ #top-bar { position: fixed; width: 100vw; - width: 100svw; + width: 100dvw; } #bg1, #bg_custom { height: 100vh !important; - height: 100svh !important; + height: 100dvh !important; width: 100vw !important; - width: 100svw !important; + width: 100dvw !important; background-position: center; } @@ -142,13 +144,7 @@ #sheld, #character_popup, - .drawer-content - - /* , - #world_popup */ - { - /*max-height: calc(100vh - 36px); - max-height: calc(100svh - 36px);*/ + .drawer-content { width: 100% !important; margin: 0 auto; max-width: 100%; @@ -219,10 +215,10 @@ #floatingPrompt, #cfgConfig, #logprobsViewer, - #movingDivs > div { - /* 100vh are fallback units for browsers that don't support svh */ + #movingDivs>div { + /* 100vh are fallback units for browsers that don't support dvh */ height: calc(100vh - 45px); - height: calc(100svh - 45px); + height: calc(100dvh - 45px); min-width: 100% !important; width: 100% !important; max-width: 100% !important; @@ -245,7 +241,7 @@ #floatingPrompt, #cfgConfig, #logprobsViewer, - #movingDivs > div { + #movingDivs>div { height: min-content; } @@ -282,9 +278,9 @@ body.waifuMode #sheld { height: 40vh; - height: 40svh; + height: 40dvh; top: 60vh; - top: 60svh; + top: 60dvh; bottom: 0 !important; } @@ -321,16 +317,16 @@ body.waifuMode .zoomed_avatar { width: fit-content; max-height: calc(60vh - 60px); - max-height: calc(60svh - 60px); + max-height: calc(60dvh - 60px); max-width: 90vw; - max-width: 90svw; + max-width: 90dvw; } .scrollableInner { overflow-y: auto; overflow-x: hidden; max-height: calc(100vh - 90px); - max-height: calc(100svh - 90px); + max-height: calc(100dvh - 90px); } .horde_multiple_hint { @@ -366,9 +362,9 @@ body:not(.waifuMode) .zoomed_avatar { max-height: calc(60vh - 60px); - max-height: calc(60svh - 60px); + max-height: calc(60dvh - 60px); max-width: 90vw; - max-width: 90svw; + max-width: 90dvw; left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); @@ -449,9 +445,9 @@ min-height: unset; max-height: unset; width: 100vw; - width: 100svw; + width: 100dvw; height: calc(100vh - 36px); - height: calc(100svh - 36px); + height: calc(100dvh - 36px); padding-right: max(env(safe-area-inset-right), 0px); padding-left: max(env(safe-area-inset-left), 0px); padding-bottom: 0; @@ -481,10 +477,10 @@ top: 0; margin: 0 auto; height: calc(100vh - 70px); - height: calc(100svh - 70px); + height: calc(100dvh - 70px); width: calc(100% - 5px); max-height: calc(100vh - 70px); - max-height: calc(100svh - 70px); + max-height: calc(100dvh - 70px); max-width: calc(100% - 5px); } diff --git a/public/css/popup-safari-fix.css b/public/css/popup-safari-fix.css index 6838e3352..e9dbdc7f9 100644 --- a/public/css/popup-safari-fix.css +++ b/public/css/popup-safari-fix.css @@ -7,5 +7,5 @@ body.safari .popup.large_dialogue_popup .popup-body { body.safari .popup .popup-body { height: fit-content; max-height: 90vh; - max-height: 90svh; + max-height: 90dvh; } diff --git a/public/css/popup.css b/public/css/popup.css index f05006749..6809d7248 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -16,8 +16,8 @@ dialog { display: flex; flex-direction: column; - max-height: calc(100svh - 2em); - max-width: calc(100svw - 2em); + max-height: calc(100dvh - 2em); + max-width: calc(100dvw - 2em); min-height: fit-content; /* Overflow visible so elements (like toasts) can appear outside of the dialog. '.popup-body' is hiding overflow for the real content. */ @@ -103,7 +103,7 @@ body.no-blur .popup[open]::backdrop { .popup #toast-container { /* Fix toastr in dialogs by actually placing it at the top of the screen via transform */ - height: 100svh; + height: 100dvh; top: calc(50% + var(--topBarBlockSize)); left: 50%; transform: translate(-50%, -50%); @@ -115,7 +115,7 @@ body.no-blur .popup[open]::backdrop { .popup-crop-wrap { margin: 10px auto; max-height: 75vh; - max-height: 75svh; + max-height: 75dvh; max-width: 100%; } diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 774e86089..387ce0b27 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -360,7 +360,7 @@ body.waifuMode #top-bar { body.waifuMode #sheld { height: 40vh; - height: 40svh; + height: 40dvh; top: calc(100% - 40vh); bottom: 0; } diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index c27d89fd3..2a7774924 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -231,8 +231,8 @@ flex-direction: column; } body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder { - min-height: 50svh; - height: 50svh; + min-height: 50dvh; + height: 50dvh; } } .popup:has(#qr--modalEditor) { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index e2a0e64fe..8b2f13524 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -297,8 +297,8 @@ } >#qr--main>.qr--modal-messageContainer>#qr--modal-messageHolder { - min-height: 50svh; - height: 50svh; + min-height: 50dvh; + height: 50dvh; } } } diff --git a/public/style.css b/public/style.css index 95a61cdae..23da460d9 100644 --- a/public/style.css +++ b/public/style.css @@ -137,7 +137,6 @@ body { width: 100%; /*fallback for JS load*/ height: 100vh; - height: 100svh; height: 100dvh; /*defaults as 100%, then reassigned via JS as pixels, will work on PC and Android*/ /*height: calc(var(--doc-height) - 1px);*/ @@ -451,7 +450,7 @@ code { border-radius: 5px; background-color: var(--black70a); padding: 0 3px; - /* max-width: calc(100svw - 95px); */ + /* max-width: calc(100dvw - 95px); */ line-height: var(--mainFontSize); color: var(--white70a); } @@ -535,13 +534,13 @@ body.reduced-motion #bg_custom { flex-direction: column; /* -1px to give sheld some wiggle room to bounce off tobar when moving*/ height: calc(100vh - var(--topBarBlockSize) - 1px); - height: calc(100svh - var(--topBarBlockSize) - 1px); - max-height: calc(100svh - var(--topBarBlockSize) - 1px); + height: calc(100dvh - var(--topBarBlockSize) - 1px); + max-height: calc(100dvh - var(--topBarBlockSize) - 1px); overflow-x: hidden; /* max-width: 50vw; */ position: absolute; left: calc((100vw - var(--sheldWidth))/2); - left: calc((100svw - var(--sheldWidth))/2); + left: calc((100dvw - var(--sheldWidth))/2); top: var(--topBarBlockSize); margin: 0 auto; left: 0; @@ -1160,12 +1159,12 @@ textarea { font-family: var(--mainFontFamily); padding: 5px 10px; max-height: 90vh; - max-height: 90svh; + max-height: 90dvh; } textarea.autoSetHeight { max-height: 50vh; - max-height: 50svh; + max-height: 50dvh; } input, @@ -1179,7 +1178,7 @@ select { min-height: calc(var(--bottomFormBlockSize) + 2px); height: calc(var(--bottomFormBlockSize) + 2px); max-height: 50vh; - max-height: 50svh; + max-height: 50dvh; word-wrap: break-word; resize: vertical; display: block; @@ -2111,14 +2110,14 @@ textarea::placeholder { @media screen and (max-width: 1000px) { #form_create textarea { flex-grow: 1; - min-height: 20svh; + min-height: 20dvh; } } @media screen and (min-width: 1001px) { #description_textarea { height: 29vh; - height: 29svh; + height: 29dvh; } #firstmessage_textarea { @@ -2394,8 +2393,8 @@ input[type="file"] { #floatingPrompt, #cfgConfig { overflow-y: auto; - max-width: 90svw; - max-height: 90svh; + max-width: 90dvw; + max-height: 90dvh; min-width: 100px; min-height: 100px; border-radius: 10px; @@ -2411,7 +2410,7 @@ input[type="file"] { top: 0; margin: 0; right: unset; - width: calc(((100svw - var(--sheldWidth)) / 2) - 1px); + width: calc(((100dvw - var(--sheldWidth)) / 2) - 1px); } @@ -2768,7 +2767,7 @@ input[type=search]:focus::-webkit-search-cancel-button { flex-wrap: wrap; width: calc(var(--sheldWidth) - 10px); max-width: 100vw; - max-width: 100svw; + max-width: 100dvw; justify-content: space-evenly; } @@ -3123,7 +3122,7 @@ grammarly-extension { #dialogue_popup { width: 500px; max-width: 90vw; - max-width: 90svw; + max-width: 90dvw; position: absolute; z-index: 9999; margin-left: auto; @@ -3139,7 +3138,7 @@ grammarly-extension { background-color: var(--SmartThemeBlurTintColor); border-radius: 10px; max-height: 90vh; - max-height: 90svh; + max-height: 90dvh; display: flex; flex-direction: column; overflow-y: hidden; @@ -3153,9 +3152,9 @@ grammarly-extension { .large_dialogue_popup { height: 90vh !important; - height: 90svh !important; + height: 90dvh !important; max-width: 90vw !important; - max-width: 90svw !important; + max-width: 90dvw !important; } .wide_dialogue_popup { @@ -3307,7 +3306,7 @@ grammarly-extension { position: absolute; width: 100%; height: 100vh; - height: 100svh; + height: 100dvh; z-index: 9999; top: 0; } @@ -3315,9 +3314,9 @@ grammarly-extension { #bgtest { display: none; width: 100vw; - width: 100svw; + width: 100dvw; height: 100vh; - height: 100svh; + height: 100dvh; position: absolute; z-index: -100; background-color: red; @@ -3954,7 +3953,7 @@ input[type="range"]::-webkit-slider-thumb { position: absolute; width: 100%; height: 100vh; - height: 100svh; + height: 100dvh; z-index: 2058; } @@ -3967,11 +3966,11 @@ input[type="range"]::-webkit-slider-thumb { min-width: 100px; max-width: var(--sheldWidth); height: calc(100vh - 84px); - height: calc(100svh - 84px); + height: calc(100dvh - 84px); min-height: calc(100vh - 84px); - min-height: calc(100svh - 84px); + min-height: calc(100dvh - 84px); max-height: calc(100vh - 84px); - max-height: calc(100svh - 84px); + max-height: calc(100dvh - 84px); position: absolute; z-index: 4001; margin-left: auto; @@ -4050,7 +4049,7 @@ h5 { position: absolute; width: 100%; height: 100vh; - height: 100svh; + height: 100dvh; z-index: 4100; top: 0; background-color: var(--black70a); @@ -4064,7 +4063,7 @@ h5 { max-width: var(--sheldWidth); height: min-content; max-height: calc(100vh - var(--topBarBlockSize)); - max-height: calc(100svh - var(--topBarBlockSize)); + max-height: calc(100dvh - var(--topBarBlockSize)); min-height: 100px; position: absolute; z-index: 2066; @@ -4384,14 +4383,14 @@ a { overflow-wrap: break-word; white-space: normal; max-width: calc(((100vw - 500px) / 2) - 10px); - max-width: calc(((100svw - 500px) / 2) - 10px); + max-width: calc(((100dvw - 500px) / 2) - 10px); position: absolute; z-index: 9999; max-height: 90vh; - max-height: 90svh; + max-height: 90dvh; /*unsure why, but this prevents scrollbars*/ height: 49vh; - height: 49svh; + height: 49dvh; padding: 5px; overflow-y: auto; @@ -4427,11 +4426,11 @@ a { #right-nav-panel { width: calc((100vw - var(--sheldWidth) - 2px) /2); - width: calc((100svw - var(--sheldWidth) - 2px) /2); + width: calc((100dvw - var(--sheldWidth) - 2px) /2); max-height: calc(100vh - var(--topBarBlockSize)); - max-height: calc(100svh - var(--topBarBlockSize)); + max-height: calc(100dvh - var(--topBarBlockSize)); height: calc(100vh - var(--topBarBlockSize)); - height: calc(100svh - var(--topBarBlockSize)); + height: calc(100dvh - var(--topBarBlockSize)); position: fixed; top: 0; margin: 0; @@ -4484,7 +4483,7 @@ a { border-radius: 10px; max-width: 100%; max-height: 40vh; - max-height: 40svh; + max-height: 40dvh; image-rendering: -webkit-optimize-contrast; } @@ -4576,18 +4575,18 @@ body:not(.caption) .mes_img_caption { .img_enlarged_container pre { max-height: 25vh; - max-height: 25svh; + max-height: 25dvh; flex-shrink: 0; overflow: auto; } .popup:has(.img_enlarged.zoomed).large_dialogue_popup { height: 100vh !important; - height: 100svh !important; + height: 100dvh !important; max-height: 100vh !important; - max-height: 100svh !important; + max-height: 100dvh !important; max-width: 100vw !important; - max-width: 100svw !important; + max-width: 100dvw !important; padding: 0; } @@ -4774,7 +4773,7 @@ body:has(#character_popup.open) #top-settings-holder:has(.drawer-content.openDra width: var(--sheldWidth); overflow-y: auto; max-height: calc(100vh - calc(var(--topBarBlockSize) + var(--bottomFormBlockSize))); - max-height: calc(100svh - calc(var(--topBarBlockSize) + var(--bottomFormBlockSize))); + max-height: calc(100dvh - calc(var(--topBarBlockSize) + var(--bottomFormBlockSize))); display: none; position: absolute; top: var(--topBarBlockSize); @@ -4809,11 +4808,11 @@ body:not(.movingUI) .drawer-content.maximized { .fillLeft { width: calc((100vw - var(--sheldWidth) - 2px) /2); - width: calc((100svw - var(--sheldWidth) - 2px) /2); + width: calc((100dvw - var(--sheldWidth) - 2px) /2); height: calc(100vh - var(--topBarBlockSize)); - height: calc(100svh - var(--topBarBlockSize)); + height: calc(100dvh - var(--topBarBlockSize)); max-height: calc(100vh - var(--topBarBlockSize)); - max-height: calc(100svh - var(--topBarBlockSize)); + max-height: calc(100dvh - var(--topBarBlockSize)); position: fixed; top: 0; margin: 0; @@ -5063,7 +5062,7 @@ body:not(.movingUI) .drawer-content.maximized { width: 100%; /* margin-inline: 10px; */ max-height: 90vh; - max-width: 90svh; + max-width: 90dvh; } .zoomed_avatar img { From 11155770e44d06af2c8874f29bf211066e721dc1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 15 Jul 2024 00:44:50 +0300 Subject: [PATCH 118/393] Add widget resize --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 450f33f0e..3f49cd424 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ SillyTavern - + From db1094e391c3f46dad673836d9483b1e761deba1 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 14 Jul 2024 18:58:13 -0400 Subject: [PATCH 119/393] 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 120/393] 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 121/393] 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 122/393] 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 123/393] 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 124/393] 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 125/393] 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 126/393] 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 127/393] 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 128/393] 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 129/393] 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 130/393] 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 131/393] 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 135/393] 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 136/393] 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 152/393] 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 153/393] 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 154/393] 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 155/393] 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 156/393] 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 157/393] 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 158/393] 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 159/393] 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 160/393] 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 161/393] 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 + //