Skip to content

Commit 8214cab

Browse files
authored
Bring back async API, allow API connection to LSP process (#2620)
1 parent 7d8e77a commit 8214cab

59 files changed

Lines changed: 4579 additions & 1750 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vscode/tasks.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@
117117
}
118118
}
119119
},
120+
{
121+
"label": "Watch api package",
122+
"type": "npm",
123+
"script": "watch",
124+
"path": "_packages/api",
125+
"isBackground": true,
126+
"group": "build",
127+
"presentation": {
128+
"panel": "dedicated",
129+
"reveal": "never"
130+
},
131+
"problemMatcher": [
132+
"$tsc-watch"
133+
]
134+
},
120135
{
121136
"label": "Watch for extension run",
122137
"dependsOn": [

Herebyfile.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ const goBuildFlags = [
104104
...(options.debug ? ["-gcflags=all=-N -l"] : []),
105105
];
106106

107+
const goBuildEnv = {
108+
...(options.race ? {} : { CGO_ENABLED: "0" }),
109+
};
110+
107111
/**
108112
* @template T
109113
* @param {() => T} fn
@@ -192,7 +196,8 @@ export const lib = task({
192196
function buildTsgo(opts) {
193197
opts ||= {};
194198
const out = opts.out ?? "./built/local/";
195-
return $({ cancelSignal: opts.abortSignal, env: opts.env })`go build ${goBuildFlags} ${opts.extraFlags ?? []} ${options.debug ? goBuildTags("noembed") : goBuildTags("noembed", "release")} -o ${out} ./cmd/tsgo`;
199+
const env = { ...goBuildEnv, ...opts.env };
200+
return $({ cancelSignal: opts.abortSignal, env })`go build ${goBuildFlags} ${opts.extraFlags ?? []} ${options.debug ? goBuildTags("noembed") : goBuildTags("noembed", "release")} -o ${out} ./cmd/tsgo`;
196201
}
197202

198203
export const tsgoBuild = task({
@@ -469,6 +474,14 @@ export const testTools = task({
469474
run: runTestTools,
470475
});
471476

477+
export const buildAPI = task({
478+
name: "build:api",
479+
description: "Builds @typescript/api and @typescript/ast.",
480+
run: async () => {
481+
await $`npm run -w @typescript/api build`;
482+
},
483+
});
484+
472485
export const buildAPITests = task({
473486
name: "build:api:test",
474487
description: "Builds the @typescript/api tests.",

_extension/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@
141141
"title": "TypeScript Native Preview: Stop CPU Profile",
142142
"enablement": "typescript.native-preview.cpuProfileRunning",
143143
"category": "Developer"
144+
},
145+
{
146+
"command": "typescript.native-preview.initializeAPIConnection.ui",
147+
"title": "Initialize API Connection",
148+
"enablement": "typescript.native-preview.serverRunning",
149+
"category": "Developer: TypeScript Native Preview"
144150
}
145151
]
146152
},

_extension/src/client.ts

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ import {
2525
} from "./util";
2626
import { getLanguageForUri } from "./util";
2727

28-
export class Client {
28+
export class Client implements vscode.Disposable {
2929
private outputChannel: vscode.LogOutputChannel;
3030
private traceOutputChannel: vscode.LogOutputChannel;
31+
private initializedEventEmitter: vscode.EventEmitter<void>;
3132
private telemetryReporter: tr.TelemetryReporter;
3233

3334
private documentSelector: Array<{ scheme: string; language: string; }>;
@@ -36,13 +37,19 @@ export class Client {
3637

3738
private isDisposed = false;
3839
private disposables: vscode.Disposable[] = [];
40+
isInitialized = false;
3941

4042
private exe: ExeInfo | undefined;
41-
private onStartedCallbacks: Set<() => void> = new Set();
4243

43-
constructor(outputChannel: vscode.LogOutputChannel, traceOutputChannel: vscode.LogOutputChannel, telemetryReporter: tr.TelemetryReporter) {
44+
constructor(
45+
outputChannel: vscode.LogOutputChannel,
46+
traceOutputChannel: vscode.LogOutputChannel,
47+
initializedEventEmitter: vscode.EventEmitter<void>,
48+
telemetryReporter: tr.TelemetryReporter,
49+
) {
4450
this.outputChannel = outputChannel;
4551
this.traceOutputChannel = traceOutputChannel;
52+
this.initializedEventEmitter = initializedEventEmitter;
4653
this.telemetryReporter = telemetryReporter;
4754
this.documentSelector = [
4855
...jsTsLanguageModes.map(language => ({ scheme: "file", language })),
@@ -105,12 +112,7 @@ export class Client {
105112
};
106113
}
107114

108-
async initialize(context: vscode.ExtensionContext): Promise<vscode.Disposable> {
109-
const exe = await getExe(context);
110-
return this.start(context, exe);
111-
}
112-
113-
async start(context: vscode.ExtensionContext, exe: { path: string; version: string; }): Promise<vscode.Disposable> {
115+
async start(exe: { path: string; version: string; }): Promise<void> {
114116
this.exe = exe;
115117
this.outputChannel.appendLine(`Resolved to ${this.exe.path}`);
116118
this.telemetryReporter.sendTelemetryEvent("languageServer.start", {
@@ -156,11 +158,12 @@ export class Client {
156158
serverOptions,
157159
this.clientOptions,
158160
);
161+
this.disposables.push(this.client);
159162

160163
this.outputChannel.appendLine(`Starting language server...`);
161164
await this.client.start();
162-
vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", true);
163-
this.onStartedCallbacks.forEach(callback => callback());
165+
this.isInitialized = true;
166+
this.initializedEventEmitter.fire();
164167

165168
if (this.traceOutputChannel.logLevel !== vscode.LogLevel.Trace) {
166169
this.traceOutputChannel.appendLine(`To see LSP trace output, set this output's log level to "Trace" (gear icon next to the dropdown).`);
@@ -195,58 +198,48 @@ export class Client {
195198
registerTagClosingFeature("typescript", this.documentSelector, this.client),
196199
registerTagClosingFeature("javascript", this.documentSelector, this.client),
197200
);
198-
199-
return new vscode.Disposable(() => {
200-
this.dispose();
201-
vscode.commands.executeCommand("setContext", "typescript.native-preview.serverRunning", false);
202-
vscode.commands.executeCommand("setContext", "typescript.native-preview.cpuProfileRunning", false);
203-
});
204201
}
205202

206-
dispose() {
203+
async dispose(): Promise<void> {
207204
if (this.isDisposed) {
208205
return;
209206
}
210207
this.isDisposed = true;
211-
212-
this.client?.dispose();
213-
while (this.disposables.length > 0) {
214-
const d = this.disposables.pop()!;
215-
d.dispose();
216-
}
208+
await Promise.all(this.disposables.map(d => d.dispose()));
217209
}
218210

219211
getCurrentExe(): { path: string; version: string; } | undefined {
220212
return this.exe;
221213
}
222214

223-
onStarted(callback: () => void): vscode.Disposable {
224-
if (this.exe) {
225-
callback();
226-
return new vscode.Disposable(() => {});
215+
/**
216+
* Initialize an API session and return the socket path for connecting.
217+
* This allows other extensions to get a direct connection to the API server.
218+
*/
219+
async initializeAPISession(pipe?: string): Promise<{ sessionId: string; pipe: string; }> {
220+
if (!this.client) {
221+
throw new Error("Language client is not initialized");
227222
}
228-
229-
this.onStartedCallbacks.add(callback);
230-
231-
return new vscode.Disposable(() => {
232-
this.onStartedCallbacks.delete(callback);
233-
});
223+
return this.client.sendRequest<{ sessionId: string; pipe: string; }>("custom/initializeAPISession", { pipe });
234224
}
235225

236-
async restart(context: vscode.ExtensionContext): Promise<vscode.Disposable> {
226+
/**
227+
* Restart the language server if the executable path has not changed.
228+
* Returns true if a restart was performed.
229+
*/
230+
async tryRestart(context: vscode.ExtensionContext): Promise<boolean> {
237231
if (!this.client) {
238232
return Promise.reject(new Error("Language client is not initialized"));
239233
}
240234
const exe = await getExe(context);
241235
if (exe.path !== this.exe?.path) {
242-
this.outputChannel.appendLine(`Executable path changed from ${this.exe?.path} to ${exe.path}`);
243-
this.outputChannel.appendLine(`Restarting language server with new executable...`);
244-
return this.start(context, exe);
236+
return false;
245237
}
246238

239+
this.isInitialized = false;
247240
this.outputChannel.appendLine(`Restarting language server...`);
248-
this.client.restart();
249-
return new vscode.Disposable(() => {});
241+
await this.client.restart();
242+
return true;
250243
}
251244

252245
// Developer/debugging methods

_extension/src/commands.ts

Lines changed: 0 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -24,118 +24,6 @@ export function registerEnablementCommands(context: vscode.ExtensionContext, tel
2424
}));
2525
}
2626

