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/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. diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts index 43cf60705dd1..f745780180a9 100644 --- a/src/client/activation/activationService.ts +++ b/src/client/activation/activationService.ts @@ -1,106 +1,131 @@ // 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 { 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 { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types'; +import { Commands } from './languageServer/constants'; +import { RefCountedLanguageServer } from './refCountedLanguageServer'; +import { + IExtensionActivationService, + ILanguageServerActivator, + ILanguageServerCache, + LanguageServerActivator +} from './types'; const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; const workspacePathNameForGlobalWorkspaces = ''; -type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator }; + +interface IActivatedServer { + key: string; + server: ILanguageServerActivator; + jedi: boolean; +} @injectable() -export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable { - private lsActivatedWorkspaces = new Map(); - private currentActivator?: ActivatorInfo; - private jediActivatedOnce: boolean = false; +export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable { + private cache = new Map>(); + private activatedServer?: IActivatedServer; 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( 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 { + // Get a new server and dispose of the old one (might be the same one) 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; + 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 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.deactivate(); } - await this.logStartup(jedi); - let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; - let activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); - this.currentActivator = { jedi, activator }; + // Get the new item + const result = await this.get(resource, interpreter); - 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); + // 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) { + this.activatedServer.server.dispose(); + } + + // 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. + this.activatedServer.server.activate(); + } + + 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); + if (!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.activator.dispose(); + if (this.activatedServer) { + this.activatedServer.server.dispose(); } } @swallowExceptions('Send telemetry for Language Server current selection') @@ -149,19 +174,60 @@ 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.cache.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); + const server = await this.cache.get(folder); + server?.dispose(); // This should remove it from the cache if this is the last instance. } } } + private async onDidChangeInterpreter() { + // 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 { + 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; + } + } + + await this.logStartup(jedi); + let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; + let server = this.serviceContainer.get(ILanguageServerActivator, serverName); + try { + await server.start(resource, interpreter); + } catch (ex) { + if (jedi) { + throw ex; + } + await this.logStartup(jedi); + serverName = LanguageServerActivator.Jedi; + server = this.serviceContainer.get(ILanguageServerActivator, serverName); + await server.start(resource, interpreter); + } + + // Wrap the returned server in something that ref counts it. + return new RefCountedLanguageServer(server, serverName, () => { + // When we finally remove the last ref count, remove from the cache + this.cache.delete(key); + + // Dispose of the actual server. + server.dispose(); + }); + } + private async logStartup(isJedi: boolean): Promise { const outputLine = isJedi ? 'Starting Jedi Python language engine.' @@ -177,10 +243,9 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv return; } const jedi = this.useJedi(); - if (this.currentActivator && this.currentActivator.jedi === jedi) { + if (this.activatedServer && this.activatedServer.jedi === jedi) { return; } - const item = await this.appShell.showInformationMessage( 'Please reload the window switching between language engines.', 'Reload' @@ -189,7 +254,15 @@ 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}-${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/jedi.ts b/src/client/activation/jedi.ts index e9189ffee6f9..1fde50f581a8 100644 --- a/src/client/activation/jedi.ts +++ b/src/client/activation/jedi.ts @@ -1,17 +1,39 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { inject, injectable } from 'inversify'; -import { DocumentFilter, languages } from 'vscode'; +import { + CancellationToken, + CodeLens, + commands, + 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 { 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'; 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'; @@ -25,96 +47,175 @@ 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[]; + 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; + private registrations: IDisposable[] = []; + private objectDefinitionProvider: PythonObjectDefinitionProvider | undefined; + constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { this.context = this.serviceManager.get(IExtensionContext); this.documentSelector = PYTHON; } - public async activate(_resource: Resource): Promise { + public async start(_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)); - context.subscriptions.push(jediFactory); - context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - + const jediFactory = (this.jediFactory = new JediFactory(context.asAbsolutePath('.'), interpreter, this.serviceManager)); context.subscriptions.push(jediFactory); - context.subscriptions.push( - languages.registerRenameProvider(this.documentSelector, new PythonRenameProvider(this.serviceManager)) - ); - const definitionProvider = new PythonDefinitionProvider(jediFactory); + const serviceContainer = this.serviceManager.get(IServiceContainer); - context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, definitionProvider)); - context.subscriptions.push( - languages.registerHoverProvider(this.documentSelector, new PythonHoverProvider(jediFactory)) - ); - context.subscriptions.push( - languages.registerReferenceProvider(this.documentSelector, new PythonReferenceProvider(jediFactory)) - ); - context.subscriptions.push( - languages.registerCompletionItemProvider( - this.documentSelector, - new PythonCompletionItemProvider(jediFactory, this.serviceManager), - '.' - ) - ); - context.subscriptions.push( - languages.registerCodeLensProvider( - this.documentSelector, - this.serviceManager.get(IShebangCodeLensProvider) - ) - ); + 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); + this.objectDefinitionProvider = new PythonObjectDefinitionProvider(jediFactory); + this.symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); + this.signatureProvider = new PythonSignatureProvider(jediFactory); - const onTypeDispatcher = new OnTypeFormattingDispatcher({ - '\n': new OnEnterFormatter(), - ':': new BlockFormatProviders() - }); - const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); - if (onTypeTriggers) { - context.subscriptions.push( - languages.registerOnTypeFormattingEditProvider( - PYTHON, - onTypeDispatcher, - onTypeTriggers.first, - ...onTypeTriggers.more - ) - ); + 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); } - const serviceContainer = this.serviceManager.get(IServiceContainer); - context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); + const testManagementService = this.serviceManager.get(ITestManagementService); + testManagementService + .activate(this.symbolProvider) + .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); + } - const symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); - context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider)); + public deactivate() { + this.registrations.forEach(r => r.dispose()); + this.registrations = []; + } + + public activate() { + if (this.registrations.length === 0 && + this.renameProvider && + this.definitionProvider && + this.hoverProvider && + this.referenceProvider && + this.completionProvider && + this.codeLensProvider && + this.symbolProvider && + this.signatureProvider) { - const pythonSettings = this.serviceManager.get(IConfigurationService).getSettings(); - if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push( - languages.registerSignatureHelpProvider( + // 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, - new PythonSignatureProvider(jediFactory), - '(', - ',' + 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, + '(', + ',' + ) + ); + } } + } - context.subscriptions.push( - languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer)) - ); - - const testManagementService = this.serviceManager.get(ITestManagementService); - testManagementService - .activate(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 { + this.registrations.forEach(r => r.dispose()); if (this.jediFactory) { this.jediFactory.dispose(); } diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts index 0bb2199c9f0c..09d087d73329 100644 --- a/src/client/activation/languageServer/activator.ts +++ b/src/client/activation/languageServer/activator.ts @@ -1,15 +1,36 @@ // 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 { 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 { ILanguageServerActivator, ILanguageServerDownloader, @@ -36,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): Promise { + public async start(resource: Resource, interpreter?: PythonInterpreter): Promise { if (!resource) { resource = this.workspace.hasWorkspaceFolders ? this.workspace.workspaceFolders![0].uri @@ -44,7 +65,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 +104,230 @@ export class LanguageServerExtensionActivator implements ILanguageServerActivato content.runtimeOptions.configProperties['System.Globalization.Invariant'] = true; await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); } + + public activate(): void { + this.manager.connect(); + } + + public deactivate(): void { + this.manager.disconnect(); + } + + 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); + } + + 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) { + 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/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts index 3ba770c093af..817b0fa91bc3 100644 --- a/src/client/activation/languageServer/analysisOptions.ts +++ b/src/client/activation/languageServer/analysisOptions.ts @@ -1,45 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; -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 { IInterpreterService } from '../../interpreter/contracts'; +import { PythonInterpreter } from '../../interpreter/contracts'; import { ILanguageServerAnalysisOptions, ILanguageServerFolderService, ILanguageServerOutputChannel } from '../types'; @injectable() @@ -50,30 +22,27 @@ 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, @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(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService ) { 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); - disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); this.disposables.push(disposable); } @@ -84,11 +53,12 @@ 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 = {}; - const interpreterInfo = 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... @@ -163,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/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/languageClientMiddleware.ts b/src/client/activation/languageServer/languageClientMiddleware.ts new file mode 100644 index 000000000000..5af9e4768e28 --- /dev/null +++ b/src/client/activation/languageServer/languageClientMiddleware.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { + CancellationToken, + CodeAction, + CodeActionContext, + CodeLens, + Command, + CompletionContext, + CompletionItem, + Definition, + DefinitionLink, + Diagnostic, + DocumentHighlight, + DocumentLink, + DocumentSymbol, + FormattingOptions, + Location, + Position, + ProviderResult, + Range, + SignatureHelp, + SymbolInformation, + TextDocument, + TextEdit, + Uri, + WorkspaceEdit +} from 'vscode'; +import { + HandleDiagnosticsSignature, + Middleware, + PrepareRenameSignature, + ProvideCodeActionsSignature, + ProvideCodeLensesSignature, + ProvideCompletionItemsSignature, + ProvideDefinitionSignature, + ProvideDocumentFormattingEditsSignature, + ProvideDocumentHighlightsSignature, + ProvideDocumentLinksSignature, + ProvideDocumentRangeFormattingEditsSignature, + ProvideDocumentSymbolsSignature, + ProvideHoverSignature, + ProvideOnTypeFormattingEditsSignature, + ProvideReferencesSignature, + ProvideRenameEditsSignature, + ProvideSignatureHelpSignature, + ProvideWorkspaceSymbolsSignature, + ResolveCodeLensSignature, + ResolveCompletionItemSignature, + ResolveDocumentLinkSignature +} from 'vscode-languageclient'; + +import { HiddenFilePrefix } from '../../common/constants'; +import { IPythonExtensionBanner } from '../../common/types'; + +export class LanguageClientMiddleware implements Middleware { + private connected = false; // Default to not forwarding to VS code. + + public constructor(private readonly surveyBanner: IPythonExtensionBanner) { + } + + public connect() { + this.connected = true; + } + + 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/languageServer.ts b/src/client/activation/languageServer/languageServerProxy.ts similarity index 82% rename from src/client/activation/languageServer/languageServer.ts rename to src/client/activation/languageServer/languageServerProxy.ts index 05040c935d2e..013965d075e3 100644 --- a/src/client/activation/languageServer/languageServer.ts +++ b/src/client/activation/languageServer/languageServerProxy.ts @@ -1,27 +1,25 @@ // 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 { Commands } from './constants'; +import { ILanguageClientFactory, ILanguageServerProxy, LanguageClientFactory } from '../types'; 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[] = []; @@ -33,8 +31,7 @@ export class LanguageServer implements ILanguageServer { @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(); } @@ -58,9 +55,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) { @@ -77,8 +74,6 @@ export class LanguageServer implements ILanguageServer { sendTelemetryEvent(eventName, telemetryEvent.Measurements, telemetryEvent.Properties); }); } - - this.registerCommands(); await this.registerTestServices(); } else { await this.startupCompleted.promise; @@ -112,13 +107,4 @@ export class LanguageServer implements ILanguageServer { } 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/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts index 91c6da33c718..1e97b83e4824 100644 --- a/src/client/activation/languageServer/manager.ts +++ b/src/client/activation/languageServer/manager.ts @@ -1,52 +1,75 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import '../../common/extensions'; -'use strict'; +import { inject, injectable, named } from 'inversify'; -import { inject, injectable } from 'inversify'; -import '../../common/extensions'; import { traceDecorators } from '../../common/logger'; -import { IDisposable, Resource } 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'; import { captureTelemetry } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension, ILanguageServerManager } from '../types'; +import { + ILanguageServerAnalysisOptions, + ILanguageServerExtension, + ILanguageServerManager, + ILanguageServerProxy +} from '../types'; +import { LanguageClientMiddleware } from './languageClientMiddleware'; @injectable() export class LanguageServerManager implements ILanguageServerManager { - private languageServer?: ILanguageServer; + private languageServerProxy?: ILanguageServerProxy; private resource!: Resource; + 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, - @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.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); + await this.analysisOptions.initialize(resource, interpreter); await this.startLanguageServer(); } + public connect() { + this.connected = true; + this.middleware?.connect(); + } + public disconnect() { + this.connected = false; + this.middleware?.disconnect(); + } protected registerCommandHandler() { 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 +79,25 @@ 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); + 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(); } } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts new file mode 100644 index 000000000000..328b2a5145c9 --- /dev/null +++ b/src/client/activation/refCountedLanguageServer.ts @@ -0,0 +1,98 @@ +// 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 { Resource } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { PythonInterpreter } from '../interpreter/contracts'; +import { ILanguageServerActivator, LanguageServerActivator } from './types'; + +export class RefCountedLanguageServer implements ILanguageServerActivator { + private refCount = 1; + constructor(private impl: ILanguageServerActivator, 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) { + this.disposeCallback(); + } + } + + public start(_resource: Resource, _interpreter: PythonInterpreter | undefined): Promise { + throw new Error('Server should have already been started. Do not start the wrapper.'); + } + + public activate() { + this.impl.activate(); + } + + public deactivate() { + this.impl.deactivate(); + } + + public clearAnalysisCache() { + this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); + } + + 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 89a09d291894..97818ce70e63 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,12 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -'use strict'; - import { ActiveResourceService } from '../common/application/activeResource'; import { IActiveResourceService } from '../common/application/types'; 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'; @@ -21,20 +24,50 @@ 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, + ILanguageServerCache, + ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, + ILanguageServerDownloader, + ILanguageServerExtension, + ILanguageServerFolderService, + ILanguageServerManager, + ILanguageServerOutputChannel, + ILanguageServerPackageService, + ILanguageServerProxy, + IPlatformData, + LanguageClientFactory, + LanguageServerActivator +} 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); @@ -58,7 +91,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader); serviceManager.addSingleton(IPlatformData, PlatformData); serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions); - serviceManager.addSingleton(ILanguageServer, LanguageServer); + 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 078d98760b1a..09f9f10c68de 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,41 @@ export enum LanguageServerActivator { DotNet = 'DotNet' } +// tslint:disable-next-line: interface-name +export interface DocumentHandler { + handleOpen(document: TextDocument): void; + handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void; +} + +// tslint:disable-next-line: interface-name +export interface LanguageServerCommandHandler { + clearAnalysisCache(): void; +} + +export interface ILanguageServer extends + RenameProvider, + DefinitionProvider, + HoverProvider, + ReferenceProvider, + CompletionItemProvider, + CodeLensProvider, + DocumentSymbolProvider, + SignatureHelpProvider, + Partial, + Partial, + IDisposable { +} + export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends IDisposable { - activate(resource: Resource): Promise; +export interface ILanguageServerActivator extends ILanguageServer { + start(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; + activate(): void; + deactivate(): void; +} + +export const ILanguageServerCache = Symbol('ILanguageServerCache'); +export interface ILanguageServerCache { + get(resource: Resource, interpreter?: PythonInterpreter): Promise; } export type FolderVersionPair = { path: string; version: SemVer }; @@ -98,17 +143,20 @@ 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 { readonly onDidChange: Event; - initialize(resource: Resource): Promise; + initialize(resource: Resource, interpreter: PythonInterpreter | undefined): Promise; getAnalysisOptions(): Promise; } 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; + connect(): void; + disconnect(): void; } export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); export interface ILanguageServerExtension extends IDisposable { @@ -116,19 +164,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/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/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/client/datascience/interactive-common/intellisense/baseIntellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts similarity index 66% rename from src/client/datascience/interactive-common/intellisense/baseIntellisenseProvider.ts rename to src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 54f938188f14..0dd06123dbaf 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,20 @@ 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 { 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'; +import { HiddenFileFormatString } from '../../../constants'; +import { IInterpreterService, PythonInterpreter } from '../../../interpreter/contracts'; import { concatMultilineStringInput } from '../../common'; import { Identifiers, Settings } from '../../constants'; import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution, INotebook } from '../../types'; @@ -40,24 +44,36 @@ 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; + private languageServer: ILanguageServer | undefined; + private resource: Resource; + private interpreter: PythonInterpreter | undefined; 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 ) { } @@ -65,6 +81,10 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList if (this.temporaryFile) { this.temporaryFile.dispose(); } + if (this.languageServer) { + this.languageServer.dispose(); + this.languageServer = undefined; + } } public get postMessage(): Event<{ message: string; payload: any }> { @@ -75,27 +95,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 +151,49 @@ 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); + + // See if the resource or the interpreter are different + if (resource?.toString() !== this.resource?.toString() || interpreter?.path !== this.interpreter?.path || this.languageServer === undefined) { + this.resource = resource; + this.interpreter = interpreter; + + // Get an instance of the language server (so we ref count it ) + 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; + } catch (e) { + traceError(e); + } + } + return this.languageServer; + } + protected getDocument(resource?: Uri): Promise { if (!this.documentPromise) { this.documentPromise = createDeferred(); @@ -164,11 +219,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 +443,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 +461,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 +470,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 +479,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 +488,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 +497,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,21 +511,23 @@ export abstract class BaseIntellisenseProvider implements IInteractiveWindowList }; })); - await this.handleChanges(Identifiers.EmptyFileName, document, changes); + await this.handleChanges(document, changes); } } 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(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 +541,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/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index 908a8edcf558..545ed6eee778 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/jupyterCommandFinder.ts b/src/client/datascience/jupyter/jupyterCommandFinder.ts index a37bf2cdcc43..2f47bc23cef8 100644 --- a/src/client/datascience/jupyter/jupyterCommandFinder.ts +++ b/src/client/datascience/jupyter/jupyterCommandFinder.ts @@ -265,6 +265,8 @@ export class JupyterCommandFinderImpl { found.error = firstError; } + // 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); } @@ -362,7 +364,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 575496dbbc52..e9cbaae49eb7 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,6 +313,9 @@ export class JupyterExecutionBase implements IJupyterExecution { // See if we can find the command try { const result = await this.findBestCommand(command, cancelToken); + + // 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); 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/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 8354c81cce98..0aacd3043cef 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; metadata?: nbformat.INotebookMetadata; } 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/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts index 5e18b2396e51..5975dc816ede 100644 --- a/src/client/languageServices/jediProxyFactory.ts +++ b/src/client/languageServices/jediProxyFactory.ts @@ -1,4 +1,6 @@ import { Disposable, Uri, workspace } from 'vscode'; + +import { PythonInterpreter } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; @@ -6,7 +8,7 @@ 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 +29,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 3e549192144c..8194b7760830 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,19 +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()) @@ -310,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() { @@ -359,7 +343,7 @@ export class JediProxy implements Disposable { this.proc.kill(); } // tslint:disable-next-line:no-empty - } catch (ex) {} + } catch (ex) { } this.proc = undefined; } @@ -373,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; @@ -650,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) { @@ -710,14 +694,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. 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[]; -} diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index 4fb64c57d97d..5b5349716644 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -22,7 +22,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 2930e5769de1..e0c110d34e95 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,25 @@ 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 { 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: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 +50,8 @@ suite('Activation - ActivationService', () => { let state: TypeMoq.IMock>; let experiments: TypeMoq.IMock; let workspaceConfig: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let interpreterChangedHandler!: Function; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); appShell = TypeMoq.Mock.ofType(); @@ -61,6 +72,12 @@ 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 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)); @@ -93,6 +110,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); @@ -112,9 +132,12 @@ suite('Activation - ActivationService', () => { lsSupported: boolean = true ) { activator - .setup(a => a.activate(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; @@ -328,6 +351,142 @@ suite('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.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .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 + .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 % 2 === 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.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup(a => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + let connectCount = 0; + activator + .setup(a => a.activate()) + .returns(() => { + connectCount = connectCount + 1; + }); + 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(); + + // Hold onto the second item and switch two more times. Verify that + // reconnect happens + const server = await activationService.get(folder1.uri); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(3, 'Reconnect is not happening'); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(4, 'Reconnect is not happening'); + server.dispose(); + }); if (!jediIsEnabled) { test('Revert to jedi when LS activation fails', async () => { pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); @@ -351,7 +510,7 @@ suite('Activation - ActivationService', () => { .returns(() => activatorDotNet.object) .verifiable(TypeMoq.Times.once()); activatorDotNet - .setup(a => a.activate(undefined)) + .setup(a => a.start(undefined, undefined)) .returns(() => Promise.reject(new Error(''))) .verifiable(TypeMoq.Times.once()); serviceContainer @@ -364,7 +523,11 @@ suite('Activation - ActivationService', () => { .returns(() => activatorJedi.object) .verifiable(TypeMoq.Times.once()); activatorJedi - .setup(a => a.activate(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()); @@ -380,9 +543,12 @@ suite('Activation - ActivationService', () => { resource: Resource ) { activator - .setup(a => a.activate(TypeMoq.It.isValue(resource))) + .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([])); @@ -451,12 +617,12 @@ suite('Activation - ActivationService', () => { activator3 .setup(d => d.dispose()) .verifiable(TypeMoq.Times.once()); - workspaceFoldersChangedHandler.call(activationService); + await workspaceFoldersChangedHandler.call(activationService); workspaceService.verifyAll(); 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); @@ -467,7 +633,7 @@ suite('Activation - ActivationService', () => { .returns(() => activator1.object) .verifiable(TypeMoq.Times.once()); activator1 - .setup(a => a.activate(folder1.uri)) + .setup(a => a.start(folder1.uri, undefined)) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); experiments @@ -476,6 +642,7 @@ suite('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(); @@ -485,9 +652,12 @@ suite('Activation - ActivationService', () => { .returns(() => activator2.object) .verifiable(TypeMoq.Times.once()); activator2 - .setup(a => a.activate(folder2.uri)) + .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) @@ -495,6 +665,7 @@ suite('Activation - ActivationService', () => { await activationService.activate(folder2.uri); serviceContainer.verifyAll(); activator1.verifyAll(); + activator1.verify(a => a.activate(), TypeMoq.Times.exactly(2)); activator2.verifyAll(); experiments.verifyAll(); }); @@ -514,6 +685,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 +697,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 +738,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); @@ -582,8 +760,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()); @@ -650,6 +828,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 +840,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 +881,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 +1004,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 +1016,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 +1057,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/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts index 7f035270076b..637fb2ebc074 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); + await activator.start(undefined); - verify(manager.start(undefined)).once(); + verify(manager.start(undefined, undefined)).once(); verify(workspaceService.hasWorkspaceFolders).once(); }); test('Manager must be disposed', async () => { @@ -70,14 +70,20 @@ 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)).thenResolve(); + when(manager.start(undefined, undefined)).thenResolve(); when(settings.downloadLanguageServer).thenReturn(false); - await activator.activate(undefined); + await activator.start(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,15 +94,15 @@ 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); when(fs.fileExists(mscorlib)).thenResolve(true); - await activator.activate(undefined); + await activator.start(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 +114,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); @@ -116,17 +122,17 @@ 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(); 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 +140,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); + await activator.start(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/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts index 2efd2c6ba1fe..acaf54141818 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,15 @@ 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,8 +56,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; let pathUtils: IPathUtils; @@ -66,8 +65,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(); lsOutputChannel @@ -77,49 +74,40 @@ suite('Language Server - Analysis Options', () => { lsFolderService = mock(LanguageServerFolderService); analysisOptions = new TestClass(context.object, instance(envVarsProvider), instance(configurationService), - instance(workspace), instance(surveyBanner), - instance(interpreterService), lsOutputChannel.object, + instance(workspace), + lsOutputChannel.object, instance(pathUtils), instance(lsFolderService)); }); 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); + 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); + 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); @@ -178,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); + 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/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..40601ccd57f9 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(); @@ -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()); @@ -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/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/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts index c1fdf216b75c..c3eadf070870 100644 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -1,20 +1,24 @@ // 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 { 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); @@ -25,19 +29,22 @@ 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; + let surveyBanner: IPythonExtensionBanner; const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; setup(() => { serviceContainer = mock(ServiceContainer); analysisOptions = mock(LanguageServerAnalysisOptions); - languageServer = mock(LanguageServer); + languageServer = mock(LanguageServerProxy); lsExtension = mock(LanguageServerExtension); + surveyBanner = mock(ProposeLanguageServerBanner); manager = new LanguageServerManager( instance(serviceContainer), instance(analysisOptions), - instance(lsExtension) + instance(lsExtension), + instance(surveyBanner) ); }); @@ -54,18 +61,18 @@ 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(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.initialize(resource, undefined)).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 +87,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 +98,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 +119,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 +140,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 +158,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/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(); diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts index fd69a7c50c24..4cd15ae5616e 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'; @@ -33,12 +39,11 @@ import { registerTypes } from '../../client/activation/serviceRegistry'; import { IDownloadChannelRule, IExtensionActivationManager, - IExtensionActivationService, IExtensionSingleActivationService, ILanguageClientFactory, - ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, + ILanguageServerCache, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, @@ -46,6 +51,7 @@ import { ILanguageServerManager, ILanguageServerOutputChannel, ILanguageServerPackageService, + ILanguageServerProxy, IPlatformData, LanguageClientFactory, LanguageServerActivator @@ -53,7 +59,13 @@ import { import { ActiveResourceService } from '../../client/common/application/activeResource'; import { IActiveResourceService } from '../../client/common/application/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'; @@ -61,7 +73,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(() => { @@ -71,7 +83,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(); @@ -95,7 +107,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.add(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 ee93c8f30096..743d2bddcce1 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'; @@ -18,11 +17,49 @@ import { Uri, ViewColumn, WorkspaceConfiguration, - WorkspaceFolder + WorkspaceFolder, + WorkspaceFoldersChangeEvent } from 'vscode'; import * as vsls from 'vsls/vscode'; -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; +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, + ILanguageServerPackageService, + ILanguageServerProxy, + LanguageServerActivator +} 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, @@ -42,6 +79,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'; @@ -82,6 +121,7 @@ import { TerminalActivationProviders } from '../../client/common/terminal/types'; import { + BANNER_NAME_LS_SURVEY, IAsyncDisposableRegistry, IConfigurationService, ICurrentProcess, @@ -91,6 +131,7 @@ import { ILogger, IPathUtils, IPersistentStateFactory, + IPythonExtensionBanner, IsWindows } from '../../client/common/types'; import { Deferred, sleep } from '../../client/common/utils/async'; @@ -110,9 +151,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'; @@ -242,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'; @@ -253,8 +293,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'; @@ -283,6 +323,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', @@ -292,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; @@ -308,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; } @@ -399,9 +452,13 @@ 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.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); 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); @@ -418,6 +475,28 @@ 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); + 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); @@ -471,6 +550,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); @@ -480,6 +561,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); @@ -604,6 +686,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); } @@ -718,10 +801,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 d50f3d1e1149..2974f3326842 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -7,6 +7,7 @@ import { IDisposable } from 'monaco-editor'; import { Disposable } from 'vscode'; 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'; @@ -20,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(); }); @@ -108,6 +107,46 @@ 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 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); + typeCode(getInteractiveEditor(wrapper), 'print'); + 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) => { if (ioc.mockJupyter) { // This test only works when mocking. 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/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 b417d347765f..dd951abef464 100644 --- a/src/test/datascience/mockLanguageClient.ts +++ b/src/test/datascience/mockLanguageClient.ts @@ -1,14 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { - CancellationToken, - DiagnosticCollection, - Disposable, - Event, - OutputChannel, - TextDocumentContentChangeEvent -} from 'vscode'; +import { CancellationToken, DiagnosticCollection, Disposable, Event, OutputChannel } from 'vscode'; import { Code2ProtocolConverter, CompletionItem, @@ -24,6 +17,7 @@ import { NotificationHandler0, NotificationType, NotificationType0, + Position, Protocol2CodeConverter, RequestHandler, RequestHandler0, @@ -33,40 +27,45 @@ import { ServerOptions, StateChangeEvent, StaticFeature, + 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 { MockProtocolConverter } from './mockProtocolConverter'; +import { MockCode2ProtocolConverter } from './mockCode2ProtocolConverter'; +import { MockProtocol2CodeConverter } from './mockProtocol2CodeConverter'; // 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; + 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 { + 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 +82,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 @@ -144,10 +143,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.'); @@ -209,14 +208,15 @@ export class MockLanguageClient extends LanguageClient { } private applyChanges(changes: TextDocumentContentChangeEvent[]) { - changes.forEach(c => { - const before = this.contents.substr(0, c.rangeOffset); - const after = this.contents.substr(c.rangeOffset + c.rangeLength); + changes.forEach((c: TextDocumentContentChangeEvent) => { + 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}`; }); } - private getDocumentCompletions() : CompletionItem[] { + private getDocumentCompletions(): CompletionItem[] { const lines = this.contents.splitLines(); return lines.map(l => { return { @@ -226,4 +226,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/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts index f9daf07ed2ff..a886dc95efd8 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 start(_resource: Uri | undefined, _options: LanguageClientOptions): Promise { - if (!this.mockLanguageClient) { - this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); - } - return Promise.resolve(); + public getDocumentContents(): string { + return this.contents; + } + + public getVersionId(): number | null { + return this.versionId; + } + + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + this.versionId = document.version; + this.applyChanges(changes); + this.resolveNotificationPromise(); + } + + public handleOpen(_document: TextDocument) { + noop(); + } + + public provideRenameEdits(_document: TextDocument, _position: Position, _newName: string, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public provideDefinition(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public provideHover(_document: TextDocument, _position: Position, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; } - public loadExtension(_args?: {} | undefined): void { - throw new Error('Method not implemented.'); + public provideReferences(_document: TextDocument, _position: Position, _context: ReferenceContext, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; } - public dispose(): void | undefined { - this.mockLanguageClient = undefined; + public provideCompletionItems(_document: TextDocument, _position: Position, _token: CancellationToken, _context: CompletionContext): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public provideDocumentSymbols(_document: TextDocument, _token: CancellationToken): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public provideSignatureHelp(_document: TextDocument, _position: Position, _token: CancellationToken, _context: SignatureHelpContext): ProviderResult { + this.resolveNotificationPromise(); + return null; + } + public dispose(): void { + noop(); + } + + public disconnect(): void { + noop(); } + public reconnect(): 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; + } + + private resolveNotificationPromise() { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + this.notificationPromise = undefined; + } + } } 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; + } + +} 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 ee2707a48b98..730f29519667 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -534,7 +534,8 @@ suite('DataScience notebook tests', () => { // Create again, we should get the same server from the cache const server2 = await createNotebook(true); - assert.ok(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 = { @@ -549,10 +550,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.ok(server3, 'Server not created'); - assert.ok(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`); } @@ -1099,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); @@ -1115,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;