diff --git a/.gitignore b/.gitignore index 42ab88b..a639840 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .vscode-test/** node_modules mise.local.toml -dist/ \ No newline at end of file +dist/ +.DS_Store \ No newline at end of file diff --git a/package.json b/package.json index d2de143..fd844f7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "Mise VSCode", "publisher": "hverlin", "description": "VSCode extension for mise (manged dev tools, tasks and environment variables)", - "version": "0.0.2", + "version": "0.0.3", "repository": { "type": "git", "url": "https://github.com/hverlin/mise-vscode" @@ -59,6 +59,10 @@ "command": "mise.refreshEntry", "title": "Refresh", "icon": "$(refresh)" + }, + { + "command": "mise.runTask", + "title": "Run Mise Task" } ], "menus": { diff --git a/src/extension.ts b/src/extension.ts index 846045b..2baf0c7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,10 @@ import * as vscode from "vscode"; import { MiseService } from "./miseService"; import { MiseEnvsProvider } from "./providers/envProvider"; -import { MiseTasksProvider } from "./providers/tasksProvider"; +import { + MiseTasksProvider, + registerMiseCommands, +} from "./providers/tasksProvider"; import { MiseToolsProvider } from "./providers/toolsProvider"; let statusBarItem: vscode.StatusBarItem; @@ -20,6 +23,7 @@ export function activate(context: vscode.ExtensionContext) { statusBarItem.show(); statusBarItem.text = "$(tools) Mise"; statusBarItem.tooltip = "Click to refresh Mise"; + registerMiseCommands(context, tasksProvider); vscode.window.registerTreeDataProvider("miseTasksView", tasksProvider); vscode.window.registerTreeDataProvider("miseToolsView", toolsProvider); diff --git a/src/miseService.ts b/src/miseService.ts index 84892af..043ceaf 100644 --- a/src/miseService.ts +++ b/src/miseService.ts @@ -6,10 +6,17 @@ import { logger } from "./utils/logger"; const execAsync = promisify(exec); export class MiseService { + private terminal: vscode.Terminal | undefined; + private readonly workspaceRoot: string | undefined; + + constructor() { + this.workspaceRoot = vscode.workspace.rootPath; + } + async getTasks(): Promise { try { const { stdout } = await execAsync("mise tasks ls --json", { - cwd: vscode.workspace.rootPath, + cwd: this.workspaceRoot, }); return JSON.parse(stdout).map((task: MiseTask) => ({ name: task.name, @@ -27,7 +34,7 @@ export class MiseService { try { const { stdout } = await execAsync("mise ls --current --offline --json", { - cwd: vscode.workspace.rootPath, + cwd: this.workspaceRoot, }); logger.info(`Got stdout from mise ls 4 command ${stdout}`); return Object.entries(JSON.parse(stdout)).flatMap(([toolName, tools]) => { @@ -51,7 +58,7 @@ export class MiseService { async getEnvs(): Promise { try { const { stdout } = await execAsync("mise env --json", { - cwd: vscode.workspace.rootPath, + cwd: this.workspaceRoot, }); return Object.entries(JSON.parse(stdout)).map(([key, value]) => ({ @@ -63,4 +70,37 @@ export class MiseService { return []; } } + + async runTask(taskName: string): Promise { + const terminal = this.getOrCreateTerminal(); + terminal.show(); + terminal.sendText(`mise run ${taskName}`); + } + + private getOrCreateTerminal(): vscode.Terminal { + if (!this.terminal || this._isTerminalClosed(this.terminal)) { + this.terminal = vscode.window.createTerminal({ + name: "Mise Tasks", + cwd: this.workspaceRoot, + }); + + vscode.window.onDidCloseTerminal((closedTerminal) => { + if (closedTerminal === this.terminal) { + this.terminal = undefined; + } + }); + } + return this.terminal; + } + + private _isTerminalClosed(terminal: vscode.Terminal): boolean { + return vscode.window.terminals.indexOf(terminal) === -1; + } + + dispose() { + if (this.terminal) { + this.terminal.dispose(); + this.terminal = undefined; + } + } } diff --git a/src/providers/tasksProvider.ts b/src/providers/tasksProvider.ts index f151a9f..007dcd7 100644 --- a/src/providers/tasksProvider.ts +++ b/src/providers/tasksProvider.ts @@ -1,12 +1,12 @@ import * as vscode from "vscode"; import type { MiseService } from "../miseService"; -export class MiseTasksProvider implements vscode.TreeDataProvider { +export class MiseTasksProvider implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter< - TaskItem | undefined | null | void - > = new vscode.EventEmitter(); + TreeNode | undefined | null | void + > = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event< - TaskItem | undefined | null | void + TreeNode | undefined | null | void > = this._onDidChangeTreeData.event; constructor(private miseService: MiseService) {} @@ -15,19 +15,93 @@ export class MiseTasksProvider implements vscode.TreeDataProvider { this._onDidChangeTreeData.fire(); } - getTreeItem(element: TaskItem): vscode.TreeItem { + getTreeItem(element: TreeNode): vscode.TreeItem { return element; } - async getChildren(): Promise { - const tasks = await this.miseService.getTasks(); - return tasks.map((task) => new TaskItem(task)); + async getChildren(element?: TreeNode): Promise { + if (!element) { + // Root level - return source groups + const tasks = await this.miseService.getTasks(); + const groupedTasks = this.groupTasksBySource(tasks); + + return Object.entries(groupedTasks).map( + ([source, tasks]) => new SourceGroupItem(source, tasks), + ); + } + + if (element instanceof SourceGroupItem) { + // Source group level - return tasks + return element.tasks.map((task) => new TaskItem(task)); + } + + return []; + } + + private groupTasksBySource(tasks: MiseTask[]): Record { + return tasks.reduce( + (groups, task) => { + const source = task.source || "Unknown"; + if (!groups[source]) { + groups[source] = []; + } + groups[source].push(task); + return groups; + }, + {} as Record, + ); + } + + async runTask(taskItem: TaskItem) { + try { + await this.miseService.runTask(taskItem.task.name); + vscode.window.showInformationMessage( + `Task '${taskItem.task.name}' started`, + ); + } catch (error) { + vscode.window.showErrorMessage( + `Failed to run task '${taskItem.task.name}': ${error}`, + ); + } + } +} + +type TreeNode = SourceGroupItem | TaskItem; + +class SourceGroupItem extends vscode.TreeItem { + constructor( + public readonly source: string, + public readonly tasks: MiseTask[], + ) { + super(source, vscode.TreeItemCollapsibleState.Expanded); + this.tooltip = `Source: ${source}\nTasks: ${tasks.length}`; + this.iconPath = new vscode.ThemeIcon("folder"); } } class TaskItem extends vscode.TreeItem { - constructor(task: MiseTask) { + constructor(public readonly task: MiseTask) { super(task.name, vscode.TreeItemCollapsibleState.None); this.tooltip = `Task: ${task.name}\nSource: ${task.source}\nDescription: ${task.description}`; + this.iconPath = new vscode.ThemeIcon("play"); + + // Add command to run the task + this.command = { + title: "Run Task", + command: "mise.runTask", + arguments: [this], + }; } } + +// Register the command in your extension's activate function: +export function registerMiseCommands( + context: vscode.ExtensionContext, + taskProvider: MiseTasksProvider, +) { + context.subscriptions.push( + vscode.commands.registerCommand("mise.runTask", (taskItem: TaskItem) => { + taskProvider.runTask(taskItem); + }), + ); +}