atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
199 lines (180 loc) • 7.86 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var _commonsNodeNuclideUri2;
function _commonsNodeNuclideUri() {
return _commonsNodeNuclideUri2 = _interopRequireDefault(require('../../commons-node/nuclideUri'));
}
var NON_UPPERCASE_CHARS_REGEXP = /[^a-z0-9]/g;
/**
* Returns the score of the common subsequence between `needle` and `haystack` or -1 if there is
* no common subsequence.
* A lower number means `needle` is more relevant to `haystack`.
*/
function scoreCommonSubsequence(needle, haystack_) {
var haystack = haystack_;
haystack = haystack.toLowerCase();
haystack = haystack.replace(NON_UPPERCASE_CHARS_REGEXP, '');
if (needle.length === haystack.length) {
return needle === haystack ? 0 : -1;
}
var needleIndex = 0;
var haystackIndex = 0;
var score = 0;
var inGap = false;
while (needleIndex < needle.length && haystackIndex < haystack.length) {
if (needle[needleIndex] === haystack[haystackIndex]) {
needleIndex++;
haystackIndex++;
inGap = false;
} else {
haystackIndex++;
score += inGap ? 2 : 20;
inGap = true;
}
}
if (needleIndex >= needle.length) {
return score + haystack.length + haystackIndex;
}
return -1;
}
var NOT_CAPITAL_LETTERS_REGEXP = /[^A-Z]/g;
/**
* Checks if `needle` matches exactly the first character followed by all uppercase letters in
* `haystack`. E.g. 'fbide' matches 'FaceBookIntegratedDevelopmentEnvironment' and
* 'faceBookIntegratedDevelopmentEnvironment'.
*/
function checkIfMatchesCamelCaseLetters(needle, haystack) {
var uppercase = haystack.substring(0, 1) + haystack.substring(1).replace(NOT_CAPITAL_LETTERS_REGEXP, '');
return needle.toLowerCase() === uppercase.toLowerCase();
}
var CAPITAL_LETTERS_REGEXP = /[A-Z]/;
var IMPORTANT_DELIMITERS_REGEXP = /[_\-.]/;
function isLetterImportant(index, name) {
if (index <= 1) {
return true;
}
if (CAPITAL_LETTERS_REGEXP.test(name[index])) {
return true;
}
var previousCharacter = name[index - 1];
if (IMPORTANT_DELIMITERS_REGEXP.test(previousCharacter)) {
return true;
}
return false;
}
/**
* FBIDE indexes each filepath by important characters it contains.
* This is a temporary workaround that allow calculating important characters on the fly rather
* than relying on the index. Once the index is implemented, consumers of this need to be updated.
*/
// TODO(jxg): replace with "important characters" index.
function importantCharactersForString(str) {
var importantCharacters = new Set();
for (var index = 0; index < str.length; index++) {
var char = str[index];
if (!importantCharacters.has(char) && isLetterImportant(index, str)) {
importantCharacters.add(char);
}
}
return importantCharacters;
}
var __test__ = {
checkIfMatchesCamelCaseLetters: checkIfMatchesCamelCaseLetters,
isLetterImportant: isLetterImportant,
importantCharactersForString: importantCharactersForString,
scoreCommonSubsequence: scoreCommonSubsequence
};
exports.__test__ = __test__;
var QueryItem = (function () {
function QueryItem(filepath) {
_classCallCheck(this, QueryItem);
this._filepath = filepath;
this._filepathLowercase = filepath.toLowerCase();
this._filename = (_commonsNodeNuclideUri2 || _commonsNodeNuclideUri()).default.basename(this._filepathLowercase);
this._importantCharacters = importantCharactersForString(this._filename);
}
/**
* Scores this object's string against the query given.
*
* To search:
* a.) Cut the first letter off the query
* b.) Lookup the list of terms which contain that letter (we indexed it earlier)
* c.) Compare our query against each term in that list
* d.) If our query is a common subsequence of one of the terms, add it to the results list
* e.) While we compare our query, we keep track of a score:
* i.) The more gaps there are between matching characters, the higher the score
* ii.) The more letters which are the incorrect case, the higher the score
* iii.) Direct matches have a score of 0.
* iv.) The later we find out that we've matched, the higher the score
* v.) Longer terms have higher scores
* - The more your query is spreads out across the result,
* the less likely it is what you're looking for.
* - The shorter the result, the closer the length is to what you searched for,
* so it's more likely.
* - The earlier we find the match, the more likely it is to be what you're looking for.
* - The more cases of the characters that match, the more likely it is to be what you want.
* f.) Sort the results by the score
*/
_createClass(QueryItem, [{
key: 'score',
value: function score(query) {
var score = this._getScoreFor(query);
return score == null ? null : { score: score, value: this._filepath, matchIndexes: [] };
}
}, {
key: '_getScoreFor',
value: function _getScoreFor(query) {
// Purely defensive, as query is guaranteed to be non-empty.
if (query.length === 0) {
return null;
}
// Check if this a "possible result".
// TODO consider building a directory-level index from important_character -> QueryItem,
// akin to FBIDE's implementation.
var firstChar = query[0].toLowerCase();
if (!this._importantCharacters.has(firstChar)) {
return null;
}
if (query.length >= 3 && checkIfMatchesCamelCaseLetters(query, this._filename)) {
// If we match the uppercase characters of the filename, we should be ranked the highest
return 0;
} else {
var sub = this._filepathLowercase.indexOf(query.toLowerCase());
if (sub !== -1 && query.length < this._filename.length) {
/**
* We add the length of the term so we can be ranked alongside the
* scores generated by `scoreCommonSubsequence` which also factors in the
* length.
* This way when you search for `EdisonController`,
* EdisonController scores 0
* EdixxsonController scores 40 (from `scoreCommonSubsequence` scoring)
* SomethingBlahBlahEdisonController scores 50 from substring scoring
* WebDecisionController scores 52 (from `scoreCommonSubsequence` scoring)
*/
return sub + this._filename.length;
} else {
// TODO(jxg): Investigate extending scoreCommonSubsequence to consider subsequences
// bidirectionally, or use (some proxy for) edit distance.
var score = scoreCommonSubsequence(query, this._filename);
if (score !== -1) {
return score;
}
}
}
return null;
}
}]);
return QueryItem;
})();
exports.default = QueryItem;