Skip to content

Commit 7fea432

Browse files
authored
Disable language services if Pyrefly extension installed + active (#24987)
For #24850 Summary: Background: A new typechecker called Pyrefly will be featured at Pycon with a [talk](https://us.pycon.org/2025/schedule/presentation/118/), [website/sandbox](https://pyrefly.org/) (still WIP), and [extension](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly) (still WIP). This extension will provide ultrafast typechecking and language services. When the Pyrefly extension is installed, `ms-python.python` should not start Jedi or Pylance unless [`python.pyrefly.disableLanguageServices`](facebook/pyrefly@4d7e23c) is set to `true`. Because of the separation of vscode's `getExtensions` API and config reading logic, I chose to augment `DefaultLSType` with fallback information in case Pyrefly is disabled. This lets `configSettings` pick the correct jedi/pylance without knowing if Pyrefly will be enabled or disabled. Test Plan: still can't get pyright to work in the local extension build but I do see my breakpoints hit and the correct languageServer set https://github.com/user-attachments/assets/395bacbb-7ad0-4357-b084-cd5e88062801
1 parent 5a33fc1 commit 7fea432

File tree

8 files changed

+125
-7
lines changed

8 files changed

+125
-7
lines changed

src/client/common/configSettings.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import { sendSettingTelemetry } from '../telemetry/envFileTelemetry';
2121
import { ITestingSettings } from '../testing/configuration/types';
2222
import { IWorkspaceService } from './application/types';
2323
import { WorkspaceService } from './application/workspace';
24-
import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants';
24+
import { DEFAULT_INTERPRETER_SETTING, isTestExecution, PYREFLY_EXTENSION_ID } from './constants';
2525
import {
2626
IAutoCompleteSettings,
2727
IDefaultLanguageServer,
2828
IExperiments,
29+
IExtensions,
2930
IInterpreterPathService,
3031
IInterpreterSettings,
3132
IPythonSettings,
@@ -140,6 +141,7 @@ export class PythonSettings implements IPythonSettings {
140141
workspace: IWorkspaceService,
141142
private readonly interpreterPathService: IInterpreterPathService,
142143
private readonly defaultLS: IDefaultLanguageServer | undefined,
144+
private readonly extensions: IExtensions,
143145
) {
144146
this.workspace = workspace || new WorkspaceService();
145147
this.workspaceRoot = workspaceFolder;
@@ -152,6 +154,7 @@ export class PythonSettings implements IPythonSettings {
152154
workspace: IWorkspaceService,
153155
interpreterPathService: IInterpreterPathService,
154156
defaultLS: IDefaultLanguageServer | undefined,
157+
extensions: IExtensions,
155158
): PythonSettings {
156159
workspace = workspace || new WorkspaceService();
157160
const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri;
@@ -164,6 +167,7 @@ export class PythonSettings implements IPythonSettings {
164167
workspace,
165168
interpreterPathService,
166169
defaultLS,
170+
extensions,
167171
);
168172
PythonSettings.pythonSettings.set(workspaceFolderKey, settings);
169173
settings.onDidChange((event) => PythonSettings.debounceConfigChangeNotification(event));
@@ -275,7 +279,14 @@ export class PythonSettings implements IPythonSettings {
275279
userLS === 'Microsoft' ||
276280
!Object.values(LanguageServerType).includes(userLS as LanguageServerType)
277281
) {
278-
this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None;
282+
if (
283+
this.extensions.getExtension(PYREFLY_EXTENSION_ID) &&
284+
pythonSettings.get<boolean>('pyrefly.disableLanguageServices') !== true
285+
) {
286+
this.languageServer = LanguageServerType.None;
287+
} else {
288+
this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None;
289+
}
279290
this.languageServerIsDefault = true;
280291
} else if (userLS === 'JediLSP') {
281292
// Switch JediLSP option to Jedi.

src/client/common/configuration/service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { IServiceContainer } from '../../ioc/types';
88
import { IWorkspaceService } from '../application/types';
99
import { PythonSettings } from '../configSettings';
1010
import { isUnitTestExecution } from '../constants';
11-
import { IConfigurationService, IDefaultLanguageServer, IInterpreterPathService, IPythonSettings } from '../types';
11+
import {
12+
IConfigurationService,
13+
IDefaultLanguageServer,
14+
IExtensions,
15+
IInterpreterPathService,
16+
IPythonSettings,
17+
} from '../types';
1218

1319
@injectable()
1420
export class ConfigurationService implements IConfigurationService {
@@ -29,12 +35,14 @@ export class ConfigurationService implements IConfigurationService {
2935
);
3036
const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
3137
const defaultLS = this.serviceContainer.tryGet<IDefaultLanguageServer>(IDefaultLanguageServer);
38+
const extensions = this.serviceContainer.get<IExtensions>(IExtensions);
3239
return PythonSettings.getInstance(
3340
resource,
3441
InterpreterAutoSelectionService,
3542
this.workspaceService,
3643
interpreterPathService,
3744
defaultLS,
45+
extensions,
3846
);
3947
}
4048

src/client/common/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const PYTHON_NOTEBOOKS = [
2222

2323
export const PVSC_EXTENSION_ID = 'ms-python.python';
2424
export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';
25+
export const PYREFLY_EXTENSION_ID = 'meta.pyrefly';
2526
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
2627
export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard';
2728
export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255';

src/test/common/configSettings/configSettings.pythonPath.unit.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
1717
import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry';
1818
import { MockAutoSelectionService } from '../../mocks/autoSelector';
1919
import { untildify } from '../../../client/common/helpers';
20+
import { MockExtensions } from '../../mocks/extensions';
2021

2122
suite('Python Settings - pythonPath', () => {
2223
class CustomPythonSettings extends PythonSettings {
@@ -64,6 +65,7 @@ suite('Python Settings - pythonPath', () => {
6465
workspaceService.object,
6566
interpreterPathService.object,
6667
undefined,
68+
new MockExtensions(),
6769
);
6870
configSettings.update(pythonSettings.object);
6971

@@ -78,6 +80,7 @@ suite('Python Settings - pythonPath', () => {
7880
workspaceService.object,
7981
interpreterPathService.object,
8082
undefined,
83+
new MockExtensions(),
8184
);
8285
configSettings.update(pythonSettings.object);
8386

@@ -93,6 +96,7 @@ suite('Python Settings - pythonPath', () => {
9396
workspaceService.object,
9497
interpreterPathService.object,
9598
undefined,
99+
new MockExtensions(),
96100
);
97101

98102
configSettings.update(pythonSettings.object);
@@ -110,6 +114,7 @@ suite('Python Settings - pythonPath', () => {
110114
workspaceService.object,
111115
interpreterPathService.object,
112116
undefined,
117+
new MockExtensions(),
113118
);
114119
configSettings.update(pythonSettings.object);
115120

@@ -126,6 +131,7 @@ suite('Python Settings - pythonPath', () => {
126131
workspaceService.object,
127132
interpreterPathService.object,
128133
undefined,
134+
new MockExtensions(),
129135
);
130136
configSettings.update(pythonSettings.object);
131137

@@ -145,6 +151,7 @@ suite('Python Settings - pythonPath', () => {
145151
workspaceService.object,
146152
interpreterPathService.object,
147153
undefined,
154+
new MockExtensions(),
148155
);
149156
configSettings.update(pythonSettings.object);
150157

@@ -166,6 +173,7 @@ suite('Python Settings - pythonPath', () => {
166173
workspaceService.object,
167174
interpreterPathService.object,
168175
undefined,
176+
new MockExtensions(),
169177
);
170178
configSettings.update(pythonSettings.object);
171179

@@ -184,6 +192,7 @@ suite('Python Settings - pythonPath', () => {
184192
workspaceService.object,
185193
interpreterPathService.object,
186194
undefined,
195+
new MockExtensions(),
187196
);
188197
interpreterPathService.setup((i) => i.get(typemoq.It.isAny())).returns(() => 'custom');
189198
pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python');
@@ -204,6 +213,7 @@ suite('Python Settings - pythonPath', () => {
204213
workspaceService.object,
205214
interpreterPathService.object,
206215
undefined,
216+
new MockExtensions(),
207217
);
208218
interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python');
209219
configSettings.update(pythonSettings.object);

src/test/common/configSettings/configSettings.unit.test.ts

+50-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ITestingSettings } from '../../../client/testing/configuration/types';
2727
import { MockAutoSelectionService } from '../../mocks/autoSelector';
2828
import { MockMemento } from '../../mocks/mementos';
2929
import { untildify } from '../../../client/common/helpers';
30+
import { MockExtensions } from '../../mocks/extensions';
3031

3132
suite('Python Settings', async () => {
3233
class CustomPythonSettings extends PythonSettings {
@@ -40,13 +41,15 @@ suite('Python Settings', async () => {
4041
let config: TypeMoq.IMock<WorkspaceConfiguration>;
4142
let expected: CustomPythonSettings;
4243
let settings: CustomPythonSettings;
44+
let extensions: MockExtensions;
4345
setup(() => {
4446
sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns();
4547
config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Loose);
4648

4749
const workspaceService = new WorkspaceService();
4850
const workspaceMemento = new MockMemento();
4951
const globalMemento = new MockMemento();
52+
extensions = new MockExtensions();
5053
const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento);
5154
expected = new CustomPythonSettings(
5255
undefined,
@@ -55,7 +58,8 @@ suite('Python Settings', async () => {
5558
new InterpreterPathService(persistentStateFactory, workspaceService, [], {
5659
remoteName: undefined,
5760
} as IApplicationEnvironment),
58-
undefined,
61+
{ defaultLSType: LanguageServerType.Jedi },
62+
extensions,
5963
);
6064
settings = new CustomPythonSettings(
6165
undefined,
@@ -64,7 +68,8 @@ suite('Python Settings', async () => {
6468
new InterpreterPathService(persistentStateFactory, workspaceService, [], {
6569
remoteName: undefined,
6670
} as IApplicationEnvironment),
67-
undefined,
71+
{ defaultLSType: LanguageServerType.Jedi },
72+
extensions,
6873
);
6974
expected.defaultInterpreterPath = 'python';
7075
});
@@ -226,7 +231,7 @@ suite('Python Settings', async () => {
226231
const values = [
227232
{ ls: LanguageServerType.Jedi, expected: LanguageServerType.Jedi, default: false },
228233
{ ls: LanguageServerType.JediLSP, expected: LanguageServerType.Jedi, default: false },
229-
{ ls: LanguageServerType.Microsoft, expected: LanguageServerType.None, default: true },
234+
{ ls: LanguageServerType.Microsoft, expected: LanguageServerType.Jedi, default: true },
230235
{ ls: LanguageServerType.Node, expected: LanguageServerType.Node, default: false },
231236
{ ls: LanguageServerType.None, expected: LanguageServerType.None, default: false },
232237
];
@@ -235,7 +240,48 @@ suite('Python Settings', async () => {
235240
testLanguageServer(v.ls, v.expected, v.default);
236241
});
237242

238-
testLanguageServer('invalid' as LanguageServerType, LanguageServerType.None, true);
243+
testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi, true);
244+
});
245+
246+
function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) {
247+
test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${
248+
pyreflyDisabled ? 'disabled' : 'enabled'
249+
}`, () => {
250+
if (pyreflyInstalled) {
251+
extensions.extensionIdsToFind = ['meta.pyrefly'];
252+
} else {
253+
extensions.extensionIdsToFind = [];
254+
}
255+
config.setup((c) => c.get<boolean>('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled);
256+
257+
config
258+
.setup((c) => c.get<string>('languageServer'))
259+
.returns(() => undefined)
260+
.verifiable(TypeMoq.Times.once());
261+
262+
settings.update(config.object);
263+
264+
if (languageServerDisabled) {
265+
expect(settings.languageServer).to.equal(LanguageServerType.None);
266+
} else {
267+
expect(settings.languageServer).not.to.equal(LanguageServerType.None);
268+
}
269+
expect(settings.languageServerIsDefault).to.equal(true);
270+
config.verifyAll();
271+
});
272+
}
273+
274+
suite('pyrefly languageServer settings', async () => {
275+
const values = [
276+
{ pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true },
277+
{ pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false },
278+
{ pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false },
279+
{ pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false },
280+
];
281+
282+
values.forEach((v) => {
283+
testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled);
284+
});
239285
});
240286

241287
function testExperiments(enabled: boolean) {

src/test/extensionSettings.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PersistentStateFactory } from '../client/common/persistentState';
1313
import { IPythonSettings, Resource } from '../client/common/types';
1414
import { PythonEnvironment } from '../client/pythonEnvironments/info';
1515
import { MockMemento } from './mocks/mementos';
16+
import { MockExtensions } from './mocks/extensions';
1617

1718
export function getExtensionSettings(resource: Uri | undefined): IPythonSettings {
1819
const vscode = require('vscode') as typeof import('vscode');
@@ -41,6 +42,7 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings
4142
const workspaceMemento = new MockMemento();
4243
const globalMemento = new MockMemento();
4344
const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento);
45+
const extensions = new MockExtensions();
4446
return pythonSettings.PythonSettings.getInstance(
4547
resource,
4648
new AutoSelectionService(),
@@ -49,5 +51,6 @@ export function getExtensionSettings(resource: Uri | undefined): IPythonSettings
4951
remoteName: undefined,
5052
} as IApplicationEnvironment),
5153
undefined,
54+
extensions,
5255
);
5356
}

src/test/mocks/extension.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { injectable } from 'inversify';
2+
import { Extension, ExtensionKind, Uri } from 'vscode';
3+
4+
@injectable()
5+
export class MockExtension<T> implements Extension<T> {
6+
id!: string;
7+
extensionUri!: Uri;
8+
extensionPath!: string;
9+
isActive!: boolean;
10+
packageJSON: any;
11+
extensionKind!: ExtensionKind;
12+
exports!: T;
13+
activate(): Thenable<T> {
14+
throw new Error('Method not implemented.');
15+
}
16+
}

src/test/mocks/extensions.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { injectable } from 'inversify';
2+
import { IExtensions } from '../../client/common/types';
3+
import { Extension, Event } from 'vscode';
4+
import { MockExtension } from './extension';
5+
6+
@injectable()
7+
export class MockExtensions implements IExtensions {
8+
extensionIdsToFind: unknown[] = [];
9+
all: readonly Extension<unknown>[] = [];
10+
onDidChange: Event<void> = () => {
11+
throw new Error('Method not implemented');
12+
};
13+
getExtension(extensionId: string): Extension<unknown> | undefined;
14+
getExtension<T>(extensionId: string): Extension<T> | undefined;
15+
getExtension(extensionId: unknown): import('vscode').Extension<unknown> | undefined {
16+
if (this.extensionIdsToFind.includes(extensionId)) {
17+
return new MockExtension();
18+
}
19+
}
20+
determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> {
21+
throw new Error('Method not implemented.');
22+
}
23+
}

0 commit comments

Comments
 (0)