@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
164 lines • 7 kB
JavaScript
// 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