Skip to content

Disable language services if Pyrefly extension installed + active #24987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 23, 2025
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.');
}
}