diff options
Diffstat (limited to 'packages/bun-vscode/src')
-rw-r--r-- | packages/bun-vscode/src/activate.ts | 118 | ||||
-rw-r--r-- | packages/bun-vscode/src/dap.ts | 1305 | ||||
-rw-r--r-- | packages/bun-vscode/src/extension.ts | 16 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/debug.ts | 153 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/lockfile.ts (renamed from packages/bun-vscode/src/lockfile.ts) | 24 | ||||
-rw-r--r-- | packages/bun-vscode/src/jsc.ts | 308 | ||||
-rw-r--r-- | packages/bun-vscode/src/web-extension.ts | 9 |
7 files changed, 170 insertions, 1763 deletions
diff --git a/packages/bun-vscode/src/activate.ts b/packages/bun-vscode/src/activate.ts deleted file mode 100644 index cbcb8cc1a..000000000 --- a/packages/bun-vscode/src/activate.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as vscode from "vscode"; -import { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode"; -import { DAPAdapter } from "./dap"; -import lockfile from "./lockfile"; - -export function activateBunDebug(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) { - lockfile(context); - - context.subscriptions.push( - vscode.commands.registerCommand("extension.bun.runEditorContents", (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging( - undefined, - { - type: "bun", - name: "Run File", - request: "launch", - program: targetResource.fsPath, - }, - { noDebug: true }, - ); - } - }), - vscode.commands.registerCommand("extension.bun.debugEditorContents", (resource: vscode.Uri) => { - let targetResource = resource; - if (!targetResource && vscode.window.activeTextEditor) { - targetResource = vscode.window.activeTextEditor.document.uri; - } - if (targetResource) { - vscode.debug.startDebugging(undefined, { - type: "bun", - name: "Debug File", - request: "launch", - program: targetResource.fsPath, - stopOnEntry: true, - }); - } - }), - ); - - context.subscriptions.push( - vscode.commands.registerCommand("extension.bun.getProgramName", config => { - return vscode.window.showInputBox({ - placeHolder: "Please enter the name of a file in the workspace folder", - value: "src/index.js", - }); - }), - ); - - const provider = new BunConfigurationProvider(); - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider)); - - context.subscriptions.push( - vscode.debug.registerDebugConfigurationProvider( - "bun", - { - provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> { - return [ - { - name: "Launch", - request: "launch", - type: "bun", - program: "${file}", - }, - ]; - }, - }, - vscode.DebugConfigurationProviderTriggerKind.Dynamic, - ), - ); - - if (!factory) { - factory = new InlineDebugAdapterFactory(); - } - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory)); - if ("dispose" in factory) { - // @ts-expect-error ??? - context.subscriptions.push(factory); - } -} - -class BunConfigurationProvider implements vscode.DebugConfigurationProvider { - resolveDebugConfiguration( - folder: WorkspaceFolder | undefined, - config: DebugConfiguration, - token?: CancellationToken, - ): ProviderResult<DebugConfiguration> { - // if launch.json is missing or empty - if (!config.type && !config.request && !config.name) { - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === "javascript") { - config.type = "bun"; - config.name = "Launch"; - config.request = "launch"; - config.program = "${file}"; - config.stopOnEntry = true; - } - } - - if (!config.program) { - return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => { - return undefined; // abort launch - }); - } - - return config; - } -} - -class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { - createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> { - return new vscode.DebugAdapterInlineImplementation(new DAPAdapter(_session)); - } -} diff --git a/packages/bun-vscode/src/dap.ts b/packages/bun-vscode/src/dap.ts deleted file mode 100644 index efa7e08b4..000000000 --- a/packages/bun-vscode/src/dap.ts +++ /dev/null @@ -1,1305 +0,0 @@ -import * as vscode from "vscode"; -import { spawn, type ChildProcess } from "node:child_process"; -import { - ContinuedEvent, - InitializedEvent, - LoadedSourceEvent, - LoggingDebugSession, - OutputEvent, - StoppedEvent, - ThreadEvent, - Thread, - ExitedEvent, - TerminatedEvent, - Source, - BreakpointEvent, - DebugSession, -} from "@vscode/debugadapter"; -import type { DebugProtocol as DAP } from "@vscode/debugprotocol"; -import { JSCClient, type JSC } from "./jsc"; -import { isAbsolute } from "node:path"; - -const capabilities: Required<DAP.Capabilities> = { - /** The debug adapter supports the `configurationDone` request. */ - supportsConfigurationDoneRequest: true, - /** The debug adapter supports function breakpoints. */ - supportsFunctionBreakpoints: true, - /** The debug adapter supports conditional breakpoints. */ - supportsConditionalBreakpoints: true, - /** The debug adapter supports breakpoints that break execution after a specified number of hits. */ - supportsHitConditionalBreakpoints: true, // TODO - /** The debug adapter supports a (side effect free) `evaluate` request for data hovers. */ - supportsEvaluateForHovers: true, - /** Available exception filter options for the `setExceptionBreakpoints` request. */ - exceptionBreakpointFilters: [], - /** The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests. */ - supportsStepBack: false, - /** The debug adapter supports setting a variable to a value. */ - supportsSetVariable: false, // TODO - /** The debug adapter supports restarting a frame. */ - supportsRestartFrame: false, // TODO - /** The debug adapter supports the `gotoTargets` request. */ - supportsGotoTargetsRequest: false, // TODO - /** The debug adapter supports the `stepInTargets` request. */ - supportsStepInTargetsRequest: false, // TODO - /** The debug adapter supports the `completions` request. */ - supportsCompletionsRequest: false, // TODO - /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the `.` character. */ - completionTriggerCharacters: [".", "[", '"', "'"], - /** The debug adapter supports the `modules` request. */ - supportsModulesRequest: true, - /** The set of additional module information exposed by the debug adapter. */ - additionalModuleColumns: [], - /** Checksum algorithms supported by the debug adapter. */ - supportedChecksumAlgorithms: [], - /** The debug adapter supports the `restart` request. In this case a client should not implement `restart` by terminating and relaunching the adapter but by calling the `restart` request. */ - supportsRestartRequest: false, - /** The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request. */ - supportsExceptionOptions: true, - /** The debug adapter supports a `format` attribute on the `stackTrace`, `variables`, and `evaluate` requests. */ - supportsValueFormattingOptions: false, // TODO - /** The debug adapter supports the `exceptionInfo` request. */ - supportsExceptionInfoRequest: true, - /** The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request. */ - supportTerminateDebuggee: true, - /** The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request. */ - supportSuspendDebuggee: false, - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the `startFrame` and `levels` arguments and the `totalFrames` result of the `stackTrace` request are supported. */ - supportsDelayedStackTraceLoading: true, - /** The debug adapter supports the `loadedSources` request. */ - supportsLoadedSourcesRequest: true, - /** The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`. */ - supportsLogPoints: true, - /** The debug adapter supports the `terminateThreads` request. */ - supportsTerminateThreadsRequest: false, - /** The debug adapter supports the `setExpression` request. */ - supportsSetExpression: false, // TODO - /** The debug adapter supports the `terminate` request. */ - supportsTerminateRequest: true, - /** The debug adapter supports data breakpoints. */ - supportsDataBreakpoints: true, - /** The debug adapter supports the `readMemory` request. */ - supportsReadMemoryRequest: false, - /** The debug adapter supports the `writeMemory` request. */ - supportsWriteMemoryRequest: false, - /** The debug adapter supports the `disassemble` request. */ - supportsDisassembleRequest: false, - /** The debug adapter supports the `cancel` request. */ - supportsCancelRequest: false, - /** The debug adapter supports the `breakpointLocations` request. */ - supportsBreakpointLocationsRequest: true, - /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ - supportsClipboardContext: false, // TODO - /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ - supportsSteppingGranularity: false, // TODO - /** The debug adapter supports adding breakpoints based on instruction references. */ - supportsInstructionBreakpoints: true, - /** The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request. */ - supportsExceptionFilterOptions: false, // TODO - /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ - supportsSingleThreadExecutionRequests: false, -}; - -const nodejsCapabilities: DAP.Capabilities = { - supportsConfigurationDoneRequest: true, - supportsFunctionBreakpoints: false, - supportsConditionalBreakpoints: true, - supportsHitConditionalBreakpoints: true, - supportsEvaluateForHovers: true, - supportsReadMemoryRequest: true, - supportsWriteMemoryRequest: true, - exceptionBreakpointFilters: [ - { - filter: "all", - label: "Caught Exceptions", - default: false, - supportsCondition: true, - description: "Breaks on all throw errors, even if they're caught later.", - conditionDescription: `error.name == "MyError"`, - }, - { - filter: "uncaught", - label: "Uncaught Exceptions", - default: false, - supportsCondition: true, - description: "Breaks only on errors or promise rejections that are not handled.", - conditionDescription: `error.name == "MyError"`, - }, - ], - supportsStepBack: false, - supportsSetVariable: true, - supportsRestartFrame: true, - supportsGotoTargetsRequest: false, - supportsStepInTargetsRequest: true, - supportsCompletionsRequest: true, - supportsModulesRequest: false, - additionalModuleColumns: [], - supportedChecksumAlgorithms: [], - supportsRestartRequest: true, - supportsExceptionOptions: false, - supportsValueFormattingOptions: true, - supportsExceptionInfoRequest: true, - supportTerminateDebuggee: true, - supportsDelayedStackTraceLoading: true, - supportsLoadedSourcesRequest: true, - supportsLogPoints: true, - supportsTerminateThreadsRequest: false, - supportsSetExpression: true, - supportsTerminateRequest: false, - completionTriggerCharacters: [".", "[", '"', "'"], - supportsBreakpointLocationsRequest: true, - supportsClipboardContext: true, - supportsExceptionFilterOptions: true, - //supportsEvaluationOptions: extended ? true : false, - //supportsDebuggerProperties: extended ? true : false, - //supportsSetSymbolOptions: extended ? true : false, - //supportsDataBreakpoints: false, - //supportsDisassembleRequest: false, -}; - -export type LaunchRequestArguments = DAP.LaunchRequestArguments & { - program: string; -}; - -export type AttachRequestArguments = DAP.AttachRequestArguments & { - url?: string; - port?: number; -}; - -export class DAPAdapter extends LoggingDebugSession implements Context { - #session: vscode.DebugSession; - #pendingSources = new Map<string, Array<{ resolve: Function; reject: Function }>>(); - #client?: JSCClient; - #process?: ChildProcess; - #thread?: DAP.Thread; - #ready: AbortController; - #sources: Map<string, DAP.Source>; - #scriptIds: Map<number, number>; - #stackFrames: DAP.StackFrame[]; - #scopes: Map<number, DAP.Scope[]>; - #frameIds = new Array<string>(64); - #callFrames = new Array<JSC.Debugger.CallFrame>(64); - #callFramesRange = [0, 0]; - - public constructor(session: vscode.DebugSession) { - super(); - this.#resumePromise = new Promise(resolve => { - this.#resume = resolve; - }); - - this.#ready = new AbortController(); - this.#session = session; - this.#sources = new Map(); - this.#scriptIds = new Map(); - this.#stackFrames = []; - this.#scopes = new Map(); - // 1-based lines and columns - this.setDebuggerLinesStartAt1(true); - this.setDebuggerColumnsStartAt1(false); - } - - #ack<R extends DAP.Response = DAP.Response>(response: R, extra?: Partial<R>["body"]): void { - this.sendResponse({ ...response, body: extra, success: true }); - } - - #nack(response: DAP.Response, error?: unknown): void { - const message = error instanceof Error ? error.message : String(error); - this.sendResponse({ ...response, success: false, message }); - } - - #todo(response: DAP.Response, label: string): void { - this.#nack(response, `TODO: ${label}`); - } - - #noop(response: DAP.Response, label: string): void { - this.#nack(response, `Not supported: ${label}`); - } - - async #send<R extends DAP.Response, T extends keyof JSC.RequestMap>( - response: R, - method: T, - params?: JSC.Request<T>["params"], - callback?: (result: JSC.ResponseMap[T]) => Partial<R["body"]> | void, - ) { - try { - const result = await this.#client.fetch(method, params); - const ack = callback?.(result); - if (ack) { - this.#ack(response, ack); - } else { - this.#ack(response); - } - } catch (error) { - console.error(error); - this.#nack(response, error); - } - } - - getReferenceId(objectId: string): number { - try { - const { injectedScriptId, id } = JSON.parse(objectId); - const referenceId = Number(`${injectedScriptId}${id}`); - if (isNaN(referenceId)) { - throw new Error(); - } - return referenceId; - } catch { - return hashCode(objectId); - } - } - - getObjectId(referenceId: number): string { - const objectId = String(referenceId); - try { - const injectedScriptId = Number(objectId.slice(0, 1)); - const id = Number(objectId.slice(1)); - return JSON.stringify({ injectedScriptId, id }); - } catch { - return objectId; - } - } - - getStackFrameId(callFrameId: string): number { - try { - const { injectedScriptId, ordinal } = JSON.parse(callFrameId); - const frameId = Number(`${injectedScriptId}${ordinal}`); - if (isNaN(frameId)) { - throw new Error(); - } - return frameId; - } catch { - return hashCode(callFrameId); - } - } - - getCallFrameId(stackFrameId: number): string { - const objectId = String(stackFrameId); - try { - const injectedScriptId = Number(objectId.slice(0, 1)); - const ordinal = Number(objectId.slice(1)); - return JSON.stringify({ injectedScriptId, ordinal }); - } catch { - return objectId; - } - } - - getSource(scriptId: string): DAP.Source | undefined { - return this.#sources.get(scriptId); - } - - getModuleId(scriptId: string): number | undefined { - return undefined; // TODO - } - - async getProperties(objectId: string): Promise<JSC.Runtime.PropertyDescriptor[]> { - const { properties } = await this.#client.fetch("Runtime.getDisplayableProperties", { - objectId, - }); - let hasEntries = false; - for (const { name } of properties) { - if (name === "entries") { - hasEntries = true; - } - // HACK: Do not call on arrays, as it appears to error the debugger. - // Internal error [code: -32000] - if (name === "at") { - hasEntries = false; - break; - } - } - if (!hasEntries) { - return properties; - } - const { entries } = await this.#client.fetch("Runtime.getCollectionEntries", { - objectId, - }); - const results: JSC.Runtime.PropertyDescriptor[] = [...properties.reverse()]; - for (let i = entries.length - 1; i >= 0; i--) { - const { key, value } = entries[i]; - results.push({ - name: key?.description ?? `${i}`, - value, - }); - } - return results.reverse(); - } - - protected onEvent(event: JSC.Event): void { - console.log(Date.now(), "JSC Event:", event); - const { method, params } = event; - this[method]?.(params); - } - - protected ["Debugger.scriptParsed"](event: JSC.Debugger.ScriptParsedEvent): void { - let { sourceURL, url, scriptId } = event; - - if (!url) { - url = sourceURL; - } - - if (!url) { - return; // If the script has no URL, it is an `eval` command. - } - - const name = vscode.workspace.asRelativePath(url); - const scriptIdNumber = scriptIdFromEvent(scriptId); - const source = new Source(name, url, scriptIdNumber); - source.sourceReference = scriptIdNumber; - this.#sources.set(scriptId, source); - this.#scriptIds.set(hashCode(url), scriptIdNumber); - this.sendEvent(new LoadedSourceEvent("new", source)); - const pendingMap = this.#pendingSources; - const promises = pendingMap.get(url); - if (promises) { - pendingMap.delete(url); - for (const { resolve } of promises) { - resolve(); - } - } - - if (!this.#thread) { - this.#thread = new Thread(0, url); - this.sendEvent(new ThreadEvent("started", 0)); - } - } - - protected ["Debugger.paused"](event: JSC.Debugger.PausedEvent): void { - const { reason, callFrames, asyncStackTrace } = event; - - this.sendEvent(new StoppedEvent(pauseReason(reason), this.#thread?.id ?? 0)); - const stackFrames: DAP.StackFrame[] = []; - const scopes: Map<number, DAP.Scope[]> = new Map(); - const frameIds = this.#frameIds; - frameIds.length = - callFrames.length + - (asyncStackTrace?.callFrames?.length || 0) + - (asyncStackTrace?.parentStackTrace?.callFrames?.length || 0); - const frames = this.#callFrames; - frames.length = callFrames.length; - let frameId = 0; - for (const callFrame of callFrames) { - const stackFrame = formatStackFrame(this, callFrame); - frames[frameId] = callFrame; - frameIds[frameId++] = callFrame.callFrameId; - stackFrames.push(stackFrame); - const frameScopes: DAP.Scope[] = []; - for (const scope of callFrame.scopeChain) { - frameScopes.push(...formatScope(this, scope)); - } - scopes.set(stackFrame.id, frameScopes); - } - - if (asyncStackTrace?.callFrames?.length) { - for (const callFrame of asyncStackTrace.callFrames) { - frameIds[frameId++] = ""; - stackFrames.push(formatAsyncStackFrame(this, callFrame)); - } - } - - if (asyncStackTrace?.parentStackTrace?.callFrames?.length) { - for (const callFrame of asyncStackTrace.parentStackTrace.callFrames) { - frameIds[frameId++] = ""; - stackFrames.push(formatAsyncStackFrame(this, callFrame)); - } - } - this.#scopes = scopes; - this.#stackFrames = stackFrames; - } - - protected ["Debugger.resumed"](event: JSC.Debugger.ResumedEvent): void { - this.#frameIds.length = 0; - this.#callFrames.length = 0; - this.sendEvent(new ContinuedEvent(this.#thread?.id)); - } - - handleMessage(message: DAP.ProtocolMessage): void { - console.log(Date.now(), "DAP Request:", message); - super.handleMessage(message); - } - - sendResponse(response: DAP.Response): void { - console.log(Date.now(), "DAP Response:", response); - super.sendResponse(response); - } - - sendEvent(event: DAP.Event): void { - console.log(Date.now(), "DAP Event:", event); - super.sendEvent(event); - } - - runInTerminalRequest( - args: DAP.RunInTerminalRequestArguments, - timeout: number, - cb: (response: DAP.RunInTerminalResponse) => void, - ): void { - // TODO - } - - protected initializeRequest(response: DAP.InitializeResponse, args: DAP.InitializeRequestArguments): void { - this.#ack(response, nodejsCapabilities); - this.sendEvent(new InitializedEvent()); - } - - protected async disconnectRequest( - response: DAP.DisconnectResponse, - args: DAP.DisconnectArguments, - request?: DAP.Request, - ): Promise<void> { - await this.#client?.fetch("Debugger.disable"); - const { terminateDebuggee } = args; - if (terminateDebuggee) { - this.#process?.kill(); - } - await this.#ack(response); - } - - async #launch(path: string): Promise<void> { - this.#process?.kill(); - const url = "localhost:9232"; - // // TODO: Change to "bun" before merging, or make it configurable - // const process = spawn("bun-debug", ["--inspect=" + url, "run", path], { - // cwd: this.#session.workspaceFolder?.uri?.fsPath, - // stdio: ["ignore", "pipe", "pipe"], - // env: { - // ...globalThis.process.env, - // NODE_ENV: "development", - // BUN_DEBUG_QUIET_LOGS: "1", - // FORCE_COLOR: "1", - // }, - // }); - // let resolve, - // reject, - // promise = new Promise<void>((res, rej) => { - // resolve = res; - // reject = rej; - // }); - // process.on("error", error => { - // console.error(error); - - // vscode.window.showErrorMessage(`Failed to start Bun: ${error.message}`); - // this.sendEvent(new ExitedEvent(-1)); - // this.#process = undefined; - // reject(error); - // }); - // process.on("exit", exitCode => { - // this.sendEvent(new ExitedEvent(exitCode)); - // this.#process = undefined; - // }); - // process.stdout.on("data", (data: Buffer) => { - // console.log(data); - // this.sendEvent(new OutputEvent(data.toString(), "stdout")); - // }); - // process.stderr.on("data", (data: Buffer) => { - // console.error(data); - // this.sendEvent(new OutputEvent(data.toString(), "stderr")); - // }); - // this.#process = process; - - // process.once("spawn", () => { - // this.#attach(url).then(resolve, reject); - // }); - return this.#attach(url); - } - - async #attach(url: string): Promise<void> { - this.#client?.close(); - const client = new JSCClient({ - url, - onEvent: this.onEvent.bind(this), - onResponse(response) { - console.log(Date.now(), "JSC Response:", response); - }, - onRequest(request) { - console.log(Date.now(), "JSC Request:", request); - }, - onError(error) { - console.error("JSC Error:", error); - }, - onClose(code, reason) { - console.log(Date.now(), "JSC Close:", code, reason); - this.#process?.kill(); - }, - }); - this.#client = client; - globalThis.jsc = client; - await client.ready; - await Promise.all([ - client.fetch("Runtime.enable"), - client.fetch("Console.enable"), - client.fetch("Debugger.enable"), - client.fetch("Debugger.setAsyncStackTraceDepth", { depth: 100 }), - client.fetch("Debugger.setPauseOnDebuggerStatements", { enabled: true }), - client.fetch("Debugger.setBreakpointsActive", { active: true }), - ]); - } - - protected async launchRequest( - response: DAP.LaunchResponse, - args: LaunchRequestArguments, - request?: DAP.Request, - ): Promise<void> { - await new Promise<void>(resolve => { - if (this.#ready.signal.aborted) { - resolve(); - return; - } - this.#ready.signal.addEventListener("abort", () => { - resolve(); - }); - }); - const { program } = args; - try { - await this.#launch(program); - this.#ack(response); - } catch (error) { - this.#nack(response, error); - } - } - - #resume = undefined; - #resumePromise = undefined; - - protected async attachRequest( - response: DAP.AttachResponse, - args: AttachRequestArguments, - request?: DAP.Request, - ): Promise<void> { - console.log("Attach!"); - const { url, port } = args; - try { - if (url) { - await this.#attach(url); - } else if (port) { - await this.#attach(`localhost:${port}`); - } else { - await this.#attach("localhost:9229"); - } - this.#resume(); - this.#ack(response); - } catch (error) { - this.#nack(response, error); - } - } - - protected terminateRequest( - response: DAP.TerminateResponse, - args: DAP.TerminateArguments, - request?: DAP.Request, - ): void { - this.#client?.close(); - this.sendEvent(new TerminatedEvent()); - this.#ack(response); - } - - protected restartRequest(response: DAP.RestartResponse, args: DAP.RestartArguments, request?: DAP.Request): void { - this.#noop(response, "restartRequest"); - } - - getScriptIdFromSource(source: DAP.Source): string { - if (source.sourceReference) { - return String(source.sourceReference); - } - - // @ts-expect-error - if (source.sourceReferenceInternal) { - // @ts-expect-error - return String(source.sourceReferenceInternal); - } - - const { path } = source; - const id = this.#scriptIds.get(hashCode(path)); - if (!id) { - return undefined; - } - - return String(id); - } - - async waitForSourceToBeLoaded(source: DAP.Source): Promise<void> { - const pendingMap = this.#pendingSources; - let promises = pendingMap.get(source.path); - if (!promises) { - promises = []; - pendingMap.set(source.path, promises); - } - - await new Promise<void>((resolve, reject) => { - promises.push({ resolve, reject }); - }); - } - - protected async setBreakPointsRequest( - response: DAP.SetBreakpointsResponse, - args: DAP.SetBreakpointsArguments, - request?: DAP.Request, - ): Promise<void> { - if (!args.breakpoints?.length) { - this.#nack(response, "No breakpoints"); - return; - } - if (!this.#client) { - await this.#resumePromise; - } - - const { source, breakpoints } = args; - let scriptId = this.getScriptIdFromSource(source); - - if (!scriptId) { - await this.waitForSourceToBeLoaded(source); - scriptId = this.getScriptIdFromSource(source); - } - - const results: DAP.Breakpoint[] = await Promise.all( - breakpoints.map(({ line, column }) => - this.#client - .fetch("Debugger.setBreakpoint", { - location: { - scriptId, - lineNumber: line, - columnNumber: column, - }, - }) - .then( - ({ breakpointId, actualLocation }) => { - return { - id: Number(breakpointId), - line: actualLocation.lineNumber, - // column: actualLocation.columnNumber, - verified: true, - }; - }, - err => { - if (err?.code === -32000) { - return undefined; - } - - throw err; - }, - ), - ), - ); - this.#ack(response, { breakpoints: results.filter(Boolean) }); - } - - protected setFunctionBreakPointsRequest( - response: DAP.SetFunctionBreakpointsResponse, - args: DAP.SetFunctionBreakpointsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setFunctionBreakPointsRequest"); - } - - protected setExceptionBreakPointsRequest( - response: DAP.SetExceptionBreakpointsResponse, - args: DAP.SetExceptionBreakpointsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setExceptionBreakPointsRequest"); - } - - protected configurationDoneRequest( - response: DAP.ConfigurationDoneResponse, - args: DAP.ConfigurationDoneArguments, - request?: DAP.Request, - ): void { - super.configurationDoneRequest(response, args, request); - this.#ready.abort(); - // this.#ack(response); - } - - protected async continueRequest( - response: DAP.ContinueResponse, - args: DAP.ContinueArguments, - request?: DAP.Request, - ): Promise<void> { - await this.#send(response, "Debugger.resume"); - } - - protected async nextRequest( - response: DAP.NextResponse, - args: DAP.NextArguments, - request?: DAP.Request, - ): Promise<void> { - await this.#send(response, "Debugger.stepNext"); - } - - protected async stepInRequest( - response: DAP.StepInResponse, - args: DAP.StepInArguments, - request?: DAP.Request, - ): Promise<void> { - await this.#send(response, "Debugger.stepInto"); - } - - protected async stepOutRequest( - response: DAP.StepOutResponse, - args: DAP.StepOutArguments, - request?: DAP.Request, - ): Promise<void> { - await this.#send(response, "Debugger.stepOut"); - } - - protected stepBackRequest(response: DAP.StepBackResponse, args: DAP.StepBackArguments, request?: DAP.Request): void { - this.#todo(response, "stepBackRequest"); - } - - protected reverseContinueRequest( - response: DAP.ReverseContinueResponse, - args: DAP.ReverseContinueArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "reverseContinueRequest"); - } - - protected restartFrameRequest( - response: DAP.RestartFrameResponse, - args: DAP.RestartFrameArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "restartFrameRequest"); - } - - protected gotoRequest(response: DAP.GotoResponse, args: DAP.GotoArguments, request?: DAP.Request): void { - this.#todo(response, "gotoRequest"); - } - - protected pauseRequest(response: DAP.PauseResponse, args: DAP.PauseArguments, request?: DAP.Request): void { - this.#send(response, "Debugger.pause"); - } - - protected async sourceRequest( - response: DAP.SourceResponse, - args: DAP.SourceArguments, - request?: DAP.Request, - ): Promise<void> { - const { sourceReference, source } = args; - const scriptId = sourceReference ? String(sourceReference) : this.getScriptIdFromSource(source); - - await this.#send(response, "Debugger.getScriptSource", { scriptId }, ({ scriptSource }) => ({ - content: scriptSource, - })); - } - - protected threadsRequest(response: DAP.ThreadsResponse, request?: DAP.Request): void { - if (this.#thread) { - this.#ack(response, { threads: [this.#thread] }); - } else { - this.#ack(response, { threads: [] }); - } - } - - protected terminateThreadsRequest( - response: DAP.TerminateThreadsResponse, - args: DAP.TerminateThreadsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "terminateThreadsRequest"); - } - - protected stackTraceRequest( - response: DAP.StackTraceResponse, - args: DAP.StackTraceArguments, - request?: DAP.Request, - ): void { - const totalFrames = this.#stackFrames.length; - const { startFrame = 0, levels = totalFrames } = args; - const stackFrames = this.#stackFrames.slice( - (this.#callFramesRange[0] = startFrame), - (this.#callFramesRange[1] = Math.min(totalFrames, startFrame + levels)), - ); - - this.#ack(response, { - stackFrames, - totalFrames, - }); - } - - protected scopesRequest(response: DAP.ScopesResponse, args: DAP.ScopesArguments, request?: DAP.Request): void { - const { frameId } = args; - const scopes = this.#scopes.get(frameId) ?? []; - this.#ack(response, { scopes }); - } - - protected async variablesRequest( - response: DAP.VariablesResponse, - args: DAP.VariablesArguments, - request?: DAP.Request, - ): Promise<void> { - const { variablesReference } = args; - const objectId = this.getObjectId(variablesReference); - try { - const variables = await formatObject(this, objectId); - this.#ack(response, { variables }); - } catch (error) { - this.#nack(response, error); - } - } - - protected setVariableRequest( - response: DAP.SetVariableResponse, - args: DAP.SetVariableArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setVariableRequest"); - } - - protected setExpressionRequest( - response: DAP.SetExpressionResponse, - args: DAP.SetExpressionArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setExpressionRequest"); - } - - protected async evaluateRequest( - response: DAP.EvaluateResponse, - args: DAP.EvaluateArguments, - request?: DAP.Request, - ): Promise<void> { - const { context, expression, frameId } = args; - let callFrame: JSC.Debugger.CallFrame = - typeof frameId === "number" - ? this.#callFrames[this.#stackFrames.findIndex(frame => frame.id === frameId)] - : undefined; - - if (callFrame && context === "hover") { - if (expression.includes(".")) { - // TODO: use getDisplayableProperties to make this side-effect free. - // for each ".", call Runtime.getProperties on the previous result and find the specific property - await this.#send( - response, - "Debugger.evaluateOnCallFrame", - { - expression, - callFrameId: callFrame.callFrameId, - "includeCommandLineAPI": false, - }, - ({ result: { objectId, value, description }, wasThrown }) => { - return { - result: value ?? description, - variablesReference: objectId ? this.getReferenceId(objectId) : 0, - }; - }, - ); - } else { - for (let scope of callFrame.scopeChain) { - if (scope.empty || scope.type === "with" || scope.type === "global") { - continue; - } - - // For the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. - const { properties } = await this.#client.fetch("Runtime.getDisplayableProperties", { - "objectId": scope.object.objectId, - "fetchStart": 0, - "fetchCount": 100, - }); - - for (let i = 0; i < properties.length; i++) { - const prop = properties[i]; - const { name, value } = prop; - if (name === expression) { - if (value) { - const { objectId } = value; - response.body = { - result: value?.value || value?.description || "", - variablesReference: objectId ? this.getReferenceId(objectId) : 0, - type: value?.type || "undefined", - presentationHint: presentationHintForProperty(prop, !!value.classPrototype), - }; - } else { - response.body = { - result: "undefined", - variablesReference: 0, - type: "undefined", - }; - } - response.success = true; - this.sendResponse(response); - return; - } - } - } - } - } else { - await this.#send( - response, - "Runtime.evaluate", - { - expression, - includeCommandLineAPI: true, - }, - ({ result: { objectId, value, description }, wasThrown }) => { - return { - result: value ?? description, - variablesReference: objectId ? this.getReferenceId(objectId) : 0, - }; - }, - ); - } - } - - protected stepInTargetsRequest( - response: DAP.StepInTargetsResponse, - args: DAP.StepInTargetsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "stepInTargetsRequest"); - } - - protected gotoTargetsRequest( - response: DAP.GotoTargetsResponse, - args: DAP.GotoTargetsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "gotoTargetsRequest"); - } - - protected completionsRequest( - response: DAP.CompletionsResponse, - args: DAP.CompletionsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "completionsRequest"); - } - - protected exceptionInfoRequest( - response: DAP.ExceptionInfoResponse, - args: DAP.ExceptionInfoArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "exceptionInfoRequest"); - } - - protected loadedSourcesRequest( - response: DAP.LoadedSourcesResponse, - args: DAP.LoadedSourcesArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "loadedSourcesRequest"); - } - - protected dataBreakpointInfoRequest( - response: DAP.DataBreakpointInfoResponse, - args: DAP.DataBreakpointInfoArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "dataBreakpointInfoRequest"); - } - - protected setDataBreakpointsRequest( - response: DAP.SetDataBreakpointsResponse, - args: DAP.SetDataBreakpointsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setDataBreakpointsRequest"); - } - - protected readMemoryRequest( - response: DAP.ReadMemoryResponse, - args: DAP.ReadMemoryArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "readMemoryRequest"); - } - - protected writeMemoryRequest( - response: DAP.WriteMemoryResponse, - args: DAP.WriteMemoryArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "writeMemoryRequest"); - } - - protected disassembleRequest( - response: DAP.DisassembleResponse, - args: DAP.DisassembleArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "disassembleRequest"); - } - - protected cancelRequest(response: DAP.CancelResponse, args: DAP.CancelArguments, request?: DAP.Request): void { - this.#todo(response, "cancelRequest"); - } - - protected async breakpointLocationsRequest( - response: DAP.BreakpointLocationsResponse, - args: DAP.BreakpointLocationsArguments, - request?: DAP.Request, - ): Promise<void> { - const { line, endLine, column, endColumn, source } = args; - console.log(source); - let scriptId: string = this.getScriptIdFromSource(source); - if (!scriptId) { - await this.waitForSourceToBeLoaded(source); - scriptId = this.getScriptIdFromSource(source); - } - - if (!scriptId) { - this.#nack(response, new Error("Either source.path or source.sourceReference must be specified")); - return; - } - - await this.#send( - response, - "Debugger.getBreakpointLocations", - { - start: { - scriptId, - lineNumber: line, - columnNumber: column, - }, - end: { - scriptId, - lineNumber: endLine ?? line + 1, - columnNumber: endColumn, - }, - }, - ({ locations }) => { - return { - breakpoints: locations.map(({ lineNumber, columnNumber }) => ({ - line: lineNumber, - column: columnNumber, - })), - }; - }, - ); - } - - protected setInstructionBreakpointsRequest( - response: DAP.SetInstructionBreakpointsResponse, - args: DAP.SetInstructionBreakpointsArguments, - request?: DAP.Request, - ): void { - this.#todo(response, "setInstructionBreakpointsRequest"); - } - - protected customRequest(command: string, response: DAP.Response, args: any, request?: DAP.Request): void { - super.customRequest(command, response, args, request); - } - - protected convertClientLineToDebugger(line: number): number { - return line; - } - - protected convertDebuggerLineToClient(line: number): number { - return line; - } - - protected convertClientColumnToDebugger(column: number): number { - return column; - } - - protected convertDebuggerColumnToClient(column: number): number { - return column; - } - - protected convertClientPathToDebugger(clientPath: string): string { - return clientPath; - } - - protected convertDebuggerPathToClient(debuggerPath: string): string { - return debuggerPath; - } -} - -function hashCode(string: string): number { - let hash = 0, - i, - chr; - if (string.length === 0) return hash; - for (i = 0; i < string.length; i++) { - chr = string.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; -} - -interface Context { - getReferenceId(objectId: string): number; - getObjectId(referenceId: number): string; - getStackFrameId(callFrameId: string): number; - getCallFrameId(stackFrameId: number): string; - getSource(scriptId: string): DAP.Source | undefined; - getModuleId(scriptId: string): number | undefined; - getProperties(objectId: string): Promise<JSC.Runtime.PropertyDescriptor[]>; -} - -function formatStackFrame(ctx: Context, callFrame: JSC.Debugger.CallFrame): DAP.StackFrame { - const { callFrameId, functionName, location } = callFrame; - const { scriptId, lineNumber, columnNumber = 0 } = location; - const source = ctx.getSource(scriptId); - return { - id: ctx.getStackFrameId(callFrameId), - name: functionName || "<anonymous>", - line: lineNumber, - column: columnNumber, - source, - moduleId: ctx.getModuleId(scriptId), - presentationHint: source?.presentationHint || !source?.path ? "subtle" : "normal", - }; -} - -function formatAsyncStackFrame(ctx: Context, callFrame: JSC.Console.CallFrame): DAP.StackFrame { - const { functionName, scriptId, lineNumber, columnNumber } = callFrame; - return { - id: hashCode(functionName + "-" + scriptId + "-" + lineNumber + "-" + columnNumber), - name: functionName || "<anonymous>", - line: lineNumber, - column: columnNumber, - source: ctx.getSource(scriptId), - moduleId: scriptId, - }; -} - -function formatScope(ctx: Context, scope: JSC.Debugger.Scope): DAP.Scope[] { - const { name, type, location, object, empty } = scope; - if (empty) { - return []; - } - const presentationHint = formatScopeHint(type); - const title = presentationHint.charAt(0).toUpperCase() + presentationHint.slice(1); - const displayName = name ? `${title}: ${name}` : title; - return [ - { - name: displayName, - presentationHint, - expensive: presentationHint === "globals", - variablesReference: object ? ctx.getReferenceId(object.objectId) : 0, - line: location?.lineNumber, - column: location?.columnNumber, - source: location && ctx.getSource(location.scriptId), - }, - ]; -} - -function formatScopeHint(type: JSC.Debugger.Scope["type"]): "arguments" | "locals" | "globals" | "" { - switch (type) { - case "closure": - return "locals"; // ? - case "functionName": - case "with": - case "catch": - case "nestedLexical": - return "locals"; - case "global": - case "globalLexicalEnvironment": - return "globals"; - default: - return ""; - } -} - -async function formatObject(ctx: Context, objectId: JSC.Runtime.RemoteObjectId): Promise<DAP.Variable[]> { - const properties = await ctx.getProperties(objectId); - return properties.flatMap(property => formatProperty(ctx, property)); -} - -function formatProperty(ctx: Context, propertyDescriptor: JSC.Runtime.PropertyDescriptor): DAP.Variable[] { - const { name, value, get, set, symbol, isOwn } = propertyDescriptor; - const variables: DAP.Variable[] = []; - if (value) { - variables.push(formatPropertyValue(ctx, name, value, propertyDescriptor)); - } - return variables; -} - -function formatPropertyValue( - ctx: Context, - name: string, - remoteObject: JSC.Runtime.RemoteObject, - descriptor: JSC.Runtime.PropertyDescriptor, -): DAP.Variable { - const { type, subtype, value, description, objectId } = remoteObject; - return { - name, - value: description ?? "", - type: subtype ?? type, - variablesReference: objectId ? ctx.getReferenceId(objectId) : 0, - presentationHint: - value && typeof value !== "object" && typeof value !== "undefined" - ? formatPropertyHint(descriptor, typeof value) - : formatPropertyHint(value || descriptor, ""), - }; -} - -function formatPropertyHint( - propertyDescriptor: JSC.Runtime.PropertyDescriptor, - valueType: string, -): DAP.VariablePresentationHint { - const { value, get, set, configurable, enumerable, writable, isOwn } = propertyDescriptor; - const hasGetter = get?.type !== "undefined"; - const hasSetter = set?.type !== "undefined"; - const hint: DAP.VariablePresentationHint = { - kind: (value && formatPropertyKind(value)) ?? "property", - attributes: [], - visibility: "public", - }; - if (!writable && !hasSetter && hasGetter) { - hint.attributes.push("readOnly"); - } - if (!enumerable && !hasGetter) { - hint.visibility = "internal"; - } - - if (valueType) { - hint.kind = "data"; - } - - return hint; -} - -function formatPropertyKind(remoteObject: JSC.Runtime.RemoteObject): DAP.VariablePresentationHint["kind"] { - const { type, subtype, className, value } = remoteObject; - if (type === "function") { - return "method"; - } - if (subtype === "class") { - return "class"; - } - if (className?.endsWith("Event")) { - return "event"; - } - if (value) { - return "data"; - } - - return "property"; -} - -function scriptIdFromLocation(location: JSC.Debugger.Location): number { - const id = Number(location.scriptId); - console.log({ id }); - return id; -} - -function pauseReason(reason: JSC.EventMap["Debugger.paused"]["reason"]): DAP.StoppedEvent["body"]["reason"] { - return reason; -} - -function scriptIdFromEvent(scriptId: JSC.Debugger.ScriptParsedEvent["scriptId"]): number { - return Number(scriptId); -} - -function presentationHintForProperty(property: JSC.Runtime.PropertyDescriptor, isClass): DAP.VariablePresentationHint { - const attributes = []; - if (!property.writable) { - attributes.push("readOnly"); - } - - let kind = ""; - if (isClass) { - kind = "class"; - } else if (property.get || property.set) { - kind = "property"; - } else if (property.value) { - kind = "data"; - } - - return { - attributes, - visibility: property.isPrivate || property.symbol || !property.enumerable ? "private" : "public", - kind, - }; -} diff --git a/packages/bun-vscode/src/extension.ts b/packages/bun-vscode/src/extension.ts index f840dd7a9..e333aedd7 100644 --- a/packages/bun-vscode/src/extension.ts +++ b/packages/bun-vscode/src/extension.ts @@ -1,16 +1,10 @@ import * as vscode from "vscode"; -import { activateBunDebug } from "./activate"; - -const runMode: "external" | "server" | "namedPipeServer" | "inline" = "inline"; +import activateLockfile from "./features/lockfile"; +import activateDebug from "./features/debug"; export function activate(context: vscode.ExtensionContext) { - if (runMode === "inline") { - activateBunDebug(context); - return; - } - throw new Error(`This extension does not support '${runMode}' mode.`); + activateLockfile(context); + activateDebug(context); } -export function deactivate() { - // No-op -} +export function deactivate() {} diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts new file mode 100644 index 000000000..3b841ea66 --- /dev/null +++ b/packages/bun-vscode/src/features/debug.ts @@ -0,0 +1,153 @@ +import * as vscode from "vscode"; +import type { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode"; +import type { DAP } from "../../../bun-debug-adapter-protocol"; +import { DebugAdapter } from "../../../bun-debug-adapter-protocol"; +import { DebugSession } from "@vscode/debugadapter"; + +const debugConfiguration: vscode.DebugConfiguration = { + type: "bun", + request: "launch", + name: "Debug Bun", + program: "${file}", + watch: true, +}; + +const runConfiguration: vscode.DebugConfiguration = { + type: "bun", + request: "launch", + name: "Run Bun", + program: "${file}", + watch: true, +}; + +const attachConfiguration: vscode.DebugConfiguration = { + type: "bun", + request: "attach", + name: "Attach to Bun", + url: "ws://localhost:6499/", +}; + +const debugConfigurations: vscode.DebugConfiguration[] = [debugConfiguration, attachConfiguration]; + +export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) { + context.subscriptions.push( + vscode.commands.registerCommand("extension.bun.runFile", (resource: vscode.Uri) => { + let targetResource = resource; + if (!targetResource && vscode.window.activeTextEditor) { + targetResource = vscode.window.activeTextEditor.document.uri; + } + if (targetResource) { + vscode.debug.startDebugging(undefined, runConfiguration, { + noDebug: true, + }); + } + }), + vscode.commands.registerCommand("extension.bun.debugFile", (resource: vscode.Uri) => { + let targetResource = resource; + if (!targetResource && vscode.window.activeTextEditor) { + targetResource = vscode.window.activeTextEditor.document.uri; + } + if (targetResource) { + vscode.debug.startDebugging(undefined, { + ...debugConfiguration, + program: targetResource.fsPath, + }); + } + }), + ); + + const provider = new BunConfigurationProvider(); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider)); + + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider( + "bun", + { + provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> { + return debugConfigurations; + }, + }, + vscode.DebugConfigurationProviderTriggerKind.Dynamic, + ), + ); + + if (!factory) { + factory = new InlineDebugAdapterFactory(); + } + context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory)); + if ("dispose" in factory && typeof factory.dispose === "function") { + // @ts-ignore + context.subscriptions.push(factory); + } +} + +class BunConfigurationProvider implements vscode.DebugConfigurationProvider { + resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + config: DebugConfiguration, + token?: CancellationToken, + ): ProviderResult<DebugConfiguration> { + if (!config.type && !config.request && !config.name) { + const editor = vscode.window.activeTextEditor; + if (editor && isJavaScript(editor.document.languageId)) { + Object.assign(config, debugConfiguration); + } + } + return config; + } +} + +class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { + createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> { + const adapter = new VSCodeAdapter(_session); + return new vscode.DebugAdapterInlineImplementation(adapter); + } +} + +function isJavaScript(languageId: string): boolean { + return ( + languageId === "javascript" || + languageId === "javascriptreact" || + languageId === "typescript" || + languageId === "typescriptreact" + ); +} + +export class VSCodeAdapter extends DebugSession { + #adapter: DebugAdapter; + #dap: vscode.OutputChannel; + + constructor(session: vscode.DebugSession) { + super(); + this.#dap = vscode.window.createOutputChannel("Debug Adapter Protocol"); + this.#adapter = new DebugAdapter({ + sendToAdapter: this.sendMessage.bind(this), + }); + } + + sendMessage(message: DAP.Request | DAP.Response | DAP.Event): void { + console.log("[dap] -->", message); + this.#dap.appendLine("--> " + JSON.stringify(message)); + + const { type } = message; + if (type === "response") { + this.sendResponse(message); + } else if (type === "event") { + this.sendEvent(message); + } else { + throw new Error(`Not supported: ${type}`); + } + } + + handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void { + console.log("[dap] <--", message); + this.#dap.appendLine("<-- " + JSON.stringify(message)); + + this.#adapter.accept(message); + } + + dispose() { + this.#adapter.close(); + this.#dap.dispose(); + } +} diff --git a/packages/bun-vscode/src/lockfile.ts b/packages/bun-vscode/src/features/lockfile.ts index 22b48faba..81adf5b9e 100644 --- a/packages/bun-vscode/src/lockfile.ts +++ b/packages/bun-vscode/src/features/lockfile.ts @@ -52,10 +52,10 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro process.stderr.on("data", (data: Buffer) => { stderr += data.toString(); }); - process.on("error", (error) => { + process.on("error", error => { reject(error); }); - process.on("exit", (code) => { + process.on("exit", code => { if (code === 0) { resolve(stdout); } else { @@ -65,19 +65,15 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro }); } -export default function(context: vscode.ExtensionContext): void { +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, - }, + + vscode.window.registerCustomEditorProvider(viewType, provider, { + supportsMultipleEditorsPerDocument: true, + webviewOptions: { + enableFindWidget: true, + retainContextWhenHidden: true, }, - ); + }); } diff --git a/packages/bun-vscode/src/jsc.ts b/packages/bun-vscode/src/jsc.ts deleted file mode 100644 index 5b8d4ed84..000000000 --- a/packages/bun-vscode/src/jsc.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Socket, createConnection } from "node:net"; -import { inspect } from "node:util"; -import type { JSC } from "../types/jsc"; -export type { JSC }; - -export type JSCClientOptions = { - url: string | URL; - retry?: boolean; - onEvent?: (event: JSC.Event) => void; - onRequest?: (request: JSC.Request) => void; - onResponse?: (response: JSC.Response) => void; - onError?: (error: Error) => void; - onClose?: (code: number, reason: string) => void; -}; -const headerInvalidNumber = 2147483646; - -// We use non-printable characters to separate messages in the stream. -// These should never appear in textual messages. - -// These are non-sequential so that code which just counts up from 0 doesn't accidentally parse them as messages. -// 0x12 0x11 0x13 0x14 as a little-endian 32-bit unsigned integer -const headerPrefix = "\x14\x13\x11\x12"; - -// 0x14 0x12 0x13 0x11 as a little-endian 32-bit unsigned integer -const headerSuffixString = "\x11\x13\x12\x14"; - -const headerSuffixInt = Buffer.from(headerSuffixString).readInt32LE(0); -const headerPrefixInt = Buffer.from(headerPrefix).readInt32LE(0); - -const messageLengthBuffer = new ArrayBuffer(12); -const messageLengthDataView = new DataView(messageLengthBuffer); -messageLengthDataView.setInt32(0, headerPrefixInt, true); -messageLengthDataView.setInt32(8, headerSuffixInt, true); - -function writeJSONMessageToBuffer(message: any) { - const asString = JSON.stringify(message); - const byteLength = Buffer.byteLength(asString, "utf8"); - const buffer = Buffer.allocUnsafe(12 + byteLength); - buffer.writeInt32LE(headerPrefixInt, 0); - buffer.writeInt32LE(byteLength, 4); - buffer.writeInt32LE(headerSuffixInt, 8); - if (buffer.write(asString, 12, byteLength, "utf8") !== byteLength) { - throw new Error("Failed to write message to buffer"); - } - - return buffer; -} - -let currentMessageLength = 0; -const DEBUGGING = true; -function extractMessageLengthAndOffsetFromBytes(buffer: Buffer, offset: number) { - const bufferLength = buffer.length; - while (offset < bufferLength) { - const headerStart = buffer.indexOf(headerPrefix, offset, "binary"); - if (headerStart === -1) { - if (DEBUGGING) { - console.error("No header found in buffer of length " + bufferLength + " starting at offset " + offset); - } - return headerInvalidNumber; - } - - // [headerPrefix (4), byteLength (4), headerSuffix (4)] - if (bufferLength <= headerStart + 12) { - if (DEBUGGING) { - console.error( - "Not enough bytes for header in buffer of length " + bufferLength + " starting at offset " + offset, - ); - } - return headerInvalidNumber; - } - - const prefix = buffer.readInt32LE(headerStart); - const byteLengthInt = buffer.readInt32LE(headerStart + 4); - const suffix = buffer.readInt32LE(headerStart + 8); - - if (prefix !== headerPrefixInt || suffix !== headerSuffixInt) { - offset = headerStart + 1; - currentMessageLength = 0; - - if (DEBUGGING) { - console.error( - "Invalid header in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix, - byteLengthInt, - suffix, - ); - } - continue; - } - - if (byteLengthInt < 0) { - if (DEBUGGING) { - console.error( - "Invalid byteLength in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix, - byteLengthInt, - suffix, - ); - } - - return headerInvalidNumber; - } - - if (byteLengthInt === 0) { - // Ignore 0-length messages - // Shouldn't happen in practice - offset = headerStart + 12; - currentMessageLength = 0; - - if (DEBUGGING) { - console.error( - "Ignoring 0-length message in buffer of length " + bufferLength + " starting at offset " + offset, - ); - console.error({ - buffer: buffer, - string: buffer.toString(), - }); - } - - continue; - } - - currentMessageLength = byteLengthInt; - - return headerStart + 12; - } - - if (DEBUGGING) { - if (bufferLength > 0) - console.error("Header not found in buffer of length " + bufferLength + " starting at offset " + offset); - } - - return headerInvalidNumber; -} - -class StreamingReader { - pendingBuffer: Buffer; - - constructor() { - this.pendingBuffer = Buffer.alloc(0); - } - - *onMessage(chunk: Buffer) { - let buffer: Buffer; - if (this.pendingBuffer.length > 0) { - this.pendingBuffer = buffer = Buffer.concat([this.pendingBuffer, chunk]); - } else { - this.pendingBuffer = buffer = chunk; - } - - currentMessageLength = 0; - - for ( - let offset = extractMessageLengthAndOffsetFromBytes(buffer, 0); - buffer.length > 0 && offset !== headerInvalidNumber; - currentMessageLength = 0, offset = extractMessageLengthAndOffsetFromBytes(buffer, 0) - ) { - const messageLength = currentMessageLength; - const start = offset; - const end = start + messageLength; - offset = end; - const messageChunk = buffer.slice(start, end); - this.pendingBuffer = buffer = buffer.slice(offset); - yield messageChunk.toString(); - } - } -} - -export class JSCClient { - #options: JSCClientOptions; - #requestId: number; - #pendingMessages: Buffer[]; - #pendingRequests: Map<number, (result: unknown) => void>; - #socket: Socket; - #ready?: Promise<void>; - #reader = new StreamingReader(); - signal?: AbortSignal; - - constructor(options: JSCClientOptions) { - this.#options = options; - this.#socket = undefined; - this.#requestId = 1; - - this.#pendingMessages = []; - this.#pendingRequests = new Map(); - } - - get ready(): Promise<void> { - if (!this.#ready) { - this.#ready = this.#connect(); - } - return this.#ready; - } - - #connect(): Promise<void> { - const { url, retry, onError, onResponse, onEvent, onClose } = this.#options; - let [host, port] = typeof url === "string" ? url.split(":") : [url.hostname, url.port]; - if (port == null) { - if (host == null) { - host = "localhost"; - port = "9229"; - } else { - port = "9229"; - } - } - - if (host == null) { - host = "localhost"; - } - var resolve, - reject, - promise = new Promise<void>((r1, r2) => { - resolve = r1; - reject = r2; - }), - socket: Socket; - let didConnect = false; - - this.#socket = socket = createConnection( - { - host, - port: Number(port), - }, - () => { - for (const message of this.#pendingMessages) { - this.#send(message); - } - this.#pendingMessages.length = 0; - didConnect = true; - resolve(); - }, - ) - .once("error", e => { - const error = new Error(`Socket error: ${e?.message || e}`); - reject(error); - }) - .on("data", buffer => { - for (const message of this.#reader.onMessage(buffer)) { - let received: JSC.Event | JSC.Response; - try { - received = JSON.parse(message); - } catch { - const error = new Error(`Invalid WebSocket data: ${inspect(message)}`); - onError?.(error); - return; - } - console.log({ received }); - if ("id" in received) { - onResponse?.(received); - if ("error" in received) { - const { message, code = "?" } = received.error; - const error = new Error(`${message} [code: ${code}]`); - error.code = code; - onError?.(error); - this.#pendingRequests.get(received.id)?.(error); - } else { - this.#pendingRequests.get(received.id)?.(received.result); - } - } else { - onEvent?.(received); - } - } - }) - .on("close", hadError => { - if (didConnect) { - onClose?.(hadError ? 1 : 0, "Socket closed"); - } - }); - - return promise; - } - - #send(message: any): void { - const socket = this.#socket; - const framed = writeJSONMessageToBuffer(message); - if (socket && !socket.connecting) { - socket.write(framed); - } else { - this.#pendingMessages.push(framed); - } - } - - async fetch<T extends keyof JSC.RequestMap>( - method: T, - params?: JSC.Request<T>["params"], - ): Promise<JSC.ResponseMap[T]> { - const request: JSC.Request<T> = { - id: this.#requestId++, - method, - params, - }; - this.#options.onRequest?.(request); - return new Promise((resolve, reject) => { - const done = (result: Error | JSC.ResponseMap[T]) => { - this.#pendingRequests.delete(request.id); - if (result instanceof Error) { - reject(result); - } else { - resolve(result); - } - }; - this.#pendingRequests.set(request.id, done); - this.#send(request); - }); - } - - close(): void { - if (this.#socket) this.#socket.end(); - } -} diff --git a/packages/bun-vscode/src/web-extension.ts b/packages/bun-vscode/src/web-extension.ts index 2a66793b5..cd2e2623e 100644 --- a/packages/bun-vscode/src/web-extension.ts +++ b/packages/bun-vscode/src/web-extension.ts @@ -1,10 +1,5 @@ import * as vscode from "vscode"; -import { activateBunDebug } from "./activate"; -export function activate(context: vscode.ExtensionContext) { - activateBunDebug(context); -} +export function activate(context: vscode.ExtensionContext) {} -export function deactivate() { - // No-op -} +export function deactivate() {} |