Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/client/activation/languageClientMiddlewareBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,6 +97,8 @@ export class LanguageClientMiddlewareBase implements Middleware {
settingDict._envPYTHONPATH = envPYTHONPATH;
}
}

this.configurationHook(item, settings[i] as LSPObject);
}

return settings;
Expand All @@ -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<boolean> {
return this.connectedPromise.promise;
}
Expand Down
55 changes: 55 additions & 0 deletions src/client/activation/node/analysisOptions.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<boolean> {
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,
);
}
}
29 changes: 27 additions & 2 deletions src/client/activation/node/languageClientMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,13 +19,17 @@ export class NodeLanguageClientMiddleware extends LanguageClientMiddleware {

private readonly jupyterExtensionIntegration: JupyterExtensionIntegration;

private readonly workspaceService: IWorkspaceService;

public constructor(
serviceContainer: IServiceContainer,
private getClient: () => LanguageClient | undefined,
serverVersion?: string,
) {
super(serviceContainer, LanguageServerType.Node, serverVersion);

this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService);

this.lspNotebooksExperiment = serviceContainer.get<LspNotebooksExperiment>(LspNotebooksExperiment);
this.setupHidingMiddleware(serviceContainer);

Expand Down Expand Up @@ -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');
}
}
}
}
3 changes: 2 additions & 1 deletion src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions src/client/common/application/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/client/languageServer/pylanceLSExtensionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class PylanceLSExtensionManager extends LanguageServerCapabilities
this.analysisOptions = new NodeLanguageServerAnalysisOptions(
outputChannel,
workspaceService,
experimentService,
lspNotebooksExperiment,
);
this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions);
Expand Down
16 changes: 13 additions & 3 deletions src/test/activation/node/analysisOptions.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// 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';
import { LspNotebooksExperiment } from '../../../client/activation/node/lspNotebooksExperiment';
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 {
Expand All @@ -32,17 +32,27 @@ suite('Pylance Language Server - Analysis Options', () => {
let outputChannel: IOutputChannel;
let lsOutputChannel: typemoq.IMock<ILanguageServerOutputChannel>;
let workspace: typemoq.IMock<IWorkspaceService>;
let experimentService: IExperimentService;
let lspNotebooksExperiment: typemoq.IMock<LspNotebooksExperiment>;

setup(() => {
outputChannel = typemoq.Mock.ofType<IOutputChannel>().object;
workspace = typemoq.Mock.ofType<IWorkspaceService>();
workspace.setup((w) => w.isVirtualWorkspace).returns(() => false);
const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>();
workspace.setup((w) => w.getConfiguration('editor', undefined, true)).returns(() => workspaceConfig.object);
workspaceConfig.setup((w) => w.get('formatOnType')).returns(() => true);
lsOutputChannel = typemoq.Mock.ofType<ILanguageServerOutputChannel>();
lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel);
experimentService = typemoq.Mock.ofType<IExperimentService>().object;
lspNotebooksExperiment = typemoq.Mock.ofType<LspNotebooksExperiment>();
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', () => {
Expand Down