diff --git a/src/client/activation/common/activatorBase.ts b/src/client/activation/common/activatorBase.ts index 43ab2777cfc6..7a3c972ef960 100644 --- a/src/client/activation/common/activatorBase.ts +++ b/src/client/activation/common/activatorBase.ts @@ -18,7 +18,6 @@ import { SignatureHelpContext, SymbolInformation, TextDocument, - TextDocumentContentChangeEvent, WorkspaceEdit } from 'vscode'; import * as vscodeLanguageClient from 'vscode-languageclient/node'; @@ -72,34 +71,25 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi this.manager.disconnect(); } - public handleOpen(document: TextDocument): void { + public get connection() { const languageClient = this.getLanguageClient(); if (languageClient) { - languageClient.sendNotification( - vscodeLanguageClient.DidOpenTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asOpenTextDocumentParams(document) - ); + // Return an object that looks like a connection + return { + sendNotification: languageClient.sendNotification.bind(languageClient), + sendRequest: languageClient.sendRequest.bind(languageClient), + sendProgress: languageClient.sendProgress.bind(languageClient), + onRequest: languageClient.onRequest.bind(languageClient), + onNotification: languageClient.onNotification.bind(languageClient), + onProgress: languageClient.onProgress.bind(languageClient) + }; } } - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void { + public get capabilities() { const languageClient = this.getLanguageClient(); if (languageClient) { - // If the language client doesn't support incremental, just send the whole document - if (this.textDocumentSyncKind === vscodeLanguageClient.TextDocumentSyncKind.Full) { - languageClient.sendNotification( - vscodeLanguageClient.DidChangeTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asChangeTextDocumentParams(document) - ); - } else { - languageClient.sendNotification( - vscodeLanguageClient.DidChangeTextDocumentNotification.type, - languageClient.code2ProtocolConverter.asChangeTextDocumentParams({ - document, - contentChanges: changes - }) - ); - } + return languageClient.initializeResult?.capabilities; } } @@ -169,23 +159,6 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi } } - private get textDocumentSyncKind(): vscodeLanguageClient.TextDocumentSyncKind { - const languageClient = this.getLanguageClient(); - if (languageClient?.initializeResult?.capabilities?.textDocumentSync) { - const syncOptions = languageClient.initializeResult.capabilities.textDocumentSync; - const syncKind = - syncOptions !== undefined && syncOptions.hasOwnProperty('change') - ? (syncOptions as vscodeLanguageClient.TextDocumentSyncOptions).change - : syncOptions; - if (syncKind !== undefined) { - return syncKind as vscodeLanguageClient.TextDocumentSyncKind; - } - } - - // Default is full if not provided - return vscodeLanguageClient.TextDocumentSyncKind.Full; - } - private async handleProvideRenameEdits( document: TextDocument, position: Position, diff --git a/src/client/activation/jedi/multiplexingActivator.ts b/src/client/activation/jedi/multiplexingActivator.ts index 454ea7bbf898..70f5d664fc62 100644 --- a/src/client/activation/jedi/multiplexingActivator.ts +++ b/src/client/activation/jedi/multiplexingActivator.ts @@ -9,8 +9,7 @@ import { Position, ReferenceContext, SignatureHelpContext, - TextDocument, - TextDocumentContentChangeEvent + TextDocument } from 'vscode'; // tslint:disable-next-line: import-name import { IWorkspaceService } from '../../common/application/types'; @@ -73,15 +72,15 @@ export class MultiplexingJediLanguageServerActivator implements ILanguageServerA return this.onDidChangeCodeLensesEmitter.event; } - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { - if (this.realLanguageServer && this.realLanguageServer.handleChanges) { - this.realLanguageServer.handleChanges(document, changes); + public get connection() { + if (this.realLanguageServer) { + return this.realLanguageServer.connection; } } - public handleOpen(document: TextDocument) { - if (this.realLanguageServer && this.realLanguageServer.handleOpen) { - this.realLanguageServer.handleOpen(document); + public get capabilities() { + if (this.realLanguageServer) { + return this.realLanguageServer.capabilities; } } diff --git a/src/client/activation/refCountedLanguageServer.ts b/src/client/activation/refCountedLanguageServer.ts index f589d6257ac7..52e76bf6a4de 100644 --- a/src/client/activation/refCountedLanguageServer.ts +++ b/src/client/activation/refCountedLanguageServer.ts @@ -17,7 +17,6 @@ import { SignatureHelpContext, SymbolInformation, TextDocument, - TextDocumentContentChangeEvent, WorkspaceEdit } from 'vscode'; @@ -65,12 +64,12 @@ export class RefCountedLanguageServer implements ILanguageServerActivator { this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop(); } - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { - this.impl.handleChanges ? this.impl.handleChanges(document, changes) : noop(); + public get connection() { + return this.impl.connection; } - public handleOpen(document: TextDocument) { - this.impl.handleOpen ? this.impl.handleOpen(document) : noop(); + public get capabilities() { + return this.impl.capabilities; } public provideRenameEdits( diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 9d8549b9b8a1..68316191360d 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -13,11 +13,10 @@ import { HoverProvider, ReferenceProvider, RenameProvider, - SignatureHelpProvider, - TextDocument, - TextDocumentContentChangeEvent + SignatureHelpProvider } from 'vscode'; import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; +import * as lsp from 'vscode-languageserver-protocol'; import { NugetPackage } from '../common/nuget/types'; import { IDisposable, IOutputChannel, LanguageServerDownloadChannels, Resource } from '../common/types'; import { PythonEnvironment } from '../pythonEnvironments/info'; @@ -73,17 +72,20 @@ export enum LanguageServerType { export const DotNetLanguageServerFolder = 'languageServer'; export const NodeLanguageServerFolder = 'nodeLanguageServer'; -// 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; } +/** + * This interface is a subset of the vscode-protocol connection interface. + * It's the minimum set of functions needed in order to talk to a language server. + */ +export type ILanguageServerConnection = Pick< + lsp.ProtocolConnection, + 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' +>; + export interface ILanguageServer extends RenameProvider, DefinitionProvider, @@ -93,9 +95,11 @@ export interface ILanguageServer CodeLensProvider, DocumentSymbolProvider, SignatureHelpProvider, - Partial, Partial, - IDisposable {} + IDisposable { + readonly connection?: ILanguageServerConnection; + readonly capabilities?: lsp.ServerCapabilities; +} export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); export interface ILanguageServerActivator extends ILanguageServer { diff --git a/src/client/datascience/api/jupyterIntegration.ts b/src/client/datascience/api/jupyterIntegration.ts index 36bb40d93ff0..98c154fe1706 100644 --- a/src/client/datascience/api/jupyterIntegration.ts +++ b/src/client/datascience/api/jupyterIntegration.ts @@ -7,9 +7,12 @@ import { inject, injectable } from 'inversify'; import { dirname } from 'path'; -import { CancellationToken, Event, Uri } from 'vscode'; +import { CancellationToken, Disposable, Event, Uri } from 'vscode'; +import * as lsp from 'vscode-languageserver-protocol'; +import { ILanguageServerCache, ILanguageServerConnection } from '../../activation/types'; import { InterpreterUri } from '../../common/installer/types'; import { IExtensions, IInstaller, InstallerResponse, Product, Resource } from '../../common/types'; +import { isResource } from '../../common/utils/misc'; import { getDebugpyPackagePath } from '../../debugger/extension/adapter/remoteLaunchers'; import { IEnvironmentActivationService } from '../../interpreter/activation/types'; import { IInterpreterQuickPickItem, IInterpreterSelector } from '../../interpreter/configuration/types'; @@ -18,6 +21,11 @@ import { IWindowsStoreInterpreter } from '../../interpreter/locators/types'; import { WindowsStoreInterpreter } from '../../pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; import { PythonEnvironment } from '../../pythonEnvironments/info'; +export interface ILanguageServer extends Disposable { + readonly connection: ILanguageServerConnection; + readonly capabilities: lsp.ServerCapabilities; +} + type PythonApiForJupyterExtension = { /** * IInterpreterService @@ -57,9 +65,14 @@ type PythonApiForJupyterExtension = { * Returns path to where `debugpy` is. In python extension this is `/pythonFiles/lib/python`. */ getDebuggerPath(): Promise; + /** + * Returns a ILanguageServer that can be used for communicating with a language server process. + * @param resource file that determines which connection to return + */ + getLanguageServer(resource?: InterpreterUri): Promise; }; -type JupyterExtensionApi = { +export type JupyterExtensionApi = { registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; }; @@ -71,19 +84,11 @@ export class JupyterExtensionIntegration { @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, @inject(IInstaller) private readonly installer: IInstaller, - @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(ILanguageServerCache) private readonly languageServerCache: ILanguageServerCache ) {} - public async integrateWithJupyterExtension(): Promise { - const jupyterExtension = this.extensions.getExtension('ms-ai-tools.jupyter'); - if (!jupyterExtension) { - return; - } - await jupyterExtension.activate(); - if (!jupyterExtension.isActive) { - return; - } - const jupyterExtensionApi = jupyterExtension.exports; + public registerApi(jupyterExtensionApi: JupyterExtensionApi) { jupyterExtensionApi.registerPythonApi({ onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter, getActiveInterpreter: async (resource?: Uri) => this.interpreterService.getActiveInterpreter(resource), @@ -104,7 +109,34 @@ export class JupyterExtensionIntegration { resource?: InterpreterUri, cancel?: CancellationToken ): Promise => this.installer.install(product, resource, cancel), - getDebuggerPath: async () => dirname(getDebugpyPackagePath()) + getDebuggerPath: async () => dirname(getDebugpyPackagePath()), + getLanguageServer: async (r) => { + const resource = isResource(r) ? r : undefined; + const interpreter = !isResource(r) ? r : undefined; + const client = await this.languageServerCache.get(resource, interpreter); + + // Some langauge servers don't support the connection yet. (like Jedi until we switch to LSP) + if (client && client.connection && client.capabilities) { + return { + connection: client.connection, + capabilities: client.capabilities, + dispose: client.dispose + }; + } + return undefined; + } }); } + + public async integrateWithJupyterExtension(): Promise { + const jupyterExtension = this.extensions.getExtension('ms-ai-tools.jupyter'); + if (!jupyterExtension) { + return; + } + await jupyterExtension.activate(); + if (!jupyterExtension.isActive) { + return; + } + this.registerApi(jupyterExtension.exports); + } } diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts index 64909c7e06a5..9124ccd5a842 100644 --- a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -22,7 +22,6 @@ import { import { CancellationToken } from 'vscode-jsonrpc'; import * as vscodeLanguageClient from 'vscode-languageclient/node'; import { concatMultilineString } from '../../../../datascience-ui/common'; -import { ILanguageServer, ILanguageServerCache } from '../../../activation/types'; import { IWorkspaceService } from '../../../common/application/types'; import { CancellationError } from '../../../common/cancellation'; import { traceError, traceWarning } from '../../../common/logger'; @@ -34,6 +33,7 @@ import { HiddenFileFormatString } from '../../../constants'; import { IInterpreterService } from '../../../interpreter/contracts'; import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { sendTelemetryWhenDone } from '../../../telemetry'; +import { JupyterExtensionIntegration } from '../../api/jupyterIntegration'; import { Identifiers, Settings, Telemetry } from '../../constants'; import { ICell, @@ -65,6 +65,7 @@ import { convertToVSCodeCompletionItem } from './conversion'; import { IntellisenseDocument } from './intellisenseDocument'; +import { NotebookLanguageServer } from './notebookLanguageServer'; // These regexes are used to get the text from jupyter output by recognizing escape charactor \x1b const DocStringRegex = /\x1b\[1;31mDocstring:\x1b\[0m\s+([\s\S]*?)\r?\n\x1b\[1;31m/; @@ -101,7 +102,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { private notebookType: 'interactive' | 'native' = 'interactive'; private potentialResource: Uri | undefined; private sentOpenDocument: boolean = false; - private languageServer: ILanguageServer | undefined; + private languageServer: NotebookLanguageServer | undefined; private resource: Resource; private interpreter: PythonEnvironment | undefined; @@ -110,7 +111,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, @inject(INotebookProvider) private notebookProvider: INotebookProvider, @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache, + @inject(JupyterExtensionIntegration) private jupyterApiProvider: JupyterExtensionIntegration, @inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableProvider: IJupyterVariables ) {} @@ -198,7 +199,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { return this.documentPromise.promise; } - protected async getLanguageServer(token: CancellationToken): Promise { + protected async getLanguageServer(token: CancellationToken): Promise { // Resource should be our potential resource if its set. Otherwise workspace root const resource = this.potentialResource || @@ -225,22 +226,26 @@ export class IntellisenseProvider implements IInteractiveWindowListener { // Get an instance of the language server (so we ref count it ) try { - const languageServer = await this.languageServerCache.get(resource, interpreter); + const languageServer = await NotebookLanguageServer.create( + this.jupyterApiProvider, + 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 (document && languageServer) { // 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()); + languageServer.sendOpen(document); + languageServer.sendChanges(document, document.getFullContentChanges()); } else { this.sentOpenDocument = true; - languageServer.handleOpen(document); + languageServer.sendOpen(document); } } @@ -352,7 +357,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener { token: CancellationToken ): Promise { const [languageServer, document] = await Promise.all([this.getLanguageServer(token), this.getDocument()]); - if (languageServer && languageServer.resolveCompletionItem && document) { + if (languageServer && document) { const vscodeCompItem: CompletionItem = convertToVSCodeCompletionItem(item); // Needed by Jedi in completionSource.ts to resolve the item @@ -378,12 +383,12 @@ export class IntellisenseProvider implements IInteractiveWindowListener { if (document) { // Broadcast an update to the language server const languageServer = await this.getLanguageServer(CancellationToken.None); - if (languageServer && languageServer.handleChanges && languageServer.handleOpen) { + if (languageServer) { if (!this.sentOpenDocument) { this.sentOpenDocument = true; - return languageServer.handleOpen(document); + return languageServer.sendOpen(document); } else { - return languageServer.handleChanges(document, changes); + return languageServer.sendChanges(document, changes); } } } diff --git a/src/client/datascience/interactive-common/intellisense/notebookLanguageServer.ts b/src/client/datascience/interactive-common/intellisense/notebookLanguageServer.ts new file mode 100644 index 000000000000..33d635b17e21 --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/notebookLanguageServer.ts @@ -0,0 +1,154 @@ +import { + CancellationToken, + CompletionContext, + CompletionItem, + Disposable, + Position, + SignatureHelpContext, + TextDocument, + TextDocumentContentChangeEvent +} from 'vscode'; +import * as c2p from 'vscode-languageclient/lib/common/codeConverter'; +import * as p2c from 'vscode-languageclient/lib/common/protocolConverter'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; +import * as lsp from 'vscode-languageserver-protocol'; +import { ILanguageServerConnection } from '../../../activation/types'; +import { Resource } from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ILanguageServer, JupyterExtensionIntegration } from '../../api/jupyterIntegration'; + +/** + * Class that wraps a language server for use by webview based notebooks + */ +export class NotebookLanguageServer implements Disposable { + private code2ProtocolConverter = c2p.createConverter(); + private protocol2CodeConverter = p2c.createConverter(); + private connection: ILanguageServerConnection; + private capabilities: lsp.ServerCapabilities; + private disposeConnection: () => void; + private constructor(ls: ILanguageServer) { + this.connection = ls.connection; + this.capabilities = ls.capabilities; + this.disposeConnection = ls.dispose.bind(ls); + } + + public static async create( + jupyterApiProvider: JupyterExtensionIntegration, + resource: Resource, + interpreter: PythonEnvironment | undefined + ): Promise { + // Create a server wrapper if we can get a connection to a language server + const deferred = createDeferred(); + jupyterApiProvider.registerApi({ + registerPythonApi: (api) => { + api.getLanguageServer(interpreter ? interpreter : resource) + .then((c) => { + if (c) { + deferred.resolve(new NotebookLanguageServer(c)); + } else { + deferred.resolve(undefined); + } + }) + .catch(deferred.reject); + } + }); + return deferred.promise; + } + + public dispose() { + this.disposeConnection(); + } + + public sendOpen(document: TextDocument) { + this.connection.sendNotification( + vscodeLanguageClient.DidOpenTextDocumentNotification.type, + this.code2ProtocolConverter.asOpenTextDocumentParams(document) + ); + } + + public sendChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + // If the language client doesn't support incremental, just send the whole document + if (this.textDocumentSyncKind === vscodeLanguageClient.TextDocumentSyncKind.Full) { + this.connection.sendNotification( + vscodeLanguageClient.DidChangeTextDocumentNotification.type, + this.code2ProtocolConverter.asChangeTextDocumentParams(document) + ); + } else { + this.connection.sendNotification( + vscodeLanguageClient.DidChangeTextDocumentNotification.type, + this.code2ProtocolConverter.asChangeTextDocumentParams({ + document, + contentChanges: changes + }) + ); + } + } + + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken, + context: CompletionContext + ) { + const args = this.code2ProtocolConverter.asCompletionParams(document, position, context); + const result = await this.connection.sendRequest(vscodeLanguageClient.CompletionRequest.type, args, token); + if (result) { + return this.protocol2CodeConverter.asCompletionResult(result); + } + } + + public async provideSignatureHelp( + document: TextDocument, + position: Position, + token: CancellationToken, + _context: SignatureHelpContext + ) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: this.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: this.code2ProtocolConverter.asPosition(position) + }; + const result = await this.connection.sendRequest(vscodeLanguageClient.SignatureHelpRequest.type, args, token); + if (result) { + return this.protocol2CodeConverter.asSignatureHelp(result); + } + } + + public async provideHover(document: TextDocument, position: Position, token: CancellationToken) { + const args: vscodeLanguageClient.TextDocumentPositionParams = { + textDocument: this.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: this.code2ProtocolConverter.asPosition(position) + }; + const result = await this.connection.sendRequest(vscodeLanguageClient.HoverRequest.type, args, token); + if (result) { + return this.protocol2CodeConverter.asHover(result); + } + } + + public async resolveCompletionItem(item: CompletionItem, token: CancellationToken) { + const result = await this.connection.sendRequest( + vscodeLanguageClient.CompletionResolveRequest.type, + this.code2ProtocolConverter.asCompletionItem(item), + token + ); + if (result) { + return this.protocol2CodeConverter.asCompletionItem(result); + } + } + + private get textDocumentSyncKind(): vscodeLanguageClient.TextDocumentSyncKind { + if (this.capabilities.textDocumentSync) { + const syncOptions = this.capabilities.textDocumentSync; + const syncKind = + syncOptions !== undefined && syncOptions.hasOwnProperty('change') + ? (syncOptions as vscodeLanguageClient.TextDocumentSyncOptions).change + : syncOptions; + if (syncKind !== undefined) { + return syncKind as vscodeLanguageClient.TextDocumentSyncKind; + } + } + + // Default is full if not provided + return vscodeLanguageClient.TextDocumentSyncKind.Full; + } +} diff --git a/src/datascience-ui/interactive-common/intellisenseProvider.ts b/src/datascience-ui/interactive-common/intellisenseProvider.ts index 035340ca480f..10636423b75c 100644 --- a/src/datascience-ui/interactive-common/intellisenseProvider.ts +++ b/src/datascience-ui/interactive-common/intellisenseProvider.ts @@ -119,7 +119,7 @@ export class IntellisenseProvider // Our code strips out _documentPosition and possibly other items that are too large to send // so instead of returning the new resolve completion item, just return the old item with documentation added in // which is what we are resolving the item to get - return Promise.resolve({ ...item, documentation: newItem.documentation }); + return Promise.resolve({ ...item, documentation: newItem?.documentation }); } else { return Promise.resolve(item); } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 326a6824c8ff..cf84425ee398 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -186,6 +186,7 @@ import { Architecture } from '../../client/common/utils/platform'; import { EnvironmentVariablesService } from '../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../client/common/variables/types'; +import { JupyterExtensionIntegration } from '../../client/datascience/api/jupyterIntegration'; import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; import { JupyterCommandLineSelectorCommand } from '../../client/datascience/commands/commandLineSelector'; import { CommandRegistry } from '../../client/datascience/commands/commandRegistry'; @@ -760,6 +761,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv ); + this.serviceManager.addSingleton( + JupyterExtensionIntegration, + JupyterExtensionIntegration + ); this.serviceManager.addSingleton(ITerminalManager, TerminalManager); this.serviceManager.addSingleton(ILanguageServerProxy, MockLanguageServerProxy); this.serviceManager.addSingleton( diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts index 8089af05eeb1..8e783d7a1ee0 100644 --- a/src/test/datascience/intellisense.unit.test.ts +++ b/src/test/datascience/intellisense.unit.test.ts @@ -10,7 +10,7 @@ import { Uri } from 'vscode'; import { LanguageServerType } from '../../client/activation/types'; import { IWorkspaceService } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; -import { IConfigurationService } from '../../client/common/types'; +import { IConfigurationService, IInstaller } from '../../client/common/types'; import { Identifiers } from '../../client/datascience/constants'; import { IntellisenseDocument } from '../../client/datascience/interactive-common/intellisense/intellisenseDocument'; import { IntellisenseProvider } from '../../client/datascience/interactive-common/intellisense/intellisenseProvider'; @@ -21,10 +21,15 @@ import { } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; import { ICell, IDataScienceFileSystem, INotebookProvider } from '../../client/datascience/types'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { IInterpreterSelector } from '../../client/interpreter/configuration/types'; import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../client/interpreter/locators/types'; import { createEmptyCell, generateTestCells } from '../../datascience-ui/interactive-common/mainState'; import { generateReverseChange, IMonacoTextModel } from '../../datascience-ui/react-common/monacoHelpers'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockExtensions } from './mockExtensions'; +import { MockJupyterExtensionIntegration } from './mockJupyterExtensionIntegration'; import { MockLanguageServerCache } from './mockLanguageServerCache'; // tslint:disable:no-any unified-signatures @@ -76,13 +81,27 @@ suite('DataScience Intellisense Unit Tests', () => { .returns((f1: Uri, f2: Uri) => { return f1?.fsPath?.toLowerCase() === f2.fsPath?.toLowerCase(); }); + const selector = TypeMoq.Mock.ofType(); + const storeInterpreter = TypeMoq.Mock.ofType(); + const installer = TypeMoq.Mock.ofType(); + const envService = TypeMoq.Mock.ofType(); + + const extensionRegister = new MockJupyterExtensionIntegration( + new MockExtensions(), + interpreterService.object, + selector.object, + storeInterpreter.object, + installer.object, + envService.object, + languageServerCache + ); intellisenseProvider = new IntellisenseProvider( workspaceService.object, fileSystem.object, notebookProvider.object, interpreterService.object, - languageServerCache, + extensionRegister, instance(variableProvider) ); intellisenseDocument = await intellisenseProvider.getDocument(); diff --git a/src/test/datascience/mockJupyterExtensionIntegration.ts b/src/test/datascience/mockJupyterExtensionIntegration.ts new file mode 100644 index 000000000000..047bba26c0d0 --- /dev/null +++ b/src/test/datascience/mockJupyterExtensionIntegration.ts @@ -0,0 +1,29 @@ +import { ILanguageServerCache } from '../../client/activation/types'; +import { IExtensions, IInstaller } from '../../client/common/types'; +import { JupyterExtensionIntegration } from '../../client/datascience/api/jupyterIntegration'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { IInterpreterSelector } from '../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../client/interpreter/locators/types'; + +export class MockJupyterExtensionIntegration extends JupyterExtensionIntegration { + constructor( + extensions: IExtensions, + interpreterService: IInterpreterService, + interpreterSelector: IInterpreterSelector, + windowsStoreInterpreter: IWindowsStoreInterpreter, + installer: IInstaller, + envActivation: IEnvironmentActivationService, + languageServerCache: ILanguageServerCache + ) { + super( + extensions, + interpreterService, + interpreterSelector, + windowsStoreInterpreter, + installer, + envActivation, + languageServerCache + ); + } +} diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts index 9f7f29086519..1557eba0d0e4 100644 --- a/src/test/datascience/mockLanguageServer.ts +++ b/src/test/datascience/mockLanguageServer.ts @@ -21,6 +21,7 @@ import { TextDocumentContentChangeEvent, WorkspaceEdit } from 'vscode'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; import { ILanguageServer } from '../../client/activation/types'; import { createDeferred, Deferred } from '../../client/common/utils/async'; @@ -45,14 +46,22 @@ export class MockLanguageServer implements ILanguageServer { return this.versionId; } - public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { - this.versionId = document.version; - this.applyChanges(changes); - this.resolveNotificationPromise(); + public get connection() { + // Return an object that looks like a connection + return { + sendNotification: this.sendNotification.bind(this) as any, + sendRequest: noop as any, + sendProgress: noop as any, + onRequest: noop as any, + onNotification: noop as any, + onProgress: noop as any + }; } - public handleOpen(_document: TextDocument) { - noop(); + public get capabilities() { + return { + textDocumentSync: 2 // This is increment value. Means we support changes + } as any; } public provideRenameEdits( @@ -130,15 +139,37 @@ export class MockLanguageServer implements ILanguageServer { noop(); } + private sendNotification(method: any, params: any): void { + if (method === vscodeLanguageClient.DidChangeTextDocumentNotification.type) { + const doc = params.textDocument; + this.versionId = doc.version; + const changes = params.contentChanges; + this.applyChanges(changes); + this.resolveNotificationPromise(); + } + } + private applyChanges(changes: TextDocumentContentChangeEvent[]) { changes.forEach((c) => { - const before = this.contents.substr(0, c.rangeOffset); - const after = this.contents.substr(c.rangeOffset + c.rangeLength); + const offset = this.computeOffset(c); + const before = this.contents.substr(0, offset); + const after = this.contents.substr(offset + c.rangeLength); this.contents = `${before}${c.text}${after}`; }); this.versionId = this.versionId + 1; } + private computeOffset(c: TextDocumentContentChangeEvent): number { + // range offset is no longer available. Have to compute it using the contents + const lines = this.contents.splitLines({ trim: false, removeEmptyEntries: false }); + let offset = 0; + for (let i = 0; i < c.range.start.line; i += 1) { + offset += lines[i].length + 1; // + 1 for the linefeed + } + offset += c.range.start.character; + return offset; + } + private resolveNotificationPromise() { if (this.notificationPromise) { this.notificationPromise.resolve(); diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 9f6fb3c2ab76..c561b9fcb54e 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -13,6 +13,11 @@ import * as vscode from 'vscode'; export * from './extHostedTypes'; export * from './uri'; +const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +export function escapeCodicons(text: string): string { + return text.replace(escapeCodiconsRegex, (match, escaped) => (escaped ? match : `\\${match}`)); +} + export namespace vscMock { export enum ExtensionKind { /** @@ -165,6 +170,78 @@ export namespace vscMock { Outdent = 3 } + export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2 + } + + export class MarkdownString { + public value: string; + public isTrusted?: boolean; + public readonly supportThemeIcons?: boolean; + + constructor(value?: string, supportThemeIcons: boolean = false) { + this.value = value ?? ''; + this.supportThemeIcons = supportThemeIcons; + } + + public static isMarkdownString(thing: any): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; + } + return ( + thing && thing.appendCodeblock && thing.appendMarkdown && thing.appendText && thing.value !== undefined + ); + } + + public appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) + .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + .replace(/\n/, '\n\n'); + + return this; + } + + public appendMarkdown(value: string): MarkdownString { + this.value += value; + + return this; + } + + public appendCodeblock(code: string, language: string = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } + } + + export class Hover { + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + public range: vscode.Range | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: vscode.Range + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); + } + if (Array.isArray(contents)) { + this.contents = contents; + } else if (MarkdownString.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + this.range = range; + } + } + export class CodeActionKind { public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); public static readonly QuickFix: CodeActionKind = new CodeActionKind('quick.fix'); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 2ee192f112e1..f3380316faa8 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -64,6 +64,8 @@ export function initialize() { }; } +mockedVSCode.MarkdownString = vscodeMocks.vscMock.MarkdownString; +mockedVSCode.Hover = vscodeMocks.vscMock.Hover; mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; mockedVSCode.ExtensionKind = vscodeMocks.vscMock.ExtensionKind; mockedVSCode.CodeAction = vscodeMocks.vscMock.CodeAction; @@ -96,6 +98,8 @@ mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditor mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; mockedVSCode.CodeActionKind = vscodeMocks.vscMock.CodeActionKind; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; +mockedVSCode.CompletionTriggerKind = vscodeMocks.vscMock.CompletionTriggerKind; mockedVSCode.DebugAdapterExecutable = vscodeMocks.vscMock.DebugAdapterExecutable; mockedVSCode.DebugAdapterServer = vscodeMocks.vscMock.DebugAdapterServer; mockedVSCode.QuickInputButtons = vscodeMocks.vscMockExtHostedTypes.QuickInputButtons;