Skip to content

Commit e0e9e5b

Browse files
kyliauKeen Yee Liau
authored andcommitted
fix: restart the server with new options on configuration change
Currently, when users change their vscode configuration (say when they enable Ivy mode), they'll have to manually reload the window to force the new configuration to take effect. However, we cannot just start and stop the `LanguageClient` connection because the server options passed to the client initally is immutable. To fix this, we wrap the `LanguageClient` in our own class `AngularLanguageClient`, and reconstruct the server options every time the server is restarted. Code in `client/src/extension.ts` is copied to `client/src/client.ts` to avoid circular dependency between `extension.ts` and `commands.ts`.
1 parent 4f80416 commit e0e9e5b

File tree

3 files changed

+267
-192
lines changed

3 files changed

+267
-192
lines changed

client/src/client.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as fs from 'fs';
10+
import * as path from 'path';
11+
import * as vscode from 'vscode';
12+
import * as lsp from 'vscode-languageclient';
13+
14+
import {ProjectLoadingFinish, ProjectLoadingStart} from '../common/notifications';
15+
import {NgccProgress, NgccProgressToken, NgccProgressType} from '../common/progress';
16+
17+
import {ProgressReporter} from './progress-reporter';
18+
19+
export class AngularLanguageClient implements vscode.Disposable {
20+
private client: lsp.LanguageClient|null = null;
21+
private readonly disposables: vscode.Disposable[] = [];
22+
private readonly outputChannel: vscode.OutputChannel;
23+
private readonly clientOptions: lsp.LanguageClientOptions;
24+
private readonly name = 'Angular Language Service';
25+
26+
constructor(private readonly context: vscode.ExtensionContext) {
27+
this.outputChannel = vscode.window.createOutputChannel(this.name);
28+
// Options to control the language client
29+
this.clientOptions = {
30+
// Register the server for Angular templates and TypeScript documents
31+
documentSelector: [
32+
// scheme: 'file' means listen to changes to files on disk only
33+
// other option is 'untitled', for buffer in the editor (like a new doc)
34+
{scheme: 'file', language: 'html'},
35+
{scheme: 'file', language: 'typescript'},
36+
],
37+
synchronize: {
38+
fileEvents: [
39+
// Notify the server about file changes to tsconfig.json contained in the workspace
40+
vscode.workspace.createFileSystemWatcher('**/tsconfig.json'),
41+
]
42+
},
43+
// Don't let our output console pop open
44+
revealOutputChannelOn: lsp.RevealOutputChannelOn.Never,
45+
outputChannel: this.outputChannel,
46+
};
47+
}
48+
49+
/**
50+
* Spin up the language server in a separate process and establish a connection.
51+
*/
52+
async start(): Promise<void> {
53+
if (this.client !== null) {
54+
throw new Error(`An existing client is running. Call stop() first.`);
55+
}
56+
57+
// If the extension is launched in debug mode then the debug server options are used
58+
// Otherwise the run options are used
59+
const serverOptions: lsp.ServerOptions = {
60+
run: getServerOptions(this.context, false /* debug */),
61+
debug: getServerOptions(this.context, true /* debug */),
62+
};
63+
64+
// Create the language client and start the client.
65+
const forceDebug = process.env['NG_DEBUG'] === 'true';
66+
this.client = new lsp.LanguageClient(
67+
this.name,
68+
serverOptions,
69+
this.clientOptions,
70+
forceDebug,
71+
);
72+
this.disposables.push(this.client.start());
73+
await this.client.onReady();
74+
// Must wait for the client to be ready before registering notification
75+
// handlers.
76+
registerNotificationHandlers(this.client);
77+
registerProgressHandlers(this.client, this.context);
78+
}
79+
80+
/**
81+
* Kill the language client and perform some clean ups.
82+
*/
83+
async stop(): Promise<void> {
84+
if (this.client === null) {
85+
return;
86+
}
87+
await this.client.stop();
88+
this.outputChannel.clear();
89+
this.dispose();
90+
this.client = null;
91+
}
92+
93+
get initializeResult(): lsp.InitializeResult|undefined {
94+
return this.client?.initializeResult;
95+
}
96+
97+
dispose() {
98+
for (let d = this.disposables.pop(); d !== undefined; d = this.disposables.pop()) {
99+
d.dispose();
100+
}
101+
}
102+
}
103+
104+
function registerNotificationHandlers(client: lsp.LanguageClient) {
105+
client.onNotification(ProjectLoadingStart, () => {
106+
vscode.window.withProgress(
107+
{
108+
location: vscode.ProgressLocation.Window,
109+
title: 'Initializing Angular language features',
110+
},
111+
() => new Promise<void>((resolve) => {
112+
client.onNotification(ProjectLoadingFinish, resolve);
113+
}),
114+
);
115+
});
116+
}
117+
118+
function registerProgressHandlers(client: lsp.LanguageClient, context: vscode.ExtensionContext) {
119+
const progressReporters = new Map<string, ProgressReporter>();
120+
const disposable =
121+
client.onProgress(NgccProgressType, NgccProgressToken, async (params: NgccProgress) => {
122+
const {configFilePath} = params;
123+
if (!progressReporters.has(configFilePath)) {
124+
progressReporters.set(configFilePath, new ProgressReporter());
125+
}
126+
const reporter = progressReporters.get(configFilePath)!;
127+
if (params.done) {
128+
reporter.finish();
129+
progressReporters.delete(configFilePath);
130+
if (!params.success) {
131+
const selection = await vscode.window.showErrorMessage(
132+
`Failed to run ngcc. Ivy language service is disabled. ` +
133+
`Please see the extension output for more information.`,
134+
{modal: true},
135+
'See error message',
136+
);
137+
if (selection) {
138+
client.outputChannel.show();
139+
}
140+
}
141+
} else {
142+
reporter.report(params.message);
143+
}
144+
});
145+
// Dispose the progress handler on exit
146+
context.subscriptions.push(disposable);
147+
}
148+
149+
/**
150+
* Return the paths for the module that corresponds to the specified `configValue`,
151+
* and use the specified `bundled` as fallback if none is provided.
152+
* @param configName
153+
* @param bundled
154+
*/
155+
function getProbeLocations(configValue: string|null, bundled: string): string[] {
156+
const locations = [];
157+
// Always use config value if it's specified
158+
if (configValue) {
159+
locations.push(configValue);
160+
}
161+
// Prioritize the bundled version
162+
locations.push(bundled);
163+
// Look in workspaces currently open
164+
const workspaceFolders = vscode.workspace.workspaceFolders || [];
165+
for (const folder of workspaceFolders) {
166+
locations.push(folder.uri.fsPath);
167+
}
168+
return locations;
169+
}
170+
171+
/**
172+
* Construct the arguments that's used to spawn the server process.
173+
* @param ctx vscode extension context
174+
* @param debug true if debug mode is on
175+
*/
176+
function constructArgs(ctx: vscode.ExtensionContext, debug: boolean): string[] {
177+
const config = vscode.workspace.getConfiguration();
178+
const args: string[] = [];
179+
180+
const ngLog: string = config.get('angular.log', 'off');
181+
if (ngLog !== 'off') {
182+
// Log file does not yet exist on disk. It is up to the server to create the file.
183+
const logFile = path.join(ctx.logPath, 'nglangsvc.log');
184+
args.push('--logFile', logFile);
185+
args.push('--logVerbosity', debug ? 'verbose' : ngLog);
186+
}
187+
188+
const ngdk: string|null = config.get('angular.ngdk', null);
189+
const ngProbeLocations = getProbeLocations(ngdk, ctx.extensionPath);
190+
args.push('--ngProbeLocations', ngProbeLocations.join(','));
191+
192+
const experimentalIvy: boolean = config.get('angular.experimental-ivy', false);
193+
if (experimentalIvy) {
194+
args.push('--experimental-ivy');
195+
}
196+
197+
const tsdk: string|null = config.get('typescript.tsdk', null);
198+
const tsProbeLocations = getProbeLocations(tsdk, ctx.extensionPath);
199+
args.push('--tsProbeLocations', tsProbeLocations.join(','));
200+
201+
return args;
202+
}
203+
204+
function getServerOptions(ctx: vscode.ExtensionContext, debug: boolean): lsp.NodeModule {
205+
// Environment variables for server process
206+
const prodEnv = {
207+
// Force TypeScript to use the non-polling version of the file watchers.
208+
TSC_NONPOLLING_WATCHER: true,
209+
};
210+
const devEnv = {
211+
...prodEnv,
212+
NG_DEBUG: true,
213+
};
214+
215+
// Node module for the language server
216+
const prodBundle = ctx.asAbsolutePath('server');
217+
const devBundle = ctx.asAbsolutePath(path.join('dist', 'server', 'server.js'));
218+
219+
// Argv options for Node.js
220+
const prodExecArgv: string[] = [];
221+
const devExecArgv: string[] = [
222+
// do not lazily evaluate the code so all breakpoints are respected
223+
'--nolazy',
224+
// If debugging port is changed, update .vscode/launch.json as well
225+
'--inspect=6009',
226+
];
227+
228+
return {
229+
// VS Code Insider launches extensions in debug mode by default but users
230+
// install prod bundle so we have to check whether dev bundle exists.
231+
module: debug && fs.existsSync(devBundle) ? devBundle : prodBundle,
232+
transport: lsp.TransportKind.ipc,
233+
args: constructArgs(ctx, debug),
234+
options: {
235+
env: debug ? devEnv : prodEnv,
236+
execArgv: debug ? devExecArgv : prodExecArgv,
237+
},
238+
};
239+
}

