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":";;AAoJA,4BA4BC;AAhLD,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,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAExD,MAAM,mBAAmB,GAAG,IAAI,MAAM,CAAC,oBAAoB,CAAC,CAAC,oBAAoB,CAAC,EAAE,EAAE,CAAC,CAAC;AAExF,SAAS,YAAY,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,mCAAmC;IAMrC;QAJiB,aAAQ,GAAG,IAAI,MAAM,CAAC,YAAY,EAAQ,CAAC;QAE5D,8BAAyB,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAG5C,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC5B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE;YACjE,IAAI,CAAC,CAAC,oBAAoB,CAAC,4BAA4B,CAAC,EAAE,CAAC;gBACvD,IAAI,CAAC,oBAAoB,EAAE,CAAC;gBAC5B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACzB,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,oBAAoB;QACxB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAW,gBAAgB,EAAE,EAAE,CAAC;aACtD,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;aACzD,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACnB,kDAAkD;aACjD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;QAEzC,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,MAAM;YACjC,CAAC,CAAC,IAAI,MAAM,CAAC,SAAS,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,GAAG,CAAC;YAC7E,CAAC,CAAC,SAAS,CAAC;IACpB,CAAC;IAED,6BAA6B,CACzB,QAA6B,EAC7B,KAA+B;QAE/B,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;QAC3B,CAAC;QAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;YAC1C,IAAI,KAAK,CAAC,uBAAuB,EAAE,CAAC;gBAChC,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;YAC3B,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAChE,IAAI,KAAK,EAAE,CAAC;gBACR,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;gBACtD,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACxD,CAAC;QACL,CAAC;QACD,OAAO,OAAO,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO;QACH,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC;CACJ;AAED,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;IAE/C,sCAAsC;IACtC,MAAM,qBAAqB,GAAG,IAAI,mCAAmC,EAAE,CAAC;IACxE,OAAO,CAAC,aAAa,CAAC,IAAI,CACtB,qBAAqB,EACrB,MAAM,CAAC,SAAS,CAAC,sCAAsC,CACnD,EAAE,QAAQ,EAAE,aAAa,EAAE,EAC3B,qBAAqB,EACrB,mBAAmB,CACtB,CACJ,CAAC;AACN,CAAC;AAED,SAAS,UAAU,KAAK,CAAC;AAEzB,MAAM,CAAC,OAAO,GAAG;IAChB,QAAQ;IACR,UAAU;CACV,CAAC"} \ No newline at end of file diff --git a/package.json b/package.json index d98ec59..5909984 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,38 @@ "language": "duckyscript", "path": "./snippets/snippets.json" } + ], + "configuration": { + "type": "object", + "title": "DuckyScript", + "properties": { + "duckyscript.customCommands": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "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": { diff --git a/src/extension.ts b/src/extension.ts index 0865492..7fc8535 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -54,6 +54,71 @@ const updatedCommandGroups = commandGroups.map(group => const ALL_COMMANDS = commandGroups.flat(); +const CUSTOM_COMMAND_TOKEN = 'duckyscriptCustomCommand'; + +const semanticTokenLegend = new vscode.SemanticTokensLegend([CUSTOM_COMMAND_TOKEN], []); + +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(); + this._configListener = 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', []) + .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 + ): vscode.ProviderResult { + 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 implements vscode.CompletionItemProvider { // This method is called when the user activates the suggestions (e.g., Ctrl+Space) @@ -98,6 +163,17 @@ export function activate(context: vscode.ExtensionContext) { ); 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() { } 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",