From 83d58810c1f4c034b460f9e14c388d5c55dc5235 Mon Sep 17 00:00:00 2001 From: webbrain-one <295484252+webbrain-one@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:55:54 +0300 Subject: [PATCH 1/2] Support custom DuckyScript commands in syntax highlighting Allow users to configure additional commands via settings.json to extend syntax highlighting. This enables support for custom extensions used by projects like USBArmyKnife and Flipper Zero. --- package.json | 14 +++++++++++- src/extension.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d98ec59..62a95a5 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,19 @@ "language": "duckyscript", "path": "./snippets/snippets.json" } - ] + ], + "configuration": { + "type": "object", + "title": "DuckyScript", + "properties": { + "duckyscript.customCommands": { + "type": "array", + "items": { "type": "string" }, + "default": [], + "description": "List of custom commands to highlight in DuckyScript files." + } + } + } }, "scripts": { "vscode:prepublish": "npm run compile", diff --git a/src/extension.ts b/src/extension.ts index 0865492..b604d1f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -54,6 +54,54 @@ const updatedCommandGroups = commandGroups.map(group => const ALL_COMMANDS = commandGroups.flat(); +const semanticTokenLegend = new vscode.SemanticTokensLegend( + ['customCommand'], + [] +); + +class CustomCommandSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider { + private customCommands: Set = new Set(); + private _emitter = new vscode.EventEmitter(); + onDidChangeSemanticTokens = this._emitter.event; + + constructor() { + this.updateCustomCommands(); + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('duckyscript.customCommands')) { + this.updateCustomCommands(); + this._emitter.fire(); + } + }); + } + + private updateCustomCommands() { + const config = vscode.workspace.getConfiguration('duckyscript'); + const commands = config.get('customCommands', []); + this.customCommands = new Set(commands.map(c => c.toUpperCase())); + } + + provideDocumentSemanticTokens( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend); + for (let i = 0; i < document.lineCount; i++) { + const line = document.lineAt(i); + const match = line.text.match(/^\s*([A-Z_][A-Z0-9_]*)\b/); + if (match && this.customCommands.has(match[1].toUpperCase())) { + builder.push({ + line: i, + startColumn: match.index ?? 0, + length: match[1].length, + tokenType: 0, + tokenModifiers: 0 + }); + } + } + return builder.build(); + } +} + // We implement a CompletionItemProvider for our language class MyLanguageCompletionItemProvider implements vscode.CompletionItemProvider { // This method is called when the user activates the suggestions (e.g., Ctrl+Space) @@ -98,6 +146,13 @@ export function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push(providerDisposable); + + // Custom commands syntax highlighting + const customTokenProvider = vscode.languages.registerDocumentSemanticTokensProvider( + { language: 'duckyscript' }, + new CustomCommandSemanticTokensProvider() + ); + context.subscriptions.push(customTokenProvider); } function deactivate() { } From 546eda011f8efa060f541568f49deb1c17c0c0fd Mon Sep 17 00:00:00 2001 From: aleff-github Date: Wed, 9 Sep 2026 08:07:03 +0200 Subject: [PATCH 2/2] fix: make custom command highlighting work end-to-end Builds on @webbrain-one's implementation in #3, which is kept as the base of this branch. - pass the legend to registerDocumentSemanticTokensProvider and use the numeric SemanticTokensBuilder.push overload (the branch did not compile) - declare duckyscriptCustomCommand in contributes.semanticTokenTypes and map it to support.function.builtin.duckyscript, so any theme colours it - enable semanticHighlighting in the two bundled themes, which shipped with it turned off - match against the configured command list (escaped, case-insensitive, longest-first) and compute the start column correctly on indented lines - dispose the configuration listener and emitter, honour cancellation Closes #2 Co-authored-by: webbrain-one <295484252+webbrain-one@users.noreply.github.com> Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TRLqQ6ZvofMhPeunTaR8GX --- out/extension.js | 53 ++++++++++++++++++ out/extension.js.map | 2 +- package.json | 26 ++++++++- src/extension.ts | 71 +++++++++++++++--------- themes/DarkDuckyScript-color-theme.json | 2 +- themes/LightDuckyScript-color-theme.json | 2 +- 6 files changed, 125 insertions(+), 31 deletions(-) diff --git a/out/extension.js b/out/extension.js index 6680e78..c98667c 100644 --- a/out/extension.js +++ b/out/extension.js @@ -48,6 +48,56 @@ const commandGroups = [ const suffix = `\n*Source: https://docs.hak5.org/hak5-usb-rubber-ducky/duckyscript-tm-quick-reference*\n\n*Did you find something incorrect or something missing? [Write it to me](https://github.com/aleff-github/DuckyScriptCookbook/issues) to contribute or edit it yourself!*`; const updatedCommandGroups = commandGroups.map(group => group.map(command => command.doc.appendMarkdown(suffix))); const ALL_COMMANDS = commandGroups.flat(); +const CUSTOM_COMMAND_TOKEN = 'duckyscriptCustomCommand'; +const semanticTokenLegend = new vscode.SemanticTokensLegend([CUSTOM_COMMAND_TOKEN], []); +function escapeRegExp(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} +class CustomCommandSemanticTokensProvider { + constructor() { + this._emitter = new vscode.EventEmitter(); + this.onDidChangeSemanticTokens = this._emitter.event; + this.updateCustomCommands(); + this._configListener = vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('duckyscript.customCommands')) { + this.updateCustomCommands(); + this._emitter.fire(); + } + }); + } + updateCustomCommands() { + const config = vscode.workspace.getConfiguration('duckyscript'); + const commands = config.get('customCommands', []) + .filter(c => typeof c === 'string' && c.trim().length > 0) + .map(c => c.trim()) + // Longest first, so PREFIX_LONG wins over PREFIX. + .sort((a, b) => b.length - a.length); + this.commandPattern = commands.length + ? new RegExp(`^\\s*(${commands.map(escapeRegExp).join('|')})(?![\\w-])`, 'i') + : undefined; + } + provideDocumentSemanticTokens(document, token) { + const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend); + if (!this.commandPattern) { + return builder.build(); + } + for (let i = 0; i < document.lineCount; i++) { + if (token.isCancellationRequested) { + return builder.build(); + } + const match = this.commandPattern.exec(document.lineAt(i).text); + if (match) { + const startColumn = match[0].length - match[1].length; + builder.push(i, startColumn, match[1].length, 0, 0); + } + } + return builder.build(); + } + dispose() { + this._configListener.dispose(); + this._emitter.dispose(); + } +} // We implement a CompletionItemProvider for our language class MyLanguageCompletionItemProvider { // This method is called when the user activates the suggestions (e.g., Ctrl+Space) @@ -76,6 +126,9 @@ function activate(context) { // Completion provider const providerDisposable = vscode.languages.registerCompletionItemProvider({ language: 'duckyscript' }, new MyLanguageCompletionItemProvider()); context.subscriptions.push(providerDisposable); + // Custom commands syntax highlighting + const customCommandProvider = new CustomCommandSemanticTokensProvider(); + context.subscriptions.push(customCommandProvider, vscode.languages.registerDocumentSemanticTokensProvider({ language: 'duckyscript' }, customCommandProvider, semanticTokenLegend)); } function deactivate() { } module.exports = { diff --git a/out/extension.js.map b/out/extension.js.map index 36319e2..e7fda0f 100644 --- a/out/extension.js.map +++ b/out/extension.js.map @@ -1 +1 @@ -{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;AAmFA,4BAiBC;AApGD,iCAAiC;AACjC,gFAA0E;AAC1E,sDAA2D;AAC3D,8DAAoE;AACpE,8CAAoD;AACpD,4CAAkD;AAClD,8EAAoF;AACpF,oDAA0D;AAC1D,8CAAoD;AACpD,oDAA0D;AAC1D,gDAAsD;AACtD,sEAA4E;AAC5E,wCAA8C;AAC9C,0CAAgD;AAChD,4CAAkD;AAClD,oDAA0D;AAC1D,4CAAkD;AAClD,gEAAsE;AACtE,8DAA2D;AAC3D,4DAA2D;AAC3D,8CAAoD;AACpD,oDAA0D;AAI1D,MAAM,aAAa,GAAG;IAClB,gCAAmB;IACnB,wCAAuB;IACvB,wBAAe;IACf,sBAAc;IACd,wDAA+B;IAC/B,8BAAkB;IAClB,wBAAe;IACf,8BAAkB;IAClB,0BAAgB;IAChB,gDAA2B;IAC3B,kBAAY;IACZ,oBAAa;IACb,sBAAc;IACd,8BAAkB;IAClB,sBAAc;IACd,0CAAwB;IACxB,+BAAc;IACd,+BAAe;IACf,wBAAe;IACf,8BAAkB;CACnB,CAAC;AAEJ,MAAM,MAAM,GAAG,oQAAoQ,CAAC;AAEpR,MAAM,oBAAoB,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CACnD,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAC3D,CAAC;AAEF,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,EAAE,CAAC;AAE1C,yDAAyD;AACzD,MAAM,gCAAgC;IAClC,mFAAmF;IAC5E,sBAAsB,CACzB,QAA6B,EAC7B,QAAyB,EACzB,KAA+B,EAC/B,OAAiC;QAGjC,MAAM,WAAW,GAA4B,YAAY,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YACtE,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;YACzF,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC7B,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC;YACjC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAC/B,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QAEL,OAAO,WAAW,CAAC;IACvB,CAAC;CACJ;AAGD;;;GAGG;AACH,SAAgB,QAAQ,CAAC,OAAgC;IACxD,UAAU;IACV,MAAM,QAAQ,GAAG,IAAI,gDAAqB,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO,CAAC,aAAa,CAAC,IAAI,CACzB,MAAM,CAAC,MAAM,CAAC,2BAA2B,CAAC,gDAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IACtF,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC5D,OAAO,CAAC,aAAa,CAAC,IAAI,CACzB,MAAM,CAAC,MAAM,CAAC,2BAA2B,CAAC,gDAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;IAEH,sBAAsB;IACnB,MAAM,kBAAkB,GAAG,MAAM,CAAC,SAAS,CAAC,8BAA8B,CACtE,EAAE,QAAQ,EAAE,aAAa,EAAE,EAC3B,IAAI,gCAAgC,EAAE,CACzC,CAAC;IAEF,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,UAAU,KAAK,CAAC;AAEzB,MAAM,CAAC,OAAO,GAAG;IAChB,QAAQ;IACR,UAAU;CACV,CAAC"} \ No newline at end of file +{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 62a95a5..5909984 100644 --- a/package.json +++ b/package.json @@ -118,12 +118,32 @@ "properties": { "duckyscript.customCommands": { "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "default": [], - "description": "List of custom commands to highlight in DuckyScript files." + "scope": "resource", + "markdownDescription": "Extra DuckyScript commands to highlight, for firmware that extends the language (e.g. [USB Army Knife](https://github.com/i-am-shodan/USBArmyKnife/wiki/Commands), Flipper Zero). Matching is case-insensitive and applies to the first word of a line." } } - } + }, + "semanticTokenTypes": [ + { + "id": "duckyscriptCustomCommand", + "superType": "function", + "description": "A user-defined DuckyScript command from duckyscript.customCommands." + } + ], + "semanticTokenScopes": [ + { + "language": "duckyscript", + "scopes": { + "duckyscriptCustomCommand": [ + "support.function.builtin.duckyscript" + ] + } + } + ] }, "scripts": { "vscode:prepublish": "npm run compile", diff --git a/src/extension.ts b/src/extension.ts index b604d1f..7fc8535 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -54,19 +54,23 @@ const updatedCommandGroups = commandGroups.map(group => const ALL_COMMANDS = commandGroups.flat(); -const semanticTokenLegend = new vscode.SemanticTokensLegend( - ['customCommand'], - [] -); +const CUSTOM_COMMAND_TOKEN = 'duckyscriptCustomCommand'; + +const semanticTokenLegend = new vscode.SemanticTokensLegend([CUSTOM_COMMAND_TOKEN], []); -class CustomCommandSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider { - private customCommands: Set = new Set(); - private _emitter = new vscode.EventEmitter(); +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +class CustomCommandSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider, vscode.Disposable { + private commandPattern: RegExp | undefined; + private readonly _emitter = new vscode.EventEmitter(); + private readonly _configListener: vscode.Disposable; onDidChangeSemanticTokens = this._emitter.event; constructor() { this.updateCustomCommands(); - vscode.workspace.onDidChangeConfiguration(e => { + this._configListener = vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('duckyscript.customCommands')) { this.updateCustomCommands(); this._emitter.fire(); @@ -76,30 +80,43 @@ class CustomCommandSemanticTokensProvider implements vscode.DocumentSemanticToke private updateCustomCommands() { const config = vscode.workspace.getConfiguration('duckyscript'); - const commands = config.get('customCommands', []); - this.customCommands = new Set(commands.map(c => c.toUpperCase())); + const commands = config.get('customCommands', []) + .filter(c => typeof c === 'string' && c.trim().length > 0) + .map(c => c.trim()) + // Longest first, so PREFIX_LONG wins over PREFIX. + .sort((a, b) => b.length - a.length); + + this.commandPattern = commands.length + ? new RegExp(`^\\s*(${commands.map(escapeRegExp).join('|')})(?![\\w-])`, 'i') + : undefined; } provideDocumentSemanticTokens( document: vscode.TextDocument, - _token: vscode.CancellationToken + token: vscode.CancellationToken ): vscode.ProviderResult { const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend); + if (!this.commandPattern) { + return builder.build(); + } + for (let i = 0; i < document.lineCount; i++) { - const line = document.lineAt(i); - const match = line.text.match(/^\s*([A-Z_][A-Z0-9_]*)\b/); - if (match && this.customCommands.has(match[1].toUpperCase())) { - builder.push({ - line: i, - startColumn: match.index ?? 0, - length: match[1].length, - tokenType: 0, - tokenModifiers: 0 - }); + if (token.isCancellationRequested) { + return builder.build(); + } + const match = this.commandPattern.exec(document.lineAt(i).text); + if (match) { + const startColumn = match[0].length - match[1].length; + builder.push(i, startColumn, match[1].length, 0, 0); } } return builder.build(); } + + dispose() { + this._configListener.dispose(); + this._emitter.dispose(); + } } // We implement a CompletionItemProvider for our language @@ -148,11 +165,15 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(providerDisposable); // Custom commands syntax highlighting - const customTokenProvider = vscode.languages.registerDocumentSemanticTokensProvider( - { language: 'duckyscript' }, - new CustomCommandSemanticTokensProvider() + const customCommandProvider = new CustomCommandSemanticTokensProvider(); + context.subscriptions.push( + customCommandProvider, + vscode.languages.registerDocumentSemanticTokensProvider( + { language: 'duckyscript' }, + customCommandProvider, + semanticTokenLegend + ) ); - context.subscriptions.push(customTokenProvider); } function deactivate() { } diff --git a/themes/DarkDuckyScript-color-theme.json b/themes/DarkDuckyScript-color-theme.json index ec8f101..a0ea7a5 100644 --- a/themes/DarkDuckyScript-color-theme.json +++ b/themes/DarkDuckyScript-color-theme.json @@ -1,7 +1,7 @@ { "name": "DuckyScript - Night Owl based", "type": "dark", - "semanticHighlighting": false, + "semanticHighlighting": true, "colors": { "contrastBorder": "#122d42", "focusBorder": "#122d42", diff --git a/themes/LightDuckyScript-color-theme.json b/themes/LightDuckyScript-color-theme.json index 239c9b4..369bf92 100644 --- a/themes/LightDuckyScript-color-theme.json +++ b/themes/LightDuckyScript-color-theme.json @@ -1,7 +1,7 @@ { "name": "DuckyScript - Night Owl Light based", "type": "light", - "semanticHighlighting": false, + "semanticHighlighting": true, "colors": { // Base "foreground": "#403f53",