From c5c5ccd66488ef9a9e33f42cda955074ea1cffac Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Fri, 22 Nov 2019 15:23:15 -0800 Subject: [PATCH 01/26] Rework original idea to have smaller impact. Put cache inside of the activationService --- src/client/activation/activationService.ts | 146 ++++++----- src/client/activation/jedi.ts | 112 ++++++-- .../activation/languageServer/activator.ts | 239 +++++++++++++++++- .../languageServer/languageClientFactory.ts | 19 +- ...nguageServer.ts => languageServerProxy.ts} | 14 +- .../activation/languageServer/manager.ts | 41 +-- src/client/activation/serviceRegistry.ts | 49 +++- src/client/activation/types.ts | 55 +++- .../dotNetIntellisenseProvider.ts | 150 ----------- ...nseProvider.ts => intellisenseProvider.ts} | 148 ++++++++--- .../intellisense/jediIntellisenseProvider.ts | 119 --------- .../datascience/jupyter/jupyterNotebook.ts | 18 +- .../jupyter/jupyterServerFactory.ts | 4 +- .../jupyter/liveshare/guestJupyterNotebook.ts | 14 +- .../jupyter/liveshare/hostJupyterNotebook.ts | 6 +- .../jupyter/liveshare/hostJupyterServer.ts | 8 +- src/client/datascience/serviceRegistry.ts | 6 +- src/client/datascience/types.ts | 1 + .../languageServer/activator.unit.test.ts | 22 +- .../languageClientFactory.unit.test.ts | 16 +- .../languageServer.unit.test.ts | 24 +- .../languageServer/manager.unit.test.ts | 44 ++-- .../activation/serviceRegistry.unit.test.ts | 32 ++- .../datascience/dataScienceIocContainer.ts | 12 +- src/test/datascience/execution.unit.test.ts | 16 +- .../datascience/intellisense.unit.test.ts | 153 ++++++----- src/test/datascience/mockLanguageClient.ts | 16 +- src/test/datascience/mockLanguageServer.ts | 119 +++++++-- .../datascience/mockLanguageServerCache.ts | 23 ++ .../datascience/mockLanguageServerProxy.ts | 37 +++ 30 files changed, 1035 insertions(+), 628 deletions(-) rename src/client/activation/languageServer/{languageServer.ts => languageServerProxy.ts} (92%) delete mode 100644 src/client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider.ts rename src/client/datascience/interactive-common/intellisense/{baseIntellisenseProvider.ts => intellisenseProvider.ts} (72%) delete mode 100644 src/client/datascience/interactive-common/intellisense/jediIntellisenseProvider.ts create mode 100644 src/test/datascience/mockLanguageServerCache.ts create mode 100644 src/test/datascience/mockLanguageServerProxy.ts diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 43cf60705dd1..0d65e06a1276 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -1,43 +1,59 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; +import '../common/extensions'; import { inject, injectable } from 'inversify'; import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode'; + import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported'; import { IDiagnosticsService } from '../application/diagnostics/types'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { LSControl, LSEnabled } from '../common/experimentGroups'; -import '../common/extensions'; import { traceError } from '../common/logger'; -import { IConfigurationService, IDisposableRegistry, IExperimentsManager, IOutputChannel, IPersistentStateFactory, IPythonSettings, Resource } from '../common/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IOutputChannel, + IPersistentStateFactory, + IPythonSettings, + Resource +} from '../common/types'; import { swallowExceptions } from '../common/utils/decorators'; +import { IInterpreterService, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; -import { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types'; +import { + IExtensionActivationService, + ILanguageServer, + ILanguageServerActivator, + ILanguageServerCache, + LanguageServerActivator +} from './types'; const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; const workspacePathNameForGlobalWorkspaces = ''; -type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator }; +type ActivatorInfo = { jedi: boolean; server: ILanguageServerActivator }; @injectable() -export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable { - private lsActivatedWorkspaces = new Map(); +export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable { + private lsActivatedServers = new Map>(); + private jediServer: ILanguageServerActivator | undefined; private currentActivator?: ActivatorInfo; - private jediActivatedOnce: boolean = false; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; private readonly appShell: IApplicationShell; private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; + private readonly interpreterService: IInterpreterService; private resource!: Resource; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @inject(IExperimentsManager) private readonly abExperiments: IExperimentsManager) { this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.interpreterService = this.serviceContainer.get(IInterpreterService); this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.appShell = this.serviceContainer.get(IApplicationShell); this.lsNotSupportedDiagnosticService = this.serviceContainer.get( @@ -52,55 +68,24 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv public async activate(resource: Resource): Promise { this.resource = resource; - let jedi = this.useJedi(); - if (!jedi) { - if (this.lsActivatedWorkspaces.has(this.getWorkspacePathKey(resource))) { - return; - } - const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined); - this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); - if (diagnostic.length) { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false }); - jedi = true; - } - } else { - if (this.jediActivatedOnce) { - return; - } - this.jediActivatedOnce = true; - } - - await this.logStartup(jedi); - let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; - let activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); - this.currentActivator = { jedi, activator }; + // Do the same thing as a get. + await this.get(resource); + } - try { - await activator.activate(resource); - if (!jedi) { - this.lsActivatedWorkspaces.set(this.getWorkspacePathKey(resource), activator); - } - } catch (ex) { - if (jedi) { - return; - } - //Language server fails, reverting to jedi - if (this.jediActivatedOnce) { - return; - } - this.jediActivatedOnce = true; - jedi = true; - await this.logStartup(jedi); - activatorName = LanguageServerActivator.Jedi; - activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); - this.currentActivator = { jedi, activator }; - await activator.activate(resource); + public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { + // See if we already have it or not + const key = await this.getKey(resource, interpreter); + let result: Promise | undefined = this.lsActivatedServers.get(key); + if (!result) { + result = this.createServer(resource, interpreter); + this.lsActivatedServers.set(key, result); } + return result; } public dispose() { if (this.currentActivator) { - this.currentActivator.activator.dispose(); + this.currentActivator.server.dispose(); } } @swallowExceptions('Send telemetry for Language Server current selection') @@ -149,19 +134,59 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv return enabled; } - protected onWorkspaceFoldersChanged() { + protected async onWorkspaceFoldersChanged() { //If an activated workspace folder was removed, dispose its activator - const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspacePathKey(workspaceFolder.uri)); - const activatedWkspcKeys = Array.from(this.lsActivatedWorkspaces.keys()); + const workspaceKeys = await Promise.all(this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getKey(workspaceFolder.uri))); + const activatedWkspcKeys = Array.from(this.lsActivatedServers.keys()); const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { - this.lsActivatedWorkspaces.get(folder)!.dispose(); - this.lsActivatedWorkspaces!.delete(folder); + this.lsActivatedServers.get(folder)!.then(a => a.dispose()).ignoreErrors(); + this.lsActivatedServers!.delete(folder); } } } + private async createServer(resource: Resource, interpreter?: PythonInterpreter): Promise { + let jedi = this.useJedi(); + if (!jedi) { + const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined); + this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); + if (diagnostic.length) { + sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false }); + jedi = true; + } + } else if (this.jediServer) { + return this.jediServer; + } + + await this.logStartup(jedi); + let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; + let server = this.serviceContainer.get(ILanguageServerActivator, serverName); + this.currentActivator = { jedi, server }; + + try { + await server.activate(resource); + } catch (ex) { + if (jedi) { + throw ex; + } + jedi = true; + await this.logStartup(jedi); + serverName = LanguageServerActivator.Jedi; + server = this.serviceContainer.get(ILanguageServerActivator, serverName); + this.currentActivator = { jedi, server }; + await server.activate(resource, interpreter); + } + + // Jedi is always a singleton. Don't need to create it more than once. + if (jedi) { + this.jediServer = server; + } + + return server; + } + private async logStartup(isJedi: boolean): Promise { const outputLine = isJedi ? 'Starting Jedi Python language engine.' @@ -189,7 +214,10 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv this.serviceContainer.get(ICommandManager).executeCommand('workbench.action.reloadWindow'); } } - private getWorkspacePathKey(resource: Resource): string { - return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); + private async getKey(resource: Resource, interpreter?: PythonInterpreter): Promise { + const resourcePortion = this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); + interpreter = interpreter ? interpreter : await this.interpreterService.getActiveInterpreter(resource); + const interperterPortion = interpreter ? interpreter.path : ''; + return `${resourcePortion}-${interperterPortion}`; } } diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index e9189ffee6f9..5ec091b546c8 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -2,10 +2,32 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { DocumentFilter, languages } from 'vscode'; +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentFilter, + DocumentSymbol, + Event, + Hover, + languages, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + WorkspaceEdit +} from 'vscode'; + import { PYTHON } from '../common/constants'; import { IConfigurationService, IExtensionContext, ILogger, Resource } from '../common/types'; -import { IShebangCodeLensProvider } from '../interpreter/contracts'; +import { IShebangCodeLensProvider, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; import { PythonCompletionItemProvider } from '../providers/completionProvider'; @@ -28,12 +50,21 @@ export class JediExtensionActivator implements ILanguageServerActivator { private readonly context: IExtensionContext; private jediFactory?: JediFactory; private readonly documentSelector: DocumentFilter[]; + private renameProvider: PythonRenameProvider | undefined; + private hoverProvider: PythonHoverProvider | undefined; + private definitionProvider: PythonDefinitionProvider | undefined; + private referenceProvider: PythonReferenceProvider | undefined; + private completionProvider: PythonCompletionItemProvider | undefined; + private codeLensProvider: IShebangCodeLensProvider | undefined; + private symbolProvider: JediSymbolProvider | undefined; + private signatureProvider: PythonSignatureProvider | undefined; + constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { this.context = this.serviceManager.get(IExtensionContext); this.documentSelector = PYTHON; } - public async activate(_resource: Resource): Promise { + public async activate(_resource: Resource, _interpreter?: PythonInterpreter): Promise { if (this.jediFactory) { throw new Error('Jedi already started'); } @@ -43,30 +74,35 @@ export class JediExtensionActivator implements ILanguageServerActivator { context.subscriptions.push(jediFactory); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + this.renameProvider = new PythonRenameProvider(this.serviceManager); + this.definitionProvider = new PythonDefinitionProvider(jediFactory); + this.hoverProvider = new PythonHoverProvider(jediFactory); + this.referenceProvider = new PythonReferenceProvider(jediFactory); + this.completionProvider = new PythonCompletionItemProvider(jediFactory, this.serviceManager); + this.codeLensProvider = this.serviceManager.get(IShebangCodeLensProvider); + context.subscriptions.push(jediFactory); context.subscriptions.push( - languages.registerRenameProvider(this.documentSelector, new PythonRenameProvider(this.serviceManager)) + languages.registerRenameProvider(this.documentSelector, this.renameProvider) ); - const definitionProvider = new PythonDefinitionProvider(jediFactory); - - context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, definitionProvider)); + context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider)); context.subscriptions.push( - languages.registerHoverProvider(this.documentSelector, new PythonHoverProvider(jediFactory)) + languages.registerHoverProvider(this.documentSelector, this.hoverProvider) ); context.subscriptions.push( - languages.registerReferenceProvider(this.documentSelector, new PythonReferenceProvider(jediFactory)) + languages.registerReferenceProvider(this.documentSelector, this.referenceProvider) ); context.subscriptions.push( languages.registerCompletionItemProvider( this.documentSelector, - new PythonCompletionItemProvider(jediFactory, this.serviceManager), + this.completionProvider, '.' ) ); context.subscriptions.push( languages.registerCodeLensProvider( this.documentSelector, - this.serviceManager.get(IShebangCodeLensProvider) + this.codeLensProvider ) ); @@ -87,17 +123,17 @@ export class JediExtensionActivator implements ILanguageServerActivator { } const serviceContainer = this.serviceManager.get(IServiceContainer); + this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); + this.signatureProvider = new PythonSignatureProvider(jediFactory); context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - - const symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); - context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider)); + context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { context.subscriptions.push( languages.registerSignatureHelpProvider( this.documentSelector, - new PythonSignatureProvider(jediFactory), + this.signatureProvider, '(', ',' ) @@ -110,10 +146,54 @@ export class JediExtensionActivator implements ILanguageServerActivator { const testManagementService = this.serviceManager.get(ITestManagementService); testManagementService - .activate(symbolProvider) + .activate(this.symbolProvider) .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); } + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { + if (this.renameProvider) { + return this.renameProvider.provideRenameEdits(document, position, newName, token); + } + } + public provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + if (this.definitionProvider) { + return this.definitionProvider.provideDefinition(document, position, token); + } + } + public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + if (this.hoverProvider) { + return this.hoverProvider.provideHover(document, position, token); + } + } + public provideReferences(document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult { + if (this.referenceProvider) { + return this.referenceProvider.provideReferences(document, position, context, token); + } + } + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, _context: CompletionContext): ProviderResult { + if (this.completionProvider) { + return this.completionProvider.provideCompletionItems(document, position, token); + } + } + public get onDidChangeCodeLenses(): Event | undefined { + return this.codeLensProvider ? this.codeLensProvider.onDidChangeCodeLenses : undefined; + } + public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { + if (this.codeLensProvider) { + return this.codeLensProvider.provideCodeLenses(document, token); + } + } + public provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult { + if (this.symbolProvider) { + return this.symbolProvider.provideDocumentSymbols(document, token); + } + } + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, _context: SignatureHelpContext): ProviderResult { + if (this.signatureProvider) { + return this.signatureProvider.provideSignatureHelp(document, position, token); + } + } + public dispose(): void { if (this.jediFactory) { this.jediFactory.dispose(); diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index 0bb2199c9f0c..4a81f478522b 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -1,15 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { inject, injectable } from 'inversify'; import * as path from 'path'; +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + TextDocumentContentChangeEvent, + WorkspaceEdit +} from 'vscode'; +import * as vscodeLanguageClient from 'vscode-languageclient'; + import { IWorkspaceService } from '../../common/application/types'; import { traceDecorators } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, Resource } from '../../common/types'; import { EXTENSION_ROOT_DIR } from '../../constants'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { ILanguageServerActivator, ILanguageServerDownloader, @@ -36,7 +56,7 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato @inject(IConfigurationService) private readonly configurationService: IConfigurationService ) { } @traceDecorators.error('Failed to activate language server') - public async activate(resource: Resource): Promise { + public async activate(resource: Resource, interpreter?: PythonInterpreter): Promise { if (!resource) { resource = this.workspace.hasWorkspaceFolders ? this.workspace.workspaceFolders![0].uri @@ -44,7 +64,7 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato } this.resource = resource; await this.ensureLanguageServerIsAvailable(resource); - await this.manager.start(resource); + await this.manager.start(resource, interpreter); } public dispose(): void { this.manager.dispose(); @@ -83,4 +103,213 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato content.runtimeOptions.configProperties['System.Globalization.Invariant'] = true; await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); } + + public handleOpen(document: TextDocument): void { + const languageClient = this.getLanguageClient(); + if (languageClient) { + languageClient.sendNotification(vscodeLanguageClient.DidOpenTextDocumentNotification.type, + languageClient.code2ProtocolConverter.asOpenTextDocumentParams(document)); + } + } + + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void { + const languageClient = this.getLanguageClient(); + if (languageClient) { + languageClient.sendNotification(vscodeLanguageClient.DidChangeTextDocumentNotification.type, + languageClient.code2ProtocolConverter.asChangeTextDocumentParams({ document, contentChanges: changes })); + } + } + + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { + return this.handleProvideRenameEdits(document, position, newName, token); + } + + public provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + return this.handleProvideDefinition(document, position, token); + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + return this.handleProvideHover(document, position, token); + } + + public provideReferences(document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult { + return this.handleProvideReferences(document, position, context, token); + } + + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { + return this.handleProvideCompletionItems(document, position, token, context); + } + + public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { + return this.handleProvideCodeLenses(document, token); + } + + public provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult { + return this.handleProvideDocumentSymbols(document, token); + } + + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult { + return this.handleProvideSignatureHelp(document, position, token, context); + } + + private getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { + const proxy = this.manager.languageProxy; + if (proxy) { + return proxy.languageClient; + } + } + + private async handleProvideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.RenameParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + newName + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.RenameRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asWorkspaceEdit(result); + } + } + } + + private async handleProvideDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position) + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.DefinitionRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asDefinitionResult(result); + } + } + } + + private async handleProvideHover(document: TextDocument, position: Position, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position) + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.HoverRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asHover(result); + } + } + } + + private async handleProvideReferences(document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.ReferenceParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position), + context + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.ReferencesRequest.type, + args, + token + ); + if (result) { + // Remove undefined part. + return result.map(l => { + const r = languageClient!.protocol2CodeConverter.asLocation(l); + return r!; + }); + } + } + } + + private async handleProvideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.CodeLensParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document) + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.CodeLensRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asCodeLenses(result); + } + } + } + + private async handleProvideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args = languageClient.code2ProtocolConverter.asCompletionParams(document, position, context); + const result = await languageClient.sendRequest( + vscodeLanguageClient.CompletionRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asCompletionResult(result); + } + } + } + + private async handleProvideDocumentSymbols(document: TextDocument, token: CancellationToken): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.DocumentSymbolParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document) + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.DocumentSymbolRequest.type, + args, + token + ); + if (result && result.length) { + // tslint:disable-next-line: no-any + if ((result[0] as any).range) { + // Document symbols + const docSymbols = result as vscodeLanguageClient.DocumentSymbol[]; + return languageClient.protocol2CodeConverter.asDocumentSymbols(docSymbols); + } else { + // Document symbols + const symbols = result as vscodeLanguageClient.SymbolInformation[]; + return languageClient.protocol2CodeConverter.asSymbolInformations(symbols); + } + } + } + } + + private async handleProvideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, _context: SignatureHelpContext): Promise { + const languageClient = this.getLanguageClient(); + if (languageClient) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: languageClient.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: languageClient.code2ProtocolConverter.asPosition(position) + }; + const result = await languageClient.sendRequest( + vscodeLanguageClient.SignatureHelpRequest.type, + args, + token + ); + if (result) { + return languageClient.protocol2CodeConverter.asSignatureHelp(result); + } + } + } } diff --git a/src/client/activation/languageServer/languageClientFactory.ts b/src/client/activation/languageServer/languageClientFactory.ts index f030297ec183..a71f591027aa 100644 --- a/src/client/activation/languageServer/languageClientFactory.ts +++ b/src/client/activation/languageServer/languageClientFactory.ts @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient'; + import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; import { IConfigurationService, Resource } from '../../common/types'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { ILanguageClientFactory, ILanguageServerFolderService, IPlatformData, LanguageClientFactory } from '../types'; // tslint:disable:no-require-imports no-require-imports no-var-requires max-classes-per-file @@ -24,15 +23,15 @@ export class BaseLanguageClientFactory implements ILanguageClientFactory { @inject(IConfigurationService) private readonly configurationService: IConfigurationService, @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, @inject(IEnvironmentActivationService) private readonly environmentActivationService: IEnvironmentActivationService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions): Promise { + public async createLanguageClient(resource: Resource, interpreter: PythonInterpreter | undefined, clientOptions: LanguageClientOptions): Promise { const settings = this.configurationService.getSettings(resource); const factory = settings.downloadLanguageServer ? this.downloadedFactory : this.simpleFactory; - const env = await this.getEnvVars(resource); - return factory.createLanguageClient(resource, clientOptions, env); + const env = await this.getEnvVars(resource, interpreter); + return factory.createLanguageClient(resource, interpreter, clientOptions, env); } - private async getEnvVars(resource: Resource): Promise { - const envVars = await this.environmentActivationService.getActivatedEnvironmentVariables(resource); + private async getEnvVars(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { + const envVars = await this.environmentActivationService.getActivatedEnvironmentVariables(resource, interpreter); if (envVars && Object.keys(envVars).length > 0) { return envVars; } @@ -51,7 +50,7 @@ export class BaseLanguageClientFactory implements ILanguageClientFactory { export class DownloadedLanguageClientFactory implements ILanguageClientFactory { constructor(@inject(IPlatformData) private readonly platformData: IPlatformData, @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { + public async createLanguageClient(resource: Resource, _interpreter: PythonInterpreter | undefined, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, this.platformData.engineExecutableName); const options = { stdio: 'pipe', env }; @@ -75,7 +74,7 @@ export class DownloadedLanguageClientFactory implements ILanguageClientFactory { export class SimpleLanguageClientFactory implements ILanguageClientFactory { constructor(@inject(IPlatformData) private readonly platformData: IPlatformData, @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { + public async createLanguageClient(resource: Resource, _interpreter: PythonInterpreter | undefined, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); const options = { stdio: 'pipe', env }; const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, this.platformData.engineDllName); diff --git a/src/client/activation/languageServer/languageServer.ts b/src/client/activation/languageServer/languageServerProxy.ts similarity index 92% rename from src/client/activation/languageServer/languageServer.ts rename to src/client/activation/languageServer/languageServerProxy.ts index 05040c935d2e..d4e6426b9c5a 100644 --- a/src/client/activation/languageServer/languageServer.ts +++ b/src/client/activation/languageServer/languageServerProxy.ts @@ -1,27 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; +import '../../common/extensions'; import { inject, injectable, named } from 'inversify'; import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; + import { ICommandManager } from '../../common/application/types'; -import '../../common/extensions'; import { traceDecorators, traceError } from '../../common/logger'; import { IConfigurationService, Resource } from '../../common/types'; import { createDeferred, Deferred, sleep } from '../../common/utils/async'; import { swallowExceptions } from '../../common/utils/decorators'; import { noop } from '../../common/utils/misc'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { LanguageServerSymbolProvider } from '../../providers/symbolProvider'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITestManagementService } from '../../testing/types'; -import { ILanguageClientFactory, ILanguageServer, LanguageClientFactory } from '../types'; +import { ILanguageClientFactory, ILanguageServerProxy, LanguageClientFactory } from '../types'; import { Commands } from './constants'; import { ProgressReporting } from './progress'; @injectable() -export class LanguageServer implements ILanguageServer { +export class LanguageServerProxy implements ILanguageServerProxy { public languageClient: LanguageClient | undefined; private startupCompleted: Deferred; private readonly disposables: Disposable[] = []; @@ -58,9 +58,9 @@ export class LanguageServer implements ILanguageServer { @traceDecorators.error('Failed to start language server') @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_ENABLED, undefined, true) - public async start(resource: Resource, options: LanguageClientOptions): Promise { + public async start(resource: Resource, interpreter: PythonInterpreter | undefined, options: LanguageClientOptions): Promise { if (!this.languageClient) { - this.languageClient = await this.factory.createLanguageClient(resource, options); + this.languageClient = await this.factory.createLanguageClient(resource, interpreter, options); this.disposables.push(this.languageClient!.start()); await this.serverReady(); if (this.disposed) { diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 91c6da33c718..c2a48aa72718 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -1,22 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; +import '../../common/extensions'; import { inject, injectable } from 'inversify'; -import '../../common/extensions'; + import { traceDecorators } from '../../common/logger'; import { IDisposable, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension, ILanguageServerManager } from '../types'; +import { + ILanguageServerAnalysisOptions, + ILanguageServerExtension, + ILanguageServerManager, + ILanguageServerProxy +} from '../types'; @injectable() export class LanguageServerManager implements ILanguageServerManager { - private languageServer?: ILanguageServer; + private languageServerProxy?: ILanguageServerProxy; private resource!: Resource; + private interpreter: PythonInterpreter | undefined; private disposables: IDisposable[] = []; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @@ -24,18 +30,23 @@ export class LanguageServerManager implements ILanguageServerManager { @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension ) { } public dispose() { - if (this.languageServer) { - this.languageServer.dispose(); + if (this.languageProxy) { + this.languageProxy.dispose(); } this.disposables.forEach(d => d.dispose()); } + + public get languageProxy() { + return this.languageServerProxy; + } @traceDecorators.error('Failed to start Language Server') - public async start(resource: Resource): Promise { - if (this.languageServer) { + public async start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise { + if (this.languageProxy) { throw new Error('Language Server already started'); } this.registerCommandHandler(); this.resource = resource; + this.interpreter = interpreter; this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); await this.analysisOptions.initialize(resource); @@ -45,8 +56,8 @@ export class LanguageServerManager implements ILanguageServerManager { this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); } protected loadExtensionIfNecessary() { - if (this.languageServer && this.lsExtension.loadExtensionArgs) { - this.languageServer.loadExtension(this.lsExtension.loadExtensionArgs); + if (this.languageProxy && this.lsExtension.loadExtensionArgs) { + this.languageProxy.loadExtension(this.lsExtension.loadExtensionArgs); } } @debounceSync(1000) @@ -56,17 +67,17 @@ export class LanguageServerManager implements ILanguageServerManager { @traceDecorators.error('Failed to restart Language Server') @traceDecorators.verbose('Restarting Language Server') protected async restartLanguageServer(): Promise { - if (this.languageServer) { - this.languageServer.dispose(); + if (this.languageProxy) { + this.languageProxy.dispose(); } await this.startLanguageServer(); } @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_STARTUP, undefined, true) @traceDecorators.verbose('Starting Language Server') protected async startLanguageServer(): Promise { - this.languageServer = this.serviceContainer.get(ILanguageServer); + this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); const options = await this.analysisOptions!.getAnalysisOptions(); - await this.languageServer.start(this.resource, options); + await this.languageServerProxy.start(this.resource, this.interpreter, options); this.loadExtensionIfNecessary(); } } diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 4b8daee8dc9b..c038d8c9c463 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { INugetRepository } from '../common/nuget/types'; -import { BANNER_NAME_DS_SURVEY, BANNER_NAME_INTERACTIVE_SHIFTENTER, BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types'; +import { + BANNER_NAME_DS_SURVEY, + BANNER_NAME_INTERACTIVE_SHIFTENTER, + BANNER_NAME_LS_SURVEY, + BANNER_NAME_PROPOSE_LS, + IPythonExtensionBanner +} from '../common/types'; import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner'; import { InteractiveShiftEnterBanner } from '../datascience/shiftEnterBanner'; import { IServiceManager } from '../ioc/types'; @@ -19,17 +22,45 @@ import { LanguageServerExtensionActivator } from './languageServer/activator'; import { LanguageServerAnalysisOptions } from './languageServer/analysisOptions'; import { DownloadBetaChannelRule, DownloadDailyChannelRule } from './languageServer/downloadChannelRules'; import { LanguageServerDownloader } from './languageServer/downloader'; -import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from './languageServer/languageClientFactory'; -import { LanguageServer } from './languageServer/languageServer'; +import { + BaseLanguageClientFactory, + DownloadedLanguageClientFactory, + SimpleLanguageClientFactory +} from './languageServer/languageClientFactory'; import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService'; import { LanguageServerExtension } from './languageServer/languageServerExtension'; import { LanguageServerFolderService } from './languageServer/languageServerFolderService'; -import { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } from './languageServer/languageServerPackageRepository'; +import { + BetaLanguageServerPackageRepository, + DailyLanguageServerPackageRepository, + LanguageServerDownloadChannel, + StableLanguageServerPackageRepository +} from './languageServer/languageServerPackageRepository'; import { LanguageServerPackageService } from './languageServer/languageServerPackageService'; +import { LanguageServerProxy } from './languageServer/languageServerProxy'; import { LanguageServerManager } from './languageServer/manager'; import { LanguageServerOutputChannel } from './languageServer/outputChannel'; import { PlatformData } from './languageServer/platformData'; -import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types'; +import { + IDownloadChannelRule, + IExtensionActivationManager, + IExtensionActivationService, + IExtensionSingleActivationService, + ILanguageClientFactory, + ILanguageServerActivator, + ILanguageServerAnalysisOptions, + ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, + ILanguageServerDownloader, + ILanguageServerExtension, + ILanguageServerFolderService, + ILanguageServerManager, + ILanguageServerOutputChannel, + ILanguageServerPackageService, + ILanguageServerProxy, + IPlatformData, + LanguageClientFactory, + LanguageServerActivator +} from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IExtensionActivationService, LanguageServerExtensionActivationService); @@ -56,7 +87,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader); serviceManager.addSingleton(IPlatformData, PlatformData); serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions); - serviceManager.addSingleton(ILanguageServer, LanguageServer); + serviceManager.addSingleton(ILanguageServerProxy, LanguageServerProxy); serviceManager.add(ILanguageServerManager, LanguageServerManager); serviceManager.addSingleton(ILanguageServerOutputChannel, LanguageServerOutputChannel); serviceManager.addSingleton(IExtensionSingleActivationService, ExtensionSurveyPrompt); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 078d98760b1a..3919b08ad3c4 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -4,10 +4,23 @@ 'use strict'; import { SemVer } from 'semver'; -import { Event } from 'vscode'; +import { + CodeLensProvider, + CompletionItemProvider, + DefinitionProvider, + DocumentSymbolProvider, + Event, + HoverProvider, + ReferenceProvider, + RenameProvider, + SignatureHelpProvider, + TextDocument, + TextDocumentContentChangeEvent +} from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { NugetPackage } from '../common/nuget/types'; import { IDisposable, IOutputChannel, LanguageServerDownloadChannels, Resource } from '../common/types'; +import { PythonInterpreter } from '../interpreter/contracts'; export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); /** @@ -55,9 +68,32 @@ export enum LanguageServerActivator { DotNet = 'DotNet' } +// tslint:disable-next-line: interface-name +export interface DocumentHandler { + handleOpen(document: TextDocument): void; + handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void; +} + +export interface ILanguageServer extends + RenameProvider, + DefinitionProvider, + HoverProvider, + ReferenceProvider, + CompletionItemProvider, + CodeLensProvider, + DocumentSymbolProvider, + SignatureHelpProvider, + Partial { +} + export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends IDisposable { - activate(resource: Resource): Promise; +export interface ILanguageServerActivator extends ILanguageServer, IDisposable { + activate(resource: Resource, interpreter?: PythonInterpreter): Promise; +} + +export const ILanguageServerCache = Symbol('ILanguageServerCache'); +export interface ILanguageServerCache { + get(resource: Resource, interpreter?: PythonInterpreter): Promise; } export type FolderVersionPair = { path: string; version: SemVer }; @@ -98,7 +134,7 @@ export enum LanguageClientFactory { } export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); export interface ILanguageClientFactory { - createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise; + createLanguageClient(resource: Resource, interpreter: PythonInterpreter | undefined, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise; } export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); export interface ILanguageServerAnalysisOptions extends IDisposable { @@ -108,7 +144,8 @@ export interface ILanguageServerAnalysisOptions extends IDisposable { } export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { - start(resource: Resource): Promise; + readonly languageProxy: ILanguageServerProxy | undefined; + start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; } export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); export interface ILanguageServerExtension extends IDisposable { @@ -116,19 +153,19 @@ export interface ILanguageServerExtension extends IDisposable { loadExtensionArgs?: {}; register(): void; } -export const ILanguageServer = Symbol('ILanguageServer'); -export interface ILanguageServer extends IDisposable { +export const ILanguageServerProxy = Symbol('ILanguageServerProxy'); +export interface ILanguageServerProxy extends IDisposable { /** * LanguageClient in use */ languageClient: LanguageClient | undefined; - start(resource: Resource, options: LanguageClientOptions): Promise; + start(resource: Resource, interpreter: PythonInterpreter | undefined, options: LanguageClientOptions): Promise; /** * Sends a request to LS so as to load other extensions. * This is used as a plugin loader mechanism. * Anyone (such as intellicode) wanting to interact with LS, needs to send this request to LS. * @param {{}} [args] - * @memberof ILanguageServer + * @memberof ILanguageServerProxy */ loadExtension(args?: {}): void; } diff --git a/src/client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider.ts deleted file mode 100644 index 8ae070c41869..000000000000 --- a/src/client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { CancellationToken, TextDocumentContentChangeEvent, Uri } from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient'; - -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../../activation/types'; -import { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { Identifiers } from '../../constants'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types'; -import { BaseIntellisenseProvider } from './baseIntellisenseProvider'; -import { convertToMonacoCompletionList, convertToMonacoHover, convertToMonacoSignatureHelp } from './conversion'; -import { IntellisenseDocument } from './intellisenseDocument'; - -// tslint:disable:no-any -@injectable() -export class DotNetIntellisenseProvider extends BaseIntellisenseProvider implements IInteractiveWindowListener { - - private languageClientPromise: Deferred | undefined; - private sentOpenDocument: boolean = false; - private active: boolean = false; - - constructor( - @inject(ILanguageServer) private languageServer: ILanguageServer, - @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IJupyterExecution) jupyterExecution: IJupyterExecution, - @inject(IInteractiveWindowProvider) interactiveWindowProvider: IInteractiveWindowProvider - ) { - super(workspaceService, fileSystem, jupyterExecution, interactiveWindowProvider); - - // Make sure we're active. We still listen to messages for adding and editing cells, - // but we don't actually return any data. - const isLsActive = () => { - const lsSetting = this.configService.getSettings().jediEnabled; - return !lsSetting; - }; - this.active = isLsActive(); - - // Listen for updates to settings to change this flag. Don't bother disposing the config watcher. It lives - // till the extension dies anyway. - this.configService.getSettings().onDidChange(() => this.active = isLsActive()); - } - - protected get isActive(): boolean { - return this.active; - } - - protected async provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken): Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.CompletionRequest.type, - languageClient.code2ProtocolConverter.asCompletionParams(document, docPos, context), - token); - return convertToMonacoCompletionList(result, true); - } - - return { - suggestions: [], - incomplete: false - }; - } - protected async provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken): Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.HoverRequest.type, - languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, docPos), - token); - return convertToMonacoHover(result); - } - - return { - contents: [] - }; - } - protected async provideSignatureHelp(position: monacoEditor.Position, _context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken): Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.SignatureHelpRequest.type, - languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, docPos), - token); - return convertToMonacoSignatureHelp(result); - } - - return { - signatures: [], - activeParameter: 0, - activeSignature: 0 - }; - } - - protected async handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]): Promise { - // Then see if we can talk to our language client - if (this.active && document) { - - // Cache our document state as it may change after we get our language client. Async call may allow a change to - // come in before we send the first doc open. - const docItem = document.textDocumentItem; - const docItemId = document.textDocumentId; - - // Broadcast an update to the language server - const languageClient = await this.getLanguageClient(originalFile === Identifiers.EmptyFileName || originalFile === undefined ? undefined : Uri.file(originalFile)); - - if (!this.sentOpenDocument) { - this.sentOpenDocument = true; - return languageClient.sendNotification(vscodeLanguageClient.DidOpenTextDocumentNotification.type, { textDocument: docItem }); - } else { - return languageClient.sendNotification(vscodeLanguageClient.DidChangeTextDocumentNotification.type, { textDocument: docItemId, contentChanges: changes }); - } - } - } - - private getLanguageClient(file?: Uri): Promise { - if (!this.languageClientPromise) { - this.languageClientPromise = createDeferred(); - this.startup(file) - .then(() => { - this.languageClientPromise!.resolve(this.languageServer.languageClient); - }) - .catch((e: any) => { - this.languageClientPromise!.reject(e); - }); - } - return this.languageClientPromise.promise; - } - - private async startup(resource?: Uri): Promise { - // Start up the language server. We'll use this to talk to the language server - const options = await this.analysisOptions!.getAnalysisOptions(); - await this.languageServer.start(resource, options); - } -} diff --git a/src/client/datascience/interactive-common/intellisense/baseIntellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts similarity index 72% rename from src/client/datascience/interactive-common/intellisense/baseIntellisenseProvider.ts rename to src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 54f938188f14..109d992b38c0 100644 --- a/src/client/datascience/interactive-common/intellisense/baseIntellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -3,7 +3,7 @@ 'use strict'; import '../../../common/extensions'; -import { injectable, unmanaged } from 'inversify'; +import { inject, injectable } from 'inversify'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; import * as path from 'path'; import * as uuid from 'uuid/v4'; @@ -12,16 +12,19 @@ import { CancellationTokenSource, Event, EventEmitter, + SignatureHelpContext, TextDocumentContentChangeEvent, Uri } from 'vscode'; -import { HiddenFileFormatString } from '../../../../client/constants'; +import { ILanguageServer, ILanguageServerCache } from '../../../activation/types'; import { IWorkspaceService } from '../../../common/application/types'; import { CancellationError } from '../../../common/cancellation'; import { traceWarning } from '../../../common/logger'; import { IFileSystem, TemporaryFile } from '../../../common/platform/types'; import { createDeferred, Deferred, waitForPromise } from '../../../common/utils/async'; +import { HiddenFileFormatString } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { concatMultilineStringInput } from '../../common'; import { Identifiers, Settings } from '../../constants'; import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution, INotebook } from '../../types'; @@ -40,24 +43,33 @@ import { IRemoveCell, ISwapCells } from '../interactiveWindowTypes'; -import { convertStringsToSuggestions } from './conversion'; +import { + convertStringsToSuggestions, + convertToMonacoCompletionList, + convertToMonacoHover, + convertToMonacoSignatureHelp +} from './conversion'; import { IntellisenseDocument } from './intellisenseDocument'; // tslint:disable:no-any @injectable() -export abstract class BaseIntellisenseProvider implements IInteractiveWindowListener { +export class IntellisenseProvider implements IInteractiveWindowListener { private documentPromise: Deferred | undefined; private temporaryFile: TemporaryFile | undefined; private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ message: string; payload: any }>(); private cancellationSources: Map = new Map(); private notebookIdentity: Uri | undefined; + private potentialResource: Uri | undefined; + private sentOpenDocument: boolean = false; constructor( - @unmanaged() private workspaceService: IWorkspaceService, - @unmanaged() private fileSystem: IFileSystem, - @unmanaged() private jupyterExecution: IJupyterExecution, - @unmanaged() private interactiveWindowProvider: IInteractiveWindowProvider + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache ) { } @@ -75,27 +87,19 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList switch (message) { case InteractiveWindowMessages.CancelCompletionItemsRequest: case InteractiveWindowMessages.CancelHoverRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleCancel); - } + this.dispatchMessage(message, payload, this.handleCancel); break; case InteractiveWindowMessages.ProvideCompletionItemsRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleCompletionItemsRequest); - } + this.dispatchMessage(message, payload, this.handleCompletionItemsRequest); break; case InteractiveWindowMessages.ProvideHoverRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleHoverRequest); - } + this.dispatchMessage(message, payload, this.handleHoverRequest); break; case InteractiveWindowMessages.ProvideSignatureHelpRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleSignatureHelpRequest); - } + this.dispatchMessage(message, payload, this.handleSignatureHelpRequest); break; case InteractiveWindowMessages.EditCell: @@ -139,6 +143,18 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList } } + protected async getLanguageServer(): Promise { + // Resource should be our potential resource if its set. Otherwise workspace root + const resource = this.potentialResource || (this.workspaceService.rootPath ? Uri.parse(this.workspaceService.rootPath) : undefined); + + // Interpreter should be the interpreter currently active in the notebook + const activeNotebook = await this.getNotebook(); + const interpreter = activeNotebook ? await activeNotebook.getMatchingInterpreter() : await this.interpreterService.getActiveInterpreter(resource); + + // Use the resource and the interpreter to get our language server + return this.languageServerCache.get(resource, interpreter); + } + protected getDocument(resource?: Uri): Promise { if (!this.documentPromise) { this.documentPromise = createDeferred(); @@ -164,11 +180,70 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList return this.documentPromise.promise; } - protected abstract get isActive(): boolean; - protected abstract provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken): Promise; - protected abstract provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken): Promise; - protected abstract provideSignatureHelp(position: monacoEditor.Position, context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken): Promise; - protected abstract handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]): Promise; + protected async provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken): Promise { + const languageServer = await this.getLanguageServer(); + const document = await this.getDocument(); + if (languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const result = await languageServer.provideCompletionItems(document, docPos, token, context); + if (result) { + return convertToMonacoCompletionList(result, true); + } + } + + return { + suggestions: [], + incomplete: false + }; + } + protected async provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken): Promise { + const languageServer = await this.getLanguageServer(); + const document = await this.getDocument(); + if (languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const result = await languageServer.provideHover(document, docPos, token); + if (result) { + return convertToMonacoHover(result); + } + } + + return { + contents: [] + }; + } + protected async provideSignatureHelp(position: monacoEditor.Position, context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken): Promise { + const languageServer = await this.getLanguageServer(); + const document = await this.getDocument(); + if (languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const result = await languageServer.provideSignatureHelp(document, docPos, token, context as SignatureHelpContext); + if (result) { + return convertToMonacoSignatureHelp(result); + } + } + + return { + signatures: [], + activeParameter: 0, + activeSignature: 0 + }; + } + + protected async handleChanges(document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]): Promise { + // For the dot net language server, we have to send extra data to the language server + if (document) { + // Broadcast an update to the language server + const languageServer = await this.getLanguageServer(); + if (languageServer && languageServer.handleChanges && languageServer.handleOpen) { + if (!this.sentOpenDocument) { + this.sentOpenDocument = true; + return languageServer.handleOpen(document); + } else { + return languageServer.handleChanges(document, changes); + } + } + } + } private dispatchMessage(_message: T, payload: any, handler: (args: M[T]) => void) { const args = payload as M[T]; @@ -329,11 +404,16 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList } private async addCell(request: IAddCell): Promise { + // Save this request file as our potential resource + if (request.cell.file !== Identifiers.EmptyFileName) { + this.potentialResource = Uri.file(request.cell.file); + } + // Get the document and then pass onto the sub class const document = await this.getDocument(request.cell.file === Identifiers.EmptyFileName ? undefined : Uri.file(request.cell.file)); if (document) { const changes = document.addCell(request.fullText, request.currentText, request.cell.id); - return this.handleChanges(request.cell.file, document, changes); + return this.handleChanges(document, changes); } } @@ -342,7 +422,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.insertCell(request.cell.id, request.code, request.codeCellAboveId); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } @@ -351,7 +431,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.edit(request.changes, request.id); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } @@ -360,7 +440,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.remove(request.id); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } @@ -369,7 +449,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.swap(request.firstCellId, request.secondCellId); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } @@ -378,7 +458,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.removeAll(); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } @@ -392,7 +472,7 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList }; })); - await this.handleChanges(Identifiers.EmptyFileName, document, changes); + await this.handleChanges(document, changes); } } @@ -401,12 +481,13 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList const document = await this.getDocument(); if (document) { const changes = document.removeAllCells(); - return this.handleChanges(undefined, document, changes); + return this.handleChanges(document, changes); } } private setIdentity(identity: INotebookIdentity) { this.notebookIdentity = Uri.parse(identity.resource); + this.potentialResource = Uri.parse(identity.resource); } private async getNotebook(): Promise { @@ -420,4 +501,5 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList return undefined; } + } diff --git a/src/client/datascience/interactive-common/intellisense/jediIntellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/jediIntellisenseProvider.ts deleted file mode 100644 index e8dadf649efb..000000000000 --- a/src/client/datascience/interactive-common/intellisense/jediIntellisenseProvider.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { CancellationToken, TextDocumentContentChangeEvent } from 'vscode'; - -import { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../common/types'; -import { IServiceManager } from '../../../ioc/types'; -import { JediFactory } from '../../../languageServices/jediProxyFactory'; -import { PythonCompletionItemProvider } from '../../../providers/completionProvider'; -import { PythonHoverProvider } from '../../../providers/hoverProvider'; -import { PythonSignatureProvider } from '../../../providers/signatureProvider'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types'; -import { BaseIntellisenseProvider } from './baseIntellisenseProvider'; -import { convertToMonacoCompletionList, convertToMonacoHover, convertToMonacoSignatureHelp } from './conversion'; -import { IntellisenseDocument } from './intellisenseDocument'; - -// tslint:disable:no-any -@injectable() -export class JediIntellisenseProvider extends BaseIntellisenseProvider implements IInteractiveWindowListener { - - private active: boolean = false; - private pythonHoverProvider: PythonHoverProvider | undefined; - private pythonCompletionItemProvider: PythonCompletionItemProvider | undefined; - private pythonSignatureHelpProvider: PythonSignatureProvider | undefined; - private jediFactory: JediFactory; - private readonly context: IExtensionContext; - - constructor( - @inject(IServiceManager) private serviceManager: IServiceManager, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IJupyterExecution) jupyterExecution: IJupyterExecution, - @inject(IInteractiveWindowProvider) interactiveWindowProvider: IInteractiveWindowProvider - ) { - super(workspaceService, fileSystem, jupyterExecution, interactiveWindowProvider); - - this.context = this.serviceManager.get(IExtensionContext); - this.jediFactory = new JediFactory(this.context.asAbsolutePath('.'), this.serviceManager); - this.disposables.push(this.jediFactory); - - // Make sure we're active. We still listen to messages for adding and editing cells, - // but we don't actually return any data. - const isJediActive = () => { - return this.configService.getSettings().jediEnabled; - }; - this.active = isJediActive(); - - // Listen for updates to settings to change this flag - disposables.push(this.configService.getSettings().onDidChange(() => this.active = isJediActive())); - - // Create our jedi wrappers if necessary - if (this.active) { - this.pythonHoverProvider = new PythonHoverProvider(this.jediFactory); - this.pythonCompletionItemProvider = new PythonCompletionItemProvider(this.jediFactory, this.serviceManager); - this.pythonSignatureHelpProvider = new PythonSignatureProvider(this.jediFactory); - } - } - - public dispose() { - super.dispose(); - this.jediFactory.dispose(); - } - protected get isActive(): boolean { - return this.active; - } - protected async provideCompletionItems(position: monacoEditor.Position, _context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken): Promise { - const document = await this.getDocument(); - if (this.pythonCompletionItemProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonCompletionItemProvider.provideCompletionItems(document, docPos, token); - return convertToMonacoCompletionList(result, false); - } - - return { - suggestions: [], - incomplete: false - }; - } - protected async provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken): Promise { - const document = await this.getDocument(); - if (this.pythonHoverProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonHoverProvider.provideHover(document, docPos, token); - return convertToMonacoHover(result); - } - - return { - contents: [] - }; - } - protected async provideSignatureHelp(position: monacoEditor.Position, _context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken): Promise { - const document = await this.getDocument(); - if (this.pythonSignatureHelpProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonSignatureHelpProvider.provideSignatureHelp(document, docPos, token); - return convertToMonacoSignatureHelp(result); - } - - return { - signatures: [], - activeParameter: 0, - activeSignature: 0 - }; - } - - protected handleChanges(_originalFile: string | undefined, _document: IntellisenseDocument, _changes: TextDocumentContentChangeEvent[]): Promise { - // We don't need to forward these to jedi. It always uses the entire document - return Promise.resolve(); - } - -} diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index 5059a4bd4332..03f64f441f62 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; import '../../common/extensions'; // tslint:disable-next-line: no-require-imports @@ -23,6 +22,7 @@ import { createDeferred, Deferred, waitForPromise } from '../../common/utils/asy import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { generateCells } from '../cellFactory'; import { CellMatcher } from '../cellMatcher'; @@ -157,7 +157,8 @@ export class JupyterNotebookBase implements INotebook { resource: Uri, private getDisposedError: () => Error, private workspace: IWorkspaceService, - private applicationService: IApplicationShell + private applicationService: IApplicationShell, + private interpreterService: IInterpreterService ) { this.sessionStartTime = Date.now(); this._resource = resource; @@ -438,7 +439,7 @@ export class JupyterNotebookBase implements INotebook { cursor_pos: offsetInCode }), cancelToken); if (result && result.content) { - if ('matches' in result.content){ + if ('matches' in result.content) { return { matches: result.content.matches, cursor: { @@ -450,7 +451,7 @@ export class JupyterNotebookBase implements INotebook { } else { return { matches: [], - cursor : {start: 0, end: 0}, + cursor: { start: 0, end: 0 }, metadata: [] }; } @@ -461,6 +462,13 @@ export class JupyterNotebookBase implements INotebook { throw new Error(localize.DataScience.sessionDisposed()); } + public async getMatchingInterpreter(): Promise { + // tslint:disable-next-line: no-suspicious-comment + // TODO: This should use the kernel to determine which interpreter matches. Right now + // just return the active one + return this.interpreterService.getActiveInterpreter(); + } + private finishUncompletedCells() { const copyPending = [...this.pendingCellSubscriptions]; copyPending.forEach(c => c.cancel()); @@ -734,7 +742,7 @@ export class JupyterNotebookBase implements INotebook { .catch(e => { // @jupyterlab/services throws a `Canceled` error when the kernel is interrupted. // Such an error must be ignored. - if (e && e instanceof Error && e.message === 'Canceled'){ + if (e && e instanceof Error && e.message === 'Canceled') { subscriber.complete(this.sessionStartTime); } else { subscriber.error(this.sessionStartTime, e); diff --git a/src/client/datascience/jupyter/jupyterServerFactory.ts b/src/client/datascience/jupyter/jupyterServerFactory.ts index cde2e8cf79fa..2765688646cd 100644 --- a/src/client/datascience/jupyter/jupyterServerFactory.ts +++ b/src/client/datascience/jupyter/jupyterServerFactory.ts @@ -24,6 +24,7 @@ import { GuestJupyterServer } from './liveshare/guestJupyterServer'; import { HostJupyterServer } from './liveshare/hostJupyterServer'; import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; import { ILiveShareHasRole } from './liveshare/types'; +import { IInterpreterService } from '../../interpreter/contracts'; interface IJupyterServerInterface extends IRoleBasedObject, INotebookServer { } @@ -38,7 +39,8 @@ type JupyterServerClassType = { sessionManager: IJupyterSessionManagerFactory, workspaceService: IWorkspaceService, loggers: INotebookExecutionLogger[], - appShell: IApplicationShell + appShell: IApplicationShell, + interpreterService: IInterpreterService ): IJupyterServerInterface; }; // tslint:enable:callable-types diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts index f55a9c486323..bc00ba328a28 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts @@ -13,8 +13,16 @@ import { IConfigurationService, IDisposableRegistry } from '../../../common/type import { createDeferred } from '../../../common/utils/async'; import * as localize from '../../../common/utils/localize'; import { noop } from '../../../common/utils/misc'; +import { PythonInterpreter } from '../../../interpreter/contracts'; import { LiveShare, LiveShareCommands } from '../../constants'; -import { ICell, INotebook, INotebookCompletion, INotebookExecutionLogger, INotebookServer, InterruptResult } from '../../types'; +import { + ICell, + INotebook, + INotebookCompletion, + INotebookExecutionLogger, + INotebookServer, + InterruptResult +} from '../../types'; import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; import { ResponseQueue } from './responseQueue'; import { IExecuteObservableResponse, ILiveShareParticipant, IServerResponse } from './types'; @@ -172,6 +180,10 @@ export class GuestJupyterNotebook } } + public getMatchingInterpreter(): Promise { + return Promise.resolve(undefined); + } + private onServerResponse = (args: Object) => { const er = args as IExecuteObservableResponse; traceInfo(`Guest serverResponse ${er.pos} ${er.id}`); diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts index 0edf833bbf74..fd180e0260dc 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts @@ -30,6 +30,7 @@ import { LiveShareParticipantHost } from './liveShareParticipantMixin'; import { ResponseQueue } from './responseQueue'; import { IRoleBasedObject } from './roleBasedFactory'; import { IExecuteObservableResponse, IResponseMapping, IServerResponse, ServerResponseType } from './types'; +import { IInterpreterService } from '../../../interpreter/contracts'; // tslint:disable:no-any @@ -52,9 +53,10 @@ export class HostJupyterNotebook resource: vscode.Uri, getDisposedError: () => Error, workspace: IWorkspaceService, - appService: IApplicationShell + appService: IApplicationShell, + interpreterService: IInterpreterService, ) { - super(liveShare, session, configService, disposableRegistry, owner, launchInfo, loggers, resource, getDisposedError, workspace, appService); + super(liveShare, session, configService, disposableRegistry, owner, launchInfo, loggers, resource, getDisposedError, workspace, appService, interpreterService); } public dispose = async (): Promise => { diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index 3d3ca4cb441d..48174d3b208d 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -29,6 +29,7 @@ import { JupyterServerBase } from '../jupyterServer'; import { HostJupyterNotebook } from './hostJupyterNotebook'; import { LiveShareParticipantHost } from './liveShareParticipantMixin'; import { IRoleBasedObject } from './roleBasedFactory'; +import { IInterpreterService } from '../../../interpreter/contracts'; // tslint:disable-next-line: no-require-imports // tslint:disable:no-any @@ -48,7 +49,9 @@ export class HostJupyterServer sessionManager: IJupyterSessionManagerFactory, private workspaceService: IWorkspaceService, loggers: INotebookExecutionLogger[], - private appService: IApplicationShell) { + private appService: IApplicationShell, + private interpreterService: IInterpreterService + ) { super(liveShare, asyncRegistry, disposableRegistry, configService, sessionManager, loggers); } @@ -177,7 +180,8 @@ export class HostJupyterServer resource, this.getDisposedError.bind(this), this.workspaceService, - this.appService); + this.appService, + this.interpreterService); // Wait for it to be ready traceInfo(`Waiting for idle (session) ${this.id}`); diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index 8ed4cefeaa4a..eae6a76c1b3f 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -18,8 +18,7 @@ import { DataScienceErrorHandler } from './errorHandler/errorHandler'; import { GatherExecution } from './gather/gather'; import { GatherListener } from './gather/gatherListener'; import { DebugListener } from './interactive-common/debugListener'; -import { DotNetIntellisenseProvider } from './interactive-common/intellisense/dotNetIntellisenseProvider'; -import { JediIntellisenseProvider } from './interactive-common/intellisense/jediIntellisenseProvider'; +import { IntellisenseProvider } from './interactive-common/intellisense/intellisenseProvider'; import { LinkProvider } from './interactive-common/linkProvider'; import { ShowPlotListener } from './interactive-common/showPlotListener'; import { AutoSaveService } from './interactive-ipynb/autoSaveService'; @@ -99,8 +98,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDataViewerProvider, DataViewerProvider); serviceManager.add(IDataViewer, DataViewer); serviceManager.addSingleton(IExtensionSingleActivationService, Decorator); - serviceManager.add(IInteractiveWindowListener, DotNetIntellisenseProvider); - serviceManager.add(IInteractiveWindowListener, JediIntellisenseProvider); + serviceManager.add(IInteractiveWindowListener, IntellisenseProvider); serviceManager.add(IInteractiveWindowListener, LinkProvider); serviceManager.add(IInteractiveWindowListener, ShowPlotListener); serviceManager.add(IInteractiveWindowListener, DebugListener); diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 013a41dcc87f..13a59050d044 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -100,6 +100,7 @@ export interface INotebook extends IAsyncDisposable { getSysInfo(): Promise; setMatplotLibStyle(useDark: boolean): Promise; addLogger(logger: INotebookExecutionLogger): void; + getMatchingInterpreter(): Promise; } export interface INotebookServerOptions { diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts index 7f035270076b..50f3d9384d10 100644 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ b/src/test/activation/languageServer/activator.unit.test.ts @@ -57,12 +57,12 @@ suite('Language Server - Activator', () => { }); test('Manager must be started without any workspace', async () => { when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); + when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); await activator.activate(undefined); - verify(manager.start(undefined)).once(); + verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); }); test('Manager must be disposed', async () => { @@ -72,12 +72,12 @@ suite('Language Server - Activator', () => { }); test('Do not download LS if not required', async () => { when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); + when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); await activator.activate(undefined); - verify(manager.start(undefined)).once(); + verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); verify(lsFolderService.getLanguageServerFolderName(anything())).never(); verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); @@ -88,7 +88,7 @@ suite('Language Server - Activator', () => { const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); + when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(true); when(lsFolderService.getLanguageServerFolderName(anything())) .thenResolve(languageServerFolder); @@ -96,7 +96,7 @@ suite('Language Server - Activator', () => { await activator.activate(undefined); - verify(manager.start(undefined)).once(); + verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); verify(lsFolderService.getLanguageServerFolderName(anything())).once(); verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); @@ -108,7 +108,7 @@ suite('Language Server - Activator', () => { const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); + when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(true); when(lsFolderService.getLanguageServerFolderName(anything())) .thenResolve(languageServerFolder); @@ -122,11 +122,11 @@ suite('Language Server - Activator', () => { verify(lsFolderService.getLanguageServerFolderName(anything())).once(); verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); - verify(manager.start(undefined)).never(); + verify(manager.start(undefined, undefined)).never(); deferred.resolve(); await sleep(1); - verify(manager.start(undefined)).once(); + verify(manager.start(undefined, undefined)).once(); await promise; }); @@ -134,12 +134,12 @@ suite('Language Server - Activator', () => { const uri = Uri.file(__filename); when(workspaceService.hasWorkspaceFolders).thenReturn(true); when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri }]); - when(manager.start(uri)).thenResolve(); + when(manager.start(uri, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); await activator.activate(undefined); - verify(manager.start(uri)).once(); + verify(manager.start(uri, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); verify(workspaceService.workspaceFolders).once(); }); diff --git a/src/test/activation/languageServer/languageClientFactory.unit.test.ts b/src/test/activation/languageServer/languageClientFactory.unit.test.ts index 0e34a6b09104..c9406f7169d7 100644 --- a/src/test/activation/languageServer/languageClientFactory.unit.test.ts +++ b/src/test/activation/languageServer/languageClientFactory.unit.test.ts @@ -50,11 +50,11 @@ suite('Language Server - LanguageClient Factory', () => { when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); - await factory.createLanguageClient(uri, options); + await factory.createLanguageClient(uri, undefined, options); verify(configurationService.getSettings(uri)).once(); - verify(downloadFactory.createLanguageClient(uri, options, env)).once(); - verify(simpleFactory.createLanguageClient(uri, options, env)).never(); + verify(downloadFactory.createLanguageClient(uri, undefined, options, env)).once(); + verify(simpleFactory.createLanguageClient(uri, undefined, options, env)).never(); }); test('Simple factory is used when not required to download the LS', async () => { const downloadFactory = mock(DownloadedLanguageClientFactory); @@ -69,11 +69,11 @@ suite('Language Server - LanguageClient Factory', () => { when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); - await factory.createLanguageClient(uri, options); + await factory.createLanguageClient(uri, undefined, options); verify(configurationService.getSettings(uri)).once(); - verify(downloadFactory.createLanguageClient(uri, options, env)).never(); - verify(simpleFactory.createLanguageClient(uri, options, env)).once(); + verify(downloadFactory.createLanguageClient(uri, undefined, options, env)).never(); + verify(simpleFactory.createLanguageClient(uri, undefined, options, env)).once(); }); test('Download factory will make use of the language server folder name and client will be created', async () => { const platformData = mock(PlatformData); @@ -104,7 +104,7 @@ suite('Language Server - LanguageClient Factory', () => { } rewiremock('vscode-languageclient').with({ LanguageClient: MockClass }); - const client = await factory.createLanguageClient(uri, options, { FOO: 'bar' }); + const client = await factory.createLanguageClient(uri, undefined, options, { FOO: 'bar' }); verify(lsFolderService.getLanguageServerFolderName(anything())).once(); verify(platformData.engineExecutableName).atLeast(1); @@ -140,7 +140,7 @@ suite('Language Server - LanguageClient Factory', () => { } rewiremock('vscode-languageclient').with({ LanguageClient: MockClass }); - const client = await factory.createLanguageClient(uri, options, { FOO: 'bar' }); + const client = await factory.createLanguageClient(uri, undefined, options, { FOO: 'bar' }); verify(lsFolderService.getLanguageServerFolderName(anything())).once(); verify(platformData.engineExecutableName).never(); diff --git a/src/test/activation/languageServer/languageServer.unit.test.ts b/src/test/activation/languageServer/languageServer.unit.test.ts index df25ee555205..e5dc5f4eff12 100644 --- a/src/test/activation/languageServer/languageServer.unit.test.ts +++ b/src/test/activation/languageServer/languageServer.unit.test.ts @@ -9,7 +9,7 @@ import * as typemoq from 'typemoq'; import { Uri } from 'vscode'; import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { BaseLanguageClientFactory } from '../../../client/activation/languageServer/languageClientFactory'; -import { LanguageServer } from '../../../client/activation/languageServer/languageServer'; +import { LanguageServerProxy } from '../../../client/activation/languageServer/languageServerProxy'; import { ILanguageClientFactory } from '../../../client/activation/types'; import { ICommandManager } from '../../../client/common/application/types'; import '../../../client/common/extensions'; @@ -21,7 +21,7 @@ import { ITestManagementService } from '../../../client/testing/types'; //tslint:disable:no-require-imports no-require-imports no-var-requires no-any no-unnecessary-class max-func-body-length suite('Language Server - LanguageServer', () => { - class LanguageServerTest extends LanguageServer { + class LanguageServerTest extends LanguageServerProxy { // tslint:disable-next-line:no-unnecessary-override public async registerTestServices() { return super.registerTestServices(); @@ -76,7 +76,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => onTelemetryDisposable.object); client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); const startDisposable = typemoq.Mock.ofType(); client.setup(c => c.stop()).returns(() => Promise.resolve()); client @@ -96,7 +96,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => false as any) .verifiable(typemoq.Times.once()); - server.start(uri, options).ignoreErrors(); + server.start(uri, undefined, options).ignoreErrors(); // Even though server has started request should not yet be sent out. // Not until language client has initialized. @@ -132,7 +132,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => onTelemetryDisposable.object); client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); const startDisposable = typemoq.Mock.ofType(); client.setup(c => c.stop()).returns(() => Promise.resolve()); client @@ -152,7 +152,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => false as any) .verifiable(typemoq.Times.once()); - const promise = server.start(uri, options); + const promise = server.start(uri, undefined, options); // Even though server has started request should not yet be sent out. // Not until language client has initialized. @@ -203,7 +203,7 @@ suite('Language Server - LanguageServer', () => { .verifiable(typemoq.Times.once()); client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); const startDisposable = typemoq.Mock.ofType(); client.setup(c => c.stop()).returns(() => Promise.resolve()); client @@ -211,7 +211,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => startDisposable.object) .verifiable(typemoq.Times.once()); - server.start(uri, options).ignoreErrors(); + server.start(uri, undefined, options).ignoreErrors(); // Initialize language client and verify that the request was sent out. client @@ -249,7 +249,7 @@ suite('Language Server - LanguageServer', () => { .verifiable(typemoq.Times.once()); client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); const startDisposable = typemoq.Mock.ofType(); client.setup(c => c.stop()).returns(() => Promise.resolve()); client @@ -257,7 +257,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => startDisposable.object) .verifiable(typemoq.Times.once()); - server.start(uri, options).ignoreErrors(); + server.start(uri, undefined, options).ignoreErrors(); // Initialize language client and verify that the request was sent out. client @@ -291,7 +291,7 @@ suite('Language Server - LanguageServer', () => { .setup(c => c.initializeResult) .returns(() => undefined) .verifiable(typemoq.Times.atLeastOnce()); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); const startDisposable = typemoq.Mock.ofType(); client.setup(c => c.stop()).returns(() => Promise.resolve()); client @@ -299,7 +299,7 @@ suite('Language Server - LanguageServer', () => { .returns(() => startDisposable.object) .verifiable(typemoq.Times.once()); - const promise = server.start(uri, options); + const promise = server.start(uri, undefined, options); // Wait until we start ls client and check if it is ready. await sleep(200); // Confirm we checked if it is ready. diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index c1fdf216b75c..b02726af1f80 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -1,18 +1,20 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { instance, mock, verify, when } from 'ts-mockito'; import { Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; + import { LanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; -import { LanguageServer } from '../../../client/activation/languageServer/languageServer'; import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; +import { LanguageServerProxy } from '../../../client/activation/languageServer/languageServerProxy'; import { LanguageServerManager } from '../../../client/activation/languageServer/manager'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension } from '../../../client/activation/types'; +import { + ILanguageServerAnalysisOptions, + ILanguageServerExtension, + ILanguageServerProxy +} from '../../../client/activation/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; import { sleep } from '../../core'; @@ -25,14 +27,14 @@ suite('Language Server - Manager', () => { let manager: LanguageServerManager; let serviceContainer: IServiceContainer; let analysisOptions: ILanguageServerAnalysisOptions; - let languageServer: ILanguageServer; + let languageServer: ILanguageServerProxy; let lsExtension: ILanguageServerExtension; let onChangeAnalysisHandler: Function; const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; setup(() => { serviceContainer = mock(ServiceContainer); analysisOptions = mock(LanguageServerAnalysisOptions); - languageServer = mock(LanguageServer); + languageServer = mock(LanguageServerProxy); lsExtension = mock(LanguageServerExtension); manager = new LanguageServerManager( instance(serviceContainer), @@ -57,15 +59,15 @@ suite('Language Server - Manager', () => { when(analysisOptions.initialize(resource)).thenResolve(); when(analysisOptions.getAnalysisOptions()).thenResolve(languageClientOptions); when(analysisOptions.onDidChange).thenReturn(analysisChangeFn as any); - when(serviceContainer.get(ILanguageServer)).thenReturn(instance(languageServer)); - when(languageServer.start(resource, languageClientOptions)).thenResolve(); + when(serviceContainer.get(ILanguageServerProxy)).thenReturn(instance(languageServer)); + when(languageServer.start(resource, undefined, languageClientOptions)).thenResolve(); - await manager.start(resource); + await manager.start(resource, undefined); verify(analysisOptions.initialize(resource)).once(); verify(analysisOptions.getAnalysisOptions()).once(); - verify(serviceContainer.get(ILanguageServer)).once(); - verify(languageServer.start(resource, languageClientOptions)).once(); + verify(serviceContainer.get(ILanguageServerProxy)).once(); + verify(languageServer.start(resource, undefined, languageClientOptions)).once(); expect(invoked).to.be.true; expect(analysisHandlerRegistered).to.be.true; verify(languageServer.dispose()).never(); @@ -80,7 +82,7 @@ suite('Language Server - Manager', () => { test('Attempting to start LS will throw an exception', async () => { await startLanguageServer(); - await expect(manager.start(resource)).to.eventually.be.rejectedWith('Language Server already started'); + await expect(manager.start(resource, undefined)).to.eventually.be.rejectedWith('Language Server already started'); }); test('Changes in analysis options must restart LS', async () => { await startLanguageServer(); @@ -91,8 +93,8 @@ suite('Language Server - Manager', () => { verify(languageServer.dispose()).once(); verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); + verify(serviceContainer.get(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); }); test('Changes in analysis options must throttled when restarting LS', async () => { await startLanguageServer(); @@ -112,8 +114,8 @@ suite('Language Server - Manager', () => { verify(languageServer.dispose()).once(); verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); + verify(serviceContainer.get(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); }); test('Multiple changes in analysis options must restart LS twice', async () => { await startLanguageServer(); @@ -133,8 +135,8 @@ suite('Language Server - Manager', () => { verify(languageServer.dispose()).once(); verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); + verify(serviceContainer.get(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); await onChangeAnalysisHandler.call(manager); await onChangeAnalysisHandler.call(manager); @@ -151,8 +153,8 @@ suite('Language Server - Manager', () => { verify(languageServer.dispose()).twice(); verify(analysisOptions.getAnalysisOptions()).thrice(); - verify(serviceContainer.get(ILanguageServer)).thrice(); - verify(languageServer.start(resource, languageClientOptions)).thrice(); + verify(serviceContainer.get(ILanguageServerProxy)).thrice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).thrice(); }); test('Must load extension when command was been sent before starting LS', async () => { const args = { x: 1 }; diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index ae1c95c2e733..52edc19bf0c5 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -1,8 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { instance, mock, verify } from 'ts-mockito'; import { AATesting } from '../../client/activation/aaTesting'; @@ -12,11 +9,19 @@ import { ExtensionSurveyPrompt } from '../../client/activation/extensionSurvey'; import { JediExtensionActivator } from '../../client/activation/jedi'; import { LanguageServerExtensionActivator } from '../../client/activation/languageServer/activator'; import { LanguageServerAnalysisOptions } from '../../client/activation/languageServer/analysisOptions'; -import { DownloadBetaChannelRule, DownloadDailyChannelRule } from '../../client/activation/languageServer/downloadChannelRules'; +import { + DownloadBetaChannelRule, + DownloadDailyChannelRule +} from '../../client/activation/languageServer/downloadChannelRules'; import { LanguageServerDownloader } from '../../client/activation/languageServer/downloader'; -import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from '../../client/activation/languageServer/languageClientFactory'; -import { LanguageServer } from '../../client/activation/languageServer/languageServer'; -import { LanguageServerCompatibilityService } from '../../client/activation/languageServer/languageServerCompatibilityService'; +import { + BaseLanguageClientFactory, + DownloadedLanguageClientFactory, + SimpleLanguageClientFactory +} from '../../client/activation/languageServer/languageClientFactory'; +import { + LanguageServerCompatibilityService +} from '../../client/activation/languageServer/languageServerCompatibilityService'; import { LanguageServerExtension } from '../../client/activation/languageServer/languageServerExtension'; import { LanguageServerFolderService } from '../../client/activation/languageServer/languageServerFolderService'; import { @@ -26,6 +31,7 @@ import { StableLanguageServerPackageRepository } from '../../client/activation/languageServer/languageServerPackageRepository'; import { LanguageServerPackageService } from '../../client/activation/languageServer/languageServerPackageService'; +import { LanguageServerProxy } from '../../client/activation/languageServer/languageServerProxy'; import { LanguageServerManager } from '../../client/activation/languageServer/manager'; import { LanguageServerOutputChannel } from '../../client/activation/languageServer/outputChannel'; import { PlatformData } from '../../client/activation/languageServer/platformData'; @@ -36,7 +42,6 @@ import { IExtensionActivationService, IExtensionSingleActivationService, ILanguageClientFactory, - ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, @@ -46,12 +51,19 @@ import { ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerPackageService, + ILanguageServerProxy, IPlatformData, LanguageClientFactory, LanguageServerActivator } from '../../client/activation/types'; import { INugetRepository } from '../../client/common/nuget/types'; -import { BANNER_NAME_DS_SURVEY, BANNER_NAME_INTERACTIVE_SHIFTENTER, BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../../client/common/types'; +import { + BANNER_NAME_DS_SURVEY, + BANNER_NAME_INTERACTIVE_SHIFTENTER, + BANNER_NAME_LS_SURVEY, + BANNER_NAME_PROPOSE_LS, + IPythonExtensionBanner +} from '../../client/common/types'; import { DataScienceSurveyBanner } from '../../client/datascience/dataScienceSurveyBanner'; import { InteractiveShiftEnterBanner } from '../../client/datascience/shiftEnterBanner'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -93,7 +105,7 @@ suite('Unit Tests - Activation Service Registry', () => { verify(serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader)).once(); verify(serviceManager.addSingleton(IPlatformData, PlatformData)).once(); verify(serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions)).once(); - verify(serviceManager.addSingleton(ILanguageServer, LanguageServer)).once(); + verify(serviceManager.addSingleton(ILanguageServerProxy, LanguageServerProxy)).once(); verify(serviceManager.add(ILanguageServerManager, LanguageServerManager)).once(); verify(serviceManager.addSingleton(IExtensionSingleActivationService, AATesting)).once(); verify(serviceManager.addSingleton(ILanguageServerOutputChannel, LanguageServerOutputChannel)).once(); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index fd32cfb78932..73e2aadcb065 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -22,7 +22,7 @@ import { } from 'vscode'; import * as vsls from 'vsls/vscode'; -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; +import { ILanguageServerAnalysisOptions, ILanguageServerProxy } from '../../client/activation/types'; import { TerminalManager } from '../../client/common/application/terminalManager'; import { IApplicationShell, @@ -110,9 +110,7 @@ import { CodeWatcher } from '../../client/datascience/editor-integration/codewat import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; import { GatherExecution } from '../../client/datascience/gather/gather'; import { GatherListener } from '../../client/datascience/gather/gatherListener'; -import { - DotNetIntellisenseProvider -} from '../../client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider'; +import { IntellisenseProvider } from '../../client/datascience/interactive-common/intellisense/intellisenseProvider'; import { AutoSaveService } from '../../client/datascience/interactive-ipynb/autoSaveService'; import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; import { NativeEditorCommandListener } from '../../client/datascience/interactive-ipynb/nativeEditorCommandListener'; @@ -253,8 +251,8 @@ import { MockDocumentManager } from './mockDocumentManager'; import { MockExtensions } from './mockExtensions'; import { MockJupyterManager, SupportedCommands } from './mockJupyterManager'; import { MockJupyterManagerFactory } from './mockJupyterManagerFactory'; -import { MockLanguageServer } from './mockLanguageServer'; import { MockLanguageServerAnalysisOptions } from './mockLanguageServerAnalysisOptions'; +import { MockLanguageServerProxy } from './mockLanguageServerProxy'; import { MockLiveShareApi } from './mockLiveShare'; import { MockWorkspaceConfiguration } from './mockWorkspaceConfig'; import { blurWindow, createMessageEvent } from './reactHelpers'; @@ -398,9 +396,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv); this.serviceManager.addSingleton(ITerminalManager, TerminalManager); this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); - this.serviceManager.addSingleton(ILanguageServer, MockLanguageServer); + this.serviceManager.addSingleton(ILanguageServerProxy, MockLanguageServerProxy); this.serviceManager.addSingleton(ILanguageServerAnalysisOptions, MockLanguageServerAnalysisOptions); - this.serviceManager.add(IInteractiveWindowListener, DotNetIntellisenseProvider); + this.serviceManager.add(IInteractiveWindowListener, IntellisenseProvider); this.serviceManager.add(IInteractiveWindowListener, AutoSaveService); this.serviceManager.add(IProtocolParser, ProtocolParser); this.serviceManager.addSingleton(IDebugService, MockDebuggerService); diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index 30bfab84e2bf..9f126de5a18f 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -123,6 +123,10 @@ class MockJupyterNotebook implements INotebook { public async dispose(): Promise { return Promise.resolve(); } + + public getMatchingInterpreter(): Promise { + return Promise.resolve(undefined); + } } // tslint:disable:no-any no-http-string no-multiline-string max-func-body-length @@ -538,7 +542,7 @@ suite('Jupyter Execution', async () => { function createExecution(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean): JupyterExecutionFactory { return createExecutionAndReturnProcessService(activeInterpreter, notebookStdErr, skipSearch).jupyterExecutionFactory; } - function createExecutionAndReturnProcessService(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean, runInDocker?: boolean): {workingPythonExecutionService: TypeMoq.IMock; jupyterExecutionFactory: JupyterExecutionFactory} { + function createExecutionAndReturnProcessService(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean, runInDocker?: boolean): { workingPythonExecutionService: TypeMoq.IMock; jupyterExecutionFactory: JupyterExecutionFactory } { // Setup defaults when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); when(interpreterService.getActiveInterpreter()).thenResolve(activeInterpreter); @@ -547,7 +551,7 @@ suite('Jupyter Execution', async () => { when(interpreterService.getInterpreterDetails(match('/foo/baz/python.exe'))).thenResolve(missingKernelPython); when(interpreterService.getInterpreterDetails(match('/bar/baz/python.exe'))).thenResolve(missingNotebookPython); when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter'); - if (runInDocker){ + if (runInDocker) { when(fileSystem.readFile('/proc/self/cgroup')).thenResolve('hello docker world'); } // Create our working python and process service. @@ -692,7 +696,7 @@ suite('Jupyter Execution', async () => { } test('Working notebook and commands found', async () => { - const { workingPythonExecutionService, jupyterExecutionFactory} = createExecutionAndReturnProcessService(workingPython); + const { workingPythonExecutionService, jupyterExecutionFactory } = createExecutionAndReturnProcessService(workingPython); when(executionFactory.createDaemon(deepEqual({ daemonModule: PythonDaemonModule, pythonPath: workingPython.path }))).thenResolve(workingPythonExecutionService.object); await assert.eventually.equal(jupyterExecutionFactory.isNotebookSupported(), true, 'Notebook not supported'); @@ -705,7 +709,7 @@ suite('Jupyter Execution', async () => { }).timeout(10000); test('Includes correct args for running in docker', async () => { - const { workingPythonExecutionService, jupyterExecutionFactory} = createExecutionAndReturnProcessService(workingPython, undefined, undefined, true); + const { workingPythonExecutionService, jupyterExecutionFactory } = createExecutionAndReturnProcessService(workingPython, undefined, undefined, true); when(executionFactory.createDaemon(deepEqual({ daemonModule: PythonDaemonModule, pythonPath: workingPython.path }))).thenResolve(workingPythonExecutionService.object); await assert.eventually.equal(jupyterExecutionFactory.isNotebookSupported(), true, 'Notebook not supported'); @@ -732,7 +736,7 @@ suite('Jupyter Execution', async () => { test('Slow notebook startups throws exception', async () => { const daemonService = mock(PythonDaemonExecutionService); const stdErr = 'Failure'; - const proc = {on: noop} as any as ChildProcess; + const proc = { on: noop } as any as ChildProcess; const out = new Observable>(s => s.next({ source: 'stderr', out: stdErr })); when(daemonService.execModuleObservable(anything(), anything(), anything())).thenReturn({ dispose: noop, proc: proc, out }); when(executionFactory.createDaemon(deepEqual({ daemonModule: PythonDaemonModule, pythonPath: workingPython.path }))).thenResolve(instance(daemonService)); @@ -856,7 +860,7 @@ suite('Jupyter Execution', async () => { }).timeout(20_000); test('Kernelspec is deleted on exit', async () => { - const { workingPythonExecutionService, jupyterExecutionFactory} = createExecutionAndReturnProcessService(missingKernelPython); + const { workingPythonExecutionService, jupyterExecutionFactory } = createExecutionAndReturnProcessService(missingKernelPython); when(interpreterService.getActiveInterpreter(undefined)).thenResolve(missingKernelPython); when(executionFactory.createDaemon(deepEqual({ daemonModule: PythonDaemonModule, pythonPath: missingKernelPython.path }))).thenResolve(workingPythonExecutionService.object); diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts index 72d332bf440e..dc65e586ed14 100644 --- a/src/test/datascience/intellisense.unit.test.ts +++ b/src/test/datascience/intellisense.unit.test.ts @@ -5,15 +5,12 @@ import { expect } from 'chai'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; import * as TypeMoq from 'typemoq'; -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; import { IWorkspaceService } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService } from '../../client/common/types'; import { Identifiers } from '../../client/datascience/constants'; -import { - DotNetIntellisenseProvider -} from '../../client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider'; +import { IntellisenseProvider } from '../../client/datascience/interactive-common/intellisense/intellisenseProvider'; import { IInteractiveWindowMapping, InteractiveWindowMessages @@ -24,9 +21,10 @@ import { IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { createEmptyCell, generateTestCells } from '../../datascience-ui/interactive-common/mainState'; import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockLanguageClient } from './mockLanguageClient'; +import { MockLanguageServerCache } from './mockLanguageServerCache'; // tslint:disable:no-any unified-signatures const TestCellContents = `myvar = """ # Lorem Ipsum @@ -46,8 +44,8 @@ df // tslint:disable-next-line: max-func-body-length suite('DataScience Intellisense Unit Tests', () => { let intellisenseProvider: IInteractiveWindowListener; - let languageServer: TypeMoq.IMock; - let analysisOptions: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let languageServerCache: MockLanguageServerCache; let workspaceService: TypeMoq.IMock; let configService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; @@ -59,12 +57,9 @@ suite('DataScience Intellisense Unit Tests', () => { } }(undefined, new MockAutoSelectionService()); - const languageClient = new MockLanguageClient( - 'mockLanguageClient', { module: 'dummy' }, {}); - setup(() => { - languageServer = TypeMoq.Mock.ofType(); - analysisOptions = TypeMoq.Mock.ofType(); + languageServerCache = new MockLanguageServerCache(); + interpreterService = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); configService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); @@ -72,25 +67,21 @@ suite('DataScience Intellisense Unit Tests', () => { interactiveWindowProvider = TypeMoq.Mock.ofType(); pythonSettings.jediEnabled = false; - languageServer.setup(l => l.start(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); - analysisOptions.setup(a => a.getAnalysisOptions()).returns(() => Promise.resolve({})); - languageServer.setup(l => l.languageClient).returns(() => languageClient); configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings); workspaceService.setup(w => w.rootPath).returns(() => '/foo/bar'); - intellisenseProvider = new DotNetIntellisenseProvider( - languageServer.object, - analysisOptions.object, + intellisenseProvider = new IntellisenseProvider( workspaceService.object, - configService.object, fileSystem.object, jupyterExecution.object, - interactiveWindowProvider.object + interactiveWindowProvider.object, + interpreterService.object, + languageServerCache ); }); function sendMessage(type: T, payload?: M[T]): Promise { - const result = languageClient.waitForNotification(); + const result = languageServerCache.getMockServer().waitForNotification(); intellisenseProvider.onMessage(type.toString(), payload); return result; } @@ -171,170 +162,174 @@ suite('DataScience Intellisense Unit Tests', () => { return sendMessage(InteractiveWindowMessages.LoadAllCellsComplete, { cells }); } + function getDocumentContents(): string { + return languageServerCache.getMockServer().getDocumentContents(); + } + test('Add a single cell', async () => { await addCell('import sys\n\n', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n\n\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n\n\n', 'Document not set'); }); test('Add two cells', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\n', 'Document not set after double'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\n', 'Document not set after double'); }); test('Add a cell and edit', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('i', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); await addCode('m', 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nim', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nim', 'Document not set after edit'); await addCode('\n', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nim\n', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nim\n', 'Document not set after edit'); }); test('Add a cell and remove', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('i', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); await removeCode(1, 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); await addCode('\n', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n\n', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\n\n', 'Document not set after edit'); }); test('Remove a section in the middle', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('import os', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport os', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nimport os', 'Document not set after edit'); await removeCode(1, 4, 7, 4); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimp os', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nimp os', 'Document not set after edit'); }); test('Remove a bunch in a row', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('p', 1, 1, 0); await addCode('r', 1, 2, 1); await addCode('i', 1, 3, 2); await addCode('n', 1, 4, 3); await addCode('t', 1, 5, 4); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nprint', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nprint', 'Document not set after edit'); await removeCode(1, 5, 6, 1); await removeCode(1, 4, 5, 1); await removeCode(1, 3, 4, 1); await removeCode(1, 2, 3, 1); await removeCode(1, 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); }); test('Remove from a line', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('s', 1, 1, 0); await addCode('y', 1, 2, 1); await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); await addCode('\n', 1, 4, 3); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys\n', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys\n', 'Document not set after edit'); await addCode('s', 2, 1, 3); await addCode('y', 2, 2, 4); await addCode('s', 2, 3, 5); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys\nsys', 'Document not set after edit'); await removeCode(1, 3, 4, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsy\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsy\nsys', 'Document not set after edit'); }); test('Add cell after adding code', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('s', 1, 1, 0); await addCode('y', 1, 2, 1); await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a second cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a second cell broken'); }); test('Collapse expand cell', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await updateCell('import sys\nsys.version_info', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Readding a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Readding a cell broken'); await updateCell('import sys', 'import sys\nsys.version_info', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Collapsing a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Collapsing a cell broken'); await updateCell('import sys', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Updating a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Updating a cell broken'); }); test('Collapse expand cell after adding code', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('s', 1, 1, 0); await addCode('y', 1, 2, 1); await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); await updateCell('import sys\nsys.version_info', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Readding a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Readding a cell broken'); await updateCell('import sys', 'import sys\nsys.version_info', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Collapsing a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Collapsing a cell broken'); await updateCell('import sys', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Updating a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Updating a cell broken'); }); test('Add a cell and remove it', async () => { await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); await addCode('s', 1, 1, 0); await addCode('y', 1, 2, 1); await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); await removeCell('1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Removing a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Removing a cell broken'); await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a cell broken'); await addCell('import bar', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Adding a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Adding a cell broken'); await removeCell('1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Removing a cell broken'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Removing a cell broken'); }); test('Add a bunch of cells and remove them', async () => { await addCode('s', 1, 1, 0); await addCode('y', 1, 2, 1); await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('sys', 'Document not set after edit'); + expect(getDocumentContents()).to.be.eq('sys', 'Document not set after edit'); await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set'); await addCell('import foo', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nsys', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nsys', 'Document not set'); await addCell('import bar', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Document not set'); await removeAllCells(); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Removing all cells broken'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Removing all cells broken'); await addCell('import baz', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nimport baz\nsys', 'Document not set'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nimport baz\nsys', 'Document not set'); }); test('Load remove and insert', async () => { const cells = generateTestCells('foo.py', 1); await loadAllCells(cells); - expect(languageClient.getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); await removeAllCells(); - expect(languageClient.getDocumentContents()).to.be.eq('', 'Remove all cells is failing'); + expect(getDocumentContents()).to.be.eq('', 'Remove all cells is failing'); await insertCell('6', 'foo'); - expect(languageClient.getDocumentContents()).to.be.eq('foo\n', 'Insert after remove'); + expect(getDocumentContents()).to.be.eq('foo\n', 'Insert after remove'); await insertCell('7', 'bar', '6'); - expect(languageClient.getDocumentContents()).to.be.eq('foo\nbar\n', 'Double insert after remove'); + expect(getDocumentContents()).to.be.eq('foo\nbar\n', 'Double insert after remove'); }); test('Swap cells around', async () => { const cells = generateTestCells('foo.py', 1); await loadAllCells(cells); await swapCells('0', '1'); // 2nd cell is markdown - expect(languageClient.getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells should skip swapping on markdown'); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells should skip swapping on markdown'); await swapCells('0', '2'); const afterSwap = `df myvar = """ # Lorem Ipsum @@ -349,15 +344,15 @@ Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. """ df `; - expect(languageClient.getDocumentContents()).to.be.eq(afterSwap, 'Swap cells failed'); + expect(getDocumentContents()).to.be.eq(afterSwap, 'Swap cells failed'); await swapCells('0', '2'); - expect(languageClient.getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells back failed'); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells back failed'); }); test('Insert and swap', async () => { const cells = generateTestCells('foo.py', 1); await loadAllCells(cells); - expect(languageClient.getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); await insertCell('6', 'foo'); const afterInsert = `foo myvar = """ # Lorem Ipsum @@ -373,7 +368,7 @@ Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. df df `; - expect(languageClient.getDocumentContents()).to.be.eq(afterInsert, 'Insert cell failed'); + expect(getDocumentContents()).to.be.eq(afterInsert, 'Insert cell failed'); await insertCell('7', 'foo', '0'); const afterInsert2 = `foo myvar = """ # Lorem Ipsum @@ -390,9 +385,9 @@ foo df df `; - expect(languageClient.getDocumentContents()).to.be.eq(afterInsert2, 'Insert2 cell failed'); + expect(getDocumentContents()).to.be.eq(afterInsert2, 'Insert2 cell failed'); await removeCell('7'); - expect(languageClient.getDocumentContents()).to.be.eq(afterInsert, 'Remove 2 cell failed'); + expect(getDocumentContents()).to.be.eq(afterInsert, 'Remove 2 cell failed'); await swapCells('0', '2'); const afterSwap = `foo df @@ -408,7 +403,7 @@ Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. """ df `; - expect(languageClient.getDocumentContents()).to.be.eq(afterSwap, 'Swap cell failed'); + expect(getDocumentContents()).to.be.eq(afterSwap, 'Swap cell failed'); }); }); diff --git a/src/test/datascience/mockLanguageClient.ts b/src/test/datascience/mockLanguageClient.ts index b417d347765f..efc4fdbd6fc5 100644 --- a/src/test/datascience/mockLanguageClient.ts +++ b/src/test/datascience/mockLanguageClient.ts @@ -44,8 +44,8 @@ import { MockProtocolConverter } from './mockProtocolConverter'; // tslint:disable:no-any unified-signatures export class MockLanguageClient extends LanguageClient { - private notificationPromise : Deferred | undefined; - private contents : string; + private notificationPromise: Deferred | undefined; + private contents: string; private versionId: number | null; private converter: MockProtocolConverter; @@ -56,17 +56,17 @@ export class MockLanguageClient extends LanguageClient { this.versionId = 0; this.converter = new MockProtocolConverter(); } - public waitForNotification() : Promise { + public waitForNotification(): Promise { this.notificationPromise = createDeferred(); return this.notificationPromise.promise; } // Returns the current contents of the document being built by the completion provider calls - public getDocumentContents() : string { + public getDocumentContents(): string { return this.contents; } - public getVersionId() : number | null { + public getVersionId(): number | null { return this.versionId; } @@ -83,7 +83,7 @@ export class MockLanguageClient extends LanguageClient { public sendRequest(type: RequestType, params: P, token?: CancellationToken | undefined): Thenable; public sendRequest(method: string, token?: CancellationToken | undefined): Thenable; public sendRequest(method: string, param: any, token?: CancellationToken | undefined): Thenable; - public sendRequest(_method: any, _param?: any, _token?: any) : Thenable { + public sendRequest(_method: any, _param?: any, _token?: any): Thenable { switch (_method.method) { case 'textDocument/completion': // Just return one for each line of our contents @@ -209,14 +209,14 @@ export class MockLanguageClient extends LanguageClient { } private applyChanges(changes: TextDocumentContentChangeEvent[]) { - changes.forEach(c => { + changes.forEach((c: TextDocumentContentChangeEvent) => { const before = this.contents.substr(0, c.rangeOffset); const after = this.contents.substr(c.rangeOffset + c.rangeLength); this.contents = `${before}${c.text}${after}`; }); } - private getDocumentCompletions() : CompletionItem[] { + private getDocumentCompletions(): CompletionItem[] { const lines = this.contents.splitLines(); return lines.map(l => { return { diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts index f9daf07ed2ff..40711b8f7c4f 100644 --- a/src/test/datascience/mockLanguageServer.ts +++ b/src/test/datascience/mockLanguageServer.ts @@ -1,36 +1,117 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + TextDocumentContentChangeEvent, + WorkspaceEdit +} from 'vscode'; import { ILanguageServer } from '../../client/activation/types'; -import { MockLanguageClient } from './mockLanguageClient'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; // tslint:disable:no-any unified-signatures -@injectable() export class MockLanguageServer implements ILanguageServer { - private mockLanguageClient: MockLanguageClient | undefined; + private notificationPromise: Deferred | undefined; + private contents = ''; + private versionId: number = 0; - public get languageClient(): LanguageClient | undefined { - if (!this.mockLanguageClient) { - this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); - } - return this.mockLanguageClient; + public waitForNotification(): Promise { + this.notificationPromise = createDeferred(); + return this.notificationPromise.promise; + } + + public getDocumentContents(): string { + return this.contents; + } + + public getVersionId(): number | null { + return this.versionId; + } + + public handleChanges(_document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + this.applyChanges(changes); + } + + public handleOpen(document: TextDocument) { + this.contents = document.getText(); + this.versionId = document.version; } - public start(_resource: Uri | undefined, _options: LanguageClientOptions): Promise { - if (!this.mockLanguageClient) { - this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); + public provideRenameEdits(_document: TextDocument, _position: Position, _newName: string, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); } - return Promise.resolve(); + return null; } - public loadExtension(_args?: {} | undefined): void { - throw new Error('Method not implemented.'); + public provideDefinition(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; } - public dispose(): void | undefined { - this.mockLanguageClient = undefined; + public provideHover(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public provideReferences(_document: TextDocument, _position: Position, _context: ReferenceContext, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public provideCompletionItems(_document: TextDocument, _position: Position, _token: CancellationToken, _context: CompletionContext): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public provideDocumentSymbols(_document: TextDocument, _token: CancellationToken): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public provideSignatureHelp(_document: TextDocument, _position: Position, _token: CancellationToken, _context: SignatureHelpContext): ProviderResult { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + } + return null; + } + public dispose(): void { + noop(); } + private applyChanges(changes: TextDocumentContentChangeEvent[]) { + changes.forEach(c => { + const before = this.contents.substr(0, c.rangeOffset); + const after = this.contents.substr(c.rangeOffset + c.rangeLength); + this.contents = `${before}${c.text}${after}`; + }); + this.versionId = this.versionId + 1; + } } diff --git a/src/test/datascience/mockLanguageServerCache.ts b/src/test/datascience/mockLanguageServerCache.ts new file mode 100644 index 000000000000..8e83b6608497 --- /dev/null +++ b/src/test/datascience/mockLanguageServerCache.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Uri } from 'vscode'; + +import { ILanguageServer, ILanguageServerCache } from '../../client/activation/types'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; +import { MockLanguageServer } from './mockLanguageServer'; + +// tslint:disable:no-any unified-signatures +@injectable() +export class MockLanguageServerCache implements ILanguageServerCache { + private mockLanguageServer = new MockLanguageServer(); + + public get(_resource: Uri | undefined, _interpreter?: PythonInterpreter | undefined): Promise { + return Promise.resolve(this.mockLanguageServer); + } + + public getMockServer(): MockLanguageServer { + return this.mockLanguageServer; + } +} diff --git a/src/test/datascience/mockLanguageServerProxy.ts b/src/test/datascience/mockLanguageServerProxy.ts new file mode 100644 index 000000000000..4a4c8a9bab42 --- /dev/null +++ b/src/test/datascience/mockLanguageServerProxy.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; + +import { ILanguageServerProxy } from '../../client/activation/types'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; +import { MockLanguageClient } from './mockLanguageClient'; + +// tslint:disable:no-any unified-signatures +@injectable() +export class MockLanguageServerProxy implements ILanguageServerProxy { + private mockLanguageClient: MockLanguageClient | undefined; + + public get languageClient(): LanguageClient | undefined { + if (!this.mockLanguageClient) { + this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); + } + return this.mockLanguageClient; + } + + public start(_resource: Uri | undefined, _interpreter: PythonInterpreter | undefined, _options: LanguageClientOptions): Promise { + if (!this.mockLanguageClient) { + this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); + } + return Promise.resolve(); + } + public loadExtension(_args?: {} | undefined): void { + throw new Error('Method not implemented.'); + } + public dispose(): void | undefined { + this.mockLanguageClient = undefined; + } + +} From 6585f6d4364e3aef27907e891b085e914c478067 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 09:29:19 -0800 Subject: [PATCH 02/26] Add ref counting for language server --- src/client/activation/activationService.ts | 79 +++++++++++++------ .../languageServer/analysisOptions.ts | 14 ++-- .../activation/languageServer/manager.ts | 2 +- .../activation/refCountedLanguageServer.ts | 75 ++++++++++++++++++ src/client/activation/serviceRegistry.ts | 4 +- src/client/activation/types.ts | 7 +- .../intellisense/intellisenseProvider.ts | 24 +++++- .../jupyter/jupyterServerFactory.ts | 6 +- .../jupyter/liveshare/hostJupyterNotebook.ts | 4 +- .../jupyter/liveshare/hostJupyterServer.ts | 2 +- src/client/ioc/serviceManager.ts | 4 +- src/client/ioc/types.ts | 2 +- .../analysisOptions.unit.test.ts | 6 +- .../languageServer/manager.unit.test.ts | 4 +- 14 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 src/client/activation/refCountedLanguageServer.ts diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 0d65e06a1276..f73b6287931a 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -25,6 +25,7 @@ import { IInterpreterService, PythonInterpreter } from '../interpreter/contracts import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { IExtensionActivationService, ILanguageServer, @@ -35,19 +36,19 @@ import { const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; const workspacePathNameForGlobalWorkspaces = ''; -type ActivatorInfo = { jedi: boolean; server: ILanguageServerActivator }; @injectable() export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable { - private lsActivatedServers = new Map>(); - private jediServer: ILanguageServerActivator | undefined; - private currentActivator?: ActivatorInfo; + private cache = new Map>(); + private jediServer: RefCountedLanguageServer | undefined; + private activatedServer?: ILanguageServer; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; private readonly appShell: IApplicationShell; private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; private readonly interpreterService: IInterpreterService; private resource!: Resource; + private interpreter: PythonInterpreter | undefined; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @@ -64,28 +65,42 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv disposables.push(this); disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); + disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); } public async activate(resource: Resource): Promise { + // Get a new server and dispose of the old one (might be the same one) this.resource = resource; - // Do the same thing as a get. - await this.get(resource); + this.interpreter = await this.interpreterService.getActiveInterpreter(resource); + const result = await this.get(resource, this.interpreter); + if (this.activatedServer) { + this.activatedServer.dispose(); + } + this.activatedServer = result; } public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { // See if we already have it or not const key = await this.getKey(resource, interpreter); - let result: Promise | undefined = this.lsActivatedServers.get(key); + let result: Promise | undefined = this.cache.get(key); if (!result) { - result = this.createServer(resource, interpreter); - this.lsActivatedServers.set(key, result); + // Create a special ref counted result so we don't dispose of the + // server too soon. + result = this.createRefCountedServer(resource, interpreter, key); + this.cache.set(key, result); + } else { + // Increment ref count if already exists. + result = result.then(r => { + r.increment(); + return r; + }); } return result; } public dispose() { - if (this.currentActivator) { - this.currentActivator.server.dispose(); + if (this.activatedServer) { + this.activatedServer.dispose(); } } @swallowExceptions('Send telemetry for Language Server current selection') @@ -137,17 +152,29 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv protected async onWorkspaceFoldersChanged() { //If an activated workspace folder was removed, dispose its activator const workspaceKeys = await Promise.all(this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getKey(workspaceFolder.uri))); - const activatedWkspcKeys = Array.from(this.lsActivatedServers.keys()); + const activatedWkspcKeys = Array.from(this.cache.keys()); const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { - this.lsActivatedServers.get(folder)!.then(a => a.dispose()).ignoreErrors(); - this.lsActivatedServers!.delete(folder); + this.cache.get(folder)!.then(a => a.dispose()).ignoreErrors(); } } } - private async createServer(resource: Resource, interpreter?: PythonInterpreter): Promise { + private async onDidChangeInterpreter() { + // If there's a current item in the map, dispose of it. The dispose + // should remove it from the map + const key = await this.getKey(this.resource, this.interpreter); + const item = this.cache.get(key); + if (item) { + (await item).dispose(); + } + + // Then create a new one for our current resource + await this.activate(this.resource); + } + + private async createRefCountedServer(resource: Resource, interpreter: PythonInterpreter | undefined, key: string): Promise { let jedi = this.useJedi(); if (!jedi) { const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined); @@ -163,8 +190,6 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv await this.logStartup(jedi); let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; let server = this.serviceContainer.get(ILanguageServerActivator, serverName); - this.currentActivator = { jedi, server }; - try { await server.activate(resource); } catch (ex) { @@ -175,16 +200,26 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv await this.logStartup(jedi); serverName = LanguageServerActivator.Jedi; server = this.serviceContainer.get(ILanguageServerActivator, serverName); - this.currentActivator = { jedi, server }; await server.activate(resource, interpreter); } // Jedi is always a singleton. Don't need to create it more than once. if (jedi) { - this.jediServer = server; - } + this.jediServer = new RefCountedLanguageServer(server, () => { + // When we remove the jedi server, remove it from the cache. + this.cache.delete(key); + }); + return this.jediServer; + } else { + return new RefCountedLanguageServer(server, () => { + // When we finally remove the last ref count, remove from the cache + this.cache.delete(key); - return server; + // For non jedi, actually dispose of the language server so the .net process + // shuts down + server.dispose(); + }); + } } private async logStartup(isJedi: boolean): Promise { @@ -202,7 +237,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv return; } const jedi = this.useJedi(); - if (this.currentActivator && this.currentActivator.jedi === jedi) { + if (jedi && this.jediServer) { return; } diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts index 3ba770c093af..56e2841bf788 100644 --- a/src/client/activation/languageServer/analysisOptions.ts +++ b/src/client/activation/languageServer/analysisOptions.ts @@ -39,7 +39,7 @@ import { } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IInterpreterService } from '../../interpreter/contracts'; +import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; import { ILanguageServerAnalysisOptions, ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; @injectable() @@ -50,6 +50,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt private disposables: Disposable[] = []; private languageServerFolder: string = ''; private resource: Resource; + private interpreter: PythonInterpreter | undefined; private output: IOutputChannel; private readonly didChange = new EventEmitter(); constructor(@inject(IExtensionContext) private readonly context: IExtensionContext, @@ -64,15 +65,18 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt ) { this.output = this.lsOutputChannel.channel; } - public async initialize(resource: Resource) { + public async initialize(resource: Resource, interpreter: PythonInterpreter | undefined) { this.resource = resource; + this.interpreter = interpreter; this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); let disposable = this.workspace.onDidChangeConfiguration(this.onSettingsChangedHandler, this); this.disposables.push(disposable); - disposable = this.interpreterService.onDidChangeInterpreter(() => this.didChange.fire(), this); - this.disposables.push(disposable); + if (!this.interpreter) { + disposable = this.interpreterService.onDidChangeInterpreter(() => this.didChange.fire(), this); + this.disposables.push(disposable); + } disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); this.disposables.push(disposable); @@ -88,7 +92,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt public async getAnalysisOptions(): Promise { const properties: Record = {}; - const interpreterInfo = await this.interpreterService.getActiveInterpreter(this.resource); + const interpreterInfo = this.interpreter ? this.interpreter : await this.interpreterService.getActiveInterpreter(this.resource); if (!interpreterInfo) { // tslint:disable-next-line:no-suspicious-comment // TODO: How do we handle this? It is pretty unlikely... diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index c2a48aa72718..e72622b4ea82 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -49,7 +49,7 @@ export class LanguageServerManager implements ILanguageServerManager { this.interpreter = interpreter; this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); - await this.analysisOptions.initialize(resource); + await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); } protected registerCommandHandler() { diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts new file mode 100644 index 000000000000..6c30f99c8abe --- /dev/null +++ b/src/client/activation/refCountedLanguageServer.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + TextDocumentContentChangeEvent, + WorkspaceEdit +} from 'vscode'; + +import { noop } from '../common/utils/misc'; +import { ILanguageServer } from './types'; + +export class RefCountedLanguageServer implements ILanguageServer { + private refCount = 1; + constructor(private impl: ILanguageServer, private disposeCallback: () => void) { + } + + public increment = () => { + this.refCount += 1; + } + + public dispose() { + this.refCount = Math.max(0, this.refCount - 1); + if (this.refCount === 0) { + this.disposeCallback(); + } + } + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + this.impl.handleChanges ? this.impl.handleChanges(document, changes) : noop(); + } + + public handleOpen(document: TextDocument) { + this.impl.handleOpen ? this.impl.handleOpen(document) : noop(); + } + + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { + return this.impl.provideRenameEdits(document, position, newName, token); + } + public provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + return this.impl.provideDefinition(document, position, token); + } + public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { + return this.impl.provideHover(document, position, token); + } + public provideReferences(document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult { + return this.impl.provideReferences(document, position, context, token); + } + public provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult { + return this.impl.provideCompletionItems(document, position, token, context); + } + public provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult { + return this.impl.provideCodeLenses(document, token); + } + public provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult { + return this.impl.provideDocumentSymbols(document, token); + } + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult { + return this.impl.provideSignatureHelp(document, position, token, context); + } + +} diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index c038d8c9c463..938a74ff276f 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -49,6 +49,7 @@ import { ILanguageClientFactory, ILanguageServerActivator, ILanguageServerAnalysisOptions, + ILanguageServerCache, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, @@ -63,7 +64,8 @@ import { } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IExtensionActivationService, LanguageServerExtensionActivationService); + serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); + serviceManager.addBinding(ILanguageServerCache, IExtensionActivationService); serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); serviceManager.add(ILanguageServerActivator, JediExtensionActivator, LanguageServerActivator.Jedi); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 3919b08ad3c4..6a9e99e0fd46 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -83,11 +83,12 @@ export interface ILanguageServer extends CodeLensProvider, DocumentSymbolProvider, SignatureHelpProvider, - Partial { + Partial, + IDisposable { } export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends ILanguageServer, IDisposable { +export interface ILanguageServerActivator extends ILanguageServer { activate(resource: Resource, interpreter?: PythonInterpreter): Promise; } @@ -139,7 +140,7 @@ export interface ILanguageClientFactory { export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); export interface ILanguageServerAnalysisOptions extends IDisposable { readonly onDidChange: Event; - initialize(resource: Resource): Promise; + initialize(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; getAnalysisOptions(): Promise; } export const ILanguageServerManager = Symbol('ILanguageServerManager'); diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 109d992b38c0..e13ce68cf263 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -22,9 +22,10 @@ import { IWorkspaceService } from '../../../common/application/types'; import { CancellationError } from '../../../common/cancellation'; import { traceWarning } from '../../../common/logger'; import { IFileSystem, TemporaryFile } from '../../../common/platform/types'; +import { Resource } from '../../../common/types'; import { createDeferred, Deferred, waitForPromise } from '../../../common/utils/async'; import { HiddenFileFormatString } from '../../../constants'; -import { IInterpreterService } from '../../../interpreter/contracts'; +import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { concatMultilineStringInput } from '../../common'; import { Identifiers, Settings } from '../../constants'; import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution, INotebook } from '../../types'; @@ -62,6 +63,9 @@ export class IntellisenseProvider implements IInteractiveWindowListener { private notebookIdentity: Uri | undefined; private potentialResource: Uri | undefined; private sentOpenDocument: boolean = false; + private languageServer: ILanguageServer | undefined; + private resource: Resource; + private interpreter: PythonInterpreter | undefined; constructor( @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @@ -151,8 +155,23 @@ export class IntellisenseProvider implements IInteractiveWindowListener { const activeNotebook = await this.getNotebook(); const interpreter = activeNotebook ? await activeNotebook.getMatchingInterpreter() : await this.interpreterService.getActiveInterpreter(resource); + // See if the resource or the interpreter are different + if (resource !== this.resource || interpreter !== this.interpreter || this.languageServer === undefined) { + this.resource = resource; + this.interpreter = interpreter; + + // Get an instance of the language server (so we ref count it ) + const languageServer = await this.languageServerCache.get(resource, interpreter); + + // Dispose of our old language service + this.languageServer?.dispose(); + + // Save the ref. + this.languageServer = languageServer; + } + // Use the resource and the interpreter to get our language server - return this.languageServerCache.get(resource, interpreter); + return this.languageServer; } protected getDocument(resource?: Uri): Promise { @@ -480,6 +499,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { // This is the one that acts like a reset const document = await this.getDocument(); if (document) { + this.sentOpenDocument = false; const changes = document.removeAllCells(); return this.handleChanges(document, changes); } diff --git a/src/client/datascience/jupyter/jupyterServerFactory.ts b/src/client/datascience/jupyter/jupyterServerFactory.ts index 2765688646cd..719279b52e53 100644 --- a/src/client/datascience/jupyter/jupyterServerFactory.ts +++ b/src/client/datascience/jupyter/jupyterServerFactory.ts @@ -61,7 +61,8 @@ export class JupyterServerFactory implements INotebookServer, ILiveShareHasRole @inject(IJupyterSessionManagerFactory) sessionManager: IJupyterSessionManagerFactory, @inject(IWorkspaceService) workspaceService: IWorkspaceService, @multiInject(INotebookExecutionLogger) @optional() loggers: INotebookExecutionLogger[] | undefined, - @inject(IApplicationShell) appShell: IApplicationShell) { + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IInterpreterService) interpreterService: IInterpreterService) { this.serverFactory = new RoleBasedFactory( liveShare, HostJupyterServer, @@ -74,7 +75,8 @@ export class JupyterServerFactory implements INotebookServer, ILiveShareHasRole sessionManager, workspaceService, loggers ? loggers : [], - appShell + appShell, + interpreterService ); } diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts index fd180e0260dc..82d14142e55e 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts @@ -14,6 +14,7 @@ import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../co import { traceError } from '../../../common/logger'; import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; import { createDeferred } from '../../../common/utils/async'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { Identifiers, LiveShare, LiveShareCommands } from '../../constants'; import { IExecuteInfo } from '../../interactive-common/interactiveWindowTypes'; import { @@ -30,7 +31,6 @@ import { LiveShareParticipantHost } from './liveShareParticipantMixin'; import { ResponseQueue } from './responseQueue'; import { IRoleBasedObject } from './roleBasedFactory'; import { IExecuteObservableResponse, IResponseMapping, IServerResponse, ServerResponseType } from './types'; -import { IInterpreterService } from '../../../interpreter/contracts'; // tslint:disable:no-any @@ -54,7 +54,7 @@ export class HostJupyterNotebook getDisposedError: () => Error, workspace: IWorkspaceService, appService: IApplicationShell, - interpreterService: IInterpreterService, + interpreterService: IInterpreterService ) { super(liveShare, session, configService, disposableRegistry, owner, launchInfo, loggers, resource, getDisposedError, workspace, appService, interpreterService); } diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts index 48174d3b208d..92bce82bbc04 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -13,6 +13,7 @@ import { traceInfo } from '../../../common/logger'; import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../../common/types'; import * as localize from '../../../common/utils/localize'; import { StopWatch } from '../../../common/utils/stopWatch'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { sendTelemetryEvent } from '../../../telemetry'; import { Identifiers, LiveShare, LiveShareCommands, RegExpValues, Telemetry } from '../../constants'; import { @@ -29,7 +30,6 @@ import { JupyterServerBase } from '../jupyterServer'; import { HostJupyterNotebook } from './hostJupyterNotebook'; import { LiveShareParticipantHost } from './liveShareParticipantMixin'; import { IRoleBasedObject } from './roleBasedFactory'; -import { IInterpreterService } from '../../../interpreter/contracts'; // tslint:disable-next-line: no-require-imports // tslint:disable:no-any diff --git a/src/client/ioc/serviceManager.ts b/src/client/ioc/serviceManager.ts index 0fd5e82bbdad..76fcf8e0e367 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -23,8 +23,8 @@ export class ServiceManager implements IServiceManager { } // tslint:disable-next-line:no-any - public addBinding(serviceIdentifier1: identifier, serviceIdentifier2: identifier): void { - this.container.bind(serviceIdentifier2).toService(serviceIdentifier1); + public addBinding(from: identifier, to: identifier): void { + this.container.bind(to).toService(from); } // tslint:disable-next-line:no-any diff --git a/src/client/ioc/types.ts b/src/client/ioc/types.ts index 4c6cb69c247f..e07f82ae811f 100644 --- a/src/client/ioc/types.ts +++ b/src/client/ioc/types.ts @@ -30,7 +30,7 @@ export interface IServiceManager { addSingleton(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; addSingletonInstance(serviceIdentifier: interfaces.ServiceIdentifier, instance: T, name?: string | number | symbol): void; addFactory(factoryIdentifier: interfaces.ServiceIdentifier>, factoryMethod: interfaces.FactoryCreator): void; - addBinding(serviceIdentifier1: interfaces.ServiceIdentifier, serviceIdentifier2: interfaces.ServiceIdentifier): void; + addBinding(from: interfaces.ServiceIdentifier, to: interfaces.ServiceIdentifier): void; get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; getAll(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T[]; rebind(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts index 2efd2c6ba1fe..6d391fee6c73 100644 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ b/src/test/activation/languageServer/analysisOptions.unit.test.ts @@ -89,7 +89,7 @@ suite('Language Server - Analysis Options', () => { when(interpreterService.onDidChangeInterpreter).thenReturn(() => disposable2.object); when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); - await analysisOptions.initialize(undefined); + await analysisOptions.initialize(undefined, undefined); verify(workspace.onDidChangeConfiguration).once(); verify(interpreterService.onDidChangeInterpreter).once(); @@ -117,7 +117,7 @@ suite('Language Server - Analysis Options', () => { let settingsChangedInvokedCount = 0; analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); - await analysisOptions.initialize(undefined); + await analysisOptions.initialize(undefined, undefined); expect(configChangedHandler).to.not.be.undefined; expect(interpreterChangedHandler).to.not.be.undefined; @@ -189,7 +189,7 @@ suite('Language Server - Analysis Options', () => { let settingsChangedInvokedCount = 0; analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); - await analysisOptions.initialize(uri); + await analysisOptions.initialize(uri, undefined); expect(configChangedHandler).to.not.be.undefined; expect(interpreterChangedHandler).to.not.be.undefined; expect(envVarChangedHandler).to.not.be.undefined; diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index b02726af1f80..b36d9d17f397 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -56,7 +56,7 @@ suite('Language Server - Manager', () => { analysisHandlerRegistered = true; onChangeAnalysisHandler = handler; }; - when(analysisOptions.initialize(resource)).thenResolve(); + when(analysisOptions.initialize(resource, undefined)).thenResolve(); when(analysisOptions.getAnalysisOptions()).thenResolve(languageClientOptions); when(analysisOptions.onDidChange).thenReturn(analysisChangeFn as any); when(serviceContainer.get(ILanguageServerProxy)).thenReturn(instance(languageServer)); @@ -64,7 +64,7 @@ suite('Language Server - Manager', () => { await manager.start(resource, undefined); - verify(analysisOptions.initialize(resource)).once(); + verify(analysisOptions.initialize(resource, undefined)).once(); verify(analysisOptions.getAnalysisOptions()).once(); verify(serviceContainer.get(ILanguageServerProxy)).once(); verify(languageServer.start(resource, undefined, languageClientOptions)).once(); From 7dab6ad3324f88ccc24cd93b33ca7ee58aa9ed14 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 10:56:05 -0800 Subject: [PATCH 03/26] Ref count idea --- src/client/activation/activationService.ts | 25 ++++++++++--------- .../activation/languageServer/activator.ts | 12 ++++++++- .../languageServer/analysisOptions.ts | 11 ++------ .../languageServer/languageServerProxy.ts | 16 +----------- .../activation/refCountedLanguageServer.ts | 5 ++++ src/client/activation/serviceRegistry.ts | 2 +- src/client/activation/types.ts | 8 +++++- .../intellisense/intellisenseProvider.ts | 11 ++++++++ .../datascience/jupyter/jupyterNotebook.ts | 9 ++++--- .../activation/activationService.unit.test.ts | 16 ++++++------ .../analysisOptions.unit.test.ts | 2 +- .../languageServer.unit.test.ts | 2 +- 12 files changed, 66 insertions(+), 53 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index f73b6287931a..0fb01d42a429 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -21,10 +21,12 @@ import { Resource } from '../common/types'; import { swallowExceptions } from '../common/utils/decorators'; +import { noop } from '../common/utils/misc'; import { IInterpreterService, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; +import { Commands } from './languageServer/constants'; import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { IExtensionActivationService, @@ -61,11 +63,13 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv IDiagnosticsService, LSNotSupportedDiagnosticServiceId ); + const commandManager = this.serviceContainer.get(ICommandManager); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this))); + disposables.push(commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCaches.bind(this))); } public async activate(resource: Resource): Promise { @@ -162,16 +166,8 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv } private async onDidChangeInterpreter() { - // If there's a current item in the map, dispose of it. The dispose - // should remove it from the map - const key = await this.getKey(this.resource, this.interpreter); - const item = this.cache.get(key); - if (item) { - (await item).dispose(); - } - - // Then create a new one for our current resource - await this.activate(this.resource); + // Reactivate the resource. It should destroy the old one if it's different. + return this.activate(this.resource); } private async createRefCountedServer(resource: Resource, interpreter: PythonInterpreter | undefined, key: string): Promise { @@ -191,7 +187,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; let server = this.serviceContainer.get(ILanguageServerActivator, serverName); try { - await server.activate(resource); + await server.activate(resource, interpreter); } catch (ex) { if (jedi) { throw ex; @@ -252,7 +248,12 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv private async getKey(resource: Resource, interpreter?: PythonInterpreter): Promise { const resourcePortion = this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); interpreter = interpreter ? interpreter : await this.interpreterService.getActiveInterpreter(resource); - const interperterPortion = interpreter ? interpreter.path : ''; + const interperterPortion = interpreter ? `${interpreter.path}-${interpreter.envName}` : ''; return `${resourcePortion}-${interperterPortion}`; } + + private async onClearAnalysisCaches() { + const values = await Promise.all([...this.cache.values()]); + values.forEach(v => v.clearAnalysisCache ? v.clearAnalysisCache() : noop()); + } } diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index 4a81f478522b..01194d0813ba 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -25,9 +25,10 @@ import { import * as vscodeLanguageClient from 'vscode-languageclient'; import { IWorkspaceService } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; +import { traceDecorators, traceError } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; import { IConfigurationService, Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { PythonInterpreter } from '../../interpreter/contracts'; import { @@ -152,6 +153,15 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato return this.handleProvideSignatureHelp(document, position, token, context); } + public clearAnalysisCache(): void { + const languageClient = this.getLanguageClient(); + if (languageClient) { + languageClient.sendRequest('python/clearAnalysisCache').then(noop, ex => + traceError('Request python/clearAnalysisCache failed', ex) + ); + } + } + private getLanguageClient(): vscodeLanguageClient.LanguageClient | undefined { const proxy = this.manager.languageProxy; if (proxy) { diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts index 56e2841bf788..11e707bd225c 100644 --- a/src/client/activation/languageServer/analysisOptions.ts +++ b/src/client/activation/languageServer/analysisOptions.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { @@ -39,7 +38,7 @@ import { } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { ILanguageServerAnalysisOptions, ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; @injectable() @@ -58,7 +57,6 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt @inject(IConfigurationService) private readonly configuration: IConfigurationService, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IPythonExtensionBanner) @named(BANNER_NAME_LS_SURVEY) private readonly surveyBanner: IPythonExtensionBanner, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService @@ -73,11 +71,6 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt let disposable = this.workspace.onDidChangeConfiguration(this.onSettingsChangedHandler, this); this.disposables.push(disposable); - if (!this.interpreter) { - disposable = this.interpreterService.onDidChangeInterpreter(() => this.didChange.fire(), this); - this.disposables.push(disposable); - } - disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); this.disposables.push(disposable); } @@ -92,7 +85,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt public async getAnalysisOptions(): Promise { const properties: Record = {}; - const interpreterInfo = this.interpreter ? this.interpreter : await this.interpreterService.getActiveInterpreter(this.resource); + const interpreterInfo = this.interpreter; if (!interpreterInfo) { // tslint:disable-next-line:no-suspicious-comment // TODO: How do we handle this? It is pretty unlikely... diff --git a/src/client/activation/languageServer/languageServerProxy.ts b/src/client/activation/languageServer/languageServerProxy.ts index d4e6426b9c5a..013965d075e3 100644 --- a/src/client/activation/languageServer/languageServerProxy.ts +++ b/src/client/activation/languageServer/languageServerProxy.ts @@ -5,7 +5,6 @@ import '../../common/extensions'; import { inject, injectable, named } from 'inversify'; import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { ICommandManager } from '../../common/application/types'; import { traceDecorators, traceError } from '../../common/logger'; import { IConfigurationService, Resource } from '../../common/types'; import { createDeferred, Deferred, sleep } from '../../common/utils/async'; @@ -17,7 +16,6 @@ import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; import { ITestManagementService } from '../../testing/types'; import { ILanguageClientFactory, ILanguageServerProxy, LanguageClientFactory } from '../types'; -import { Commands } from './constants'; import { ProgressReporting } from './progress'; @injectable() @@ -33,8 +31,7 @@ export class LanguageServerProxy implements ILanguageServerProxy { @named(LanguageClientFactory.base) private readonly factory: ILanguageClientFactory, @inject(ITestManagementService) private readonly testManager: ITestManagementService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ICommandManager) private readonly commandManager: ICommandManager + @inject(IConfigurationService) private readonly configurationService: IConfigurationService ) { this.startupCompleted = createDeferred(); } @@ -77,8 +74,6 @@ export class LanguageServerProxy implements ILanguageServerProxy { sendTelemetryEvent(eventName, telemetryEvent.Measurements, telemetryEvent.Properties); }); } - - this.registerCommands(); await this.registerTestServices(); } else { await this.startupCompleted.promise; @@ -112,13 +107,4 @@ export class LanguageServerProxy implements ILanguageServerProxy { } await this.testManager.activate(new LanguageServerSymbolProvider(this.languageClient!)); } - private registerCommands() { - const disposable = this.commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCache, this); - this.disposables.push(disposable); - } - private onClearAnalysisCache() { - this.languageClient!.sendRequest('python/clearAnalysisCache').then(noop, ex => - traceError('Request python/clearAnalysisCache failed', ex) - ); - } } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index 6c30f99c8abe..d3f66b619001 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -39,6 +39,11 @@ export class RefCountedLanguageServer implements ILanguageServer { this.disposeCallback(); } } + + public clearAnalysisCache() { + this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); + } + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { this.impl.handleChanges ? this.impl.handleChanges(document, changes) : noop(); } diff --git a/src/client/activation/serviceRegistry.ts b/src/client/activation/serviceRegistry.ts index 938a74ff276f..51618666e981 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -89,7 +89,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader); serviceManager.addSingleton(IPlatformData, PlatformData); serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions); - serviceManager.addSingleton(ILanguageServerProxy, LanguageServerProxy); + serviceManager.add(ILanguageServerProxy, LanguageServerProxy); serviceManager.add(ILanguageServerManager, LanguageServerManager); serviceManager.addSingleton(ILanguageServerOutputChannel, LanguageServerOutputChannel); serviceManager.addSingleton(IExtensionSingleActivationService, ExtensionSurveyPrompt); diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 6a9e99e0fd46..c310ff6ceabd 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -74,6 +74,11 @@ export interface DocumentHandler { handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void; } +// tslint:disable-next-line: interface-name +export interface LanguageServerCommandHandler { + clearAnalysisCache(): void; +} + export interface ILanguageServer extends RenameProvider, DefinitionProvider, @@ -84,12 +89,13 @@ export interface ILanguageServer extends DocumentSymbolProvider, SignatureHelpProvider, Partial, + Partial, IDisposable { } export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); export interface ILanguageServerActivator extends ILanguageServer { - activate(resource: Resource, interpreter?: PythonInterpreter): Promise; + activate(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; } export const ILanguageServerCache = Symbol('ILanguageServerCache'); diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index e13ce68cf263..5ff5ca29f09e 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -81,6 +81,10 @@ export class IntellisenseProvider implements IInteractiveWindowListener { if (this.temporaryFile) { this.temporaryFile.dispose(); } + if (this.languageServer) { + this.languageServer.dispose(); + this.languageServer = undefined; + } } public get postMessage(): Event<{ message: string; payload: any }> { @@ -166,6 +170,13 @@ export class IntellisenseProvider implements IInteractiveWindowListener { // Dispose of our old language service this.languageServer?.dispose(); + // This new language server does not know about our document, so tell it. + const document = await this.getDocument(); + if (document && languageServer.handleOpen) { + languageServer.handleOpen(document); + this.sentOpenDocument = true; + } + // Save the ref. this.languageServer = languageServer; } diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index 03f64f441f62..ab359684df43 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -145,6 +145,7 @@ export class JupyterNotebookBase implements INotebook { private _disposed: boolean = false; private _workingDirectory: string | undefined; private _loggers: INotebookExecutionLogger[] = []; + private interpreter: PythonInterpreter | undefined; constructor( _liveShare: ILiveShareApi, // This is so the liveshare mixin works @@ -163,6 +164,9 @@ export class JupyterNotebookBase implements INotebook { this.sessionStartTime = Date.now(); this._resource = resource; this._loggers = [...loggers]; + // Save our interpreter and don't change it. Later on when kernel changes + // are possible, recompute it. + this.interpreterService.getActiveInterpreter(resource).then(i => this.interpreter = i).ignoreErrors(); } public get server(): INotebookServer { @@ -463,10 +467,7 @@ export class JupyterNotebookBase implements INotebook { } public async getMatchingInterpreter(): Promise { - // tslint:disable-next-line: no-suspicious-comment - // TODO: This should use the kernel to determine which interpreter matches. Right now - // just return the active one - return this.interpreterService.getActiveInterpreter(); + return this.interpreter; } private finishUncompletedCells() { diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 2930e5769de1..1f6acff5892f 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -112,7 +112,7 @@ suite('Activation - ActivationService', () => { lsSupported: boolean = true ) { activator - .setup(a => a.activate(undefined)) + .setup(a => a.activate(undefined, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); let activatorName = LanguageServerActivator.Jedi; @@ -351,7 +351,7 @@ suite('Activation - ActivationService', () => { .returns(() => activatorDotNet.object) .verifiable(TypeMoq.Times.once()); activatorDotNet - .setup(a => a.activate(undefined)) + .setup(a => a.activate(undefined, undefined)) .returns(() => Promise.reject(new Error(''))) .verifiable(TypeMoq.Times.once()); serviceContainer @@ -364,7 +364,7 @@ suite('Activation - ActivationService', () => { .returns(() => activatorJedi.object) .verifiable(TypeMoq.Times.once()); activatorJedi - .setup(a => a.activate(undefined)) + .setup(a => a.activate(undefined, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -380,7 +380,7 @@ suite('Activation - ActivationService', () => { resource: Resource ) { activator - .setup(a => a.activate(TypeMoq.It.isValue(resource))) + .setup(a => a.activate(TypeMoq.It.isValue(resource), undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); lsNotSupportedDiagnosticService @@ -467,7 +467,7 @@ suite('Activation - ActivationService', () => { .returns(() => activator1.object) .verifiable(TypeMoq.Times.once()); activator1 - .setup(a => a.activate(folder1.uri)) + .setup(a => a.activate(folder1.uri, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); experiments @@ -485,7 +485,7 @@ suite('Activation - ActivationService', () => { .returns(() => activator2.object) .verifiable(TypeMoq.Times.once()); activator2 - .setup(a => a.activate(folder2.uri)) + .setup(a => a.activate(folder2.uri, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); experiments @@ -582,8 +582,8 @@ suite('Activation - ActivationService', () => { .verifiable(TypeMoq.Times.exactly(2)); state.setup(s => s.updateValue(TypeMoq.It.isValue(true))) .returns(() => { - state.setup(s => s.value).returns(() => true); - return Promise.resolve(); + state.setup(s => s.value).returns(() => true); + return Promise.resolve(); }) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts index 6d391fee6c73..6f3c727d0900 100644 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ b/src/test/activation/languageServer/analysisOptions.unit.test.ts @@ -78,7 +78,7 @@ suite('Language Server - Analysis Options', () => { analysisOptions = new TestClass(context.object, instance(envVarsProvider), instance(configurationService), instance(workspace), instance(surveyBanner), - instance(interpreterService), lsOutputChannel.object, + lsOutputChannel.object, instance(pathUtils), instance(lsFolderService)); }); test('Initialize will add event handlers and will dispose them when running dispose', async () => { diff --git a/src/test/activation/languageServer/languageServer.unit.test.ts b/src/test/activation/languageServer/languageServer.unit.test.ts index e5dc5f4eff12..40601ccd57f9 100644 --- a/src/test/activation/languageServer/languageServer.unit.test.ts +++ b/src/test/activation/languageServer/languageServer.unit.test.ts @@ -43,7 +43,7 @@ suite('Language Server - LanguageServer', () => { commandManager.setup(c => c.registerCommand(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())).returns(() => { return typemoq.Mock.ofType().object; }); - server = new LanguageServerTest(instance(clientFactory), instance(testManager), configService.object, commandManager.object); + server = new LanguageServerTest(instance(clientFactory), instance(testManager), configService.object); }); teardown(() => { client.setup(c => c.stop()).returns(() => Promise.resolve()); From 23a9a16bdfdc53edd21b4b52afe87cf4a40b86d3 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 12:43:33 -0800 Subject: [PATCH 04/26] Fix intellisense to work on restart of a jupyter server --- .../intellisense/intellisenseDocument.ts | 16 ++++++++++++++++ .../intellisense/intellisenseProvider.ts | 17 ++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts b/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts index b7f53e9c0835..575716810c40 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts @@ -94,6 +94,10 @@ export class IntellisenseDocument implements TextDocument { public get isUntitled(): boolean { return true; } + + public get isReadOnly(): boolean { + return !this.inEditMode; + } public get languageId(): string { return PYTHON_LANGUAGE; } @@ -146,6 +150,18 @@ export class IntellisenseDocument implements TextDocument { return this._contents.substr(startOffset, endOffset - startOffset); } } + + public getFullContentChanges(): TextDocumentContentChangeEvent[] { + return [ + { + range: this.createSerializableRange(new Position(0, 0), new Position(0, 0)), + rangeOffset: 0, + rangeLength: 0, // Adds are always zero + text: this._contents + } + ]; + } + public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { if (!regexp) { // use default when custom-regexp isn't provided diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 5ff5ca29f09e..737f9e0bb39b 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -172,9 +172,16 @@ export class IntellisenseProvider implements IInteractiveWindowListener { // This new language server does not know about our document, so tell it. const document = await this.getDocument(); - if (document && languageServer.handleOpen) { - languageServer.handleOpen(document); - this.sentOpenDocument = true; + if (document && languageServer.handleOpen && languageServer.handleChanges) { + // If we already sent an open document, that means we need to send both the open and + // the new changes + if (this.sentOpenDocument) { + languageServer.handleOpen(document); + languageServer.handleChanges(document, document.getFullContentChanges()); + } else { + this.sentOpenDocument = true; + languageServer.handleOpen(document); + } } // Save the ref. @@ -507,9 +514,9 @@ export class IntellisenseProvider implements IInteractiveWindowListener { } private async restartKernel(): Promise { - // This is the one that acts like a reset + // This is the one that acts like a reset if this is the interactive window const document = await this.getDocument(); - if (document) { + if (document && document.isReadOnly) { this.sentOpenDocument = false; const changes = document.removeAllCells(); return this.handleChanges(document, changes); From 2c35a872fd87ff22bc2b48fda028b1928b2795db Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 16:11:14 -0800 Subject: [PATCH 05/26] Disconnect from the language server when activating --- src/client/activation/activationService.ts | 19 +- .../activation/languageServer/activator.ts | 4 + .../languageServer/analysisOptions.ts | 53 +---- .../languageClientMiddleware.ts | 204 ++++++++++++++++++ .../activation/languageServer/manager.ts | 13 +- .../activation/refCountedLanguageServer.ts | 4 + src/client/activation/types.ts | 7 + .../interactive-common/interactiveBase.ts | 26 --- .../liveshare/guestJupyterExecution.ts | 2 +- .../jupyter/liveshare/hostJupyterExecution.ts | 2 +- .../jupyter/liveshare/serverCache.ts | 14 +- src/client/datascience/types.ts | 1 - .../analysisOptions.unit.test.ts | 17 +- .../languageServer/manager.unit.test.ts | 7 +- 14 files changed, 270 insertions(+), 103 deletions(-) create mode 100644 src/client/activation/languageServer/languageClientMiddleware.ts diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 0fb01d42a429..cb66ee7a97b7 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -39,11 +39,16 @@ import { const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; const workspacePathNameForGlobalWorkspaces = ''; +interface IActivatedServer { + key: string; + server: ILanguageServer; +} + @injectable() export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable { private cache = new Map>(); private jediServer: RefCountedLanguageServer | undefined; - private activatedServer?: ILanguageServer; + private activatedServer?: IActivatedServer; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; private readonly appShell: IApplicationShell; @@ -76,11 +81,17 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv // Get a new server and dispose of the old one (might be the same one) this.resource = resource; this.interpreter = await this.interpreterService.getActiveInterpreter(resource); + const key = await this.getKey(resource, this.interpreter); const result = await this.get(resource, this.interpreter); if (this.activatedServer) { - this.activatedServer.dispose(); + // The activatedServer is the one handling intellisense requests for the entire ide, deactivate it if different. + // However it should still be able to handle direct requests (like for DS windows) + if (this.activatedServer.key !== key && this.activatedServer.server.disconnect) { + this.activatedServer.server.disconnect(); + } + this.activatedServer.server.dispose(); } - this.activatedServer = result; + this.activatedServer = { key, server: result }; } public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { @@ -104,7 +115,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv public dispose() { if (this.activatedServer) { - this.activatedServer.dispose(); + this.activatedServer.server.dispose(); } } @swallowExceptions('Send telemetry for Language Server current selection') diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index 01194d0813ba..e09bd04b13c0 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -105,6 +105,10 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); } + public disconnect(): void { + this.manager.disconnect(); + } + public handleOpen(document: TextDocument): void { const languageClient = this.getLanguageClient(); if (languageClient) { diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts index 11e707bd225c..817b0fa91bc3 100644 --- a/src/client/activation/languageServer/analysisOptions.ts +++ b/src/client/activation/languageServer/analysisOptions.ts @@ -1,41 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; -import { - CancellationToken, - CompletionContext, - ConfigurationChangeEvent, - Diagnostic, - Disposable, - Event, - EventEmitter, - Position, - TextDocument, - Uri, - WorkspaceFolder -} from 'vscode'; -import { - DocumentFilter, - DocumentSelector, - HandleDiagnosticsSignature, - LanguageClientOptions, - ProvideCompletionItemsSignature, - RevealOutputChannelOn -} from 'vscode-languageclient'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, WorkspaceFolder } from 'vscode'; +import { DocumentFilter, DocumentSelector, LanguageClientOptions, RevealOutputChannelOn } from 'vscode-languageclient'; import { IWorkspaceService } from '../../common/application/types'; -import { HiddenFilePrefix, isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; +import { isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; import { traceDecorators, traceError } from '../../common/logger'; -import { - BANNER_NAME_LS_SURVEY, - IConfigurationService, - IExtensionContext, - IOutputChannel, - IPathUtils, - IPythonExtensionBanner, - Resource -} from '../../common/types'; +import { IConfigurationService, IExtensionContext, IOutputChannel, IPathUtils, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { PythonInterpreter } from '../../interpreter/contracts'; @@ -56,7 +29,6 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, @inject(IConfigurationService) private readonly configuration: IConfigurationService, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPythonExtensionBanner) @named(BANNER_NAME_LS_SURVEY) private readonly surveyBanner: IPythonExtensionBanner, @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService @@ -81,6 +53,7 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt this.disposables.forEach(d => d.dispose()); this.didChange.dispose(); } + // tslint:disable-next-line: max-func-body-length @traceDecorators.error('Failed to get analysis options') public async getAnalysisOptions(): Promise { const properties: Record = {}; @@ -160,20 +133,6 @@ export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOpt analysisUpdates: true, traceLogging: true, // Max level, let LS decide through settings actual level of logging. asyncStartup: true - }, - middleware: { - provideCompletionItem: (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => { - this.surveyBanner.showBanner().ignoreErrors(); - return next(document, position, context, token); - }, - handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => { - // Skip sending if this is a special file. - const filePath = uri.fsPath; - const baseName = filePath ? path.basename(filePath) : undefined; - if (!baseName || !baseName.startsWith(HiddenFilePrefix)) { - next(uri, diagnostics); - } - } } }; } diff --git a/src/client/activation/languageServer/languageClientMiddleware.ts b/src/client/activation/languageServer/languageClientMiddleware.ts new file mode 100644 index 000000000000..46e12404fcd4 --- /dev/null +++ b/src/client/activation/languageServer/languageClientMiddleware.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { + CancellationToken, + CompletionContext, + ConfigurationChangeEvent, + Diagnostic, + Disposable, + Event, + EventEmitter, + Position, + TextDocument, + Uri, + WorkspaceFolder, + CompletionItem, + ProviderResult, + SignatureHelp, + Definition, + DefinitionLink, + DocumentLink, + DocumentHighlight, + SymbolInformation, + DocumentSymbol, + CodeActionContext, + Command, + Range, + Location, + CodeAction, + CodeLens, + FormattingOptions, + TextEdit, + WorkspaceEdit +} from 'vscode'; +import { + DocumentFilter, + DocumentSelector, + HandleDiagnosticsSignature, + LanguageClientOptions, + ProvideCompletionItemsSignature, + ProvideHoverSignature, + RevealOutputChannelOn, + Middleware, + ResolveCompletionItemSignature, + ProvideSignatureHelpSignature, + ProvideDefinitionSignature, + ResolveDocumentLinkSignature, + ProvideDocumentLinksSignature, + PrepareRenameSignature, + ProvideReferencesSignature, + ProvideDocumentSymbolsSignature, + ProvideCodeActionsSignature, + ResolveCodeLensSignature, + ProvideCodeLensesSignature, + ProvideWorkspaceSymbolsSignature, + ProvideDocumentHighlightsSignature, + ProvideDocumentFormattingEditsSignature, + ProvideDocumentRangeFormattingEditsSignature, + ProvideOnTypeFormattingEditsSignature, + ProvideRenameEditsSignature +} from 'vscode-languageclient'; + +import { IWorkspaceService } from '../../common/application/types'; +import { HiddenFilePrefix, isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; +import { traceDecorators, traceError, traceInfo } from '../../common/logger'; +import { + BANNER_NAME_LS_SURVEY, + IConfigurationService, + IExtensionContext, + IOutputChannel, + IPathUtils, + IPythonExtensionBanner, + Resource +} from '../../common/types'; +import { debounceSync } from '../../common/utils/decorators'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonInterpreter } from '../../interpreter/contracts'; +import { ILanguageServerAnalysisOptions, ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; + +export class LanguageClientMiddleware implements Middleware { + private connected = true; + + public constructor(private readonly surveyBanner: IPythonExtensionBanner) { + } + + public disconnect() { + this.connected = false; + } + + public provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) { + if (this.connected) { + this.surveyBanner.showBanner().ignoreErrors(); + return next(document, position, context, token); + } + } + + public provideHover(document: TextDocument, position: Position, token: CancellationToken, next: ProvideHoverSignature) { + if (this.connected) { + return next(document, position, token); + } + } + + public handleDiagnostics(uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) { + if (this.connected) { + // Skip sending if this is a special file. + const filePath = uri.fsPath; + const baseName = filePath ? path.basename(filePath) : undefined; + if (!baseName || !baseName.startsWith(HiddenFilePrefix)) { + next(uri, diagnostics); + } + } + } + + public resolveCompletionItem(item: CompletionItem, token: CancellationToken, next: ResolveCompletionItemSignature): ProviderResult { + if (this.connected) { + return next(item, token); + } + } + public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, next: ProvideSignatureHelpSignature): ProviderResult { + if (this.connected) { + return next(document, position, token); + } + } + public provideDefinition(document: TextDocument, position: Position, token: CancellationToken, next: ProvideDefinitionSignature): ProviderResult { + if (this.connected) { + return next(document, position, token); + } + } + public provideReferences(document: TextDocument, position: Position, options: { + includeDeclaration: boolean; + }, token: CancellationToken, next: ProvideReferencesSignature): ProviderResult { + if (this.connected) { + return next(document, position, options, token); + } + } + public provideDocumentHighlights(document: TextDocument, position: Position, token: CancellationToken, next: ProvideDocumentHighlightsSignature): ProviderResult { + if (this.connected) { + return next(document, position, token); + } + } + public provideDocumentSymbols(document: TextDocument, token: CancellationToken, next: ProvideDocumentSymbolsSignature): ProviderResult { + if (this.connected) { + return next(document, token); + } + } + public provideWorkspaceSymbols(query: string, token: CancellationToken, next: ProvideWorkspaceSymbolsSignature): ProviderResult { + if (this.connected) { + return next(query, token); + } + } + public provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken, next: ProvideCodeActionsSignature): ProviderResult<(Command | CodeAction)[]> { + if (this.connected) { + return next(document, range, context, token); + } + } + public provideCodeLenses(document: TextDocument, token: CancellationToken, next: ProvideCodeLensesSignature): ProviderResult { + if (this.connected) { + return next(document, token); + } + } + public resolveCodeLens(codeLens: CodeLens, token: CancellationToken, next: ResolveCodeLensSignature): ProviderResult { + if (this.connected) { + return next(codeLens, token); + } + } + public provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentFormattingEditsSignature): ProviderResult { + if (this.connected) { + return next(document, options, token); + } + } + public provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken, next: ProvideDocumentRangeFormattingEditsSignature): ProviderResult { + if (this.connected) { + return next(document, range, options, token); + } + } + public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken, next: ProvideOnTypeFormattingEditsSignature): ProviderResult { + if (this.connected) { + return next(document, position, ch, options, token); + } + } + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken, next: ProvideRenameEditsSignature): ProviderResult { + if (this.connected) { + return next(document, position, newName, token); + } + } + public prepareRename(document: TextDocument, position: Position, token: CancellationToken, next: PrepareRenameSignature): ProviderResult { + if (this.connected) { + return next(document, position, token); + } + } + public provideDocumentLinks(document: TextDocument, token: CancellationToken, next: ProvideDocumentLinksSignature): ProviderResult { + if (this.connected) { + return next(document, token); + } + } + public resolveDocumentLink(link: DocumentLink, token: CancellationToken, next: ResolveDocumentLinkSignature): ProviderResult { + if (this.connected) { + return next(link, token); + } + } +} diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index e72622b4ea82..136aa5c0ff3c 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -2,10 +2,10 @@ // Licensed under the MIT License. import '../../common/extensions'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import { traceDecorators } from '../../common/logger'; -import { IDisposable, Resource } from '../../common/types'; +import { IDisposable, Resource, IPythonExtensionBanner, BANNER_NAME_LS_SURVEY } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { PythonInterpreter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; @@ -17,17 +17,20 @@ import { ILanguageServerManager, ILanguageServerProxy } from '../types'; +import { LanguageClientMiddleware } from './languageClientMiddleware'; @injectable() export class LanguageServerManager implements ILanguageServerManager { private languageServerProxy?: ILanguageServerProxy; private resource!: Resource; private interpreter: PythonInterpreter | undefined; + private middleware: LanguageClientMiddleware | undefined; private disposables: IDisposable[] = []; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension + @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension, + @inject(IPythonExtensionBanner) @named(BANNER_NAME_LS_SURVEY) private readonly surveyBanner: IPythonExtensionBanner ) { } public dispose() { if (this.languageProxy) { @@ -52,6 +55,9 @@ export class LanguageServerManager implements ILanguageServerManager { await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); } + public disconnect() { + this.middleware?.disconnect(); + } protected registerCommandHandler() { this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); } @@ -77,6 +83,7 @@ export class LanguageServerManager implements ILanguageServerManager { protected async startLanguageServer(): Promise { this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); const options = await this.analysisOptions!.getAnalysisOptions(); + options.middleware = this.middleware = new LanguageClientMiddleware(this.surveyBanner); await this.languageServerProxy.start(this.resource, this.interpreter, options); this.loadExtensionIfNecessary(); } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index d3f66b619001..fe043774dd1d 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -40,6 +40,10 @@ export class RefCountedLanguageServer implements ILanguageServer { } } + public disconnect() { + this.impl.disconnect ? this.impl.disconnect() : noop(); + } + public clearAnalysisCache() { this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index c310ff6ceabd..41e53b1256ef 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -79,6 +79,11 @@ export interface LanguageServerCommandHandler { clearAnalysisCache(): void; } +// tslint:disable-next-line: interface-name +export interface RegisteredServer { + disconnect(): void; +} + export interface ILanguageServer extends RenameProvider, DefinitionProvider, @@ -90,6 +95,7 @@ export interface ILanguageServer extends SignatureHelpProvider, Partial, Partial, + Partial, IDisposable { } @@ -153,6 +159,7 @@ export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { readonly languageProxy: ILanguageServerProxy | undefined; start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + disconnect(): void; } export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); export interface ILanguageServerExtension extends IDisposable { diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index 387279f507a6..197839abb0f2 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -78,7 +78,6 @@ import { InteractiveWindowMessageListener } from './interactiveWindowMessageList @injectable() export abstract class InteractiveBase extends WebViewHost implements IInteractiveBase { - private interpreterChangedDisposable: Disposable; private unfinishedCells: ICell[] = []; private restartingKernel: boolean = false; private potentiallyUnfinishedStatus: Disposable[] = []; @@ -132,9 +131,6 @@ export abstract class InteractiveBase extends WebViewHost this.activating()); this.disposables.push(handler); @@ -293,9 +289,6 @@ export abstract class InteractiveBase extends WebViewHost l.dispose()); - if (this.interpreterChangedDisposable) { - this.interpreterChangedDisposable.dispose(); - } this.updateContexts(undefined); } @@ -935,13 +928,6 @@ export abstract class InteractiveBase extends WebViewHost { - // Update our load promise. We need to restart the jupyter server - if (this.loadPromise) { - this.loadPromise = this.reloadWithNew(); - } - } - private async stopServer(): Promise { if (this.loadPromise) { await this.loadPromise; @@ -954,18 +940,6 @@ export abstract class InteractiveBase extends WebViewHost { - const status = this.setStatus(localize.DataScience.startingJupyter(), true); - try { - // Not the same as reload, we need to actually wait for the server. - await this.stopServer(); - await this.startServer(); - await this.addSysInfo(SysInfoReason.New); - } finally { - status.dispose(); - } - } - private async reloadAfterShutdown(): Promise { try { this.stopServer().ignoreErrors(); diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts index 6fd4af360e70..453f57023eec 100644 --- a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts @@ -58,7 +58,7 @@ export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExec configuration, serviceContainer); asyncRegistry.push(this); - this.serverCache = new ServerCache(configuration, workspace, fileSystem, interpreterService); + this.serverCache = new ServerCache(configuration, workspace, fileSystem); } public async dispose(): Promise { diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts index 363a4331bb89..ddb1a8f9cb08 100644 --- a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +++ b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts @@ -59,7 +59,7 @@ export class HostJupyterExecution workspace, configService, serviceContainer); - this.serverCache = new ServerCache(configService, workspace, fileSys, interpreterService); + this.serverCache = new ServerCache(configService, workspace, fileSys); } public async dispose(): Promise { diff --git a/src/client/datascience/jupyter/liveshare/serverCache.ts b/src/client/datascience/jupyter/liveshare/serverCache.ts index 99597935224f..5e14b8f674cf 100644 --- a/src/client/datascience/jupyter/liveshare/serverCache.ts +++ b/src/client/datascience/jupyter/liveshare/serverCache.ts @@ -9,7 +9,6 @@ import * as uuid from 'uuid/v4'; import { IWorkspaceService } from '../../../common/application/types'; import { IFileSystem } from '../../../common/platform/types'; import { IAsyncDisposable, IConfigurationService } from '../../../common/types'; -import { IInterpreterService } from '../../../interpreter/contracts'; import { INotebookServer, INotebookServerOptions } from '../../types'; export class ServerCache implements IAsyncDisposable { @@ -19,8 +18,7 @@ export class ServerCache implements IAsyncDisposable { constructor( private configService: IConfigurationService, private workspace: IWorkspaceService, - private fileSystem: IFileSystem, - private interpreterService: IInterpreterService + private fileSystem: IFileSystem ) { } public async get(options?: INotebookServerOptions): Promise { @@ -62,16 +60,13 @@ export class ServerCache implements IAsyncDisposable { } public async generateDefaultOptions(options?: INotebookServerOptions): Promise { - const activeInterpreter = await this.interpreterService.getActiveInterpreter(); - const activeInterpreterPath = activeInterpreter ? activeInterpreter.path : undefined; return { enableDebugging: options ? options.enableDebugging : false, uri: options ? options.uri : undefined, useDefaultConfig: options ? options.useDefaultConfig : true, // Default for this is true. usingDarkTheme: options ? options.usingDarkTheme : undefined, purpose: options ? options.purpose : uuid(), - workingDir: options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory(), - interpreterPath: options && options.interpreterPath ? options.interpreterPath : activeInterpreterPath + workingDir: options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory() }; } @@ -81,12 +76,9 @@ export class ServerCache implements IAsyncDisposable { } else { // combine all the values together to make a unique key const uri = options.uri ? options.uri : ''; - const interpreter = options.interpreterPath ? options.interpreterPath : ''; const useFlag = options.useDefaultConfig ? 'true' : 'false'; const debug = options.enableDebugging ? 'true' : 'false'; - // tslint:disable-next-line:no-suspicious-comment - // TODO: Should there be some separator in the key? - return `${options.purpose}${uri}${useFlag}${options.workingDir}${debug}${interpreter}`; + return `${options.purpose}${uri}${useFlag}${options.workingDir}${debug}`; } } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 13a59050d044..069da853d04e 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -109,7 +109,6 @@ export interface INotebookServerOptions { usingDarkTheme?: boolean; useDefaultConfig?: boolean; workingDir?: string; - interpreterPath?: string; purpose: string; } diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts index 6f3c727d0900..ed60cf7f1984 100644 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ b/src/test/activation/languageServer/analysisOptions.unit.test.ts @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { expect } from 'chai'; import { instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { ConfigurationChangeEvent, Uri, WorkspaceFolder } from 'vscode'; import { DocumentSelector } from 'vscode-languageclient'; + import { LanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; import { LanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; import { ILanguageServerFolderService, ILanguageServerOutputChannel } from '../../../client/activation/types'; @@ -16,12 +14,17 @@ import { WorkspaceService } from '../../../client/common/application/workspace'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { PYTHON_LANGUAGE } from '../../../client/common/constants'; import { PathUtils } from '../../../client/common/platform/pathUtils'; -import { IConfigurationService, IDisposable, IExtensionContext, IOutputChannel, IPathUtils, IPythonExtensionBanner } from '../../../client/common/types'; +import { + IConfigurationService, + IDisposable, + IExtensionContext, + IOutputChannel, + IPathUtils +} from '../../../client/common/types'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { ProposeLanguageServerBanner } from '../../../client/languageServices/proposeLanguageServerBanner'; import { sleep } from '../../core'; // tslint:disable:no-unnecessary-override no-any chai-vague-errors no-unused-expression max-func-body-length @@ -55,7 +58,6 @@ suite('Language Server - Analysis Options', () => { let envVarsProvider: IEnvironmentVariablesProvider; let configurationService: IConfigurationService; let workspace: IWorkspaceService; - let surveyBanner: IPythonExtensionBanner; let interpreterService: IInterpreterService; let outputChannel: IOutputChannel; let lsOutputChannel: typemoq.IMock; @@ -66,7 +68,6 @@ suite('Language Server - Analysis Options', () => { envVarsProvider = mock(EnvironmentVariablesProvider); configurationService = mock(ConfigurationService); workspace = mock(WorkspaceService); - surveyBanner = mock(ProposeLanguageServerBanner); interpreterService = mock(InterpreterService); outputChannel = typemoq.Mock.ofType().object; lsOutputChannel = typemoq.Mock.ofType(); @@ -77,7 +78,7 @@ suite('Language Server - Analysis Options', () => { lsFolderService = mock(LanguageServerFolderService); analysisOptions = new TestClass(context.object, instance(envVarsProvider), instance(configurationService), - instance(workspace), instance(surveyBanner), + instance(workspace), lsOutputChannel.object, instance(pathUtils), instance(lsFolderService)); }); diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index b36d9d17f397..c3eadf070870 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -15,8 +15,10 @@ import { ILanguageServerExtension, ILanguageServerProxy } from '../../../client/activation/types'; +import { IPythonExtensionBanner } from '../../../client/common/types'; import { ServiceContainer } from '../../../client/ioc/container'; import { IServiceContainer } from '../../../client/ioc/types'; +import { ProposeLanguageServerBanner } from '../../../client/languageServices/proposeLanguageServerBanner'; import { sleep } from '../../core'; use(chaiAsPromised); @@ -30,16 +32,19 @@ suite('Language Server - Manager', () => { let languageServer: ILanguageServerProxy; let lsExtension: ILanguageServerExtension; let onChangeAnalysisHandler: Function; + let surveyBanner: IPythonExtensionBanner; const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; setup(() => { serviceContainer = mock(ServiceContainer); analysisOptions = mock(LanguageServerAnalysisOptions); languageServer = mock(LanguageServerProxy); lsExtension = mock(LanguageServerExtension); + surveyBanner = mock(ProposeLanguageServerBanner); manager = new LanguageServerManager( instance(serviceContainer), instance(analysisOptions), - instance(lsExtension) + instance(lsExtension), + instance(surveyBanner) ); }); From 1ecf96154edc51d866e80d8df14f3664faf82b66 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 16:12:01 -0800 Subject: [PATCH 06/26] Fix linter issues --- .../languageClientMiddleware.ts | 81 +++++++------------ .../activation/languageServer/manager.ts | 2 +- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/src/client/activation/languageServer/languageClientMiddleware.ts b/src/client/activation/languageServer/languageClientMiddleware.ts index 46e12404fcd4..d735a415e8dc 100644 --- a/src/client/activation/languageServer/languageClientMiddleware.ts +++ b/src/client/activation/languageServer/languageClientMiddleware.ts @@ -3,79 +3,56 @@ import * as path from 'path'; import { CancellationToken, + CodeAction, + CodeActionContext, + CodeLens, + Command, CompletionContext, - ConfigurationChangeEvent, - Diagnostic, - Disposable, - Event, - EventEmitter, - Position, - TextDocument, - Uri, - WorkspaceFolder, CompletionItem, - ProviderResult, - SignatureHelp, Definition, DefinitionLink, - DocumentLink, + Diagnostic, DocumentHighlight, - SymbolInformation, + DocumentLink, DocumentSymbol, - CodeActionContext, - Command, - Range, - Location, - CodeAction, - CodeLens, FormattingOptions, + Location, + Position, + ProviderResult, + Range, + SignatureHelp, + SymbolInformation, + TextDocument, TextEdit, + Uri, WorkspaceEdit } from 'vscode'; import { - DocumentFilter, - DocumentSelector, HandleDiagnosticsSignature, - LanguageClientOptions, - ProvideCompletionItemsSignature, - ProvideHoverSignature, - RevealOutputChannelOn, Middleware, - ResolveCompletionItemSignature, - ProvideSignatureHelpSignature, - ProvideDefinitionSignature, - ResolveDocumentLinkSignature, - ProvideDocumentLinksSignature, PrepareRenameSignature, - ProvideReferencesSignature, - ProvideDocumentSymbolsSignature, ProvideCodeActionsSignature, - ResolveCodeLensSignature, ProvideCodeLensesSignature, - ProvideWorkspaceSymbolsSignature, - ProvideDocumentHighlightsSignature, + ProvideCompletionItemsSignature, + ProvideDefinitionSignature, ProvideDocumentFormattingEditsSignature, + ProvideDocumentHighlightsSignature, + ProvideDocumentLinksSignature, ProvideDocumentRangeFormattingEditsSignature, + ProvideDocumentSymbolsSignature, + ProvideHoverSignature, ProvideOnTypeFormattingEditsSignature, - ProvideRenameEditsSignature + ProvideReferencesSignature, + ProvideRenameEditsSignature, + ProvideSignatureHelpSignature, + ProvideWorkspaceSymbolsSignature, + ResolveCodeLensSignature, + ResolveCompletionItemSignature, + ResolveDocumentLinkSignature } from 'vscode-languageclient'; -import { IWorkspaceService } from '../../common/application/types'; -import { HiddenFilePrefix, isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; -import { traceDecorators, traceError, traceInfo } from '../../common/logger'; -import { - BANNER_NAME_LS_SURVEY, - IConfigurationService, - IExtensionContext, - IOutputChannel, - IPathUtils, - IPythonExtensionBanner, - Resource -} from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { PythonInterpreter } from '../../interpreter/contracts'; -import { ILanguageServerAnalysisOptions, ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; +import { HiddenFilePrefix } from '../../common/constants'; +import { IPythonExtensionBanner } from '../../common/types'; export class LanguageClientMiddleware implements Middleware { private connected = true; diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 136aa5c0ff3c..01b5e3bb4ccb 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -5,7 +5,7 @@ import '../../common/extensions'; import { inject, injectable, named } from 'inversify'; import { traceDecorators } from '../../common/logger'; -import { IDisposable, Resource, IPythonExtensionBanner, BANNER_NAME_LS_SURVEY } from '../../common/types'; +import { BANNER_NAME_LS_SURVEY, IDisposable, IPythonExtensionBanner, Resource } from '../../common/types'; import { debounceSync } from '../../common/utils/decorators'; import { PythonInterpreter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; From 97d05a83a543e089fcb99e730bb1db2248554fcd Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 16:34:48 -0800 Subject: [PATCH 07/26] Make jedi per interpreter as well --- src/client/activation/activationService.ts | 31 ++++---------- src/client/activation/jedi.ts | 35 +++++++++------- .../languageServices/jediProxyFactory.ts | 5 ++- src/client/providers/jediProxy.ts | 40 +++++++------------ 4 files changed, 44 insertions(+), 67 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index cb66ee7a97b7..b6825db09aa7 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -47,7 +47,6 @@ interface IActivatedServer { @injectable() export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable { private cache = new Map>(); - private jediServer: RefCountedLanguageServer | undefined; private activatedServer?: IActivatedServer; private readonly workspaceService: IWorkspaceService; private readonly output: OutputChannel; @@ -190,8 +189,6 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false }); jedi = true; } - } else if (this.jediServer) { - return this.jediServer; } await this.logStartup(jedi); @@ -210,23 +207,14 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv await server.activate(resource, interpreter); } - // Jedi is always a singleton. Don't need to create it more than once. - if (jedi) { - this.jediServer = new RefCountedLanguageServer(server, () => { - // When we remove the jedi server, remove it from the cache. - this.cache.delete(key); - }); - return this.jediServer; - } else { - return new RefCountedLanguageServer(server, () => { - // When we finally remove the last ref count, remove from the cache - this.cache.delete(key); + // Wrap the returned server in something that ref counts it. + return new RefCountedLanguageServer(server, () => { + // When we finally remove the last ref count, remove from the cache + this.cache.delete(key); - // For non jedi, actually dispose of the language server so the .net process - // shuts down - server.dispose(); - }); - } + // Dispose of the actual server. + server.dispose(); + }); } private async logStartup(isJedi: boolean): Promise { @@ -243,11 +231,6 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv if (workspacesUris.findIndex(uri => event.affectsConfiguration(`python.${jediEnabledSetting}`, uri)) === -1) { return; } - const jedi = this.useJedi(); - if (jedi && this.jediServer) { - return; - } - const item = await this.appShell.showInformationMessage( 'Please reload the window switching between language engines.', 'Reload' diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index 5ec091b546c8..acdaabea90a3 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { inject, injectable } from 'inversify'; import { CancellationToken, @@ -26,7 +25,7 @@ import { } from 'vscode'; import { PYTHON } from '../common/constants'; -import { IConfigurationService, IExtensionContext, ILogger, Resource } from '../common/types'; +import { IConfigurationService, IDisposable, IExtensionContext, ILogger, Resource } from '../common/types'; import { IShebangCodeLensProvider, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { JediFactory } from '../languageServices/jediProxyFactory'; @@ -58,19 +57,19 @@ export class JediExtensionActivator implements ILanguageServerActivator { private codeLensProvider: IShebangCodeLensProvider | undefined; private symbolProvider: JediSymbolProvider | undefined; private signatureProvider: PythonSignatureProvider | undefined; + private registrations: IDisposable[] = []; constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { this.context = this.serviceManager.get(IExtensionContext); this.documentSelector = PYTHON; } - public async activate(_resource: Resource, _interpreter?: PythonInterpreter): Promise { + public async activate(_resource: Resource, interpreter: PythonInterpreter | undefined): Promise { if (this.jediFactory) { throw new Error('Jedi already started'); } const context = this.context; - - const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager)); + const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), interpreter, this.serviceManager)); context.subscriptions.push(jediFactory); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); @@ -82,24 +81,24 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.codeLensProvider = this.serviceManager.get(IShebangCodeLensProvider); context.subscriptions.push(jediFactory); - context.subscriptions.push( + this.registrations.push( languages.registerRenameProvider(this.documentSelector, this.renameProvider) ); - context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider)); - context.subscriptions.push( + this.registrations.push(languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider)); + this.registrations.push( languages.registerHoverProvider(this.documentSelector, this.hoverProvider) ); - context.subscriptions.push( + this.registrations.push( languages.registerReferenceProvider(this.documentSelector, this.referenceProvider) ); - context.subscriptions.push( + this.registrations.push( languages.registerCompletionItemProvider( this.documentSelector, this.completionProvider, '.' ) ); - context.subscriptions.push( + this.registrations.push( languages.registerCodeLensProvider( this.documentSelector, this.codeLensProvider @@ -112,7 +111,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { }); const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); if (onTypeTriggers) { - context.subscriptions.push( + this.registrations.push( languages.registerOnTypeFormattingEditProvider( PYTHON, onTypeDispatcher, @@ -126,11 +125,11 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); this.signatureProvider = new PythonSignatureProvider(jediFactory); context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); + this.registrations.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push( + this.registrations.push( languages.registerSignatureHelpProvider( this.documentSelector, this.signatureProvider, @@ -140,7 +139,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { ); } - context.subscriptions.push( + this.registrations.push( languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer)) ); @@ -150,6 +149,11 @@ export class JediExtensionActivator implements ILanguageServerActivator { .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); } + public disconnect() { + this.registrations.forEach(r => r.dispose()); + this.registrations = []; + } + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { if (this.renameProvider) { return this.renameProvider.provideRenameEdits(document, position, newName, token); @@ -195,6 +199,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { } public dispose(): void { + this.registrations.forEach(r => r.dispose()); if (this.jediFactory) { this.jediFactory.dispose(); } diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts index 5e18b2396e51..2e735768050a 100644 --- a/src/client/languageServices/jediProxyFactory.ts +++ b/src/client/languageServices/jediProxyFactory.ts @@ -1,12 +1,13 @@ import { Disposable, Uri, workspace } from 'vscode'; import { IServiceContainer } from '../ioc/types'; import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; +import { PythonInterpreter } from '../interpreter/contracts'; export class JediFactory implements Disposable { private disposables: Disposable[]; private jediProxyHandlers: Map>; - constructor(private extensionRootPath: string, private serviceContainer: IServiceContainer) { + constructor(private extensionRootPath: string, private interpreter: PythonInterpreter | undefined, private serviceContainer: IServiceContainer) { this.disposables = []; this.jediProxyHandlers = new Map>(); } @@ -27,7 +28,7 @@ export class JediFactory implements Disposable { } if (!this.jediProxyHandlers.has(workspacePath)) { - const jediProxy = new JediProxy(this.extensionRootPath, workspacePath, this.serviceContainer); + const jediProxy = new JediProxy(this.extensionRootPath, workspacePath, this.interpreter, this.serviceContainer); const jediProxyHandler = new JediProxyHandler(jediProxy); this.disposables.push(jediProxy, jediProxyHandler); this.jediProxyHandlers.set(workspacePath, jediProxyHandler); diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index eea57fd58080..19a55328f85a 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -17,7 +17,7 @@ import { createDeferred, Deferred } from '../common/utils/async'; import { swallowExceptions } from '../common/utils/decorators'; import { StopWatch } from '../common/utils/stopWatch'; import { IEnvironmentVariablesProvider } from '../common/variables/types'; -import { IInterpreterService } from '../interpreter/contracts'; +import { PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; @@ -155,15 +155,12 @@ export class JediProxy implements Disposable { private readonly disposables: Disposable[] = []; private timer?: NodeJS.Timer | number; - public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) { + public constructor(private extensionRootDir: string, workspacePath: string, interpreter: PythonInterpreter | undefined, private serviceContainer: IServiceContainer) { this.workspacePath = workspacePath; const configurationService = serviceContainer.get(IConfigurationService); this.pythonSettings = configurationService.getSettings(Uri.file(workspacePath)); - this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; + this.lastKnownPythonInterpreter = interpreter ? interpreter.path : this.pythonSettings.pythonPath; this.logger = serviceContainer.get(ILogger); - const interpreterService = serviceContainer.get(IInterpreterService); - const disposable = interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)); - this.disposables.push(disposable); this.initialized = createDeferred(); this.startLanguageServer() .then(() => this.initialized.resolve()) @@ -306,15 +303,6 @@ export class JediProxy implements Disposable { return deferred.promise; } - @swallowExceptions('JediProxy') - private async onDidChangeInterpreter() { - if (this.lastKnownPythonInterpreter === this.pythonSettings.pythonPath) { - return; - } - this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; - this.additionalAutoCompletePaths = await this.buildAutoCompletePaths(); - this.restartLanguageServer().ignoreErrors(); - } // @debounce(1500) @swallowExceptions('JediProxy') private async environmentVariablesChangeHandler() { @@ -355,7 +343,7 @@ export class JediProxy implements Disposable { this.proc.kill(); } // tslint:disable-next-line:no-empty - } catch (ex) {} + } catch (ex) { } this.proc = undefined; } @@ -369,7 +357,7 @@ export class JediProxy implements Disposable { this.languageServerStarted.reject(new Error('Language Server not started.')); } this.languageServerStarted = createDeferred(); - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath), pythonPath: this.lastKnownPythonInterpreter }); // Check if the python path is valid. if ((await pythonProcess.getExecutablePath().catch(() => '')).length === 0) { return; @@ -646,7 +634,7 @@ export class JediProxy implements Disposable { private async getPathFromPythonCommand(args: string[]): Promise { try { - const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath), pythonPath: this.lastKnownPythonInterpreter }); const result = await pythonProcess.exec(args, { cwd: this.workspacePath }); const lines = result.stdout.trim().splitLines(); if (lines.length === 0) { @@ -705,14 +693,14 @@ export class JediProxy implements Disposable { // Add support for paths relative to workspace. const extraPaths = this.pythonSettings.autoComplete ? this.pythonSettings.autoComplete.extraPaths.map(extraPath => { - if (path.isAbsolute(extraPath)) { - return extraPath; - } - if (typeof this.workspacePath !== 'string') { - return ''; - } - return path.join(this.workspacePath, extraPath); - }) + if (path.isAbsolute(extraPath)) { + return extraPath; + } + if (typeof this.workspacePath !== 'string') { + return ''; + } + return path.join(this.workspacePath, extraPath); + }) : []; // Always add workspace path into extra paths. From 0cbeacd321a976bbc200955a14f069ef1da6d408 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Mon, 25 Nov 2019 17:19:29 -0800 Subject: [PATCH 08/26] Fix up some static command registrations --- src/client/activation/activationService.ts | 25 ++++++---- src/client/activation/jedi.ts | 23 ++++++--- .../intellisense/intellisenseProvider.ts | 48 ++++++++++--------- .../providers/objectDefinitionProvider.ts | 6 --- 4 files changed, 58 insertions(+), 44 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index b6825db09aa7..897649b3f0f9 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -54,7 +54,6 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; private readonly interpreterService: IInterpreterService; private resource!: Resource; - private interpreter: PythonInterpreter | undefined; constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, @@ -79,17 +78,25 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv public async activate(resource: Resource): Promise { // Get a new server and dispose of the old one (might be the same one) this.resource = resource; - this.interpreter = await this.interpreterService.getActiveInterpreter(resource); - const key = await this.getKey(resource, this.interpreter); - const result = await this.get(resource, this.interpreter); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const key = await this.getKey(resource, interpreter); + + // If we have an old server with a different key, then disconnect it as the + // creation of the new server may fail if this server is still connected + if (this.activatedServer && this.activatedServer.key !== key && this.activatedServer.server.disconnect) { + this.activatedServer.server.disconnect(); + } + + // Get the new item + const result = await this.get(resource, interpreter); + + // Now we dispose. This ensures the object stays alive if it's the same object because + // we dispose after we increment the ref count. if (this.activatedServer) { - // The activatedServer is the one handling intellisense requests for the entire ide, deactivate it if different. - // However it should still be able to handle direct requests (like for DS windows) - if (this.activatedServer.key !== key && this.activatedServer.server.disconnect) { - this.activatedServer.server.disconnect(); - } this.activatedServer.server.dispose(); } + + // Save our active server. this.activatedServer = { key, server: result }; } diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index acdaabea90a3..b9fa1af16930 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'; import { CancellationToken, CodeLens, + commands, CompletionContext, CompletionItem, CompletionList, @@ -24,7 +25,7 @@ import { WorkspaceEdit } from 'vscode'; -import { PYTHON } from '../common/constants'; +import { PYTHON, Commands } from '../common/constants'; import { IConfigurationService, IDisposable, IExtensionContext, ILogger, Resource } from '../common/types'; import { IShebangCodeLensProvider, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer, IServiceManager } from '../ioc/types'; @@ -32,7 +33,7 @@ import { JediFactory } from '../languageServices/jediProxyFactory'; import { PythonCompletionItemProvider } from '../providers/completionProvider'; import { PythonDefinitionProvider } from '../providers/definitionProvider'; import { PythonHoverProvider } from '../providers/hoverProvider'; -import { activateGoToObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; +import { PythonObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; import { PythonReferenceProvider } from '../providers/referenceProvider'; import { PythonRenameProvider } from '../providers/renameProvider'; import { PythonSignatureProvider } from '../providers/signatureProvider'; @@ -46,6 +47,7 @@ import { ILanguageServerActivator } from './types'; @injectable() export class JediExtensionActivator implements ILanguageServerActivator { + private static workspaceSymbols: WorkspaceSymbols | undefined; private readonly context: IExtensionContext; private jediFactory?: JediFactory; private readonly documentSelector: DocumentFilter[]; @@ -58,6 +60,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { private symbolProvider: JediSymbolProvider | undefined; private signatureProvider: PythonSignatureProvider | undefined; private registrations: IDisposable[] = []; + private objectDefinitionProvider: PythonObjectDefinitionProvider | undefined; constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { this.context = this.serviceManager.get(IExtensionContext); @@ -71,7 +74,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { const context = this.context; const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), interpreter, this.serviceManager)); context.subscriptions.push(jediFactory); - context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); + const serviceContainer = this.serviceManager.get(IServiceContainer); this.renameProvider = new PythonRenameProvider(this.serviceManager); this.definitionProvider = new PythonDefinitionProvider(jediFactory); @@ -79,8 +82,18 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.referenceProvider = new PythonReferenceProvider(jediFactory); this.completionProvider = new PythonCompletionItemProvider(jediFactory, this.serviceManager); this.codeLensProvider = this.serviceManager.get(IShebangCodeLensProvider); + this.objectDefinitionProvider = new PythonObjectDefinitionProvider(jediFactory); + + if (!JediExtensionActivator.workspaceSymbols) { + // Workspace symbols is static because it doesn't rely on the jediFactory. + JediExtensionActivator.workspaceSymbols = new WorkspaceSymbols(serviceContainer); + context.subscriptions.push(JediExtensionActivator.workspaceSymbols); + } + + // Make sure commands are in the registration list that gets disposed when the language server is disconnected from the + // IDE. + this.registrations.push(commands.registerCommand('python.goToPythonObject', () => this.objectDefinitionProvider!.goToObjectDefinition())); - context.subscriptions.push(jediFactory); this.registrations.push( languages.registerRenameProvider(this.documentSelector, this.renameProvider) ); @@ -121,10 +134,8 @@ export class JediExtensionActivator implements ILanguageServerActivator { ); } - const serviceContainer = this.serviceManager.get(IServiceContainer); this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); this.signatureProvider = new PythonSignatureProvider(jediFactory); - context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); this.registrations.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 737f9e0bb39b..ffa0e960d193 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -20,7 +20,7 @@ import { import { ILanguageServer, ILanguageServerCache } from '../../../activation/types'; import { IWorkspaceService } from '../../../common/application/types'; import { CancellationError } from '../../../common/cancellation'; -import { traceWarning } from '../../../common/logger'; +import { traceWarning, traceError } from '../../../common/logger'; import { IFileSystem, TemporaryFile } from '../../../common/platform/types'; import { Resource } from '../../../common/types'; import { createDeferred, Deferred, waitForPromise } from '../../../common/utils/async'; @@ -151,7 +151,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { } } - protected async getLanguageServer(): Promise { + protected async getLanguageServer(): Promise { // Resource should be our potential resource if its set. Otherwise workspace root const resource = this.potentialResource || (this.workspaceService.rootPath ? Uri.parse(this.workspaceService.rootPath) : undefined); @@ -165,30 +165,32 @@ export class IntellisenseProvider implements IInteractiveWindowListener { this.interpreter = interpreter; // Get an instance of the language server (so we ref count it ) - const languageServer = await this.languageServerCache.get(resource, interpreter); - - // Dispose of our old language service - this.languageServer?.dispose(); - - // This new language server does not know about our document, so tell it. - const document = await this.getDocument(); - if (document && languageServer.handleOpen && languageServer.handleChanges) { - // If we already sent an open document, that means we need to send both the open and - // the new changes - if (this.sentOpenDocument) { - languageServer.handleOpen(document); - languageServer.handleChanges(document, document.getFullContentChanges()); - } else { - this.sentOpenDocument = true; - languageServer.handleOpen(document); + try { + const languageServer = await this.languageServerCache.get(resource, interpreter); + + // Dispose of our old language service + this.languageServer?.dispose(); + + // This new language server does not know about our document, so tell it. + const document = await this.getDocument(); + if (document && languageServer.handleOpen && languageServer.handleChanges) { + // If we already sent an open document, that means we need to send both the open and + // the new changes + if (this.sentOpenDocument) { + languageServer.handleOpen(document); + languageServer.handleChanges(document, document.getFullContentChanges()); + } else { + this.sentOpenDocument = true; + languageServer.handleOpen(document); + } } - } - // Save the ref. - this.languageServer = languageServer; + // Save the ref. + this.languageServer = languageServer; + } catch (e) { + traceError(e); + } } - - // Use the resource and the interpreter to get our language server return this.languageServer; } diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts index d284e05ffb5c..2c6b01732700 100644 --- a/src/client/providers/objectDefinitionProvider.ts +++ b/src/client/providers/objectDefinitionProvider.ts @@ -85,9 +85,3 @@ export class PythonObjectDefinitionProvider { return vscode.window.showInputBox({ prompt: 'Enter Object Path', validateInput: this.intputValidation }); } } - -export function activateGoToObjectDefinitionProvider(jediFactory: JediFactory): vscode.Disposable[] { - const def = new PythonObjectDefinitionProvider(jediFactory); - const commandRegistration = vscode.commands.registerCommand('python.goToPythonObject', () => def.goToObjectDefinition()); - return [def, commandRegistration] as vscode.Disposable[]; -} From 8df275cb21d8d9668d0e428f030d4e8cf25e3b89 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 09:00:25 -0800 Subject: [PATCH 09/26] Get all the unit tests to pass --- src/client/activation/activationService.ts | 12 ++-- src/client/activation/jedi.ts | 2 +- .../activation/refCountedLanguageServer.ts | 8 ++- .../intellisense/intellisenseProvider.ts | 2 +- .../activation/activationManager.unit.test.ts | 2 +- .../activation/activationService.unit.test.ts | 55 +++++++++++++++---- .../analysisOptions.unit.test.ts | 17 ------ .../languageServer/downloader.unit.test.ts | 2 +- .../languageServerPackageService.unit.test.ts | 2 +- .../languageServer/platformData.unit.test.ts | 2 +- 10 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 897649b3f0f9..856581640346 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -42,6 +42,7 @@ const workspacePathNameForGlobalWorkspaces = ''; interface IActivatedServer { key: string; server: ILanguageServer; + jedi: boolean; } @injectable() @@ -97,10 +98,10 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv } // Save our active server. - this.activatedServer = { key, server: result }; + this.activatedServer = { key, server: result, jedi: result.type === LanguageServerActivator.Jedi }; } - public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { + public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { // See if we already have it or not const key = await this.getKey(resource, interpreter); let result: Promise | undefined = this.cache.get(key); @@ -207,7 +208,6 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv if (jedi) { throw ex; } - jedi = true; await this.logStartup(jedi); serverName = LanguageServerActivator.Jedi; server = this.serviceContainer.get(ILanguageServerActivator, serverName); @@ -215,7 +215,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv } // Wrap the returned server in something that ref counts it. - return new RefCountedLanguageServer(server, () => { + return new RefCountedLanguageServer(server, serverName, () => { // When we finally remove the last ref count, remove from the cache this.cache.delete(key); @@ -238,6 +238,10 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv if (workspacesUris.findIndex(uri => event.affectsConfiguration(`python.${jediEnabledSetting}`, uri)) === -1) { return; } + const jedi = this.useJedi(); + if (this.activatedServer && this.activatedServer.jedi === jedi) { + return; + } const item = await this.appShell.showInformationMessage( 'Please reload the window switching between language engines.', 'Reload' diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index b9fa1af16930..74764d3728f4 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -25,7 +25,7 @@ import { WorkspaceEdit } from 'vscode'; -import { PYTHON, Commands } from '../common/constants'; +import { PYTHON } from '../common/constants'; import { IConfigurationService, IDisposable, IExtensionContext, ILogger, Resource } from '../common/types'; import { IShebangCodeLensProvider, PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer, IServiceManager } from '../ioc/types'; diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index fe043774dd1d..ac1f2030cd03 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -22,17 +22,21 @@ import { } from 'vscode'; import { noop } from '../common/utils/misc'; -import { ILanguageServer } from './types'; +import { ILanguageServer, LanguageServerActivator } from './types'; export class RefCountedLanguageServer implements ILanguageServer { private refCount = 1; - constructor(private impl: ILanguageServer, private disposeCallback: () => void) { + constructor(private impl: ILanguageServer, private _type: LanguageServerActivator, private disposeCallback: () => void) { } public increment = () => { this.refCount += 1; } + public get type() { + return this._type; + } + public dispose() { this.refCount = Math.max(0, this.refCount - 1); if (this.refCount === 0) { diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index ffa0e960d193..bd1f58e5bd66 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -20,7 +20,7 @@ import { import { ILanguageServer, ILanguageServerCache } from '../../../activation/types'; import { IWorkspaceService } from '../../../common/application/types'; import { CancellationError } from '../../../common/cancellation'; -import { traceWarning, traceError } from '../../../common/logger'; +import { traceError, traceWarning } from '../../../common/logger'; import { IFileSystem, TemporaryFile } from '../../../common/platform/types'; import { Resource } from '../../../common/types'; import { createDeferred, Deferred, waitForPromise } from '../../../common/utils/async'; diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 2d67ef90ff09..434798bf14c9 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -21,7 +21,7 @@ import { InterpreterService } from '../../client/interpreter/interpreterService' import { sleep } from '../core'; // tslint:disable:max-func-body-length no-any -suite('Activation - ActivationManager', () => { +suite('Language Server Activation - ActivationManager', () => { class ExtensionActivationManagerTest extends ExtensionActivationManager { // tslint:disable-next-line:no-unnecessary-override public addHandlers() { diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 1f6acff5892f..4c2b7eb54508 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -1,14 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - import { expect } from 'chai'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { ConfigurationChangeEvent, Disposable, Uri, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationChangeEvent, Disposable, EventEmitter, Uri, WorkspaceConfiguration } from 'vscode'; + import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; import { FolderVersionPair, @@ -22,12 +18,23 @@ import { IDiagnostic, IDiagnosticsService } from '../../client/application/diagn import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; import { LSControl, LSEnabled } from '../../client/common/experimentGroups'; import { IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposable, IDisposableRegistry, IExperimentsManager, IOutputChannel, IPersistentState, IPersistentStateFactory, IPythonSettings, Resource } from '../../client/common/types'; +import { + IConfigurationService, + IDisposable, + IDisposableRegistry, + IExperimentsManager, + IOutputChannel, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Resource +} from '../../client/common/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; -// tslint:disable:no-any +// tslint:disable:max-func-body-length no-any -suite('Activation - ActivationService', () => { +suite('Language Server Activation - ActivationService', () => { [true, false].forEach(jediIsEnabled => { suite(`Test activation - ${jediIsEnabled ? 'Jedi is enabled' : 'Jedi is disabled'}`, () => { let serviceContainer: TypeMoq.IMock; @@ -41,6 +48,7 @@ suite('Activation - ActivationService', () => { let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -61,6 +69,9 @@ suite('Activation - ActivationService', () => { workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); workspaceService.setup(w => w.workspaceFolders).returns(() => []); configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + interpreterService = TypeMoq.Mock.ofType(); + const e = new EventEmitter(); + interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => e.event); langFolderServiceMock .setup(l => l.getCurrentLanguageServerDirectory()) .returns(() => Promise.resolve(folderVer)); @@ -93,6 +104,9 @@ suite('Activation - ActivationService', () => { serviceContainer .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) .returns(() => platformService.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); @@ -451,7 +465,7 @@ suite('Activation - ActivationService', () => { activator3 .setup(d => d.dispose()) .verifiable(TypeMoq.Times.once()); - workspaceFoldersChangedHandler.call(activationService); + await workspaceFoldersChangedHandler.call(activationService); workspaceService.verifyAll(); activator3.verifyAll(); }); @@ -514,6 +528,7 @@ suite('Activation - ActivationService', () => { let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -525,6 +540,9 @@ suite('Activation - ActivationService', () => { const configService = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); experiments = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + const e = new EventEmitter(); + interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => e.event); const langFolderServiceMock = TypeMoq.Mock.ofType(); const folderVer: FolderVersionPair = { path: '', @@ -563,6 +581,9 @@ suite('Activation - ActivationService', () => { serviceContainer .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) .returns(() => platformService.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); @@ -650,6 +671,7 @@ suite('Activation - ActivationService', () => { let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -661,6 +683,9 @@ suite('Activation - ActivationService', () => { const configService = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); experiments = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + const e = new EventEmitter(); + interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => e.event); const langFolderServiceMock = TypeMoq.Mock.ofType(); const folderVer: FolderVersionPair = { path: '', @@ -699,6 +724,9 @@ suite('Activation - ActivationService', () => { serviceContainer .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) .returns(() => platformService.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); @@ -819,6 +847,7 @@ suite('Activation - ActivationService', () => { let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -830,6 +859,9 @@ suite('Activation - ActivationService', () => { const configService = TypeMoq.Mock.ofType(); pythonSettings = TypeMoq.Mock.ofType(); experiments = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + const e = new EventEmitter(); + interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => e.event); const langFolderServiceMock = TypeMoq.Mock.ofType(); const folderVer: FolderVersionPair = { path: '', @@ -868,6 +900,9 @@ suite('Activation - ActivationService', () => { serviceContainer .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) .returns(() => platformService.object); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) .returns(() => langFolderServiceMock.object); diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts index ed60cf7f1984..acaf54141818 100644 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ b/src/test/activation/languageServer/analysisOptions.unit.test.ts @@ -23,8 +23,6 @@ import { } from '../../../client/common/types'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { sleep } from '../../core'; // tslint:disable:no-unnecessary-override no-any chai-vague-errors no-unused-expression max-func-body-length @@ -58,7 +56,6 @@ suite('Language Server - Analysis Options', () => { let envVarsProvider: IEnvironmentVariablesProvider; let configurationService: IConfigurationService; let workspace: IWorkspaceService; - let interpreterService: IInterpreterService; let outputChannel: IOutputChannel; let lsOutputChannel: typemoq.IMock; let pathUtils: IPathUtils; @@ -68,7 +65,6 @@ suite('Language Server - Analysis Options', () => { envVarsProvider = mock(EnvironmentVariablesProvider); configurationService = mock(ConfigurationService); workspace = mock(WorkspaceService); - interpreterService = mock(InterpreterService); outputChannel = typemoq.Mock.ofType().object; lsOutputChannel = typemoq.Mock.ofType(); lsOutputChannel @@ -84,43 +80,34 @@ suite('Language Server - Analysis Options', () => { }); test('Initialize will add event handlers and will dispose them when running dispose', async () => { const disposable1 = typemoq.Mock.ofType(); - const disposable2 = typemoq.Mock.ofType(); const disposable3 = typemoq.Mock.ofType(); when(workspace.onDidChangeConfiguration).thenReturn(() => disposable1.object); - when(interpreterService.onDidChangeInterpreter).thenReturn(() => disposable2.object); when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); await analysisOptions.initialize(undefined, undefined); verify(workspace.onDidChangeConfiguration).once(); - verify(interpreterService.onDidChangeInterpreter).once(); verify(envVarsProvider.onDidEnvironmentVariablesChange).once(); disposable1.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); disposable3.setup(d => d.dispose()).verifiable(typemoq.Times.once()); analysisOptions.dispose(); disposable1.verifyAll(); - disposable2.verifyAll(); disposable3.verifyAll(); }); test('Changes to settings or interpreter will be debounced', async () => { const disposable1 = typemoq.Mock.ofType(); - const disposable2 = typemoq.Mock.ofType(); const disposable3 = typemoq.Mock.ofType(); let configChangedHandler!: Function; - let interpreterChangedHandler!: Function; when(workspace.onDidChangeConfiguration).thenReturn(cb => { configChangedHandler = cb; return disposable1.object; }); - when(interpreterService.onDidChangeInterpreter).thenReturn(cb => { interpreterChangedHandler = cb; return disposable2.object; }); when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); let settingsChangedInvokedCount = 0; analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); await analysisOptions.initialize(undefined, undefined); expect(configChangedHandler).to.not.be.undefined; - expect(interpreterChangedHandler).to.not.be.undefined; for (let i = 0; i < 100; i += 1) { configChangedHandler.call(analysisOptions); @@ -179,20 +166,16 @@ suite('Language Server - Analysis Options', () => { test('Changes to settings will be filtered to current resource', async () => { const uri = Uri.file(__filename); const disposable1 = typemoq.Mock.ofType(); - const disposable2 = typemoq.Mock.ofType(); const disposable3 = typemoq.Mock.ofType(); let configChangedHandler!: Function; - let interpreterChangedHandler!: Function; let envVarChangedHandler!: Function; when(workspace.onDidChangeConfiguration).thenReturn(cb => { configChangedHandler = cb; return disposable1.object; }); - when(interpreterService.onDidChangeInterpreter).thenReturn(cb => { interpreterChangedHandler = cb; return disposable2.object; }); when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(cb => { envVarChangedHandler = cb; return disposable3.object; }); let settingsChangedInvokedCount = 0; analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); await analysisOptions.initialize(uri, undefined); expect(configChangedHandler).to.not.be.undefined; - expect(interpreterChangedHandler).to.not.be.undefined; expect(envVarChangedHandler).to.not.be.undefined; for (let i = 0; i < 100; i += 1) { diff --git a/src/test/activation/languageServer/downloader.unit.test.ts b/src/test/activation/languageServer/downloader.unit.test.ts index ac3aa6b0625c..f4483f45edbd 100644 --- a/src/test/activation/languageServer/downloader.unit.test.ts +++ b/src/test/activation/languageServer/downloader.unit.test.ts @@ -29,7 +29,7 @@ import { MockOutputChannel } from '../../mockClasses'; use(chaiAsPromised); // tslint:disable-next-line:max-func-body-length -suite('Activation - Downloader', () => { +suite('Language Server Activation - Downloader', () => { let languageServerDownloader: LanguageServerDownloader; let folderService: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; diff --git a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts b/src/test/activation/languageServer/languageServerPackageService.unit.test.ts index c246adcfdd03..c329c9454355 100644 --- a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts +++ b/src/test/activation/languageServer/languageServerPackageService.unit.test.ts @@ -21,7 +21,7 @@ import { IServiceContainer } from '../../../client/ioc/types'; const downloadBaseFileName = 'Python-Language-Server'; -suite('Language', () => { +suite('Language Server - Package Service', () => { let serviceContainer: typeMoq.IMock; let platform: typeMoq.IMock; let lsPackageService: LanguageServerPackageService; diff --git a/src/test/activation/languageServer/platformData.unit.test.ts b/src/test/activation/languageServer/platformData.unit.test.ts index c813d8b965be..8206c428075d 100644 --- a/src/test/activation/languageServer/platformData.unit.test.ts +++ b/src/test/activation/languageServer/platformData.unit.test.ts @@ -32,7 +32,7 @@ const testDataModuleName = [ ]; // tslint:disable-next-line:max-func-body-length -suite('Activation - platform data', () => { +suite('Language Server Activation - platform data', () => { test('Name and hash (Windows/Mac)', async () => { for (const t of testDataWinMac) { const platformService = TypeMoq.Mock.ofType(); From 62f9f9fe7c0a607cb98c279d80b1ac2ebb5f4dc6 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 10:52:28 -0800 Subject: [PATCH 10/26] Fix unit tests for interpreter changed --- .../activation/activationService.unit.test.ts | 127 +++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 4c2b7eb54508..7f6b2ed1d59a 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -29,8 +29,10 @@ import { IPythonSettings, Resource } from '../../client/common/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IInterpreterService, PythonInterpreter, InterpreterType } from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { noop } from '../../client/common/utils/misc'; // tslint:disable:max-func-body-length no-any @@ -49,6 +51,7 @@ suite('Language Server Activation - ActivationService', () => { let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; + let interpreterChangedHandler!: Function; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -70,8 +73,11 @@ suite('Language Server Activation - ActivationService', () => { workspaceService.setup(w => w.workspaceFolders).returns(() => []); configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); interpreterService = TypeMoq.Mock.ofType(); - const e = new EventEmitter(); - interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => e.event); + const disposable = TypeMoq.Mock.ofType(); + interpreterService.setup(i => i.onDidChangeInterpreter(TypeMoq.It.isAny())).returns((cb) => { + interpreterChangedHandler = cb; + return disposable.object; + }); langFolderServiceMock .setup(l => l.getCurrentLanguageServerDirectory()) .returns(() => Promise.resolve(folderVer)); @@ -342,6 +348,121 @@ suite('Language Server Activation - ActivationService', () => { appShell.verifyAll(); cmdManager.verifyAll(); }); + test('More than one LS is created for multiple interpreters', async () => { + const interpreter1: PythonInterpreter = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const interpreter2: PythonInterpreter = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType(); + activator + .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.dispose()).returns(noop).verifiable(TypeMoq.Times.exactly(2)); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + let diagnostics: IDiagnostic[]; + if (!jediIsEnabled) { + diagnostics = [TypeMoq.It.isAny()]; + } else { + diagnostics = []; + } + lsNotSupportedDiagnosticService + .setup(l => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup(l => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); + const ls1 = await activationService.get(folder1.uri, interpreter1); + const ls2 = await activationService.get(folder1.uri, interpreter2); + expect(ls1).not.to.be.equal(ls2, 'Interpreter does not create new LS'); + const ls3 = await activationService.get(undefined, interpreter1); + expect(ls1).to.be.equal(ls3, 'Interpreter does return same LS'); + ls3.dispose(); + ls1.dispose(); + ls2.dispose(); + activator.verifyAll(); + }); + test('Changing interpreter will activate a new LS', async () => { + const interpreter1: PythonInterpreter = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + const interpreter2: PythonInterpreter = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + type: InterpreterType.Unknown + }; + let getActiveCount = 0; + interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => { + if (getActiveCount <= 0) { + getActiveCount += 1; + return Promise.resolve(interpreter1); + } + getActiveCount += 1; + return Promise.resolve(interpreter2); + }); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType(); + activator + .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + let diagnostics: IDiagnostic[]; + if (!jediIsEnabled) { + diagnostics = [TypeMoq.It.isAny()]; + } else { + diagnostics = []; + } + lsNotSupportedDiagnosticService + .setup(l => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup(l => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + + pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); + const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); + await activationService.activate(folder1.uri); + await interpreterChangedHandler(); + activator.verifyAll(); + }); if (!jediIsEnabled) { test('Revert to jedi when LS activation fails', async () => { pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); From 2923d321b1ab33f6fe8e35401c63a99fe6768ab3 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 12:28:21 -0800 Subject: [PATCH 11/26] Add some functional tests --- .../activation/activationService.unit.test.ts | 6 ++-- .../datascience/dataScienceIocContainer.ts | 11 ++++++- .../intellisense.functional.test.tsx | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 7f6b2ed1d59a..d16a14157918 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -29,10 +29,10 @@ import { IPythonSettings, Resource } from '../../client/common/types'; -import { IInterpreterService, PythonInterpreter, InterpreterType } from '../../client/interpreter/contracts'; -import { IServiceContainer } from '../../client/ioc/types'; -import { Architecture } from '../../client/common/utils/platform'; import { noop } from '../../client/common/utils/misc'; +import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; // tslint:disable:max-func-body-length no-any diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 73e2aadcb065..d458ab0168e5 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -22,7 +22,14 @@ import { } from 'vscode'; import * as vsls from 'vsls/vscode'; -import { ILanguageServerAnalysisOptions, ILanguageServerProxy } from '../../client/activation/types'; +import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { LanguageServerManager } from '../../client/activation/languageServer/manager'; +import { + ILanguageServerAnalysisOptions, + ILanguageServerCache, + ILanguageServerManager, + ILanguageServerProxy +} from '../../client/activation/types'; import { TerminalManager } from '../../client/common/application/terminalManager'; import { IApplicationShell, @@ -397,6 +404,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(ITerminalManager, TerminalManager); this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); this.serviceManager.addSingleton(ILanguageServerProxy, MockLanguageServerProxy); + this.serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); + this.serviceManager.add(ILanguageServerManager, LanguageServerManager); this.serviceManager.addSingleton(ILanguageServerAnalysisOptions, MockLanguageServerAnalysisOptions); this.serviceManager.add(IInteractiveWindowListener, IntellisenseProvider); this.serviceManager.add(IInteractiveWindowListener, AutoSaveService); diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx index d50f3d1e1149..e4ef5c32b516 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -6,6 +6,7 @@ import { ReactWrapper } from 'enzyme'; import { IDisposable } from 'monaco-editor'; import { Disposable } from 'vscode'; +import { IConfigurationService } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; import { noop } from '../core'; @@ -108,6 +109,35 @@ suite('DataScience Intellisense tests', () => { verifyIntellisenseVisible(wrapper, 'print'); }, () => { return ioc; }); + runMountedTest('Multiple interpreters', async (wrapper) => { + // Create an interactive window so that it listens to the results. + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); + await interactiveWindow.show(); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + let suggestion = waitForSuggestion(wrapper); + typeCode(getInteractiveEditor(wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(wrapper, 'print'); + + // Clear the code + const editor = getInteractiveEditor(wrapper); + const inst = editor.instance() as MonacoEditor; + inst.state.model!.setValue(''); + + // Then change our current interpreter + const config = ioc.get(IConfigurationService); + await config.updateSetting('python.pythonPath', '/foo/bar/python'); + + // Type in again, make sure it works (should use the current interpreter in the server) + suggestion = waitForSuggestion(wrapper); + typeCode(getInteractiveEditor(wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(wrapper, 'print'); + }, () => { return ioc; }); + runMountedTest('Jupyter autocomplete', async (wrapper) => { if (ioc.mockJupyter) { // This test only works when mocking. From 9fffedafa3e5f37622748f1d2966bb1588f71308 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 12:47:06 -0800 Subject: [PATCH 12/26] Get functional tests to have the right services --- .../datascience/dataScienceIocContainer.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 6941fa20a377..d608c7720563 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -18,18 +18,38 @@ import { Uri, ViewColumn, WorkspaceConfiguration, - WorkspaceFolder + WorkspaceFolder, + WorkspaceFoldersChangeEvent } from 'vscode'; import * as vsls from 'vsls/vscode'; import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { + LanguageServerCompatibilityService +} from '../../client/activation/languageServer/languageServerCompatibilityService'; import { LanguageServerManager } from '../../client/activation/languageServer/manager'; import { ILanguageServerAnalysisOptions, ILanguageServerCache, + ILanguageServerCompatibilityService, ILanguageServerManager, ILanguageServerProxy } from '../../client/activation/types'; +import { + LSNotSupportedDiagnosticService, + LSNotSupportedDiagnosticServiceId +} from '../../client/application/diagnostics/checks/lsNotSupported'; +import { DiagnosticFilterService } from '../../client/application/diagnostics/filter'; +import { + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../client/application/diagnostics/promptHandler'; +import { + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../client/application/diagnostics/types'; import { TerminalManager } from '../../client/common/application/terminalManager'; import { IApplicationShell, @@ -49,6 +69,8 @@ import { WorkspaceService } from '../../client/common/application/workspace'; import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { DotNetCompatibilityService } from '../../client/common/dotnet/compatibilityService'; +import { IDotNetCompatibilityService } from '../../client/common/dotnet/types'; import { ExperimentsManager } from '../../client/common/experiments'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { IInstallationChannelManager } from '../../client/common/installer/types'; @@ -288,6 +310,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private shouldMockJupyter: boolean; private asyncRegistry: AsyncDisposableRegistry; private configChangeEvent = new EventEmitter(); + private worksaceFoldersChangedEvent = new EventEmitter(); private documentManager = new MockDocumentManager(); private workingPython: PythonInterpreter = { path: '/foo/bar/python.exe', @@ -425,6 +448,14 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); this.serviceManager.addSingleton(InterpreterFilter, InterpreterFilter); this.serviceManager.addSingleton(JupyterCommandFinder, JupyterCommandFinder); + this.serviceManager.addSingleton(IDiagnosticsService, LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId); + this.serviceManager.addSingleton(ILanguageServerCompatibilityService, LanguageServerCompatibilityService); + this.serviceManager.addSingleton>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId); + this.serviceManager.addSingleton(IDiagnosticFilterService, DiagnosticFilterService); + + // Don't check for dot net compatibility + const dotNetCompability = mock(DotNetCompatibilityService); + this.serviceManager.addSingletonInstance(IDotNetCompatibilityService, instance(dotNetCompability)); // Disable experiments. const experimentManager = mock(ExperimentsManager); @@ -487,6 +518,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { when(workspaceService.getConfiguration(anything())).thenReturn(instance(workspaceConfig)); when(workspaceService.getConfiguration(anything(), anything())).thenReturn(instance(workspaceConfig)); when(workspaceService.onDidChangeConfiguration).thenReturn(this.configChangeEvent.event); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(this.worksaceFoldersChangedEvent.event); interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); const startTime = Date.now(); datascience.setup(d => d.activationStartTime).returns(() => startTime); From 4221c1e0b185bcee7974bacbc8acfaae4db1ef2c Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 13:53:05 -0800 Subject: [PATCH 13/26] Fix other functional tests to pass --- .../jupyter/jupyterCommandFinder.ts | 4 +- .../datascience/jupyter/jupyterExecution.ts | 6 +-- .../datascience/dataScienceIocContainer.ts | 39 ++++++++++++++++--- .../intellisense.functional.test.tsx | 2 - src/test/datascience/mockJupyterManager.ts | 1 - 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/client/datascience/jupyter/jupyterCommandFinder.ts b/src/client/datascience/jupyter/jupyterCommandFinder.ts index a37bf2cdcc43..495b5054f9b1 100644 --- a/src/client/datascience/jupyter/jupyterCommandFinder.ts +++ b/src/client/datascience/jupyter/jupyterCommandFinder.ts @@ -265,7 +265,7 @@ export class JupyterCommandFinderImpl { found.error = firstError; } - if (found.status === ModuleExistsStatus.NotFound) { + if (found && found.status === ModuleExistsStatus.NotFound) { this.sendSearchTelemetry(command, 'nowhere', stopWatch.elapsedTime, cancelToken); } @@ -362,7 +362,7 @@ export class JupyterCommandFinderImpl { // Creating daemons for other interpreters might not be what we want. // E.g. users can have dozens of pipenv or conda environments. // In such cases, we'd end up creating n*3 python processes that are long lived. - if (!currentInterpreter || currentInterpreter.path !== interpreter.path){ + if (!currentInterpreter || currentInterpreter.path !== interpreter.path) { return pythonService!; } diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index 755a926a05ad..31553d34183f 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -251,7 +251,7 @@ export class JupyterExecutionBase implements IJupyterExecution { traceInfo(`Launching ${options ? options.purpose : 'unknown type of'} server`); const useDefaultConfig = options && options.useDefaultConfig ? true : false; const metadata = options?.metadata; - const launchResults = await this.startNotebookServer({useDefaultConfig, metadata}, cancelToken); + const launchResults = await this.startNotebookServer({ useDefaultConfig, metadata }, cancelToken); if (launchResults) { connection = launchResults.connection; kernelSpec = launchResults.kernelSpec; @@ -287,7 +287,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // tslint:disable-next-line: max-func-body-length @captureTelemetry(Telemetry.StartJupyter) - private async startNotebookServer(options: {useDefaultConfig: boolean; metadata?: nbformat.INotebookMetadata}, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { + private async startNotebookServer(options: { useDefaultConfig: boolean; metadata?: nbformat.INotebookMetadata }, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommand(JupyterCommands.NotebookCommand, cancelToken); this.checkNotebookCommand(notebookCommand); @@ -313,7 +313,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // See if we can find the command try { const result = await this.findBestCommand(command, cancelToken); - return result.command !== undefined; + return result && result.command !== undefined; } catch (err) { this.logger.logWarning(err); return false; diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index d608c7720563..d719cf057157 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; //tslint:disable:trailing-comma no-any import * as child_process from 'child_process'; import { ReactWrapper } from 'enzyme'; @@ -24,16 +23,27 @@ import { import * as vsls from 'vsls/vscode'; import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { LanguageServerExtensionActivator } from '../../client/activation/languageServer/activator'; +import { LanguageServerDownloader } from '../../client/activation/languageServer/downloader'; import { LanguageServerCompatibilityService } from '../../client/activation/languageServer/languageServerCompatibilityService'; +import { LanguageServerExtension } from '../../client/activation/languageServer/languageServerExtension'; +import { LanguageServerFolderService } from '../../client/activation/languageServer/languageServerFolderService'; +import { LanguageServerPackageService } from '../../client/activation/languageServer/languageServerPackageService'; import { LanguageServerManager } from '../../client/activation/languageServer/manager'; import { + ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCache, ILanguageServerCompatibilityService, + ILanguageServerDownloader, + ILanguageServerExtension, + ILanguageServerFolderService, ILanguageServerManager, - ILanguageServerProxy + ILanguageServerPackageService, + ILanguageServerProxy, + LanguageServerActivator } from '../../client/activation/types'; import { LSNotSupportedDiagnosticService, @@ -111,6 +121,7 @@ import { TerminalActivationProviders } from '../../client/common/terminal/types'; import { + BANNER_NAME_LS_SURVEY, IAsyncDisposableRegistry, IConfigurationService, ICurrentProcess, @@ -120,6 +131,7 @@ import { ILogger, IPathUtils, IPersistentStateFactory, + IPythonExtensionBanner, IsWindows } from '../../client/common/types'; import { Deferred, sleep } from '../../client/common/utils/async'; @@ -269,6 +281,7 @@ import { import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { LanguageServerSurveyBanner } from '../../client/languageServices/languageServerSurveyBanner'; import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; import { ICodeExecutionHelper } from '../../client/terminals/types'; import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; @@ -427,6 +440,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv); this.serviceManager.addSingleton(ITerminalManager, TerminalManager); this.serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); + this.serviceManager.add(ILanguageServerActivator, LanguageServerExtensionActivator, LanguageServerActivator.DotNet); + this.serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension); this.serviceManager.addSingleton(ILanguageServerProxy, MockLanguageServerProxy); this.serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService); this.serviceManager.add(ILanguageServerManager, LanguageServerManager); @@ -455,8 +470,22 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Don't check for dot net compatibility const dotNetCompability = mock(DotNetCompatibilityService); + when(dotNetCompability.isSupported()).thenResolve(true); this.serviceManager.addSingletonInstance(IDotNetCompatibilityService, instance(dotNetCompability)); + // Don't allow a banner to show up + const extensionBanner = mock(LanguageServerSurveyBanner); + this.serviceManager.addSingletonInstance(IPythonExtensionBanner, instance(extensionBanner), BANNER_NAME_LS_SURVEY); + + // Don't allow the download to happen + const downloader = mock(LanguageServerDownloader); + this.serviceManager.addSingletonInstance(ILanguageServerDownloader, instance(downloader)); + + const folderService = mock(LanguageServerFolderService); + const packageService = mock(LanguageServerPackageService); + this.serviceManager.addSingletonInstance(ILanguageServerFolderService, instance(folderService)); + this.serviceManager.addSingletonInstance(ILanguageServerPackageService, instance(packageService)); + // Disable experiments. const experimentManager = mock(ExperimentsManager); when(experimentManager.inExperiment(anything())).thenReturn(false); @@ -509,6 +538,8 @@ export class DataScienceIocContainer extends UnitTestIocContainer { runStartupCommands: '', debugJustMyCode: true }; + this.pythonSettings.jediEnabled = false; + this.pythonSettings.downloadLanguageServer = false; const workspaceConfig = this.mockedWorkspaceConfig = mock(MockWorkspaceConfiguration); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings); @@ -757,10 +788,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } } - public enableJedi(enabled: boolean) { - this.pythonSettings.jediEnabled = enabled; - } - public addInterpreter(newInterpreter: PythonInterpreter, commands: SupportedCommands) { if (this.mockJupyter) { this.mockJupyter.addInterpreter(newInterpreter, commands); diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx index e4ef5c32b516..18ed24aca9b8 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -21,8 +21,6 @@ suite('DataScience Intellisense tests', () => { setup(() => { ioc = new DataScienceIocContainer(); - // For this test, jedi is turned off so we use our mock language server - ioc.enableJedi(false); ioc.registerDataScienceTypes(); }); diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index 648bd80ffd85..e4943de7f90f 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -76,7 +76,6 @@ export class MockJupyterManager implements IJupyterSessionManager { // Setup our interpreter service this.interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => this.changedInterpreterEvent.event); - this.interpreterService.setup(i => i.getActiveInterpreter()).returns(() => Promise.resolve(this.activeInterpreter)); this.interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve(this.activeInterpreter)); this.interpreterService.setup(i => i.getInterpreters()).returns(() => Promise.resolve(this.installedInterpreters)); this.interpreterService.setup(i => i.getInterpreterDetails(TypeMoq.It.isAnyString())).returns((p) => { From 0aa62aa38c8e79aa46509f1a8a6dfbc4f9bf52a1 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 16:21:32 -0800 Subject: [PATCH 14/26] Get functional test working for having a different interpreter --- .../intellisense/intellisenseLine.ts | 47 +++ .../datascience/dataScienceIocContainer.ts | 9 + .../intellisense.functional.test.tsx | 17 +- ...erter.ts => mockCode2ProtocolConverter.ts} | 64 +++- src/test/datascience/mockJupyterManager.ts | 10 +- src/test/datascience/mockLanguageClient.ts | 46 ++- .../datascience/mockProtocol2CodeConverter.ts | 304 ++++++++++++++++++ .../datascience/notebook.functional.test.ts | 8 +- 8 files changed, 478 insertions(+), 27 deletions(-) create mode 100644 src/client/datascience/interactive-common/intellisense/intellisenseLine.ts rename src/test/datascience/{mockProtocolConverter.ts => mockCode2ProtocolConverter.ts} (66%) create mode 100644 src/test/datascience/mockProtocol2CodeConverter.ts diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts b/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts new file mode 100644 index 000000000000..1bdb78b33516 --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import { Position, Range, TextLine } from 'vscode'; + +export class IntellisenseLine implements TextLine { + + private _range: Range; + private _rangeWithLineBreak: Range; + private _firstNonWhitespaceIndex: number | undefined; + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + public get lineNumber(): number { + return this._line; + } + public get text(): string { + return this._contents; + } + public get range(): Range { + return this._range; + } + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index d719cf057157..d304f5bac1c4 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -333,6 +333,14 @@ export class DataScienceIocContainer extends UnitTestIocContainer { type: InterpreterType.Unknown, architecture: Architecture.x64, }; + private workingPython2: PythonInterpreter = { + path: '/foo/baz/python.exe', + version: new SemVer('3.6.7-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + type: InterpreterType.Unknown, + architecture: Architecture.x64, + }; private extraListeners: ((m: string, p: any) => void)[] = []; private webPanelProvider: TypeMoq.IMock | undefined; @@ -674,6 +682,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); + this.addInterpreter(this.workingPython2, SupportedCommands.all); this.addInterpreter(this.workingPython, SupportedCommands.all); } diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx index 18ed24aca9b8..2974f3326842 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -6,8 +6,8 @@ import { ReactWrapper } from 'enzyme'; import { IDisposable } from 'monaco-editor'; import { Disposable } from 'vscode'; -import { IConfigurationService } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; +import { IInterpreterService } from '../../client/interpreter/contracts'; import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; import { noop } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; @@ -125,8 +125,15 @@ suite('DataScience Intellisense tests', () => { inst.state.model!.setValue(''); // Then change our current interpreter - const config = ioc.get(IConfigurationService); - await config.updateSetting('python.pythonPath', '/foo/bar/python'); + const interpreterService = ioc.get(IInterpreterService); + const oldActive = await interpreterService.getActiveInterpreter(); + const interpreters = await interpreterService.getInterpreters(); + if (interpreters.length > 1 && oldActive) { + const firstOther = interpreters.filter(i => i.path !== oldActive.path); + ioc.forceSettingsChanged(firstOther[0].path); + const active = await interpreterService.getActiveInterpreter(); + assert.notDeepEqual(active, oldActive, 'Should have changed interpreter'); + } // Type in again, make sure it works (should use the current interpreter in the server) suggestion = waitForSuggestion(wrapper); @@ -134,6 +141,10 @@ suite('DataScience Intellisense tests', () => { await suggestion.promise; suggestion.disposable.dispose(); verifyIntellisenseVisible(wrapper, 'print'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + inst.state.model!.setValue(''); }, () => { return ioc; }); runMountedTest('Jupyter autocomplete', async (wrapper) => { diff --git a/src/test/datascience/mockProtocolConverter.ts b/src/test/datascience/mockCode2ProtocolConverter.ts similarity index 66% rename from src/test/datascience/mockProtocolConverter.ts rename to src/test/datascience/mockCode2ProtocolConverter.ts index bf8143ab8b25..e583890af9b6 100644 --- a/src/test/datascience/mockProtocolConverter.ts +++ b/src/test/datascience/mockCode2ProtocolConverter.ts @@ -6,23 +6,60 @@ import { Code2ProtocolConverter } from 'vscode-languageclient'; import * as proto from 'vscode-languageserver-protocol'; // tslint:disable:no-any unified-signatures -export class MockProtocolConverter implements Code2ProtocolConverter { +export class MockCode2ProtocolConverter implements Code2ProtocolConverter { public asUri(_uri: code.Uri): string { throw new Error('Method not implemented.'); } - public asTextDocumentIdentifier(_textDocument: code.TextDocument): proto.TextDocumentIdentifier { - throw new Error('Method not implemented.'); + public asTextDocumentIdentifier(textDocument: code.TextDocument): proto.TextDocumentIdentifier { + return { uri: textDocument.uri.toString() }; } - public asVersionedTextDocumentIdentifier(_textDocument: code.TextDocument): proto.VersionedTextDocumentIdentifier { - throw new Error('Method not implemented.'); + public asVersionedTextDocumentIdentifier(textDocument: code.TextDocument): proto.VersionedTextDocumentIdentifier { + return { uri: textDocument.uri.toString(), version: textDocument.version }; } - public asOpenTextDocumentParams(_textDocument: code.TextDocument): proto.DidOpenTextDocumentParams { - throw new Error('Method not implemented.'); + public asOpenTextDocumentParams(textDocument: code.TextDocument): proto.DidOpenTextDocumentParams { + return { + textDocument: { + uri: textDocument.uri.toString(), + languageId: 'PYTHON', + version: textDocument.version, + text: textDocument.getText() + } + }; } + public asChangeTextDocumentParams(textDocument: code.TextDocument): proto.DidChangeTextDocumentParams; public asChangeTextDocumentParams(event: code.TextDocumentChangeEvent): proto.DidChangeTextDocumentParams; - public asChangeTextDocumentParams(_event: any): proto.DidChangeTextDocumentParams { - throw new Error('Method not implemented.'); + public asChangeTextDocumentParams(arg: any): proto.DidChangeTextDocumentParams { + if (this.isTextDocument(arg)) { + return { + textDocument: { + uri: arg.uri.toString(), + version: arg.version + }, + contentChanges: [{ text: arg.getText() }] + }; + } else if (this.isTextDocumentChangeEvent(arg)) { + const document = arg.document; + return { + textDocument: { + uri: document.uri.toString(), + version: document.version + }, + contentChanges: arg.contentChanges.map((change): proto.TextDocumentContentChangeEvent => { + const range = change.range; + return { + range: { + start: { line: range.start.line, character: range.start.character }, + end: { line: range.end.line, character: range.end.character } + }, + rangeLength: change.rangeLength, + text: change.text + }; + }) + }; + } else { + throw Error('Unsupported text document change parameter'); + } } public asCloseTextDocumentParams(_textDocument: code.TextDocument): proto.DidCloseTextDocumentParams { throw new Error('Method not implemented.'); @@ -119,4 +156,13 @@ export class MockProtocolConverter implements Code2ProtocolConverter { public asDocumentLinkParams(_textDocument: code.TextDocument): proto.DocumentLinkParams { throw new Error('Method not implemented.'); } + private isTextDocumentChangeEvent(value: any): value is code.TextDocumentChangeEvent { + const candidate = value; + return !!candidate.document && !!candidate.contentChanges; + } + + private isTextDocument(value: any): value is code.TextDocument { + const candidate = value; + return !!candidate.uri && !!candidate.version; + } } diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index e4943de7f90f..677c812e984c 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -88,7 +88,7 @@ export class MockJupyterManager implements IJupyterSessionManager { // Listen to configuration changes like the real interpreter service does so that we fire our settings changed event const configService = serviceManager.get(IConfigurationService); if (configService && configService !== null) { - configService.getSettings().onDidChange(this.onConfigChanged.bind(this)); + configService.getSettings().onDidChange(this.onConfigChanged.bind(this, configService)); } // Stick our services into the service manager @@ -302,8 +302,12 @@ export class MockJupyterManager implements IJupyterSessionManager { return Promise.resolve([]); } - private onConfigChanged = () => { - this.changedInterpreterEvent.fire(); + private onConfigChanged(configService: IConfigurationService) { + const pythonPath = configService.getSettings().pythonPath; + if (this.activeInterpreter === undefined || pythonPath !== this.activeInterpreter.path) { + this.activeInterpreter = this.installedInterpreters.filter(f => f.path === pythonPath)[0]; + this.changedInterpreterEvent.fire(); + } } private createNewSession(): MockJupyterSession { diff --git a/src/test/datascience/mockLanguageClient.ts b/src/test/datascience/mockLanguageClient.ts index efc4fdbd6fc5..f42e5886ff9e 100644 --- a/src/test/datascience/mockLanguageClient.ts +++ b/src/test/datascience/mockLanguageClient.ts @@ -6,8 +6,7 @@ import { DiagnosticCollection, Disposable, Event, - OutputChannel, - TextDocumentContentChangeEvent + OutputChannel } from 'vscode'; import { Code2ProtocolConverter, @@ -24,6 +23,7 @@ import { NotificationHandler0, NotificationType, NotificationType0, + Position, Protocol2CodeConverter, RequestHandler, RequestHandler0, @@ -34,27 +34,32 @@ import { StateChangeEvent, StaticFeature, TextDocumentItem, + TextDocumentContentChangeEvent, Trace, VersionedTextDocumentIdentifier } from 'vscode-languageclient'; import { createDeferred, Deferred } from '../../client/common/utils/async'; import { noop } from '../core'; -import { MockProtocolConverter } from './mockProtocolConverter'; +import { MockCode2ProtocolConverter } from './mockCode2ProtocolConverter'; +import { MockProtocol2CodeConverter } from './mockProtocol2CodeConverter'; +import { IntellisenseLine } from '../../client/datascience/interactive-common/intellisense/intellisenseLine'; // tslint:disable:no-any unified-signatures export class MockLanguageClient extends LanguageClient { private notificationPromise: Deferred | undefined; private contents: string; private versionId: number | null; - private converter: MockProtocolConverter; + private code2Protocol: MockCode2ProtocolConverter; + private protocol2Code: MockProtocol2CodeConverter; public constructor(name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean) { (LanguageClient.prototype as any).checkVersion = noop; super(name, serverOptions, clientOptions, forceDebug); this.contents = ''; this.versionId = 0; - this.converter = new MockProtocolConverter(); + this.code2Protocol = new MockCode2ProtocolConverter(); + this.protocol2Code = new MockProtocol2CodeConverter(); } public waitForNotification(): Promise { this.notificationPromise = createDeferred(); @@ -144,10 +149,10 @@ export class MockLanguageClient extends LanguageClient { throw new Error('Method not implemented.'); } public get protocol2CodeConverter(): Protocol2CodeConverter { - throw new Error('Method not implemented.'); + return this.protocol2Code; } public get code2ProtocolConverter(): Code2ProtocolConverter { - return this.converter; + return this.code2Protocol; } public get onTelemetry(): Event { throw new Error('Method not implemented.'); @@ -210,8 +215,9 @@ export class MockLanguageClient extends LanguageClient { private applyChanges(changes: TextDocumentContentChangeEvent[]) { changes.forEach((c: TextDocumentContentChangeEvent) => { - const before = this.contents.substr(0, c.rangeOffset); - const after = this.contents.substr(c.rangeOffset + c.rangeLength); + const offset = c.range ? this.getOffset(c.range.start) : 0; + const before = this.contents.substr(0, offset); + const after = c.rangeLength ? this.contents.substr(offset + c.rangeLength) : ''; this.contents = `${before}${c.text}${after}`; }); } @@ -226,4 +232,26 @@ export class MockLanguageClient extends LanguageClient { }; }); } + + private createLines(): IntellisenseLine[] { + const split = this.contents.splitLines({ trim: false, removeEmptyEntries: false }); + let prevLine: IntellisenseLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + private createTextLine(line: string, index: number, prevLine: IntellisenseLine | undefined): IntellisenseLine { + return new IntellisenseLine(line, index, prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0); + } + + private getOffset(position: Position): number { + const lines = this.createLines(); + if (position.line >= 0 && position.line < lines.length) { + return lines[position.line].offset + position.character; + } + return 0; + } } diff --git a/src/test/datascience/mockProtocol2CodeConverter.ts b/src/test/datascience/mockProtocol2CodeConverter.ts new file mode 100644 index 000000000000..55077b443145 --- /dev/null +++ b/src/test/datascience/mockProtocol2CodeConverter.ts @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as code from 'vscode'; +import { Protocol2CodeConverter } from 'vscode-languageclient'; +// tslint:disable-next-line: match-default-export-name +import protocolCompletionItem from 'vscode-languageclient/lib/protocolCompletionItem'; +import * as proto from 'vscode-languageserver-protocol'; + +// tslint:disable:no-any unified-signatures +export class MockProtocol2CodeConverter implements Protocol2CodeConverter { + public asUri(_value: string): code.Uri { + throw new Error('Method not implemented.'); + } + + public asDiagnostic(_diagnostic: proto.Diagnostic): code.Diagnostic { + throw new Error('Method not implemented.'); + } + public asDiagnostics(_diagnostics: proto.Diagnostic[]): code.Diagnostic[] { + throw new Error('Method not implemented.'); + } + + public asPosition(value: proto.Position): code.Position; + public asPosition(value: undefined): undefined; + public asPosition(value: null): null; + public asPosition(value: proto.Position | null | undefined): code.Position | null | undefined; + public asPosition(value: any): any { + if (!value) { + return undefined; + } + return new code.Position(value.line, value.character); + } + public asRange(value: proto.Range): code.Range; + public asRange(value: undefined): undefined; + public asRange(value: null): null; + public asRange(value: proto.Range | null | undefined): code.Range | null | undefined; + public asRange(value: any): any { + if (!value) { + return undefined; + } + return new code.Range(this.asPosition(value.start), this.asPosition(value.end)); + } + public asDiagnosticSeverity(_value: number | null | undefined): code.DiagnosticSeverity { + throw new Error('Method not implemented.'); + } + public asHover(hover: proto.Hover): code.Hover; + public asHover(hover: null | undefined): undefined; + public asHover(hover: proto.Hover | null | undefined): code.Hover | undefined; + public asHover(_hover: any): any { + throw new Error('Method not implemented.'); + } + public asCompletionResult(result: proto.CompletionList): code.CompletionList; + public asCompletionResult(result: proto.CompletionItem[]): code.CompletionItem[]; + public asCompletionResult(result: null | undefined): undefined; + public asCompletionResult(result: proto.CompletionList | proto.CompletionItem[] | null | undefined): code.CompletionList | code.CompletionItem[] | undefined; + public asCompletionResult(result: any): any { + if (!result) { + return undefined; + } + if (Array.isArray(result)) { + const items = result; + return items.map(this.asCompletionItem.bind(this)); + } + const list = result; + return new code.CompletionList(list.items.map(this.asCompletionItem.bind(this)), list.isIncomplete); + } + public asCompletionItem(item: proto.CompletionItem): protocolCompletionItem { + const result = new protocolCompletionItem(item.label); + if (item.detail) { result.detail = item.detail; } + if (item.documentation) { + result.documentation = item.documentation.toString(); + result.documentationFormat = '$string'; + } + if (item.filterText) { result.filterText = item.filterText; } + const insertText = this.asCompletionInsertText(item); + if (insertText) { + result.insertText = insertText.text; + result.range = insertText.range; + result.fromEdit = insertText.fromEdit; + } + if (typeof item.kind === 'number') { + const [itemKind, original] = this.asCompletionItemKind(item.kind); + result.kind = itemKind; + if (original) { + result.originalItemKind = original; + } + } + if (item.sortText) { result.sortText = item.sortText; } + if (item.additionalTextEdits) { result.additionalTextEdits = this.asTextEdits(item.additionalTextEdits); } + if (this.isStringArray(item.commitCharacters)) { result.commitCharacters = item.commitCharacters.slice(); } + if (item.command) { result.command = this.asCommand(item.command); } + if (item.deprecated === true || item.deprecated === false) { + result.deprecated = item.deprecated; + } + if (item.preselect === true || item.preselect === false) { result.preselect = item.preselect; } + if (item.data !== undefined) { result.data = item.data; } + return result; + } + public asTextEdit(edit: null | undefined): undefined; + public asTextEdit(edit: proto.TextEdit): code.TextEdit; + public asTextEdit(edit: proto.TextEdit | null | undefined): code.TextEdit | undefined; + public asTextEdit(_edit: any): any { + throw new Error('Method not implemented.'); + } + public asTextEdits(items: proto.TextEdit[]): code.TextEdit[]; + public asTextEdits(items: null | undefined): undefined; + public asTextEdits(items: proto.TextEdit[] | null | undefined): code.TextEdit[] | undefined; + public asTextEdits(_items: any): any { + throw new Error('Method not implemented.'); + } + public asSignatureHelp(item: null | undefined): undefined; + public asSignatureHelp(item: proto.SignatureHelp): code.SignatureHelp; + public asSignatureHelp(item: proto.SignatureHelp | null | undefined): code.SignatureHelp | undefined; + public asSignatureHelp(_item: any): any { + throw new Error('Method not implemented.'); + } + public asSignatureInformation(_item: proto.SignatureInformation): code.SignatureInformation { + throw new Error('Method not implemented.'); + } + public asSignatureInformations(_items: proto.SignatureInformation[]): code.SignatureInformation[] { + throw new Error('Method not implemented.'); + } + public asParameterInformation(_item: proto.ParameterInformation): code.ParameterInformation { + throw new Error('Method not implemented.'); + } + public asParameterInformations(_item: proto.ParameterInformation[]): code.ParameterInformation[] { + throw new Error('Method not implemented.'); + } + public asLocation(item: proto.Location): code.Location; + public asLocation(item: null | undefined): undefined; + public asLocation(item: proto.Location | null | undefined): code.Location | undefined; + public asLocation(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDeclarationResult(item: proto.Declaration): code.Location | code.Location[]; + public asDeclarationResult(item: proto.LocationLink[]): code.LocationLink[]; + public asDeclarationResult(item: null | undefined): undefined; + public asDeclarationResult(item: proto.Location | proto.Location[] | proto.LocationLink[] | null | undefined): code.Location | code.Location[] | code.LocationLink[] | undefined; + public asDeclarationResult(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDefinitionResult(item: proto.Definition): code.Definition; + public asDefinitionResult(item: proto.LocationLink[]): code.LocationLink[]; + public asDefinitionResult(item: null | undefined): undefined; + public asDefinitionResult(item: proto.Location | proto.LocationLink[] | proto.Location[] | null | undefined): code.Location | code.LocationLink[] | code.Location[] | undefined; + public asDefinitionResult(_item: any): any { + throw new Error('Method not implemented.'); + } + public asReferences(values: proto.Location[]): code.Location[]; + public asReferences(values: null | undefined): code.Location[] | undefined; + public asReferences(values: proto.Location[] | null | undefined): code.Location[] | undefined; + public asReferences(_values: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentHighlightKind(_item: number): code.DocumentHighlightKind { + throw new Error('Method not implemented.'); + } + public asDocumentHighlight(_item: proto.DocumentHighlight): code.DocumentHighlight { + throw new Error('Method not implemented.'); + } + public asDocumentHighlights(values: proto.DocumentHighlight[]): code.DocumentHighlight[]; + public asDocumentHighlights(values: null | undefined): undefined; + public asDocumentHighlights(values: proto.DocumentHighlight[] | null | undefined): code.DocumentHighlight[] | undefined; + public asDocumentHighlights(_values: any): any { + throw new Error('Method not implemented.'); + } + public asSymbolInformation(_item: proto.SymbolInformation, _uri?: code.Uri | undefined): code.SymbolInformation { + throw new Error('Method not implemented.'); + } + public asSymbolInformations(values: proto.SymbolInformation[], uri?: code.Uri | undefined): code.SymbolInformation[]; + public asSymbolInformations(values: null | undefined, uri?: code.Uri | undefined): undefined; + public asSymbolInformations(values: proto.SymbolInformation[] | null | undefined, uri?: code.Uri | undefined): code.SymbolInformation[] | undefined; + public asSymbolInformations(_values: any, _uri?: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentSymbol(_value: proto.DocumentSymbol): code.DocumentSymbol { + throw new Error('Method not implemented.'); + } + public asDocumentSymbols(value: null | undefined): undefined; + public asDocumentSymbols(value: proto.DocumentSymbol[]): code.DocumentSymbol[]; + public asDocumentSymbols(value: proto.DocumentSymbol[] | null | undefined): code.DocumentSymbol[] | undefined; + public asDocumentSymbols(_value: any): any { + throw new Error('Method not implemented.'); + } + public asCommand(_item: proto.Command): code.Command { + throw new Error('Method not implemented.'); + } + public asCommands(items: proto.Command[]): code.Command[]; + public asCommands(items: null | undefined): undefined; + public asCommands(items: proto.Command[] | null | undefined): code.Command[] | undefined; + public asCommands(_items: any): any { + throw new Error('Method not implemented.'); + } + public asCodeAction(item: proto.CodeAction): code.CodeAction; + public asCodeAction(item: null | undefined): undefined; + public asCodeAction(item: proto.CodeAction | null | undefined): code.CodeAction | undefined; + public asCodeAction(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeActionKind(item: null | undefined): undefined; + public asCodeActionKind(item: string): code.CodeActionKind; + public asCodeActionKind(item: string | null | undefined): code.CodeActionKind | undefined; + public asCodeActionKind(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeActionKinds(item: null | undefined): undefined; + public asCodeActionKinds(items: string[]): code.CodeActionKind[]; + public asCodeActionKinds(item: string[] | null | undefined): code.CodeActionKind[] | undefined; + public asCodeActionKinds(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeLens(item: proto.CodeLens): code.CodeLens; + public asCodeLens(item: null | undefined): undefined; + public asCodeLens(item: proto.CodeLens | null | undefined): code.CodeLens | undefined; + public asCodeLens(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeLenses(items: proto.CodeLens[]): code.CodeLens[]; + public asCodeLenses(items: null | undefined): undefined; + public asCodeLenses(items: proto.CodeLens[] | null | undefined): code.CodeLens[] | undefined; + public asCodeLenses(_items: any): any { + throw new Error('Method not implemented.'); + } + public asWorkspaceEdit(item: proto.WorkspaceEdit): code.WorkspaceEdit; + public asWorkspaceEdit(item: null | undefined): undefined; + public asWorkspaceEdit(item: proto.WorkspaceEdit | null | undefined): code.WorkspaceEdit | undefined; + public asWorkspaceEdit(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentLink(_item: proto.DocumentLink): code.DocumentLink { + throw new Error('Method not implemented.'); + } + public asDocumentLinks(items: proto.DocumentLink[]): code.DocumentLink[]; + public asDocumentLinks(items: null | undefined): undefined; + public asDocumentLinks(items: proto.DocumentLink[] | null | undefined): code.DocumentLink[] | undefined; + public asDocumentLinks(_items: any): any { + throw new Error('Method not implemented.'); + } + public asColor(_color: proto.Color): code.Color { + throw new Error('Method not implemented.'); + } + public asColorInformation(_ci: proto.ColorInformation): code.ColorInformation { + throw new Error('Method not implemented.'); + } + public asColorInformations(colorPresentations: proto.ColorInformation[]): code.ColorInformation[]; + public asColorInformations(colorPresentations: null | undefined): undefined; + public asColorInformations(colorInformation: proto.ColorInformation[] | null | undefined): code.ColorInformation[]; + public asColorInformations(_colorInformation: any): any { + throw new Error('Method not implemented.'); + } + public asColorPresentation(_cp: proto.ColorPresentation): code.ColorPresentation { + throw new Error('Method not implemented.'); + } + public asColorPresentations(colorPresentations: proto.ColorPresentation[]): code.ColorPresentation[]; + public asColorPresentations(colorPresentations: null | undefined): undefined; + public asColorPresentations(colorPresentations: proto.ColorPresentation[] | null | undefined): undefined; + public asColorPresentations(_colorPresentations: any): any { + throw new Error('Method not implemented.'); + } + public asFoldingRangeKind(_kind: string | undefined): code.FoldingRangeKind | undefined { + throw new Error('Method not implemented.'); + } + public asFoldingRange(_r: proto.FoldingRange): code.FoldingRange { + throw new Error('Method not implemented.'); + } + public asFoldingRanges(foldingRanges: proto.FoldingRange[]): code.FoldingRange[]; + public asFoldingRanges(foldingRanges: null | undefined): undefined; + public asFoldingRanges(foldingRanges: proto.FoldingRange[] | null | undefined): code.FoldingRange[] | undefined; + public asFoldingRanges(foldingRanges: proto.FoldingRange[] | null | undefined): code.FoldingRange[] | undefined; + public asFoldingRanges(_foldingRanges: any): any { + throw new Error('Method not implemented.'); + } + + private asCompletionItemKind(value: proto.CompletionItemKind): [code.CompletionItemKind, proto.CompletionItemKind | undefined] { + // Protocol item kind is 1 based, codes item kind is zero based. + if (proto.CompletionItemKind.Text <= value && value <= proto.CompletionItemKind.TypeParameter) { + return [value - 1, undefined]; + } + return [code.CompletionItemKind.Text, value]; + } + + private isStringArray(value: any): value is string[] { + return Array.isArray(value) && (value).every(elem => typeof elem === 'string'); + } + + private asCompletionInsertText(item: proto.CompletionItem): { text: string | code.SnippetString; range?: code.Range; fromEdit: boolean } | undefined { + if (item.textEdit) { + if (item.insertTextFormat === proto.InsertTextFormat.Snippet) { + return { text: new code.SnippetString(item.textEdit.newText), range: this.asRange(item.textEdit.range), fromEdit: true }; + } else { + return { text: item.textEdit.newText, range: this.asRange(item.textEdit.range), fromEdit: true }; + } + } else if (item.insertText) { + if (item.insertTextFormat === proto.InsertTextFormat.Snippet) { + return { text: new code.SnippetString(item.insertText), fromEdit: false }; + } else { + return { text: item.insertText, fromEdit: false }; + } + } else { + return undefined; + } + } + +} diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index bfef86272771..45a5c8bd4ef6 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -533,7 +533,8 @@ suite('DataScience notebook tests', () => { // Create again, we should get the same server from the cache const server2 = await createNotebook(true); - assert.equal(server, server2, 'With no settings changed we should return the cached server'); + // tslint:disable-next-line: triple-equals + assert.ok(server == server2, 'With no settings changed we should return the cached server'); // Create a new mock interpreter with a different path const newPython: PythonInterpreter = { @@ -548,9 +549,10 @@ suite('DataScience notebook tests', () => { // Add interpreter into mock jupyter service and set it as active ioc.addInterpreter(newPython, SupportedCommands.all); - // Create a new notebook, we should not be the same anymore + // Create a new notebook, we should still be the same as interpreter is just saved for notebook creation const server3 = await createNotebook(true); - assert.notEqual(server, server3, 'With interpreter changed we should return a new server'); + // tslint:disable-next-line: triple-equals + assert.ok(server == server3, 'With interpreter changed we should not return a new server'); } else { console.log(`Skipping Change Interpreter test in non-mocked Jupyter case`); } From 677f2390c960bf72288a15f7619cb208d1c17f10 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 16:30:10 -0800 Subject: [PATCH 15/26] Add news entry --- news/1 Enhancements/8206.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1 Enhancements/8206.md diff --git a/news/1 Enhancements/8206.md b/news/1 Enhancements/8206.md new file mode 100644 index 000000000000..73d6f8fd086b --- /dev/null +++ b/news/1 Enhancements/8206.md @@ -0,0 +1 @@ +Support a per interpreter language server so that notebooks that aren't using the currently selected python can still have intellisense. From 68135db1b6539d9f93e9f41e55cc3d89a55dae0b Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 16:30:22 -0800 Subject: [PATCH 16/26] Fix linter problems. --- src/test/datascience/mockLanguageClient.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/test/datascience/mockLanguageClient.ts b/src/test/datascience/mockLanguageClient.ts index f42e5886ff9e..dd951abef464 100644 --- a/src/test/datascience/mockLanguageClient.ts +++ b/src/test/datascience/mockLanguageClient.ts @@ -1,13 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { - CancellationToken, - DiagnosticCollection, - Disposable, - Event, - OutputChannel -} from 'vscode'; +import { CancellationToken, DiagnosticCollection, Disposable, Event, OutputChannel } from 'vscode'; import { Code2ProtocolConverter, CompletionItem, @@ -33,17 +27,17 @@ import { ServerOptions, StateChangeEvent, StaticFeature, - TextDocumentItem, TextDocumentContentChangeEvent, + TextDocumentItem, Trace, VersionedTextDocumentIdentifier } from 'vscode-languageclient'; import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { IntellisenseLine } from '../../client/datascience/interactive-common/intellisense/intellisenseLine'; import { noop } from '../core'; import { MockCode2ProtocolConverter } from './mockCode2ProtocolConverter'; import { MockProtocol2CodeConverter } from './mockProtocol2CodeConverter'; -import { IntellisenseLine } from '../../client/datascience/interactive-common/intellisense/intellisenseLine'; // tslint:disable:no-any unified-signatures export class MockLanguageClient extends LanguageClient { From e25e802c44ad4f9e8c408d0745f752e4bf0fef25 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 26 Nov 2019 17:08:13 -0800 Subject: [PATCH 17/26] Fix reconnect issue when Notebook is 'holding' a language server --- src/client/activation/activationService.ts | 6 + src/client/activation/jedi.ts | 134 ++++++++++-------- .../activation/languageServer/activator.ts | 4 + .../languageClientMiddleware.ts | 4 + .../activation/languageServer/manager.ts | 3 + .../activation/refCountedLanguageServer.ts | 4 + src/client/activation/types.ts | 4 +- .../activation/activationService.unit.test.ts | 17 ++- src/test/datascience/mockLanguageServer.ts | 8 ++ 9 files changed, 119 insertions(+), 65 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 856581640346..56ff4a798f5f 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -99,6 +99,12 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv // Save our active server. this.activatedServer = { key, server: result, jedi: result.type === LanguageServerActivator.Jedi }; + + // Force this server to reconnect (if disconnected) as it should be the active + // language server for all of VS code. + if (this.activatedServer.server && this.activatedServer.server.reconnect) { + this.activatedServer.server.reconnect(); + } } public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index 74764d3728f4..eef27736fae3 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -83,6 +83,8 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.completionProvider = new PythonCompletionItemProvider(jediFactory, this.serviceManager); this.codeLensProvider = this.serviceManager.get(IShebangCodeLensProvider); this.objectDefinitionProvider = new PythonObjectDefinitionProvider(jediFactory); + this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); + this.signatureProvider = new PythonSignatureProvider(jediFactory); if (!JediExtensionActivator.workspaceSymbols) { // Workspace symbols is static because it doesn't rely on the jediFactory. @@ -90,69 +92,8 @@ export class JediExtensionActivator implements ILanguageServerActivator { context.subscriptions.push(JediExtensionActivator.workspaceSymbols); } - // Make sure commands are in the registration list that gets disposed when the language server is disconnected from the - // IDE. - this.registrations.push(commands.registerCommand('python.goToPythonObject', () => this.objectDefinitionProvider!.goToObjectDefinition())); - - this.registrations.push( - languages.registerRenameProvider(this.documentSelector, this.renameProvider) - ); - this.registrations.push(languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider)); - this.registrations.push( - languages.registerHoverProvider(this.documentSelector, this.hoverProvider) - ); - this.registrations.push( - languages.registerReferenceProvider(this.documentSelector, this.referenceProvider) - ); - this.registrations.push( - languages.registerCompletionItemProvider( - this.documentSelector, - this.completionProvider, - '.' - ) - ); - this.registrations.push( - languages.registerCodeLensProvider( - this.documentSelector, - this.codeLensProvider - ) - ); - - const onTypeDispatcher = new OnTypeFormattingDispatcher({ - '\n': new OnEnterFormatter(), - ':': new BlockFormatProviders() - }); - const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); - if (onTypeTriggers) { - this.registrations.push( - languages.registerOnTypeFormattingEditProvider( - PYTHON, - onTypeDispatcher, - onTypeTriggers.first, - ...onTypeTriggers.more - ) - ); - } - - this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); - this.signatureProvider = new PythonSignatureProvider(jediFactory); - this.registrations.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); - - const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); - if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - this.registrations.push( - languages.registerSignatureHelpProvider( - this.documentSelector, - this.signatureProvider, - '(', - ',' - ) - ); - } - - this.registrations.push( - languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer)) - ); + // Do the same thing we'd do on reconnect, that is, register for the VS code provider callbacks. + this.reconnect(); const testManagementService = this.serviceManager.get(ITestManagementService); testManagementService @@ -165,6 +106,73 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.registrations = []; } + public reconnect() { + if (this.registrations.length === 0 && + this.renameProvider && + this.definitionProvider && + this.hoverProvider && + this.referenceProvider && + this.completionProvider && + this.codeLensProvider && + this.symbolProvider && + this.signatureProvider) { + + // Make sure commands are in the registration list that gets disposed when the language server is disconnected from the + // IDE. + this.registrations.push(commands.registerCommand('python.goToPythonObject', () => this.objectDefinitionProvider!.goToObjectDefinition())); + this.registrations.push( + languages.registerRenameProvider(this.documentSelector, this.renameProvider) + ); + this.registrations.push(languages.registerDefinitionProvider(this.documentSelector, this.definitionProvider)); + this.registrations.push( + languages.registerHoverProvider(this.documentSelector, this.hoverProvider) + ); + this.registrations.push( + languages.registerReferenceProvider(this.documentSelector, this.referenceProvider) + ); + this.registrations.push( + languages.registerCompletionItemProvider( + this.documentSelector, + this.completionProvider, + '.' + ) + ); + this.registrations.push( + languages.registerCodeLensProvider( + this.documentSelector, + this.codeLensProvider + ) + ); + const onTypeDispatcher = new OnTypeFormattingDispatcher({ + '\n': new OnEnterFormatter(), + ':': new BlockFormatProviders() + }); + const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); + if (onTypeTriggers) { + this.registrations.push( + languages.registerOnTypeFormattingEditProvider( + PYTHON, + onTypeDispatcher, + onTypeTriggers.first, + ...onTypeTriggers.more + ) + ); + } + this.registrations.push(languages.registerDocumentSymbolProvider(this.documentSelector, this.symbolProvider)); + const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); + if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { + this.registrations.push( + languages.registerSignatureHelpProvider( + this.documentSelector, + this.signatureProvider, + '(', + ',' + ) + ); + } + } + } + public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { if (this.renameProvider) { return this.renameProvider.provideRenameEdits(document, position, newName, token); diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index e09bd04b13c0..de98f7014921 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -109,6 +109,10 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato this.manager.disconnect(); } + public reconnect(): void { + this.manager.reconnect(); + } + public handleOpen(document: TextDocument): void { const languageClient = this.getLanguageClient(); if (languageClient) { diff --git a/src/client/activation/languageServer/languageClientMiddleware.ts b/src/client/activation/languageServer/languageClientMiddleware.ts index d735a415e8dc..f0fed75bbdb4 100644 --- a/src/client/activation/languageServer/languageClientMiddleware.ts +++ b/src/client/activation/languageServer/languageClientMiddleware.ts @@ -64,6 +64,10 @@ export class LanguageClientMiddleware implements Middleware { this.connected = false; } + public reconnect() { + this.connected = true; + } + public provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) { if (this.connected) { this.surveyBanner.showBanner().ignoreErrors(); diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 01b5e3bb4ccb..c935db44ffe5 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -58,6 +58,9 @@ export class LanguageServerManager implements ILanguageServerManager { public disconnect() { this.middleware?.disconnect(); } + public reconnect() { + this.middleware?.reconnect(); + } protected registerCommandHandler() { this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index ac1f2030cd03..3a7bbcf83237 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -48,6 +48,10 @@ export class RefCountedLanguageServer implements ILanguageServer { this.impl.disconnect ? this.impl.disconnect() : noop(); } + public reconnect() { + this.impl.reconnect ? this.impl.reconnect() : noop(); + } + public clearAnalysisCache() { this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 41e53b1256ef..af233b862957 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -82,6 +82,7 @@ export interface LanguageServerCommandHandler { // tslint:disable-next-line: interface-name export interface RegisteredServer { disconnect(): void; + reconnect(): void; } export interface ILanguageServer extends @@ -95,7 +96,7 @@ export interface ILanguageServer extends SignatureHelpProvider, Partial, Partial, - Partial, + RegisteredServer, IDisposable { } @@ -160,6 +161,7 @@ export interface ILanguageServerManager extends IDisposable { readonly languageProxy: ILanguageServerProxy | undefined; start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; disconnect(): void; + reconnect(): void; } export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); export interface ILanguageServerExtension extends IDisposable { diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index d16a14157918..1250759b2fc5 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -424,7 +424,7 @@ suite('Language Server Activation - ActivationService', () => { }; let getActiveCount = 0; interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => { - if (getActiveCount <= 0) { + if (getActiveCount % 2 === 0) { getActiveCount += 1; return Promise.resolve(interpreter1); } @@ -441,6 +441,12 @@ suite('Language Server Activation - ActivationService', () => { .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); + let reconnectCount = 0; + activator + .setup(a => a.reconnect()) + .returns(() => { + reconnectCount = reconnectCount + 1; + }); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) .returns(() => activator.object); @@ -462,6 +468,15 @@ suite('Language Server Activation - ActivationService', () => { await activationService.activate(folder1.uri); await interpreterChangedHandler(); activator.verifyAll(); + + // Hold onto the second item and switch two more times. Verify that + // reconnect happens + const server = await activationService.get(folder1.uri); + await interpreterChangedHandler(); + expect(reconnectCount).to.be.equal(3, 'Reconnect is not happening'); + await interpreterChangedHandler(); + expect(reconnectCount).to.be.equal(4, 'Reconnect is not happening'); + server.dispose(); }); if (!jediIsEnabled) { test('Revert to jedi when LS activation fails', async () => { diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts index 40711b8f7c4f..5da23f6bf9d2 100644 --- a/src/test/datascience/mockLanguageServer.ts +++ b/src/test/datascience/mockLanguageServer.ts @@ -106,6 +106,14 @@ export class MockLanguageServer implements ILanguageServer { noop(); } + public disconnect(): void { + noop(); + } + + public reconnect(): void { + noop(); + } + private applyChanges(changes: TextDocumentContentChangeEvent[]) { changes.forEach(c => { const before = this.contents.substr(0, c.rangeOffset); From a1bff7fab2a05e07e89c09299575833ba0cc16d0 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 09:39:16 -0800 Subject: [PATCH 18/26] Change connect/disconnect to activate/deactivate --- src/client/activation/activationService.ts | 17 +++--- src/client/activation/jedi.ts | 9 ++-- .../activation/languageServer/activator.ts | 10 ++-- .../languageClientMiddleware.ts | 10 ++-- .../activation/languageServer/manager.ts | 6 +-- .../activation/refCountedLanguageServer.ts | 20 ++++--- src/client/activation/types.ts | 13 ++--- .../activation/activationService.unit.test.ts | 54 +++++++++++++------ .../languageServer/activator.unit.test.ts | 16 ++++-- 9 files changed, 89 insertions(+), 66 deletions(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 56ff4a798f5f..e8cf91855e6d 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -30,7 +30,6 @@ import { Commands } from './languageServer/constants'; import { RefCountedLanguageServer } from './refCountedLanguageServer'; import { IExtensionActivationService, - ILanguageServer, ILanguageServerActivator, ILanguageServerCache, LanguageServerActivator @@ -41,7 +40,7 @@ const workspacePathNameForGlobalWorkspaces = ''; interface IActivatedServer { key: string; - server: ILanguageServer; + server: ILanguageServerActivator; jedi: boolean; } @@ -82,10 +81,10 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv const interpreter = await this.interpreterService.getActiveInterpreter(resource); const key = await this.getKey(resource, interpreter); - // If we have an old server with a different key, then disconnect it as the + // If we have an old server with a different key, then deactivate it as the // creation of the new server may fail if this server is still connected - if (this.activatedServer && this.activatedServer.key !== key && this.activatedServer.server.disconnect) { - this.activatedServer.server.disconnect(); + if (this.activatedServer && this.activatedServer.key !== key) { + this.activatedServer.server.deactivate(); } // Get the new item @@ -102,9 +101,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv // Force this server to reconnect (if disconnected) as it should be the active // language server for all of VS code. - if (this.activatedServer.server && this.activatedServer.server.reconnect) { - this.activatedServer.server.reconnect(); - } + this.activatedServer.server.activate(); } public async get(resource: Resource, interpreter?: PythonInterpreter): Promise { @@ -209,7 +206,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; let server = this.serviceContainer.get(ILanguageServerActivator, serverName); try { - await server.activate(resource, interpreter); + await server.start(resource, interpreter); } catch (ex) { if (jedi) { throw ex; @@ -217,7 +214,7 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv await this.logStartup(jedi); serverName = LanguageServerActivator.Jedi; server = this.serviceContainer.get(ILanguageServerActivator, serverName); - await server.activate(resource, interpreter); + await server.start(resource, interpreter); } // Wrap the returned server in something that ref counts it. diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts index eef27736fae3..1fde50f581a8 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -67,7 +67,7 @@ export class JediExtensionActivator implements ILanguageServerActivator { this.documentSelector = PYTHON; } - public async activate(_resource: Resource, interpreter: PythonInterpreter | undefined): Promise { + public async start(_resource: Resource, interpreter: PythonInterpreter | undefined): Promise { if (this.jediFactory) { throw new Error('Jedi already started'); } @@ -92,21 +92,18 @@ export class JediExtensionActivator implements ILanguageServerActivator { context.subscriptions.push(JediExtensionActivator.workspaceSymbols); } - // Do the same thing we'd do on reconnect, that is, register for the VS code provider callbacks. - this.reconnect(); - const testManagementService = this.serviceManager.get(ITestManagementService); testManagementService .activate(this.symbolProvider) .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); } - public disconnect() { + public deactivate() { this.registrations.forEach(r => r.dispose()); this.registrations = []; } - public reconnect() { + public activate() { if (this.registrations.length === 0 && this.renameProvider && this.definitionProvider && diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index de98f7014921..09d087d73329 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -57,7 +57,7 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato @inject(IConfigurationService) private readonly configurationService: IConfigurationService ) { } @traceDecorators.error('Failed to activate language server') - public async activate(resource: Resource, interpreter?: PythonInterpreter): Promise { + public async start(resource: Resource, interpreter?: PythonInterpreter): Promise { if (!resource) { resource = this.workspace.hasWorkspaceFolders ? this.workspace.workspaceFolders![0].uri @@ -105,12 +105,12 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); } - public disconnect(): void { - this.manager.disconnect(); + public activate(): void { + this.manager.connect(); } - public reconnect(): void { - this.manager.reconnect(); + public deactivate(): void { + this.manager.disconnect(); } public handleOpen(document: TextDocument): void { diff --git a/src/client/activation/languageServer/languageClientMiddleware.ts b/src/client/activation/languageServer/languageClientMiddleware.ts index f0fed75bbdb4..5af9e4768e28 100644 --- a/src/client/activation/languageServer/languageClientMiddleware.ts +++ b/src/client/activation/languageServer/languageClientMiddleware.ts @@ -55,17 +55,17 @@ import { HiddenFilePrefix } from '../../common/constants'; import { IPythonExtensionBanner } from '../../common/types'; export class LanguageClientMiddleware implements Middleware { - private connected = true; + private connected = false; // Default to not forwarding to VS code. public constructor(private readonly surveyBanner: IPythonExtensionBanner) { } - public disconnect() { - this.connected = false; + public connect() { + this.connected = true; } - public reconnect() { - this.connected = true; + public disconnect() { + this.connected = false; } public provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) { diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index c935db44ffe5..1dc3180af3cc 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -55,12 +55,12 @@ export class LanguageServerManager implements ILanguageServerManager { await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); } + public connect() { + this.middleware?.connect(); + } public disconnect() { this.middleware?.disconnect(); } - public reconnect() { - this.middleware?.reconnect(); - } protected registerCommandHandler() { this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index 3a7bbcf83237..328b2a5145c9 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -21,12 +21,14 @@ import { WorkspaceEdit } from 'vscode'; +import { Resource } from '../common/types'; import { noop } from '../common/utils/misc'; -import { ILanguageServer, LanguageServerActivator } from './types'; +import { PythonInterpreter } from '../interpreter/contracts'; +import { ILanguageServerActivator, LanguageServerActivator } from './types'; -export class RefCountedLanguageServer implements ILanguageServer { +export class RefCountedLanguageServer implements ILanguageServerActivator { private refCount = 1; - constructor(private impl: ILanguageServer, private _type: LanguageServerActivator, private disposeCallback: () => void) { + constructor(private impl: ILanguageServerActivator, private _type: LanguageServerActivator, private disposeCallback: () => void) { } public increment = () => { @@ -44,12 +46,16 @@ export class RefCountedLanguageServer implements ILanguageServer { } } - public disconnect() { - this.impl.disconnect ? this.impl.disconnect() : noop(); + public start(_resource: Resource, _interpreter: PythonInterpreter | undefined): Promise { + throw new Error('Server should have already been started. Do not start the wrapper.'); } - public reconnect() { - this.impl.reconnect ? this.impl.reconnect() : noop(); + public activate() { + this.impl.activate(); + } + + public deactivate() { + this.impl.deactivate(); } public clearAnalysisCache() { diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index af233b862957..09f9f10c68de 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -79,12 +79,6 @@ export interface LanguageServerCommandHandler { clearAnalysisCache(): void; } -// tslint:disable-next-line: interface-name -export interface RegisteredServer { - disconnect(): void; - reconnect(): void; -} - export interface ILanguageServer extends RenameProvider, DefinitionProvider, @@ -96,13 +90,14 @@ export interface ILanguageServer extends SignatureHelpProvider, Partial, Partial, - RegisteredServer, IDisposable { } export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); export interface ILanguageServerActivator extends ILanguageServer { - activate(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + activate(): void; + deactivate(): void; } export const ILanguageServerCache = Symbol('ILanguageServerCache'); @@ -160,8 +155,8 @@ export const ILanguageServerManager = Symbol('ILanguageServerManager'); export interface ILanguageServerManager extends IDisposable { readonly languageProxy: ILanguageServerProxy | undefined; start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + connect(): void; disconnect(): void; - reconnect(): void; } export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); export interface ILanguageServerExtension extends IDisposable { diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 1250759b2fc5..7cc826e30ac7 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -132,9 +132,12 @@ suite('Language Server Activation - ActivationService', () => { lsSupported: boolean = true ) { activator - .setup(a => a.activate(undefined, undefined)) + .setup(a => a.start(undefined, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.activate()) + .verifiable(TypeMoq.Times.once()); let activatorName = LanguageServerActivator.Jedi; if (lsSupported && !jediIsEnabled) { activatorName = LanguageServerActivator.DotNet; @@ -368,13 +371,19 @@ suite('Language Server Activation - ActivationService', () => { const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const activator = TypeMoq.Mock.ofType(); activator - .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .setup(a => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); activator - .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .setup(a => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.deactivate()) + .verifiable(TypeMoq.Times.never()); + activator + .setup(a => a.activate()) + .verifiable(TypeMoq.Times.never()); activator .setup(a => a.dispose()).returns(noop).verifiable(TypeMoq.Times.exactly(2)); serviceContainer @@ -434,18 +443,18 @@ suite('Language Server Activation - ActivationService', () => { const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; const activator = TypeMoq.Mock.ofType(); activator - .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .setup(a => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); activator - .setup(a => a.activate(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .setup(a => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); - let reconnectCount = 0; + let connectCount = 0; activator - .setup(a => a.reconnect()) + .setup(a => a.activate()) .returns(() => { - reconnectCount = reconnectCount + 1; + connectCount = connectCount + 1; }); serviceContainer .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) @@ -473,9 +482,9 @@ suite('Language Server Activation - ActivationService', () => { // reconnect happens const server = await activationService.get(folder1.uri); await interpreterChangedHandler(); - expect(reconnectCount).to.be.equal(3, 'Reconnect is not happening'); + expect(connectCount).to.be.equal(3, 'Reconnect is not happening'); await interpreterChangedHandler(); - expect(reconnectCount).to.be.equal(4, 'Reconnect is not happening'); + expect(connectCount).to.be.equal(4, 'Reconnect is not happening'); server.dispose(); }); if (!jediIsEnabled) { @@ -501,7 +510,7 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => activatorDotNet.object) .verifiable(TypeMoq.Times.once()); activatorDotNet - .setup(a => a.activate(undefined, undefined)) + .setup(a => a.start(undefined, undefined)) .returns(() => Promise.reject(new Error(''))) .verifiable(TypeMoq.Times.once()); serviceContainer @@ -514,7 +523,11 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => activatorJedi.object) .verifiable(TypeMoq.Times.once()); activatorJedi - .setup(a => a.activate(undefined, undefined)) + .setup(a => a.start(undefined, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatorJedi + .setup(a => a.activate()) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); @@ -530,9 +543,12 @@ suite('Language Server Activation - ActivationService', () => { resource: Resource ) { activator - .setup(a => a.activate(TypeMoq.It.isValue(resource), undefined)) + .setup(a => a.start(TypeMoq.It.isValue(resource), undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.activate()) + .verifiable(TypeMoq.Times.once()); lsNotSupportedDiagnosticService .setup(l => l.diagnose(undefined)) .returns(() => Promise.resolve([])); @@ -606,7 +622,7 @@ suite('Language Server Activation - ActivationService', () => { activator3.verifyAll(); }); } else { - test('Jedi is only activated once', async () => { + test('Jedi is only started once', async () => { pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); const activator1 = TypeMoq.Mock.ofType(); const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); @@ -617,7 +633,7 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => activator1.object) .verifiable(TypeMoq.Times.once()); activator1 - .setup(a => a.activate(folder1.uri, undefined)) + .setup(a => a.start(folder1.uri, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); experiments @@ -626,6 +642,7 @@ suite('Language Server Activation - ActivationService', () => { .verifiable(TypeMoq.Times.never()); await activationService.activate(folder1.uri); activator1.verifyAll(); + activator1.verify(a => a.activate(), TypeMoq.Times.once()); serviceContainer.verifyAll(); experiments.verifyAll(); @@ -635,16 +652,21 @@ suite('Language Server Activation - ActivationService', () => { .returns(() => activator2.object) .verifiable(TypeMoq.Times.once()); activator2 - .setup(a => a.activate(folder2.uri, undefined)) + .setup(a => a.start(folder2.uri, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.never()); + activator2 + .setup(a => a.activate()) + .verifiable(TypeMoq.Times.never()); experiments .setup(ex => ex.inExperiment(TypeMoq.It.isAny())) .returns(() => false) .verifiable(TypeMoq.Times.never()); await activationService.activate(folder2.uri); serviceContainer.verifyAll(); + activator1ActivateCount = 2; activator1.verifyAll(); + activator1.verify(a => a.activate(), TypeMoq.Times.exactly(2)); activator2.verifyAll(); experiments.verifyAll(); }); diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts index 50f3d9384d10..637fb2ebc074 100644 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ b/src/test/activation/languageServer/activator.unit.test.ts @@ -60,7 +60,7 @@ suite('Language Server - Activator', () => { when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(undefined); + await activator.start(undefined); verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -70,12 +70,18 @@ suite('Language Server - Activator', () => { verify(manager.dispose()).once(); }); + test('Server should be disconnected but be started', async () => { + await activator.start(undefined); + + verify(manager.start(undefined, undefined)).once(); + verify(manager.connect()).never(); + }); test('Do not download LS if not required', async () => { when(workspaceService.hasWorkspaceFolders).thenReturn(false); when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(undefined); + await activator.start(undefined); verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -94,7 +100,7 @@ suite('Language Server - Activator', () => { .thenResolve(languageServerFolder); when(fs.fileExists(mscorlib)).thenResolve(true); - await activator.activate(undefined); + await activator.start(undefined); verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); @@ -116,7 +122,7 @@ suite('Language Server - Activator', () => { when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)) .thenReturn(deferred.promise); - const promise = activator.activate(undefined); + const promise = activator.start(undefined); await sleep(1); verify(workspaceService.hasWorkspaceFolders).once(); verify(lsFolderService.getLanguageServerFolderName(anything())).once(); @@ -137,7 +143,7 @@ suite('Language Server - Activator', () => { when(manager.start(uri, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(undefined); + await activator.start(undefined); verify(manager.start(uri, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); From 24d3b19d027dfb8d3c6ae3ec503f9406f6ae4919 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 09:39:28 -0800 Subject: [PATCH 19/26] Fix build error --- src/test/activation/activationService.unit.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index 7cc826e30ac7..e0c110d34e95 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -664,7 +664,6 @@ suite('Language Server Activation - ActivationService', () => { .verifiable(TypeMoq.Times.never()); await activationService.activate(folder2.uri); serviceContainer.verifyAll(); - activator1ActivateCount = 2; activator1.verifyAll(); activator1.verify(a => a.activate(), TypeMoq.Times.exactly(2)); activator2.verifyAll(); From cfc346d3328957e5325a88cf68690bc56e8596a5 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 11:18:55 -0800 Subject: [PATCH 20/26] Remove test only failure logic --- src/client/datascience/jupyter/jupyterCommandFinder.ts | 4 +++- src/client/datascience/jupyter/jupyterExecution.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/datascience/jupyter/jupyterCommandFinder.ts b/src/client/datascience/jupyter/jupyterCommandFinder.ts index 495b5054f9b1..2f47bc23cef8 100644 --- a/src/client/datascience/jupyter/jupyterCommandFinder.ts +++ b/src/client/datascience/jupyter/jupyterCommandFinder.ts @@ -265,7 +265,9 @@ export class JupyterCommandFinderImpl { found.error = firstError; } - if (found && found.status === ModuleExistsStatus.NotFound) { + // Note to self, if found is undefined, check that your test is actually + // setting up different services correctly. Some method must be undefined. + if (found.status === ModuleExistsStatus.NotFound) { this.sendSearchTelemetry(command, 'nowhere', stopWatch.elapsedTime, cancelToken); } diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index f1d8151639c8..e9cbaae49eb7 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -313,7 +313,10 @@ export class JupyterExecutionBase implements IJupyterExecution { // See if we can find the command try { const result = await this.findBestCommand(command, cancelToken); - return result && result.command !== undefined; + + // Note to self, if result is undefined, check that your test is actually + // setting up different services correctly. Some method must be undefined. + return result.command !== undefined; } catch (err) { this.logger.logWarning(err); return false; From 1bc99279091f0ce1e87e769c8d24d45fbf78db6f Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 12:35:10 -0800 Subject: [PATCH 21/26] Fix notebook functional failure from second active interpreter --- .../datascience/dataScienceIocContainer.ts | 4 ++ .../datascience/notebook.functional.test.ts | 38 +++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index d304f5bac1c4..743d2bddcce1 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -357,6 +357,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return this.workingPython; } + public get workingInterpreter2() { + return this.workingPython2; + } + public get onContextSet(): Event<{ name: string; value: boolean }> { return this.contextSetEvent.event; } diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index 52820e6c3faa..730f29519667 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -1100,6 +1100,26 @@ plt.show()`, assert.equal(outputs[outputs.length - 1], '1', 'Cell outputs not captured'); }); + async function disableJupyter(pythonPath: string) { + const factory = ioc.serviceManager.get(IPythonExecutionFactory); + const service = await factory.create({ pythonPath }); + const mockService = service as MockPythonService; + mockService.addExecResult(['-m', 'jupyter', 'notebook', '--version'], () => { + return Promise.resolve({ + stdout: '9.9.9.9', + stderr: 'Not supported' + }); + }); + + mockService.addExecResult(['-m', 'notebook', '--version'], () => { + return Promise.resolve({ + stdout: '', + stderr: 'Not supported' + }); + }); + + } + test('Notebook launch failure', async function () { jupyterExecution = ioc.serviceManager.get(IJupyterExecution); processFactory = ioc.serviceManager.get(IProcessServiceFactory); @@ -1116,22 +1136,8 @@ plt.show()`, ioc.serviceManager.rebindInstance(IApplicationShell, instance(application)); // Change notebook command to fail with some goofy output - const factory = ioc.serviceManager.get(IPythonExecutionFactory); - const service = await factory.create({ pythonPath: ioc.workingInterpreter.path }); - const mockService = service as MockPythonService; - mockService.addExecResult(['-m', 'jupyter', 'notebook', '--version'], () => { - return Promise.resolve({ - stdout: '9.9.9.9', - stderr: 'Not supported' - }); - }); - - mockService.addExecResult(['-m', 'notebook', '--version'], () => { - return Promise.resolve({ - stdout: '', - stderr: 'Not supported' - }); - }); + await disableJupyter(ioc.workingInterpreter.path); + await disableJupyter(ioc.workingInterpreter2.path); // Try creating a notebook let threw = false; From fc88336fbfb552162518a7cc7d1c4fd121bd6aaf Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 12:41:50 -0800 Subject: [PATCH 22/26] Review feedback --- src/client/activation/activationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index e8cf91855e6d..f745780180a9 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -181,7 +181,8 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { - this.cache.get(folder)!.then(a => a.dispose()).ignoreErrors(); + const server = await this.cache.get(folder); + server?.dispose(); // This should remove it from the cache if this is the last instance. } } } From da18351ca4de478f011915589ab3c21c53bb6f31 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 14:20:01 -0800 Subject: [PATCH 23/26] Fix intellisense unit tests and linter problem --- .../intellisense/intellisenseProvider.ts | 2 +- .../languageServices/jediProxyFactory.ts | 3 +- src/test/datascience/mockLanguageServer.ts | 48 ++++++++----------- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index bd1f58e5bd66..0dd06123dbaf 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -160,7 +160,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { const interpreter = activeNotebook ? await activeNotebook.getMatchingInterpreter() : await this.interpreterService.getActiveInterpreter(resource); // See if the resource or the interpreter are different - if (resource !== this.resource || interpreter !== this.interpreter || this.languageServer === undefined) { + if (resource?.toString() !== this.resource?.toString() || interpreter?.path !== this.interpreter?.path || this.languageServer === undefined) { this.resource = resource; this.interpreter = interpreter; diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts index 2e735768050a..5975dc816ede 100644 --- a/src/client/languageServices/jediProxyFactory.ts +++ b/src/client/languageServices/jediProxyFactory.ts @@ -1,7 +1,8 @@ import { Disposable, Uri, workspace } from 'vscode'; + +import { PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; -import { PythonInterpreter } from '../interpreter/contracts'; export class JediFactory implements Disposable { private disposables: Disposable[]; diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts index 5da23f6bf9d2..a886dc95efd8 100644 --- a/src/test/datascience/mockLanguageServer.ts +++ b/src/test/datascience/mockLanguageServer.ts @@ -45,61 +45,46 @@ export class MockLanguageServer implements ILanguageServer { return this.versionId; } - public handleChanges(_document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + this.versionId = document.version; this.applyChanges(changes); + this.resolveNotificationPromise(); } - public handleOpen(document: TextDocument) { - this.contents = document.getText(); - this.versionId = document.version; + public handleOpen(_document: TextDocument) { + noop(); } public provideRenameEdits(_document: TextDocument, _position: Position, _newName: string, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideDefinition(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideHover(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideReferences(_document: TextDocument, _position: Position, _context: ReferenceContext, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideCompletionItems(_document: TextDocument, _position: Position, _token: CancellationToken, _context: CompletionContext): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideDocumentSymbols(_document: TextDocument, _token: CancellationToken): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public provideSignatureHelp(_document: TextDocument, _position: Position, _token: CancellationToken, _context: SignatureHelpContext): ProviderResult { - if (this.notificationPromise) { - this.notificationPromise.resolve(); - } + this.resolveNotificationPromise(); return null; } public dispose(): void { @@ -122,4 +107,11 @@ export class MockLanguageServer implements ILanguageServer { }); this.versionId = this.versionId + 1; } + + private resolveNotificationPromise() { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + this.notificationPromise = undefined; + } + } } From 5d4253226b1827dce84c709170cbc3fa7fd6d240 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 14:53:29 -0800 Subject: [PATCH 24/26] Fix service registry unit tests --- src/test/activation/serviceRegistry.unit.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index bc5e21af8e81..94750034b8ac 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -54,7 +54,8 @@ import { ILanguageServerProxy, IPlatformData, LanguageClientFactory, - LanguageServerActivator + LanguageServerActivator, + ILanguageServerCache } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { IActiveResourceService } from '../../client/common/application/types'; @@ -73,7 +74,7 @@ import { IServiceManager } from '../../client/ioc/types'; import { LanguageServerSurveyBanner } from '../../client/languageServices/languageServerSurveyBanner'; import { ProposeLanguageServerBanner } from '../../client/languageServices/proposeLanguageServerBanner'; -suite('Unit Tests - Activation Service Registry', () => { +suite('Unit Tests - Language Server Activation Service Registry', () => { let serviceManager: IServiceManager; setup(() => { @@ -83,7 +84,7 @@ suite('Unit Tests - Activation Service Registry', () => { test('Ensure services are registered', async () => { registerTypes(instance(serviceManager)); - verify(serviceManager.addSingleton(IExtensionActivationService, LanguageServerExtensionActivationService)).once(); + verify(serviceManager.addSingleton(ILanguageServerCache, LanguageServerExtensionActivationService)).once(); verify(serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension)).once(); verify(serviceManager.add(IExtensionActivationManager, ExtensionActivationManager)).once(); verify(serviceManager.add(ILanguageServerActivator, JediExtensionActivator, LanguageServerActivator.Jedi)).once(); @@ -107,7 +108,7 @@ suite('Unit Tests - Activation Service Registry', () => { verify(serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader)).once(); verify(serviceManager.addSingleton(IPlatformData, PlatformData)).once(); verify(serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions)).once(); - verify(serviceManager.addSingleton(ILanguageServerProxy, LanguageServerProxy)).once(); + verify(serviceManager.add(ILanguageServerProxy, LanguageServerProxy)).once(); verify(serviceManager.add(ILanguageServerManager, LanguageServerManager)).once(); verify(serviceManager.addSingleton(IExtensionSingleActivationService, AATesting)).once(); verify(serviceManager.addSingleton(ILanguageServerOutputChannel, LanguageServerOutputChannel)).once(); From 2f9b1ddc8bc5051dcf12232582424f7e6368da59 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 14:58:01 -0800 Subject: [PATCH 25/26] Fix linter problems --- src/test/activation/serviceRegistry.unit.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index 94750034b8ac..4cd15ae5616e 100644 --- a/src/test/activation/serviceRegistry.unit.test.ts +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -39,11 +39,11 @@ import { registerTypes } from '../../client/activation/serviceRegistry'; import { IDownloadChannelRule, IExtensionActivationManager, - IExtensionActivationService, IExtensionSingleActivationService, ILanguageClientFactory, ILanguageServerActivator, ILanguageServerAnalysisOptions, + ILanguageServerCache, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, @@ -54,8 +54,7 @@ import { ILanguageServerProxy, IPlatformData, LanguageClientFactory, - LanguageServerActivator, - ILanguageServerCache + LanguageServerActivator } from '../../client/activation/types'; import { ActiveResourceService } from '../../client/common/application/activeResource'; import { IActiveResourceService } from '../../client/common/application/types'; From 03c986946e36dcf38df853db444b595818f5c9d8 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Wed, 27 Nov 2019 15:44:33 -0800 Subject: [PATCH 26/26] Fix smoke test for language server. --- .vscode/launch.json | 24 +++++++++++++++++++ .../activation/languageServer/manager.ts | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9c565a3be2a4..9da198e61514 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -94,6 +94,30 @@ }, "skipFiles": ["/**"] }, + { + // Note, for the smoke test you want to debug, you may need to copy the file, + // rename it and remove a check for only smoke tests. + "name": "Tests (Smoke, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/testMultiRootWkspc/smokeTests", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "env": { + "VSC_PYTHON_CI_TEST_GREP": "Smoke Test" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, { "name": "Tests (Single Workspace, VS Code, *.test.ts)", "type": "extensionHost", diff --git a/src/client/activation/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 1dc3180af3cc..1e97b83e4824 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -26,6 +26,7 @@ export class LanguageServerManager implements ILanguageServerManager { private interpreter: PythonInterpreter | undefined; private middleware: LanguageClientMiddleware | undefined; private disposables: IDisposable[] = []; + private connected: boolean = false; constructor( @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, @@ -56,9 +57,11 @@ export class LanguageServerManager implements ILanguageServerManager { await this.startLanguageServer(); } public connect() { + this.connected = true; this.middleware?.connect(); } public disconnect() { + this.connected = false; this.middleware?.disconnect(); } protected registerCommandHandler() { @@ -87,6 +90,13 @@ export class LanguageServerManager implements ILanguageServerManager { this.languageServerProxy = this.serviceContainer.get(ILanguageServerProxy); const options = await this.analysisOptions!.getAnalysisOptions(); options.middleware = this.middleware = new LanguageClientMiddleware(this.surveyBanner); + + // Make sure the middleware is connected if we restart and we we're already connected. + if (this.connected) { + this.middleware.connect(); + } + + // Then use this middleware to start a new language client. await this.languageServerProxy.start(this.resource, this.interpreter, options); this.loadExtensionIfNecessary(); }