Skip to content
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ ptvsd*.log
pydevd*.log
nodeLanguageServer/**
nodeLanguageServer.*/**

bundledLanguageServer/**
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ webpack.datascience-*.config.js
.vscode test/**
languageServer/**
languageServer.*/**
nodeLanguageServer/**
nodeLanguageServer.*/**
bin/**
build/**
BuildOutput/**
Expand Down
7 changes: 7 additions & 0 deletions src/client/activation/common/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { createDeferred } from '../../common/utils/async';
import { Common, LanguageService } from '../../common/utils/localize';
import { StopWatch } from '../../common/utils/stopWatch';
import { IServiceContainer } from '../../ioc/types';
import { traceError } from '../../logging';
import { sendTelemetryEvent } from '../../telemetry';
import { EventName } from '../../telemetry/constants';
import {
Expand Down Expand Up @@ -58,6 +59,12 @@ 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');
return;
}

const [downloadUri, lsVersion, lsName] = await this.getDownloadInfo(resource);
const timer: StopWatch = new StopWatch();
let success: boolean = true;
Expand Down
4 changes: 4 additions & 0 deletions src/client/activation/common/languageServerFolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export abstract class LanguageServerFolderService implements ILanguageServerFold
@unmanaged() protected readonly languageServerFolder: string
) {}

public isBundled(): boolean {
return false;
}

@traceDecorators.verbose('Get language server folder name')
public async getLanguageServerFolderName(resource: Resource): Promise<string> {
const currentFolder = await this.getCurrentLanguageServerDirectory();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LanguageServerFolderService } from '../common/languageServerFolderServi
import { DotNetLanguageServerFolder } from '../types';

// Must match languageServerVersion* keys in package.json
const DotNetLanguageServerMinVersionKey = 'languageServerVersion';
export const DotNetLanguageServerMinVersionKey = 'languageServerVersion';

@injectable()
export class DotNetLanguageServerFolderService extends LanguageServerFolderService {
Expand Down
80 changes: 76 additions & 4 deletions src/client/activation/node/languageServerFolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,89 @@
'use strict';

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

@injectable()
export class NodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
// Must match languageServerVersion* keys in package.json
export const NodeLanguageServerVersionKey = 'languageServerVersionV2';

class FallbackNodeLanguageServerFolderService extends LanguageServerFolderService {
constructor(serviceContainer: IServiceContainer) {
super(serviceContainer, NodeLanguageServerFolder);
}

protected getMinimalLanguageServerVersion(): string {
return '0.0.0';
}
}

@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
) {
this.fallback = new FallbackNodeLanguageServerFolderService(serviceContainer);
Comment thread
jakebailey marked this conversation as resolved.

// 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.
Comment thread
jakebailey marked this conversation as resolved.
if (
configService.getSettings().downloadLanguageServer &&
!workspaceService.getConfiguration('python').get<string>('packageName')
Comment thread
jakebailey marked this conversation as resolved.
) {
const ver = appEnv.packageJson[NodeLanguageServerVersionKey] as string;
this._bundledVersion = semver.parse(ver) || undefined;
Comment thread
jakebailey marked this conversation as resolved.
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 getLanguageServerFolderName(resource: Resource): Promise<string> {
if (this._bundledVersion) {
return BundledLanguageServerFolder;
}
return this.fallback.getLanguageServerFolderName(resource);
}

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

public async getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> {
if (this._bundledVersion) {
return { path: BundledLanguageServerFolder, version: this._bundledVersion };
}
return this.fallback.getCurrentLanguageServerDirectory();
}
}
4 changes: 2 additions & 2 deletions src/client/activation/node/languageServerProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export class NodeLanguageServerProxy implements ILanguageServerProxy {
options: LanguageClientOptions
): Promise<void> {
if (!this.languageClient) {
const lsVersion = await this.folderService.getLatestLanguageServerVersion(resource);
this.lsVersion = lsVersion?.version.format();
const directory = await this.folderService.getCurrentLanguageServerDirectory();
this.lsVersion = directory?.version.format();

this.cancellationStrategy = new FileBasedCancellationStrategy();
options.connectionOptions = { cancellationStrategy: this.cancellationStrategy };
Expand Down
2 changes: 2 additions & 0 deletions src/client/activation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export enum LanguageServerType {

export const DotNetLanguageServerFolder = 'languageServer';
export const NodeLanguageServerFolder = 'nodeLanguageServer';
export const BundledLanguageServerFolder = 'bundledLanguageServer';
Comment thread
jakebailey marked this conversation as resolved.

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

export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader');
Expand Down
39 changes: 39 additions & 0 deletions src/test/activation/languageServer/downloader.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,29 @@ suite('Language Server Activation - Downloader', () => {
throw failure;
}
}
class LanguageServeBundledTest extends LanguageServerDownloader {
// tslint:disable-next-line:no-unnecessary-override
public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> {
return super.downloadLanguageServer(destinationFolder, res);
}
// tslint:disable-next-line:no-unnecessary-override
public async getDownloadInfo(_res?: Resource): Promise<string[]> {
throw failure;
}
public async downloadFile(): Promise<string> {
throw failure;
}
protected async unpackArchive(_extensionPath: string, _tempFilePath: string): Promise<void> {
throw failure;
}
}
let output: TypeMoq.IMock<IOutputChannel>;
let appShell: TypeMoq.IMock<IApplicationShell>;
let fs: TypeMoq.IMock<IFileSystem>;
let platformData: TypeMoq.IMock<IPlatformData>;
let languageServerDownloaderTest: LanguageServerDownloaderTest;
let languageServerExtractorTest: LanguageServerExtractorTest;
let languageServerBundledTest: LanguageServeBundledTest;
setup(() => {
appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict);
folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict);
Expand Down Expand Up @@ -293,8 +310,18 @@ suite('Language Server Activation - Downloader', () => {
workspaceService.object,
undefined as any
);
languageServerBundledTest = new LanguageServeBundledTest(
lsOutputChannel.object,
undefined as any,
folderService.object,
appShell.object,
fs.object,
workspaceService.object,
undefined as any
);
});
test('Display error message if LS downloading fails', async () => {
folderService.setup((f) => f.isBundled()).returns(() => 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 @@ -318,6 +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);
const pkg = makePkgInfo('ls', 'xyz');
folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg));
output.setup((o) => o.appendLine(LanguageService.extractionFailedOutputMessage()));
Expand All @@ -340,6 +368,17 @@ suite('Language Server Activation - Downloader', () => {
fs.verifyAll();
platformData.verifyAll();
});
test('No download if bundled', async () => {
folderService.setup((f) => f.isBundled()).returns(() => true);

await languageServerBundledTest.downloadLanguageServer('', resource);

folderService.verifyAll();
output.verifyAll();
appShell.verifyAll();
fs.verifyAll();
platformData.verifyAll();
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,15 @@ suite('Language Server Folder Service', () => {
assert.deepEqual(result, expectedLSDirectory);
});
});

suite('Method isBundled()', () => {
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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { expect } from 'chai';
import * as typeMoq from 'typemoq';
import { WorkspaceConfiguration } from 'vscode';
import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService';
import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService';
import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types';
import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository';
Expand Down Expand Up @@ -44,7 +45,7 @@ suite('Language Server Package Service', () => {
);
serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetRepository))).returns(() => nugetRepo);
const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>();
const packageJson = { languageServerVersion: '0.0.1' };
const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' };
appEnv.setup((e) => e.packageJson).returns(() => packageJson);
const platform = typeMoq.Mock.ofType<IPlatformService>();
const lsPackageService = new DotNetLanguageServerPackageService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
azureCDNBlobStorageAccount,
LanguageServerDownloadChannel
} from '../../../client/activation/common/packageRepository';
import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService';
import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService';
import { PlatformName } from '../../../client/activation/types';
import { IApplicationEnvironment } from '../../../client/common/application/types';
Expand Down Expand Up @@ -41,7 +42,7 @@ suite('Language Server - Package Service', () => {
lsPackageService.getLanguageServerDownloadChannel = () => 'stable';
});
function setMinVersionOfLs(version: string) {
const packageJson = { languageServerVersion: version };
const packageJson = { [DotNetLanguageServerMinVersionKey]: version };
appVersion.setup((e) => e.packageJson).returns(() => packageJson);
}
[true, false].forEach((is64Bit) => {
Expand Down
Loading