Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1225,13 +1225,14 @@
"python.languageServer": {
"type": "string",
"enum": [
"Default",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this block could use an enumDescriptions to define the semantics of each of these.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you go: #16141

"Jedi",
"JediLSP",
"Pylance",
"Microsoft",
"None"
],
"default": "Jedi",
"default": "Default",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I remove this change now?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine for now, why do you think this needs to be removed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can probably stay regardless, but the other code makes Pylance the default (and IDK when we want to actually flip that switch; this PR or another.

@kimadeline Kim-Adeline Miguel (kimadeline) May 4, 2021

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll be fine; hopefully #16069 will be merged before this one, we can then merge default-language-server in main (PR here), and then when this PR makes it in default-language-server we can hold off on merging it in main again until we're good to go.

"description": "Defines type of the language server.",
"scope": "window"
},
Expand Down
46 changes: 46 additions & 0 deletions src/client/activation/common/defaultlanguageServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { injectable } from 'inversify';
import { PYLANCE_EXTENSION_ID } from '../../common/constants';
import { JediLSP } from '../../common/experiments/groups';
import { IDefaultLanguageServer, IExperimentService, IExtensions } from '../../common/types';
import { IServiceManager } from '../../ioc/types';
import { ILSExtensionApi } from '../node/languageServerFolderService';
import { LanguageServerType } from '../types';

export type PotentialDefault = LanguageServerType.Jedi | LanguageServerType.JediLSP | LanguageServerType.Node;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need not be exported.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch; this should be with IDefaultLanguageServer.


@injectable()
class DefaultLanguageServer implements IDefaultLanguageServer {
public readonly defaultLSType: PotentialDefault;

constructor(defaultServer: PotentialDefault) {
this.defaultLSType = defaultServer;
}
}

export async function setDefaultLanguageServer(
experimentService: IExperimentService,
extensions: IExtensions,
serviceManager: IServiceManager,
): Promise<void> {
const lsType = await getDefaultLanguageServer(experimentService, extensions);
serviceManager.addSingletonInstance<IDefaultLanguageServer>(
IDefaultLanguageServer,
new DefaultLanguageServer(lsType),
);
}

async function getDefaultLanguageServer(
experimentService: IExperimentService,
extensions: IExtensions,
): Promise<PotentialDefault> {
if (extensions.getExtension<ILSExtensionApi>(PYLANCE_EXTENSION_ID)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have liked to call activate here and figure out of Pylance is functional or not, but Pylance hard deps on Python, and this means that activate will block forever due to the cycle.

We'll have to rely on later checks to fallback to Jedi.

return LanguageServerType.Node;
}

return (await experimentService.inExperiment(JediLSP.experiment))
Comment thread
kimadeline marked this conversation as resolved.
? LanguageServerType.JediLSP
: LanguageServerType.Jedi;
}
27 changes: 17 additions & 10 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class PythonSettings implements IPythonSettings {
private readonly experimentsManager?: IExperimentsManager,
private readonly interpreterPathService?: IInterpreterPathService,
private readonly interpreterSecurityService?: IInterpreterSecurityService,
private readonly defaultJedi?: IDefaultLanguageServer,
private readonly defaultLS?: IDefaultLanguageServer,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meaning of IDefaultLanguageServer has changed to return what "Default" will mean; before it also checked the user's settings, which was too much. This file is what actually determines the final result.

) {
this.workspace = workspace || new WorkspaceService();
this.workspaceRoot = workspaceFolder;
Expand All @@ -181,7 +181,7 @@ export class PythonSettings implements IPythonSettings {
experimentsManager?: IExperimentsManager,
interpreterPathService?: IInterpreterPathService,
interpreterSecurityService?: IInterpreterSecurityService,
defaultJedi?: IDefaultLanguageServer,
defaultLS?: IDefaultLanguageServer,
): PythonSettings {
workspace = workspace || new WorkspaceService();
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri;
Expand All @@ -195,7 +195,7 @@ export class PythonSettings implements IPythonSettings {
experimentsManager,
interpreterPathService,
interpreterSecurityService,
defaultJedi,
defaultLS,
);
PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
// Pass null to avoid VSC from complaining about not passing in a value.
Expand Down Expand Up @@ -284,14 +284,21 @@ export class PythonSettings implements IPythonSettings {

this.useIsolation = systemVariables.resolveAny(pythonSettings.get<boolean>('useIsolation', true))!;

const defaultServer = this.defaultJedi
? this.defaultJedi.defaultLSType
: pythonSettings.get<LanguageServerType>('languageServer');
let ls = defaultServer ?? LanguageServerType.Jedi;
ls = systemVariables.resolveAny(ls);
if (!Object.values(LanguageServerType).includes(ls)) {
ls = LanguageServerType.Jedi;
// Get as a string and verify; don't just accept.
let userLS = pythonSettings.get<string>('languageServer');
userLS = systemVariables.resolveAny(userLS);

let ls: LanguageServerType;
if (
!userLS ||
userLS === 'Default' ||
!Object.values(LanguageServerType).includes(userLS as LanguageServerType)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this condition signifies. Does this mean if user has selected an invalid LS value, we select the default LS in those cases?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. We can't trust user values from enums, so this checks for invalid ones and then selects the default if they are present. That's sort of the problem with the VS code settings API; it's too easy to make mistakes.

) {
ls = this.defaultLS?.defaultLSType ?? LanguageServerType.Jedi;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultLS can be undefined, so I had to hardcode some default here, but I'm honestly not sure under which conditions this can be the case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you removed the condition in setDefaultLanguageServer I don't see why it would ever be undefined either, unless this instance gets added before setDefaultLanguageServer is called?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried making it required, and hit a bunch of places that simply don't have access to the data needed, so IDK what's going on there.

} else {
ls = userLS as LanguageServerType;
}

this.languageServer = ls;

this.jediPath = systemVariables.resolveAny(pythonSettings.get<string>('jediPath'))!;
Expand Down
4 changes: 2 additions & 2 deletions src/client/common/configuration/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ export class ConfigurationService implements IConfigurationService {
const interpreterSecurityService = this.serviceContainer.get<IInterpreterSecurityService>(
IInterpreterSecurityService,
);
const defaultJedi = this.serviceContainer.tryGet<IDefaultLanguageServer>(IDefaultLanguageServer);
const defaultLS = this.serviceContainer.tryGet<IDefaultLanguageServer>(IDefaultLanguageServer);
return PythonSettings.getInstance(
resource,
InterpreterAutoSelectionService,
this.workspaceService,
experiments,
interpreterPathService,
interpreterSecurityService,
defaultJedi,
defaultLS,
);
}

Expand Down
46 changes: 2 additions & 44 deletions src/client/common/experiments/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@

'use strict';

import { injectable } from 'inversify';
import { LanguageServerType } from '../../activation/types';
import { IServiceManager } from '../../ioc/types';
import { IWorkspaceService } from '../application/types';
import { IDefaultLanguageServer, IExperimentService } from '../types';
import { DiscoveryVariants, JediLSP } from './groups';
import { IExperimentService } from '../types';
import { DiscoveryVariants } from './groups';

export async function inDiscoveryExperiment(experimentService: IExperimentService): Promise<boolean> {
const results = await Promise.all([
Expand All @@ -17,41 +13,3 @@ export async function inDiscoveryExperiment(experimentService: IExperimentServic
]);
return results.includes(true);
}

@injectable()
class DefaultLanguageServer implements IDefaultLanguageServer {
public readonly defaultLSType: LanguageServerType.Jedi | LanguageServerType.JediLSP;

constructor(defaultServer: LanguageServerType.Jedi | LanguageServerType.JediLSP) {
this.defaultLSType = defaultServer;
}
}

export async function setDefaultLanguageServerByExperiment(
experimentService: IExperimentService,
workspaceService: IWorkspaceService,
serviceManager: IServiceManager,
): Promise<void> {
const settings = workspaceService.getConfiguration('python');
const lsSetting = settings.inspect('languageServer');
if (lsSetting) {
if (
lsSetting.globalValue ||
lsSetting.globalLanguageValue ||
lsSetting.workspaceFolderValue ||
lsSetting.workspaceFolderLanguageValue ||
lsSetting.workspaceValue ||
lsSetting.workspaceLanguageValue
) {
return Promise.resolve();
}
}
const lsType = (await experimentService.inExperiment(JediLSP.experiment))
? LanguageServerType.JediLSP
: LanguageServerType.Jedi;
serviceManager.addSingletonInstance<IDefaultLanguageServer>(
IDefaultLanguageServer,
new DefaultLanguageServer(lsType),
);
return Promise.resolve();
}
6 changes: 4 additions & 2 deletions src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
IDisposableRegistry,
IExperimentService,
IExperimentsManager,
IExtensions,
IOutputChannel,
} from './common/types';
import { noop } from './common/utils/misc';
Expand Down Expand Up @@ -61,7 +62,7 @@ import * as pythonEnvironments from './pythonEnvironments';

import { ActivationResult, ExtensionState } from './components';
import { Components } from './extensionInit';
import { setDefaultLanguageServerByExperiment } from './common/experiments/helpers';
import { setDefaultLanguageServer } from './activation/common/defaultlanguageServer';

export async function activateComponents(
// `ext` is passed to any extra activation funcs.
Expand Down Expand Up @@ -131,7 +132,8 @@ async function activateLegacy(ext: ExtensionState): Promise<ActivationResult> {
await experimentService.activate();

const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService);
await setDefaultLanguageServerByExperiment(experimentService, workspaceService, serviceManager);
const extensions = serviceContainer.get<IExtensions>(IExtensions);
await setDefaultLanguageServer(experimentService, extensions, serviceManager);

const configuration = serviceManager.get<IConfigurationService>(IConfigurationService);
// We should start logging using the log level as soon as possible, so set it as soon as we can access the level.
Expand Down
103 changes: 103 additions & 0 deletions src/test/activation/defaultLanguageServer.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { expect } from 'chai';
import { anything, instance, mock, when, verify } from 'ts-mockito';
import { Extension } from 'vscode';
import { setDefaultLanguageServer } from '../../client/activation/common/defaultlanguageServer';
import { LanguageServerType } from '../../client/activation/types';
import { PYLANCE_EXTENSION_ID } from '../../client/common/constants';
import { JediLSP } from '../../client/common/experiments/groups';
import { ExperimentService } from '../../client/common/experiments/service';
import { IDefaultLanguageServer, IExperimentService, IExtensions } from '../../client/common/types';
import { ServiceManager } from '../../client/ioc/serviceManager';
import { IServiceManager } from '../../client/ioc/types';

suite('Activation - setDefaultLanguageServer()', () => {
let experimentService: IExperimentService;
let extensions: IExtensions;
let extension: Extension<unknown>;
let serviceManager: IServiceManager;
setup(() => {
experimentService = mock(ExperimentService);
extensions = mock();
extension = mock();
serviceManager = mock(ServiceManager);
});

test('Pylance not installed and NOT in experiment', async () => {
let defaultServerType;

when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(undefined);
when(experimentService.inExperiment(JediLSP.experiment)).thenResolve(false);
when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall(
(_symbol, value: IDefaultLanguageServer) => {
defaultServerType = value.defaultLSType;
},
);

await setDefaultLanguageServer(instance(experimentService), instance(extensions), instance(serviceManager));

verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once();
verify(experimentService.inExperiment(JediLSP.experiment)).once();
verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once();
expect(defaultServerType).to.equal(LanguageServerType.Jedi);
});

test('Pylance not installed and in experiment', async () => {
let defaultServerType;
when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(undefined);
when(experimentService.inExperiment(JediLSP.experiment)).thenResolve(true);
when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall(
(_symbol, value: IDefaultLanguageServer) => {
defaultServerType = value.defaultLSType;
},
);

await setDefaultLanguageServer(instance(experimentService), instance(extensions), instance(serviceManager));

verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once();
verify(experimentService.inExperiment(JediLSP.experiment)).once();
verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once();
expect(defaultServerType).to.equal(LanguageServerType.JediLSP);
});

test('Pylance installed and NOT in experiment', async () => {
let defaultServerType;

when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(extension));
when(experimentService.inExperiment(JediLSP.experiment)).thenResolve(false);
when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall(
(_symbol, value: IDefaultLanguageServer) => {
defaultServerType = value.defaultLSType;
},
);

await setDefaultLanguageServer(instance(experimentService), instance(extensions), instance(serviceManager));

verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once();
verify(experimentService.inExperiment(JediLSP.experiment)).never();
verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once();
expect(defaultServerType).to.equal(LanguageServerType.Node);
});

test('Pylance installed and in experiment', async () => {
let defaultServerType;
when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(extension));
when(experimentService.inExperiment(JediLSP.experiment)).thenResolve(true);
when(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).thenCall(
(_symbol, value: IDefaultLanguageServer) => {
defaultServerType = value.defaultLSType;
},
);

await setDefaultLanguageServer(instance(experimentService), instance(extensions), instance(serviceManager));

verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once();
verify(experimentService.inExperiment(JediLSP.experiment)).never();
verify(serviceManager.addSingletonInstance<IDefaultLanguageServer>(IDefaultLanguageServer, anything())).once();
expect(defaultServerType).to.equal(LanguageServerType.Node);
});
});
Loading