Skip to content

Commit 1434a20

Browse files
committed
Fix restartLanguageServer command not found in command palette (#22045)
- Move python.analysis.restartLanguageServer command registration from individual language server managers to LanguageServerWatcher.register(), so it is always available regardless of which language server type is active - Fix async/forEach bug in restartLanguageServers() — forEach ignores returned Promises, so awaits inside were silently dropped; replaced with for...of - Fix resource URI reconstruction in restartLanguageServers() — map keys 'Pylance'/'None' are not URIs, Jedi keys are fsPath strings needing Uri.file() not Uri.parse() - Remove now-redundant static commandDispose and command registration from NodeLanguageServerManager and JediLanguageServerManager - Update unit tests: fix disposable count assertions, fix ICommandManager mocks, add test for restart command handler
1 parent 03f6e24 commit 1434a20

4 files changed

Lines changed: 103 additions & 35 deletions

File tree

src/client/activation/jedi/manager.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { IServiceContainer } from '../../ioc/types';
1313
import { PythonEnvironment } from '../../pythonEnvironments/info';
1414
import { captureTelemetry } from '../../telemetry';
1515
import { EventName } from '../../telemetry/constants';
16-
import { Commands } from '../commands';
1716
import { JediLanguageClientMiddleware } from './languageClientMiddleware';
1817
import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types';
1918
import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging';
@@ -27,8 +26,6 @@ export class JediLanguageServerManager implements ILanguageServerManager {
2726

2827
private disposables: IDisposable[] = [];
2928

30-
private static commandDispose: IDisposable;
31-
3229
private connected = false;
3330

3431
private lsVersion: string | undefined;
@@ -37,15 +34,8 @@ export class JediLanguageServerManager implements ILanguageServerManager {
3734
private readonly serviceContainer: IServiceContainer,
3835
private readonly analysisOptions: ILanguageServerAnalysisOptions,
3936
private readonly languageServerProxy: ILanguageServerProxy,
40-
commandManager: ICommandManager,
41-
) {
42-
if (JediLanguageServerManager.commandDispose) {
43-
JediLanguageServerManager.commandDispose.dispose();
44-
}
45-
JediLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => {
46-
this.restartLanguageServer().ignoreErrors();
47-
});
48-
}
37+
_commandManager: ICommandManager,
38+
) {}
4939

