diff --git a/src/client/activation/languageClientMiddlewareBase.ts b/src/client/activation/languageClientMiddlewareBase.ts index 29a49b8bd35d..67ec24701189 100644 --- a/src/client/activation/languageClientMiddlewareBase.ts +++ b/src/client/activation/languageClientMiddlewareBase.ts @@ -10,6 +10,7 @@ import { Middleware, ResponseError, } from 'vscode-languageclient'; +import { ConfigurationItem } from 'vscode-languageserver-protocol'; import { HiddenFilePrefix } from '../common/constants'; import { IConfigurationService } from '../common/types'; @@ -96,6 +97,8 @@ export class LanguageClientMiddlewareBase implements Middleware { settingDict._envPYTHONPATH = envPYTHONPATH; } } + + this.configurationHook(item, settings[i] as LSPObject); } return settings; @@ -107,6 +110,9 @@ export class LanguageClientMiddlewareBase implements Middleware { return undefined; } + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {} + private get connected(): Promise { return this.connectedPromise.promise; } diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts index f06ed52b7b54..a410405d3131 100644 --- a/src/client/activation/node/analysisOptions.ts +++ b/src/client/activation/node/analysisOptions.ts @@ -1,18 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { ConfigurationTarget, extensions, WorkspaceConfiguration } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; +import * as semver from 'semver'; import { IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IExperimentService } from '../../common/types'; import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; import { ILanguageServerOutputChannel } from '../types'; import { LspNotebooksExperiment } from './lspNotebooksExperiment'; +const EDITOR_CONFIG_SECTION = 'editor'; +const FORMAT_ON_TYPE_CONFIG_SETTING = 'formatOnType'; + export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor( lsOutputChannel: ILanguageServerOutputChannel, workspace: IWorkspaceService, + private readonly experimentService: IExperimentService, private readonly lspNotebooksExperiment: LspNotebooksExperiment, ) { super(lsOutputChannel, workspace); @@ -25,6 +33,53 @@ export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOpt trustedWorkspaceSupport: true, lspNotebooksSupport: this.lspNotebooksExperiment.isInNotebooksExperiment(), lspInteractiveWindowSupport: this.lspNotebooksExperiment.isInNotebooksExperimentWithInteractiveWindowSupport(), + autoIndentSupport: await this.isAutoIndentEnabled(), } as unknown) as LanguageClientOptions; } + + private async isAutoIndentEnabled() { + const editorConfig = this.getPythonSpecificEditorSection(); + let formatOnTypeEffectiveValue = editorConfig.get(FORMAT_ON_TYPE_CONFIG_SETTING); + const formatOnTypeInspect = editorConfig.inspect(FORMAT_ON_TYPE_CONFIG_SETTING); + const formatOnTypeSetForPython = formatOnTypeInspect?.globalLanguageValue !== undefined; + + const inExperiment = await this.isInAutoIndentExperiment(); + + if (inExperiment !== formatOnTypeSetForPython) { + if (inExperiment) { + await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, true); + } else if (formatOnTypeInspect?.globalLanguageValue !== false) { + await NodeLanguageServerAnalysisOptions.setPythonSpecificFormatOnType(editorConfig, undefined); + } + + formatOnTypeEffectiveValue = this.getPythonSpecificEditorSection().get(FORMAT_ON_TYPE_CONFIG_SETTING); + } + + return inExperiment && formatOnTypeEffectiveValue; + } + + private async isInAutoIndentExperiment(): Promise { + if (await this.experimentService.inExperiment('pylanceAutoIndent')) { + return true; + } + + const pylanceVersion = extensions.getExtension(PYLANCE_EXTENSION_ID)?.packageJSON.version; + return pylanceVersion && semver.prerelease(pylanceVersion)?.includes('dev'); + } + + private getPythonSpecificEditorSection() { + return this.workspace.getConfiguration(EDITOR_CONFIG_SECTION, undefined, /* languageSpecific */ true); + } + + private static async setPythonSpecificFormatOnType( + editorConfig: WorkspaceConfiguration, + value: boolean | undefined, + ) { + await editorConfig.update( + FORMAT_ON_TYPE_CONFIG_SETTING, + value, + ConfigurationTarget.Global, + /* overrideInLanguage */ true, + ); + } } diff --git a/src/client/activation/node/languageClientMiddleware.ts b/src/client/activation/node/languageClientMiddleware.ts index e1e9cb447bc1..9c1d4c468191 100644 --- a/src/client/activation/node/languageClientMiddleware.ts +++ b/src/client/activation/node/languageClientMiddleware.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { LanguageClient } from 'vscode-languageclient/node'; -import { IJupyterExtensionDependencyManager } from '../../common/application/types'; +import { ConfigurationItem, LanguageClient, LSPObject } from 'vscode-languageclient/node'; +import { IJupyterExtensionDependencyManager, IWorkspaceService } from '../../common/application/types'; import { IServiceContainer } from '../../ioc/types'; import { JupyterExtensionIntegration } from '../../jupyter/jupyterIntegration'; import { traceLog } from '../../logging'; @@ -19,6 +19,8 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { private readonly jupyterExtensionIntegration: JupyterExtensionIntegration; + private readonly workspaceService: IWorkspaceService; + public constructor( serviceContainer: IServiceContainer, private getClient: () => LanguageClient | undefined, @@ -26,6 +28,8 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { ) { super(serviceContainer, LanguageServerType.Node, serverVersion); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.lspNotebooksExperiment = serviceContainer.get(LspNotebooksExperiment); this.setupHidingMiddleware(serviceContainer); @@ -82,4 +86,25 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware { return result; } + + // eslint-disable-next-line class-methods-use-this + protected configurationHook(item: ConfigurationItem, settings: LSPObject): void { + if (item.section === 'editor') { + if (this.workspaceService) { + // Get editor.formatOnType using Python language id so [python] setting + // will be honored if present. + const editorConfig = this.workspaceService.getConfiguration( + item.section, + undefined, + /* languageSpecific */ true, + ); + + const settingDict: LSPObject & { formatOnType?: boolean } = settings as LSPObject & { + formatOnType: boolean; + }; + + settingDict.formatOnType = editorConfig.get('formatOnType'); + } + } + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index a91aeed75b04..256404810237 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -837,9 +837,10 @@ export interface IWorkspaceService { * * @param section A dot-separated identifier. * @param resource A resource for which the configuration is asked for + * @param languageSpecific Should the [python] language-specific settings be obtained? * @return The full configuration or a subset. */ - getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration; + getConfiguration(section?: string, resource?: Uri, languageSpecific?: boolean): WorkspaceConfiguration; /** * Opens an untitled text document. The editor will prompt the user for a file diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 69d5dff965a3..0a5fd8d81816 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -39,8 +39,16 @@ export class WorkspaceService implements IWorkspaceService { public get workspaceFile() { return workspace.workspaceFile; } - public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { - return workspace.getConfiguration(section, resource || null); + public getConfiguration( + section?: string, + resource?: Uri, + languageSpecific: boolean = false, + ): WorkspaceConfiguration { + if (languageSpecific) { + return workspace.getConfiguration(section, { uri: resource, languageId: 'python' }); + } else { + return workspace.getConfiguration(section, resource); + } } public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { return uri ? workspace.getWorkspaceFolder(uri) : undefined; diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts index faa1bb75c4bc..2cc74308feea 100644 --- a/src/client/languageServer/pylanceLSExtensionManager.ts +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -58,6 +58,7 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities this.analysisOptions = new NodeLanguageServerAnalysisOptions( outputChannel, workspaceService, + experimentService, lspNotebooksExperiment, ); this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); diff --git a/src/test/activation/node/analysisOptions.unit.test.ts b/src/test/activation/node/analysisOptions.unit.test.ts index 0518fac170e9..8d16f0c0d9c9 100644 --- a/src/test/activation/node/analysisOptions.unit.test.ts +++ b/src/test/activation/node/analysisOptions.unit.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { assert, expect } from 'chai'; import * as typemoq from 'typemoq'; -import { WorkspaceFolder } from 'vscode'; +import { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; import { DocumentFilter } from 'vscode-languageclient/node'; import { NodeLanguageServerAnalysisOptions } from '../../../client/activation/node/analysisOptions'; @@ -10,7 +10,7 @@ import { LspNotebooksExperiment } from '../../../client/activation/node/lspNoteb import { ILanguageServerOutputChannel } from '../../../client/activation/types'; import { IWorkspaceService } from '../../../client/common/application/types'; import { PYTHON, PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { IOutputChannel } from '../../../client/common/types'; +import { IExperimentService, IOutputChannel } from '../../../client/common/types'; suite('Pylance Language Server - Analysis Options', () => { class TestClass extends NodeLanguageServerAnalysisOptions { @@ -32,17 +32,27 @@ suite('Pylance Language Server - Analysis Options', () => { let outputChannel: IOutputChannel; let lsOutputChannel: typemoq.IMock; let workspace: typemoq.IMock; + let experimentService: IExperimentService; let lspNotebooksExperiment: typemoq.IMock; setup(() => { outputChannel = typemoq.Mock.ofType().object; workspace = typemoq.Mock.ofType(); workspace.setup((w) => w.isVirtualWorkspace).returns(() => false); + const workspaceConfig = typemoq.Mock.ofType(); + workspace.setup((w) => w.getConfiguration('editor', undefined, true)).returns(() => workspaceConfig.object); + workspaceConfig.setup((w) => w.get('formatOnType')).returns(() => true); lsOutputChannel = typemoq.Mock.ofType(); lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); + experimentService = typemoq.Mock.ofType().object; lspNotebooksExperiment = typemoq.Mock.ofType(); lspNotebooksExperiment.setup((l) => l.isInNotebooksExperiment()).returns(() => false); - analysisOptions = new TestClass(lsOutputChannel.object, workspace.object, lspNotebooksExperiment.object); + analysisOptions = new TestClass( + lsOutputChannel.object, + workspace.object, + experimentService, + lspNotebooksExperiment.object, + ); }); test('Workspace folder is undefined', () => {