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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,3 @@ ptvsd*.log
pydevd*.log
nodeLanguageServer/**
nodeLanguageServer.*/**
bundledLanguageServer/**
1 change: 1 addition & 0 deletions news/2 Fixes/12614.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stop looking for mspythonconfig.json file in subfolders.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"onCommand:python.datascience.selectjupytercommandline",
"onCommand:python.enableSourceMapSupport",
"onNotebookEditor:jupyter-notebook",
"workspaceContains:**/mspythonconfig.json"
"workspaceContains:mspythonconfig.json"
],
"main": "./out/client/extension",
"contributes": {
Expand Down
2 changes: 1 addition & 1 deletion src/client/activation/activationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export class LanguageServerExtensionActivationService
outputLine = 'Starting Microsoft Python language server.';
break;
case LanguageServerType.Node:
outputLine = 'Starting Node.js language server.';
outputLine = 'Starting Pylance language server.';
break;
case LanguageServerType.None:
outputLine = 'Editor support is inactive since language server is set to None.';
Expand Down
6 changes: 3 additions & 3 deletions src/client/activation/common/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export class LanguageServerDownloader implements ILanguageServerDownloader {
}

public async downloadLanguageServer(destinationFolder: string, resource: Resource): Promise<void> {
if (this.lsFolderService.isBundled()) {
// Sanity check; a bundled LS should never be downloaded.
traceError('Attempted to download bundled language server');
if (await this.lsFolderService.skipDownload()) {
// Sanity check; this case should not be hit if skipDownload is true elsewhere.
traceError('Attempted to download with skipDownload true.');
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export abstract class LanguageServerFolderService implements ILanguageServerFold
@unmanaged() protected readonly languageServerFolder: string
) {}

public isBundled(): boolean {
public async skipDownload(): Promise<boolean> {
return false;
}

Expand Down
10 changes: 7 additions & 3 deletions src/client/activation/node/languageClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ export class NodeLanguageClientFactory implements ILanguageClientFactory {
const commandArgs = (clientOptions.connectionOptions
?.cancellationStrategy as FileBasedCancellationStrategy).getCommandLineArguments();

const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource);
const bundlePath = path.join(EXTENSION_ROOT_DIR, languageServerFolder, 'server.bundle.js');
const nonBundlePath = path.join(EXTENSION_ROOT_DIR, languageServerFolder, 'server.js');
const folderName = await this.languageServerFolderService.getLanguageServerFolderName(resource);
const languageServerFolder = path.isAbsolute(folderName)
? folderName
: path.join(EXTENSION_ROOT_DIR, folderName);

const bundlePath = path.join(languageServerFolder, 'server.bundle.js');
const nonBundlePath = path.join(languageServerFolder, 'server.js');
const modulePath = (await this.fs.fileExists(nonBundlePath)) ? nonBundlePath : bundlePath;
const debugOptions = { execArgv: ['--nolazy', '--inspect=6600'] };

Expand Down
109 changes: 66 additions & 43 deletions src/client/activation/node/languageServerFolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@

'use strict';

import * as assert from 'assert';
import { inject, injectable } from 'inversify';
import * as semver from 'semver';
import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types';
import * as path from 'path';
import { SemVer } from 'semver';
import { IWorkspaceService } from '../../common/application/types';
import { NugetPackage } from '../../common/nuget/types';
import { IConfigurationService, Resource } from '../../common/types';
import { IConfigurationService, IExtensions, Resource } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { traceWarning } from '../../logging';
import { LanguageServerFolderService } from '../common/languageServerFolderService';
import {
BundledLanguageServerFolder,
FolderVersionPair,
ILanguageServerFolderService,
NodeLanguageServerFolder
} from '../types';
import { FolderVersionPair, ILanguageServerFolderService, NodeLanguageServerFolder } from '../types';

// Must match languageServerVersion* keys in package.json
export const NodeLanguageServerVersionKey = 'languageServerVersionV2';
export const PylanceExtensionName = 'ms-python.vscode-pylance';

class FallbackNodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(serviceContainer: IServiceContainer) {
Expand All @@ -31,62 +26,90 @@ class FallbackNodeLanguageServerFolderService extends LanguageServerFolderServic
}
}

// Exported for testing.
export interface ILanguageServerFolder {
path: string;
version: string; // SemVer, in string form to avoid cross-extension type issues.
}

// Exported for testing.
export interface ILSExtensionApi {
languageServerFolder?(): Promise<ILanguageServerFolder>;
}

@injectable()
export class NodeLanguageServerFolderService implements ILanguageServerFolderService {
private readonly _bundledVersion: semver.SemVer | undefined;
private readonly fallback: FallbackNodeLanguageServerFolderService;

constructor(
@inject(IServiceContainer) serviceContainer: IServiceContainer,
@inject(IConfigurationService) configService: IConfigurationService,
@inject(IWorkspaceService) workspaceService: IWorkspaceService,
@inject(IApplicationEnvironment) appEnv: IApplicationEnvironment
@inject(IConfigurationService) private configService: IConfigurationService,
@inject(IWorkspaceService) private workspaceService: IWorkspaceService,
@inject(IExtensions) readonly extensions: IExtensions
) {
this.fallback = new FallbackNodeLanguageServerFolderService(serviceContainer);

// downloadLanguageServer is a bit of a misnomer; if false then this indicates that a local
// development copy should be run instead of a "real" build, telemetry discarded, etc.
// So, we require it to be true, even though in the bundled case no real download happens.
if (
configService.getSettings().downloadLanguageServer &&
!workspaceService.getConfiguration('python').get<string>('packageName')
) {
const ver = appEnv.packageJson[NodeLanguageServerVersionKey] as string;
this._bundledVersion = semver.parse(ver) || undefined;
if (this._bundledVersion === undefined) {
traceWarning(
`invalid language server version ${ver} in package.json (${NodeLanguageServerVersionKey})`
);
}
}
}

public get bundledVersion(): semver.SemVer | undefined {
return this._bundledVersion;
}

public isBundled(): boolean {
return this._bundledVersion !== undefined;
public async skipDownload(): Promise<boolean> {
return (await this.lsExtensionApi()) !== undefined;
}

public async getLanguageServerFolderName(resource: Resource): Promise<string> {
if (this._bundledVersion) {
return BundledLanguageServerFolder;
const lsf = await this.languageServerFolder();
if (lsf) {
assert.ok(path.isAbsolute(lsf.path));
return lsf.path;
}
return this.fallback.getLanguageServerFolderName(resource);
}

public async getLatestLanguageServerVersion(resource: Resource): Promise<NugetPackage | undefined> {
if (this._bundledVersion) {
if (await this.lsExtensionApi()) {
return undefined;
}
return this.fallback.getLatestLanguageServerVersion(resource);
}

public async getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> {
if (this._bundledVersion) {
return { path: BundledLanguageServerFolder, version: this._bundledVersion };
const lsf = await this.languageServerFolder();
if (lsf) {
assert.ok(path.isAbsolute(lsf.path));
return {
path: lsf.path,
version: new SemVer(lsf.version)
};
}
return this.fallback.getCurrentLanguageServerDirectory();
}

protected async languageServerFolder(): Promise<ILanguageServerFolder | undefined> {
const extension = await this.lsExtensionApi();
if (!extension?.languageServerFolder) {
return undefined;
}
return extension.languageServerFolder();
}

private async lsExtensionApi(): Promise<ILSExtensionApi | undefined> {
// downloadLanguageServer is a bit of a misnomer; if false then this indicates that a local
// development copy should be run instead of a "real" build, telemetry discarded, etc.
// So, we require it to be true, even though in the pinned case no real download happens.
if (
!this.configService.getSettings().downloadLanguageServer ||
this.workspaceService.getConfiguration('python').get<string>('packageName')
) {
return undefined;
}

const extension = this.extensions.getExtension<ILSExtensionApi>(PylanceExtensionName);
if (!extension) {
return undefined;
}

if (!extension.isActive) {
return extension.activate();
}

return extension.exports;
}
}
28 changes: 25 additions & 3 deletions src/client/activation/node/languageServerProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import '../../common/extensions';

import { inject, injectable } from 'inversify';
import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient';
import {
DidChangeConfigurationNotification,
Disposable,
LanguageClient,
LanguageClientOptions
} from 'vscode-languageclient';

import { DeprecatePythonPath } from '../../common/experiments/groups';
import { traceDecorators, traceError } from '../../common/logger';
import { IConfigurationService, Resource } from '../../common/types';
import { IConfigurationService, IExperimentsManager, IInterpreterPathService, Resource } from '../../common/types';
import { createDeferred, Deferred, sleep } from '../../common/utils/async';
import { swallowExceptions } from '../../common/utils/decorators';
import { noop } from '../../common/utils/misc';
Expand All @@ -32,7 +38,9 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy {
@inject(ILanguageClientFactory) private readonly factory: ILanguageClientFactory,
@inject(ITestManagementService) private readonly testManager: ITestManagementService,
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
@inject(ILanguageServerFolderService) private readonly folderService: ILanguageServerFolderService
@inject(ILanguageServerFolderService) private readonly folderService: ILanguageServerFolderService,
@inject(IExperimentsManager) private readonly experiments: IExperimentsManager,
@inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService
) {
this.startupCompleted = createDeferred<void>();
}
Expand Down Expand Up @@ -95,6 +103,20 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy {
const progressReporting = new ProgressReporting(this.languageClient!);
this.disposables.push(progressReporting);

if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) {
this.disposables.push(
this.interpreterPathService.onDidChange(() => {
// Manually send didChangeConfiguration in order to get the server to requery
// the workspace configurations (to then pick up pythonPath set in the middleware).
// This is needed as interpreter changes via the interpreter path service happen
// outside of VS Code's settings (which would mean VS Code sends the config updates itself).
this.languageClient!.sendNotification(DidChangeConfigurationNotification.type, {
settings: null
});
})
);
}

const settings = this.configurationService.getSettings(resource);
if (settings.downloadLanguageServer) {
this.languageClient.onTelemetry((telemetryEvent) => {
Expand Down
5 changes: 2 additions & 3 deletions src/client/activation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,12 @@ export interface IExtensionActivationService {
export enum LanguageServerType {
Jedi = 'Jedi',
Microsoft = 'Microsoft',
Node = 'Node',
Node = 'Pylance',
None = 'None'
}

export const DotNetLanguageServerFolder = 'languageServer';
export const NodeLanguageServerFolder = 'nodeLanguageServer';
export const BundledLanguageServerFolder = 'bundledLanguageServer';

// tslint:disable-next-line: interface-name
export interface DocumentHandler {
Expand Down Expand Up @@ -117,7 +116,7 @@ export interface ILanguageServerFolderService {
getLanguageServerFolderName(resource: Resource): Promise<string>;
getLatestLanguageServerVersion(resource: Resource): Promise<NugetPackage | undefined>;
getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined>;
isBundled(): boolean;
skipDownload(): Promise<boolean>;
}

export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader');
Expand Down
6 changes: 3 additions & 3 deletions src/test/activation/languageServer/downloader.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ suite('Language Server Activation - Downloader', () => {
);
});
test('Display error message if LS downloading fails', async () => {
folderService.setup((f) => f.isBundled()).returns(() => false);
folderService.setup((f) => f.skipDownload()).returns(async () => false);
const pkg = makePkgInfo('ls', 'xyz');
folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg));
output.setup((o) => o.appendLine(LanguageService.downloadFailedOutputMessage()));
Expand All @@ -345,7 +345,7 @@ suite('Language Server Activation - Downloader', () => {
platformData.verifyAll();
});
test('Display error message if LS extraction fails', async () => {
folderService.setup((f) => f.isBundled()).returns(() => false);
folderService.setup((f) => f.skipDownload()).returns(async () => false);
const pkg = makePkgInfo('ls', 'xyz');
folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg));
output.setup((o) => o.appendLine(LanguageService.extractionFailedOutputMessage()));
Expand All @@ -369,7 +369,7 @@ suite('Language Server Activation - Downloader', () => {
platformData.verifyAll();
});
test('No download if bundled', async () => {
folderService.setup((f) => f.isBundled()).returns(() => true);
folderService.setup((f) => f.skipDownload()).returns(async () => true);

await languageServerBundledTest.downloadLanguageServer('', resource);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,15 @@ suite('Language Server Folder Service', () => {
});
});

suite('Method isBundled()', () => {
suite('Method skipDownload()', () => {
setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object);
});

test('isBundled is false', () => {
expect(languageServerFolderService.isBundled()).to.be.equal(false, 'isBundled should be false');
test('skipDownload is false', async () => {
const skipDownload = await languageServerFolderService.skipDownload();
expect(skipDownload).to.be.equal(false, 'skipDownload should be false');
});
});
});
Loading