jupyterlab-emrys
Version:
A computational environment for Jupyter. Powered by Emrys
370 lines (369 loc) • 12.5 kB
JavaScript
// 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 = {}));