diff options
Diffstat (limited to 'packages/bun-vscode')
-rw-r--r-- | packages/bun-vscode/README.md | 14 | ||||
-rw-r--r-- | packages/bun-vscode/assets/vscode.css | 69 | ||||
-rw-r--r--[-rwxr-xr-x] | packages/bun-vscode/bun.lockb | bin | 53586 -> 53586 bytes | |||
-rw-r--r-- | packages/bun-vscode/example/package.json | 6 | ||||
-rw-r--r-- | packages/bun-vscode/package.json | 38 | ||||
-rw-r--r-- | packages/bun-vscode/scripts/build.mjs | 3 | ||||
-rw-r--r-- | packages/bun-vscode/src/extension.ts | 12 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/debug.ts | 88 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/lockfile.ts | 79 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/lockfile/index.ts | 109 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/lockfile/lockfile.style.ts | 35 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/tasks/package.json.ts | 201 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/tasks/tasks.ts | 59 |
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 Binary files differindex c879b518e..c879b518e 100755..100644 --- a/packages/bun-vscode/bun.lockb +++ b/packages/bun-vscode/bun.lockb 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"> ${parts[0]} </span><span class="mtk16">${parts[1]}</span></span>`; + } + if (line.startsWith(" ")) { + const leftPart = `<span class="mtk6"> ${parts[0]} </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} </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; +} |