Skip to content

Commit cf686d2

Browse files
author
Kartik Raj
authored
Added support for multi root workspaces with the new language server server (#4244)
* Added functionality * News entry * Corrected functionality * Register command only once * Activate jedi only once * Make sure activation is filtered to resource * Corrected activationManager * Corrected configSettings bug * Added functional tests * Added unit tests * Handle multiple folders being removed simultaneously * code reviews
1 parent 37d6818 commit cf686d2

19 files changed

+373
-151
lines changed

news/2 Fixes/3008.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for multi root workspaces with the new language server server

src/client/activation/activationManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ export class ExtensionActivationManager implements IExtensionActivationManager {
7777
}
7878
}
7979
protected onWorkspaceFoldersChanged() {
80+
//If an activated workspace folder was removed, delete its key
81+
const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspaceKey(workspaceFolder.uri));
82+
const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys());
83+
const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0);
84+
if (activatedWkspcFoldersRemoved.length > 0) {
85+
for (const folder of activatedWkspcFoldersRemoved) {
86+
this.activatedWorkspaces.delete(folder);
87+
}
88+
}
8089
this.addRemoveDocOpenedHandlers();
8190
}
8291
protected hasMultipleWorkspaces() {

src/client/activation/activationService.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { EventName } from '../telemetry/constants';
1717
import { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types';
1818

1919
const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled';
20+
const workspacePathNameForGlobalWorkspaces = '';
2021
type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator };
2122

2223
@injectable()
2324
export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable {
25+
private lsActivatedWorkspaces = new Map<string, ILanguageServerActivator>();
2426
private currentActivator?: ActivatorInfo;
25-
private activatedOnce: boolean = false;
27+
private jediActivatedOnce: boolean = false;
2628
private readonly workspaceService: IWorkspaceService;
2729
private readonly output: OutputChannel;
2830
private readonly appShell: IApplicationShell;
@@ -40,45 +42,54 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
4042
const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry);
4143
disposables.push(this);
4244
disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)));
45+
disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
4346
}
4447

4548
public async activate(resource: Resource): Promise<void> {
46-
if (this.currentActivator || this.activatedOnce) {
47-
return;
48-
}
49-
this.resource = resource;
50-
this.activatedOnce = true;
51-
5249
let jedi = this.useJedi();
5350
if (!jedi) {
51+
if (this.lsActivatedWorkspaces.has(this.getWorkspacePathKey(resource))) {
52+
return;
53+
}
5454
const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined);
5555
this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors();
5656
if (diagnostic.length) {
5757
sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED);
5858
jedi = true;
5959
}
60+
} else {
61+
if (this.jediActivatedOnce) {
62+
return;
63+
}
64+
this.jediActivatedOnce = true;
6065
}
6166

67+
this.resource = resource;
6268
await this.logStartup(jedi);
63-
6469
let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet;
6570
let activator = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, activatorName);
6671
this.currentActivator = { jedi, activator };
6772

6873
try {
69-
await activator.activate();
70-
return;
74+
await activator.activate(resource);
75+
if (!jedi) {
76+
this.lsActivatedWorkspaces.set(this.getWorkspacePathKey(resource), activator);
77+
}
7178
} catch (ex) {
7279
if (jedi) {
7380
return;
7481
}
7582
//Language server fails, reverting to jedi
83+
if (this.jediActivatedOnce) {
84+
return;
85+
}
86+
this.jediActivatedOnce = true;
7687
jedi = true;
7788
await this.logStartup(jedi);
7889
activatorName = LanguageServerActivator.Jedi;
7990
activator = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, activatorName);
8091
this.currentActivator = { jedi, activator };
81-
await activator.activate();
92+
await activator.activate(resource);
8293
}
8394
}
8495

@@ -88,6 +99,19 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
8899
}
89100
}
90101

