Skip to content

Commit 352d9a5

Browse files
authored
Add API to get language server from external extensions (#14021)
* Ideas for lang server API with python extension * Working with new API * Minimize API surface * Fix up tests for intellisense * Fix unit tests * Put back custom editor service * Remove unnecessary changes for service registry * Code review feedback * Move connection type to types file
1 parent 68e127a commit 352d9a5

14 files changed

+432
-101
lines changed

src/client/activation/common/activatorBase.ts

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
SignatureHelpContext,
1919
SymbolInformation,
2020
TextDocument,
21-
TextDocumentContentChangeEvent,
2221
WorkspaceEdit
2322
} from 'vscode';
2423
import * as vscodeLanguageClient from 'vscode-languageclient/node';
@@ -72,34 +71,25 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi
7271
this.manager.disconnect();
7372
}
7473

75-
public handleOpen(document: TextDocument): void {
74+
public get connection() {
7675
const languageClient = this.getLanguageClient();
7776
if (languageClient) {
78-
languageClient.sendNotification(
79-
vscodeLanguageClient.DidOpenTextDocumentNotification.type,
80-
languageClient.code2ProtocolConverter.asOpenTextDocumentParams(document)
81-
);
77+
// Return an object that looks like a connection
78+
return {
79+
sendNotification: languageClient.sendNotification.bind(languageClient),
80+
sendRequest: languageClient.sendRequest.bind(languageClient),
81+
sendProgress: languageClient.sendProgress.bind(languageClient),
82+
onRequest: languageClient.onRequest.bind(languageClient),
83+
onNotification: languageClient.onNotification.bind(languageClient),
84+
onProgress: languageClient.onProgress.bind(languageClient)
85+
};
8286
}
8387
}
8488

85-
public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void {
89+
public get capabilities() {
8690
const languageClient = this.getLanguageClient();
8791
if (languageClient) {
88-
// If the language client doesn't support incremental, just send the whole document
89-
if (this.textDocumentSyncKind === vscodeLanguageClient.TextDocumentSyncKind.Full) {
90-
languageClient.sendNotification(
91-
vscodeLanguageClient.DidChangeTextDocumentNotification.type,
92-
languageClient.code2ProtocolConverter.asChangeTextDocumentParams(document)
93-
);
94-
} else {
95-
languageClient.sendNotification(
96-
vscodeLanguageClient.DidChangeTextDocumentNotification.type,
97-
languageClient.code2ProtocolConverter.asChangeTextDocumentParams({
98-
document,
99-
contentChanges: changes
100-
})
101-
);
102-
}
92+
return languageClient.initializeResult?.capabilities;
10393
}
10494
}
10595

@@ -169,23 +159,6 @@ export abstract class LanguageServerActivatorBase implements ILanguageServerActi
169159
}
170160
}
171161

172-
private get textDocumentSyncKind(): vscodeLanguageClient.TextDocumentSyncKind {
173-
const languageClient = this.getLanguageClient();
174-
if (languageClient?.initializeResult?.capabilities?.textDocumentSync) {
175-
const syncOptions = languageClient.initializeResult.capabilities.textDocumentSync;
176-
const syncKind =
177-
syncOptions !== undefined && syncOptions.hasOwnProperty('change')
178-
? (syncOptions as vscodeLanguageClient.TextDocumentSyncOptions).change
179-
: syncOptions;
180-
if (syncKind !== undefined) {
181-
return syncKind as vscodeLanguageClient.TextDocumentSyncKind;
182-
}
183-
}
184-
185-
// Default is full if not provided
186-
return vscodeLanguageClient.TextDocumentSyncKind.Full;
187-
}
188-
189162
private async handleProvideRenameEdits(
190163
document: TextDocument,
191164
position: Position,

src/client/activation/jedi/multiplexingActivator.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import {
99
Position,
1010
ReferenceContext,
1111
SignatureHelpContext,
12-
TextDocument,
13-
TextDocumentContentChangeEvent
12+
TextDocument
1413
} from 'vscode';
1514
// tslint:disable-next-line: import-name
1615
import { IWorkspaceService } from '../../common/application/types';
@@ -73,15 +72,15 @@ export class MultiplexingJediLanguageServerActivator implements ILanguageServerA
7372
return this.onDidChangeCodeLensesEmitter.event;
7473
}
7574

