2018-07-13 18:39:55 +02:00
|
|
|
/*
|
|
|
|
* Copyright (c) Codiad & Kent Safranski (codiad.com), distributed
|
|
|
|
* as-is and without warranty under the MIT License. See
|
|
|
|
* [root]/license.txt for more. This information must remain intact.
|
|
|
|
*/
|
|
|
|
|
|
|
|
(function (global, $) {
|
|
|
|
|
|
|
|
var EventEmitter = ace.require('ace/lib/event_emitter').EventEmitter;
|
|
|
|
var Range = ace.require('ace/range').Range;
|
|
|
|
|
|
|
|
|
|
|
|
var codiad = global.codiad;
|
|
|
|
|
|
|
|
$(function () {
|
|
|
|
codiad.autocomplete.init();
|
|
|
|
});
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
|
|
// Autocomplete Component for Codiad
|
|
|
|
// ---------------------------------
|
|
|
|
// Show a popup with word completion suggestions.
|
|
|
|
//
|
|
|
|
//////////////////////////////////////////////////////////////////
|
2018-07-26 21:39:40 +02:00
|
|
|
|
2018-07-13 18:39:55 +02:00
|
|
|
codiad.autocomplete = {
|
|
|
|
|
|
|
|
wordRegex: /[^a-zA-Z_0-9\$]+/,
|
|
|
|
|
|
|
|
isVisible: false,
|
|
|
|
|
|
|
|
standardGoLineDownExec: null,
|
|
|
|
|
|
|
|
standardGoLineUpExec: null,
|
|
|
|
|
|
|
|
_suggestionCache: null,
|
|
|
|
|
|
|
|
standardGoToRightExec: null,
|
|
|
|
|
|
|
|
standardGoToLeftExec: null,
|
|
|
|
|
|
|
|
standardIndentExec: null,
|
|
|
|
|
|
|
|
init: function () {
|
|
|
|
var _this = this;
|
|
|
|
|
|
|
|
this.$onDocumentChange = this.onDocumentChange.bind(this);
|
|
|
|
this.$selectNextSuggestion = this.selectNextSuggestion.bind(this);
|
|
|
|
this.$selectPreviousSuggestion = this.selectPreviousSuggestion.bind(this);
|
|
|
|
this.$complete = this.complete.bind(this);
|
|
|
|
this.$hide = this.hide.bind(this);
|
|
|
|
|
|
|
|
/* Catch click on suggestion */
|
|
|
|
$('#autocomplete li').live('click', function () {
|
|
|
|
$('#autocomplete li.active-suggestion').removeClass('active-suggestion');
|
|
|
|
$(this).addClass('active-suggestion');
|
|
|
|
_this.complete();
|
|
|
|
});
|
|
|
|
|
|
|
|
/* In debug mode, run some tests here. */
|
|
|
|
// this._testSimpleMatchScorer();
|
|
|
|
// this._testFuzzyMatcher();
|
|
|
|
},
|
|
|
|
|
|
|
|
suggest: function () {
|
|
|
|
var _this = this;
|
|
|
|
|
|
|
|
var cursorPosition = this._getEditor().getCursorPosition();
|
|
|
|
var foundSuggestions = this.updateSuggestions(cursorPosition);
|
|
|
|
if (foundSuggestions) {
|
|
|
|
this.addListenerToOnDocumentChange();
|
|
|
|
|
|
|
|
// Show the completion popup.
|
|
|
|
this.show();
|
|
|
|
|
|
|
|
// handle click-out autoclosing.
|
2018-07-26 21:39:40 +02:00
|
|
|
var fn = function ( event ) {
|
|
|
|
let keycodes = [ 9, 10, 13 ]
|
|
|
|
|
|
|
|
/*if( ! keycodes.includes( event.keyCode ) ) {
|
|
|
|
return;
|
|
|
|
}*/
|
2018-07-13 18:39:55 +02:00
|
|
|
_this.hide();
|
|
|
|
$(window).off('click', fn);
|
|
|
|
};
|
|
|
|
$(window).on('click', fn);
|
|
|
|
} else {
|
|
|
|
this.clearSuggestionCache();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Update the suggestions for the word at the given position. Return true if
|
|
|
|
* some suitable suggestions could be found, false if not. */
|
|
|
|
updateSuggestions: function (position) {
|
|
|
|
var _this = this;
|
|
|
|
|
|
|
|
var session = this._getEditSession();
|
|
|
|
|
|
|
|
/* Extract the word being typed. Keep only the part of the word
|
|
|
|
* which is before the given position. It is somehow the prefix of
|
|
|
|
* the wanted full word. Make sure we only keep one word. */
|
|
|
|
var token = session.getTokenAt(position.row, position.column);
|
|
|
|
if (!token) {
|
|
|
|
/* No word at the given position. */
|
|
|
|
this.removeSuggestions();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var prefix = token.value.substr(0, position.column - token.start);
|
|
|
|
prefix = prefix.split(this.wordRegex).slice(-1)[0];
|
|
|
|
if (prefix === '') {
|
|
|
|
/* No word at the given position. */
|
|
|
|
this.removeSuggestions();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Build and order the suggestions themselves. */
|
|
|
|
// TODO cache suggestions and augment them incrementally.
|
|
|
|
var suggestionsAndDistance = this.getSuggestions(position);
|
|
|
|
var suggestions = this.rankSuggestions(prefix, suggestionsAndDistance);
|
|
|
|
if (suggestions.length < 1) {
|
|
|
|
/* No suitable suggestions found. */
|
|
|
|
this.removeSuggestions();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Remove the existing suggestions and populate the popup with the
|
|
|
|
* updated ones. */
|
|
|
|
this.removeSuggestions();
|
|
|
|
var popupContent = $('#autocomplete #suggestions');
|
|
|
|
$.each(suggestions, function (index, suggestion) {
|
|
|
|
/* First get rid of the suggestion suffix. */
|
|
|
|
suggestion = suggestion.slice(0, -1);
|
|
|
|
|
|
|
|
var indexes = _this.getMatchIndexes(prefix, suggestion);
|
|
|
|
$.each(indexes.reverse(), function (index, matchIndex) {
|
|
|
|
suggestion = suggestion.substr(0, matchIndex) +
|
|
|
|
'<span class="matched">' +
|
|
|
|
suggestion.substr(matchIndex, 1) +
|
|
|
|
'</span>' +
|
|
|
|
suggestion.substr(matchIndex + 1);
|
|
|
|
});
|
|
|
|
popupContent.append('<li class="suggestion">' + suggestion + '</li>');
|
|
|
|
});
|
|
|
|
|
|
|
|
this.selectFirstSuggestion();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
show: function () {
|
|
|
|
this.isVisible = true;
|
|
|
|
|
|
|
|
var popup = $('#autocomplete');
|
|
|
|
popup.css({
|
|
|
|
'top': this._computeTopOffset(),
|
|
|
|
'left': this._computeLeftOffset(),
|
|
|
|
'font-family': $('.ace_editor').css('font-family'),
|
|
|
|
'font-size': $('.ace_editor').css('font-size')
|
|
|
|
});
|
|
|
|
popup.slideToggle('fast', function(){
|
|
|
|
$(this).css('overflow', '');
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addKeyboardCommands();
|
|
|
|
},
|
|
|
|
|
|
|
|
hide: function () {
|
|
|
|
this.isVisible = false;
|
|
|
|
|
|
|
|
$('#autocomplete').hide();
|
|
|
|
this.removeSuggestions();
|
|
|
|
this.clearSuggestionCache();
|
|
|
|
|
|
|
|
this.removeListenerToOnDocumentChange();
|
|
|
|
this.removeKeyboardCommands();
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Return a jQuery object containing the currently selected suggestion. */
|
|
|
|
getSelectedSuggestion: function () {
|
|
|
|
var selectedSuggestion = $('#autocomplete li.suggestion.active-suggestion');
|
|
|
|
|
|
|
|
if (selectedSuggestion.length < 1) {
|
|
|
|
alert(i18n('No suggestion selected. Might be a bug.'));
|
|
|
|
} else if (selectedSuggestion.length > 1) {
|
|
|
|
alert(i18n('More than one suggestions selected. Might be a bug.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
return selectedSuggestion;
|
|
|
|
},
|
|
|
|
|
|
|
|
selectFirstSuggestion: function () {
|
|
|
|
var firstChild = $('li.suggestion:first-child');
|
|
|
|
firstChild.addClass('active-suggestion');
|
|
|
|
this._ensureVisible(firstChild, $('#autocomplete'));
|
|
|
|
},
|
|
|
|
|
|
|
|
selectLastSuggestion: function () {
|
|
|
|
var lastChild = $('li.suggestion:last-child');
|
|
|
|
lastChild.addClass('active-suggestion');
|
|
|
|
this._ensureVisible(lastChild, $('#autocomplete'));
|
|
|
|
},
|
|
|
|
|
|
|
|
selectNextSuggestion: function () {
|
|
|
|
var selectedSuggestion = this.getSelectedSuggestion();
|
|
|
|
selectedSuggestion.removeClass('active-suggestion');
|
|
|
|
var nextSuggestion = selectedSuggestion.next();
|
|
|
|
if (nextSuggestion.length > 0) {
|
|
|
|
nextSuggestion.addClass('active-suggestion');
|
|
|
|
this._ensureVisible(nextSuggestion, $('#autocomplete'));
|
|
|
|
} else {
|
|
|
|
/* The currently selected suggestion is the last one.
|
|
|
|
* Go back to first one. */
|
|
|
|
this.selectFirstSuggestion();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
selectPreviousSuggestion: function () {
|
|
|
|
var selectedSuggestion = this.getSelectedSuggestion();
|
|
|
|
selectedSuggestion.removeClass('active-suggestion');
|
|
|
|
var previousSuggestion = selectedSuggestion.prev();
|
|
|
|
if (previousSuggestion.length > 0) {
|
|
|
|
previousSuggestion.addClass('active-suggestion');
|
|
|
|
this._ensureVisible(previousSuggestion, $('#autocomplete'));
|
|
|
|
} else {
|
|
|
|
/* The currently selected suggestion is the first one.
|
|
|
|
* Go back to last one. */
|
|
|
|
this.selectLastSuggestion();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
addListenerToOnDocumentChange: function () {
|
|
|
|
var session = this._getEditSession();
|
|
|
|
session.addEventListener('change', this.$onDocumentChange);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeListenerToOnDocumentChange: function () {
|
|
|
|
var session = this._getEditSession();
|
|
|
|
session.removeEventListener('change', this.$onDocumentChange);
|
|
|
|
},
|
|
|
|
|
|
|
|
onDocumentChange: function (e) {
|
2018-07-26 21:39:40 +02:00
|
|
|
|
|
|
|
if ( e.data === undefined || e.data === null || e.data.text.search(/^\s+$/) !== -1) {
|
2018-07-13 18:39:55 +02:00
|
|
|
this.hide();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var position = null;
|
|
|
|
if (e.data.action === 'insertText') {
|
2018-07-26 21:39:40 +02:00
|
|
|
|
|
|
|
if( codiad.autosave.saving === false ) {
|
|
|
|
position = e.data.range.end;
|
|
|
|
} else {
|
|
|
|
var start = new Date().getTime();
|
|
|
|
for (var i = 0; i < 1e7; i++) {
|
|
|
|
if ((new Date().getTime() - start) > 50){
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
position = e.data.range.end;
|
|
|
|
}
|
2018-07-13 18:39:55 +02:00
|
|
|
} else if (e.data.action === 'removeText') {
|
|
|
|
position = e.data.range.start;
|
|
|
|
} else {
|
|
|
|
alert('Unkown document change action.');
|
|
|
|
}
|
|
|
|
|
|
|
|
var foundSuggestions = this.updateSuggestions(position);
|
|
|
|
if (!foundSuggestions) {
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
addKeyboardCommands: function () {
|
|
|
|
var _this = this;
|
|
|
|
var commandManager = this._getEditor().commands;
|
|
|
|
|
|
|
|
/* Save the standard commands that will be overwritten. */
|
|
|
|
this.standardGoLineDownExec = commandManager.commands.golinedown.exec;
|
|
|
|
this.standardGoLineUpExec = commandManager.commands.golineup.exec;
|
|
|
|
|
|
|
|
this.standardGoToRightExec = commandManager.commands.gotoright.exec;
|
|
|
|
this.standardGoToLeftExec = commandManager.commands.gotoleft.exec;
|
|
|
|
this.standardIndentExec = commandManager.commands.indent.exec;
|
|
|
|
|
|
|
|
/* Overwrite with the completion specific implementations. */
|
|
|
|
commandManager.commands.golinedown.exec = this.$selectNextSuggestion;
|
|
|
|
commandManager.commands.golineup.exec = this.$selectPreviousSuggestion;
|
|
|
|
|
|
|
|
commandManager.commands.gotoright.exec = this.$complete;
|
|
|
|
commandManager.commands.gotoleft.exec = this.$hide;
|
|
|
|
commandManager.commands.indent.exec = this.$complete;
|
|
|
|
|
|
|
|
commandManager.addCommand({
|
|
|
|
name: 'hideautocomplete',
|
|
|
|
bindKey: 'Esc',
|
|
|
|
exec: function () {
|
|
|
|
_this.hide();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
commandManager.addCommand({
|
|
|
|
name: 'autocomplete',
|
|
|
|
bindKey: 'Return',
|
|
|
|
exec: this.$complete
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
removeKeyboardCommands: function () {
|
|
|
|
var commandManager = this._getEditor().commands;
|
|
|
|
|
|
|
|
/* Make sure the standard exec have been initialized. */
|
|
|
|
if (this.standardGoLineDownExec !== null) {
|
|
|
|
commandManager.commands.golinedown.exec = this.standardGoLineDownExec;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.standardGoLineUpExec !== null) {
|
|
|
|
commandManager.commands.golineup.exec = this.standardGoLineUpExec;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.standardGoToRightExec !== null) {
|
|
|
|
commandManager.commands.gotoright.exec = this.standardGoToRightExec;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.standardGoToLeftExec !== null) {
|
|
|
|
commandManager.commands.gotoleft.exec = this.standardGoToLeftExec;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.standardIndentExec !== null) {
|
|
|
|
commandManager.commands.indent.exec = this.standardIndentExec;
|
|
|
|
}
|
|
|
|
|
|
|
|
commandManager.removeCommand('hideautocomplete');
|
|
|
|
commandManager.removeCommand('autocomplete');
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Complete the word at the given position with the currently selected
|
|
|
|
* suggestion. Only the part of the word before the position is
|
|
|
|
* replaced. */
|
|
|
|
complete: function (position) {
|
|
|
|
var editor = this._getEditor();
|
|
|
|
var session = this._getEditSession();
|
|
|
|
|
|
|
|
var position = editor.getCursorPosition();
|
|
|
|
|
|
|
|
/* Get the length of the word being typed. */
|
|
|
|
var token = session.getTokenAt(position.row, position.column);
|
|
|
|
if (!token) {
|
|
|
|
/* No token at the given position. */
|
|
|
|
this.clearSuggestionCache();
|
|
|
|
this.hide();
|
|
|
|
editor.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
var prefix = token.value.substr(0, position.column - token.start);
|
|
|
|
var prefixLength = prefix.split(this.wordRegex).slice(-1)[0].length;
|
|
|
|
|
|
|
|
var range = new Range(position.row,
|
|
|
|
position.column - prefixLength,
|
|
|
|
position.row,
|
|
|
|
position.column);
|
|
|
|
|
|
|
|
var suggestion = this.getSelectedSuggestion().text();
|
|
|
|
session.replace(range, suggestion);
|
|
|
|
|
|
|
|
this.hide();
|
|
|
|
editor.focus();
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Remove the suggestions from the Dom. */
|
|
|
|
removeSuggestions: function () {
|
|
|
|
$('.suggestion').remove();
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Get suggestions of completion for the current position in the
|
|
|
|
* document. */
|
|
|
|
getSuggestions: function (position) {
|
|
|
|
|
|
|
|
/* If suggestions are cached,
|
|
|
|
* return them directely */
|
|
|
|
if (this._suggestionCache) {
|
|
|
|
return this._suggestionCache;
|
|
|
|
}
|
|
|
|
|
|
|
|
var doc = this._getDocument();
|
|
|
|
|
|
|
|
/* FIXME For now, make suggestions on the whole file content except
|
|
|
|
* the current token. Might be a little bit smarter, e.g., remove
|
|
|
|
* all the keywords associated with the current language. */
|
|
|
|
|
|
|
|
/* Get all the text, put a marker at the cursor position. The
|
|
|
|
* marker uses word character so that it won't be discarded by a
|
|
|
|
* word split. */
|
|
|
|
var markerString = '__autocomplete_marker__';
|
|
|
|
var text = doc.getLines(0, position.row - 1).join("\n") + "\n";
|
|
|
|
var currentLine = doc.getLine(position.row);
|
|
|
|
text += currentLine.substr(0, position.column);
|
|
|
|
text += markerString;
|
|
|
|
if (position.column === currentLine.length) {
|
|
|
|
// position is at end of line, add a break line.
|
|
|
|
text += "\n";
|
|
|
|
}
|
|
|
|
text += currentLine.substr(position.column + 1);
|
|
|
|
text += doc.getLines(position.row + 1, doc.getLength()).join("\n") + "\n";
|
|
|
|
|
|
|
|
/* Split the text into words. */
|
|
|
|
var suggestions = text.split(this.wordRegex);
|
|
|
|
|
|
|
|
/* Get the index of the word at the cursor position. */
|
|
|
|
var markerIndex = 0;
|
|
|
|
var markedWord = '';
|
|
|
|
$.each(suggestions, function (index, value) {
|
|
|
|
if (value.search(markerString) !== -1) {
|
|
|
|
markerIndex = index;
|
|
|
|
markedWord = value;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/* Build an object associating the suggestions with their distance
|
|
|
|
* to the word at cursor position. To make sure that no suggestion
|
|
|
|
* string overrides a built-in method of the suggestionsAndDistance
|
|
|
|
* object, suffix all the suggestions with '-'. Afterward, make
|
|
|
|
* sure of removing that suffix before using the stored
|
|
|
|
* suggestions! */
|
|
|
|
var suggestionsAndDistance = {};
|
|
|
|
$.each(suggestions, function (index, suggestion) {
|
|
|
|
var distance = Math.abs(index - markerIndex);
|
|
|
|
if (!suggestionsAndDistance[suggestion + '-'] ||
|
|
|
|
distance < suggestionsAndDistance[suggestion]) {
|
|
|
|
suggestionsAndDistance[suggestion + '-'] = distance;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/* Remove from the suggestions the word under the cursor. */
|
|
|
|
delete suggestionsAndDistance[markedWord + '-'];
|
|
|
|
|
|
|
|
/* Fill the cache */
|
|
|
|
this._suggestionCache = suggestionsAndDistance;
|
|
|
|
|
|
|
|
return suggestionsAndDistance;
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Clear the suggestion cache */
|
|
|
|
clearSuggestionCache: function () {
|
|
|
|
this._suggestionCache = null;
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Given an object associating suggestions and their distances to the
|
|
|
|
* word under the cursor (the prefix), return a ranked array of
|
|
|
|
* suggestions with the best match first. The suggestions are ranked
|
|
|
|
* based on if they match the prefix fuzzily, how much they match the
|
|
|
|
* given prefix in the computeSimpleMatchScore sense and on their
|
|
|
|
* distances to the prefix. */
|
|
|
|
rankSuggestions: function (prefix, suggestionsAndDistance) {
|
|
|
|
/* Initialize maxScore to one to ensure removing the non matching
|
|
|
|
* suggestions (those with a zero score). */
|
|
|
|
var maxScore = 1;
|
|
|
|
var suggestionsAndMatchScore = {};
|
|
|
|
for (var suggestion in suggestionsAndDistance) {
|
|
|
|
if (Object.prototype.hasOwnProperty.call(suggestionsAndDistance, suggestion)) {
|
|
|
|
var score = this.computeSimpleMatchScore(prefix, suggestion);
|
|
|
|
if (score > maxScore) {
|
|
|
|
maxScore = score;
|
|
|
|
}
|
|
|
|
suggestionsAndMatchScore[suggestion] = score;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Remove the suggestions that do not match the prefix fuzzily. */
|
|
|
|
for (suggestion in suggestionsAndMatchScore) {
|
|
|
|
if (Object.prototype.hasOwnProperty.call(suggestionsAndMatchScore, suggestion)) {
|
|
|
|
if (!this.isMatchingFuzzily(prefix, suggestion)) {
|
|
|
|
delete suggestionsAndMatchScore[suggestion];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Now for each suggestion we have its matching score and its
|
|
|
|
* distance to the word under the cursor. So compute its final
|
|
|
|
* score as a combination of both. */
|
|
|
|
var suggestionsAndFinalScore = {};
|
|
|
|
for (suggestion in suggestionsAndMatchScore) {
|
|
|
|
if (Object.prototype.hasOwnProperty.call(suggestionsAndMatchScore, suggestion)) {
|
|
|
|
//suggestionsAndFinalScore[suggestion] = suggestionsAndMatchScore[suggestion] -
|
|
|
|
// suggestionsAndDistance[suggestion];
|
|
|
|
suggestionsAndFinalScore[suggestion] = suggestionsAndMatchScore[suggestion] / suggestion.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Make an array of suggestions and make sure to rank them in the
|
|
|
|
* ascending scores order. */
|
|
|
|
var suggestions = [];
|
|
|
|
for (suggestion in suggestionsAndFinalScore) {
|
|
|
|
if (Object.prototype.hasOwnProperty.call(suggestionsAndFinalScore, suggestion)) {
|
|
|
|
suggestions.push(suggestion);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
suggestions.sort(function (firstSuggestion, secondSuggestion) {
|
|
|
|
return suggestionsAndFinalScore[secondSuggestion] - suggestionsAndFinalScore[firstSuggestion];
|
|
|
|
});
|
|
|
|
|
|
|
|
return suggestions;
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Return the number of consecutive letters starting from the first
|
|
|
|
* letter in suggestion that match prefix. For instance,
|
|
|
|
* this.computeSimpleMatchScore(cod, codiad) will return 3. If
|
|
|
|
* suggestion is shorter than prefix, return a score of zero. The score
|
|
|
|
* is computed using a Vim-like smartcase behavior. */
|
|
|
|
computeSimpleMatchScore: function (prefix, suggestion) {
|
|
|
|
/* Use a Vim-like smartcase behavior. If prefix is all lowercase,
|
|
|
|
* compute the match score case insensitive, if it is not, compute
|
|
|
|
* the score case sensitive. */
|
|
|
|
var localSuggestion = this._isLowerCase(prefix) ? suggestion.toLowerCase() : suggestion;
|
|
|
|
|
|
|
|
if (localSuggestion.length < prefix.length) {
|
|
|
|
return 0;
|
|
|
|
} else if (localSuggestion === prefix) {
|
|
|
|
return prefix.length;
|
|
|
|
} else {
|
|
|
|
var score = 0;
|
|
|
|
for (var i = 0; i < prefix.length; ++i) {
|
|
|
|
if (localSuggestion[i] === prefix[i]) {
|
|
|
|
++score;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return score;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Return true if suggestion fuzzily matches prefix. Because everybody
|
|
|
|
* loves fuzzy matches.
|
|
|
|
* For instance, this.isMatchingFuzzily(mlf, mylongfunctionname)
|
|
|
|
* will return true. The score is computed using a Vim-like smartcase
|
|
|
|
* behavior. */
|
|
|
|
isMatchingFuzzily: function (prefix, suggestion) {
|
|
|
|
/* Use a Vim-like smartcase behavior. If prefix is all lowercase,
|
|
|
|
* compute the match score case insensitive, if it is not, compute
|
|
|
|
* the score case sensitive. */
|
|
|
|
var localSuggestion = this._isLowerCase(prefix) ? suggestion.toLowerCase() : suggestion;
|
|
|
|
|
|
|
|
/* Escape the characters that have a special meaning for regex in
|
|
|
|
* the prefix. */
|
|
|
|
var localPrefix = prefix.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
|
|
|
|
|
|
var fuzzyRegex = '^.*?';
|
|
|
|
for (var i = 0; i < localPrefix.length; ++i) {
|
|
|
|
if (localPrefix[i] === '\\') {
|
|
|
|
fuzzyRegex += localPrefix[i];
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
|
|
|
|
fuzzyRegex += localPrefix[i];
|
|
|
|
fuzzyRegex += '.*?';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (localSuggestion.search(fuzzyRegex) !== -1) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
getMatchIndexes: function (prefix, suggestion) {
|
|
|
|
/* Use a Vim-like smartcase behavior. If prefix is all lowercase,
|
|
|
|
* find the match indexes case insensitive, if it is not, find them
|
|
|
|
* case sensitive. */
|
|
|
|
var localSuggestion = this._isLowerCase(prefix) ? suggestion.toLowerCase() : suggestion;
|
|
|
|
|
|
|
|
var matchIndexes = [];
|
|
|
|
var startIndex = 0;
|
|
|
|
for (var i = 0; i < prefix.length; ++i) {
|
|
|
|
var index = localSuggestion.indexOf(prefix[i], startIndex);
|
|
|
|
matchIndexes.push(index);
|
|
|
|
startIndex = index + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return matchIndexes;
|
|
|
|
},
|
|
|
|
|
|
|
|
_isLowerCase: function (str) {
|
|
|
|
return (str.toLowerCase() === str);
|
|
|
|
},
|
|
|
|
|
|
|
|
_ensureVisible: function (el, parent) {
|
|
|
|
var offset = 1;
|
|
|
|
var paneMin = parent.scrollTop();
|
|
|
|
var paneMax = paneMin + parent.innerHeight();
|
|
|
|
var itemMin = el.position().top + paneMin - offset;
|
|
|
|
var itemMax = itemMin + el.outerHeight() + 2*offset;
|
|
|
|
if (itemMax > paneMax) {
|
|
|
|
parent.stop().animate({
|
|
|
|
scrollTop: itemMax - parent.innerHeight()
|
|
|
|
}, 100);
|
|
|
|
} else if (itemMin < paneMin) {
|
|
|
|
parent.stop().animate({
|
|
|
|
scrollTop: itemMin
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_computeTopOffset: function () {
|
|
|
|
/* FIXME How to handle multiple cursors? This seems to compute the
|
|
|
|
* offset using the position of the last created cursor. */
|
|
|
|
var cursor = $('.ace_cursor');
|
|
|
|
if (cursor.length > 0) {
|
|
|
|
var fontSize = codiad.editor.getActive().container.style.fontSize.replace('px', '');
|
|
|
|
var interLine = 1.7;
|
|
|
|
cursor = $(cursor[0]);
|
|
|
|
var top = cursor.offset().top + fontSize * interLine;
|
|
|
|
return top;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_computeLeftOffset: function () {
|
|
|
|
/* FIXME How to handle multiple cursors? This seems to compute the
|
|
|
|
* offset using the position of the last created cursor. */
|
|
|
|
var cursor = $('.ace_cursor');
|
|
|
|
if (cursor.length > 0) {
|
|
|
|
cursor = $(cursor[0]);
|
|
|
|
var left = cursor.offset().left;
|
|
|
|
return left;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Set of helper methods to manipulate the editor. */
|
|
|
|
_getEditor: function () {
|
|
|
|
return codiad.editor.getActive();
|
|
|
|
},
|
|
|
|
|
|
|
|
_getEditSession: function () {
|
|
|
|
return codiad.editor.getActive().getSession();
|
|
|
|
},
|
|
|
|
|
|
|
|
_getDocument: function () {
|
|
|
|
return codiad.editor.getActive().getSession().getDocument();
|
|
|
|
},
|
|
|
|
|
|
|
|
/* Some unit tests. */
|
|
|
|
//i don't need to translate this... this is just for testing things...
|
|
|
|
_testSimpleMatchScorer: function () {
|
|
|
|
var prefix = 'myprefix';
|
|
|
|
var suggestion = 'myprefixisshort';
|
|
|
|
var score = this.computeSimpleMatchScore(prefix, suggestion);
|
|
|
|
if (score !== 8) {
|
|
|
|
alert('_testSimpleMatchScorer lowercase test failed.');
|
|
|
|
}
|
|
|
|
|
|
|
|
prefix = 'MYPREFIX';
|
|
|
|
suggestion = 'MYPREFIXISSHORT';
|
|
|
|
score = this.computeSimpleMatchScore(prefix, suggestion);
|
|
|
|
if (score !== 8) {
|
|
|
|
alert('_testSimpleMatchScorer uppercase test failed.');
|
|
|
|
}
|
|
|
|
|
|
|
|
prefix = 'myPrefix';
|
|
|
|
suggestion = 'myprefixisshort';
|
|
|
|
score = this.computeSimpleMatchScore(prefix, suggestion);
|
|
|
|
if (score !== 2) {
|
|
|
|
alert('_testSimpleMatchScorer mixed case vs. lowercase test failed.');
|
|
|
|
}
|
|
|
|
|
|
|
|
prefix = 'myPrefixIs';
|
|
|
|
suggestion = 'myPrefixIsShort';
|
|
|
|
score = this.computeSimpleMatchScore(prefix, suggestion);
|
|
|
|
if (score !== 10) {
|
|
|
|
alert('_testSimpleMatchScorer mixed case test failed.');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_testFuzzyMatcher: function () {
|
|
|
|
var isMatching = this.isMatchingFuzzily('mlf', 'mylongfunctionname');
|
|
|
|
if (!isMatching) {
|
|
|
|
alert('_testFuzzyMatcher mlf vs. mylongfunctionname failed.');
|
|
|
|
}
|
|
|
|
|
|
|
|
isMatching = this.isMatchingFuzzily('mLn', 'myLongFunctionName');
|
|
|
|
if (!isMatching) {
|
|
|
|
alert('_testFuzzyMatcher mLn vs. myLongFunctionName failed.');
|
|
|
|
}
|
|
|
|
isMatching = this.isMatchingFuzzily('mLFuny', 'myLongFunctionName');
|
|
|
|
if (isMatching) {
|
|
|
|
alert('_testFuzzyMatcher mLFuny. myLongFunctionName failed.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
2018-07-26 21:39:40 +02:00
|
|
|
})(this, jQuery);
|