Skip to content

Commit b68bcd7

Browse files
authored
Logic to control Pylance auto-indent experiment (#19722)
1 parent 5cfa71d commit b68bcd7

File tree

7 files changed

+114
-8
lines changed

7 files changed

+114
-8
lines changed

src/client/activation/languageClientMiddlewareBase.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Middleware,
1111
ResponseError,
1212
} from 'vscode-languageclient';
13+
import { ConfigurationItem } from 'vscode-languageserver-protocol';
1314

1415
import { HiddenFilePrefix } from '../common/constants';
1516
import { IConfigurationService } from '../common/types';
@@ -96,6 +97,8 @@ export class LanguageClientMiddlewareBase implements Middleware {
9697
settingDict._envPYTHONPATH = envPYTHONPATH;
9798
}
9899
}
100+
101+
this.configurationHook(item, settings[i] as LSPObject);
99102
}
100103

101104
return settings;
@@ -107,6 +110,9 @@ export class LanguageClientMiddlewareBase implements Middleware {
107110
return undefined;
108111
}
109112

113+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function
114+
protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {}
115+
110116
private get connected(): Promise<boolean> {
111117
return this.connectedPromise.promise;
112118
}

src/client/activation/node/analysisOptions.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import { ConfigurationTarget, extensions, WorkspaceConfiguration } from 'vscode';
45
import { LanguageClientOptions } from 'vscode-languageclient';
6+
import * as semver from 'semver';
57
import { IWorkspaceService } from '../../common/application/types';
8+
import { PYLANCE_EXTENSION_ID } from '../../common/constants';
9+
import { IExperimentService } from '../../common/types';
610

711
import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions';
812
import { ILanguageServerOutputChannel } from '../types';
913
import { LspNotebooksExperiment } from './lspNotebooksExperiment';
1014

15+
const EDITOR_CONFIG_SECTION = 'editor';
16+
const FORMAT_ON_TYPE_CONFIG_SETTING = 'formatOnType';
17+
1118
export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase {
1219
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
1320
constructor(
1421
lsOutputChannel: ILanguageServerOutputChannel,
1522
workspace: IWorkspaceService,
23+
private readonly experimentService: IExperimentService,
1624
private readonly lspNotebooksExperiment: LspNotebooksExperiment,
1725
) {
1826
super(lsOutputChannel, workspace);
@@ -25,6 +33,53 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt
2533
trustedWorkspaceSupport: true,
2634
lspNotebooksSupport: this.lspNotebooksExperiment.isInNotebooksExperiment(),
2735
lspInteractiveWindowSupport: this.lspNotebooksExperiment.isInNotebooksExperimentWithInteractiveWindowSupport(),
36+
autoIndentSupport: await this.isAutoIndentEnabled(),
2837
} as unknown) as LanguageClientOptions;
2938
}
39+
40+
private async isAutoIndentEnabled() {
41+
const editorConfig = this.getPythonSpecificEditorSection();
42+
let formatOnTypeEffectiveValue = editorConfig.get(FORMAT_ON_TYPE_CONFIG_SETTING);
43+
const formatOnTypeInspect = editorConfig.inspect(FORMAT_ON_TYPE_CONFIG_SETTING);
44+
const formatOnTypeSetForPython = formatOnTypeInspect?.globalLanguageValue !== undefined;
45+
46+
const inExperiment = await this.isInAutoIndentExperiment();
47+
48+
if (inExperiment !== formatOnTypeSetForPython) {
49+
if (inExperiment) {
50+
await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true);
51+
} else if (formatOnTypeInspect?.globalLanguageValue !== false) {
52+
await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, undefined);
53+
}
54+
55+
formatOnTypeEffectiveValue = this.getPythonSpecificEditorSection().get(FORMAT_ON_TYPE_CONFIG_SETTING);
56+
}
57+
58+
return inExperiment && formatOnTypeEffectiveValue;
59+
}
60+
61+
private async isInAutoIndentExperiment(): Promise<boolean> {
62+
if (await this.experimentService.inExperiment('pylanceAutoIndent')) {
63+
return true;
64+
}
65+
66+
const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version;
67+
return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev');
68+
}
69+
70+
private getPythonSpecificEditorSection() {
71+
return this.workspace.getConfiguration(EDITOR_CONFIG_SECTION, undefined, /* languageSpecific */ true);
72+
}
73+
74+
private static async setPythonSpecificFormatOnType(
75+
editorConfig: WorkspaceConfiguration,
76+
value: boolean | undefined,
77+
) {
78+
await editorConfig.update(
79+
FORMAT_ON_TYPE_CONFIG_SETTING,
80+
value,
81+
ConfigurationTarget.Global,
82+
/* overrideInLanguage */ true,
83+
);
84+
}
3085
}

src/client/activation/node/languageClientMiddleware.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Licensed under the MIT License.
33

