Allow LS to be bundled, versioned (#12034)
Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
This commit is contained in:
Родитель
b859a3de88
Коммит
e42fc1e7bc
|
@ -42,4 +42,4 @@ ptvsd*.log
|
|||
pydevd*.log
|
||||
nodeLanguageServer/**
|
||||
nodeLanguageServer.*/**
|
||||
|
||||
bundledLanguageServer/**
|
||||
|
|
|
@ -58,6 +58,8 @@ webpack.datascience-*.config.js
|
|||
.vscode test/**
|
||||
languageServer/**
|
||||
languageServer.*/**
|
||||
nodeLanguageServer/**
|
||||
nodeLanguageServer.*/**
|
||||
bin/**
|
||||
build/**
|
||||
BuildOutput/**
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -4,13 +4,25 @@
|
|||
'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);
|
||||
}
|
||||
|
||||
|
@ -18,3 +30,63 @@ export class NodeLanguageServerFolderService extends LanguageServerFolderService
|
|||
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);
|
||||
|
||||
// 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 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -72,6 +72,7 @@ export enum LanguageServerType {
|
|||
|
||||
export const DotNetLanguageServerFolder = 'languageServer';
|
||||
export const NodeLanguageServerFolder = 'nodeLanguageServer';
|
||||
export const BundledLanguageServerFolder = 'bundledLanguageServer';
|
||||
|
||||
// tslint:disable-next-line: interface-name
|
||||
export interface DocumentHandler {
|
||||
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
@ -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()));
|
||||
|
@ -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()));
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
'use strict';
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { Uri, WorkspaceConfiguration } from 'vscode';
|
||||
import {
|
||||
NodeLanguageServerFolderService,
|
||||
NodeLanguageServerVersionKey
|
||||
} from '../../../client/activation/node/languageServerFolderService';
|
||||
import { BundledLanguageServerFolder } from '../../../client/activation/types';
|
||||
import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types';
|
||||
import { IConfigurationService, IPythonSettings } from '../../../client/common/types';
|
||||
import { IServiceContainer } from '../../../client/ioc/types';
|
||||
|
||||
// tslint:disable:max-func-body-length
|
||||
|
||||
suite('Node Language Server Folder Service', () => {
|
||||
const resource = Uri.parse('a');
|
||||
const version = '0.0.1-test';
|
||||
|
||||
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
|
||||
let pythonSettings: TypeMoq.IMock<IPythonSettings>;
|
||||
let configService: TypeMoq.IMock<IConfigurationService>;
|
||||
let workspaceConfiguration: TypeMoq.IMock<WorkspaceConfiguration>;
|
||||
let workspaceService: TypeMoq.IMock<IWorkspaceService>;
|
||||
let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>;
|
||||
|
||||
setup(() => {
|
||||
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
|
||||
configService = TypeMoq.Mock.ofType<IConfigurationService>();
|
||||
pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>();
|
||||
configService.setup((c) => c.getSettings(undefined)).returns(() => pythonSettings.object);
|
||||
workspaceConfiguration = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
|
||||
workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>();
|
||||
workspaceService
|
||||
.setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny()))
|
||||
.returns(() => workspaceConfiguration.object);
|
||||
appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>();
|
||||
});
|
||||
|
||||
test('With packageName set', () => {
|
||||
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
|
||||
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: version }));
|
||||
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => 'somePackageName');
|
||||
|
||||
const folderService = new NodeLanguageServerFolderService(
|
||||
serviceContainer.object,
|
||||
configService.object,
|
||||
workspaceService.object,
|
||||
appEnvironment.object
|
||||
);
|
||||
|
||||
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
|
||||
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
|
||||
});
|
||||
|
||||
test('Invalid version', () => {
|
||||
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
|
||||
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: 'fakeversion' }));
|
||||
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
|
||||
|
||||
const folderService = new NodeLanguageServerFolderService(
|
||||
serviceContainer.object,
|
||||
configService.object,
|
||||
workspaceService.object,
|
||||
appEnvironment.object
|
||||
);
|
||||
|
||||
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
|
||||
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
|
||||
});
|
||||
|
||||
test('downloadLanguageServer set to false', () => {
|
||||
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => false);
|
||||
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: 'fakeversion' }));
|
||||
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
|
||||
|
||||
const folderService = new NodeLanguageServerFolderService(
|
||||
serviceContainer.object,
|
||||
configService.object,
|
||||
workspaceService.object,
|
||||
appEnvironment.object
|
||||
);
|
||||
|
||||
expect(folderService.bundledVersion).to.be.equal(undefined, 'expected bundledVersion to be undefined');
|
||||
expect(folderService.isBundled()).to.be.equal(false, 'isBundled should be false');
|
||||
});
|
||||
|
||||
suite('Valid configuration', () => {
|
||||
let folderService: NodeLanguageServerFolderService;
|
||||
|
||||
setup(() => {
|
||||
pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true);
|
||||
appEnvironment.setup((e) => e.packageJson).returns(() => ({ [NodeLanguageServerVersionKey]: version }));
|
||||
workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined);
|
||||
folderService = new NodeLanguageServerFolderService(
|
||||
serviceContainer.object,
|
||||
configService.object,
|
||||
workspaceService.object,
|
||||
appEnvironment.object
|
||||
);
|
||||
});
|
||||
|
||||
test('isBundled is true', () => {
|
||||
expect(folderService.isBundled()).to.be.equal(true, 'isBundled should be true');
|
||||
});
|
||||
|
||||
test('Parsed version is correct', () => {
|
||||
expect(folderService.bundledVersion!.format()).to.be.equal(version);
|
||||
});
|
||||
|
||||
test('getLanguageServerFolderName', async () => {
|
||||
const folderName = await folderService.getLanguageServerFolderName(resource);
|
||||
expect(folderName).to.be.equal(BundledLanguageServerFolder);
|
||||
});
|
||||
|
||||
test('getLatestLanguageServerVersion', async () => {
|
||||
const pkg = await folderService.getLatestLanguageServerVersion(resource);
|
||||
expect(pkg).to.equal(undefined, 'expected latest version to be undefined');
|
||||
});
|
||||
|
||||
test('Method getCurrentLanguageServerDirectory()', async () => {
|
||||
const dir = await folderService.getCurrentLanguageServerDirectory();
|
||||
assert(dir);
|
||||
expect(dir!.path).to.equal(BundledLanguageServerFolder);
|
||||
expect(dir!.version.format()).to.be.equal(version);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@ import { expect } from 'chai';
|
|||
import { SemVer } from 'semver';
|
||||
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';
|
||||
|
@ -53,7 +54,7 @@ suite('Nuget Azure Storage Repository', () => {
|
|||
// tslint:disable-next-line:no-invalid-this
|
||||
this.timeout(15000);
|
||||
const platformService = new PlatformService();
|
||||
const packageJson = { languageServerVersion: '0.0.1' };
|
||||
const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' };
|
||||
const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>();
|
||||
appEnv.setup((e) => e.packageJson).returns(() => packageJson);
|
||||
const lsPackageService = new DotNetLanguageServerPackageService(
|
||||
|
|
Загрузка…
Ссылка в новой задаче