diff --git a/src/extension/src/adapters.ts b/src/extension/src/adapters.ts index e53837c0..41781da5 100644 --- a/src/extension/src/adapters.ts +++ b/src/extension/src/adapters.ts @@ -11,7 +11,9 @@ const DEFAULT_REFRESH_CONFIG: RefreshButtonConfig = { export type TerminalExecutor = ( command: string, useVsCodeApi?: boolean, - terminalName?: string + terminalName?: string, + buttonName?: string, + buttonRef?: object ) => void; export type ConfigReader = { diff --git a/src/extension/src/command-executor.test.ts b/src/extension/src/command-executor.test.ts index f7c8f42a..2de4126c 100644 --- a/src/extension/src/command-executor.test.ts +++ b/src/extension/src/command-executor.test.ts @@ -498,7 +498,7 @@ describe("command-executor", () => { executeTerminalCommand(button, mockTerminalExecutor); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", false, undefined); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", false, undefined, "Test Button", expect.objectContaining({ command: "echo test", name: "Test Button" })); }); it("should call terminalExecutor with useVsCodeApi true", () => { @@ -511,7 +511,7 @@ describe("command-executor", () => { executeTerminalCommand(button, mockTerminalExecutor); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", true, undefined); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", true, undefined, "Test Button", expect.objectContaining({ command: "echo test", name: "Test Button", useVsCodeApi: true })); }); it("should call terminalExecutor with custom terminal name", () => { @@ -524,7 +524,7 @@ describe("command-executor", () => { executeTerminalCommand(button, mockTerminalExecutor); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", false, "Custom Terminal"); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", false, "Custom Terminal", "Test Button", expect.objectContaining({ command: "echo test", name: "Test Button", terminalName: "Custom Terminal" })); }); it("should call terminalExecutor with all parameters", () => { @@ -538,7 +538,7 @@ describe("command-executor", () => { executeTerminalCommand(button, mockTerminalExecutor); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", true, "Custom Terminal"); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo test", true, "Custom Terminal", "Test Button", expect.objectContaining({ command: "echo test", name: "Test Button", terminalName: "Custom Terminal", useVsCodeApi: true })); }); it("should not call terminalExecutor when command is undefined", () => { @@ -588,13 +588,14 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(3); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo test1", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo test2", true, undefined); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo test1", false, undefined, "Command 1[0]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo test2", true, undefined, "Command 2[1]"); expect(mockTerminalExecutor).toHaveBeenNthCalledWith( 3, "echo test3", false, - "Custom Terminal" + "Custom Terminal", + "Command 3[2]" ); }); @@ -621,8 +622,8 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(2); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo child1", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo child2", true, undefined); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo child1", false, undefined, "Group Command[0]>Child 1[0]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo child2", true, undefined, "Group Command[0]>Child 2[1]"); }); it("should not execute commands for buttons with groups but no executeAll flag", () => { @@ -673,8 +674,8 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(2); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo level3", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo level2", false, undefined); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo level3", false, undefined, "Level 1 Group[0]>Level 2 Group[0]>Level 3 Command[0]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo level2", false, undefined, "Level 1 Group[0]>Level 2 Command[1]"); }); it("should skip buttons without commands and without groups", () => { @@ -692,7 +693,7 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(1); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo valid", false, undefined); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo valid", false, undefined, "Valid Command[0]"); }); it("should skip buttons with empty command strings", () => { @@ -711,7 +712,7 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(1); - expect(mockTerminalExecutor).toHaveBeenCalledWith("echo valid", false, undefined); + expect(mockTerminalExecutor).toHaveBeenCalledWith("echo valid", false, undefined, "Valid Command[0]"); }); it("should handle empty commands array", () => { @@ -758,8 +759,8 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(2); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo regular", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo child", false, undefined); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo regular", false, undefined, "Regular Command[0]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo child", false, undefined, "Group with executeAll[1]>Child Command[0]"); }); it("should handle complex nested structure with mixed executeAll flags", () => { @@ -804,9 +805,9 @@ describe("command-executor", () => { executeCommandsRecursively(commands, mockTerminalExecutor); expect(mockTerminalExecutor).toHaveBeenCalledTimes(3); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo leaf1", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo leaf2", false, undefined); - expect(mockTerminalExecutor).toHaveBeenNthCalledWith(3, "echo direct", false, undefined); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(1, "echo leaf1", false, undefined, "Root Group[0]>Branch 1[0]>Leaf 1[0]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(2, "echo leaf2", false, undefined, "Root Group[0]>Branch 1[0]>Leaf 2[1]"); + expect(mockTerminalExecutor).toHaveBeenNthCalledWith(3, "echo direct", false, undefined, "Root Group[0]>Direct Command[2]"); }); }); }); diff --git a/src/extension/src/command-executor.ts b/src/extension/src/command-executor.ts index 6492879c..1a3ffdc2 100644 --- a/src/extension/src/command-executor.ts +++ b/src/extension/src/command-executor.ts @@ -135,7 +135,13 @@ export const executeTerminalCommand = ( ) => { if (!button.command) return; - terminalExecutor(button.command, button.useVsCodeApi || false, button.terminalName); + terminalExecutor( + button.command, + button.useVsCodeApi || false, + button.terminalName, + button.name, + button + ); }; export const executeButtonCommand = ( @@ -187,13 +193,16 @@ const showGroupQuickPick = ( export const executeCommandsRecursively = ( commands: ButtonConfig[], - terminalExecutor: TerminalExecutor + terminalExecutor: TerminalExecutor, + parentPath = "" ): void => { - commands.forEach((cmd) => { + commands.forEach((cmd, index) => { + const buttonId = parentPath ? `${parentPath}>${cmd.name}[${index}]` : `${cmd.name}[${index}]`; + if (cmd.group && cmd.executeAll) { - executeCommandsRecursively(cmd.group, terminalExecutor); + executeCommandsRecursively(cmd.group, terminalExecutor, buttonId); } else if (cmd.command) { - terminalExecutor(cmd.command, cmd.useVsCodeApi || false, cmd.terminalName); + terminalExecutor(cmd.command, cmd.useVsCodeApi || false, cmd.terminalName, buttonId); } }); }; @@ -201,5 +210,5 @@ export const executeCommandsRecursively = ( const executeAllCommands = (button: ButtonConfig, terminalExecutor: TerminalExecutor) => { if (!button.group) return; - executeCommandsRecursively(button.group, terminalExecutor); + executeCommandsRecursively(button.group, terminalExecutor, button.name); }; diff --git a/src/extension/src/command-tree-provider.ts b/src/extension/src/command-tree-provider.ts index 0d691ba5..0af2823a 100644 --- a/src/extension/src/command-tree-provider.ts +++ b/src/extension/src/command-tree-provider.ts @@ -3,6 +3,7 @@ import { ConfigReader, TerminalExecutor } from "./adapters"; import { ButtonConfig } from "./types"; export class CommandTreeItem extends vscode.TreeItem { + public readonly buttonName: string; public readonly commandString: string; public readonly terminalName?: string; public readonly useVsCodeApi: boolean; @@ -11,12 +12,14 @@ export class CommandTreeItem extends vscode.TreeItem { label: string, commandString: string, useVsCodeApi: boolean = false, - terminalName?: string + terminalName?: string, + buttonName?: string ) { super(label, vscode.TreeItemCollapsibleState.None); this.commandString = commandString; this.useVsCodeApi = useVsCodeApi; this.terminalName = terminalName; + this.buttonName = buttonName || label; this.tooltip = commandString; this.contextValue = "command"; this.command = { @@ -38,8 +41,10 @@ export class GroupTreeItem extends vscode.TreeItem { type TreeItem = CommandTreeItem | GroupTreeItem; -export const createTreeItemsFromGroup = (commands: ButtonConfig[]): TreeItem[] => { - return commands.map((cmd) => { +export const createTreeItemsFromGroup = (commands: ButtonConfig[], parentPath = ""): TreeItem[] => { + return commands.map((cmd, index) => { + const buttonId = parentPath ? `${parentPath}>${cmd.name}[${index}]` : `${cmd.name}[${index}]`; + if (cmd.group) { return new GroupTreeItem(cmd.name, cmd.group); } @@ -48,13 +53,16 @@ export const createTreeItemsFromGroup = (commands: ButtonConfig[]): TreeItem[] = cmd.name, cmd.command || "", cmd.useVsCodeApi || false, - cmd.terminalName + cmd.terminalName, + buttonId ); }); }; export const createRootTreeItems = (buttons: ButtonConfig[]): TreeItem[] => { - return buttons.map((button) => { + return buttons.map((button, index) => { + const buttonId = `${button.name}[${index}]`; + if (button.group) { return new GroupTreeItem(button.name, button.group); } @@ -64,11 +72,12 @@ export const createRootTreeItems = (buttons: ButtonConfig[]): TreeItem[] => { button.name, button.command, button.useVsCodeApi || false, - button.terminalName + button.terminalName, + buttonId ); } - return new CommandTreeItem(button.name, "", false); + return new CommandTreeItem(button.name, "", false, undefined, buttonId); }); }; @@ -82,7 +91,7 @@ export class CommandTreeProvider implements vscode.TreeDataProvider { new CommandTreeProvider(configReader); static executeFromTree = (item: CommandTreeItem, terminalExecutor: TerminalExecutor) => { - terminalExecutor(item.commandString, item.useVsCodeApi, item.terminalName); + terminalExecutor(item.commandString, item.useVsCodeApi, item.terminalName, item.buttonName); }; getChildren = (element?: TreeItem): Thenable => { @@ -91,7 +100,7 @@ export class CommandTreeProvider implements vscode.TreeDataProvider { } if (element instanceof GroupTreeItem) { - return Promise.resolve(createTreeItemsFromGroup(element.commands)); + return Promise.resolve(createTreeItemsFromGroup(element.commands, element.label)); } return Promise.resolve([]); diff --git a/src/extension/src/terminal-manager.test.ts b/src/extension/src/terminal-manager.test.ts index b9235f75..572ccc79 100644 --- a/src/extension/src/terminal-manager.test.ts +++ b/src/extension/src/terminal-manager.test.ts @@ -78,36 +78,43 @@ describe("terminal-manager", () => { jest.restoreAllMocks(); }); - it("should create separate terminals when customTerminalName is set", () => { - manager.executeCommand("npm start", false, "build"); - manager.executeCommand("npm test", false, "build"); + it("should create separate terminals for different buttonNames with same command", () => { + manager.executeCommand("npm start", false, "build", "Button A"); + manager.executeCommand("npm start", false, "build", "Button B"); expect(vscode.window.createTerminal).toHaveBeenCalledTimes(2); expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(1, "build"); expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(2, "build"); }); - it("should create new terminal every time when customTerminalName is set", () => { - manager.executeCommand("npm start", false, "build"); - manager.executeCommand("npm start", false, "build"); + it("should reuse terminal for same button configuration", () => { + manager.executeCommand("npm start", false, "build", "Button A"); + manager.executeCommand("npm start", false, "build", "Button A"); - expect(vscode.window.createTerminal).toHaveBeenCalledTimes(2); + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1); }); - it("should reuse terminal for same command without customTerminalName", () => { - manager.executeCommand("npm start", false); - manager.executeCommand("npm start", false); + it("should create separate terminals for same command with different terminalNames", () => { + manager.executeCommand("just test", false, "", "just test"); + manager.executeCommand("just test", false, undefined, "just test"); - expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1); + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(2); + expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(1, "just"); + expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(2, "just"); }); - it("should create separate terminals for different commands without customTerminalName", () => { - manager.executeCommand("npm start", false); - manager.executeCommand("npm test", false); + it("should create separate terminals for executeAll group with same command", () => { + manager.executeCommand("just test", false, "", "just test 1"); + manager.executeCommand("just test", false, undefined, "just test 2"); expect(vscode.window.createTerminal).toHaveBeenCalledTimes(2); - expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(1, "npm"); - expect(vscode.window.createTerminal).toHaveBeenNthCalledWith(2, "npm"); + }); + + it("should reuse terminal when same button is clicked again", () => { + manager.executeCommand("npm test", false, undefined, "Test Button"); + manager.executeCommand("npm test", false, undefined, "Test Button"); + + expect(vscode.window.createTerminal).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/extension/src/terminal-manager.ts b/src/extension/src/terminal-manager.ts index da99a185..505270ab 100644 --- a/src/extension/src/terminal-manager.ts +++ b/src/extension/src/terminal-manager.ts @@ -13,6 +13,8 @@ export const determineTerminalName = ( }; export class TerminalManager { + private buttonIds = new WeakMap(); + private idCounter = 0; private terminals = new Map(); static create = (): TerminalManager => new TerminalManager(); @@ -24,29 +26,48 @@ export class TerminalManager { this.terminals.clear(); }; - executeCommand: TerminalExecutor = (command, useVsCodeApi = false, customTerminalName) => { + executeCommand: TerminalExecutor = ( + command, + useVsCodeApi = false, + customTerminalName, + buttonName, + buttonRef + ) => { if (useVsCodeApi) { vscode.commands.executeCommand(command); return; } const terminalName = determineTerminalName(customTerminalName, command); + const uniqueId = this.getUniqueButtonId(buttonRef, buttonName); + const terminalKey = JSON.stringify({ + command, + name: uniqueId, + terminalName: customTerminalName, + useVsCodeApi, + }); - if (customTerminalName) { - const terminal = vscode.window.createTerminal(terminalName); - terminal.show(); - terminal.sendText(command); - return; - } - - let terminal = this.terminals.get(command); + let terminal = this.terminals.get(terminalKey); if (shouldCreateNewTerminal(terminal)) { terminal = vscode.window.createTerminal(terminalName); - this.terminals.set(command, terminal); + this.terminals.set(terminalKey, terminal); } terminal!.show(); terminal!.sendText(command); }; + + private getUniqueButtonId(buttonRef?: object, buttonName?: string): string { + if (buttonRef) { + let id = this.buttonIds.get(buttonRef); + if (!id) { + id = `btn-${this.idCounter++}`; + this.buttonIds.set(buttonRef, id); + } + return id; + } + + return buttonName ?? `temp-${this.idCounter++}`; + } }