From 31a2aa5e24921fda1804773dc130044feacaade1 Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Fri, 27 Dec 2024 22:07:31 -0800 Subject: [PATCH 1/6] fix bug where quick pick always returns empty string --- ReleaseNotes.md | 4 ++++ package-lock.json | 4 ++-- package.json | 4 ++-- src/controllers/user-commands-controller.ts | 6 ++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 40682eb..6df5120 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -60,3 +60,7 @@ Add capability to run and debug from main functions for languages that have supp Swap typescript in for awk when available targets are fetched. Awk, albeit much faster, is not as portable as the typescript solution. Remove shellscript from C++ language support. Clear the workspace state only on major version changes. + +## 1.0.5 + +Fix bug where no output from quickpik is returned. diff --git a/package-lock.json b/package-lock.json index f226a73..1e354a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bluebazel", - "version": "0.0.8", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bluebazel", - "version": "0.0.8", + "version": "1.0.5", "dependencies": { "@types/tmp": "^0.2.0", "atob": "^2.1.2", diff --git a/package.json b/package.json index f43d9ee..231daec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bluebazel", "displayName": "Blue Bazel", "description": "Bazel vscode UI integration to build, debug, and test targets", - "version": "1.0.4", + "version": "1.0.5", "engines": { "vscode": "^1.85.2" }, @@ -498,4 +498,4 @@ "repository": { "url": "https://github.com/NVIDIA/bluebazel" } -} \ No newline at end of file +} diff --git a/src/controllers/user-commands-controller.ts b/src/controllers/user-commands-controller.ts index 46fcff5..0d76bbf 100644 --- a/src/controllers/user-commands-controller.ts +++ b/src/controllers/user-commands-controller.ts @@ -129,11 +129,9 @@ export class UserCommandsController { if (elementTrimmed.length > 0) outputList.push(elementTrimmed); } - await vscode.window.showQuickPick(outputList, { 'ignoreFocusOut': true }).then(data => { - if (data !== undefined) - return data; + return vscode.window.showQuickPick(outputList, { 'ignoreFocusOut': true }).then(data => { + return data !== undefined ? data : '' }); - return ''; } catch (error) { return Promise.reject(error); } From f5eecd05168ab3f81c97b3f531e3b2f7cb3d2029 Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Sun, 29 Dec 2024 11:18:47 -0800 Subject: [PATCH 2/6] support MultiPick extension document py_test usage --- README.md | 37 ++++++++++++++++++- ReleaseNotes.md | 1 + src/controllers/user-commands-controller.ts | 41 +++++++++++++++------ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d161211..c2e6ccd 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ Here are additional keywords to receive user input at the time of the execution: 1. [Pick(`arg`)]: This shows an item list for the user to choose one from. `arg` must be a command that returns multiline string where each line corresponds to an item. 2. [Input()]: This receives a plain string input from the user. +3. [MultiPick(`arg`)]: Similar to `Pick`, this shows an item list for the user to choose one or more from. Where as `Pick` returns a single selection, `MultiPick` returns all selections, one per line (output is one selection per line, separated by a "\n"). ### A complete example @@ -206,6 +207,18 @@ This example illustrates the `Test` button: "methodName": "bluebazel.test" } ] + }, + { + "title": "Run PyTest", + "buttons": [ + { + "title": "Test PyTest", + "command": "bazel test --build_tests_only --test_timeout=1500 --test_arg=\"--no-cov\" ", + "description": "Select and run specific test case(s)", + "tooltip": "Select and run specific test case(s) with `bazel test`", + "methodName": "bluebazel.runPyTestCase" + } + ] } ], "bluebazel.shellCommands": [ @@ -216,6 +229,18 @@ This example illustrates the `Test` button: { "name": "testTarget", "command": "bash -c 'source scripts/envsetup.sh > /dev/null && bazel query \"tests()\"" + }, + { + "name": "testTargetAsLabel", + "command": "echo -n ${bluebazel.testTarget} | perl -pe 's|^bazel-bin(/.*?)/([^/]+)$|/$1:$2|'" + }, + { + "name": "testCasePyTestListHelper", + "command": "bazel run --ui_event_filters=-info,-stdout,-stderr --noshow_progress -- --collect-only -qq --no-cov --disable-warnings --color=no | grep -E '.*/.+::.+'" + }, + { + "name": "testCaseTestArgList", + "command": "echo -n \"[MultiPick()]\" | awk '{print \"--test_arg=\\\"\"$1\"\\\"\"}' | tr \"\n\" \" \"" } ] ``` @@ -225,9 +250,19 @@ modifies it to return something in the form of `//path/...`. `testTarget` gives this input to `bazel query` to return all available tests in this path. -Finally, the button `Test` uses the output of `testTarget` to display the user the list of tests to choose from, +`testTargetAsLabel` formats the selected test target bazel path as a label. + +`testCaseTestArgList` takes a `MultiSelect` output and formats it as a series of `--test-arg` options +passed to the Bazel test command. + +The button `Test` uses the output of `testTarget` to display the user the list of tests to choose from, and executes the test using the current configs and run arguments. +Finally, the button `Test PyTest` uses the output of `testCaseTestArgList` to display the user the +list of PyTest-managed Python test cases corresponding to the current `testTarget`. `testTarget` +is assumed to support both the Bazel test and run execution phases and that it conforms to the PyTest +command line program interface. + ## Releases See [Release Notes](ReleaseNotes.md) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 6df5120..57d3ba0 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -64,3 +64,4 @@ Clear the workspace state only on major version changes. ## 1.0.5 Fix bug where no output from quickpik is returned. +Support MultiPick quickpik extension. diff --git a/src/controllers/user-commands-controller.ts b/src/controllers/user-commands-controller.ts index 0d76bbf..2effa34 100644 --- a/src/controllers/user-commands-controller.ts +++ b/src/controllers/user-commands-controller.ts @@ -55,6 +55,7 @@ export class UserCommandsController { }; private static EXTENSION_COMMANDS = { + multipick: 'MultiPick', pick: 'Pick', input: 'Input' }; @@ -118,18 +119,21 @@ export class UserCommandsController { } + private async buildPickList(input: string): Promise { + // Evaluate the inner command of the pick + const output = await this.resolveCommands(input); + // Make a list of the output + const outputList = []; + for (const element of output.split('\n')) { + const elementTrimmed = element.trim(); + if (elementTrimmed.length > 0) outputList.push(elementTrimmed); + } + return outputList; + } + private async extPick(input: string): Promise { try { - // Evaluate the inner command of the pick - const output = await this.resolveCommands(input); - // Make a list of the output - const outputList = []; - for (const element of output.split('\n')) { - const elementTrimmed = element.trim(); - if (elementTrimmed.length > 0) outputList.push(elementTrimmed); - } - - return vscode.window.showQuickPick(outputList, { 'ignoreFocusOut': true }).then(data => { + return vscode.window.showQuickPick(this.buildPickList(input), { 'ignoreFocusOut': true }).then(data => { return data !== undefined ? data : '' }); } catch (error) { @@ -137,6 +141,16 @@ export class UserCommandsController { } } + private async extPickMany(input: string): Promise { + try { + return vscode.window.showQuickPick(this.buildPickList(input), { 'ignoreFocusOut': true, 'canPickMany': true }).then(data => { + return data !== undefined ? data : [] + });; + } catch (error) { + return Promise.reject(error); + } + } + private async resolveExtensionCommands(input: string): Promise { // Execute commands let output = input; @@ -149,7 +163,10 @@ export class UserCommandsController { const extCommand = match[1]; const extArgs = match[2]; let evalRes = ''; - if (extCommand === UserCommandsController.EXTENSION_COMMANDS.pick) { + if (extCommand === UserCommandsController.EXTENSION_COMMANDS.multipick) { + const multiRes = await this.extPickMany(extArgs); + evalRes = multiRes.map((el) => el.replace(/[\r\n]/g, " ")).join("\n"); + } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.pick) { evalRes = await this.extPick(extArgs); } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.input) { await vscode.window.showInputBox( @@ -221,4 +238,4 @@ export class UserCommandsController { } return res; } -} \ No newline at end of file +} From 21c7ee89c6a566eef2e0f9b065699bb3766e45d8 Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Sun, 29 Dec 2024 17:40:49 -0800 Subject: [PATCH 3/6] enable interpolate shell command into input --- src/controllers/user-commands-controller.ts | 35 +++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/controllers/user-commands-controller.ts b/src/controllers/user-commands-controller.ts index 2effa34..977940f 100644 --- a/src/controllers/user-commands-controller.ts +++ b/src/controllers/user-commands-controller.ts @@ -133,7 +133,10 @@ export class UserCommandsController { private async extPick(input: string): Promise { try { - return vscode.window.showQuickPick(this.buildPickList(input), { 'ignoreFocusOut': true }).then(data => { + return vscode.window.showQuickPick( + this.buildPickList(input), + { 'ignoreFocusOut': true } + ).then((data) => { return data !== undefined ? data : '' }); } catch (error) { @@ -143,7 +146,10 @@ export class UserCommandsController { private async extPickMany(input: string): Promise { try { - return vscode.window.showQuickPick(this.buildPickList(input), { 'ignoreFocusOut': true, 'canPickMany': true }).then(data => { + return vscode.window.showQuickPick( + this.buildPickList(input), + { 'ignoreFocusOut': true, 'canPickMany': true } + ).then((data) => { return data !== undefined ? data : [] });; } catch (error) { @@ -151,6 +157,23 @@ export class UserCommandsController { } } + private async extInput(input: string): Promise { + try { + let resolvedInput = await this.resolveCommands(input); + if (resolvedInput !== undefined) { + // we take the first result if this is a multi-line string + resolvedInput = resolvedInput.split('\n')[0]; + } + return vscode.window.showInputBox( + { value: resolvedInput } + ).then((val) => { + return val !== undefined ? val : '' + }); + } catch (error) { + return Promise.reject(error); + } + } + private async resolveExtensionCommands(input: string): Promise { // Execute commands let output = input; @@ -169,13 +192,7 @@ export class UserCommandsController { } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.pick) { evalRes = await this.extPick(extArgs); } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.input) { - await vscode.window.showInputBox( - { value: extArgs } - ).then((val) => { - if (val !== undefined) { - evalRes = val; - } - }); + evalRes = await this.extInput(extArgs); } output = output.replace(match[0], evalRes); } From 52818b480e82e712ab3aea9455ded0a26cd105f5 Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Sun, 29 Dec 2024 21:11:26 -0800 Subject: [PATCH 4/6] support memoize shell command outputs refactor to extract resolver class to support command-scoped cache fix documentation about pytest usage in README --- README.md | 36 ++- ReleaseNotes.md | 1 + src/controllers/user-commands-controller.ts | 313 ++++++++++---------- src/services/configuration-manager.ts | 33 ++- 4 files changed, 217 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index c2e6ccd..99935ec 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ The configuration has the following syntax: "bluebazel.shellCommands": [ { "name": "myCommand1", - "command": "A shell command" + "command": "A shell command", + "memoized": false } ] ``` @@ -100,15 +101,23 @@ Example: "bluebazel.shellCommands": [ { "name": "myEcho", - "command": "echo this is my custom shell command" + "command": "echo this is my custom shell command", + "memoized": true, }, { "name": "myEchoEcho", "command": "echo " // This will execute the command `myEcho` and echo the result. + }, + { + "name": "myDecoaratedEcho", + "command": "echo \"Echo was: \"" } ] ``` +In the above example, the command `myEcho` will only be evaluated once, then memoized and reused in both +`myEchoMyEcho` and `myDecoratedEcho`. + ### Custom Button configuration Custom buttons configuration allows the user to add sections and buttons to the bazel view container, and link @@ -212,8 +221,8 @@ This example illustrates the `Test` button: "title": "Run PyTest", "buttons": [ { - "title": "Test PyTest", - "command": "bazel test --build_tests_only --test_timeout=1500 --test_arg=\"--no-cov\" ", + "title": "Run Selected Test Cases", + "command": "bazel test --build_tests_only --test_timeout=1500 ", "description": "Select and run specific test case(s)", "tooltip": "Select and run specific test case(s) with `bazel test`", "methodName": "bluebazel.runPyTestCase" @@ -235,12 +244,21 @@ This example illustrates the `Test` button: "command": "echo -n ${bluebazel.testTarget} | perl -pe 's|^bazel-bin(/.*?)/([^/]+)$|/$1:$2|'" }, { - "name": "testCasePyTestListHelper", - "command": "bazel run --ui_event_filters=-info,-stdout,-stderr --noshow_progress -- --collect-only -qq --no-cov --disable-warnings --color=no | grep -E '.*/.+::.+'" + "name": "confirmedTestTargetAsLabel", + "command": "echo -n \"[Input()]\"", + "memoized": true + }, + { + "name": "testCaseListHelper", + "command": "bazel run --ui_event_filters=-info,-stdout,-stderr --noshow_progress -- --collect-only -qq --no-cov --disable-warnings --color=no | grep -E '(.*?/).+::.+'", + }, + { + "name": "confirmExtraTestArgs", + "command": "echo -n \"[Input(--test_arg=\\\"--no-cov\\\")]\"", }, { "name": "testCaseTestArgList", - "command": "echo -n \"[MultiPick()]\" | awk '{print \"--test_arg=\\\"\"$1\"\\\"\"}' | tr \"\n\" \" \"" + "command": "echo -n \"[MultiPick()]\" | awk '{print \"--test_arg=\\\"\"$1\"\\\"\"}' | tr \"\n\" \" \"", } ] ``` @@ -252,13 +270,13 @@ modifies it to return something in the form of `//path/...`. `testTargetAsLabel` formats the selected test target bazel path as a label. -`testCaseTestArgList` takes a `MultiSelect` output and formats it as a series of `--test-arg` options +`testCaseTestArgList` takes a `MultiSelect` output and formats it as a series of `--test_arg` options passed to the Bazel test command. The button `Test` uses the output of `testTarget` to display the user the list of tests to choose from, and executes the test using the current configs and run arguments. -Finally, the button `Test PyTest` uses the output of `testCaseTestArgList` to display the user the +Finally, the button `Run Selected Test Cases` uses the output of `testCaseTestArgList` to display the user the list of PyTest-managed Python test cases corresponding to the current `testTarget`. `testTarget` is assumed to support both the Bazel test and run execution phases and that it conforms to the PyTest command line program interface. diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 57d3ba0..c7a7921 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -65,3 +65,4 @@ Clear the workspace state only on major version changes. Fix bug where no output from quickpik is returned. Support MultiPick quickpik extension. +Support memoizing shellCommand outputs for repeated use in a button command. diff --git a/src/controllers/user-commands-controller.ts b/src/controllers/user-commands-controller.ts index 977940f..6d238b3 100644 --- a/src/controllers/user-commands-controller.ts +++ b/src/controllers/user-commands-controller.ts @@ -23,7 +23,7 @@ //////////////////////////////////////////////////////////////////////////////////// import { BazelTargetManager } from '../models/bazel-target-manager'; import { BazelService } from '../services/bazel-service'; -import { ConfigurationManager } from '../services/configuration-manager'; +import { ConfigurationManager, ShellCommand } from '../services/configuration-manager'; import { ShellService } from '../services/shell-service'; import { TaskService } from '../services/task-service'; import { showProgress } from '../ui/progress'; @@ -54,11 +54,11 @@ export class UserCommandsController { formatCommand: 'bluebazel.formatCommand' }; - private static EXTENSION_COMMANDS = { - multipick: 'MultiPick', - pick: 'Pick', - input: 'Input' - }; + private static EXTENSION_COMMANDS: Map string> = new Map([ + [ 'MultiPick', (resolver, extArgs) => resolver.extPickMany(extArgs) ], + [ 'Pick', (resolver, extArgs) => resolver.extPick(extArgs) ], + [ 'Input', (resolver, extArgs) => resolver.extInput(extArgs) ] + ]); constructor( private readonly configurationManager: ConfigurationManager, @@ -68,14 +68,18 @@ export class UserCommandsController { ) { } public async runCustomTask(command: string): Promise { - let completeCommand = this.resolveKeywords(command); + const resolver = new this.ResolverContext(this); + let completeCommand = resolver.resolveKeywords(command); return showProgress(`Running ${completeCommand}`, async (cancellationToken) => { try { - completeCommand = await this.resolveExtensionCommands(completeCommand); - completeCommand = await this.resolveCommands(completeCommand); + completeCommand = await resolver.resolveExtensionCommands(completeCommand); + completeCommand = await resolver.resolveCommands(completeCommand); this.taskService.runTask(completeCommand, completeCommand, this.configurationManager.isClearTerminalBeforeAction(), cancellationToken); } catch (error) { vscode.window.showErrorMessage(`Error running custom task: ${error}`); + } finally { + + resolver.cache.clear(); } }); } @@ -87,172 +91,183 @@ export class UserCommandsController { return result; } - private resolveKeyword(keyword: string): string { - const buildTarget = this.bazelTargetManager.getSelectedTarget('build'); - const runTarget = this.bazelTargetManager.getSelectedTarget('run'); - const testTarget = this.bazelTargetManager.getSelectedTarget('test'); + private ResolverContext = class { + public cache: Map = new Map(); + constructor(private controller: UserCommandsController) { } - const keywordMap: Map string> = new Map([ - [UserCommandsController.CONFIG_KEYWORDS.runArgs, () => runTarget.getRunArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.testArgs, () => UserCommandsController.formatTestArgs(testTarget.getRunArgs().toString())], - [UserCommandsController.CONFIG_KEYWORDS.runTarget, () => { - return BazelService.formatBazelTargetFromPath(runTarget.buildPath); - }], - [UserCommandsController.CONFIG_KEYWORDS.testTarget, () => testTarget.buildPath], - [UserCommandsController.CONFIG_KEYWORDS.buildConfigs, () => buildTarget.getConfigArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.runConfigs, () => runTarget.getConfigArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.testConfigs, () => testTarget.getConfigArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.bazelBuildArgs, () => buildTarget.getBazelArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.bazelRunArgs, () => runTarget.getBazelArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.bazelTestArgs, () => testTarget.getBazelArgs().toString()], - [UserCommandsController.CONFIG_KEYWORDS.buildEnvVars, () => buildTarget.getEnvVars().toStringArray().join(' ')], - [UserCommandsController.CONFIG_KEYWORDS.runEnvVars, () => runTarget.getEnvVars().toStringArray().join(' ')], - [UserCommandsController.CONFIG_KEYWORDS.testEnvVars, () => buildTarget.getEnvVars().toStringArray().join(' ')], - [UserCommandsController.CONFIG_KEYWORDS.buildTarget, () => testTarget.buildPath], - [UserCommandsController.CONFIG_KEYWORDS.executable, () => this.configurationManager.getExecutableCommand()], - [UserCommandsController.CONFIG_KEYWORDS.formatCommand, () => this.configurationManager.getFormatCommand()], - ]); + protected resolveKeyword(keyword: string): string { + const buildTarget = this.controller.bazelTargetManager.getSelectedTarget('build'); + const runTarget = this.controller.bazelTargetManager.getSelectedTarget('run'); + const testTarget = this.controller.bazelTargetManager.getSelectedTarget('test'); - const getValue = keywordMap.get(keyword); - return getValue ? getValue() : `\${${keyword}}`; + const keywordMap: Map string> = new Map([ + [UserCommandsController.CONFIG_KEYWORDS.runArgs, () => runTarget.getRunArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.testArgs, () => UserCommandsController.formatTestArgs(testTarget.getRunArgs().toString())], + [UserCommandsController.CONFIG_KEYWORDS.runTarget, () => { + return BazelService.formatBazelTargetFromPath(runTarget.buildPath); + }], + [UserCommandsController.CONFIG_KEYWORDS.testTarget, () => testTarget.buildPath], + [UserCommandsController.CONFIG_KEYWORDS.buildConfigs, () => buildTarget.getConfigArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.runConfigs, () => runTarget.getConfigArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.testConfigs, () => testTarget.getConfigArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.bazelBuildArgs, () => buildTarget.getBazelArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.bazelRunArgs, () => runTarget.getBazelArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.bazelTestArgs, () => testTarget.getBazelArgs().toString()], + [UserCommandsController.CONFIG_KEYWORDS.buildEnvVars, () => buildTarget.getEnvVars().toStringArray().join(' ')], + [UserCommandsController.CONFIG_KEYWORDS.runEnvVars, () => runTarget.getEnvVars().toStringArray().join(' ')], + [UserCommandsController.CONFIG_KEYWORDS.testEnvVars, () => buildTarget.getEnvVars().toStringArray().join(' ')], + [UserCommandsController.CONFIG_KEYWORDS.buildTarget, () => testTarget.buildPath], + [UserCommandsController.CONFIG_KEYWORDS.executable, () => this.controller.configurationManager.getExecutableCommand()], + [UserCommandsController.CONFIG_KEYWORDS.formatCommand, () => this.controller.configurationManager.getFormatCommand()], + ]); - } + const getValue = keywordMap.get(keyword); + return getValue ? getValue() : `\${${keyword}}`; + } - private async buildPickList(input: string): Promise { - // Evaluate the inner command of the pick - const output = await this.resolveCommands(input); - // Make a list of the output - const outputList = []; - for (const element of output.split('\n')) { - const elementTrimmed = element.trim(); - if (elementTrimmed.length > 0) outputList.push(elementTrimmed); + private async buildPickList(input: string): Promise { + // Evaluate the inner command of the pick + const output = await this.resolveCommands(input); + // Make a list of the output + const outputList = []; + for (const element of output.split('\n')) { + const elementTrimmed = element.trim(); + if (elementTrimmed.length > 0) outputList.push(elementTrimmed); + } + return outputList; } - return outputList; - } - private async extPick(input: string): Promise { - try { - return vscode.window.showQuickPick( - this.buildPickList(input), - { 'ignoreFocusOut': true } - ).then((data) => { - return data !== undefined ? data : '' - }); - } catch (error) { - return Promise.reject(error); + private async extPick(input: string): Promise { + try { + return vscode.window.showQuickPick( + this.buildPickList(input), + { 'ignoreFocusOut': true } + ).then((data) => { + return data !== undefined ? data : '' + }); + } catch (error) { + return Promise.reject(error); + } } - } - private async extPickMany(input: string): Promise { - try { - return vscode.window.showQuickPick( - this.buildPickList(input), - { 'ignoreFocusOut': true, 'canPickMany': true } - ).then((data) => { - return data !== undefined ? data : [] - });; - } catch (error) { - return Promise.reject(error); + private async extPickMany(input: string): Promise { + try { + return vscode.window.showQuickPick( + this.buildPickList(input), + { 'ignoreFocusOut': true, 'canPickMany': true } + ).then((data) => { + return data !== undefined ? data : [] + }).then((data) => { + data = data.map((item) => item.replace(/\n/g, "\\n")); + return data.join("\n"); + }); + } catch (error) { + return Promise.reject(error); + } } - } - private async extInput(input: string): Promise { - try { - let resolvedInput = await this.resolveCommands(input); - if (resolvedInput !== undefined) { - // we take the first result if this is a multi-line string - resolvedInput = resolvedInput.split('\n')[0]; + private async extInput(input: string): Promise { + try { + let resolvedInput = await this.resolveCommands(input); + if (resolvedInput !== undefined) { + // we take the first result if this is a multi-line string + resolvedInput = resolvedInput.split('\n')[0]; + } + return vscode.window.showInputBox( + { value: resolvedInput } + ).then((val) => { + return val !== undefined ? val : '' + }); + } catch (error) { + return Promise.reject(error); } - return vscode.window.showInputBox( - { value: resolvedInput } - ).then((val) => { - return val !== undefined ? val : '' - }); - } catch (error) { - return Promise.reject(error); } - } - private async resolveExtensionCommands(input: string): Promise { - // Execute commands - let output = input; - const regexp = /\[([^\s]*)\(([^\s]*)\)\]/g; - let match; - try { + public async resolveExtensionCommands(input: string): Promise { + // Execute commands + let output = input; + const regexp = /\[([^\s]*)\(([^\s]*)\)\]/g; + let match; + try { + do { + match = regexp.exec(input); + if (match) { + const extCommand = match[1]; + const extArgs = match[2]; + let evalRes = ''; + const handler = UserCommandsController.EXTENSION_COMMANDS.get(extCommand); + if (handler !== undefined) { + evalRes = await handler(this, extArgs); + } + output = output.replace(match[0], evalRes); + } + } while (match); + } catch (error) { + return Promise.reject(error); + } + return output; + } + + public resolveKeywords(input: string): string { + let output = input; + // First replace keywords + const regexp = /\$\{([^\s]*)\}/g; + let match; do { match = regexp.exec(input); if (match) { - const extCommand = match[1]; - const extArgs = match[2]; - let evalRes = ''; - if (extCommand === UserCommandsController.EXTENSION_COMMANDS.multipick) { - const multiRes = await this.extPickMany(extArgs); - evalRes = multiRes.map((el) => el.replace(/[\r\n]/g, " ")).join("\n"); - } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.pick) { - evalRes = await this.extPick(extArgs); - } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.input) { - evalRes = await this.extInput(extArgs); - } - output = output.replace(match[0], evalRes); + output = output.replace(match[0], this.resolveKeyword(match[1])); } } while (match); - } catch (error) { - return Promise.reject(error); + return output; } - return output; - } - private resolveKeywords(input: string): string { - let output = input; - // First replace keywords - const regexp = /\$\{([^\s]*)\}/g; - let match; - do { - match = regexp.exec(input); - if (match) { - output = output.replace(match[0], this.resolveKeyword(match[1])); - } - } while (match); - return output; - } + private findCommandByKeyword(keyword: string): ShellCommand | undefined { + const commands = this.controller.configurationManager.getShellCommands(); + return commands.find((item) => item.name == keyword); + } - private async resolveCommands(input: string): Promise { - // Execute commands - let output = input; - const regexp = /<([^\s]*)>/g; - let match; - do { - match = regexp.exec(input); - if (match) { - try { - const currentCommand = await this.resolveCommandByKeyword(match[1]); - const evalRes = await this.shellService.runShellCommand(currentCommand); + public async resolveCommands(input: string): Promise { + // Execute commands + let output = input; + const regexp = /<([^\s]*)>/g; + let match; + do { + match = regexp.exec(input); + if (match) { + try { + const cmd = this.findCommandByKeyword(match[1]); + let evalRes = ''; + if (cmd !== undefined) { + if (cmd.memoized && this.cache.has(cmd.name)) { + evalRes = this.cache.get(cmd.name) ?? ''; + } else { + const resolvedCmd = await this.resolveCommand(cmd.command); + const cmdRes = await this.controller.shellService.runShellCommand(resolvedCmd); + evalRes = cmdRes.stdout; + this.cache.set(cmd.name, evalRes); + } + } - output = output.replace(match[0], evalRes.stdout); - } catch (error) { - return Promise.reject(error); + output = output.replace(match[0], evalRes); + } catch (error) { + return Promise.reject(error); + } } - } - } while (match); - return output; - } - - private async resolveCommandByKeyword(keyword: string): Promise { - const commands = this.configurationManager.getShellCommands(); - let res = ''; + } while (match); + return output; + } - for (const element of commands) { - if (keyword === element.name) { - res = this.resolveKeywords(element.command); - try { - res = await this.resolveExtensionCommands(res); - res = await this.resolveCommands(res); - } catch (error) { - return Promise.reject(error); - } + private async resolveCommand(command: string): Promise { + try { + let res = this.resolveKeywords(command); + res = await this.resolveExtensionCommands(res); + res = await this.resolveCommands(res); + return res; + } catch (error) { + return Promise.reject(error); } } - return res; } } diff --git a/src/services/configuration-manager.ts b/src/services/configuration-manager.ts index 401dc7c..0605ce2 100644 --- a/src/services/configuration-manager.ts +++ b/src/services/configuration-manager.ts @@ -85,9 +85,26 @@ export class UserCustomCategory { } } -export interface ShellCommand { - 'name': string, - 'command': string +export class ShellCommand { + name: string; + command: string; + memoized: boolean; + public readonly id: string; + + constructor(data: { 'name': string, 'command': string, 'memoized': boolean }) { + this.name = data.name; + this.command = data.command; + this.memoized = data.memoized ?? false; + this.id = this.name; + } + + toJSON(): { 'name': string, 'command': string, 'memoized': boolean } { + return { + 'name': this.name, + 'command': this.command, + 'memoized': this.memoized + }; + } } export class ConfigurationManager { @@ -121,11 +138,11 @@ export class ConfigurationManager { } public getShellCommands(): Array { - const result = this.getConfig().get>('shellCommands'); - if (result !== undefined) { - return result; + const shellCommands = this.getConfig().get>('shellCommands'); + if (shellCommands === undefined) { + return []; } - return []; + return shellCommands.map(commandData => new ShellCommand(commandData)); } @@ -246,4 +263,4 @@ export class ConfigurationManager { return res; } -} \ No newline at end of file +} From 23fd5dfa918f4add8ebb47bbdf9e73f5432d929a Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Tue, 31 Dec 2024 17:02:33 -0800 Subject: [PATCH 5/6] make multipick results persistent refactor to avoid trainwrecks fix variable name --- src/controllers/user-commands-controller.ts | 92 +++++++++++++-------- src/extension.ts | 2 +- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/controllers/user-commands-controller.ts b/src/controllers/user-commands-controller.ts index 6d238b3..82b88b3 100644 --- a/src/controllers/user-commands-controller.ts +++ b/src/controllers/user-commands-controller.ts @@ -22,6 +22,7 @@ // SOFTWARE. //////////////////////////////////////////////////////////////////////////////////// import { BazelTargetManager } from '../models/bazel-target-manager'; +import { WorkspaceStateManager } from '../models/workspace-state-manager'; import { BazelService } from '../services/bazel-service'; import { ConfigurationManager, ShellCommand } from '../services/configuration-manager'; import { ShellService } from '../services/shell-service'; @@ -33,6 +34,11 @@ import * as vscode from 'vscode'; /** * Controller class for executing user custom commands. */ + +type PickStateRecord = Record; + +type PickStateMap = Record; + export class UserCommandsController { private static CONFIG_KEYWORDS = { @@ -54,21 +60,26 @@ export class UserCommandsController { formatCommand: 'bluebazel.formatCommand' }; - private static EXTENSION_COMMANDS: Map string> = new Map([ - [ 'MultiPick', (resolver, extArgs) => resolver.extPickMany(extArgs) ], - [ 'Pick', (resolver, extArgs) => resolver.extPick(extArgs) ], - [ 'Input', (resolver, extArgs) => resolver.extInput(extArgs) ] - ]); + private static EXTENSION_COMMANDS = { + multipick: 'MultiPick', + pick: 'Pick', + input: 'Input' + }; constructor( private readonly configurationManager: ConfigurationManager, private readonly shellService: ShellService, // Inject the services private readonly taskService: TaskService, - private readonly bazelTargetManager: BazelTargetManager - ) { } + private readonly bazelTargetManager: BazelTargetManager, + private readonly workspaceStateManager: WorkspaceStateManager + ) { + } + + private static PICK_STATE_KEY: string = "userCommandsController.pickStateMap"; public async runCustomTask(command: string): Promise { - const resolver = new this.ResolverContext(this); + const pickStateMap: PickStateMap = this.workspaceStateManager.get(UserCommandsController.PICK_STATE_KEY, {}); + const resolver = new this.Resolver(this.bazelTargetManager, this.configurationManager, this.shellService, pickStateMap); let completeCommand = resolver.resolveKeywords(command); return showProgress(`Running ${completeCommand}`, async (cancellationToken) => { try { @@ -78,8 +89,7 @@ export class UserCommandsController { } catch (error) { vscode.window.showErrorMessage(`Error running custom task: ${error}`); } finally { - - resolver.cache.clear(); + this.workspaceStateManager.update(UserCommandsController.PICK_STATE_KEY, pickStateMap); } }); } @@ -91,15 +101,20 @@ export class UserCommandsController { return result; } - private ResolverContext = class { + private Resolver = class { public cache: Map = new Map(); - constructor(private controller: UserCommandsController) { } + constructor( + private bazelTargetManager: BazelTargetManager, + private configurationManager: ConfigurationManager, + private shellService: ShellService, + private pickStateMap: PickStateMap + ) { } protected resolveKeyword(keyword: string): string { - const buildTarget = this.controller.bazelTargetManager.getSelectedTarget('build'); - const runTarget = this.controller.bazelTargetManager.getSelectedTarget('run'); - const testTarget = this.controller.bazelTargetManager.getSelectedTarget('test'); + const buildTarget = this.bazelTargetManager.getSelectedTarget('build'); + const runTarget = this.bazelTargetManager.getSelectedTarget('run'); + const testTarget = this.bazelTargetManager.getSelectedTarget('test'); const keywordMap: Map string> = new Map([ [UserCommandsController.CONFIG_KEYWORDS.runArgs, () => runTarget.getRunArgs().toString()], @@ -118,50 +133,51 @@ export class UserCommandsController { [UserCommandsController.CONFIG_KEYWORDS.runEnvVars, () => runTarget.getEnvVars().toStringArray().join(' ')], [UserCommandsController.CONFIG_KEYWORDS.testEnvVars, () => buildTarget.getEnvVars().toStringArray().join(' ')], [UserCommandsController.CONFIG_KEYWORDS.buildTarget, () => testTarget.buildPath], - [UserCommandsController.CONFIG_KEYWORDS.executable, () => this.controller.configurationManager.getExecutableCommand()], - [UserCommandsController.CONFIG_KEYWORDS.formatCommand, () => this.controller.configurationManager.getFormatCommand()], + [UserCommandsController.CONFIG_KEYWORDS.executable, () => this.configurationManager.getExecutableCommand()], + [UserCommandsController.CONFIG_KEYWORDS.formatCommand, () => this.configurationManager.getFormatCommand()], ]); const getValue = keywordMap.get(keyword); return getValue ? getValue() : `\${${keyword}}`; } - private async buildPickList(input: string): Promise { + private async buildPickList(input: string, picker: (label: string) => boolean): Promise { // Evaluate the inner command of the pick const output = await this.resolveCommands(input); // Make a list of the output const outputList = []; for (const element of output.split('\n')) { - const elementTrimmed = element.trim(); - if (elementTrimmed.length > 0) outputList.push(elementTrimmed); + const label = element.trim(); + if (label.length > 0) { + outputList.push({ 'label': label, 'picked': picker(label) }); + } } return outputList; } - private async extPick(input: string): Promise { + private async extPick(input: vscode.QuickPickItem[]): Promise { try { return vscode.window.showQuickPick( - this.buildPickList(input), + input, { 'ignoreFocusOut': true } ).then((data) => { - return data !== undefined ? data : '' + return data !== undefined ? data.label : '' }); } catch (error) { return Promise.reject(error); } } - private async extPickMany(input: string): Promise { + private async extPickMany(input: vscode.QuickPickItem[]): Promise { try { return vscode.window.showQuickPick( - this.buildPickList(input), + input, { 'ignoreFocusOut': true, 'canPickMany': true } ).then((data) => { return data !== undefined ? data : [] }).then((data) => { - data = data.map((item) => item.replace(/\n/g, "\\n")); - return data.join("\n"); - }); + return data.map((item) => item.label); + }); } catch (error) { return Promise.reject(error); } @@ -196,9 +212,19 @@ export class UserCommandsController { const extCommand = match[1]; const extArgs = match[2]; let evalRes = ''; - const handler = UserCommandsController.EXTENSION_COMMANDS.get(extCommand); - if (handler !== undefined) { - evalRes = await handler(this, extArgs); + if (extCommand === UserCommandsController.EXTENSION_COMMANDS.multipick) { + let state = this.pickStateMap[output]; + const input = await this.buildPickList(extArgs, (label) => state[label] ?? false); + const labels = await this.extPickMany(input); + evalRes = labels.join("\n"); + state = {}; + labels.forEach((label) => state[label] = true); + this.pickStateMap[output] = state; + } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.pick) { + const input = await this.buildPickList(extArgs, (label) => false); + evalRes = await this.extPick(input); + } else if (extCommand === UserCommandsController.EXTENSION_COMMANDS.input) { + evalRes = await this.extInput(extArgs); } output = output.replace(match[0], evalRes); } @@ -224,7 +250,7 @@ export class UserCommandsController { } private findCommandByKeyword(keyword: string): ShellCommand | undefined { - const commands = this.controller.configurationManager.getShellCommands(); + const commands = this.configurationManager.getShellCommands(); return commands.find((item) => item.name == keyword); } @@ -244,7 +270,7 @@ export class UserCommandsController { evalRes = this.cache.get(cmd.name) ?? ''; } else { const resolvedCmd = await this.resolveCommand(cmd.command); - const cmdRes = await this.controller.shellService.runShellCommand(resolvedCmd); + const cmdRes = await this.shellService.runShellCommand(resolvedCmd); evalRes = cmdRes.stdout; this.cache.set(cmd.name, evalRes); } diff --git a/src/extension.ts b/src/extension.ts index 3204ed1..305e14c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -211,7 +211,7 @@ async function initExtension(context: vscode.ExtensionContext) { attachTreeDataProviderToView(context, bazelController, bazelTargetTreeProvider); // The user commands controller runs user dynamic tasks added through configuration settings. - userCommandsController = new UserCommandsController(configurationManager, shellService, taskService, bazelTargetManager); + userCommandsController = new UserCommandsController(configurationManager, shellService, taskService, bazelTargetManager, workspaceStateManager); // The workspace events controller monitors for workspace events and triggers appropriate logic // when those events fire. From 14961c6e2dbc2b6e8caae4dbaab64f6e9ef5e38b Mon Sep 17 00:00:00 2001 From: Charles Woerner Date: Mon, 30 Dec 2024 17:00:11 -0800 Subject: [PATCH 6/6] bump version to 1.1.0 --- ReleaseNotes.md | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c7a7921..548d4b6 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -64,5 +64,8 @@ Clear the workspace state only on major version changes. ## 1.0.5 Fix bug where no output from quickpik is returned. + +## 1.1.0 + Support MultiPick quickpik extension. Support memoizing shellCommand outputs for repeated use in a button command. diff --git a/package-lock.json b/package-lock.json index 1e354a9..b76d23a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bluebazel", - "version": "1.0.5", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bluebazel", - "version": "1.0.5", + "version": "1.1.0", "dependencies": { "@types/tmp": "^0.2.0", "atob": "^2.1.2", diff --git a/package.json b/package.json index 231daec..cb21780 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bluebazel", "displayName": "Blue Bazel", "description": "Bazel vscode UI integration to build, debug, and test targets", - "version": "1.0.5", + "version": "1.1.0", "engines": { "vscode": "^1.85.2" },