76-
public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) {
77-
if (this.realLanguageServer && this.realLanguageServer.handleChanges) {
78-
this.realLanguageServer.handleChanges(document, changes);
75+
public get connection() {
76+
if (this.realLanguageServer) {
77+
return this.realLanguageServer.connection;
7978
}
8079
}
8180

82-
public handleOpen(document: TextDocument) {
83-
if (this.realLanguageServer && this.realLanguageServer.handleOpen) {
84-
this.realLanguageServer.handleOpen(document);
81+
public get capabilities() {
82+
if (this.realLanguageServer) {
83+
return this.realLanguageServer.capabilities;
8584
}
8685
}
8786

src/client/activation/refCountedLanguageServer.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
SignatureHelpContext,
1818
SymbolInformation,
1919
TextDocument,
20-
TextDocumentContentChangeEvent,
2120
WorkspaceEdit
2221
} from 'vscode';
2322

@@ -65,12 +64,12 @@ export class RefCountedLanguageServer implements ILanguageServerActivator {
6564
this.impl.clearAnalysisCache ? this.impl.clearAnalysisCache() : noop();
6665
}
6766

68-
public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) {
69-
this.impl.handleChanges ? this.impl.handleChanges(document, changes) : noop();
67+
public get connection() {
68+
return this.impl.connection;
7069
}
7170

72-
public handleOpen(document: TextDocument) {
73-
this.impl.handleOpen ? this.impl.handleOpen(document) : noop();
71+
public get capabilities() {
72+
return this.impl.capabilities;
7473
}
7574

7675
public provideRenameEdits(

src/client/activation/types.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ import {
1313
HoverProvider,
1414
ReferenceProvider,
1515
RenameProvider,
16-
SignatureHelpProvider,
17-
TextDocument,
18-
TextDocumentContentChangeEvent
16+
SignatureHelpProvider
1917
} from 'vscode';
2018
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node';
19+
import * as lsp from 'vscode-languageserver-protocol';
2120
import { NugetPackage } from '../common/nuget/types';
2221
import { IDisposable, IOutputChannel, LanguageServerDownloadChannels, Resource } from '../common/types';
2322
import { PythonEnvironment } from '../pythonEnvironments/info';
@@ -73,17 +72,20 @@ export enum LanguageServerType {
7372
export const DotNetLanguageServerFolder = 'languageServer';
7473
export const NodeLanguageServerFolder = 'nodeLanguageServer';
7574

76-
// tslint:disable-next-line: interface-name
77-
export interface DocumentHandler {
78-
handleOpen(document: TextDocument): void;
79-
handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]): void;
80-
}
81-
8275
// tslint:disable-next-line: interface-name
8376
export interface LanguageServerCommandHandler {
8477
clearAnalysisCache(): void;
8578
}
8679

80+
/**
81+
* This interface is a subset of the vscode-protocol connection interface.
82+
* It's the minimum set of functions needed in order to talk to a language server.
83+
*/
84+
export type ILanguageServerConnection = Pick<
85+
lsp.ProtocolConnection,
86+
'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest'
87+
>;
88+
8789
export interface ILanguageServer
8890
extends RenameProvider,
8991
DefinitionProvider,
@@ -93,9 +95,11 @@ export interface ILanguageServer
9395
CodeLensProvider,
9496
DocumentSymbolProvider,
9597
SignatureHelpProvider,
96-
Partial<DocumentHandler>,
9798
Partial<LanguageServerCommandHandler>,
98-
IDisposable {}
99+
IDisposable {
100+
readonly connection?: ILanguageServerConnection;
101+
readonly capabilities?: lsp.ServerCapabilities;
102+
}
99103

100104
export const ILanguageServerActivator = Symbol('ILanguageServerActivator');
101105
export interface ILanguageServerActivator extends ILanguageServer {

src/client/datascience/api/jupyterIntegration.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88
import { inject, injectable } from 'inversify';
99
import { dirname } from 'path';
10-
import { CancellationToken, Event, Uri } from 'vscode';
10+
import { CancellationToken, Disposable, Event, Uri } from 'vscode';
11+
import * as lsp from 'vscode-languageserver-protocol';
12+
import { ILanguageServerCache, ILanguageServerConnection } from '../../activation/types';
1113
import { InterpreterUri } from '../../common/installer/types';
1214
import { IExtensions, IInstaller, InstallerResponse, Product, Resource } from '../../common/types';
15+
import { isResource } from '../../common/utils/misc';
1316
import { getDebugpyPackagePath } from '../../debugger/extension/adapter/remoteLaunchers';
1417
import { IEnvironmentActivationService } from '../../interpreter/activation/types';
1518
import { IInterpreterQuickPickItem, IInterpreterSelector } from '../../interpreter/configuration/types';
@@ -18,6 +21,11 @@ import { IWindowsStoreInterpreter } from '../../interpreter/locators/types';
1821
import { WindowsStoreInterpreter } from '../../pythonEnvironments/discovery/locators/services/windowsStoreInterpreter';
1922
import { PythonEnvironment } from '../../pythonEnvironments/info';
2023

24+
export interface ILanguageServer extends Disposable {
25+
readonly connection: ILanguageServerConnection;
26+
readonly capabilities: lsp.ServerCapabilities;
27+
}
28+
2129
type PythonApiForJupyterExtension = {
2230
/**
2331
* IInterpreterService
@@ -57,9 +65,14 @@ type PythonApiForJupyterExtension = {
5765
* Returns path to where `debugpy` is. In python extension this is `/pythonFiles/lib/python`.
5866
*/
5967
getDebuggerPath(): Promise<string>;
68+
/**
69+
* Returns a ILanguageServer that can be used for communicating with a language server process.
70+
* @param resource file that determines which connection to return
71+
*/
72+
getLanguageServer(resource?: InterpreterUri): Promise<ILanguageServer | undefined>;
6073
};
6174

62-
type JupyterExtensionApi = {
75+
export type JupyterExtensionApi = {
6376
registerPythonApi(interpreterService: PythonApiForJupyterExtension): void;
6477
};
6578

@@ -71,19 +84,11 @@ export class JupyterExtensionIntegration {
7184
@inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector,
7285
@inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter,
7386
@inject(IInstaller) private readonly installer: IInstaller,
74-
@inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService
87+
@inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService,
88+
@inject(ILanguageServerCache) private readonly languageServerCache: ILanguageServerCache
7589
) {}
7690

77-
public async integrateWithJupyterExtension(): Promise<void> {
78-
const jupyterExtension = this.extensions.getExtension<JupyterExtensionApi>('ms-ai-tools.jupyter');
79-
if (!jupyterExtension) {
80-
return;
81-
}
82-
await jupyterExtension.activate();
83-
if (!jupyterExtension.isActive) {
84-
return;
85-
}
86-
const jupyterExtensionApi = jupyterExtension.exports;
91+
public registerApi(jupyterExtensionApi: JupyterExtensionApi) {
8792
jupyterExtensionApi.registerPythonApi({
8893
onDidChangeInterpreter: this.interpreterService.onDidChangeInterpreter,
8994
getActiveInterpreter: async (resource?: Uri) => this.interpreterService.getActiveInterpreter(resource),
@@ -104,7 +109,34 @@ export class JupyterExtensionIntegration {
104109
resource?: InterpreterUri,
105110
cancel?: CancellationToken
106111
): Promise<InstallerResponse> => this.installer.install(product, resource, cancel),
107-
getDebuggerPath: async () => dirname(getDebugpyPackagePath())
112+
getDebuggerPath: async () => dirname(getDebugpyPackagePath()),
113+
getLanguageServer: async (r) => {
114+
const resource = isResource(r) ? r : undefined;
115+
const interpreter = !isResource(r) ? r : undefined;
116+
const client = await this.languageServerCache.get(resource, interpreter);
117+
118+
// Some langauge servers don't support the connection yet. (like Jedi until we switch to LSP)
119+
if (client && client.connection && client.capabilities) {
120+
return {
121+
connection: client.connection,
122+
capabilities: client.capabilities,
123+
dispose: client.dispose
124+
};
125+
}
126+
return undefined;
127+
}
108128
});
109129
}
130+
131+
public async integrateWithJupyterExtension(): Promise<void> {
132+
const jupyterExtension = this.extensions.getExtension<JupyterExtensionApi>('ms-ai-tools.jupyter');
133+
if (!jupyterExtension) {
134+
return;
135+
}
136+
await jupyterExtension.activate();
137+
if (!jupyterExtension.isActive) {
138+
return;
139+
}
140+
this.registerApi(jupyterExtension.exports);
141+
}
110142
}

src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
import { CancellationToken } from 'vscode-jsonrpc';
2323
import * as vscodeLanguageClient from 'vscode-languageclient/node';
2424
import { concatMultilineString } from '../../../../datascience-ui/common';
25-
import { ILanguageServer, ILanguageServerCache } from '../../../activation/types';
2625
import { IWorkspaceService } from '../../../common/application/types';
2726
import { CancellationError } from '../../../common/cancellation';
2827
import { traceError, traceWarning } from '../../../common/logger';
@@ -34,6 +33,7 @@ import { HiddenFileFormatString } from '../../../constants';
3433
import { IInterpreterService } from '../../../interpreter/contracts';
3534
import { PythonEnvironment } from '../../../pythonEnvironments/info';
3635
import { sendTelemetryWhenDone } from '../../../telemetry';
36+
import { JupyterExtensionIntegration } from '../../api/jupyterIntegration';
3737
import { Identifiers, Settings, Telemetry } from '../../constants';
3838
import {
3939
ICell,
@@ -65,6 +65,7 @@ import {
6565
convertToVSCodeCompletionItem
6666
} from './conversion';
6767
import { IntellisenseDocument } from './intellisenseDocument';
68+
import { NotebookLanguageServer } from './notebookLanguageServer';
6869

6970
// These regexes are used to get the text from jupyter output by recognizing escape charactor \x1b
7071
const DocStringRegex = /\x1b\[1;31mDocstring:\x1b\[0m\s+([\s\S]*?)\r?\n\x1b\[1;31m/;
@@ -101,7 +102,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
101102
private notebookType: 'interactive' | 'native' = 'interactive';
102103
private potentialResource: Uri | undefined;
103104
private sentOpenDocument: boolean = false;
104-
private languageServer: ILanguageServer | undefined;
105+
private languageServer: NotebookLanguageServer | undefined;
105106
private resource: Resource;
106107
private interpreter: PythonEnvironment | undefined;
107108

@@ -110,7 +111,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
110111
@inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem,
111112
@inject(INotebookProvider) private notebookProvider: INotebookProvider,
112113
@inject(IInterpreterService) private interpreterService: IInterpreterService,
113-
@inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache,
114+
@inject(JupyterExtensionIntegration) private jupyterApiProvider: JupyterExtensionIntegration,
114115
@inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableProvider: IJupyterVariables
115116
) {}
116117

@@ -198,7 +199,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
198199
return this.documentPromise.promise;
199200
}
200201

201-
protected async getLanguageServer(token: CancellationToken): Promise<ILanguageServer | undefined> {
202+
protected async getLanguageServer(token: CancellationToken): Promise<NotebookLanguageServer | undefined> {
202203
// Resource should be our potential resource if its set. Otherwise workspace root
203204
const resource =
204205
this.potentialResource ||
@@ -225,22 +226,26 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
225226

226227
// Get an instance of the language server (so we ref count it )
227228
try {
228-
const languageServer = await this.languageServerCache.get(resource, interpreter);
229+
const languageServer = await NotebookLanguageServer.create(
230+
this.jupyterApiProvider,
231+
resource,
232+
interpreter
233+
);
229234

230235
// Dispose of our old language service
231236
this.languageServer?.dispose();
232237

233238
// This new language server does not know about our document, so tell it.
234239
const document = await this.getDocument();
235-
if (document && languageServer.handleOpen && languageServer.handleChanges) {
240+
if (document && languageServer) {
236241
// If we already sent an open document, that means we need to send both the open and
237242
// the new changes
238243
if (this.sentOpenDocument) {
239-
languageServer.handleOpen(document);
240-
languageServer.handleChanges(document, document.getFullContentChanges());
244+
languageServer.sendOpen(document);
245+
languageServer.sendChanges(document, document.getFullContentChanges());
241246
} else {
242247
this.sentOpenDocument = true;
243-
languageServer.handleOpen(document);
248+
languageServer.sendOpen(document);
244249
}
245250
}
246251

@@ -352,7 +357,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
352357
token: CancellationToken
353358
): Promise<monacoEditor.languages.CompletionItem> {
354359
const [languageServer, document] = await Promise.all([this.getLanguageServer(token), this.getDocument()]);
355-
if (languageServer && languageServer.resolveCompletionItem && document) {
360+
if (languageServer && document) {
356361
const vscodeCompItem: CompletionItem = convertToVSCodeCompletionItem(item);
357362

358363
// Needed by Jedi in completionSource.ts to resolve the item
@@ -378,12 +383,12 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
378383
if (document) {
379384
// Broadcast an update to the language server
380385
const languageServer = await this.getLanguageServer(CancellationToken.None);
381-
if (languageServer && languageServer.handleChanges && languageServer.handleOpen) {
386+
if (languageServer) {
382387
if (!this.sentOpenDocument) {
383388
this.sentOpenDocument = true;
384-
return languageServer.handleOpen(document);
389+
return languageServer.sendOpen(document);
385390
} else {
386-
return languageServer.handleChanges(document, changes);
391+
return languageServer.sendChanges(document, changes);
387392
}
388393
}
389394
}

0 commit comments

Comments
 (0)