102+
protected onWorkspaceFoldersChanged() {
103+
//If an activated workspace folder was removed, dispose its activator
104+
const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspacePathKey(workspaceFolder.uri));
105+
const activatedWkspcKeys = Array.from(this.lsActivatedWorkspaces.keys());
106+
const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0);
107+
if (activatedWkspcFoldersRemoved.length > 0) {
108+
for (const folder of activatedWkspcFoldersRemoved) {
109+
this.lsActivatedWorkspaces.get(folder).dispose();
110+
this.lsActivatedWorkspaces.delete(folder);
111+
}
112+
}
113+
}
114+
91115
private async logStartup(isJedi: boolean): Promise<void> {
92116
const outputLine = isJedi
93117
? 'Starting Jedi Python language engine.'
@@ -119,4 +143,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
119143
const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
120144
return configurationService.getSettings(this.resource).jediEnabled;
121145
}
146+
private getWorkspacePathKey(resource: Resource): string {
147+
return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces);
148+
}
122149
}

src/client/activation/jedi.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { inject, injectable } from 'inversify';
55
import { DocumentFilter, languages } from 'vscode';
66
import { PYTHON } from '../common/constants';
7-
import { IConfigurationService, IExtensionContext, ILogger } from '../common/types';
7+
import { IConfigurationService, IExtensionContext, ILogger, Resource } from '../common/types';
88
import { IShebangCodeLensProvider } from '../interpreter/contracts';
99
import { IServiceContainer, IServiceManager } from '../ioc/types';
1010
import { JediFactory } from '../languageServices/jediProxyFactory';
@@ -33,7 +33,10 @@ export class JediExtensionActivator implements ILanguageServerActivator {
3333
this.documentSelector = PYTHON;
3434
}
3535

36-
public async activate(): Promise<void> {
36+
public async activate(resource: Resource): Promise<void> {
37+
if (this.jediFactory) {
38+
throw new Error('Jedi already started');
39+
}
3740
const context = this.context;
3841

3942
const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager));

