aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-vscode
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-vscode')
-rw-r--r--packages/bun-vscode/README.md14
-rw-r--r--packages/bun-vscode/assets/vscode.css69
-rw-r--r--[-rwxr-xr-x]packages/bun-vscode/bun.lockbbin53586 -> 53586 bytes
-rw-r--r--packages/bun-vscode/example/package.json6
-rw-r--r--packages/bun-vscode/package.json38
-rw-r--r--packages/bun-vscode/scripts/build.mjs3
-rw-r--r--packages/bun-vscode/src/extension.ts12
-rw-r--r--packages/bun-vscode/src/features/debug.ts88
-rw-r--r--packages/bun-vscode/src/features/lockfile.ts79
-rw-r--r--packages/bun-vscode/src/features/lockfile/index.ts109
-rw-r--r--packages/bun-vscode/src/features/lockfile/lockfile.style.ts35
-rw-r--r--packages/bun-vscode/src/features/tasks/package.json.ts201
-rw-r--r--packages/bun-vscode/src/features/tasks/tasks.ts59
13 files changed, 551 insertions, 162 deletions
diff --git a/packages/bun-vscode/README.md b/packages/bun-vscode/README.md
index c3d94924c..6848f8977 100644
--- a/packages/bun-vscode/README.md
+++ b/packages/bun-vscode/README.md
@@ -73,7 +73,7 @@ You can use the following configurations to debug JavaScript and TypeScript file
"name": "Attach to Bun",
// The URL of the WebSocket inspector to attach to.
- // This value can be retreived by using `bun --inspect`.
+ // This value can be retrieved by using `bun --inspect`.
"url": "ws://localhost:6499/",
}
]
@@ -89,12 +89,10 @@ You can use the following configurations to customize the behavior of the Bun ex
// The path to the `bun` executable.
"bun.runtime": "/path/to/bun",
- "bun.debugTerminal": {
- // If support for Bun should be added to the default "JavaScript Debug Terminal".
- "enabled": true,
-
- // If the debugger should stop on the first line of the program.
- "stopOnEntry": false,
- }
+ // If support for Bun should be added to the default "JavaScript Debug Terminal".
+ "bun.debugTerminal.enabled": true,
+
+ // If the debugger should stop on the first line of the program.
+ "bun.debugTerminal.stopOnEntry": false,
}
``` \ No newline at end of file
diff --git a/packages/bun-vscode/assets/vscode.css b/packages/bun-vscode/assets/vscode.css
new file mode 100644
index 000000000..32bdcc5b2
--- /dev/null
+++ b/packages/bun-vscode/assets/vscode.css
@@ -0,0 +1,69 @@
+.mtk1 { color: #cccccc; }
+.mtk2 { color: #1f1f1f; }
+.mtk3 { color: #d4d4d4; }
+.mtk4 { color: #000080; }
+.mtk5 { color: #6a9955; }
+.mtk6 { color: #569cd6; }
+.mtk7 { color: #b5cea8; }
+.mtk8 { color: #646695; }
+.mtk9 { color: #d7ba7d; }
+.mtk10 { color: #9cdcfe; }
+.mtk11 { color: #f44747; }
+.mtk12 { color: #ce9178; }
+.mtk13 { color: #6796e6; }
+.mtk14 { color: #808080; }
+.mtk15 { color: #d16969; }
+.mtk16 { color: #dcdcaa; }
+.mtk17 { color: #4ec9b0; }
+.mtk18 { color: #c586c0; }
+.mtk19 { color: #4fc1ff; }
+.mtk20 { color: #c8c8c8; }
+.mtk21 { color: #606060; }
+.mtk22 { color: #ffffff; }
+.mtk23 { color: #cd9731; }
+.mtk24 { color: #b267e6; }
+.mtki { font-style: italic; }
+.mtkb { font-weight: bold; }
+.mtku { text-decoration: underline; text-underline-position: under; }
+.mtks { text-decoration: line-through; }
+.mtks.mtku { text-decoration: underline line-through; text-underline-position: under; }
+
+.bunlock {
+ display: flex;
+ flex-direction: row;
+}
+
+.lines {
+ display: flex;
+ flex-direction: column;
+ width: 30px;
+ margin-right: 15px;
+ text-align: right;
+ opacity: 0.5;
+
+ font-size: var(--vscode-editor-font-size);
+ font-weight: var(--vscode-editor-font-weight);
+ font-family: var(--vscode-editor-font-family);
+ background-color: var(--vscode-editor-background);
+}
+
+.lines > span {
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
+
+code {
+ white-space: pre;
+
+ font-size: var(--vscode-editor-font-size);
+ font-weight: var(--vscode-editor-font-weight);
+ font-family: var(--vscode-editor-font-family);
+ background-color: var(--vscode-editor-background);
+}
+
+code > span {
+ display: inline-block;
+ width: 100%;
+ margin-top: 1px;
+ margin-bottom: 1px;
+}
diff --git a/packages/bun-vscode/bun.lockb b/packages/bun-vscode/bun.lockb
index c879b518e..c879b518e 100755..100644
--- a/packages/bun-vscode/bun.lockb
+++ b/packages/bun-vscode/bun.lockb
Binary files differ
diff --git a/packages/bun-vscode/example/package.json b/packages/bun-vscode/example/package.json
index 602fba159..eed10159d 100644
--- a/packages/bun-vscode/example/package.json
+++ b/packages/bun-vscode/example/package.json
@@ -7,6 +7,12 @@
"mime": "^3.0.0",
"mime-db": "^1.52.0"
},
+ "scripts": {
+ "run": "node hello.js",
+ "start": "hello.js",
+ "start:bun": "bun hello.js",
+ "start:bun:quotes": "bun run hello.js"
+ },
"trustedDependencies": [
"mime"
],
diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json
index 39b5d37de..501257eb0 100644
--- a/packages/bun-vscode/package.json
+++ b/packages/bun-vscode/package.json
@@ -54,13 +54,7 @@
"../bun-inspector-protocol"
],
"activationEvents": [
- "onLanguage:javascript",
- "onLanguage:javascriptreact",
- "onLanguage:typescript",
- "onLanguage:typescriptreact",
- "workspaceContains:**/.lockb",
- "onDebugResolve:bun",
- "onDebugDynamicConfigurations:bun"
+ "onStartupFinished"
],
"browser": "dist/web-extension.js",
"bugs": {
@@ -95,7 +89,7 @@
},
"bun.debugTerminal.stopOnEntry": {
"type": "boolean",
- "description": "If Bun should stop on entry when used in the JavaScript Debug Terminal.",
+ "description": "If the debugger should stop on the first line when used in the JavaScript Debug Terminal.",
"scope": "window",
"default": false
}
@@ -177,12 +171,12 @@
"properties": {
"runtime": {
"type": "string",
- "description": "The path to Bun.",
+ "description": "The path to the `bun` executable. Defaults to `PATH` environment variable.",
"default": "bun"
},
"runtimeArgs": {
"type": "array",
- "description": "The command-line arguments passed to Bun.",
+ "description": "The command-line arguments passed to the `bun` executable. Unlike `args`, these arguments are not passed to the program, but to the `bun` executable itself.",
"items": {
"type": "string"
},
@@ -208,7 +202,7 @@
},
"env": {
"type": "object",
- "description": "The environment variables passed to Bun.",
+ "description": "The environment variables to pass to Bun.",
"default": {}
},
"strictEnv": {
@@ -218,7 +212,7 @@
},
"stopOnEntry": {
"type": "boolean",
- "description": "If a breakpoint should be set at the first line.",
+ "description": "If the debugger should stop on the first line of the program.",
"default": false
},
"noDebug": {
@@ -231,7 +225,7 @@
"boolean",
"string"
],
- "description": "If the process should be restarted when files change.",
+ "description": "If the process should be restarted when files change. Equivalent to passing `--watch` or `--hot` to the `bun` executable.",
"enum": [
true,
false,
@@ -245,7 +239,7 @@
"properties": {
"url": {
"type": "string",
- "description": "The URL of the Bun process to attach to."
+ "description": "The URL of the WebSocket inspector to attach to."
},
"noDebug": {
"type": "boolean",
@@ -254,7 +248,7 @@
},
"stopOnEntry": {
"type": "boolean",
- "description": "If a breakpoint should when the program is attached.",
+ "description": "If the debugger should stop on the first line of the program.",
"default": false
}
}
@@ -294,6 +288,20 @@
],
"priority": "default"
}
+ ],
+ "taskDefinitions": [
+ {
+ "type": "bun",
+ "required": [
+ "script"
+ ],
+ "properties": {
+ "script": {
+ "type": "string",
+ "description": "The script to execute"
+ }
+ }
+ }
]
}
}
diff --git a/packages/bun-vscode/scripts/build.mjs b/packages/bun-vscode/scripts/build.mjs
index 261965840..4f2292599 100644
--- a/packages/bun-vscode/scripts/build.mjs
+++ b/packages/bun-vscode/scripts/build.mjs
@@ -12,6 +12,9 @@ buildSync({
external: ["vscode"],
platform: "node",
format: "cjs",
+ // The following settings are required to allow for extension debugging
+ minify: false,
+ sourcemap: true,
});
rmSync("extension", { recursive: true, force: true });
diff --git a/packages/bun-vscode/src/extension.ts b/packages/bun-vscode/src/extension.ts
index e333aedd7..175165fa7 100644
--- a/packages/bun-vscode/src/extension.ts
+++ b/packages/bun-vscode/src/extension.ts
@@ -1,10 +1,14 @@
import * as vscode from "vscode";
-import activateLockfile from "./features/lockfile";
-import activateDebug from "./features/debug";
+import { registerTaskProvider } from "./features/tasks/tasks";
+import { registerDebugger } from "./features/debug";
+import { registerPackageJsonProviders } from "./features/tasks/package.json";
+import { registerBunlockEditor } from "./features/lockfile";
export function activate(context: vscode.ExtensionContext) {
- activateLockfile(context);
- activateDebug(context);
+ registerBunlockEditor(context);
+ registerDebugger(context);
+ registerTaskProvider(context);
+ registerPackageJsonProviders(context);
}
export function deactivate() {}
diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts
index 2ea21dbe8..caa0c9378 100644
--- a/packages/bun-vscode/src/features/debug.ts
+++ b/packages/bun-vscode/src/features/debug.ts
@@ -4,43 +4,43 @@ import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
import { DebugSession } from "@vscode/debugadapter";
import { tmpdir } from "node:os";
-const debugConfiguration: vscode.DebugConfiguration = {
+export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
+ internalConsoleOptions: "neverOpen",
request: "launch",
name: "Debug File",
program: "${file}",
cwd: "${workspaceFolder}",
stopOnEntry: false,
watchMode: false,
- internalConsoleOptions: "neverOpen",
};
-const runConfiguration: vscode.DebugConfiguration = {
+export const RUN_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
+ internalConsoleOptions: "neverOpen",
request: "launch",
name: "Run File",
program: "${file}",
cwd: "${workspaceFolder}",
noDebug: true,
watchMode: false,
- internalConsoleOptions: "neverOpen",
};
-const attachConfiguration: vscode.DebugConfiguration = {
+const ATTACH_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
+ internalConsoleOptions: "neverOpen",
request: "attach",
name: "Attach Bun",
url: "ws://localhost:6499/",
stopOnEntry: false,
- internalConsoleOptions: "neverOpen",
};
const adapters = new Map<string, FileDebugSession>();
-export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
+export function registerDebugger(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
context.subscriptions.push(
- vscode.commands.registerCommand("extension.bun.runFile", RunFileCommand),
- vscode.commands.registerCommand("extension.bun.debugFile", DebugFileCommand),
+ vscode.commands.registerCommand("extension.bun.runFile", runFileCommand),
+ vscode.commands.registerCommand("extension.bun.debugFile", debugFileCommand),
vscode.debug.registerDebugConfigurationProvider(
"bun",
new DebugConfigurationProvider(),
@@ -52,15 +52,15 @@ export default function (context: vscode.ExtensionContext, factory?: vscode.Debu
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
),
vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory ?? new InlineDebugAdapterFactory()),
- vscode.window.onDidOpenTerminal(InjectDebugTerminal),
+ vscode.window.onDidOpenTerminal(injectDebugTerminal),
);
}
-function RunFileCommand(resource?: vscode.Uri): void {
+function runFileCommand(resource?: vscode.Uri): void {
const path = getActivePath(resource);
if (path) {
vscode.debug.startDebugging(undefined, {
- ...runConfiguration,
+ ...RUN_CONFIGURATION,
noDebug: true,
program: path,
runtime: getRuntime(resource),
@@ -68,22 +68,21 @@ function RunFileCommand(resource?: vscode.Uri): void {
}
}
-function DebugFileCommand(resource?: vscode.Uri): void {
+export function debugCommand(command: string) {
+ vscode.debug.startDebugging(undefined, {
+ ...DEBUG_CONFIGURATION,
+ program: command,
+ runtime: getRuntime(),
+ });
+}
+
+function debugFileCommand(resource?: vscode.Uri) {
const path = getActivePath(resource);
- if (path) {
- vscode.debug.startDebugging(undefined, {
- ...debugConfiguration,
- program: path,
- runtime: getRuntime(resource),
- });
- }
+ if (path) debugCommand(path);
}
-function InjectDebugTerminal(terminal: vscode.Terminal): void {
- const enabled = getConfig("debugTerminal.enabled");
- if (enabled === false) {
- return;
- }
+function injectDebugTerminal(terminal: vscode.Terminal): void {
+ if (!getConfig("debugTerminal.enabled")) return;
const { name, creationOptions } = terminal;
if (name !== "JavaScript Debug Terminal") {
@@ -118,16 +117,9 @@ function InjectDebugTerminal(terminal: vscode.Terminal): void {
setTimeout(() => terminal.dispose(), 100);
}
-class TerminalProfileProvider implements vscode.TerminalProfileProvider {
- provideTerminalProfile(token: vscode.CancellationToken): vscode.ProviderResult<vscode.TerminalProfile> {
- const { terminalProfile } = new TerminalDebugSession();
- return terminalProfile;
- }
-}
-
class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
provideDebugConfigurations(folder?: vscode.WorkspaceFolder): vscode.ProviderResult<vscode.DebugConfiguration[]> {
- return [debugConfiguration, runConfiguration, attachConfiguration];
+ return [DEBUG_CONFIGURATION, RUN_CONFIGURATION, ATTACH_CONFIGURATION];
}
resolveDebugConfiguration(
@@ -139,9 +131,9 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
const { request } = config;
if (request === "attach") {
- target = attachConfiguration;
+ target = ATTACH_CONFIGURATION;
} else {
- target = debugConfiguration;
+ target = DEBUG_CONFIGURATION;
}
// If the configuration is missing a default property, copy it from the template.
@@ -219,7 +211,7 @@ class TerminalDebugSession extends FileDebugSession {
this.signal = new UnixSignal();
this.signal.on("Signal.received", () => {
vscode.debug.startDebugging(undefined, {
- ...attachConfiguration,
+ ...ATTACH_CONFIGURATION,
url: this.adapter.url,
});
});
@@ -238,34 +230,18 @@ class TerminalDebugSession extends FileDebugSession {
}
}
-function getActiveDocument(): vscode.TextDocument | undefined {
- return vscode.window.activeTextEditor?.document;
-}
-
function getActivePath(target?: vscode.Uri): string | undefined {
- if (!target) {
- target = getActiveDocument()?.uri;
- }
- return target?.fsPath;
-}
-
-function isJavaScript(languageId?: string): boolean {
- return (
- languageId === "javascript" ||
- languageId === "javascriptreact" ||
- languageId === "typescript" ||
- languageId === "typescriptreact"
- );
+ return target?.fsPath ?? vscode.window.activeTextEditor?.document?.uri.fsPath;
}
function getRuntime(scope?: vscode.ConfigurationScope): string {
- const value = getConfig("runtime", scope);
+ const value = getConfig<string>("runtime", scope);
if (typeof value === "string" && value.trim().length > 0) {
return value;
}
return "bun";
}
-function getConfig<T>(path: string, scope?: vscode.ConfigurationScope): unknown {
- return vscode.workspace.getConfiguration("bun", scope).get(path);
+function getConfig<T>(path: string, scope?: vscode.ConfigurationScope) {
+ return vscode.workspace.getConfiguration("bun", scope).get<T>(path);
}
diff --git a/packages/bun-vscode/src/features/lockfile.ts b/packages/bun-vscode/src/features/lockfile.ts
deleted file mode 100644
index 81adf5b9e..000000000
--- a/packages/bun-vscode/src/features/lockfile.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as vscode from "vscode";
-import { spawn } from "node:child_process";
-
-export type BunLockfile = vscode.CustomDocument & {
- readonly preview: string;
-};
-
-export class BunLockfileEditorProvider implements vscode.CustomReadonlyEditorProvider {
- constructor(context: vscode.ExtensionContext) {}
-
- async openCustomDocument(
- uri: vscode.Uri,
- openContext: vscode.CustomDocumentOpenContext,
- token: vscode.CancellationToken,
- ): Promise<BunLockfile> {
- const preview = await previewLockfile(uri, token);
- return {
- uri,
- preview,
- dispose() {},
- };
- }
-
- async resolveCustomEditor(
- document: BunLockfile,
- webviewPanel: vscode.WebviewPanel,
- token: vscode.CancellationToken,
- ): Promise<void> {
- const { preview } = document;
- renderLockfile(webviewPanel, preview);
- }
-}
-
-function renderLockfile(webviewPanel: vscode.WebviewPanel, preview: string): void {
- // TODO: Improve syntax highlighting to match that of yarn.lock
- webviewPanel.webview.html = `<pre><code class="language-yaml">${preview}</code></pre>`;
-}
-
-function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Promise<string> {
- return new Promise((resolve, reject) => {
- const process = spawn("bun", [uri.fsPath], {
- stdio: ["ignore", "pipe", "pipe"],
- });
- token.onCancellationRequested(() => {
- process.kill();
- });
- let stdout = "";
- process.stdout.on("data", (data: Buffer) => {
- stdout += data.toString();
- });
- let stderr = "";
- process.stderr.on("data", (data: Buffer) => {
- stderr += data.toString();
- });
- process.on("error", error => {
- reject(error);
- });
- process.on("exit", code => {
- if (code === 0) {
- resolve(stdout);
- } else {
- reject(new Error(`Bun exited with code: ${code}\n${stderr}`));
- }
- });
- });
-}
-
-export default function (context: vscode.ExtensionContext): void {
- const viewType = "bun.lockb";
- const provider = new BunLockfileEditorProvider(context);
-
- vscode.window.registerCustomEditorProvider(viewType, provider, {
- supportsMultipleEditorsPerDocument: true,
- webviewOptions: {
- enableFindWidget: true,
- retainContextWhenHidden: true,
- },
- });
-}
diff --git a/packages/bun-vscode/src/features/lockfile/index.ts b/packages/bun-vscode/src/features/lockfile/index.ts
new file mode 100644
index 000000000..cef9a1768
--- /dev/null
+++ b/packages/bun-vscode/src/features/lockfile/index.ts
@@ -0,0 +1,109 @@
+import * as vscode from "vscode";
+import { spawn } from "node:child_process";
+import { styleLockfile } from "./lockfile.style";
+
+export type BunLockfile = vscode.CustomDocument & {
+ readonly preview: string;
+};
+
+export class BunLockfileEditorProvider implements vscode.CustomReadonlyEditorProvider {
+ constructor(private context: vscode.ExtensionContext) {}
+
+ async openCustomDocument(
+ uri: vscode.Uri,
+ openContext: vscode.CustomDocumentOpenContext,
+ token: vscode.CancellationToken,
+ ): Promise<BunLockfile> {
+ const preview = await previewLockfile(uri, token);
+ return {
+ uri,
+ preview,
+ dispose() {},
+ };
+ }
+
+ async resolveCustomEditor(
+ document: BunLockfile,
+ webviewPanel: vscode.WebviewPanel,
+ token: vscode.CancellationToken,
+ ): Promise<void> {
+ const { preview } = document;
+ webviewPanel.webview.options = {
+ localResourceRoots: [this.context.extensionUri],
+ };
+ renderLockfile(webviewPanel, preview, this.context.extensionUri);
+ }
+}
+
+function renderLockfile({ webview }: vscode.WebviewPanel, preview: string, extensionUri: vscode.Uri): void {
+ const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "assets", "vscode.css"));
+ const lockfileContent = styleLockfile(preview);
+
+ const lineNumbers: string[] = [];
+ for (let i = 0; i < lockfileContent.split("\n").length; i++) {
+ lineNumbers.push(`<span class="line-number">${i + 1}</span>`);
+ }
+
+ webview.html = `
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource};">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link href="${styleVSCodeUri}" rel="stylesheet" />
+ </head>
+ <body>
+ <div class="bunlock">
+ <div class="lines">
+ ${lineNumbers.join("\n")}
+ </div>
+ <code>${lockfileContent}</code>
+ </div>
+ </body>
+</html>`;
+}
+
+function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Promise<string> {
+ return new Promise((resolve, reject) => {
+ const process = spawn("bun", [uri.fsPath], {
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ token?.onCancellationRequested(() => {
+ process.kill();
+ });
+ let stdout = "";
+ process.stdout.on("data", (data: Buffer) => {
+ stdout += data.toString();
+ });
+ let stderr = "";
+ process.stderr.on("data", (data: Buffer) => {
+ stderr += data.toString();
+ });
+ process.on("error", error => {
+ reject(error);
+ });
+ process.on("exit", code => {
+ if (code === 0) {
+ resolve(stdout);
+ } else {
+ reject(new Error(`Bun exited with code: ${code}\n${stderr}`));
+ }
+ });
+ });
+}
+
+export function registerBunlockEditor(context: vscode.ExtensionContext): void {
+ const viewType = "bun.lockb";
+ const provider = new BunLockfileEditorProvider(context);
+
+ context.subscriptions.push(
+ vscode.window.registerCustomEditorProvider(viewType, provider, {
+ supportsMultipleEditorsPerDocument: true,
+ webviewOptions: {
+ enableFindWidget: true,
+ retainContextWhenHidden: true,
+ },
+ }),
+ );
+}
diff --git a/packages/bun-vscode/src/features/lockfile/lockfile.style.ts b/packages/bun-vscode/src/features/lockfile/lockfile.style.ts
new file mode 100644
index 000000000..7c4650497
--- /dev/null
+++ b/packages/bun-vscode/src/features/lockfile/lockfile.style.ts
@@ -0,0 +1,35 @@
+export function styleLockfile(preview: string) {
+ // Match all lines that don't start with a whitespace character
+ const lines = preview.split(/\n(?!\s)/);
+
+ return lines.map(styleSection).join("\n");
+}
+
+function styleSection(section: string) {
+ const lines = section.split(/\n/);
+
+ return lines.map(styleLine).join("\n");
+}
+
+function styleLine(line: string) {
+ if (line.startsWith("#")) {
+ return `<span class="mtk5">${line}</span>`;
+ }
+
+ const parts = line.trim().split(" ");
+ if (line.startsWith(" ")) {
+ return `<span><span class="mtk1">&nbsp;&nbsp;&nbsp;&nbsp;${parts[0]}&nbsp;</span><span class="mtk16">${parts[1]}</span></span>`;
+ }
+ if (line.startsWith(" ")) {
+ const leftPart = `<span class="mtk6">&nbsp;&nbsp;${parts[0]}&nbsp;</span>`;
+
+ if (parts.length === 1) return `<span>${leftPart}</span>`;
+
+ if (parts[1].startsWith('"http://') || parts[1].startsWith('"https://'))
+ return `<span>${leftPart}<span class="mtk12 detected-link">${parts[1]}</span></span>`;
+ if (parts[1].startsWith('"')) return `<span>${leftPart}<span class="mtk16">${parts[1]}</span></span>`;
+
+ return `<span>${leftPart}<span class="mtk6">${parts[1]}</span></span>`;
+ }
+ return `<span class="mtk1">${line}&nbsp;</span>`;
+}
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;
+}