5040
private static versionTelemetryProps(instance: JediLanguageServerManager) {
5141
return {
@@ -55,7 +45,6 @@ export class JediLanguageServerManager implements ILanguageServerManager {
5545

5646
public dispose(): void {
5747
this.stopLanguageServer().ignoreErrors();
58-
JediLanguageServerManager.commandDispose.dispose();
5948
this.disposables.forEach((d) => d.dispose());
6049
}
6150

src/client/activation/node/manager.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { IServiceContainer } from '../../ioc/types';
99
import { PythonEnvironment } from '../../pythonEnvironments/info';
1010
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
1111
import { EventName } from '../../telemetry/constants';
12-
import { Commands } from '../commands';
1312
import { NodeLanguageClientMiddleware } from './languageClientMiddleware';
1413
import { ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types';
1514
import { traceDecoratorError, traceDecoratorVerbose } from '../../logging';
@@ -31,23 +30,13 @@ export class NodeLanguageServerManager implements ILanguageServerManager {
3130

3231
private started = false;
3332

34-
private static commandDispose: IDisposable;
35-
3633
constructor(
3734
private readonly serviceContainer: IServiceContainer,
3835
private readonly analysisOptions: ILanguageServerAnalysisOptions,
3936
private readonly languageServerProxy: NodeLanguageServerProxy,
40-
commandManager: ICommandManager,
37+
_commandManager: ICommandManager,
4138
private readonly extensions: IExtensions,
42-
) {
43-
if (NodeLanguageServerManager.commandDispose) {
44-
NodeLanguageServerManager.commandDispose.dispose();
45-
}
46-
NodeLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => {
47-
sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'command' });
48-
this.restartLanguageServer().ignoreErrors();
49-
});
50-
}
39+
) {}
5140

5241
private static versionTelemetryProps(instance: NodeLanguageServerManager) {
5342
return {
@@ -57,7 +46,6 @@ export class NodeLanguageServerManager implements ILanguageServerManager {
5746

5847
public dispose(): void {
5948
this.stopLanguageServer().ignoreErrors();
60-
NodeLanguageServerManager.commandDispose.dispose();
6149
this.disposables.forEach((d) => d.dispose());
6250
}
6351

src/client/languageServer/watcher.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types
3030
import { sendTelemetryEvent } from '../telemetry';
3131
import { EventName } from '../telemetry/constants';
3232
import { StopWatch } from '../common/utils/stopWatch';
33+
import { Commands } from '../activation/commands';
3334

3435
@injectable()
3536
/**
@@ -113,6 +114,13 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang
113114
}),
114115
);
115116

117+
this.disposables.push(
118+
this.commandManager.registerCommand(Commands.RestartLS, async () => {
119+
sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'command' });
120+
await this.restartLanguageServers();
121+
}),
122+
);
123+
116124
this.disposables.push(
117125
new LanguageServerChangeHandler(
118126
this.languageServerType,
@@ -195,12 +203,15 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang
195203
}
196204

197205
public async restartLanguageServers(): Promise<void> {
198-
this.workspaceLanguageServers.forEach(async (_, resourceString) => {
199-
sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'notebooksExperiment' });
200-
const resource = Uri.parse(resourceString);
206+
const resourceStrings = [...this.workspaceLanguageServers.keys()];
207+
for (const resourceString of resourceStrings) {
208+
const resource =
209+
resourceString === 'Pylance' || resourceString === 'None'
210+
? undefined
211+
: Uri.file(resourceString);
201212
await this.stopLanguageServer(resource);
202213
await this.startLanguageServer(this.languageServerType, resource);
203-
});
214+
}
204215
}
205216

206217
public async get(resource?: Resource): Promise<ILanguageServerExtensionManager> {

src/test/languageServer/watcher.unit.test.ts

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ILanguageServerExtensionManager } from '../../client/languageServer/typ
2727
import { LanguageServerWatcher } from '../../client/languageServer/watcher';
2828
import * as Logging from '../../client/logging';
2929
import { PythonEnvironment } from '../../client/pythonEnvironments/info';
30+
import { Commands } from '../../client/activation/commands';
3031

3132
suite('Language server watcher', () => {
3233
let watcher: LanguageServerWatcher;
@@ -119,7 +120,11 @@ suite('Language server watcher', () => {
119120
/* do nothing */
120121
},
121122
} as unknown) as IWorkspaceService,
122-
{} as ICommandManager,
123+
({
124+
registerCommand: () => {
125+
/* do nothing */
126+
},
127+
} as unknown) as ICommandManager,
123128
{} as IFileSystem,
124129
({
125130
getExtension: () => undefined,
@@ -131,7 +136,7 @@ suite('Language server watcher', () => {
131136
disposables,
132137
);
133138
watcher.register();
134-
assert.strictEqual(disposables.length, 11);
139+
assert.strictEqual(disposables.length, 13);
135140
});
136141

137142
test('The constructor should not add a listener to onDidChange to the list of disposables if it is not a trusted workspace', () => {
@@ -164,7 +169,11 @@ suite('Language server watcher', () => {
164169
/* do nothing */
165170
},
166171
} as unknown) as IWorkspaceService,
167-
{} as ICommandManager,
172+
({
173+
registerCommand: () => {
174+
/* do nothing */
175+
},
176+
} as unknown) as ICommandManager,
168177
{} as IFileSystem,
169178
({
170179
getExtension: () => undefined,
@@ -176,7 +185,7 @@ suite('Language server watcher', () => {
176185
disposables,
177186
);
178187
watcher.register();
179-
assert.strictEqual(disposables.length, 10);
188+
assert.strictEqual(disposables.length, 12);
180189
});
181190

182191
test(`When starting the language server, the language server extension manager should not be undefined`, async () => {
@@ -488,6 +497,77 @@ suite('Language server watcher', () => {
488497
assert.ok(startLanguageServerFake.calledTwice);
489498
});
490499

500+
test('When the "Restart Language Server" command is executed, all running language servers should be stopped and restarted', async () => {
501+
let restartCommandHandler: (() => Promise<void>) | undefined;
502+
503+
watcher = new LanguageServerWatcher(
504+
({
505+
get: () => {
506+
/* do nothing */
507+
},
508+
} as unknown) as IServiceContainer,
509+
{} as ILanguageServerOutputChannel,
510+
{
511+
getSettings: () => ({ languageServer: LanguageServerType.None }),
512+
} as IConfigurationService,
513+
{} as IExperimentService,
514+
({
515+
getActiveWorkspaceUri: () => undefined,
516+
} as unknown) as IInterpreterHelper,
517+
({
518+
onDidChange: () => {
519+
/* do nothing */
520+
},
521+
} as unknown) as IInterpreterPathService,
522+
({
523+
getActiveInterpreter: () => 'python',
524+
onDidChangeInterpreterInformation: () => {
525+
/* do nothing */
526+
},
527+
} as unknown) as IInterpreterService,
528+
{} as IEnvironmentVariablesProvider,
529+
({
530+
getWorkspaceFolder: (uri: Uri) => ({ uri }),
531+
onDidChangeConfiguration: () => {
532+
/* do nothing */
533+
},
534+
onDidChangeWorkspaceFolders: () => {
535+
/* do nothing */
536+
},
537+
} as unknown) as IWorkspaceService,
538+
({
539+
registerCommand: (command: string, handler: () => Promise<void>) => {
540+
if (command === Commands.RestartLS) {
541+
restartCommandHandler = handler;
542+
}
543+
},
544+
} as unknown) as ICommandManager,
545+
{} as IFileSystem,
546+
({
547+
getExtension: () => undefined,
548+
onDidChange: () => {
549+
/* do nothing */
550+
},
551+
} as unknown) as IExtensions,
552+
{} as IApplicationShell,
553+
disposables,
554+
);
555+
watcher.register();
556+
557+
await watcher.startLanguageServer(LanguageServerType.None);
558+
559+
const stopLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'stopLanguageServer');
560+
stopLanguageServerStub.returns(Promise.resolve());
561+
const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer');
562+
startLanguageServerStub.returns(Promise.resolve());
563+
564+
assert.ok(restartCommandHandler, 'Restart command handler should be registered');
565+
await restartCommandHandler!();
566+
567+
assert.ok(stopLanguageServerStub.calledOnce, 'stopLanguageServer should be called once');
568+
assert.ok(startLanguageServerStub.calledOnce, 'startLanguageServer should be called once');
569+
});
570+
491571
test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is Jedi, do not instantiate a language server', async () => {
492572
const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer');
493573
startLanguageServerStub.returns(Promise.resolve());

0 commit comments

Comments
 (0)