UNPKG

@krassowski/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

164 lines 7 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CompleterModel } from '@jupyterlab/completer'; import { StringExt } from '@lumino/algorithm'; function escapeHTML(text) { let node = document.createElement('span'); node.textContent = text; return node.innerHTML; } /** * This will be contributed upstream */ export class GenericCompleterModel extends CompleterModel { constructor(settings = {}) { super(); // TODO: refactor upstream so that it does not block "options"? this.settings = Object.assign(Object.assign({}, GenericCompleterModel.defaultOptions), settings); } completionItems() { let query = this.query; this.query = ''; let unfilteredItems = super.completionItems(); this.query = query; // always want to sort // TODO does this behave strangely with %%<tab> if always sorting? return this._sortAndFilter(query, unfilteredItems); } setCompletionItems(newValue) { super.setCompletionItems(newValue); if (this.settings.preFilterMatches && this.current && this.cursor) { // set initial query to pre-filter items; in future we should use: // https://github.com/jupyterlab/jupyterlab/issues/9763#issuecomment-1001603348 const { start, end } = this.cursor; let query = this.current.text.substring(start, end).trim(); // special case for "Completes Paths In Strings" test case if (query.startsWith('"') || query.startsWith("'")) { query = query.substring(1); } if (query.endsWith('"') || query.endsWith("'")) { query = query.substring(0, -1); } this.query = query; } } _markFragment(value) { return `<mark>${value}</mark>`; } getFilterText(item) { return this.getHighlightableLabelRegion(item); } getHighlightableLabelRegion(item) { // TODO: ideally label and params would be separated so we don't have to do // things like these which are not language-agnostic // (assume that params follow after first opening parenthesis which may not be the case); // the upcoming LSP 3.17 includes CompletionItemLabelDetails // which separates parameters from the label // With ICompletionItems, the label may include parameters, so we exclude them from the matcher. // e.g. Given label `foo(b, a, r)` and query `bar`, // don't count parameters, `b`, `a`, and `r` as matches. const index = item.label.indexOf('('); return index > -1 ? item.label.substring(0, index) : item.label; } _sortAndFilter(query, items) { let results = []; for (let item of items) { // See if label matches query string let matched; let filterText = null; let filterMatch = null; let lowerCaseQuery = query.toLowerCase(); if (query) { filterText = this.getFilterText(item); if (this.settings.caseSensitive) { filterMatch = StringExt.matchSumOfSquares(filterText, query); } else { filterMatch = StringExt.matchSumOfSquares(filterText.toLowerCase(), lowerCaseQuery); } matched = !!filterMatch; if (!this.settings.includePerfectMatches) { matched = matched && filterText != query; } } else { matched = true; } // Filter non-matching items. Filtering may happen on a criterion different than label. if (matched) { // If the matches are substrings of label, highlight them // in this part of the label that can be highlighted (must be a prefix), // which is intended to avoid highlighting matches in function arguments etc. let labelMatch = null; if (query) { let labelPrefix = escapeHTML(this.getHighlightableLabelRegion(item)); if (labelPrefix == filterText) { labelMatch = filterMatch; } else { labelMatch = StringExt.matchSumOfSquares(labelPrefix, query); } } let label; let score; if (labelMatch) { // Highlight label text if there's a match // there won't be a match if filter text includes additional keywords // for easier search that are not a part of the label let marked = StringExt.highlight(escapeHTML(item.label), labelMatch.indices, this._markFragment); label = marked.join(''); score = labelMatch.score; } else { label = escapeHTML(item.label); score = 0; } // preserve getters (allow for lazily retrieved documentation) const itemClone = Object.create(Object.getPrototypeOf(item), Object.getOwnPropertyDescriptors(item)); itemClone.label = label; // If no insertText is present, preserve original label value // by setting it as the insertText. itemClone.insertText = item.insertText ? item.insertText : item.label; results.push({ item: itemClone, score: score }); } } results.sort(this.compareMatches); return results.map(x => x.item); } compareMatches(a, b) { var _a, _b, _c; const delta = a.score - b.score; if (delta !== 0) { return delta; } return (_c = (_a = a.item.insertText) === null || _a === void 0 ? void 0 : _a.localeCompare((_b = b.item.insertText) !== null && _b !== void 0 ? _b : '')) !== null && _c !== void 0 ? _c : 0; } } (function (GenericCompleterModel) { GenericCompleterModel.defaultOptions = { caseSensitive: true, includePerfectMatches: true, preFilterMatches: true }; })(GenericCompleterModel || (GenericCompleterModel = {})); export class LSPCompleterModel extends GenericCompleterModel { getFilterText(item) { if (item.filterText) { return item.filterText; } return super.getFilterText(item); } compareMatches(a, b) { const delta = a.score - b.score; if (delta !== 0) { return delta; } // solve ties using sortText // note: locale compare is case-insensitive return a.item.sortText.localeCompare(b.item.sortText); } } //# sourceMappingURL=model.js.map