@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
491 lines (445 loc) • 13.7 kB
text/typescript
import { LabShell } from '@jupyterlab/application';
import { ICellModel } from '@jupyterlab/cells';
import { IEditorServices } from '@jupyterlab/codeeditor';
import {
CodeMirrorEditor,
CodeMirrorEditorFactory,
EditorLanguageRegistry,
CodeMirrorMimeTypeService,
EditorExtensionRegistry,
ybinding
} from '@jupyterlab/codemirror';
import {
Context,
IDocumentWidget,
TextModelFactory,
DocumentRegistry
} from '@jupyterlab/docregistry';
import { FileEditor, FileEditorFactory } from '@jupyterlab/fileeditor';
import {
WidgetLSPAdapter,
WidgetLSPAdapterTracker,
LanguageServerManager,
CodeExtractorsManager,
DocumentConnectionManager,
FeatureManager,
ISocketConnectionOptions,
ILSPOptions,
ILSPFeatureManager
} from '@jupyterlab/lsp';
import { LSPConnection } from '@jupyterlab/lsp/lib/connection';
import * as nbformat from '@jupyterlab/nbformat';
import {
Notebook,
NotebookModel,
NotebookModelFactory,
NotebookPanel,
StaticNotebook
} from '@jupyterlab/notebook';
import { defaultRenderMime } from '@jupyterlab/rendermime/lib/testutils';
import { ServiceManagerMock } from '@jupyterlab/services/lib/testutils';
import { nullTranslator } from '@jupyterlab/translation';
import { PromiseDelegate } from '@lumino/coreutils';
import { Signal } from '@lumino/signaling';
import type * as lsProtocol from 'vscode-languageserver-protocol';
import type { MessageConnection } from 'vscode-ws-jsonrpc';
import { FileEditorAdapter } from './adapters/fileeditor';
import { NotebookAdapter } from './adapters/notebook';
import { IFeatureSettings } from './feature';
import { CodeOverridesManager } from './overrides';
import { VirtualDocument } from './virtual/document';
const DEFAULT_SERVER_ID = 'pylsp';
export interface ITestEnvironment {
documentOptions: VirtualDocument.IOptions;
adapter: WidgetLSPAdapter<any>;
init(): void;
dispose(): void;
}
export class MockLanguageServerManager extends LanguageServerManager {
async fetchSessions() {
const spec = {
languages: ['python']
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._sessions = new Map();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._sessions.set(DEFAULT_SERVER_ID, {
spec
} as any);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._sessionsChanged.emit(void 0);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._specs = new Map(Object.entries({ [DEFAULT_SERVER_ID]: spec }));
}
}
export class MockSettings<T> implements IFeatureSettings<T> {
changed: Signal<IFeatureSettings<T>, void>;
constructor(private settings: T) {
this.changed = new Signal(this);
}
get composite(): Required<T> {
return this.settings as Required<T>;
}
set(setting: keyof T, value: any): void {
this.settings[setting] = value;
}
}
namespace MockConnection {
export interface IOptions extends ILSPOptions {
serverCapabilities: lsProtocol.ServerCapabilities;
}
}
class MockConnection extends LSPConnection {
constructor(protected options: MockConnection.IOptions) {
super(options);
}
connect(ws: any): void {
this.connection = new MockMessageConnection() as MessageConnection;
this.onServerInitialized({
capabilities: this.options.serverCapabilities
});
this._isConnected = true;
}
}
namespace MockDocumentConnectionManager {
export interface IOptions extends DocumentConnectionManager.IOptions {
connection?: Partial<MockConnection.IOptions>;
}
}
class MockMessageConnection implements Partial<MessageConnection> {
onError(handler: any): any {
// no-op
}
onNotification(handler: any): any {
// no-op
}
onRequest(hander: any): any {
// no-op
}
sendNotification(handler: any): Promise<void> {
return Promise.resolve();
}
}
class MockDocumentConnectionManager extends DocumentConnectionManager {
constructor(protected options: MockDocumentConnectionManager.IOptions) {
super(options);
}
get ready() {
return Promise.resolve();
}
async connect(
options: ISocketConnectionOptions,
firstTimeoutSeconds?: number,
secondTimeoutMinutes?: number
) {
let { language, capabilities, virtualDocument } = options;
this.connectDocumentSignals(virtualDocument);
const uris = {
server: '',
base: ''
};
const matchingServers = this.languageServerManager.getMatchingServers({
language
});
const languageServerId =
matchingServers.length === 0 ? null : matchingServers[0];
if (!uris) {
return;
}
const connection = new MockConnection({
languageId: language,
serverUri: uris.server,
rootUri: uris.base,
serverIdentifier: languageServerId!,
capabilities: capabilities,
serverCapabilities: {},
...this.options.connection
});
connection.connect(null);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this._connected.emit({ connection, virtualDocument });
return connection;
}
}
export namespace TestEnvironment {
export interface IOptions {
document?: Partial<VirtualDocument.IOptions>;
connection?: Partial<MockConnection.IOptions>;
}
}
export abstract class TestEnvironment implements ITestEnvironment {
adapter: WidgetLSPAdapter<any>;
abstract widget: IDocumentWidget;
protected abstract getDefaults(): VirtualDocument.IOptions;
documentOptions: VirtualDocument.IOptions;
editorExtensionRegistry: EditorExtensionRegistry;
editorServices: IEditorServices;
featureManager: ILSPFeatureManager;
constructor(protected options?: TestEnvironment.IOptions) {
this.editorExtensionRegistry = new EditorExtensionRegistry();
this.editorExtensionRegistry.addExtension({
name: 'binding',
factory: ({ model }) =>
EditorExtensionRegistry.createImmutableExtension(
ybinding({ ytext: (model.sharedModel as any).ysource })
)
});
const shell = new LabShell({ waitForRestore: false });
const adapterTracker = new WidgetLSPAdapterTracker({ shell });
const languages = new EditorLanguageRegistry();
this.editorServices = {
factoryService: new CodeMirrorEditorFactory({
languages,
extensions: this.editorExtensionRegistry
}),
mimeTypeService: new CodeMirrorMimeTypeService(languages)
};
this._languageServerManager = new MockLanguageServerManager({});
this.connectionManager = new MockDocumentConnectionManager({
languageServerManager: this._languageServerManager,
connection: this.options?.connection,
adapterTracker
});
this.documentOptions = {
...this.getDefaults(),
...(options?.document || {})
};
this.featureManager = new FeatureManager();
}
protected abstract createWidget(): IDocumentWidget;
protected abstract getAdapterType():
| typeof FileEditorAdapter
| typeof NotebookAdapter;
get activeEditor(): CodeMirrorEditor {
return this.adapter.activeEditor!.getEditor()! as CodeMirrorEditor;
}
connectionManager: MockDocumentConnectionManager;
async init() {
this.widget = this.createWidget();
let adapterType = this.getAdapterType();
const docRegistry = new DocumentRegistry();
const { foreignCodeExtractors, overridesRegistry } = this.documentOptions;
const overridesManager = new CodeOverridesManager();
for (let language of Object.keys(overridesRegistry)) {
const cellOverrides = overridesRegistry[language].cell;
for (const cell of cellOverrides) {
overridesManager.register(
{
scope: 'cell',
pattern: cell.pattern,
replacement: cell.replacement,
reverse: cell.reverse as any
},
language
);
}
const lineOverrides = overridesRegistry[language].line;
for (const line of lineOverrides) {
overridesManager.register(
{
scope: 'line',
pattern: line.pattern,
replacement: line.replacement,
reverse: line.reverse as any
},
language
);
}
}
this.adapter = new adapterType(this.widget as any, {
docRegistry,
connectionManager: this.connectionManager,
codeOverridesManager: overridesManager,
featureManager: this.featureManager,
foreignCodeExtractorsManager: foreignCodeExtractors,
translator: nullTranslator
});
await this.widget.context.sessionContext.ready;
await this.adapter.ready;
this.connectionManager.adapters.set(
this.adapter.virtualDocument!.path,
this.adapter
);
await this.connectionManager.ready;
}
private _languageServerManager: LanguageServerManager;
dispose(): void {
this.adapter.dispose();
this._languageServerManager.dispose();
}
}
export class MockNotebookAdapter extends NotebookAdapter {
get language() {
return 'python';
}
isReady(): boolean {
return true;
}
foreingDocumentOpened = new PromiseDelegate();
async onForeignDocumentOpened(
_: VirtualDocument,
context: any
): Promise<void> {
try {
const result = await super.onForeignDocumentOpened(_, context);
this.foreingDocumentOpened.resolve(undefined);
this.foreingDocumentOpened = new PromiseDelegate();
return result;
} catch (e) {
console.warn(`onForeignDocumentOpened failed: ${e}`);
}
}
}
export class FileEditorTestEnvironment extends TestEnvironment {
protected getAdapterType() {
return FileEditorAdapter;
}
widget: IDocumentWidget<FileEditor>;
protected getDefaults(): VirtualDocument.IOptions {
return {
language: 'python',
path: 'dummy.py',
fileExtension: 'py',
hasLspSupportedFile: true,
standalone: true,
overridesRegistry: {},
foreignCodeExtractors: new CodeExtractorsManager()
};
}
createWidget(): IDocumentWidget {
let factory = new FileEditorFactory({
editorServices: this.editorServices,
factoryOptions: {
name: 'Editor',
fileTypes: ['*']
}
});
const context = new Context({
manager: new ServiceManagerMock(),
factory: new TextModelFactory(),
path: this.documentOptions.path
});
void context.initialize(true);
void context.sessionContext.initialize();
return factory.createNew(context);
}
dispose(): void {
super.dispose();
}
}
export class NotebookTestEnvironment extends TestEnvironment {
public widget: NotebookPanel;
protected getAdapterType() {
return MockNotebookAdapter;
}
get notebook(): Notebook {
return this.widget.content;
}
protected getDefaults(): VirtualDocument.IOptions {
return {
language: 'python',
path: 'notebook.ipynb',
fileExtension: 'py',
overridesRegistry: {},
foreignCodeExtractors: new CodeExtractorsManager(),
hasLspSupportedFile: false,
standalone: true
};
}
createWidget(): IDocumentWidget {
const startKernel = true;
let context = new Context({
manager: new ServiceManagerMock(),
factory: new NotebookModelFactory({}),
path: this.documentOptions.path,
kernelPreference: {
shouldStart: startKernel,
canStart: startKernel,
autoStartDefault: startKernel
}
});
void context.initialize(true);
void context.sessionContext.initialize();
const editorFactory =
this.editorServices.factoryService.newInlineEditor.bind(
this.editorServices.factoryService
);
return new NotebookPanel({
content: new Notebook({
rendermime: defaultRenderMime(),
contentFactory: new Notebook.ContentFactory({ editorFactory }),
mimeTypeService: this.editorServices.mimeTypeService,
notebookConfig: {
...StaticNotebook.defaultNotebookConfig,
windowingMode: 'none'
}
}),
context
});
}
}
export function codeCell(
source: string[] | string,
metadata: Partial<nbformat.ICodeCellMetadata> = { trusted: false }
) {
return {
cell_type: 'code',
source: source,
metadata: metadata,
execution_count: null,
outputs: []
} as nbformat.ICodeCell;
}
export function setNotebookContent(
notebook: Notebook,
cells: nbformat.ICodeCell[],
metadata = pythonNotebookMetadata
) {
let testNotebook = {
cells: cells,
metadata: metadata
} as nbformat.INotebookContent;
const model = new NotebookModel();
model.fromJSON(testNotebook);
notebook.model = model;
}
export const pythonNotebookMetadata = {
kernelspec: {
display_name: 'Python [default]',
language: 'python',
name: 'python3'
},
language_info: {
codemirror_mode: {
name: 'ipython',
version: 3
},
fileExtension: '.py',
mimetype: 'text/x-python',
name: 'python',
nbconvert_exporter: 'python',
pygments_lexer: 'ipython3',
version: '3.5.2'
},
orig_nbformat: 4.1
} as nbformat.INotebookMetadata;
export function showAllCells(notebook: Notebook) {
notebook.show();
// iterate over every cell to activate the editors
for (let i = 0; i < notebook.model!.cells.length; i++) {
notebook.activeCellIndex = i;
notebook.activeCell!.show();
}
}
export function getCellsJSON(notebook: Notebook): Array<nbformat.ICell> {
let cells: Array<ICellModel> = [];
for (let i = 0; i < notebook.model!.cells.length; i++) {
cells.push(notebook.model!.cells.get(i));
}
return cells.map(cell => cell.toJSON());
}