src/client/activation/languageServer/activator.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato
3434
@inject(ILanguageServerFolderService)
3535
private readonly languageServerFolderService: ILanguageServerFolderService,
3636
@inject(IConfigurationService) private readonly configurationService: IConfigurationService
37-
) {}
37+
) { }
3838
@traceDecorators.error('Failed to activate language server')
39-
public async activate(): Promise<void> {
40-
const mainWorkspaceUri = this.workspace.hasWorkspaceFolders
41-
? this.workspace.workspaceFolders![0].uri
42-
: undefined;
43-
await this.ensureLanguageServerIsAvailable(mainWorkspaceUri);
44-
await this.manager.start(mainWorkspaceUri);
39+
public async activate(resource: Resource): Promise<void> {
40+
if (!resource) {
41+
resource = this.workspace.hasWorkspaceFolders
42+
? this.workspace.workspaceFolders![0].uri
43+
: undefined;
44+
}
45+
await this.ensureLanguageServerIsAvailable(resource);
46+
await this.manager.start(resource);
4547
}
4648
public dispose(): void {
4749
this.manager.dispose();

src/client/activation/languageServer/analysisOptions.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as path from 'path';
88
import { CancellationToken, CompletionContext, ConfigurationChangeEvent, Disposable, Event, EventEmitter, OutputChannel, Position, TextDocument } from 'vscode';
99
import { LanguageClientOptions, ProvideCompletionItemsSignature } from 'vscode-languageclient';
1010
import { IWorkspaceService } from '../../common/application/types';
11-
import { isTestExecution, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../common/constants';
11+
import { isTestExecution, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../common/constants';
1212
import { traceDecorators, traceError } from '../../common/logger';
1313
import { BANNER_NAME_PROPOSE_LS, IConfigurationService, IExtensionContext, IOutputChannel, IPathUtils, IPythonExtensionBanner, Resource } from '../../common/types';
1414
import { debounce } from '../../common/utils/decorators';
@@ -79,7 +79,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt
7979
properties['DatabasePath'] = path.join(this.context.extensionPath, this.languageServerFolder);
8080

8181
let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : [];
82-
const settings = this.configuration.getSettings();
82+
const settings = this.configuration.getSettings(this.resource);
8383
if (settings.autoComplete) {
8484
const extraPaths = settings.autoComplete.extraPaths;
8585
if (extraPaths && extraPaths.length > 0) {
@@ -99,11 +99,20 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt
9999

100100
this.excludedFiles = this.getExcludedFiles();
101101
this.typeshedPaths = this.getTypeshedPaths();
102-
102+
const workspaceFolder = this.workspace.getWorkspaceFolder(this.resource);
103+
const documentSelector = [
104+
{ scheme: 'file', language: PYTHON_LANGUAGE },
105+
{ scheme: 'untitled', language: PYTHON_LANGUAGE }
106+
];
107+
if (workspaceFolder){
108+
// tslint:disable-next-line:no-any
109+
(documentSelector[0] as any).pattern = `${workspaceFolder.uri.fsPath}/**/*`;
110+
}
103111
// Options to control the language client
104112
return {
105113
// Register the server for Python documents
106-
documentSelector: PYTHON,
114+
documentSelector,
115+
workspaceFolder,
107116
synchronize: {
108117
configurationSection: PYTHON_LANGUAGE
109118
},
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { Event, EventEmitter } from 'vscode';
8+
import { ICommandManager } from '../../common/application/types';
9+
import '../../common/extensions';
10+
import { IDisposable } from '../../common/types';
11+
import { ILanguageServerExtension } from '../types';
12+
13+
const loadExtensionCommand = 'python._loadLanguageServerExtension';
14+
15+
@injectable()
16+
export class LanguageServerExtension implements ILanguageServerExtension {
17+
public loadExtensionArgs?: {};
18+
protected readonly _invoked = new EventEmitter<void>();
19+
private disposable?: IDisposable;
20+
constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) { }
21+
public dispose() {
22+
if (this.disposable) {
23+
this.disposable.dispose();
24+
}
25+
}
26+
public register(): Promise<void> {
27+
if (this.disposable) {
28+
return;
29+
}
30+
this.disposable = this.commandManager.registerCommand(loadExtensionCommand, args => {
31+
this.loadExtensionArgs = args;
32+
this._invoked.fire();
33+
});
34+
}
35+
public get invoked(): Event<void> {
36+
return this._invoked.event;
37+
}
38+
}

src/client/activation/languageServer/manager.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,25 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7-
import { ICommandManager } from '../../common/application/types';
87
import '../../common/extensions';
98
import { traceDecorators } from '../../common/logger';
109
import { IDisposable, Resource } from '../../common/types';
1110
import { debounce } from '../../common/utils/decorators';
1211
import { IServiceContainer } from '../../ioc/types';
1312
import { captureTelemetry } from '../../telemetry';
1413
import { EventName } from '../../telemetry/constants';
15-
import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerManager } from '../types';
16-
17-
const loadExtensionCommand = 'python._loadLanguageServerExtension';
14+
import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension, ILanguageServerManager } from '../types';
1815

1916
@injectable()
2017
export class LanguageServerManager implements ILanguageServerManager {
21-
protected static loadExtensionArgs?: {};
2218
private languageServer?: ILanguageServer;
2319
private resource!: Resource;
2420
private disposables: IDisposable[] = [];
2521
constructor(
2622
@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer,
27-
@inject(ICommandManager) private readonly commandManager: ICommandManager,
28-
@inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions
29-
) {}
23+
@inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions,
24+
@inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension
25+
) { }
3026
public dispose() {
3127
if (this.languageServer) {
3228
this.languageServer.dispose();
@@ -46,15 +42,11 @@ export class LanguageServerManager implements ILanguageServerManager {
4642
await this.startLanguageServer();
4743
}
4844
protected registerCommandHandler() {
49-
const disposable = this.commandManager.registerCommand(loadExtensionCommand, args => {
50-
LanguageServerManager.loadExtensionArgs = args;
51-
this.loadExtensionIfNecessary();
52-
});
53-
this.disposables.push(disposable);
45+
this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables);
5446
}
5547
protected loadExtensionIfNecessary() {
56-
if (this.languageServer && LanguageServerManager.loadExtensionArgs) {
57-
this.languageServer.loadExtension(LanguageServerManager.loadExtensionArgs);
48+
if (this.languageServer && this.lsExtension.loadExtensionArgs) {
49+
this.languageServer.loadExtension(this.lsExtension.loadExtensionArgs);
5850
}
5951
}
6052
@debounce(1000)

src/client/activation/serviceRegistry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ import { InterpreterDataService } from './languageServer/interpreterDataService'
2121
import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from './languageServer/languageClientFactory';
2222
import { LanguageServer } from './languageServer/languageServer';
2323
import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService';
24+
import { LanguageServerExtension } from './languageServer/languageServerExtension';
2425
import { LanguageServerFolderService } from './languageServer/languageServerFolderService';
2526
import { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } from './languageServer/languageServerPackageRepository';
2627
import { LanguageServerPackageService } from './languageServer/languageServerPackageService';
2728
import { LanguageServerManager } from './languageServer/manager';
2829
import { PlatformData } from './languageServer/platformData';
29-
import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IInterpreterDataService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types';
30+
import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IInterpreterDataService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types';
3031

3132
export function registerTypes(serviceManager: IServiceManager) {
32-
serviceManager.addSingleton<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager);
3333
serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, LanguageServerExtensionActivationService);
34+
serviceManager.addSingleton<ILanguageServerExtension>(ILanguageServerExtension, LanguageServerExtension);
35+
serviceManager.add<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager);
3436
serviceManager.add<ILanguageServerActivator>(ILanguageServerActivator, JediExtensionActivator, LanguageServerActivator.Jedi);
3537
serviceManager.add<ILanguageServerActivator>(ILanguageServerActivator, LanguageServerExtensionActivator, LanguageServerActivator.DotNet);
3638
serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY);

src/client/activation/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export enum LanguageServerActivator {
2828

2929
export const ILanguageServerActivator = Symbol('ILanguageServerActivator');
3030
export interface ILanguageServerActivator extends IDisposable {
31-
activate(): Promise<void>;
31+
activate(resource: Resource): Promise<void>;
3232
}
3333

3434
export const IHttpClient = Symbol('IHttpClient');
@@ -88,6 +88,12 @@ export const ILanguageServerManager = Symbol('ILanguageServerManager');
8888
export interface ILanguageServerManager extends IDisposable {
8989
start(resource: Resource): Promise<void>;
9090
}
91+
export const ILanguageServerExtension = Symbol('ILanguageServerExtension');
92+
export interface ILanguageServerExtension extends IDisposable {
93+
readonly invoked: Event<void>;
94+
loadExtensionArgs?: {};
95+
register(): void;
96+
}
9197
export const ILanguageServer = Symbol('ILanguageServer');
9298
export interface ILanguageServer extends IDisposable {
9399
start(resource: Resource, options: LanguageClientOptions): Promise<void>;

src/client/common/configSettings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,17 @@ export class PythonSettings implements IPythonSettings {
373373
protected getPythonExecutable(pythonPath: string) {
374374
return getPythonExecutable(pythonPath);
375375
}
376+
protected onWorkspaceFoldersChanged() {
377+
//If an activated workspace folder was removed, delete its key
378+
const workspaceKeys = this.workspace.workspaceFolders!.map(workspaceFolder => workspaceFolder.uri.fsPath);
379+
const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys());
380+
const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0);
381+
if (activatedWkspcFoldersRemoved.length > 0) {
382+
for (const folder of activatedWkspcFoldersRemoved) {
383+
PythonSettings.pythonSettings.delete(folder);
384+
}
385+
}
386+
}
376387
protected initialize(): void {
377388
const onDidChange = () => {
378389
const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot);
@@ -382,6 +393,7 @@ export class PythonSettings implements IPythonSettings {
382393
// Let's defer the change notification.
383394
this.debounceChangeNotification();
384395
};
396+
this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
385397
this.disposables.push(this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)));
386398
this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
387399
if (event.affectsConfiguration('python')) {

0 commit comments

Comments
 (0)