Skip to content
Merged
15 changes: 13 additions & 2 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ import { sendSettingTelemetry } from '../telemetry/envFileTelemetry';
import { ITestingSettings } from '../testing/configuration/types';
import { IWorkspaceService } from './application/types';
import { WorkspaceService } from './application/workspace';
import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants';
import { DEFAULT_INTERPRETER_SETTING, isTestExecution, PYREFLY_EXTENSION_ID } from './constants';
import {
IAutoCompleteSettings,
IDefaultLanguageServer,
IExperiments,
IExtensions,
IInterpreterPathService,
IInterpreterSettings,
IPythonSettings,
Expand Down Expand Up @@ -140,6 +141,7 @@ export class PythonSettings implements IPythonSettings {
workspace: IWorkspaceService,
private readonly interpreterPathService: IInterpreterPathService,
private readonly defaultLS: IDefaultLanguageServer | undefined,
private readonly extensions: IExtensions,
) {
this.workspace = workspace || new WorkspaceService();
this.workspaceRoot = workspaceFolder;
Expand All @@ -152,6 +154,7 @@ export class PythonSettings implements IPythonSettings {
workspace: IWorkspaceService,
interpreterPathService: IInterpreterPathService,
defaultLS: IDefaultLanguageServer | undefined,
extensions: IExtensions,
): PythonSettings {
workspace = workspace || new WorkspaceService();
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri;
Expand All @@ -164,6 +167,7 @@ export class PythonSettings implements IPythonSettings {
workspace,
interpreterPathService,
defaultLS,
extensions,
);
PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event));
Expand Down Expand Up @@ -275,7 +279,14 @@ export class PythonSettings implements IPythonSettings {
userLS === 'Microsoft' ||
!Object.values(LanguageServerType).includes(userLS as LanguageServerType)
) {
this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None;
if (
this.extensions.getExtension(PYREFLY_EXTENSION_ID) &&
pythonSettings.get<boolean>('pyrefly.disableLanguageServices') !== true
) {
this.languageServer = LanguageServerType.None;
} else {
this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None;
}
this.languageServerIsDefault = true;
} else if (userLS === 'JediLSP') {
// Switch JediLSP option to Jedi.
Expand Down
10 changes: 9 additions & 1 deletion src/client/common/configuration/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { IServiceContainer } from '../../ioc/types';
import { IWorkspaceService } from '../application/types';
import { PythonSettings } from '../configSettings';
import { isUnitTestExecution } from '../constants';
import { IConfigurationService, IDefaultLanguageServer, IInterpreterPathService, IPythonSettings } from '../types';
import {
IConfigurationService,
IDefaultLanguageServer,
IExtensions,
IInterpreterPathService,
IPythonSettings,
} from '../types';

@injectable()
export class ConfigurationService implements IConfigurationService {
Expand All @@ -29,12 +35,14 @@ export class ConfigurationService implements IConfigurationService {
);
const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
const defaultLS = this.serviceContainer.tryGet<IDefaultLanguageServer>(IDefaultLanguageServer);
const extensions = this.serviceContainer.get<IExtensions>(IExtensions);
return PythonSettings.getInstance(
resource,
InterpreterAutoSelectionService,
this.workspaceService,
interpreterPathService,
defaultLS,
extensions,
);
}

Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const PYTHON_NOTEBOOKS = [

export const PVSC_EXTENSION_ID = 'ms-python.python';
export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';
export const PYREFLY_EXTENSION_ID = 'meta.pyrefly';
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard';
export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry';
import { MockAutoSelectionService } from '../../mocks/autoSelector';
import { untildify } from '../../../client/common/helpers';
import { MockExtensions } from '../../mocks/extensions';

suite('Python Settings - pythonPath', () => {
class CustomPythonSettings extends PythonSettings {
Expand Down Expand Up @@ -64,6 +65,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -78,6 +80,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -93,6 +96,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);

configSettings.update(pythonSettings.object);
Expand All @@ -110,6 +114,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -126,6 +131,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -145,6 +151,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -166,6 +173,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
configSettings.update(pythonSettings.object);

Expand All @@ -184,6 +192,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'custom');
pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python');
Expand All @@ -204,6 +213,7 @@ suite('Python Settings - pythonPath', () => {
workspaceService.object,
interpreterPathService.object,
undefined,
new MockExtensions(),
);
interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python');
configSettings.update(pythonSettings.object);
Expand Down
54 changes: 50 additions & 4 deletions src/test/common/configSettings/configSettings.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ITestingSettings } from '../../../client/testing/configuration/types';
import { MockAutoSelectionService } from '../../mocks/autoSelector';
import { MockMemento } from '../../mocks/mementos';
import { untildify } from '../../../client/common/helpers';
import { MockExtensions } from '../../mocks/extensions';

suite('Python Settings', async () => {
class CustomPythonSettings extends PythonSettings {
Expand All @@ -40,13 +41,15 @@ suite('Python Settings', async () => {
let config: TypeMoq.IMock<WorkspaceConfiguration>;
let expected: CustomPythonSettings;
let settings: CustomPythonSettings;
let extensions: MockExtensions;
setup(() => {
sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns();
config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Loose);

const workspaceService = new WorkspaceService();
const workspaceMemento = new MockMemento();
const globalMemento = new MockMemento();
extensions = new MockExtensions();
const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento);
expected = new CustomPythonSettings(
undefined,
Expand All @@ -55,7 +58,8 @@ suite('Python Settings', async () => {
new InterpreterPathService(persistentStateFactory, workspaceService, [], {
remoteName: undefined,
} as IApplicationEnvironment),
undefined,
{ defaultLSType: LanguageServerType.Jedi },
extensions,
);
settings = new CustomPythonSettings(
undefined,
Expand All @@ -64,7 +68,8 @@ suite('Python Settings', async () => {
new InterpreterPathService(persistentStateFactory, workspaceService, [], {
remoteName: undefined,
} as IApplicationEnvironment),
undefined,
{ defaultLSType: LanguageServerType.Jedi },
extensions,
);
expected.defaultInterpreterPath = 'python';
});
Expand Down Expand Up @@ -226,7 +231,7 @@ suite('Python Settings', async () => {
const values = [
{ ls: LanguageServerType.Jedi, expected: LanguageServerType.Jedi, default: false },
{ ls: LanguageServerType.JediLSP, expected: LanguageServerType.Jedi, default: false },
{ ls: LanguageServerType.Microsoft, expected: LanguageServerType.None, default: true },
{ ls: LanguageServerType.Microsoft, expected: LanguageServerType.Jedi, default: true },
{ ls: LanguageServerType.Node, expected: LanguageServerType.Node, default: false },
{ ls: LanguageServerType.None, expected: LanguageServerType.None, default: false },
];
Expand All @@ -235,7 +240,48 @@ suite('Python Settings', async () => {
testLanguageServer(v.ls, v.expected, v.default);
});

testLanguageServer('invalid' as LanguageServerType, LanguageServerType.None, true);
testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi, true);
});

function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) {
test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${
pyreflyDisabled ? 'disabled' : 'enabled'
}`, () => {
if (pyreflyInstalled) {
extensions.extensionIdsToFind = ['meta.pyrefly'];
} else {
extensions.extensionIdsToFind = [];
}
config.setup((c) => c.get<boolean>('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled);

config
.setup((c) => c.get<string>('languageServer'))
.returns(() => undefined)
.verifiable(TypeMoq.Times.once());

settings.update(config.object);

if (languageServerDisabled) {
expect(settings.languageServer).to.equal(LanguageServerType.None);
} else {
expect(settings.languageServer).not.to.equal(LanguageServerType.None);
}
expect(settings.languageServerIsDefault).to.equal(true);
config.verifyAll();
});
}

suite('pyrefly languageServer settings', async () => {
const values = [
{ pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true },
{ pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false },
{ pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false },
{ pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false },
];

values.forEach((v) => {
testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled);
});
});

function testExperiments(enabled: boolean) {
Expand Down
3 changes: 3 additions & 0 deletions src/test/extensionSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PersistentStateFactory } from '../client/common/persistentState';
import { IPythonSettings, Resource } from '../client/common/types';
import { PythonEnvironment } from '../client/pythonEnvironments/info';
import { MockMemento } from './mocks/mementos';
import { MockExtensions } from './mocks/extensions';

export function getExtensionSettings(resource: Uri | undefined): IPythonSettings {
const vscode = require('vscode') as typeof import('vscode');
Expand Down Expand Up @@ -41,6 +42,7 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings
const workspaceMemento = new MockMemento();
const globalMemento = new MockMemento();
const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento);
const extensions = new MockExtensions();
return pythonSettings.PythonSettings.getInstance(
resource,
new AutoSelectionService(),
Expand All @@ -49,5 +51,6 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings
remoteName: undefined,
} as IApplicationEnvironment),
undefined,
extensions,
);
}
16 changes: 16 additions & 0 deletions src/test/mocks/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { injectable } from 'inversify';
import { Extension, ExtensionKind, Uri } from 'vscode';

@injectable()
export class MockExtension<T> implements Extension<T> {
id!: string;
extensionUri!: Uri;
extensionPath!: string;
isActive!: boolean;
packageJSON: any;
extensionKind!: ExtensionKind;
exports!: T;
activate(): Thenable<T> {
throw new Error('Method not implemented.');
}
}
23 changes: 23 additions & 0 deletions src/test/mocks/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { injectable } from 'inversify';
import { IExtensions } from '../../client/common/types';
import { Extension, Event } from 'vscode';
import { MockExtension } from './extension';

@injectable()
export class MockExtensions implements IExtensions {
extensionIdsToFind: unknown[] = [];
all: readonly Extension<unknown>[] = [];
onDidChange: Event<void> = () => {
throw new Error('Method not implemented');
};
getExtension(extensionId: string): Extension<unknown> | undefined;
getExtension<T>(extensionId: string): Extension<T> | undefined;
getExtension(extensionId: unknown): import('vscode').Extension<unknown> | undefined {
if (this.extensionIdsToFind.includes(extensionId)) {
return new MockExtension();
}
}
determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> {
throw new Error('Method not implemented.');
}
}