27-
export function registerLanguageCommands(
28-
context: vscode.ExtensionContext,
29-
client: Client,
30-
outputChannel: vscode.OutputChannel,
31-
traceOutputChannel: vscode.OutputChannel,
32-
telemetryReporter: tr.TelemetryReporter,
33-
): vscode.Disposable[] {
34-
const disposables: vscode.Disposable[] = [];
35-
36-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.restart", () => {
37-
telemetryReporter.sendTelemetryEvent("command.restartLanguageServer");
38-
return client.restart(context);
39-
}));
40-
41-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.output.focus", () => {
42-
outputChannel.show();
43-
}));
44-
45-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.lsp-trace.focus", () => {
46-
traceOutputChannel.show();
47-
}));
48-
49-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.selectVersion", async () => {
50-
}));
51-
52-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.showMenu", showCommands));
53-
54-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.reportIssue", () => {
55-
telemetryReporter.sendTelemetryEvent("command.reportIssue");
56-
vscode.commands.executeCommand("workbench.action.openIssueReporter", {
57-
extensionId: "TypeScriptTeam.native-preview",
58-
});
59-
}));
60-
61-
// Developer/debugging commands
62-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.dev.runGC", async () => {
63-
try {
64-
await client.runGC();
65-
vscode.window.showInformationMessage("Garbage collection triggered");
66-
}
67-
catch (error) {
68-
vscode.window.showErrorMessage(`Failed to run GC: ${error}`);
69-
}
70-
}));
71-
72-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.dev.saveHeapProfile", async () => {
73-
const dir = await promptForProfileDirectory();
74-
if (!dir) return;
75-
try {
76-
const file = await client.saveHeapProfile(dir);
77-
vscode.window.showInformationMessage(`Heap profile saved to: ${file}`);
78-
}
79-
catch (error) {
80-
vscode.window.showErrorMessage(`Failed to save heap profile: ${error}`);
81-
}
82-
}));
83-
84-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.dev.saveAllocProfile", async () => {
85-
const dir = await promptForProfileDirectory();
86-
if (!dir) return;
87-
try {
88-
const file = await client.saveAllocProfile(dir);
89-
vscode.window.showInformationMessage(`Allocation profile saved to: ${file}`);
90-
}
91-
catch (error) {
92-
vscode.window.showErrorMessage(`Failed to save allocation profile: ${error}`);
93-
}
94-
}));
95-
96-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.dev.startCPUProfile", async () => {
97-
const dir = await promptForProfileDirectory();
98-
if (!dir) return;
99-
try {
100-
await client.startCPUProfile(dir);
101-
vscode.commands.executeCommand("setContext", "typescript.native-preview.cpuProfileRunning", true);
102-
vscode.window.showInformationMessage(`CPU profiling started. Profile will be saved to: ${dir}`);
103-
}
104-
catch (error) {
105-
vscode.window.showErrorMessage(`Failed to start CPU profile: ${error}`);
106-
vscode.commands.executeCommand("setContext", "typescript.native-preview.cpuProfileRunning", false);
107-
}
108-
}));
109-
110-
disposables.push(vscode.commands.registerCommand("typescript.native-preview.dev.stopCPUProfile", async () => {
111-
try {
112-
const file = await client.stopCPUProfile();
113-
vscode.commands.executeCommand("setContext", "typescript.native-preview.cpuProfileRunning", false);
114-
vscode.window.showInformationMessage(`CPU profile saved to: ${file}`);
115-
}
116-
catch (error) {
117-
vscode.window.showErrorMessage(`Failed to stop CPU profile: ${error}`);
118-
}
119-
}));
120-
121-
return disposables;
122-
}
123-
124-
async function promptForProfileDirectory(): Promise<string | undefined> {
125-
const defaultDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "";
126-
const dir = await vscode.window.showInputBox({
127-
prompt: "Enter directory path for profile output",
128-
value: defaultDir,
129-
validateInput: value => {
130-
if (!value.trim()) {
131-
return "Directory path is required";
132-
}
133-
return undefined;
134-
},
135-
});
136-
return dir?.trim();
137-
}
138-
13927
/**
14028
* Updates the TypeScript Native Preview setting and reloads extension host.
14129
*/
@@ -153,47 +41,6 @@ async function updateUseTsgoSetting(enable: boolean): Promise<void> {
15341
await restartExtHostOnChangeIfNeeded();
15442
}
15543

