From 7edecc9f2ad2be7f7187b04b0f263bd807f4fd19 Mon Sep 17 00:00:00 2001 From: nightwing Date: Mon, 2 Sep 2013 00:21:51 +0400 Subject: [PATCH] improve autocomplete popup --- lib/ace/autocomplete.js | 146 +++++++++++++++++++++------------- lib/ace/autocomplete/popup.js | 60 ++++++++++---- 2 files changed, 135 insertions(+), 71 deletions(-) diff --git a/lib/ace/autocomplete.js b/lib/ace/autocomplete.js index 16707595..c4631974 100644 --- a/lib/ace/autocomplete.js +++ b/lib/ace/autocomplete.js @@ -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; diff --git a/lib/ace/autocomplete/popup.js b/lib/ace/autocomplete/popup.js index 2d0bba1a..63f545c4 100644 --- a/lib/ace/autocomplete/popup.js +++ b/lib/ace/autocomplete/popup.js @@ -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;