diff options
-rw-r--r-- | packages/bun-vscode/example/package.json | 4 | ||||
-rw-r--r-- | packages/bun-vscode/package.json | 22 | ||||
-rw-r--r-- | packages/bun-vscode/scripts/build.mjs | 4 | ||||
-rw-r--r-- | packages/bun-vscode/src/extension.ts | 12 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/debug.ts | 90 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/lockfile/index.ts (renamed from packages/bun-vscode/src/features/lockfile.ts) | 30 | ||||
-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 | 197 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/tasks/tasks.ts | 59 |
9 files changed, 372 insertions, 81 deletions
diff --git a/packages/bun-vscode/example/package.json b/packages/bun-vscode/example/package.json index 602fba159..91055b5f5 100644 --- a/packages/bun-vscode/example/package.json +++ b/packages/bun-vscode/example/package.json @@ -7,6 +7,10 @@ "mime": "^3.0.0", "mime-db": "^1.52.0" }, + "scripts": { + "run": "hello.js", + "start": "bun hello.js" + }, "trustedDependencies": [ "mime" ], diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json index f0a9e065e..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": { @@ -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..5db577281 100644 --- a/packages/bun-vscode/scripts/build.mjs +++ b/packages/bun-vscode/scripts/build.mjs @@ -12,6 +12,10 @@ 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..d5522c493 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -4,43 +4,44 @@ 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 +53,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 +69,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 +118,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 +132,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 +212,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 +231,19 @@ 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); } +
\ No newline at end of file diff --git a/packages/bun-vscode/src/features/lockfile.ts b/packages/bun-vscode/src/features/lockfile/index.ts index 777649270..df901b288 100644 --- a/packages/bun-vscode/src/features/lockfile.ts +++ b/packages/bun-vscode/src/features/lockfile/index.ts @@ -38,10 +38,10 @@ export class BunLockfileEditorProvider implements vscode.CustomReadonlyEditorPro function renderLockfile({ webview }: vscode.WebviewPanel, preview: string, extensionUri: vscode.Uri): void { const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "media", "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>`) + + const lineNumbers: string[] = []; + for (let i = 0; i < lockfileContent.split("\n").length; i++) { + lineNumbers.push(`<span class="line-number">${i + 1}</span>`); } webview.html = ` @@ -50,7 +50,7 @@ function renderLockfile({ webview }: vscode.WebviewPanel, preview: string, exten <head> <meta charset="UTF-8"> - <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource};"> + <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"> @@ -59,7 +59,7 @@ function renderLockfile({ webview }: vscode.WebviewPanel, preview: string, exten <body> <div class="bunlock"> <div class="lines"> - ${lineNumbers.join('\n')} + ${lineNumbers.join("\n")} </div> <code>${lockfileContent}</code> </div> @@ -96,15 +96,17 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro }); } -export default function (context: vscode.ExtensionContext): void { +export function registerBunlockEditor(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, - }, - }); + 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..f9a24937d --- /dev/null +++ b/packages/bun-vscode/src/features/tasks/package.json.ts @@ -0,0 +1,197 @@ +/** + * 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") ? script : `bun ${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 [name, command] = script.split(/s*:\s*/); + return { + name: name.replace(/"/g, "").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 ")) script = script.slice(4); + debugCommand(script); + }), + vscode.commands.registerCommand("extension.bun.codelens.run.task", async ({ script, name }: CommandArgs) => { + if (script.startsWith("bun ")) script = script.slice(4); + + name = `Bun Task: ${name}`; + const terminals = getActiveTerminal(name); + if (terminals.length > 0) { + terminals[0].show(); + terminals[0].sendText(`bun ${script}`); + return; + } + + const terminal = vscode.window.createTerminal({name}); + terminal.show(); + terminal.sendText(`bun ${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; +} |