From 9b8a62e8a436329767fc5d1e3b581c53adb3c455 Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Wed, 16 Apr 2025 15:02:11 -0500 Subject: [PATCH 1/9] disable language services if Pyrefly extension installed + active Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- .../common/defaultlanguageServer.ts | 12 ++++++---- src/client/common/configSettings.ts | 19 +++++++++++++-- src/client/common/constants.ts | 1 + src/client/common/types.ts | 17 +++++++++++++- src/client/languageServer/watcher.ts | 2 ++ .../defaultLanguageServer.unit.test.ts | 23 ++++++++++++++++--- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/client/activation/common/defaultlanguageServer.ts b/src/client/activation/common/defaultlanguageServer.ts index dc40a2c0ed5b..d6f8242db18e 100644 --- a/src/client/activation/common/defaultlanguageServer.ts +++ b/src/client/activation/common/defaultlanguageServer.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { PYLANCE_EXTENSION_ID, PYREFLY_EXTENSION_ID } from '../../common/constants'; import { IDefaultLanguageServer, IExtensions, DefaultLSType } from '../../common/types'; import { IServiceManager } from '../../ioc/types'; import { LanguageServerType } from '../types'; @@ -28,9 +28,13 @@ export async function setDefaultLanguageServer( } async function getDefaultLanguageServer(extensions: IExtensions): Promise { + let type = LanguageServerType.Jedi; if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { - return LanguageServerType.Node; + type = LanguageServerType.Node; } - - return LanguageServerType.Jedi; + + if (extensions.getExtension(PYREFLY_EXTENSION_ID)) { + return {type: "none or (if pyrefly language services disabled)", languageServerType: type}; + } + return {type: "always", languageServerType: type}; } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7ae3467b2cfd..a8d7f194aa2f 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -268,14 +268,29 @@ export class PythonSettings implements IPythonSettings { let userLS = pythonSettings.get('languageServer'); userLS = systemVariables.resolveAny(userLS); + // Default language server type: if `IDefaultLanguageServer?.defaultLSType` is undefined, default to `None`. + let defaultLS = LanguageServerType.None; + + let defaultLSType = this.defaultLS?.defaultLSType; + if (defaultLSType !== undefined) { + // If we are sure what to default the language server type to, use it. + if (defaultLSType.type === "always") { + defaultLS = defaultLSType.languageServerType; + } + // If Pyrefly extension is installed, keep defaultLS = None unless Pyrefly has disabled language services. + else if (defaultLSType.type === "none or (if pyrefly language services disabled)" && pythonSettings.get('pyrefly')?.get('disableLanguageServices') !== true) { + defaultLS = defaultLSType.languageServerType; + } + } + // Validate the user's input; if invalid, set it to the default. - if ( + else if ( !userLS || userLS === 'Default' || userLS === 'Microsoft' || !Object.values(LanguageServerType).includes(userLS as LanguageServerType) ) { - this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + this.languageServer = defaultLS; this.languageServerIsDefault = true; } else if (userLS === 'JediLSP') { // Switch JediLSP option to Jedi. 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/client/common/types.ts b/src/client/common/types.ts index 2cb393d89bdf..47f4e096c540 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -348,7 +348,22 @@ export interface IInterpreterPathService { copyOldInterpreterStorageValuesToNew(resource: Resource): Promise; } -export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.Node; +/** + * If Pyrefly extension is installed, LS default should be `None`. But if Pyrefly language services are disabled, + * fall back to T. + */ +export type PyreflyOr = { + type: 'none or (if pyrefly language services disabled)', + languageServerType: T, +}; + +export type Always = { + type: 'always', + languageServerType: T, +}; + +type DefaultLSTypes = LanguageServerType.Jedi | LanguageServerType.Node; +export type DefaultLSType = Always | PyreflyOr; /** * Interface used to retrieve the default language server. diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 39e6e0bb1ece..8f6a2fdbf2d2 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -297,6 +297,8 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang await this.refreshLanguageServer(resource); } else if (event.affectsConfiguration(`python.analysis.pylanceLspClientEnabled`, resource)) { await this.refreshLanguageServer(resource, /* forced */ true); + } else if (event.affectsConfiguration('python.pyrefly.languageServer', resource)) { + await this.refreshLanguageServer(resource); } }); } diff --git a/src/test/activation/defaultLanguageServer.unit.test.ts b/src/test/activation/defaultLanguageServer.unit.test.ts index a06a146b9e32..41ba7a1ab000 100644 --- a/src/test/activation/defaultLanguageServer.unit.test.ts +++ b/src/test/activation/defaultLanguageServer.unit.test.ts @@ -8,7 +8,7 @@ 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 { PYLANCE_EXTENSION_ID, PYREFLY_EXTENSION_ID } from '../../client/common/constants'; import { IDefaultLanguageServer, IExtensions } from '../../client/common/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; @@ -37,7 +37,7 @@ suite('Activation - setDefaultLanguageServer()', () => { verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); - expect(defaultServerType).to.equal(LanguageServerType.Jedi); + expect(defaultServerType).to.deep.equal({type: "always", languageServerType: LanguageServerType.Jedi}); }); test('Pylance installed', async () => { @@ -54,6 +54,23 @@ suite('Activation - setDefaultLanguageServer()', () => { verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); - expect(defaultServerType).to.equal(LanguageServerType.Node); + expect(defaultServerType).to.deep.equal({type: "always", languageServerType: LanguageServerType.Node}); + }); + + test('Pyrefly installed', async () => { + let defaultServerType; + + when(extensions.getExtension(PYREFLY_EXTENSION_ID)).thenReturn(instance(extension)); + when(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).thenCall( + (_symbol, value: IDefaultLanguageServer) => { + defaultServerType = value.defaultLSType; + }, + ); + + await setDefaultLanguageServer(instance(extensions), instance(serviceManager)); + + verify(extensions.getExtension(PYREFLY_EXTENSION_ID)).once(); + verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); + expect(defaultServerType).to.deep.equal({type: "none or (if pyrefly language services disabled)", languageServerType: LanguageServerType.Jedi}); }); }); From 2807972a9be60d66f22b33199a7d1d66c1448fba Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 18 Apr 2025 13:03:09 -0500 Subject: [PATCH 2/9] remove old changes Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- .../common/defaultlanguageServer.ts | 12 ++++------ src/client/common/configSettings.ts | 19 ++------------- src/client/common/types.ts | 17 +------------- .../defaultLanguageServer.unit.test.ts | 23 +++---------------- 4 files changed, 10 insertions(+), 61 deletions(-) diff --git a/src/client/activation/common/defaultlanguageServer.ts b/src/client/activation/common/defaultlanguageServer.ts index d6f8242db18e..dc40a2c0ed5b 100644 --- a/src/client/activation/common/defaultlanguageServer.ts +++ b/src/client/activation/common/defaultlanguageServer.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { PYLANCE_EXTENSION_ID, PYREFLY_EXTENSION_ID } from '../../common/constants'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; import { IDefaultLanguageServer, IExtensions, DefaultLSType } from '../../common/types'; import { IServiceManager } from '../../ioc/types'; import { LanguageServerType } from '../types'; @@ -28,13 +28,9 @@ export async function setDefaultLanguageServer( } async function getDefaultLanguageServer(extensions: IExtensions): Promise { - let type = LanguageServerType.Jedi; if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { - type = LanguageServerType.Node; + return LanguageServerType.Node; } - - if (extensions.getExtension(PYREFLY_EXTENSION_ID)) { - return {type: "none or (if pyrefly language services disabled)", languageServerType: type}; - } - return {type: "always", languageServerType: type}; + + return LanguageServerType.Jedi; } diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index a8d7f194aa2f..7ae3467b2cfd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -268,29 +268,14 @@ export class PythonSettings implements IPythonSettings { let userLS = pythonSettings.get('languageServer'); userLS = systemVariables.resolveAny(userLS); - // Default language server type: if `IDefaultLanguageServer?.defaultLSType` is undefined, default to `None`. - let defaultLS = LanguageServerType.None; - - let defaultLSType = this.defaultLS?.defaultLSType; - if (defaultLSType !== undefined) { - // If we are sure what to default the language server type to, use it. - if (defaultLSType.type === "always") { - defaultLS = defaultLSType.languageServerType; - } - // If Pyrefly extension is installed, keep defaultLS = None unless Pyrefly has disabled language services. - else if (defaultLSType.type === "none or (if pyrefly language services disabled)" && pythonSettings.get('pyrefly')?.get('disableLanguageServices') !== true) { - defaultLS = defaultLSType.languageServerType; - } - } - // Validate the user's input; if invalid, set it to the default. - else if ( + if ( !userLS || userLS === 'Default' || userLS === 'Microsoft' || !Object.values(LanguageServerType).includes(userLS as LanguageServerType) ) { - this.languageServer = defaultLS; + 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/types.ts b/src/client/common/types.ts index 47f4e096c540..2cb393d89bdf 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -348,22 +348,7 @@ export interface IInterpreterPathService { copyOldInterpreterStorageValuesToNew(resource: Resource): Promise; } -/** - * If Pyrefly extension is installed, LS default should be `None`. But if Pyrefly language services are disabled, - * fall back to T. - */ -export type PyreflyOr = { - type: 'none or (if pyrefly language services disabled)', - languageServerType: T, -}; - -export type Always = { - type: 'always', - languageServerType: T, -}; - -type DefaultLSTypes = LanguageServerType.Jedi | LanguageServerType.Node; -export type DefaultLSType = Always | PyreflyOr; +export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.Node; /** * Interface used to retrieve the default language server. diff --git a/src/test/activation/defaultLanguageServer.unit.test.ts b/src/test/activation/defaultLanguageServer.unit.test.ts index 41ba7a1ab000..a06a146b9e32 100644 --- a/src/test/activation/defaultLanguageServer.unit.test.ts +++ b/src/test/activation/defaultLanguageServer.unit.test.ts @@ -8,7 +8,7 @@ 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, PYREFLY_EXTENSION_ID } from '../../client/common/constants'; +import { PYLANCE_EXTENSION_ID } from '../../client/common/constants'; import { IDefaultLanguageServer, IExtensions } from '../../client/common/types'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceManager } from '../../client/ioc/types'; @@ -37,7 +37,7 @@ suite('Activation - setDefaultLanguageServer()', () => { verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); - expect(defaultServerType).to.deep.equal({type: "always", languageServerType: LanguageServerType.Jedi}); + expect(defaultServerType).to.equal(LanguageServerType.Jedi); }); test('Pylance installed', async () => { @@ -54,23 +54,6 @@ suite('Activation - setDefaultLanguageServer()', () => { verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); - expect(defaultServerType).to.deep.equal({type: "always", languageServerType: LanguageServerType.Node}); - }); - - test('Pyrefly installed', async () => { - let defaultServerType; - - when(extensions.getExtension(PYREFLY_EXTENSION_ID)).thenReturn(instance(extension)); - when(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).thenCall( - (_symbol, value: IDefaultLanguageServer) => { - defaultServerType = value.defaultLSType; - }, - ); - - await setDefaultLanguageServer(instance(extensions), instance(serviceManager)); - - verify(extensions.getExtension(PYREFLY_EXTENSION_ID)).once(); - verify(serviceManager.addSingletonInstance(IDefaultLanguageServer, anything())).once(); - expect(defaultServerType).to.deep.equal({type: "none or (if pyrefly language services disabled)", languageServerType: LanguageServerType.Jedi}); + expect(defaultServerType).to.equal(LanguageServerType.Node); }); }); From 580d5f3102a23a2bc849ed92efa7ca31ac54e45e Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 18 Apr 2025 13:10:36 -0500 Subject: [PATCH 3/9] use iextensions in configsettings Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/client/common/configSettings.ts | 16 +++++++++++++--- src/client/common/configuration/service.ts | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7ae3467b2cfd..354c512038b8 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,8 +279,14 @@ export class PythonSettings implements IPythonSettings { userLS === 'Microsoft' || !Object.values(LanguageServerType).includes(userLS as LanguageServerType) ) { - this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; - this.languageServerIsDefault = true; + if ( + this.extensions.getExtension(PYREFLY_EXTENSION_ID) && + pythonSettings.get('pyrefly')?.get('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. this.languageServer = LanguageServerType.Jedi; diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 219c8727ca16..8879f975fb28 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -8,7 +8,7 @@ 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 +29,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, ); } From 9eddd84cd276928765dbfc89e853226a19bcb136 Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 18 Apr 2025 17:57:50 -0500 Subject: [PATCH 4/9] finish Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/client/common/configSettings.ts | 2 +- .../configSettings.pythonPath.unit.test.ts | 10 ++++++++++ .../configSettings.unit.test.ts | 4 ++++ src/test/extensionSettings.ts | 3 +++ src/test/mocks/extensions.ts | 19 +++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/test/mocks/extensions.ts diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 354c512038b8..810db6cbab7d 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -281,7 +281,7 @@ export class PythonSettings implements IPythonSettings { ) { if ( this.extensions.getExtension(PYREFLY_EXTENSION_ID) && - pythonSettings.get('pyrefly')?.get('disableLanguageServices') !== true) { + pythonSettings.get('pyrefly.disableLanguageServices') !== true) { this.languageServer = LanguageServerType.None; } else { this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index 29082bb5854f..aa8048966eb7 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..3e735d1bbdb6 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 { @@ -47,6 +48,7 @@ suite('Python Settings', async () => { const workspaceService = new WorkspaceService(); const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); + const extensions = new MockExtensions(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); expected = new CustomPythonSettings( undefined, @@ -56,6 +58,7 @@ suite('Python Settings', async () => { remoteName: undefined, } as IApplicationEnvironment), undefined, + extensions ); settings = new CustomPythonSettings( undefined, @@ -65,6 +68,7 @@ suite('Python Settings', async () => { remoteName: undefined, } as IApplicationEnvironment), undefined, + extensions ); expected.defaultInterpreterPath = 'python'; }); 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/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..bc0d608a86ec --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -0,0 +1,19 @@ +import { injectable } from 'inversify'; +import { IExtensions } from '../../client/common/types'; +import { Extension, Event } from 'vscode'; + +@injectable() +export class MockExtensions implements IExtensions { + 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 { + throw new Error('Method not implemented.'); + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string; }> { + throw new Error('Method not implemented.'); + } +} From f3107e38bfa48df3d4cccdfa84b50411354e1cda Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 18 Apr 2025 17:59:07 -0500 Subject: [PATCH 5/9] format Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/client/common/configSettings.ts | 13 +++++++------ src/client/common/configuration/service.ts | 8 +++++++- .../configSettings.pythonPath.unit.test.ts | 2 +- .../configSettings/configSettings.unit.test.ts | 4 ++-- src/test/mocks/extensions.ts | 4 ++-- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 810db6cbab7d..634e0106fe7b 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -281,12 +281,13 @@ export class PythonSettings implements IPythonSettings { ) { 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; + 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. this.languageServer = LanguageServerType.Jedi; diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index 8879f975fb28..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, IExtensions, IInterpreterPathService, IPythonSettings } from '../types'; +import { + IConfigurationService, + IDefaultLanguageServer, + IExtensions, + IInterpreterPathService, + IPythonSettings, +} from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts index aa8048966eb7..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -173,7 +173,7 @@ suite('Python Settings - pythonPath', () => { workspaceService.object, interpreterPathService.object, undefined, - new MockExtensions() + new MockExtensions(), ); 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 3e735d1bbdb6..e054246de8e6 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -58,7 +58,7 @@ suite('Python Settings', async () => { remoteName: undefined, } as IApplicationEnvironment), undefined, - extensions + extensions, ); settings = new CustomPythonSettings( undefined, @@ -68,7 +68,7 @@ suite('Python Settings', async () => { remoteName: undefined, } as IApplicationEnvironment), undefined, - extensions + extensions, ); expected.defaultInterpreterPath = 'python'; }); diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts index bc0d608a86ec..6f28831e62f0 100644 --- a/src/test/mocks/extensions.ts +++ b/src/test/mocks/extensions.ts @@ -10,10 +10,10 @@ export class MockExtensions implements IExtensions { })(); getExtension(extensionId: string): Extension | undefined; getExtension(extensionId: string): Extension | undefined; - getExtension(_extensionId: unknown): import("vscode").Extension | undefined { + getExtension(_extensionId: unknown): import('vscode').Extension | undefined { throw new Error('Method not implemented.'); } - determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string; }> { + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { throw new Error('Method not implemented.'); } } From e11009d9705b7bda43477d410653a13d8f367425 Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Mon, 21 Apr 2025 12:02:07 -0500 Subject: [PATCH 6/9] remove watcher Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/client/languageServer/watcher.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts index 8f6a2fdbf2d2..39e6e0bb1ece 100644 --- a/src/client/languageServer/watcher.ts +++ b/src/client/languageServer/watcher.ts @@ -297,8 +297,6 @@ export class LanguageServerWatcher implements IExtensionActivationService, ILang await this.refreshLanguageServer(resource); } else if (event.affectsConfiguration(`python.analysis.pylanceLspClientEnabled`, resource)) { await this.refreshLanguageServer(resource, /* forced */ true); - } else if (event.affectsConfiguration('python.pyrefly.languageServer', resource)) { - await this.refreshLanguageServer(resource); } }); } From 0a0cb6085d2ce47f1fba914023257d64ac4c5ea2 Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Mon, 21 Apr 2025 13:35:31 -0500 Subject: [PATCH 7/9] fix test Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/test/mocks/extensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts index 6f28831e62f0..86e7eaaf245e 100644 --- a/src/test/mocks/extensions.ts +++ b/src/test/mocks/extensions.ts @@ -7,7 +7,7 @@ export class MockExtensions implements IExtensions { 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 { From a23a0cbc5724b112dfe8b8ee8cfbf175664f160d Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Mon, 21 Apr 2025 22:23:48 -0500 Subject: [PATCH 8/9] format Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- src/test/mocks/extensions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts index 86e7eaaf245e..325f3f65092d 100644 --- a/src/test/mocks/extensions.ts +++ b/src/test/mocks/extensions.ts @@ -5,9 +5,9 @@ import { Extension, Event } from 'vscode'; @injectable() export class MockExtensions implements IExtensions { all: readonly Extension[] = []; - onDidChange: Event = (() => { + 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 { From 592717d4f713e0bb9864896552ecb6fadd79e00b Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Mon, 21 Apr 2025 23:35:35 -0500 Subject: [PATCH 9/9] fix tests for real Summary: Test Plan: Reviewers: Subscribers: Tasks: Tags: --- .../configSettings.unit.test.ts | 52 +++++++++++++++++-- src/test/mocks/extension.ts | 16 ++++++ src/test/mocks/extensions.ts | 8 ++- 3 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 src/test/mocks/extension.ts diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index e054246de8e6..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -41,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); @@ -48,7 +49,7 @@ suite('Python Settings', async () => { const workspaceService = new WorkspaceService(); const workspaceMemento = new MockMemento(); const globalMemento = new MockMemento(); - const extensions = new MockExtensions(); + extensions = new MockExtensions(); const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); expected = new CustomPythonSettings( undefined, @@ -57,7 +58,7 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, extensions, ); settings = new CustomPythonSettings( @@ -67,7 +68,7 @@ suite('Python Settings', async () => { new InterpreterPathService(persistentStateFactory, workspaceService, [], { remoteName: undefined, } as IApplicationEnvironment), - undefined, + { defaultLSType: LanguageServerType.Jedi }, extensions, ); expected.defaultInterpreterPath = 'python'; @@ -230,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 }, ]; @@ -239,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/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 index 325f3f65092d..efe9b6b8ca31 100644 --- a/src/test/mocks/extensions.ts +++ b/src/test/mocks/extensions.ts @@ -1,17 +1,21 @@ 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 { - throw new Error('Method not implemented.'); + 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.');