UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

418 lines (381 loc) 11.3 kB
import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IEditorExtensionRegistry } from '@jupyterlab/codemirror'; import { DocumentRegistry, IDocumentWidget } from '@jupyterlab/docregistry'; import { IEditorTracker, FileEditor } from '@jupyterlab/fileeditor'; import { ILSPFeatureManager, ILSPDocumentConnectionManager, WidgetLSPAdapter, VirtualDocument } from '@jupyterlab/lsp'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITableOfContentsRegistry, TableOfContents, TableOfContentsModel, TableOfContentsFactory } from '@jupyterlab/toc'; import { Throttler } from '@lumino/polling'; import { Widget } from '@lumino/widgets'; import type * as lsProtocol from 'vscode-languageserver-protocol'; import { CodeSymbols as LSPSymbolSettings } from '../_symbol'; import { ContextAssembler } from '../context'; import { FeatureSettings, Feature } from '../feature'; import { SymbolTag } from '../lsp'; import { PLUGIN_ID } from '../tokens'; import { BrowserConsole } from '../virtual/console'; /** * Interface describing a LSP heading. */ interface IEditorHeading extends TableOfContents.IHeading { /** * LSP symbol data. */ symbol: lsProtocol.DocumentSymbol; } /** * Table of content model using LSP. */ class LSPTableOfContentsModel extends TableOfContentsModel< IEditorHeading, IDocumentWidget<FileEditor, DocumentRegistry.IModel> > { constructor( protected widget: IDocumentWidget<FileEditor, DocumentRegistry.IModel>, protected symbolFeature: SymbolFeature, protected connectionManager: ILSPDocumentConnectionManager, configuration?: TableOfContents.IConfig ) { super(widget, configuration); } /** * Type of document supported by the model. * * #### Notes * A `data-document-type` attribute with this value will be set * on the tree view `.jp-TableOfContents-content[data-document-type="..."]` */ get documentType(): string { return 'lsp'; } /** * List of configuration options supported by the model. */ get supportedOptions(): (keyof TableOfContents.IConfig)[] { return ['maximalDepth', 'numberHeaders']; } /** * Produce the headings for a document. * * @returns The list of new headings or `null` if nothing needs to be updated. */ protected async getHeadings(): Promise<IEditorHeading[] | null> { if (!this.isActive) { return null; } const adapter = [...this.connectionManager.adapters.values()].find( adapter => adapter.widget.node.contains(this.widget.node) ); if (!adapter?.virtualDocument) { return null; } const headings = new Array<IEditorHeading>(); const symbols = await this.symbolFeature.getSymbols.invoke( adapter, adapter.virtualDocument ); if (!symbols) { return null; } const processBreadthFirst = ( elements: lsProtocol.DocumentSymbol[], level = 1 ) => { for (const symbol of elements) { headings.push({ text: symbol.name, level, symbol }); if (symbol.children) { processBreadthFirst(symbol.children, level + 1); } } }; processBreadthFirst(symbols); return headings; } } class LSPEditorTableOfContentsFactory extends TableOfContentsFactory< IDocumentWidget<FileEditor>, IEditorHeading > { constructor( tracker: IEditorTracker, protected symbolFeature: SymbolFeature, protected connectionManager: ILSPDocumentConnectionManager ) { super(tracker); } /** * Whether the factory can handle the widget or not. * * @param widget - widget * @returns boolean indicating a ToC can be generated */ isApplicable(widget: Widget): boolean { const isApplicable = super.isApplicable(widget); if (isApplicable) { return this.symbolFeature.isApplicable(widget); } return false; } /** * Create a new table of contents model for the widget * * @param widget - widget * @param configuration - Table of contents configuration * @returns The table of contents model */ createNew( widget: IDocumentWidget<FileEditor, DocumentRegistry.IModel>, configuration?: TableOfContents.IConfig ): TableOfContentsModel< IEditorHeading, IDocumentWidget<FileEditor, DocumentRegistry.IModel> > { const model = super.createNew(widget, configuration); const onActiveHeadingChanged = ( model: TableOfContentsModel< IEditorHeading, IDocumentWidget<FileEditor, DocumentRegistry.IModel> >, heading: IEditorHeading | null ) => { if (heading) { widget.content.editor.setCursorPosition({ line: heading.symbol.selectionRange.start.line, column: heading.symbol.selectionRange.start.character }); } }; model.activeHeadingChanged.connect(onActiveHeadingChanged); widget.disposed.connect(() => { model.activeHeadingChanged.disconnect(onActiveHeadingChanged); }); return model; } /** * Create a new table of contents model for the widget * * @param widget - widget * @param configuration - Table of contents configuration * @returns The table of contents model */ protected _createNew( widget: IDocumentWidget<FileEditor, DocumentRegistry.IModel>, configuration?: TableOfContents.IConfig ): LSPTableOfContentsModel { return new LSPTableOfContentsModel( widget, this.symbolFeature, this.connectionManager, configuration ); } } function isSymbolInformationArray( response: lsProtocol.DocumentSymbol[] | lsProtocol.SymbolInformation[] ): response is lsProtocol.SymbolInformation[] { return ( response.length > 0 && !!(response[0] as lsProtocol.SymbolInformation).location ); } export class SymbolFeature extends Feature { readonly capabilities: lsProtocol.ClientCapabilities = { textDocument: { documentSymbol: { dynamicRegistration: true, tagSupport: { valueSet: [SymbolTag.Deprecated] }, hierarchicalDocumentSymbolSupport: true } } }; readonly id = SymbolFeature.id; protected console = new BrowserConsole().scope('Symbol'); protected settings: FeatureSettings<LSPSymbolSettings>; protected cache: WeakMap<WidgetLSPAdapter<any>, lsProtocol.DocumentSymbol>; protected contextAssembler: ContextAssembler; public getSymbols: Throttler< lsProtocol.DocumentSymbol[] | null, void, [WidgetLSPAdapter<any>, VirtualDocument] >; constructor(options: SymbolFeature.IOptions) { super(options); this.settings = options.settings; this.contextAssembler = options.contextAssembler; const { tocRegistry, editorTracker } = options; this.connectionManager = options.connectionManager; if (tocRegistry && editorTracker) { tocRegistry.add( new LSPEditorTableOfContentsFactory( editorTracker, this, this.connectionManager ) ); } this.cache = new WeakMap(); this.getSymbols = this.createThrottler(); this.settings.changed.connect(() => { this.getSymbols = this.createThrottler(); }); } isApplicable(widget: Widget): boolean { const adapter = [...this.connectionManager.adapters.values()].find( adapter => adapter.widget.node.contains(widget.node) ); if (!adapter?.virtualDocument) { return false; } const connection = this.connectionManager.connections.get( adapter.virtualDocument.uri ); if ( !( connection && connection.isReady && connection.serverCapabilities.documentSymbolProvider ) ) { return false; } return true; } protected createThrottler() { return new Throttler< lsProtocol.DocumentSymbol[] | null, void, [WidgetLSPAdapter<any>, VirtualDocument] >(this._getSymbols.bind(this), { limit: this.settings.composite.throttlerDelay || 0, edge: 'trailing' }); } private async _getSymbols( adapter: WidgetLSPAdapter<any>, virtualDocument: VirtualDocument ): Promise<lsProtocol.DocumentSymbol[] | null> { // TODO: return from cache await adapter.ready; const connection = this.connectionManager.connections.get( virtualDocument.uri )!; if ( !( connection.isReady && connection.serverCapabilities.documentSymbolProvider ) ) { return null; } // for popup use // 'workspace/symbol' const response = await connection.clientRequests[ 'textDocument/documentSymbol' ].request({ textDocument: { uri: virtualDocument.documentInfo.uri } }); // TODO: for some reason after reloading JupyterLab server errors out // unless a file in project gets opened. This may indicate that open // notifications are not sent out properly. return this._handleSymbols(response); } private _handleSymbols( response: | lsProtocol.DocumentSymbol[] | lsProtocol.SymbolInformation[] | null ): lsProtocol.DocumentSymbol[] | null { if (!response) { return null; } if (isSymbolInformationArray(response)) { return response.map(s => { return { name: s.name, kind: s.kind, tags: s.tags, deprecated: s.deprecated, range: s.location.range, selectionRange: s.location.range }; }); } return response; } } export namespace SymbolFeature { export interface IOptions extends Feature.IOptions { settings: FeatureSettings<LSPSymbolSettings>; renderMimeRegistry: IRenderMimeRegistry; editorExtensionRegistry: IEditorExtensionRegistry; contextAssembler: ContextAssembler; tocRegistry: ITableOfContentsRegistry | null; editorTracker: IEditorTracker | null; } export const id = PLUGIN_ID + ':symbol'; } export const SYMBOL_PLUGIN: JupyterFrontEndPlugin<void> = { id: SymbolFeature.id, requires: [ ILSPFeatureManager, ISettingRegistry, IRenderMimeRegistry, IEditorExtensionRegistry, ILSPDocumentConnectionManager ], optional: [ITableOfContentsRegistry, IEditorTracker], autoStart: true, activate: async ( app: JupyterFrontEnd, featureManager: ILSPFeatureManager, settingRegistry: ISettingRegistry, renderMimeRegistry: IRenderMimeRegistry, editorExtensionRegistry: IEditorExtensionRegistry, connectionManager: ILSPDocumentConnectionManager, tocRegistry: ITableOfContentsRegistry | null, editorTracker: IEditorTracker | null ) => { const contextAssembler = new ContextAssembler({ app, connectionManager }); const settings = new FeatureSettings<LSPSymbolSettings>( settingRegistry, PLUGIN_ID + ':hover' // SymbolFeature.id ); await settings.ready; if (settings.composite.disable) { return; } const feature = new SymbolFeature({ settings, renderMimeRegistry, editorExtensionRegistry, connectionManager, contextAssembler, tocRegistry, editorTracker }); featureManager.register(feature); } };