UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

246 lines 10.6 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; } /** * A lot of this was contributed upstream */ export class GenericCompleterModel extends CompleterModel { constructor(settings = {}) { super(); // TODO: refactor upstream so that it does not block "options"? this.settings = { ...GenericCompleterModel.defaultOptions, ...settings }; } completionItems() { let query = this.query; // (setting query is bad because it resets the cache; ideally we would // modify the sorting and filtering algorithm upstream). // TODO processedItemsCache this.query = ''; let unfilteredItems = super.completionItems().map(this.harmoniseItem); this.query = query; // always want to sort // TODO does this behave strangely with %%<tab> if always sorting? return this._sortAndFilter(query, unfilteredItems); } harmoniseItem(item) { return item; } _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; } createPatch(patch) { if (this.subsetMatch) { // Prevent insertion code path when auto-populating subset on tab, to avoid problems with // prefix which is a subset of token incorrectly replacing a string with file system path. // - Q: Which code path is being blocked? // A: The code path (b) discussed in https://github.com/jupyterlab/jupyterlab/issues/15130. // - Q: Why are we short- circuiting here? // A: we want to prevent `onCompletionSelected()` from proceeding with text insertion, // but direct extension of Completer handler is difficult. return undefined; } return super.createPatch(patch); } resolveQuery(userQuery, _item) { return userQuery; } _sortAndFilter(userQuery, items) { let results = []; for (let item of items) { // See if label matches query string let matched; let filterText = null; let filterMatch = null; const query = this.resolveQuery(userQuery, item); 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.bind(this)); 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, kernelCompletionsFirst: false }; })(GenericCompleterModel || (GenericCompleterModel = {})); export class LSPCompleterModel extends GenericCompleterModel { constructor(settings = {}) { super(); this._preFilterQuery = ''; this.settings = { ...LSPCompleterModel.defaultOptions, ...settings }; } getFilterText(item) { if (item.filterText) { return item.filterText; } return super.getFilterText(item); } setCompletionItems(newValue) { super.setCompletionItems(newValue); this._preFilterQuery = ''; if (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 // note: start/end from cursor are not ideal because these get populated from fetch // reply which will vary depending on what providers decide to return; we want the // actual position in token, the same as passed in request to fetch. We can get it // by searching for longest common prefix as seen below (or by counting characters). // Maybe upstream should expose it directly? const { start, end } = this.cursor; const { text, line, column } = this.original; const queryRange = text.substring(start, end).trim(); const linePrefix = text.split('\n')[line].substring(0, column).trim(); let query = ''; for (let i = queryRange.length; i > 0; i--) { if (queryRange.slice(0, i) == linePrefix.slice(-i)) { query = linePrefix.slice(-i); break; } } if (!query) { return; } let trimmedQuotes = false; // special case for "Completes Paths In Strings" test case if (query.startsWith('"') || query.startsWith("'")) { query = query.substring(1); trimmedQuotes = true; } if (query.endsWith('"') || query.endsWith("'")) { query = query.substring(0, -1); trimmedQuotes = true; } if (this.settings.preFilterMatches || trimmedQuotes) { this._preFilterQuery = query; } } } resolveQuery(userQuery, item) { return userQuery ? userQuery : item.source === 'LSP' ? this._preFilterQuery : ''; } harmoniseItem(item) { if (item.self) { const self = item.self; // reflect any changes made on copy self.insertText = item.insertText; return self; } return super.harmoniseItem(item); } compareMatches(a, b) { var _a, _b, _c, _d; // TODO: take source order from provider ranks, upstream this code const sourceOrder = { LSP: 1, kernel: this.settings.kernelCompletionsFirst ? 0 : 2, context: 3 }; const aRank = a.item.source ? (_a = sourceOrder[a.item.source]) !== null && _a !== void 0 ? _a : 4 : 4; const bRank = b.item.source ? (_b = sourceOrder[b.item.source]) !== null && _b !== void 0 ? _b : 4 : 4; return (aRank - bRank || ((_c = a.item.sortText) !== null && _c !== void 0 ? _c : 'z').localeCompare((_d = b.item.sortText) !== null && _d !== void 0 ? _d : 'z') || a.score - b.score); } } (function (LSPCompleterModel) { LSPCompleterModel.defaultOptions = { ...GenericCompleterModel.defaultOptions, preFilterMatches: true }; })(LSPCompleterModel || (LSPCompleterModel = {})); //# sourceMappingURL=model.js.map