mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
@electroid's dap changes (#4367)
* Rework terminal, launch is still WIP * Use the proper Terminal profile API * More changes * progress --------- Co-authored-by: Ashcon Partovi <ashcon@partovi.net>
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
export type * from "./src/protocol";
|
||||
export * from "./src/debugger/adapter";
|
||||
export * from "./src/debugger/signal";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Server } from "node:net";
|
||||
import { createServer } from "node:net";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development";
|
||||
|
||||
export type UnixSignalEventMap = {
|
||||
"Signal.listening": [string];
|
||||
"Signal.error": [Error];
|
||||
"Signal.received": [string];
|
||||
"Signal.closed": [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a server that listens for signals on a UNIX domain socket.
|
||||
*/
|
||||
export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
#path: string;
|
||||
#server: Server;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(path?: string) {
|
||||
super();
|
||||
this.#path = path ? parseUnixPath(path) : randomUnixPath();
|
||||
this.#server = createServer();
|
||||
this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
|
||||
this.#server.on("error", error => this.emit("Signal.error", error));
|
||||
this.#server.on("close", () => this.emit("Signal.closed"));
|
||||
this.#server.on("connection", socket => {
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
});
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.on("listening", resolve);
|
||||
this.#server.on("error", reject);
|
||||
});
|
||||
this.#server.listen(this.#path);
|
||||
}
|
||||
|
||||
emit<E extends keyof UnixSignalEventMap>(event: E, ...args: UnixSignalEventMap[E]): boolean {
|
||||
if (isDebug) {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the UNIX domain socket.
|
||||
*/
|
||||
get url(): string {
|
||||
return `unix://${this.#path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves when the server is listening or rejects if an error occurs.
|
||||
*/
|
||||
get ready(): Promise<void> {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the server.
|
||||
*/
|
||||
close(): void {
|
||||
this.#server.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function randomUnixPath(): string {
|
||||
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
|
||||
}
|
||||
|
||||
function parseUnixPath(path: string): string {
|
||||
if (path.startsWith("/")) {
|
||||
return path;
|
||||
}
|
||||
try {
|
||||
const { pathname } = new URL(path);
|
||||
return pathname;
|
||||
} catch {
|
||||
throw new Error(`Invalid UNIX path: ${path}`);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
import type { JSC } from "..";
|
||||
import type { EventEmitter } from "node:events";
|
||||
import type { JSC } from "../protocol";
|
||||
|
||||
export type InspectorEventMap = {
|
||||
[E in keyof JSC.EventMap]: [JSC.EventMap[E]];
|
||||
} & {
|
||||
"Inspector.connecting": [string];
|
||||
"Inspector.connected": [];
|
||||
"Inspector.disconnected": [Error | undefined];
|
||||
"Inspector.error": [Error];
|
||||
"Inspector.pendingRequest": [JSC.Request];
|
||||
"Inspector.request": [JSC.Request];
|
||||
"Inspector.response": [JSC.Response];
|
||||
"Inspector.event": [JSC.Event];
|
||||
};
|
||||
|
||||
/**
|
||||
* A client that can send and receive messages to/from a debugger.
|
||||
*/
|
||||
export abstract class Inspector {
|
||||
constructor(listener?: InspectorListener);
|
||||
export interface Inspector extends EventEmitter<InspectorEventMap> {
|
||||
/**
|
||||
* Starts the inspector.
|
||||
*/
|
||||
@@ -16,11 +29,6 @@ export abstract class Inspector {
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M],
|
||||
): Promise<JSC.ResponseMap[M]>;
|
||||
/**
|
||||
* Accepts a message from the debugger.
|
||||
* @param message the unparsed message from the debugger
|
||||
*/
|
||||
accept(message: string): void;
|
||||
/**
|
||||
* If the inspector is closed.
|
||||
*/
|
||||
@@ -30,20 +38,3 @@ export abstract class Inspector {
|
||||
*/
|
||||
close(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export type InspectorListener = {
|
||||
/**
|
||||
* Defines a handler when a debugger event is received.
|
||||
*/
|
||||
[M in keyof JSC.EventMap]?: (event: JSC.EventMap[M]) => void;
|
||||
} & {
|
||||
/**
|
||||
* Defines a handler when the debugger is connected or reconnected.
|
||||
*/
|
||||
["Inspector.connected"]?: () => void;
|
||||
/**
|
||||
* Defines a handler when the debugger is disconnected.
|
||||
* @param error the error that caused the disconnect, if any
|
||||
*/
|
||||
["Inspector.disconnected"]?: (error?: Error) => void;
|
||||
};
|
||||
|
||||
@@ -1,46 +1,42 @@
|
||||
import type { Inspector, InspectorListener } from ".";
|
||||
import type { Inspector, InspectorEventMap } from ".";
|
||||
import type { JSC } from "../protocol";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { WebSocket } from "ws";
|
||||
import { createServer, type Server } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
export type WebSocketInspectorOptions = {
|
||||
url?: string | URL;
|
||||
listener?: InspectorListener;
|
||||
logger?: (...messages: unknown[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a WebSocket.
|
||||
*/
|
||||
export class WebSocketInspector implements Inspector {
|
||||
#url?: URL;
|
||||
export class WebSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
|
||||
#url?: string;
|
||||
#webSocket?: WebSocket;
|
||||
#ready: Promise<boolean> | undefined;
|
||||
#requestId: number;
|
||||
#pendingRequests: Map<number, (result: unknown) => void>;
|
||||
#pendingMessages: string[];
|
||||
#listener: InspectorListener;
|
||||
#log: (...messages: unknown[]) => void;
|
||||
#pendingRequests: JSC.Request[];
|
||||
#pendingResponses: Map<number, (result: unknown) => void>;
|
||||
|
||||
constructor({ url, listener, logger }: WebSocketInspectorOptions) {
|
||||
this.#url = url ? new URL(url) : undefined;
|
||||
constructor(url?: string | URL) {
|
||||
super();
|
||||
this.#url = url ? String(url) : undefined;
|
||||
this.#requestId = 1;
|
||||
this.#pendingRequests = new Map();
|
||||
this.#pendingMessages = [];
|
||||
this.#listener = listener ?? {};
|
||||
this.#log = logger ?? (() => {});
|
||||
this.#pendingRequests = [];
|
||||
this.#pendingResponses = new Map();
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.#url!;
|
||||
}
|
||||
|
||||
async start(url?: string | URL): Promise<boolean> {
|
||||
if (url) {
|
||||
this.#url = new URL(url);
|
||||
this.#url = String(url);
|
||||
}
|
||||
if (this.#url) {
|
||||
const { href } = this.#url;
|
||||
return this.#connect(href);
|
||||
|
||||
if (!this.#url) {
|
||||
this.emit("Inspector.error", new Error("Inspector needs a URL, but none was provided"));
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
return this.#connect(this.#url);
|
||||
}
|
||||
|
||||
async #connect(url: string): Promise<boolean> {
|
||||
@@ -48,10 +44,12 @@ export class WebSocketInspector implements Inspector {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
this.close(1001, "Restarting...");
|
||||
this.emit("Inspector.connecting", url);
|
||||
|
||||
let webSocket: WebSocket;
|
||||
try {
|
||||
this.#log("connecting:", url);
|
||||
// @ts-expect-error: Node.js
|
||||
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
|
||||
webSocket = new WebSocket(url, {
|
||||
headers: {
|
||||
"Ref-Event-Loop": "1",
|
||||
@@ -61,43 +59,43 @@ export class WebSocketInspector implements Inspector {
|
||||
request.end();
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.#close(unknownToError(error));
|
||||
} catch (cause) {
|
||||
this.#close(unknownToError(cause));
|
||||
return false;
|
||||
}
|
||||
|
||||
webSocket.addEventListener("open", () => {
|
||||
this.#log("connected");
|
||||
for (const message of this.#pendingMessages) {
|
||||
this.#send(message);
|
||||
this.emit("Inspector.connected");
|
||||
|
||||
for (const request of this.#pendingRequests) {
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
}
|
||||
}
|
||||
this.#pendingMessages.length = 0;
|
||||
this.#listener["Inspector.connected"]?.();
|
||||
|
||||
this.#pendingRequests.length = 0;
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", ({ data }) => {
|
||||
if (typeof data === "string") {
|
||||
this.accept(data);
|
||||
this.#accept(data);
|
||||
}
|
||||
});
|
||||
|
||||
webSocket.addEventListener("error", event => {
|
||||
this.#log("error:", event);
|
||||
this.#close(unknownToError(event));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("unexpected-response", () => {
|
||||
this.#log("unexpected-response");
|
||||
this.#close(new Error("WebSocket upgrade failed"));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("close", ({ code, reason }) => {
|
||||
this.#log("closed:", code, reason);
|
||||
if (code === 1001) {
|
||||
if (code === 1001 || code === 1006) {
|
||||
this.#close();
|
||||
} else {
|
||||
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
|
||||
return;
|
||||
}
|
||||
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
|
||||
});
|
||||
|
||||
this.#webSocket = webSocket;
|
||||
@@ -115,19 +113,20 @@ export class WebSocketInspector implements Inspector {
|
||||
return ready;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M] | undefined,
|
||||
): Promise<JSC.ResponseMap[M]> {
|
||||
const id = this.#requestId++;
|
||||
const request = { id, method, params };
|
||||
|
||||
this.#log("-->", request);
|
||||
const request = {
|
||||
id,
|
||||
method,
|
||||
params: params ?? {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: any) => {
|
||||
this.#pendingRequests.delete(id);
|
||||
this.#pendingResponses.delete(id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
@@ -135,60 +134,62 @@ export class WebSocketInspector implements Inspector {
|
||||
}
|
||||
};
|
||||
|
||||
this.#pendingRequests.set(id, done);
|
||||
this.#send(JSON.stringify(request));
|
||||
this.#pendingResponses.set(id, done);
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.emit("Inspector.pendingRequest", request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#send(message: string): void {
|
||||
#send(request: JSC.Request): boolean {
|
||||
if (this.#webSocket) {
|
||||
const { readyState } = this.#webSocket!;
|
||||
if (readyState === WebSocket.OPEN) {
|
||||
this.#webSocket.send(message);
|
||||
this.#webSocket.send(JSON.stringify(request));
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#pendingMessages.includes(message)) {
|
||||
this.#pendingMessages.push(message);
|
||||
if (!this.#pendingRequests.includes(request)) {
|
||||
this.#pendingRequests.push(request);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
accept(message: string): void {
|
||||
let event: JSC.Event | JSC.Response;
|
||||
#accept(message: string): void {
|
||||
let data: JSC.Event | JSC.Response;
|
||||
try {
|
||||
event = JSON.parse(message);
|
||||
} catch (error) {
|
||||
this.#log("Failed to parse message:", message);
|
||||
data = JSON.parse(message);
|
||||
} catch (cause) {
|
||||
this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.#log("<--", event);
|
||||
|
||||
if (!("id" in event)) {
|
||||
const { method, params } = event;
|
||||
try {
|
||||
this.#listener[method]?.(params as any);
|
||||
} catch (error) {
|
||||
this.#log(`Failed to accept ${method} event:`, error);
|
||||
}
|
||||
if (!("id" in data)) {
|
||||
this.emit("Inspector.event", data);
|
||||
const { method, params } = data;
|
||||
this.emit(method, params);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = event;
|
||||
const resolve = this.#pendingRequests.get(id);
|
||||
this.emit("Inspector.response", data);
|
||||
|
||||
const { id } = data;
|
||||
const resolve = this.#pendingResponses.get(id);
|
||||
if (!resolve) {
|
||||
this.#log("Failed to accept response with unknown ID:", id);
|
||||
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pendingRequests.delete(id);
|
||||
if ("error" in event) {
|
||||
const { error } = event;
|
||||
this.#pendingResponses.delete(id);
|
||||
if ("error" in data) {
|
||||
const { error } = data;
|
||||
const { message } = error;
|
||||
resolve(new Error(message));
|
||||
} else {
|
||||
const { result } = event;
|
||||
const { result } = data;
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
@@ -213,54 +214,14 @@ export class WebSocketInspector implements Inspector {
|
||||
}
|
||||
|
||||
#close(error?: Error): void {
|
||||
for (const resolve of this.#pendingRequests.values()) {
|
||||
for (const resolve of this.#pendingResponses.values()) {
|
||||
resolve(error ?? new Error("WebSocket closed"));
|
||||
}
|
||||
this.#pendingRequests.clear();
|
||||
this.#listener["Inspector.disconnected"]?.(error);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnixWebSocketInspector extends WebSocketInspector {
|
||||
#unix: string;
|
||||
#server: Server;
|
||||
#ready: Promise<unknown>;
|
||||
startDebugging?: () => void;
|
||||
|
||||
constructor(options: WebSocketInspectorOptions) {
|
||||
super(options);
|
||||
this.#unix = unixSocket();
|
||||
this.#server = createServer();
|
||||
this.#server.listen(this.#unix);
|
||||
this.#ready = this.#wait().then(() => {
|
||||
setTimeout(() => {
|
||||
this.start().then(() => this.startDebugging?.());
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
get unix(): string {
|
||||
return this.#unix;
|
||||
}
|
||||
|
||||
#wait(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
console.log("waiting");
|
||||
this.#server.once("connection", socket => {
|
||||
console.log("received");
|
||||
socket.once("data", resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async start(url?: string | URL): Promise<boolean> {
|
||||
await this.#ready;
|
||||
try {
|
||||
console.log("starting");
|
||||
return await super.start(url);
|
||||
} finally {
|
||||
this.#ready = this.#wait();
|
||||
this.#pendingResponses.clear();
|
||||
if (error) {
|
||||
this.emit("Inspector.error", error);
|
||||
}
|
||||
this.emit("Inspector.disconnected", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +237,3 @@ function unknownToError(input: unknown): Error {
|
||||
|
||||
return new Error(`${input}`);
|
||||
}
|
||||
|
||||
function unixSocket(): string {
|
||||
return `${tmpdir()}/bun-inspect-${Math.random().toString(36).slice(2)}.sock`;
|
||||
}
|
||||
|
||||
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, test, expect, mock, beforeAll, afterAll } from "bun:test";
|
||||
import { WebSocketInspector } from "../../src/inspector/websocket";
|
||||
import type { Server } from "bun";
|
||||
import { serve } from "bun";
|
||||
|
||||
let server: Server;
|
||||
let url: URL;
|
||||
|
||||
describe("WebSocketInspector", () => {
|
||||
test("fails without a URL", () => {
|
||||
const ws = new WebSocketInspector();
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid URL", () => {
|
||||
const ws = new WebSocketInspector("notaurl");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with valid URL but no server", () => {
|
||||
const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid upgrade response", () => {
|
||||
const ws = new WebSocketInspector(new URL("/", url));
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBe(false);
|
||||
});
|
||||
ws.on("Inspector.connected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can disconnect from a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server multiple times", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(() => {
|
||||
expect(ws.closed).toBeFalse();
|
||||
});
|
||||
ws.on("Inspector.connected", fn0);
|
||||
const fn1 = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
}
|
||||
expect(fn0).toHaveBeenCalledTimes(3);
|
||||
expect(fn1).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("can send a request", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Debugger.setPauseOnAssertions",
|
||||
params: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalled();
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can send a request before connecting", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Runtime.enable",
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.pendingRequest", fn0);
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
const request = ws.send("Runtime.enable");
|
||||
expect(ws.start()).resolves.toBe(true);
|
||||
expect(request).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalledTimes(2);
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can receive an event", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(event => {
|
||||
expect(event).toStrictEqual({
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
scriptId: "1",
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.event", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true });
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server = serve({
|
||||
port: 0,
|
||||
fetch(request, server) {
|
||||
if (request.url.endsWith("/ws") && server.upgrade(request)) {
|
||||
return;
|
||||
}
|
||||
return new Response();
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
const { id, method } = JSON.parse(String(message));
|
||||
ws.send(JSON.stringify({ id, result: { ok: true } }));
|
||||
|
||||
if (method === "Debugger.enable") {
|
||||
ws.send(JSON.stringify({ method: "Debugger.scriptParsed", params: { scriptId: "1" } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const { hostname, port } = server;
|
||||
url = new URL(`ws://${hostname}:${port}/ws`);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server?.stop(true);
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`remoteObjectToString 1`] = `"undefined"`;
|
||||
|
||||
exports[`remoteObjectToString 2`] = `"null"`;
|
||||
|
||||
exports[`remoteObjectToString 3`] = `"true"`;
|
||||
|
||||
exports[`remoteObjectToString 4`] = `"false"`;
|
||||
|
||||
exports[`remoteObjectToString 5`] = `"0"`;
|
||||
|
||||
exports[`remoteObjectToString 6`] = `"1"`;
|
||||
|
||||
exports[`remoteObjectToString 7`] = `"3.141592653589793"`;
|
||||
|
||||
exports[`remoteObjectToString 8`] = `"-2.718281828459045"`;
|
||||
|
||||
exports[`remoteObjectToString 9`] = `"NaN"`;
|
||||
|
||||
exports[`remoteObjectToString 10`] = `"Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 11`] = `"-Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 12`] = `"0n"`;
|
||||
|
||||
exports[`remoteObjectToString 13`] = `"1n"`;
|
||||
|
||||
exports[`remoteObjectToString 14`] = `"10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 15`] = `"-10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 16`] = `""""`;
|
||||
|
||||
exports[`remoteObjectToString 17`] = `"" ""`;
|
||||
|
||||
exports[`remoteObjectToString 18`] = `""Hello""`;
|
||||
|
||||
exports[`remoteObjectToString 19`] = `""Hello World""`;
|
||||
|
||||
exports[`remoteObjectToString 20`] = `"Array(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 21`] = `"Array(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 22`] = `"Array(4) ["a", 1, null, undefined]"`;
|
||||
|
||||
exports[`remoteObjectToString 23`] = `"Array(2) [1, Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 24`] = `"Array(1) [Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 25`] = `"{}"`;
|
||||
|
||||
exports[`remoteObjectToString 26`] = `"{a: 1}"`;
|
||||
|
||||
exports[`remoteObjectToString 27`] = `"{a: 1, b: 2, c: 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 28`] = `"{a: Object}"`;
|
||||
|
||||
exports[`remoteObjectToString 29`] = `
|
||||
"ƒ() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 30`] = `
|
||||
"ƒ namedFunction() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 31`] = `
|
||||
"class {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 32`] = `
|
||||
"class namedClass {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 33`] = `
|
||||
"class namedClass {
|
||||
a() {
|
||||
}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3
|
||||
}
|
||||
];
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 34`] = `"Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)"`;
|
||||
|
||||
exports[`remoteObjectToString 35`] = `"Invalid Date"`;
|
||||
|
||||
exports[`remoteObjectToString 36`] = `"/(?:)/"`;
|
||||
|
||||
exports[`remoteObjectToString 37`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 38`] = `"/abc/g"`;
|
||||
|
||||
exports[`remoteObjectToString 39`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 40`] = `"Set(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 41`] = `"Set(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 42`] = `"WeakSet(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 43`] = `"WeakSet(3) [{a: 1}, {b: 2}, {c: 3}]"`;
|
||||
|
||||
exports[`remoteObjectToString 44`] = `"Map(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 45`] = `"Map(3) {"a" => 1, "b" => 2, "c" => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 46`] = `"WeakMap(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 47`] = `"WeakMap(3) {{a: 1} => 1, {b: 2} => 2, {c: 3} => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 48`] = `"Symbol()"`;
|
||||
|
||||
exports[`remoteObjectToString 49`] = `"Symbol(namedSymbol)"`;
|
||||
|
||||
exports[`remoteObjectToString 50`] = `"Error"`;
|
||||
|
||||
exports[`remoteObjectToString 51`] = `"TypeError: This is a TypeError"`;
|
||||
|
||||
exports[`remoteObjectToString 52`] = `"Headers {append: ƒ, delete: ƒ, get: ƒ, getAll: ƒ, has: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 53`] = `"Headers {a: "1", append: ƒ, b: "2", delete: ƒ, get: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 54`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 55`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 56`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, clone: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 57`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, clone: ƒ, …}"`;
|
||||
@@ -1,99 +0,0 @@
|
||||
console.log(
|
||||
undefined,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
Math.PI,
|
||||
-Math.E,
|
||||
NaN,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
BigInt(0),
|
||||
BigInt(1),
|
||||
BigInt("10000000000000"),
|
||||
BigInt("-10000000000000"),
|
||||
"",
|
||||
" ",
|
||||
"Hello",
|
||||
"Hello World",
|
||||
[],
|
||||
[1, 2, 3],
|
||||
["a", 1, null, undefined],
|
||||
[1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]]],
|
||||
[[[[[]]]]],
|
||||
{},
|
||||
{ a: 1 },
|
||||
{ a: 1, b: 2, c: 3 },
|
||||
{ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: 10 } } } } } } } } } },
|
||||
function () {},
|
||||
function namedFunction() {},
|
||||
class {},
|
||||
class namedClass {},
|
||||
class namedClass {
|
||||
a() {}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
},
|
||||
];
|
||||
},
|
||||
new Date(0),
|
||||
new Date(NaN),
|
||||
new RegExp(),
|
||||
new RegExp("abc"),
|
||||
new RegExp("abc", "g"),
|
||||
/abc/,
|
||||
new Set(),
|
||||
new Set([1, 2, 3]),
|
||||
new WeakSet(),
|
||||
new WeakSet([{ a: 1 }, { b: 2 }, { c: 3 }]),
|
||||
new Map(),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]),
|
||||
new WeakMap(),
|
||||
new WeakMap([
|
||||
[{ a: 1 }, 1],
|
||||
[{ b: 2 }, 2],
|
||||
[{ c: 3 }, 3],
|
||||
]),
|
||||
Symbol(),
|
||||
Symbol("namedSymbol"),
|
||||
new Error(),
|
||||
new TypeError("This is a TypeError"),
|
||||
//"a".repeat(10000),
|
||||
//["a"].fill("a", 0, 10000),
|
||||
new Headers(),
|
||||
new Headers({
|
||||
a: "1",
|
||||
b: "2",
|
||||
}),
|
||||
new Request("https://example.com/"),
|
||||
new Request("https://example.com/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
body: '{"example":true}',
|
||||
}),
|
||||
new Response(),
|
||||
new Response('{"example":true}', {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1,61 +0,0 @@
|
||||
import { beforeAll, afterAll, test, expect } from "bun:test";
|
||||
import type { PipedSubprocess } from "bun";
|
||||
import { spawn } from "bun";
|
||||
import type { JSC } from "../..";
|
||||
import { WebSocketInspector, remoteObjectToString } from "../..";
|
||||
|
||||
let subprocess: PipedSubprocess | undefined;
|
||||
let objects: JSC.Runtime.RemoteObject[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
subprocess = spawn({
|
||||
cwd: import.meta.dir,
|
||||
cmd: [process.argv0, "--inspect-wait=0", "preview.js"],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let url: URL;
|
||||
for await (const chunk of subprocess!.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
if (text.includes("ws://")) {
|
||||
url = new URL(/(ws:\/\/.*)/.exec(text)![0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
objects = await new Promise((resolve, reject) => {
|
||||
const inspector = new WebSocketInspector({
|
||||
url,
|
||||
listener: {
|
||||
["Inspector.connected"]: () => {
|
||||
inspector.send("Inspector.enable");
|
||||
inspector.send("Runtime.enable");
|
||||
inspector.send("Console.enable");
|
||||
inspector.send("Debugger.enable");
|
||||
inspector.send("Debugger.resume");
|
||||
inspector.send("Inspector.initialized");
|
||||
},
|
||||
["Inspector.disconnected"]: error => {
|
||||
reject(error);
|
||||
},
|
||||
["Console.messageAdded"]: ({ message }) => {
|
||||
const { parameters } = message;
|
||||
resolve(parameters!);
|
||||
inspector.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
inspector.start();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
subprocess?.kill();
|
||||
});
|
||||
|
||||
test("remoteObjectToString", () => {
|
||||
for (const object of objects) {
|
||||
expect(remoteObjectToString(object)).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
@@ -29,6 +29,6 @@ interface CoolThing {
|
||||
|
||||
class SuperCoolThing implements CoolThing {
|
||||
doCoolThing(): void {
|
||||
console.log("BLAH BLAH");
|
||||
console.log("BLAH BLAH", new Map([[1, 2]]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,15 @@
|
||||
],
|
||||
"priority": "default"
|
||||
}
|
||||
]
|
||||
],
|
||||
"terminal": {
|
||||
"profiles": [
|
||||
{
|
||||
"title": "Bun Terminal",
|
||||
"id": "bun"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "The Visual Studio Code extension for Bun.",
|
||||
"displayName": "Bun",
|
||||
@@ -253,6 +261,7 @@
|
||||
},
|
||||
"workspaces": [
|
||||
"../bun-debug-adapter-protocol",
|
||||
"../bun-inspector-protocol"
|
||||
"../bun-inspector-protocol",
|
||||
"../bun-wasm"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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 { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugSession } from "@vscode/debugadapter";
|
||||
import { inspect } from "node:util";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const debugConfiguration: vscode.DebugConfiguration = {
|
||||
@@ -30,8 +28,7 @@ const attachConfiguration: vscode.DebugConfiguration = {
|
||||
url: "ws://localhost:6499/",
|
||||
};
|
||||
|
||||
let channels: Record<string, vscode.OutputChannel> = {};
|
||||
let terminal: TerminalDebugSession | undefined;
|
||||
const adapters = new Map<string, FileDebugSession>();
|
||||
|
||||
export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
context.subscriptions.push(
|
||||
@@ -48,11 +45,14 @@ export default function (context: vscode.ExtensionContext, factory?: vscode.Debu
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory ?? new InlineDebugAdapterFactory()),
|
||||
(channels["dap"] = vscode.window.createOutputChannel("Debug Adapter Protocol (Bun)")),
|
||||
(channels["jsc"] = vscode.window.createOutputChannel("JavaScript Inspector (Bun)")),
|
||||
(channels["console"] = vscode.window.createOutputChannel("Console (Bun)")),
|
||||
(terminal = new TerminalDebugSession()),
|
||||
vscode.window.registerTerminalProfileProvider("bun", new TerminalProfileProvider()),
|
||||
);
|
||||
|
||||
const { terminalProfile } = new TerminalDebugSession();
|
||||
const { options } = terminalProfile;
|
||||
const terminal = vscode.window.createTerminal(options);
|
||||
terminal.show();
|
||||
context.subscriptions.push(terminal);
|
||||
}
|
||||
|
||||
function RunFileCommand(resource?: vscode.Uri): void {
|
||||
@@ -76,17 +76,24 @@ function DebugFileCommand(resource?: vscode.Uri): void {
|
||||
}
|
||||
}
|
||||
|
||||
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: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
provideDebugConfigurations(folder?: vscode.WorkspaceFolder): vscode.ProviderResult<vscode.DebugConfiguration[]> {
|
||||
return [debugConfiguration, runConfiguration, attachConfiguration];
|
||||
}
|
||||
|
||||
resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken,
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
let target: DebugConfiguration;
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
config: vscode.DebugConfiguration,
|
||||
token?: vscode.CancellationToken,
|
||||
): vscode.ProviderResult<vscode.DebugConfiguration> {
|
||||
let target: vscode.DebugConfiguration;
|
||||
|
||||
const { request } = config;
|
||||
if (request === "attach") {
|
||||
@@ -106,12 +113,16 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
createDebugAdapterDescriptor(session: vscode.DebugSession): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
const { configuration } = session;
|
||||
const { request, url } = configuration;
|
||||
|
||||
if (request === "attach" && url === terminal?.url) {
|
||||
return new vscode.DebugAdapterInlineImplementation(terminal);
|
||||
if (request === "attach") {
|
||||
for (const [adapterUrl, adapter] of adapters) {
|
||||
if (adapterUrl === url) {
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id);
|
||||
@@ -120,45 +131,28 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
readonly url: string;
|
||||
readonly adapter: DebugAdapter;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
super();
|
||||
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
|
||||
this.url = `ws+unix://${tmpdir()}/bun-vscode-${uniqueId}.sock`;
|
||||
this.adapter = new DebugAdapter({
|
||||
url: this.url,
|
||||
send: this.sendMessage.bind(this),
|
||||
logger(...messages) {
|
||||
log("jsc", ...messages);
|
||||
},
|
||||
stdout(message) {
|
||||
log("console", message);
|
||||
},
|
||||
stderr(message) {
|
||||
log("console", message);
|
||||
},
|
||||
});
|
||||
}
|
||||
const url = `ws+unix://${tmpdir()}/${uniqueId}.sock`;
|
||||
|
||||
sendMessage(message: DAP.Request | DAP.Response | DAP.Event): void {
|
||||
log("dap", "-->", message);
|
||||
this.adapter = new DebugAdapter(url);
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
|
||||
const { type } = message;
|
||||
if (type === "response") {
|
||||
this.sendResponse(message);
|
||||
} else if (type === "event") {
|
||||
this.sendEvent(message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
}
|
||||
adapters.set(url, this);
|
||||
}
|
||||
|
||||
handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
|
||||
log("dap", "<--", message);
|
||||
const { type } = message;
|
||||
|
||||
this.adapter.accept(message);
|
||||
if (type === "request") {
|
||||
this.adapter.emit("Adapter.request", message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -167,45 +161,32 @@ class FileDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
class TerminalDebugSession extends FileDebugSession {
|
||||
readonly terminal: vscode.Terminal;
|
||||
readonly signal: UnixSignal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.terminal = vscode.window.createTerminal({
|
||||
this.signal = new UnixSignal();
|
||||
this.signal.on("Signal.received", () => {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...attachConfiguration,
|
||||
url: this.adapter.url,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get terminalProfile(): vscode.TerminalProfile {
|
||||
return new vscode.TerminalProfile({
|
||||
name: "Bun Terminal",
|
||||
env: {
|
||||
"BUN_INSPECT": `1${this.url}`,
|
||||
"BUN_INSPECT_NOTIFY": `unix://${this.adapter.inspector.unix}`,
|
||||
"BUN_INSPECT": `1${this.adapter.url}`,
|
||||
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
});
|
||||
this.terminal.show();
|
||||
this.adapter.inspector.startDebugging = () => {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...attachConfiguration,
|
||||
url: this.url,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function log(channel: string, ...message: unknown[]): void {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`[${channel}]`, ...message);
|
||||
channels[channel]?.appendLine(message.map(v => inspect(v)).join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
function isJavaScript(languageId: string): boolean {
|
||||
return (
|
||||
languageId === "javascript" ||
|
||||
languageId === "javascriptreact" ||
|
||||
languageId === "typescript" ||
|
||||
languageId === "typescriptreact"
|
||||
);
|
||||
}
|
||||
|
||||
function getCurrentPath(target?: vscode.Uri): string | undefined {
|
||||
if (!target && vscode.window.activeTextEditor) {
|
||||
target = vscode.window.activeTextEditor.document.uri;
|
||||
|
||||
Reference in New Issue
Block a user