mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
[VSCode extension] Add [eval with bun] code lens on untitled, unsaved scratch JavaScript files (#14983)
This commit is contained in:
@@ -118,6 +118,8 @@ type LaunchRequest = DAP.LaunchRequest & {
|
||||
stopOnEntry?: boolean;
|
||||
noDebug?: boolean;
|
||||
watchMode?: boolean | "hot";
|
||||
__skipValidation?: boolean;
|
||||
stdin?: string;
|
||||
};
|
||||
|
||||
type AttachRequest = DAP.AttachRequest & {
|
||||
@@ -211,6 +213,24 @@ const debugSilentEvents = new Set(["Adapter.event", "Inspector.event"]);
|
||||
|
||||
let threadId = 1;
|
||||
|
||||
// Add these helper functions at the top level
|
||||
function normalizeSourcePath(sourcePath: string, untitledDocPath?: string, bunEvalPath?: string): string {
|
||||
if (!sourcePath) return sourcePath;
|
||||
|
||||
// Handle eval source paths
|
||||
if (sourcePath === bunEvalPath) {
|
||||
return bunEvalPath!;
|
||||
}
|
||||
|
||||
// Handle untitled documents
|
||||
if (sourcePath === untitledDocPath) {
|
||||
return bunEvalPath!;
|
||||
}
|
||||
|
||||
// Handle normal file paths
|
||||
return path.normalize(sourcePath);
|
||||
}
|
||||
|
||||
export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter {
|
||||
#threadId: number;
|
||||
#inspector: WebSocketInspector;
|
||||
@@ -229,8 +249,10 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
#variables: Map<number, Variable>;
|
||||
#initialized?: InitializeRequest;
|
||||
#options?: DebuggerOptions;
|
||||
#untitledDocPath?: string;
|
||||
#bunEvalPath?: string;
|
||||
|
||||
constructor(url?: string | URL) {
|
||||
constructor(url?: string | URL, untitledDocPath?: string, bunEvalPath?: string) {
|
||||
super();
|
||||
this.#threadId = threadId++;
|
||||
this.#inspector = new WebSocketInspector(url);
|
||||
@@ -252,6 +274,8 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
this.#targets = new Map();
|
||||
this.#variableId = 1;
|
||||
this.#variables = new Map();
|
||||
this.#untitledDocPath = untitledDocPath;
|
||||
this.#bunEvalPath = bunEvalPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,19 +498,25 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
strictEnv = false,
|
||||
watchMode = false,
|
||||
stopOnEntry = false,
|
||||
__skipValidation = false,
|
||||
stdin,
|
||||
} = request;
|
||||
|
||||
if (!program) {
|
||||
throw new Error("No program specified. Did you set the 'program' property in your launch.json?");
|
||||
if (!__skipValidation && !program) {
|
||||
throw new Error("No program specified");
|
||||
}
|
||||
|
||||
if (!isJavaScript(program)) {
|
||||
throw new Error("Program must be a JavaScript or TypeScript file.");
|
||||
const processArgs = [...runtimeArgs];
|
||||
|
||||
if (program === "-" && stdin) {
|
||||
processArgs.push("--eval", stdin);
|
||||
} else if (program) {
|
||||
processArgs.push(program);
|
||||
}
|
||||
|
||||
const processArgs = [...runtimeArgs, program, ...args];
|
||||
processArgs.push(...args);
|
||||
|
||||
if (isTestJavaScript(program) && !runtimeArgs.includes("test")) {
|
||||
if (program && isTestJavaScript(program) && !runtimeArgs.includes("test")) {
|
||||
processArgs.unshift("test");
|
||||
}
|
||||
|
||||
@@ -1073,15 +1103,21 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
#getBreakpointByLocation(source: Source, location: DAP.SourceBreakpoint): Breakpoint | undefined {
|
||||
console.log("getBreakpointByLocation", {
|
||||
source: sourceToId(source),
|
||||
location,
|
||||
ids: this.#getBreakpoints(sourceToId(source)).map(({ id }) => id),
|
||||
breakpointIds: this.#getBreakpoints(sourceToId(source)).map(({ breakpointId }) => breakpointId),
|
||||
lines: this.#getBreakpoints(sourceToId(source)).map(({ line }) => line),
|
||||
columns: this.#getBreakpoints(sourceToId(source)).map(({ column }) => column),
|
||||
});
|
||||
const sourceId = sourceToId(source);
|
||||
if (isDebug) {
|
||||
console.log("getBreakpointByLocation", {
|
||||
source: sourceToId(source),
|
||||
location,
|
||||
ids: this.#getBreakpoints(sourceToId(source)).map(({ id }) => id),
|
||||
breakpointIds: this.#getBreakpoints(sourceToId(source)).map(({ breakpointId }) => breakpointId),
|
||||
lines: this.#getBreakpoints(sourceToId(source)).map(({ line }) => line),
|
||||
columns: this.#getBreakpoints(sourceToId(source)).map(({ column }) => column),
|
||||
});
|
||||
}
|
||||
let sourceId = sourceToId(source);
|
||||
const untitledDocPath = this.#untitledDocPath;
|
||||
if (sourceId === untitledDocPath && this.#bunEvalPath) {
|
||||
sourceId = this.#bunEvalPath;
|
||||
}
|
||||
const [breakpoint] = this.#getBreakpoints(sourceId).filter(
|
||||
({ source, request }) => source && sourceToId(source) === sourceId && request?.line === location.line,
|
||||
);
|
||||
@@ -1089,7 +1125,18 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
#getBreakpoints(sourceId: string | number): Breakpoint[] {
|
||||
return [...this.#breakpoints.values()].flat().filter(({ source }) => source && sourceToId(source) === sourceId);
|
||||
let output = [];
|
||||
let all = this.#breakpoints;
|
||||
for (const breakpoints of all.values()) {
|
||||
for (const breakpoint of breakpoints) {
|
||||
const source = breakpoint.source;
|
||||
if (source && sourceToId(source) === sourceId) {
|
||||
output.push(breakpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
#getFutureBreakpoints(breakpointId: string): FutureBreakpoint[] {
|
||||
@@ -1632,7 +1679,12 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
}
|
||||
|
||||
#addSource(source: Source): Source {
|
||||
const { sourceId, scriptId, path, sourceReference } = source;
|
||||
let { sourceId, scriptId, path } = source;
|
||||
|
||||
// Normalize the source path
|
||||
if (path) {
|
||||
path = source.path = normalizeSourcePath(path, this.#untitledDocPath, this.#bunEvalPath);
|
||||
}
|
||||
|
||||
const oldSource = this.#getSourceIfPresent(sourceId);
|
||||
if (oldSource) {
|
||||
@@ -1704,10 +1756,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
|
||||
return source;
|
||||
}
|
||||
|
||||
// If the source does not have a path or is a builtin module,
|
||||
// it cannot be retrieved from the file system.
|
||||
if (typeof sourceId === "number" || !path.isAbsolute(sourceId)) {
|
||||
throw new Error(`Source not found: ${sourceId}`);
|
||||
// Normalize the source path before lookup
|
||||
if (typeof sourceId === "string") {
|
||||
sourceId = normalizeSourcePath(sourceId, this.#untitledDocPath, this.#bunEvalPath);
|
||||
}
|
||||
|
||||
// If the source is not present, it may not have been loaded yet.
|
||||
|
||||
@@ -114,6 +114,14 @@
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runUnsavedCode",
|
||||
"title": "Run Unsaved Code with Bun",
|
||||
"shortTitle": "Run with Bun",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode && resourceLangId =~ /^(javascript|typescript|javascriptreact|typescriptreact)$/ && !isInDiffEditor && resourceScheme == 'untitled'",
|
||||
"icon": "$(play-circle)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
@@ -138,6 +146,20 @@
|
||||
"command": "extension.bun.debugFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "extension.bun.runUnsavedCode",
|
||||
"when": "resourceLangId =~ /^(javascript|typescript|javascriptreact|typescriptreact)$/ && !isInDiffEditor && resourceScheme == 'untitled'",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "extension.bun.runUnsavedCode",
|
||||
"when": "resourceLangId =~ /^(javascript|typescript|javascriptreact|typescriptreact)$/ && !isInDiffEditor && resourceScheme == 'untitled'",
|
||||
"group": "1_run"
|
||||
}
|
||||
]
|
||||
},
|
||||
"breakpoints": [
|
||||
|
||||
@@ -1,14 +1,52 @@
|
||||
import * as vscode from "vscode";
|
||||
import { registerDebugger } from "./features/debug";
|
||||
import { registerDebugger, debugCommand } from "./features/debug";
|
||||
import { registerBunlockEditor } from "./features/lockfile";
|
||||
import { registerPackageJsonProviders } from "./features/tasks/package.json";
|
||||
import { registerTaskProvider } from "./features/tasks/tasks";
|
||||
|
||||
async function runUnsavedCode() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || !editor.document.isUntitled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = editor.document;
|
||||
if (!["javascript", "typescript", "javascriptreact", "typescriptreact"].includes(document.languageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const code = document.getText();
|
||||
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
|
||||
|
||||
// Get the actual untitled document name
|
||||
const untitledName = `untitled:${document.uri.path}`;
|
||||
|
||||
// Create a temporary debug session without saving
|
||||
await vscode.debug.startDebugging(
|
||||
undefined,
|
||||
{
|
||||
type: "bun",
|
||||
name: "Run Unsaved Code",
|
||||
request: "launch",
|
||||
program: "-", // Special flag to indicate stdin input
|
||||
__code: code, // Pass the code through configuration
|
||||
__untitledName: untitledName, // Pass the untitled document name
|
||||
cwd, // Pass the current working directory
|
||||
},
|
||||
{
|
||||
suppressSaveBeforeStart: true, // This prevents the save dialog
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
registerBunlockEditor(context);
|
||||
registerDebugger(context);
|
||||
registerTaskProvider(context);
|
||||
registerPackageJsonProviders(context);
|
||||
|
||||
// Only register for text editors
|
||||
context.subscriptions.push(vscode.commands.registerTextEditorCommand("extension.bun.runUnsavedCode", runUnsavedCode));
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DebugSession } from "@vscode/debugadapter";
|
||||
import { DebugSession, OutputEvent } from "@vscode/debugadapter";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
DAP,
|
||||
type DAP,
|
||||
DebugAdapter,
|
||||
getAvailablePort,
|
||||
getRandomId,
|
||||
@@ -45,6 +46,10 @@ const adapters = new Map<string, FileDebugSession>();
|
||||
|
||||
export function registerDebugger(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider(
|
||||
["javascript", "typescript", "javascriptreact", "typescriptreact"],
|
||||
new BunCodeLensProvider(),
|
||||
),
|
||||
vscode.commands.registerCommand("extension.bun.runFile", runFileCommand),
|
||||
vscode.commands.registerCommand("extension.bun.debugFile", debugFileCommand),
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
@@ -144,7 +149,15 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
target = DEBUG_CONFIGURATION;
|
||||
}
|
||||
|
||||
// If the configuration is missing a default property, copy it from the template.
|
||||
if (config.program === "-" && config.__code) {
|
||||
const code = config.__code;
|
||||
delete config.__code;
|
||||
|
||||
config.stdin = code;
|
||||
config.program = "-";
|
||||
config.__skipValidation = true;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
if (config[key] === undefined) {
|
||||
config[key] = value;
|
||||
@@ -165,7 +178,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
session: vscode.DebugSession,
|
||||
): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
|
||||
const { configuration } = session;
|
||||
const { request, url } = configuration;
|
||||
const { request, url, __untitledName } = configuration;
|
||||
|
||||
if (request === "attach") {
|
||||
for (const [adapterUrl, adapter] of adapters) {
|
||||
@@ -175,32 +188,105 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id);
|
||||
const adapter = new FileDebugSession(session.id, __untitledName);
|
||||
await adapter.initialize();
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
interface DebugProtocolResponse extends DAP.Response {
|
||||
body?: {
|
||||
source?: {
|
||||
path?: string;
|
||||
};
|
||||
breakpoints?: Array<{
|
||||
source?: {
|
||||
path?: string;
|
||||
};
|
||||
verified?: boolean;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DebugProtocolEvent extends DAP.Event {
|
||||
body?: {
|
||||
source?: {
|
||||
path?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface RuntimeConsoleAPICalledEvent {
|
||||
type: string;
|
||||
args: Array<{
|
||||
type: string;
|
||||
value: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RuntimeExceptionThrownEvent {
|
||||
exceptionDetails: {
|
||||
text: string;
|
||||
exception?: {
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
adapter: DebugAdapter;
|
||||
sessionId?: string;
|
||||
untitledDocPath?: string;
|
||||
bunEvalPath?: string;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
constructor(sessionId?: string, untitledDocPath?: string) {
|
||||
super();
|
||||
this.sessionId = sessionId;
|
||||
this.untitledDocPath = untitledDocPath;
|
||||
|
||||
if (untitledDocPath) {
|
||||
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? process.cwd();
|
||||
this.bunEvalPath = join(cwd, "[eval]");
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2);
|
||||
let url;
|
||||
if (process.platform === "win32") {
|
||||
url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`;
|
||||
const url =
|
||||
process.platform === "win32"
|
||||
? `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`
|
||||
: `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
|
||||
const { untitledDocPath, bunEvalPath } = this;
|
||||
this.adapter = new DebugAdapter(url, untitledDocPath, bunEvalPath);
|
||||
|
||||
if (untitledDocPath) {
|
||||
this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => {
|
||||
if (response.body?.source?.path === bunEvalPath) {
|
||||
response.body.source.path = untitledDocPath;
|
||||
}
|
||||
if (Array.isArray(response.body?.breakpoints)) {
|
||||
for (const bp of response.body.breakpoints) {
|
||||
if (bp.source?.path === bunEvalPath) {
|
||||
bp.source.path = untitledDocPath;
|
||||
bp.verified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sendResponse(response);
|
||||
});
|
||||
|
||||
this.adapter.on("Adapter.event", (event: DebugProtocolEvent) => {
|
||||
if (event.body?.source?.path === bunEvalPath) {
|
||||
event.body.source.path = untitledDocPath;
|
||||
}
|
||||
this.sendEvent(event);
|
||||
});
|
||||
} else {
|
||||
url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
}
|
||||
this.adapter = new DebugAdapter(url);
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
|
||||
this.adapter.on("Adapter.reverseRequest", ({ command, arguments: args }) =>
|
||||
this.sendRequest(command, args, 5000, () => {}),
|
||||
);
|
||||
@@ -212,6 +298,15 @@ class FileDebugSession extends DebugSession {
|
||||
const { type } = message;
|
||||
|
||||
if (type === "request") {
|
||||
const { untitledDocPath, bunEvalPath } = this;
|
||||
const { command } = message;
|
||||
if (untitledDocPath && (command === "setBreakpoints" || command === "breakpointLocations")) {
|
||||
const args = message.arguments as any;
|
||||
if (args.source?.path === untitledDocPath) {
|
||||
args.source.path = bunEvalPath;
|
||||
}
|
||||
}
|
||||
|
||||
this.adapter.emit("Adapter.request", message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
@@ -273,3 +368,92 @@ function getRuntime(scope?: vscode.ConfigurationScope): string {
|
||||
function getConfig<T>(path: string, scope?: vscode.ConfigurationScope) {
|
||||
return vscode.workspace.getConfiguration("bun", scope).get<T>(path);
|
||||
}
|
||||
|
||||
export async function runUnsavedCode() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || !editor.document.isUntitled) return;
|
||||
|
||||
const code = editor.document.getText();
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
// Start debugging
|
||||
await vscode.debug.startDebugging(undefined, {
|
||||
...DEBUG_CONFIGURATION,
|
||||
program: "-",
|
||||
__code: code,
|
||||
__untitledName: editor.document.uri.toString(),
|
||||
console: "debugConsole",
|
||||
internalConsoleOptions: "openOnSessionStart",
|
||||
});
|
||||
|
||||
// Find our debug session instance
|
||||
const debugSession = Array.from(adapters.values()).find(
|
||||
adapter => adapter.sessionId === vscode.debug.activeDebugSession?.id,
|
||||
);
|
||||
|
||||
if (debugSession) {
|
||||
// Wait for both the inspector to connect AND the adapter to be initialized
|
||||
await new Promise<void>(resolve => {
|
||||
let inspectorConnected = false;
|
||||
let adapterInitialized = false;
|
||||
|
||||
const checkDone = () => {
|
||||
if (inspectorConnected && adapterInitialized) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
debugSession.adapter.once("Inspector.connected", () => {
|
||||
inspectorConnected = true;
|
||||
checkDone();
|
||||
});
|
||||
|
||||
debugSession.adapter.once("Adapter.initialized", () => {
|
||||
adapterInitialized = true;
|
||||
checkDone();
|
||||
});
|
||||
});
|
||||
|
||||
// Now wait for debug session to complete
|
||||
await new Promise<void>(resolve => {
|
||||
const disposable = vscode.debug.onDidTerminateDebugSession(() => {
|
||||
const duration = (performance.now() - startTime).toFixed(1);
|
||||
debugSession.sendEvent(new OutputEvent(`✓ Code execution completed in ${duration}ms\n`));
|
||||
disposable.dispose();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (vscode.debug.activeDebugSession) {
|
||||
const duration = (performance.now() - startTime).toFixed(1);
|
||||
const errorSession = adapters.get(vscode.debug.activeDebugSession.id);
|
||||
errorSession?.sendEvent(
|
||||
new OutputEvent(`✕ Error after ${duration}ms: ${err instanceof Error ? err.message : String(err)}\n`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const languageIds = ["javascript", "typescript", "javascriptreact", "typescriptreact"];
|
||||
|
||||
class BunCodeLensProvider implements vscode.CodeLensProvider {
|
||||
async provideCodeLenses(document: vscode.TextDocument): Promise<vscode.CodeLens[]> {
|
||||
if (!document.isUntitled || document.isClosed || document.lineCount === 0) return [];
|
||||
if (!languageIds.includes(document.languageId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create a range at position 0,0 with zero width
|
||||
const range = new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0));
|
||||
|
||||
return [
|
||||
new vscode.CodeLens(range, {
|
||||
title: "eval with bun",
|
||||
command: "extension.bun.runUnsavedCode",
|
||||
tooltip: "Run this unsaved, scratch file with Bun",
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user