UNPKG

@krassowski/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

353 lines 14.3 kB
import { ICodeMirror } from '@jupyterlab/codemirror'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { EditorTooltipManager } from '../components/free_tooltip'; import { CodeMirrorIntegration } from '../editor_integration/codemirror'; import { FeatureSettings } from '../feature'; import { ILSPFeatureManager, PLUGIN_ID } from '../tokens'; import { escapeMarkdown } from '../utils'; const TOOLTIP_ID = 'signature'; const CLASS_NAME = 'lsp-signature-help'; function getMarkdown(item) { if (typeof item === 'string') { return escapeMarkdown(item); } else { if (item.kind === 'markdown') { return item.value; } else { return escapeMarkdown(item.value); } } } export function extractLead(lines, size) { // try to split after paragraph const leadLines = []; let splitOnParagraph = false; for (const line of lines.slice(0, size + 1)) { const isEmpty = line.trim() == ''; if (isEmpty) { splitOnParagraph = true; break; } leadLines.push(line); } // see if we got something which does not include Markdown formatting // (so it won't lead to broken formatting if we split after it); const leadCandidate = leadLines.join('\n'); if (splitOnParagraph && leadCandidate.search(/[\\*#[\]<>_]/g) === -1) { return { lead: leadCandidate, remainder: lines.slice(leadLines.length + 1).join('\n') }; } return null; } /** * Represent signature as a Markdown element. */ export function signatureToMarkdown(item, language = '', codeHighlighter, logger, activeParameterFallback, maxLinesBeforeCollapse = 4) { const activeParameter = typeof item.activeParameter !== 'undefined' ? item.activeParameter : activeParameterFallback; let markdown; let label = item.label; if (item.parameters && activeParameter != null) { if (activeParameter > item.parameters.length) { logger.error('LSP server returned wrong number for activeSignature for: ', item); markdown = '```' + language + '\n' + label + '\n```'; } else { const parameter = item.parameters[activeParameter]; let substring = typeof parameter.label === 'string' ? parameter.label : label.slice(parameter.label[0], parameter.label[1]); markdown = codeHighlighter(label, substring, language); } } else { markdown = '```' + language + '\n' + label + '\n```'; } let details = ''; if (item.documentation) { if (typeof item.documentation === 'string' || item.documentation.kind === 'plaintext') { const plainTextDocumentation = typeof item.documentation === 'string' ? item.documentation : item.documentation.value; // TODO: make use of the MarkupContent object instead for (let line of plainTextDocumentation.split('\n')) { if (line.trim() === item.label.trim()) { continue; } details += getMarkdown(line) + '\n'; } } else { if (item.documentation.kind !== 'markdown') { logger.warn('Unknown MarkupContent kind:', item.documentation.kind); } details += item.documentation.value; } } else if (item.parameters) { details += '\n\n' + item.parameters .filter(parameter => parameter.documentation) .map(parameter => '- ' + getMarkdown(parameter.documentation)) .join('\n'); } if (details) { const lines = details.trim().split('\n'); if (lines.length > maxLinesBeforeCollapse) { const split = extractLead(lines, maxLinesBeforeCollapse); if (split) { details = split.lead + '\n<details>\n' + split.remainder + '\n</details>'; } else { details = '<details>\n' + details + '\n</details>'; } } markdown += '\n\n' + details; } else { markdown += '\n'; } return markdown; } export class SignatureCM extends CodeMirrorIntegration { get settings() { return super.settings; } get _closeCharacters() { if (!this.settings) { return []; } return this.settings.composite.closeCharacters; } register() { this.editor_handlers.set('cursorActivity', this.onCursorActivity.bind(this)); this.editor_handlers.set('blur', this.onBlur.bind(this)); this.editor_handlers.set('focus', this.onCursorActivity.bind(this)); super.register(); } onBlur(virtualEditor, event) { // hide unless the focus moved to the signature itself // (allowing user to select/copy from signature) if (this.isSignatureShown() && event.relatedTarget.closest('.' + CLASS_NAME) === null) { this._hideTooltip(); } } onCursorActivity() { var _a; if (!this.isSignatureShown()) { return; } const newRootPosition = this.virtual_editor.get_cursor_position(); const previousPosition = this.lab_integration.tooltip.position; let newEditorPosition = this.virtual_editor.root_position_to_editor(newRootPosition); // hide tooltip if exceeded position if (newEditorPosition.line === previousPosition.line && newEditorPosition.ch < previousPosition.ch) { this._hideTooltip(); } else { // otherwise, update the signature as the active parameter could have changed, // or the server may want us to close the tooltip (_a = this.requestSignature(newRootPosition, previousPosition)) === null || _a === void 0 ? void 0 : _a.catch(this.console.warn); } } get lab_integration() { return super.lab_integration; } get_markup_for_signature_help(response, language = '') { let signatures = new Array(); if (response.activeSignature != null) { if (response.activeSignature >= response.signatures.length) { this.console.error('LSP server returned wrong number for activeSignature for: ', response); } else { const item = response.signatures[response.activeSignature]; return { kind: 'markdown', value: this.signatureToMarkdown(item, language, response.activeParameter) }; } } response.signatures.forEach(item => { let markdown = this.signatureToMarkdown(item, language); signatures.push(markdown); }); return { kind: 'markdown', value: signatures.join('\n\n') }; } highlightCode(source, variable, language) { const pre = document.createElement('pre'); const code = document.createElement('code'); pre.appendChild(code); code.className = `cm-s-jupyter language-${language}`; this.lab_integration.codeMirror.CodeMirror.runMode(source, language, (token, className) => { let element; if (className) { element = document.createElement('span'); element.classList.add('cm-' + className); element.textContent = token; } else { element = document.createTextNode(token); } if (className === 'variable' && token === variable) { const mark = document.createElement('mark'); mark.appendChild(element); element = mark; } code.appendChild(element); }); return pre.outerHTML; } /** * Represent signature as a Markdown element. */ signatureToMarkdown(item, language, activeParameterFallback) { return signatureToMarkdown(item, language, this.highlightCode.bind(this), this.console, activeParameterFallback, this.settings.composite.maxLines); } _hideTooltip() { this.lab_integration.tooltip.remove(); } handleSignature(response, position_at_request, display_position = null) { this.console.log('Signature received', response); if (response || response === null) { // do not hide on undefined as it simply indicates that no new info is available // (null means close, response means update) this._hideTooltip(); } if (!this.signatureCharacter || !response || !response.signatures.length) { this.console.debug('Ignoring signature response: cursor lost or response empty'); return; } let root_position = this.virtual_editor.get_cursor_position(); // if the cursor advanced in the same line, the previously retrieved signature may still be useful // if the line changed or cursor moved backwards then no reason to keep the suggestions if (position_at_request.line != root_position.line || root_position.ch < position_at_request.ch) { this.console.debug('Ignoring signature response: cursor has receded or changed line'); } let cm_editor = this.get_cm_editor(root_position); if (!cm_editor.hasFocus()) { this.console.debug('Ignoring signature response: the corresponding editor lost focus'); return; } let editor_position = this.virtual_editor.root_position_to_editor(root_position); let language = this.get_language_at(editor_position, cm_editor); let markup = this.get_markup_for_signature_help(response, language); this.console.log('Signature will be shown', language, markup, root_position, response); this.lab_integration.tooltip.create({ markup, position: display_position === null ? editor_position : display_position, id: TOOLTIP_ID, ce_editor: this.virtual_editor.find_ce_editor(cm_editor), adapter: this.adapter, className: CLASS_NAME, tooltip: { privilege: 'forceAbove', alignment: 'start', hideOnKeyPress: false } }); } get signatureCharacters() { var _a; if (!((_a = this._signatureCharacters) === null || _a === void 0 ? void 0 : _a.length)) { this._signatureCharacters = this.connection.getLanguageSignatureCharacters(); } return this._signatureCharacters; } isSignatureShown() { return this.lab_integration.tooltip.isShown(TOOLTIP_ID); } afterChange(change, root_position) { var _a; let last_character = this.extract_last_character(change); const isSignatureShown = this.isSignatureShown(); let previousPosition = null; if (isSignatureShown) { previousPosition = this.lab_integration.tooltip.position; if (this._closeCharacters.includes(last_character)) { // remove just in case but do not short-circuit in case if we need to re-trigger this._hideTooltip(); } } // only proceed if: trigger character was used or the signature is/was visible immediately before if (!(this.signatureCharacters.includes(last_character) || isSignatureShown)) { return; } (_a = this.requestSignature(root_position, previousPosition)) === null || _a === void 0 ? void 0 : _a.catch(this.console.warn); } requestSignature(root_position, previousPosition) { var _a; if (!(this.connection.isReady && ((_a = this.connection.serverCapabilities) === null || _a === void 0 ? void 0 : _a.signatureHelpProvider))) { return; } this.signatureCharacter = root_position; let virtual_position = this.virtual_editor.root_position_to_virtual_position(root_position); return this.connection.clientRequests['textDocument/signatureHelp'] .request({ position: { line: virtual_position.line, character: virtual_position.ch }, textDocument: { uri: this.virtual_document.document_info.uri } }) .then(help => this.handleSignature(help, root_position, previousPosition)); } } class SignatureLabIntegration { constructor(app, settings, renderMimeRegistry, codeMirror) { this.codeMirror = codeMirror; this.tooltip = new EditorTooltipManager(renderMimeRegistry); } } const FEATURE_ID = PLUGIN_ID + ':signature'; export const SIGNATURE_PLUGIN = { id: FEATURE_ID, requires: [ ILSPFeatureManager, ISettingRegistry, IRenderMimeRegistry, ICodeMirror ], autoStart: true, activate: (app, featureManager, settingRegistry, renderMimeRegistry, codeMirror) => { const settings = new FeatureSettings(settingRegistry, FEATURE_ID); const labIntegration = new SignatureLabIntegration(app, settings, renderMimeRegistry, codeMirror); featureManager.register({ feature: { editorIntegrationFactory: new Map([['CodeMirrorEditor', SignatureCM]]), id: FEATURE_ID, name: 'LSP Function signature', labIntegration: labIntegration, settings: settings, capabilities: { textDocument: { signatureHelp: { dynamicRegistration: true, signatureInformation: { documentationFormat: ['markdown', 'plaintext'] } } } } } }); } }; //# sourceMappingURL=signature.js.map