UNPKG

jupyterlab-emrys

Version:

A computational environment for Jupyter. Powered by Emrys

370 lines (369 loc) 12.5 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. "use strict"; var phosphor_signaling_1 = require('phosphor-signaling'); var json_1 = require('../common/json'); /** * An implementation of a completion model. */ var CompletionModel = (function () { function CompletionModel() { this._isDisposed = false; this._options = null; this._original = null; this._current = null; this._query = ''; this._cursor = null; } Object.defineProperty(CompletionModel.prototype, "stateChanged", { /** * A signal emitted when state of the completion menu changes. */ get: function () { return Private.stateChangedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "items", { /** * The list of visible items in the completion menu. * * #### Notes * This is a read-only property. */ get: function () { return this._filter(); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "options", { /** * The unfiltered list of all available options in a completion menu. */ get: function () { return this._options; }, set: function (newValue) { if (json_1.deepEqual(newValue, this._options)) { return; } if (newValue && newValue.length) { this._options = []; (_a = this._options).push.apply(_a, newValue); } else { this._options = null; } this.stateChanged.emit(void 0); var _a; }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "original", { /** * The original completion request details. */ get: function () { return this._original; }, set: function (newValue) { if (json_1.deepEqual(newValue, this._original)) { return; } this._reset(); this._original = newValue; this.stateChanged.emit(void 0); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "current", { /** * The current text change details. */ get: function () { return this._current; }, set: function (newValue) { if (json_1.deepEqual(newValue, this._current)) { return; } // Original request must always be set before a text change. If it isn't // the model fails silently. if (!this.original) { return; } // Cursor must always be set before a text change. This happens // automatically in the completion handler, but since `current` is a public // attribute, this defensive check is necessary. if (!this._cursor) { return; } this._current = newValue; if (!this.current) { this.stateChanged.emit(void 0); return; } var original = this._original; var current = this._current; var originalLine = original.currentValue.split('\n')[original.line]; var currentLine = current.newValue.split('\n')[current.line]; // If the text change means that the original start point has been preceded, // then the completion is no longer valid and should be reset. if (currentLine.length < originalLine.length) { this.reset(); return; } else { var _a = this._cursor, start = _a.start, end = _a.end; // Clip the front of the current line. var query = currentLine.substring(start); // Clip the back of the current line. var ending = originalLine.substring(end); query = query.substring(0, query.lastIndexOf(ending)); this._query = query; } this.stateChanged.emit(void 0); }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "cursor", { /** * The cursor details that the API has used to return matching options. */ get: function () { return this._cursor; }, set: function (newValue) { // Original request must always be set before a cursor change. If it isn't // the model fails silently. if (!this.original) { return; } this._cursor = newValue; }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "query", { /** * The query against which items are filtered. */ get: function () { return this._query; }, set: function (newValue) { this._query = newValue; }, enumerable: true, configurable: true }); Object.defineProperty(CompletionModel.prototype, "isDisposed", { /** * Get whether the model is disposed. */ get: function () { return this._isDisposed; }, enumerable: true, configurable: true }); /** * Dispose of the resources held by the model. */ CompletionModel.prototype.dispose = function () { // Do nothing if already disposed. if (this.isDisposed) { return; } this._isDisposed = true; phosphor_signaling_1.clearSignalData(this); this._reset(); }; /** * Handle a text change. */ CompletionModel.prototype.handleTextChange = function (change) { var line = change.newValue.split('\n')[change.line]; // If last character entered is not whitespace, update completion. if (line[change.ch - 1] && line[change.ch - 1].match(/\S/)) { // If there is currently a completion if (this.original) { this.current = change; } } else { // If final character is whitespace, reset completion. this.reset(); } }; /** * Create a resolved patch between the original state and a patch string. * * @param patch - The patch string to apply to the original value. * * @returns A patched text change or null if original value did not exist. */ CompletionModel.prototype.createPatch = function (patch) { var original = this._original; var cursor = this._cursor; if (!original || !cursor) { return null; } var start = cursor.start, end = cursor.end; var value = original.currentValue; var prefix = value.substring(0, start); var suffix = value.substring(end); var text = prefix + patch + suffix; var position = (prefix + patch).length; return { position: position, text: text }; }; /** * Reset the state of the model and emit a state change signal. */ CompletionModel.prototype.reset = function () { this._reset(); this.stateChanged.emit(void 0); }; /** * Apply the query to the complete options list to return the matching subset. */ CompletionModel.prototype._filter = function () { var options = this._options || []; var query = this._query; if (!query) { return options.map(function (option) { return ({ raw: option, text: option }); }); } var results = []; for (var _i = 0, options_1 = options; _i < options_1.length; _i++) { var option = options_1[_i]; var match = StringSearch.sumOfSquares(option, query); if (match) { results.push({ raw: option, score: match.score, text: StringSearch.highlight(option, match.indices) }); } } return results.sort(StringSearch.scoreCmp) .map(function (result) { return ({ text: result.text, raw: result.raw }); }); }; /** * Reset the state of the model. */ CompletionModel.prototype._reset = function () { this._current = null; this._original = null; this._options = null; this._cursor = null; this._query = ''; }; return CompletionModel; }()); exports.CompletionModel = CompletionModel; /** * A namespace for completion model private data. */ var Private; (function (Private) { /** * A signal emitted when state of the completion menu changes. */ Private.stateChangedSignal = new phosphor_signaling_1.Signal(); })(Private || (Private = {})); /** * A namespace which holds string searching functionality. * * #### Notes * All of this functionality comes from phosphor-core and can be removed from * this file once the phosphor mono-repo is deployed *except* `scoreCmp`. */ var StringSearch; (function (StringSearch) { /** * Compute the sum-of-squares match for the given search text. * * @param sourceText - The text which should be searched. * * @param queryText - The query text to locate in the source text. * * @returns The match result object, or `null` if there is no match. * * #### Complexity * Linear on `sourceText`. * * #### Notes * This scoring algorithm uses a sum-of-squares approach to determine * the score. In order for there to be a match, all of the characters * in `queryText` **must** appear in `sourceText` in order. The index * of each matching character is squared and added to the score. This * means that early and consecutive character matches are preferred. * * The character match is performed with strict equality. It is case * sensitive and does not ignore whitespace. If those behaviors are * required, the text should be transformed before scoring. */ function sumOfSquares(sourceText, queryText) { var score = 0; var indices = new Array(queryText.length); for (var i = 0, j = 0, n = queryText.length; i < n; ++i, ++j) { j = sourceText.indexOf(queryText[i], j); if (j === -1) { return null; } indices[i] = j; score += j * j; } return { score: score, indices: indices }; } StringSearch.sumOfSquares = sumOfSquares; /** * A sort comparison function for item match scores. * * #### Notes * This orders the items first based on score (lower is better), then * by locale order of the item text. */ function scoreCmp(a, b) { var delta = a.score - b.score; if (delta !== 0) { return delta; } return a.raw.localeCompare(b.raw); } StringSearch.scoreCmp = scoreCmp; /** * Highlight the matched characters of a source string. * * @param source - The text which should be highlighted. * * @param indices - The indices of the matched characters. They must * appear in increasing order and must be in bounds of the source. * * @returns A string with interpolated `<mark>` tags. */ function highlight(sourceText, indices) { var k = 0; var last = 0; var result = ''; var n = indices.length; while (k < n) { var i = indices[k]; var j = indices[k]; while (++k < n && indices[k] === j + 1) { j++; } var head = sourceText.slice(last, i); var chunk = sourceText.slice(i, j + 1); result += head + "<mark>" + chunk + "</mark>"; last = j + 1; } return result + sourceText.slice(last); } StringSearch.highlight = highlight; })(StringSearch || (StringSearch = {}));