diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7ae3467b2cfd..634e0106fe7b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -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, @@ -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; @@ -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; @@ -164,6 +167,7 @@ export class PythonSettings implements IPythonSettings { workspace, interpreterPathService, defaultLS, + extensions, ); PythonSettings.pythonSettings.set(workspaceFolderKey, settings); settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event)); @@ -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('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. diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 219c8727ca16..443990b2e5da 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -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 { @@ -29,12 +35,14 @@ export class ConfigurationService implements IConfigurationService { ); const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); const defaultLS = this.serviceContainer.tryGet(IDefaultLanguageServer); + const extensions = this.serviceContainer.get(IExtensions); return PythonSettings.getInstance( resource, InterpreterAutoSelectionService, this.workspaceService, interpreterPathService, defaultLS, + extensions, ); } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 5ffa775bf04a..4a8962e86b58 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -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'; diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index 29082bb5854f..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -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 { @@ -64,6 +65,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -78,6 +80,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -93,6 +96,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -110,6 +114,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -126,6 +131,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -145,6 +151,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -166,6 +173,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, + new MockExtensions(), ); configSettings.update(pythonSettings.object); @@ -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'); @@ -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); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 43fbf17e970e..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -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 { @@ -40,6 +41,7 @@ suite('Python Settings', async () => { let config: TypeMoq.IMock; let expected: CustomPythonSettings; let settings: CustomPythonSettings; + let extensions: MockExtensions; setup(() => { sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); config = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Loose); @@ -47,6 +49,7 @@ suite('Python Settings', async () => { 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, @@ -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, @@ -64,7 +68,8 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, + extensions, ); expected.defaultInterpreterPath = 'python'; }); @@ -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 }, ]; @@ -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('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled); + + config + .setup((c) => c.get('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) { diff --git a/src/test/extensionSettings.ts b/src/test/extensionSettings.ts index 66a77589a770..2d35dcb5f4ca 100644 --- a/src/test/extensionSettings.ts +++ b/src/test/extensionSettings.ts @@ -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'); @@ -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(), @@ -49,5 +51,6 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings remoteName: undefined, } as IApplicationEnvironment), undefined, + extensions, ); } diff --git a/src/test/mocks/extension.ts b/src/test/mocks/extension.ts new file mode 100644 index 000000000000..61d70eb5ee9e --- /dev/null +++ b/src/test/mocks/extension.ts @@ -0,0 +1,16 @@ +import { injectable } from 'inversify'; +import { Extension, ExtensionKind, Uri } from 'vscode'; + +@injectable() +export class MockExtension implements Extension { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..efe9b6b8ca31 --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -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[] = []; + onDidChange: Event = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: unknown): import('vscode').Extension | undefined { + if (this.extensionIdsToFind.includes(extensionId)) { + return new MockExtension(); + } + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + throw new Error('Method not implemented.'); + } +}