client/src/commands.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import * as vscode from 'vscode';
10-
import * as lsp from 'vscode-languageclient';
1110
import {ServerOptions} from '../common/initialize';
11+
import {AngularLanguageClient} from './client';
1212

1313
/**
1414
* Represent a vscode command with an ID and an impl function `execute`.
@@ -23,30 +23,39 @@ interface Command {
2323
* @param client language client
2424
* @param context extension context for adding disposables
2525
*/
26-
function restartNgServer(client: lsp.LanguageClient, context: vscode.ExtensionContext): Command {
26+
function restartNgServer(client: AngularLanguageClient): Command {
2727
return {
2828
id: 'angular.restartNgServer',
2929
async execute() {
3030
await client.stop();
31-
context.subscriptions.push(client.start());
31+
await client.start();
3232
},
3333
};
3434
}
3535

3636
/**
3737
* Open the current server log file in a new editor.
3838
*/
39-
function openLogFile(client: lsp.LanguageClient): Command {
39+
function openLogFile(client: AngularLanguageClient): Command {
4040
return {
4141
id: 'angular.openLogFile',
4242
async execute() {
4343
const serverOptions: ServerOptions|undefined = client.initializeResult?.serverOptions;
4444
if (!serverOptions?.logFile) {
45-
// TODO: We could show a MessageItem to help users automatically update
46-
// the configuration option then restart the server, but we currently do
47-
// not reload the server options when restarting the server.
48-
vscode.window.showErrorMessage(
49-
`Angular Server logging is off. Please set 'angular.log' and reload the window.`);
45+
// Show a MessageItem to help users automatically update the
46+
// configuration option then restart the server.
47+
const selection = await vscode.window.showErrorMessage(
48+
`Angular server logging is off. Please set 'angular.log' and restart the server.`,
49+
'Enable logging and restart server',
50+
);
51+
if (selection) {
52+
const isGlobalConfig = false;
53+
await vscode.workspace.getConfiguration().update(
54+
'angular.log', 'verbose', isGlobalConfig);
55+
// Restart the server
56+
await client.stop();
57+
await client.start();
58+
}
5059
return;
5160
}
5261
const document = await vscode.workspace.openTextDocument(serverOptions.logFile);
@@ -61,9 +70,9 @@ function openLogFile(client: lsp.LanguageClient): Command {
6170
* @param context extension context for adding disposables
6271
*/
6372
export function registerCommands(
64-
client: lsp.LanguageClient, context: vscode.ExtensionContext): void {
73+
client: AngularLanguageClient, context: vscode.ExtensionContext): void {
6574
const commands: Command[] = [
66-
restartNgServer(client, context),
75+
restartNgServer(client),
6776
openLogFile(client),
6877
];
6978

0 commit comments

Comments
 (0)