Implement VSCode tasks for bun (#5529)

* Fix VSCode extension configuration documentation

* Fix config descriptions

* Fix typo

* Implement bun.lockb highlighter

* Update style

* Improve styling

* Revert bunlock change

* Implement bun tasks

* Revert change

* Package json codelens

* Fixes

* Refactor
This commit is contained in:
JeremyFunk
2023-09-17 17:59:01 +02:00
committed by GitHub
parent 0404d09cc7
commit c6b25adeea
9 changed files with 373 additions and 82 deletions

View File

@@ -7,6 +7,10 @@
"mime": "^3.0.0",
"mime-db": "^1.52.0"
},
"scripts": {
"run": "hello.js",
"start": "bun hello.js"
},
"trustedDependencies": [
"mime"
],

View File

@@ -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"
}
}
}
]
}
}

View File

@@ -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 });

View File

@@ -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() {}

View File

@@ -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 {
const path = getActivePath(resource);
if (path) {
vscode.debug.startDebugging(undefined, {
...debugConfiguration,
program: path,
runtime: getRuntime(resource),
});
}
export function debugCommand(command: string) {
vscode.debug.startDebugging(undefined, {
...DEBUG_CONFIGURATION,
program: command,
runtime: getRuntime(),
});
}
function InjectDebugTerminal(terminal: vscode.Terminal): void {
const enabled = getConfig("debugTerminal.enabled");
if (enabled === false) {
return;
}
function debugFileCommand(resource?: vscode.Uri) {
const path = getActivePath(resource);
if (path) debugCommand(path);
}
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);
}

View File

@@ -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,
},
}),
);
}

View File

@@ -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>`;
}

View File

@@ -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}`);
}),
);
}

View File

@@ -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;
}