aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-vscode/src/features/tasks
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-vscode/src/features/tasks')
-rw-r--r--packages/bun-vscode/src/features/tasks/package.json.ts201
-rw-r--r--packages/bun-vscode/src/features/tasks/tasks.ts59
2 files changed, 260 insertions, 0 deletions
diff --git a/packages/bun-vscode/src/features/tasks/package.json.ts b/packages/bun-vscode/src/features/tasks/package.json.ts
new file mode 100644
index 000000000..55947a4a1
--- /dev/null
+++ b/packages/bun-vscode/src/features/tasks/package.json.ts
@@ -0,0 +1,201 @@
+/**
+ * Automatically generates tasks from package.json scripts.
+ */
+import * as vscode from "vscode";
+import { BunTask } from "./tasks";
+import { debugCommand } from "../debug";
+
+/**
+ * Parses tasks defined in the package.json.
+ */
+export async function providePackageJsonTasks(): Promise<BunTask[]> {
+ //
+ const scripts: Record<string, string> = await (async () => {
+ try {
+ const file = vscode.Uri.file(vscode.workspace.workspaceFolders[0]?.uri.fsPath + "/package.json");
+
+ // Load contents of package.json, no need to check if file exists, we return null if it doesn't
+ const contents = await vscode.workspace.fs.readFile(file);
+ return JSON.parse(contents.toString()).scripts;
+ } catch {
+ return null;
+ }
+ })();
+ if (!scripts) return [];
+
+ return Object.entries(scripts).map(([name, script]) => {
+ // Prefix script with bun if it doesn't already start with bun
+ const shellCommand = script.startsWith("bun run ") ? script : `bun run ${script}`;
+
+ const task = new BunTask({
+ script,
+ name,
+ detail: `${shellCommand} - package.json`,
+ execution: new vscode.ShellExecution(shellCommand),
+ });
+ return task;
+ });
+}
+
+export function registerPackageJsonProviders(context: vscode.ExtensionContext) {
+ registerCodeLensProvider(context);
+ registerHoverProvider(context);
+}
+
+/**
+ * Utility function to extract the scripts from a package.json file, including their name and position in the document.
+ */
+function extractScriptsFromPackageJson(document: vscode.TextDocument) {
+ const content = document.getText();
+ const matches = content.match(/"scripts"\s*:\s*{([\s\S]*?)}/);
+ if (!matches || matches.length < 2) return null;
+
+ const startIndex = content.indexOf(matches[0]);
+ const endIndex = startIndex + matches[0].length;
+ const range = new vscode.Range(document.positionAt(startIndex), document.positionAt(endIndex));
+
+ const scripts = matches[1].split(/,\s*/).map(script => {
+ const elements = script.match(/"([^"\\]|\\.|\\\n)*"/g);
+ if (elements?.length != 2) return null;
+ const [name, command] = elements;
+ return {
+ name: name.replace('"', "").trim(),
+ command: command.replace(/(?<!\\)"/g, "").trim(),
+ range: new vscode.Range(
+ document.positionAt(startIndex + matches[0].indexOf(name)),
+ document.positionAt(startIndex + matches[0].indexOf(name) + name.length + command.length),
+ ),
+ };
+ });
+
+ return {
+ range,
+ scripts,
+ };
+}
+
+/**
+ * This function registers a CodeLens provider for package.json files. It is used to display the "Run" and "Debug" buttons
+ * above the scripts properties in package.json (inline).
+ */
+function registerCodeLensProvider(context: vscode.ExtensionContext) {
+ context.subscriptions.push(
+ // Register CodeLens provider for package.json files
+ vscode.languages.registerCodeLensProvider(
+ {
+ language: "json",
+ scheme: "file",
+ pattern: "**/package.json",
+ },
+ {
+ provideCodeLenses(document: vscode.TextDocument) {
+ const { range } = extractScriptsFromPackageJson(document);
+
+ const codeLenses: vscode.CodeLens[] = [];
+ codeLenses.push(
+ new vscode.CodeLens(range, {
+ title: "$(breakpoints-view-icon) Bun: Debug",
+ tooltip: "Debug a script using bun",
+ command: "extension.bun.codelens.run",
+ arguments: [{ type: "debug" }],
+ }),
+ new vscode.CodeLens(range, {
+ title: "$(debug-start) Bun: Run",
+ tooltip: "Run a script using bun",
+ command: "extension.bun.codelens.run",
+ arguments: [{ type: "run" }],
+ }),
+ );
+ return codeLenses;
+ },
+ resolveCodeLens(codeLens) {
+ return codeLens;
+ },
+ },
+ ),
+ // Register the commands that are executed when clicking the CodeLens buttons
+ vscode.commands.registerCommand("extension.bun.codelens.run", async ({ type }: { type: "debug" | "run" }) => {
+ const tasks = (await vscode.tasks.fetchTasks({ type: "bun" })) as BunTask[];
+ if (tasks.length === 0) return;
+
+ const pick = await vscode.window.showQuickPick(
+ tasks
+ .filter(task => task.detail.endsWith("package.json"))
+ .map(task => ({
+ label: task.name,
+ detail: task.detail,
+ })),
+ );
+ if (!pick) return;
+
+ const task = tasks.find(task => task.name === pick.label);
+ if (!task) return;
+
+ const command = type === "debug" ? "extension.bun.codelens.debug.task" : "extension.bun.codelens.run.task";
+
+ vscode.commands.executeCommand(command, {
+ script: task.definition.script,
+ name: task.name,
+ });
+ }),
+ );
+}
+
+function getActiveTerminal(name: string) {
+ return vscode.window.terminals.filter(terminal => terminal.name === name);
+}
+
+interface CommandArgs {
+ script: string;
+ name: string;
+}
+
+/**
+ * This function registers a Hover language feature provider for package.json files. It is used to display the
+ * "Run" and "Debug" buttons when hovering over a script property in package.json.
+ */
+function registerHoverProvider(context: vscode.ExtensionContext) {
+ context.subscriptions.push(
+ vscode.languages.registerHoverProvider("json", {
+ provideHover(document, position) {
+ const { scripts } = extractScriptsFromPackageJson(document);
+
+ return {
+ contents: scripts.map(script => {
+ if (!script.range.contains(position)) return null;
+
+ const command = encodeURI(JSON.stringify({ script: script.command, name: script.name }));
+
+ const markdownString = new vscode.MarkdownString(
+ `[Debug](command:extension.bun.codelens.debug.task?${command}) | [Run](command:extension.bun.codelens.run.task?${command})`,
+ );
+ markdownString.isTrusted = true;
+
+ return markdownString;
+ }),
+ };
+ },
+ }),
+ vscode.commands.registerCommand("extension.bun.codelens.debug.task", async ({ script, name }: CommandArgs) => {
+ if (script.startsWith("bun run ")) script = script.slice(8);
+ if (script.startsWith("bun ")) script = script.slice(4);
+
+ debugCommand(script);
+ }),
+ vscode.commands.registerCommand("extension.bun.codelens.run.task", async ({ script, name }: CommandArgs) => {
+ if (script.startsWith("bun run ")) script = script.slice(8);
+
+ name = `Bun Task: ${name}`;
+ const terminals = getActiveTerminal(name);
+ if (terminals.length > 0) {
+ terminals[0].show();
+ terminals[0].sendText(`bun run ${script}`);
+ return;
+ }
+
+ const terminal = vscode.window.createTerminal({ name });
+ terminal.show();
+ terminal.sendText(`bun run ${script}`);
+ }),
+ );
+}
diff --git a/packages/bun-vscode/src/features/tasks/tasks.ts b/packages/bun-vscode/src/features/tasks/tasks.ts
new file mode 100644
index 000000000..aabeb3920
--- /dev/null
+++ b/packages/bun-vscode/src/features/tasks/tasks.ts
@@ -0,0 +1,59 @@
+import * as vscode from "vscode";
+import { providePackageJsonTasks } from "./package.json";
+
+interface BunTaskDefinition extends vscode.TaskDefinition {
+ script: string;
+}
+
+export class BunTask extends vscode.Task {
+ declare definition: BunTaskDefinition;
+
+ constructor({
+ script,
+ name,
+ detail,
+ execution,
+ scope = vscode.TaskScope.Workspace,
+ }: {
+ script: string;
+ name: string;
+ detail?: string;
+ scope?: vscode.WorkspaceFolder | vscode.TaskScope.Global | vscode.TaskScope.Workspace;
+ execution?: vscode.ProcessExecution | vscode.ShellExecution | vscode.CustomExecution;
+ }) {
+ super({ type: "bun", script }, scope, name, "bun", execution);
+ this.detail = detail;
+ }
+}
+
+/**
+ * Registers the task provider for the bun extension.
+ */
+export function registerTaskProvider(context: vscode.ExtensionContext) {
+ const taskProvider: vscode.TaskProvider<BunTask> = {
+ provideTasks: async () => await providePackageJsonTasks(),
+ resolveTask: task => resolveTask(task),
+ };
+ context.subscriptions.push(vscode.tasks.registerTaskProvider("bun", taskProvider));
+}
+
+/**
+ * Parses tasks defined in the vscode tasks.json file.
+ * For more information, see https://code.visualstudio.com/api/extension-guides/task-provider
+ */
+export function resolveTask(task: BunTask): BunTask | undefined {
+ // Make sure the task has a script defined
+ const definition: BunTask["definition"] = task.definition;
+ if (!definition.script) return task;
+ const shellCommand = definition.script.startsWith("bun ") ? definition.script : `bun ${definition.script}`;
+
+ const newTask = new vscode.Task(
+ definition,
+ task.scope ?? vscode.TaskScope.Workspace,
+ task.name,
+ "bun",
+ new vscode.ShellExecution(shellCommand),
+ ) as BunTask;
+ newTask.detail = `${shellCommand} - tasks.json`;
+ return newTask;
+}