156-
/**
157-
* Shows the quick pick menu for TypeScript Native Preview commands
158-
*/
159-
async function showCommands(): Promise<void> {
160-
const commands: readonly { label: string; description: string; command: string; }[] = [
161-
{
162-
label: "$(refresh) Restart Server",
163-
description: "Restart the TypeScript Native Preview language server",
164-
command: "typescript.native-preview.restart",
165-
},
166-
{
167-
label: "$(output) Show TS Server Log",
168-
description: "Show the TypeScript Native Preview server log",
169-
command: "typescript.native-preview.output.focus",
170-
},
171-
{
172-
label: "$(debug-console) Show LSP Messages",
173-
description: "Show the LSP communication trace",
174-
command: "typescript.native-preview.lsp-trace.focus",
175-
},
176-
{
177-
label: "$(report) Report Issue",
178-
description: "Report an issue with TypeScript Native Preview",
179-
command: "typescript.native-preview.reportIssue",
180-
},
181-
{
182-
label: "$(stop-circle) Disable TypeScript Native Preview",
183-
description: "Switch back to the built-in TypeScript extension",
184-
command: "typescript.native-preview.disable",
185-
},
186-
];
187-
188-
const selected = await vscode.window.showQuickPick(commands, {
189-
placeHolder: "TypeScript Native Preview Commands",
190-
});
191-
192-
if (selected) {
193-
await vscode.commands.executeCommand(selected.command);
194-
}
195-
}
196-
19744
export const codeLensShowLocationsCommandName = "typescript.native-preview.codeLens.showLocations";
19845
export function registerCodeLensShowLocationsCommand(): vscode.Disposable {
19946
return vscode.commands.registerCommand(codeLensShowLocationsCommandName, showCodeLensLocations);

0 commit comments

Comments
 (0)