Skip to content

Commit baae7a1

Browse files
authored
Miscellaneous extension improvements (#3516)
1 parent 33b9297 commit baae7a1

5 files changed

Lines changed: 302 additions & 35 deletions

File tree

_extension/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@
6868
"experimental"
6969
]
7070
},
71+
"typescript.native-preview.additionalTsdkLocations": {
72+
"type": "array",
73+
"items": {
74+
"type": "string"
75+
},
76+
"default": [],
77+
"description": "Additional paths to tsgo binary directories or @typescript/native-preview packages that should appear in the version selector.",
78+
"tags": [
79+
"experimental"
80+
]
81+
},
82+
"typescript.native-preview.showDebugInfo": {
83+
"type": "boolean",
84+
"default": false,
85+
"description": "Show debug information (PID, executable path) in the server menu.",
86+
"tags": [
87+
"experimental"
88+
]
89+
},
7190
"typescript.native-preview.goMemLimit": {
7291
"type": "string",
7392
"description": "Set GOMEMLIMIT for the language server (e.g., '2048MiB', '4GiB'). See https://pkg.go.dev/runtime#hdr-Environment_Variables for more information.",

_extension/src/client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class Client implements vscode.Disposable {
8080
sendNotification: sendNotificationMiddleware,
8181
provideHover: () => undefined,
8282
},
83+
diagnosticCollectionName: "typescript",
8384
diagnosticPullOptions: {
8485
onChange: true,
8586
onSave: true,
@@ -248,6 +249,10 @@ export class Client implements vscode.Disposable {
248249
return this.exe;
249250
}
250251

252+
get serverPid(): number | undefined {
253+
return (this.client as any)?._serverProcess?.pid;
254+
}
255+
251256
/**
252257
* Initialize an API session and return the socket path for connecting.
253258
* This allows other extensions to get a direct connection to the API server.

_extension/src/extension.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
} from "./util";
1313

1414
import { TelemetryReporter as VSCodeTelemetryReporter } from "@vscode/extension-telemetry";
15-
import { SessionManager } from "./session";
15+
import {
16+
promptUseWorkspaceVersion,
17+
SessionManager,
18+
} from "./session";
1619
import { createTelemetryReporter } from "./telemetryReporting";
1720

1821
export interface ExtensionAPI {
@@ -111,6 +114,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<Extens
111114

112115
await sessionManager.start(context);
113116

117+
// Prompt user to use workspace version if one is detected and they haven't opted in yet.
118+
promptUseWorkspaceVersion(context).catch(err => {
119+
output.appendLine(`Error prompting to use workspace version: ${err}`);
120+
});
121+
114122
function onLanguageServerInitialized(listener: () => void): vscode.Disposable {
115123
if (sessionManager.currentSession?.client.isInitialized) {
116124
listener();

_extension/src/session.ts

Lines changed: 219 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as path from "path";
12
import * as vscode from "vscode";
23
import { ActiveJsTsEditorTracker } from "./activeJsTsEditorTracker";
34
import { Client } from "./client";
@@ -6,7 +7,12 @@ import { ManagedFileContextManager } from "./managedFileContext";
67
import { ProjectStatus } from "./projectStatus";
78
import { setupStatusBar } from "./statusBar";
89
import { TelemetryReporter } from "./telemetryReporting";
9-
import { getExe } from "./util";
10+
import {
11+
getBuiltinExePath,
12+
getExe,
13+
resolveTsdkPathToExe,
14+
useWorkspaceTsdkStorageKey,
15+
} from "./util";
1016

1117
/**
1218
* SessionManager's lifetime is equal to that of the extension. It is responsible
@@ -55,7 +61,7 @@ export class SessionManager implements vscode.Disposable {
5561
this.outputChannel.appendLine("Restarting TypeScript Native Preview...");
5662
await this.currentSession.dispose();
5763
}
58-
this.currentSession = new Session(this.outputChannel, this.traceOutputChannel, this.initializedEventEmitter, this.telemetryReporter);
64+
this.currentSession = new Session(context, this.outputChannel, this.traceOutputChannel, this.initializedEventEmitter, this.telemetryReporter);
5965
return this.currentSession.start(context);
6066
}
6167

@@ -91,19 +97,22 @@ export class SessionManager implements vscode.Disposable {
9197
class Session implements vscode.Disposable {
9298
client: Client;
9399
private disposables: vscode.Disposable[] = [];
100+
private context: vscode.ExtensionContext;
94101
private outputChannel: vscode.LogOutputChannel;
95102
private traceOutputChannel: vscode.LogOutputChannel;
96103
private telemetryReporter: TelemetryReporter;
97104
private initializedEventEmitter: vscode.EventEmitter<void>;
98105

99106
constructor(
107+
context: vscode.ExtensionContext,
100108
outputChannel: vscode.LogOutputChannel,
101109
traceOutputChannel: vscode.LogOutputChannel,
102110
initializedEventEmitter: vscode.EventEmitter<void>,
103111
telemetryReporter: TelemetryReporter,
104112
) {
105113
this.client = new Client(outputChannel, traceOutputChannel, initializedEventEmitter, telemetryReporter);
106114
this.disposables.push(this.client);
115+
this.context = context;
107116
this.outputChannel = outputChannel;
108117
this.traceOutputChannel = traceOutputChannel;
109118
this.telemetryReporter = telemetryReporter;
@@ -149,10 +158,13 @@ class Session implements vscode.Disposable {
149158
this.traceOutputChannel.show();
150159
}));
151160

152-
this.disposables.push(vscode.commands.registerCommand("typescript.native-preview.selectVersion", async () => {
161+
this.disposables.push(vscode.commands.registerCommand("typescript.selectTypeScriptVersion", async () => {
162+
await promptSelectVersion(this.context, this.client, this.outputChannel);
153163
}));
154164

155-
this.disposables.push(vscode.commands.registerCommand("typescript.native-preview.showMenu", showCommands));
165+
this.disposables.push(vscode.commands.registerCommand("typescript.native-preview.showMenu", () => {
166+
showCommands(this.client);
167+
}));
156168

157169
this.disposables.push(vscode.commands.registerCommand("typescript.native-preview.reportIssue", () => {
158170
this.telemetryReporter.sendTelemetryEvent("command.reportIssue");
@@ -247,8 +259,15 @@ class Session implements vscode.Disposable {
247259
}
248260
}
249261

250-
async function showCommands(): Promise<void> {
251-
const commands: readonly { label: string; description: string; command: string; }[] = [
262+
async function showCommands(client: Client): Promise<void> {
263+
interface CommandItem {
264+
label: string;
265+
description?: string;
266+
kind?: vscode.QuickPickItemKind;
267+
command?: string;
268+
action?: () => Promise<void>;
269+
}
270+
const commands: CommandItem[] = [
252271
{
253272
label: "$(refresh) Restart Server",
254273
description: "Restart the TypeScript Native Preview language server",
@@ -269,19 +288,212 @@ async function showCommands(): Promise<void> {
269288
description: "Report an issue with TypeScript Native Preview",
270289
command: "typescript.native-preview.reportIssue",
271290
},
291+
{
292+
label: "$(versions) Select Version",
293+
description: "Choose between bundled and workspace versions",
294+
command: "typescript.selectTypeScriptVersion",
295+
},
272296
{
273297
label: "$(stop-circle) Disable TypeScript Native Preview",
274298
description: "Switch back to the built-in TypeScript extension",
275299
command: "typescript.native-preview.disable",
276300
},
277301
];
278302

303+
const showDebugInfo = vscode.workspace.getConfiguration("typescript.native-preview").get<boolean>("showDebugInfo", false);
304+
if (showDebugInfo) {
305+
const exe = client.getCurrentExe();
306+
const pid = client.serverPid;
307+
commands.push({ label: "", kind: vscode.QuickPickItemKind.Separator });
308+
if (exe) {
309+
commands.push({
310+
label: `Executable`,
311+
description: exe.path,
312+
action: async () => {
313+
await vscode.env.clipboard.writeText(exe.path);
314+
vscode.window.showInformationMessage("Executable path copied to clipboard.");
315+
},
316+
});
317+
}
318+
if (pid) {
319+
commands.push({
320+
label: `PID`,
321+
description: `${pid}`,
322+
action: async () => {
323+
await vscode.env.clipboard.writeText(`${pid}`);
324+
vscode.window.showInformationMessage("Server PID copied to clipboard.");
325+
},
326+
});
327+
}
328+
}
329+
279330
const selected = await vscode.window.showQuickPick(commands, {
280331
placeHolder: "TypeScript Native Preview Commands",
281332
});
282333

283334
if (selected) {
284-
await vscode.commands.executeCommand(selected.command);
335+
if (selected.action) {
336+
await selected.action();
337+
}
338+
else if (selected.command) {
339+
await vscode.commands.executeCommand(selected.command);
340+
}
341+
}
342+
}
343+
344+
interface VersionQuickPickItem extends vscode.QuickPickItem {
345+
run(): Promise<void>;
346+
}
347+
348+
interface DetectedVersion {
349+
label: string;
350+
version: string;
351+
tsdkPath: string;
352+
exePath: string;
353+
}
354+
355+
async function findWorkspaceNativePreviewPackages(): Promise<DetectedVersion[]> {
356+
const results: DetectedVersion[] = [];
357+
for (const folder of vscode.workspace.workspaceFolders ?? []) {
358+
const packagePath = vscode.Uri.joinPath(folder.uri, "node_modules", "@typescript", "native-preview");
359+
const resolved = await resolveTsdkPathToExe(path.normalize(packagePath.fsPath));
360+
if (!resolved) continue;
361+
results.push({
362+
label: folder.name,
363+
version: resolved?.version ?? "unknown",
364+
tsdkPath: path.normalize(packagePath.fsPath),
365+
exePath: resolved?.path ?? "",
366+
});
367+
}
368+
return results;
369+
}
370+
371+
async function promptSelectVersion(context: vscode.ExtensionContext, client: Client, outputChannel: vscode.LogOutputChannel): Promise<void> {
372+
const config = vscode.workspace.getConfiguration("typescript.native-preview");
373+
const currentExePath = client.getCurrentExe()?.path;
374+
const builtinExe = await getBuiltinExePath(context);
375+
const workspaceVersions = await findWorkspaceNativePreviewPackages();
376+
const bundledVersion = context.extension.packageJSON.version as string;
377+
const items: VersionQuickPickItem[] = [];
378+
379+
// Bundled version
380+
items.push({
381+
label: (currentExePath === builtinExe.path ? "• " : "") + "Use Bundled Version",
382+
description: bundledVersion,
383+
detail: builtinExe.path,
384+
run: async () => {
385+
await context.workspaceState.update(useWorkspaceTsdkStorageKey, false);
386+
await config.update("tsdk", undefined, vscode.ConfigurationTarget.Workspace);
387+
outputChannel.appendLine("Switched to bundled tsgo version.");
388+
},
389+
});
390+
391+
// Workspace versions
392+
if (vscode.workspace.isTrusted) {
393+
for (const wsVersion of workspaceVersions) {
394+
const isActive = currentExePath === wsVersion.tsdkPath;
395+
items.push({
396+
label: (isActive ? "• " : "") + "Use Workspace Version",
397+
description: wsVersion.version,
398+
detail: wsVersion.tsdkPath,
399+
run: async () => {
400+
await context.workspaceState.update(useWorkspaceTsdkStorageKey, true);
401+
await config.update("tsdk", wsVersion.tsdkPath, vscode.ConfigurationTarget.Workspace);
402+
outputChannel.appendLine(`Switched to workspace tsgo version (${wsVersion.version}).`);
403+
},
404+
});
405+
}
406+
}
407+
else if (workspaceVersions.length > 0) {
408+
items.push({
409+
label: "",
410+
kind: vscode.QuickPickItemKind.Separator,
411+
run: async () => {},
412+
});
413+
items.push({
414+
label: "$(lock) Manage Workspace Trust to select a workspace version",
415+
run: async () => {
416+
await vscode.commands.executeCommand("workbench.trust.manage");
417+
},
418+
});
419+
}
420+
421+
// Additional tsdk locations from settings
422+
const additionalLocations = config.get<string[]>("additionalTsdkLocations", []);
423+
if (additionalLocations.length > 0) {
424+
items.push({
425+
label: "",
426+
kind: vscode.QuickPickItemKind.Separator,
427+
run: async () => {},
428+
});
429+
for (const loc of additionalLocations) {
430+
const resolved = await resolveTsdkPathToExe(loc);
431+
if (!resolved) continue;
432+
if (resolved.path === builtinExe.path) continue;
433+
if (workspaceVersions.some(ws => ws.exePath === resolved.path)) continue;
434+
const isActive = currentExePath === resolved.path;
435+
items.push({
436+
label: (isActive ? "• " : "") + "Use Custom Version",
437+
description: resolved.version,
438+
detail: resolved.path,
439+
run: async () => {
440+
await context.workspaceState.update(useWorkspaceTsdkStorageKey, true);
441+
await config.update("tsdk", loc, vscode.ConfigurationTarget.Workspace);
442+
outputChannel.appendLine(`Switched to custom tsgo version at ${loc}.`);
443+
},
444+
});
445+
}
446+
}
447+
448+
const selected = await vscode.window.showQuickPick<VersionQuickPickItem>(items, {
449+
placeHolder: "Select the TypeScript Native Preview version to use",
450+
});
451+
452+
if (selected) {
453+
await selected.run();
454+
// Restart server to pick up the new version
455+
await vscode.commands.executeCommand("typescript.native-preview.restart");
456+
}
457+
}
458+
459+
/**
460+
* If the workspace has `@typescript/native-preview` installed and the user
461+
* hasn't already opted in or dismissed the prompt, ask whether they'd like
462+
* to use the workspace version.
463+
*/
464+
export async function promptUseWorkspaceVersion(context: vscode.ExtensionContext): Promise<void> {
465+
if (!vscode.workspace.isTrusted) return;
466+
467+
const useWorkspaceTsdk = context.workspaceState.get<boolean>(useWorkspaceTsdkStorageKey, false);
468+
if (useWorkspaceTsdk) return; // already opted in
469+
470+
const suppressKey = "typescript.native-preview.suppressPromptWorkspaceTsdk";
471+
if (context.workspaceState.get<boolean>(suppressKey, false)) return;
472+
473+
const workspaceVersions = await findWorkspaceNativePreviewPackages();
474+
if (workspaceVersions.length === 0) return;
475+
476+
const wsVersion = workspaceVersions[0];
477+
const allow = "Allow";
478+
const dismiss = "Dismiss";
479+
const suppress = "Never in this Workspace";
480+
481+
const result = await vscode.window.showInformationMessage(
482+
`This workspace contains a TypeScript Native Preview version (${wsVersion.version}). Would you like to use the workspace version?`,
483+
allow,
484+
dismiss,
485+
suppress,
486+
);
487+
488+
if (result === allow) {
489+
if (!vscode.workspace.isTrusted) return;
490+
await context.workspaceState.update(useWorkspaceTsdkStorageKey, true);
491+
const config = vscode.workspace.getConfiguration("typescript.native-preview");
492+
await config.update("tsdk", wsVersion.tsdkPath, vscode.ConfigurationTarget.Workspace);
493+
await vscode.commands.executeCommand("typescript.native-preview.restart");
494+
}
495+
else if (result === suppress) {
496+
await context.workspaceState.update(suppressKey, true);
285497
}
286498
}
287499

0 commit comments

Comments
 (0)