44
import { Uri } from 'vscode';
5-
import { LanguageClient } from 'vscode-languageclient/node';
6-
import { IJupyterExtensionDependencyManager } from '../../common/application/types';
5+
import { ConfigurationItem, LanguageClient, LSPObject } from 'vscode-languageclient/node';
6+
import { IJupyterExtensionDependencyManager, IWorkspaceService } from '../../common/application/types';
77
import { IServiceContainer } from '../../ioc/types';
88
import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration';
99
import { traceLog } from '../../logging';
@@ -19,13 +19,17 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware {
1919

2020
private readonly jupyterExtensionIntegration: JupyterExtensionIntegration;
2121

22+
private readonly workspaceService: IWorkspaceService;
23+
2224
public constructor(
2325
serviceContainer: IServiceContainer,
2426
private getClient: () => LanguageClient | undefined,
2527
serverVersion?: string,
2628
) {
2729
super(serviceContainer, LanguageServerType.Node, serverVersion);
2830

31+
this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService);
32+
2933
this.lspNotebooksExperiment = serviceContainer.get<LspNotebooksExperiment>(LspNotebooksExperiment);
3034
this.setupHidingMiddleware(serviceContainer);
3135

@@ -82,4 +86,25 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware {
8286

8387
return result;
8488
}
89+
90+
// eslint-disable-next-line class-methods-use-this
91+
protected configurationHook(item: ConfigurationItem, settings: LSPObject): void {
92+
if (item.section === 'editor') {
93+
if (this.workspaceService) {
94+
// Get editor.formatOnType using Python language id so [python] setting
95+
// will be honored if present.
96+
const editorConfig = this.workspaceService.getConfiguration(
97+
item.section,
98+
undefined,
99+
/* languageSpecific */ true,
100+
);
101+
102+
const settingDict: LSPObject & { formatOnType?: boolean } = settings as LSPObject & {
103+
formatOnType: boolean;
104+
};
105+
106+
settingDict.formatOnType = editorConfig.get('formatOnType');
107+
}
108+
}
109+
}
85110
}

src/client/common/application/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,9 +837,10 @@ export interface IWorkspaceService {
837837
*
838838
* @param section A dot-separated identifier.
839839
* @param resource A resource for which the configuration is asked for
840+
* @param languageSpecific Should the [python] language-specific settings be obtained?
840841
* @return The full configuration or a subset.
841842
*/
842-
getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration;
843+
getConfiguration(section?: string, resource?: Uri, languageSpecific?: boolean): WorkspaceConfiguration;
843844

844845
/**
845846
* Opens an untitled text document. The editor will prompt the user for a file

src/client/common/application/workspace.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,16 @@ export class WorkspaceService implements IWorkspaceService {
3939
public get workspaceFile() {
4040
return workspace.workspaceFile;
4141
}
42-
public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration {
43-
return workspace.getConfiguration(section, resource || null);
42+
public getConfiguration(
43+
section?: string,
44+
resource?: Uri,
45+
languageSpecific: boolean = false,
46+
): WorkspaceConfiguration {
47+
if (languageSpecific) {
48+
return workspace.getConfiguration(section, { uri: resource, languageId: 'python' });
49+
} else {
50+
return workspace.getConfiguration(section, resource);
51+
}
4452
}
4553
public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined {
4654
return uri ? workspace.getWorkspaceFolder(uri) : undefined;

src/client/languageServer/pylanceLSExtensionManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities
5858
this.analysisOptions = new NodeLanguageServerAnalysisOptions(
5959
outputChannel,
6060
workspaceService,
61+
experimentService,
6162
lspNotebooksExperiment,
6263
);
6364
this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions);

src/test/activation/node/analysisOptions.unit.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
// Licensed under the MIT License.
33
import { assert, expect } from 'chai';
44
import * as typemoq from 'typemoq';
5-
import { WorkspaceFolder } from 'vscode';
5+
import { WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
66
import { DocumentFilter } from 'vscode-languageclient/node';
77

88
import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions';
99
import { LspNotebooksExperiment } from '../../../client/activation/node/lspNotebooksExperiment';
1010
import { ILanguageServerOutputChannel } from '../../../client/activation/types';
1111
import { IWorkspaceService } from '../../../client/common/application/types';
1212
import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants';
13-
import { IOutputChannel } from '../../../client/common/types';
13+
import { IExperimentService, IOutputChannel } from '../../../client/common/types';
1414

1515
suite('Pylance Language Server - Analysis Options', () => {
1616
class TestClass extends NodeLanguageServerAnalysisOptions {
@@ -32,17 +32,27 @@ suite('Pylance Language Server - Analysis Options', () => {
3232
let outputChannel: IOutputChannel;
3333
let lsOutputChannel: typemoq.IMock<ILanguageServerOutputChannel>;
3434
let workspace: typemoq.IMock<IWorkspaceService>;
35+
let experimentService: IExperimentService;
3536
let lspNotebooksExperiment: typemoq.IMock<LspNotebooksExperiment>;
3637

3738
setup(() => {
3839
outputChannel = typemoq.Mock.ofType<IOutputChannel>().object;
3940
workspace = typemoq.Mock.ofType<IWorkspaceService>();
4041
workspace.setup((w) => w.isVirtualWorkspace).returns(() => false);
42+
const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>();
43+
workspace.setup((w) => w.getConfiguration('editor', undefined, true)).returns(() => workspaceConfig.object);
44+
workspaceConfig.setup((w) => w.get('formatOnType')).returns(() => true);
4145
lsOutputChannel = typemoq.Mock.ofType<ILanguageServerOutputChannel>();
4246
lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel);
47+
experimentService = typemoq.Mock.ofType<IExperimentService>().object;
4348
lspNotebooksExperiment = typemoq.Mock.ofType<LspNotebooksExperiment>();
4449
lspNotebooksExperiment.setup((l) => l.isInNotebooksExperiment()).returns(() => false);
45-
analysisOptions = new TestClass(lsOutputChannel.object, workspace.object, lspNotebooksExperiment.object);
50+
analysisOptions = new TestClass(
51+
lsOutputChannel.object,
52+
workspace.object,
53+
experimentService,
54+
lspNotebooksExperiment.object,
55+
);
4656
});
4757

4858
test('Workspace folder is undefined', () => {

0 commit comments

Comments
 (0)