improve autocomplete popup
This commit is contained in:
parent
8cb5b1f0e9
commit
7edecc9f2a
2 changed files with 135 additions and 71 deletions
|
|
@ -61,7 +61,7 @@ var Autocomplete = function() {
|
|||
}.bind(this));
|
||||
};
|
||||
|
||||
this.openPopup = function(editor, keepPopupPosition) {
|
||||
this.openPopup = function(editor, prefix, keepPopupPosition) {
|
||||
if (!this.popup)
|
||||
this.$init();
|
||||
|
||||
|
|
@ -70,7 +70,10 @@ var Autocomplete = function() {
|
|||
var renderer = editor.renderer;
|
||||
if (!keepPopupPosition) {
|
||||
var lineHeight = renderer.layerConfig.lineHeight;
|
||||
var pos = renderer.$cursorLayer.getPixelPosition(null, true);
|
||||
|
||||
var pos = renderer.$cursorLayer.getPixelPosition(this.base, true);
|
||||
pos.left -= this.popup.getTextLeftOffset();
|
||||
|
||||
var rect = editor.container.getBoundingClientRect();
|
||||
pos.top += rect.top - renderer.layerConfig.offset;
|
||||
pos.left += rect.left;
|
||||
|
|
@ -83,18 +86,24 @@ var Autocomplete = function() {
|
|||
|
||||
this.detach = function() {
|
||||
this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
|
||||
this.editor.removeEventListener("changeSelection", this.changeListener);
|
||||
this.editor.removeEventListener("blur", this.changeListener);
|
||||
this.editor.removeEventListener("mousedown", this.changeListener);
|
||||
this.editor.off("changeSelection", this.changeListener);
|
||||
this.editor.off("blur", this.changeListener);
|
||||
this.editor.off("mousedown", this.changeListener);
|
||||
this.editor.off("mousewheel", this.mousewheelListener);
|
||||
this.changeTimer.cancel();
|
||||
|
||||
if (this.popup)
|
||||
this.popup.hide();
|
||||
|
||||
this.activated = false;
|
||||
this.completions = this.base = null;
|
||||
};
|
||||
|
||||
this.changeListener = function(e) {
|
||||
var cursor = this.editor.selection.lead;
|
||||
if (cursor.row != this.base.row || cursor.column < this.base.column) {
|
||||
this.detach();
|
||||
}
|
||||
if (this.activated)
|
||||
this.changeTimer.schedule();
|
||||
else
|
||||
|
|
@ -129,7 +138,6 @@ var Autocomplete = function() {
|
|||
};
|
||||
|
||||
this.insertMatch = function(data) {
|
||||
this.detach();
|
||||
if (!data)
|
||||
data = this.popup.getData(this.popup.getRow());
|
||||
if (!data)
|
||||
|
|
@ -147,6 +155,7 @@ var Autocomplete = function() {
|
|||
else
|
||||
this.editor.insert(data.value || data);
|
||||
}
|
||||
this.detach();
|
||||
};
|
||||
|
||||
this.commands = {
|
||||
|
|
@ -165,48 +174,15 @@ var Autocomplete = function() {
|
|||
"PageDown": function(editor) { editor.completer.popup.gotoPageUp(); }
|
||||
};
|
||||
|
||||
this.filterCompletions = function(items, needle) {
|
||||
var results = [];
|
||||
var upper = needle.toUpperCase();
|
||||
var lower = needle.toLowerCase();
|
||||
loop: for (var i = 0, item; item = items[i]; i++) {
|
||||
var caption = item.value || item.caption;
|
||||
if (!caption) continue;
|
||||
var lastIndex = -1;
|
||||
var matchMask = 0;
|
||||
var penalty = 0;
|
||||
var index, distance;
|
||||
// caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf
|
||||
for (var j = 0; j < needle.length; j++) {
|
||||
// TODO add penalty on case mismatch
|
||||
var i1 = caption.indexOf(lower[j], lastIndex + 1);
|
||||
var i2 = caption.indexOf(upper[j], lastIndex + 1);
|
||||
index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2;
|
||||
if (index < 0)
|
||||
continue loop;
|
||||
distance = index - lastIndex - 1;
|
||||
if (distance > 0) {
|
||||
// first char mismatch should be more sensitive
|
||||
if (lastIndex == -1)
|
||||
penalty += 10;
|
||||
penalty += distance;
|
||||
}
|
||||
matchMask = matchMask | (1 << index);
|
||||
lastIndex = index;
|
||||
}
|
||||
item.matchMask = matchMask;
|
||||
item.score = (item.score || 0) - penalty;
|
||||
results.push(item);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
this.gatherCompletions = function(editor, callback) {
|
||||
var session = editor.getSession();
|
||||
var pos = editor.getCursorPosition();
|
||||
|
||||
|
||||
var line = session.getLine(pos.row);
|
||||
var prefix = util.retrievePrecedingIdentifier(line, pos.column);
|
||||
|
||||
this.base = editor.getCursorPosition();
|
||||
this.base.column -= prefix.length;
|
||||
|
||||
var matches = [];
|
||||
util.parForEach(editor.completers, function(completer, next) {
|
||||
|
|
@ -241,10 +217,20 @@ var Autocomplete = function() {
|
|||
editor.on("changeSelection", this.changeListener);
|
||||
editor.on("blur", this.blurListener);
|
||||
editor.on("mousedown", this.mousedownListener);
|
||||
editor.on("mousewheel", this.mousewheelListener);
|
||||
this.updateCompletions();
|
||||
}
|
||||
|
||||
this.updateCompletions = function(keepPopupPosition) {
|
||||
if (keepPopupPosition && this.base && this.completions) {
|
||||
var pos = this.editor.getCursorPosition();
|
||||
var prefix = ace.session.getTextRange({start: this.base, end: pos});
|
||||
this.completions.setFilter(prefix);
|
||||
if (!this.completions.filtered.length)
|
||||
return this.detach();
|
||||
this.openPopup(this.editor, prefix, keepPopupPosition);
|
||||
return;
|
||||
}
|
||||
this.gatherCompletions(this.editor, function(err, results) {
|
||||
var matches = results && results.matches;
|
||||
if (!matches || !matches.length)
|
||||
|
|
@ -253,14 +239,11 @@ var Autocomplete = function() {
|
|||
// if (matches.length == 1)
|
||||
// return this.insertMatch(matches[0]);
|
||||
|
||||
|
||||
matches = this.filterCompletions(matches, results.prefix);
|
||||
matches = matches.sort(function(a, b) {
|
||||
return b.score - a.score;
|
||||
});
|
||||
this.completions = new FilteredList(matches);
|
||||
this.completions.setFilter(results.prefix);
|
||||
this.openPopup(this.editor, keepPopupPosition);
|
||||
if (!this.completions.filtered.length)
|
||||
return this.detach();
|
||||
this.openPopup(this.editor, results.prefix, keepPopupPosition);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
@ -288,16 +271,71 @@ Autocomplete.startCommand = {
|
|||
bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
|
||||
};
|
||||
|
||||
var FilteredList = function(array, mutateData) {
|
||||
var FilteredList = function(array, filterText, mutateData) {
|
||||
this.all = array;
|
||||
this.filtered = array.concat();
|
||||
this.filterText = "";
|
||||
this.filtered = array;
|
||||
this.filterText = filterText || "";
|
||||
};
|
||||
(function(){
|
||||
this.setFilter = function(str) {
|
||||
this.filterText = str;
|
||||
};
|
||||
if (str.length > this.filterText && str.lastIndexOf(this.filterText, 0) === 0)
|
||||
var matches = this.filtered;
|
||||
else
|
||||
var matches = this.all;
|
||||
|
||||
this.filterText = str;
|
||||
matches = this.filterCompletions(matches, this.filterText);
|
||||
matches = matches.sort(function(a, b) {
|
||||
return b.exactMatch - a.exactMatch || b.score - a.score;
|
||||
});
|
||||
|
||||
// make unique
|
||||
var prev = null;
|
||||
matches = matches.filter(function(item){
|
||||
var caption = item.value || item.caption || item.snippet;
|
||||
if (caption === prev) return false;
|
||||
prev = caption;
|
||||
return true;
|
||||
});
|
||||
|
||||
this.filtered = matches;
|
||||
};
|
||||
this.filterCompletions = function(items, needle) {
|
||||
var results = [];
|
||||
var upper = needle.toUpperCase();
|
||||
var lower = needle.toLowerCase();
|
||||
loop: for (var i = 0, item; item = items[i]; i++) {
|
||||
var caption = item.value || item.caption || item.snippet;
|
||||
if (!caption) continue;
|
||||
var lastIndex = -1;
|
||||
var matchMask = 0;
|
||||
var penalty = 0;
|
||||
var index, distance;
|
||||
// caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf
|
||||
for (var j = 0; j < needle.length; j++) {
|
||||
// TODO add penalty on case mismatch
|
||||
var i1 = caption.indexOf(lower[j], lastIndex + 1);
|
||||
var i2 = caption.indexOf(upper[j], lastIndex + 1);
|
||||
index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2;
|
||||
if (index < 0)
|
||||
continue loop;
|
||||
distance = index - lastIndex - 1;
|
||||
if (distance > 0) {
|
||||
// first char mismatch should be more sensitive
|
||||
if (lastIndex === -1)
|
||||
penalty += 10;
|
||||
penalty += distance;
|
||||
}
|
||||
matchMask = matchMask | (1 << index);
|
||||
lastIndex = index;
|
||||
}
|
||||
item.matchMask = matchMask;
|
||||
item.exactMatch = penalty ? 0 : 1;
|
||||
item.score = (item.score || 0) - penalty;
|
||||
results.push(item);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
}).call(FilteredList.prototype);
|
||||
|
||||
exports.Autocomplete = Autocomplete;
|
||||
|
|
|
|||
|
|
@ -89,13 +89,36 @@ var AcePopup = function(parentNode) {
|
|||
e.stop();
|
||||
});
|
||||
|
||||
var hoverMarker = new Range(-1,0,-1,Infinity);
|
||||
hoverMarker.id = popup.session.addMarker(hoverMarker, "ace_line-hover", "fullLine");
|
||||
var lastMouseEvent;
|
||||
var hoverMarker = new Range(-1,0,-1,Infinity);
|
||||
popup.setSelectOnHover = function(val) {
|
||||
if (!val) {
|
||||
hoverMarker.id = popup.session.addMarker(hoverMarker, "ace_line-hover", "fullLine");
|
||||
} else if (hoverMarker.id) {
|
||||
popup.session.removeMarker(hoverMarker.id);
|
||||
hoverMarker.id = null;
|
||||
}
|
||||
}
|
||||
popup.setSelectOnHover(false)
|
||||
popup.on("mousemove", function(e) {
|
||||
//if (popup.lastOpened)
|
||||
var row = e.getDocumentPosition().row;
|
||||
hoverMarker.start.row = hoverMarker.end.row = row;
|
||||
popup.session._emit("changeBackMarker");
|
||||
lastMouseEvent = e;
|
||||
lastMouseEvent.scrollTop = popup.renderer.scrollTop;
|
||||
var row = lastMouseEvent.getDocumentPosition().row;
|
||||
if (hoverMarker.start.row != row) {
|
||||
popup.session._emit("changeBackMarker");
|
||||
if (!hoverMarker.id)
|
||||
popup.setRow(row);
|
||||
hoverMarker.start.row = hoverMarker.end.row = row;
|
||||
}
|
||||
});
|
||||
popup.renderer.on("beforeRender", function() {
|
||||
if (lastMouseEvent && hoverMarker.start.row != -1) {
|
||||
lastMouseEvent.$pos = null;
|
||||
var row = lastMouseEvent.getDocumentPosition().row;
|
||||
if (!hoverMarker.id)
|
||||
popup.setRow(row);
|
||||
hoverMarker.start.row = hoverMarker.end.row = row;
|
||||
}
|
||||
});
|
||||
var hideHoverMarker = function() {
|
||||
hoverMarker.start.row = hoverMarker.end.row = -1;
|
||||
|
|
@ -104,12 +127,7 @@ var AcePopup = function(parentNode) {
|
|||
event.addListener(popup.container, "mouseout", hideHoverMarker);
|
||||
popup.on("hide", hideHoverMarker);
|
||||
popup.on("changeSelection", hideHoverMarker);
|
||||
popup.on("mousewheel", function(e) {
|
||||
setTimeout(function() {
|
||||
popup._signal("mousemove", e);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
popup.session.doc.getLength = function() {
|
||||
return popup.data.length;
|
||||
};
|
||||
|
|
@ -137,7 +155,7 @@ var AcePopup = function(parentNode) {
|
|||
c = data.caption[i];
|
||||
flag = data.matchMask & (1 << i) ? 1 : 0;
|
||||
if (last !== flag) {
|
||||
tokens.push({type: data.className || "" + ( flag ? ".bold" : ""), value: c});
|
||||
tokens.push({type: data.className || "" + ( flag ? "completion-highlight" : ""), value: c});
|
||||
last = flag;
|
||||
} else {
|
||||
tokens[tokens.length - 1].value += c;
|
||||
|
|
@ -176,7 +194,7 @@ var AcePopup = function(parentNode) {
|
|||
popup.setRow = function(line) {
|
||||
popup.setHighlightActiveLine(line != -1);
|
||||
popup.selection.clearSelection();
|
||||
popup.moveCursorTo(line, 0 || 0);
|
||||
popup.moveCursorTo(line || 0, 0);
|
||||
};
|
||||
|
||||
popup.hide = function() {
|
||||
|
|
@ -200,13 +218,17 @@ var AcePopup = function(parentNode) {
|
|||
|
||||
this._signal("show");
|
||||
};
|
||||
|
||||
popup.getTextLeftOffset = function() {
|
||||
return 1 + this.renderer.layerConfig.padding;
|
||||
}
|
||||
|
||||
return popup;
|
||||
};
|
||||
|
||||
dom.importCssString("\
|
||||
.ace_autocomplete.ace-tm .ace_marker-layer .ace_active-line {\
|
||||
background-color: #abbffe;\
|
||||
background-color: #CAD6FA;\
|
||||
}\
|
||||
.ace_autocomplete.ace-tm .ace_line-hover {\
|
||||
border: 1px solid #abbffe;\
|
||||
|
|
@ -223,13 +245,17 @@ dom.importCssString("\
|
|||
text-align: right;\
|
||||
z-index: -1;\
|
||||
}\
|
||||
.ace_autocomplete .ace_completion-highlight{\
|
||||
font-weight: bold\
|
||||
}\
|
||||
.ace_autocomplete {\
|
||||
width: 200px;\
|
||||
width: 280px;\
|
||||
z-index: 200000;\
|
||||
background: #f8f8f8;\
|
||||
background: #fbfbfb;\
|
||||
border: 1px lightgray solid;\
|
||||
position: fixed;\
|
||||
box-shadow: 2px 3px 5px rgba(0,0,0,.2);\
|
||||
line-height: 1.4;\
|
||||
}");
|
||||
|
||||
exports.AcePopup = AcePopup;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue