diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js index 9880d33e..52f71d20 100644 --- a/demo/kitchen-sink/demo.js +++ b/demo/kitchen-sink/demo.js @@ -251,7 +251,7 @@ commands.addCommand({ } }); -var keybindings = { +var keybindings = { ace: null, // Null = use "default" keymapping vim: require("ace/keyboard/vim").handler, emacs: "ace/keyboard/emacs", @@ -317,7 +317,7 @@ doclist.history.index = 0; doclist.cycleOpen = function(editor, dir) { var h = this.history; h.index += dir; - if (h.index >= h.length) + if (h.index >= h.length) h.index = 0; else if (h.index <= 0) h.index = h.length - 1; @@ -499,7 +499,7 @@ bindDropdown("split", function(value) { sp.setSplits(1); } else { var newEditor = (sp.getSplits() == 1); - sp.setOrientation(value == "below" ? sp.BELOW : sp.BESIDE); + sp.setOrientation(value == "below" ? sp.BELOW : sp.BESIDE); sp.setSplits(2); if (newEditor) { @@ -592,6 +592,7 @@ env.editSnippets = function() { require("ace/ext/language_tools"); env.editor.setOptions({ enableBasicAutocompletion: true, + enableLiveAutocomplete: true, enableSnippets: true }); diff --git a/lib/ace/autocomplete.js b/lib/ace/autocomplete.js index 1086c3d9..de404815 100644 --- a/lib/ace/autocomplete.js +++ b/lib/ace/autocomplete.js @@ -55,6 +55,8 @@ var Autocomplete = function() { }; (function() { + this.gatherCompletionsId = 0; + this.$init = function() { this.popup = new AcePopup(document.body || document.documentElement); this.popup.on("click", function(e) { @@ -72,6 +74,7 @@ var Autocomplete = function() { var renderer = editor.renderer; this.popup.setRow(this.autoSelect ? 0 : -1); if (!keepPopupPosition) { + this.popup.setTheme(editor.getTheme()); this.popup.setFontSize(editor.getFontSize()); var lineHeight = renderer.layerConfig.lineHeight; @@ -96,6 +99,10 @@ var Autocomplete = function() { this.editor.off("mousewheel", this.mousewheelListener); this.changeTimer.cancel(); + if (this.popup && this.popup.isOpen) { + this.gatherCompletionsId = this.gatherCompletionsId + 1; + } + if (this.popup) this.popup.hide(); @@ -146,6 +153,7 @@ var Autocomplete = function() { data = this.popup.getData(this.popup.getRow()); if (!data) return false; + if (data.completer && data.completer.insertMatch) { data.completer.insertMatch(this.editor); } else { @@ -197,18 +205,18 @@ var Autocomplete = function() { this.base.column -= prefix.length; var matches = []; - util.parForEach(editor.completers, function(completer, next) { + var total = editor.completers.length; + editor.completers.forEach(function(completer, i) { completer.getCompletions(editor, session, pos, prefix, function(err, results) { if (!err) matches = matches.concat(results); - next(); - }); - }, function() { callback(null, { prefix: prefix, - matches: matches + matches: matches, + finished: (--total === 0) }); }); + }); return true; }; @@ -243,10 +251,30 @@ var Autocomplete = function() { this.completions.setFilter(prefix); if (!this.completions.filtered.length) return this.detach(); + if (this.completions.filtered.length == 1 + && this.completions.filtered[0].value == prefix + && !this.completions.filtered[0].snippet) + return this.detach(); this.openPopup(this.editor, prefix, keepPopupPosition); return; } + + // Save current gatherCompletions session, session is close when a match is insert + var _id = this.gatherCompletionsId; this.gatherCompletions(this.editor, function(err, results) { + // Only detach if result gathering is finished + var doDetach = function() { + if (!results.finished) return; + return this.detach(); + }.bind(this); + + // Calcul prefix + var session = this.editor.getSession(); + var pos = this.editor.getCursorPosition(); + var line = session.getLine(pos.row); + var prefix = util.retrievePrecedingIdentifier(line, pos.column); + + // Results matches var matches = results && results.matches; if (!matches || !matches.length) return this.detach(); @@ -254,14 +282,32 @@ var Autocomplete = function() { // if (matches.length == 1) // return this.insertMatch(matches[0]); + // No prefix or no results -> close + if (!prefix || !prefix.length || !matches || !matches.length) + return doDetach(); + + // Wrong prefix or wrong session -> ignore + if (prefix.indexOf(results.prefix) != 0 + || _id != this.gatherCompletionsId) + return; + this.completions = new FilteredList(matches); - this.completions.setFilter(results.prefix); + this.completions.setFilter(prefix); var filtered = this.completions.filtered; + + // No results if (!filtered.length) - return this.detach(); + return doDetach(); + + // One result equals to the prefix + if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet) + return doDetach(); + + // Autoinsert if one result if (this.autoInsert && filtered.length == 1) return this.insertMatch(filtered[0]); - this.openPopup(this.editor, results.prefix, keepPopupPosition); + + this.openPopup(this.editor, prefix, keepPopupPosition); }.bind(this)); }; diff --git a/lib/ace/autocomplete/popup.js b/lib/ace/autocomplete/popup.js index 02ef21ee..a0ab7604 100644 --- a/lib/ace/autocomplete/popup.js +++ b/lib/ace/autocomplete/popup.js @@ -43,7 +43,7 @@ var $singleLineEditor = function(el) { var renderer = new Renderer(el); renderer.$maxLines = 4; - + var editor = new Editor(renderer); editor.setHighlightActiveLine(false); @@ -59,13 +59,13 @@ var $singleLineEditor = function(el) { var AcePopup = function(parentNode) { var el = dom.createElement("div"); var popup = new $singleLineEditor(el); - + if (parentNode) parentNode.appendChild(el); el.style.display = "none"; popup.renderer.content.style.cursor = "default"; popup.renderer.setStyle("ace_autocomplete"); - + popup.setOption("displayIndentGuides", false); var noop = function(){}; @@ -154,11 +154,11 @@ var AcePopup = function(parentNode) { popup.getHoveredRow = function() { return hoverMarker.start.row; }; - + event.addListener(popup.container, "mouseout", hideHoverMarker); popup.on("hide", hideHoverMarker); popup.on("changeSelection", hideHoverMarker); - + popup.session.doc.getLength = function() { return popup.data.length; }; @@ -202,7 +202,7 @@ var AcePopup = function(parentNode) { }; bgTokenizer.$updateOnChange = noop; bgTokenizer.start = noop; - + popup.session.$computeWidth = function() { return this.screenWidth = 0; } @@ -210,7 +210,7 @@ var AcePopup = function(parentNode) { // public popup.isOpen = false; popup.isTopdown = false; - + popup.data = []; popup.setData = function(list) { popup.data = list || []; @@ -235,7 +235,7 @@ var AcePopup = function(parentNode) { popup._signal("select"); } }; - + popup.on("changeSelection", function() { if (popup.isOpen) popup.setRow(popup.selection.lead.row); @@ -267,22 +267,22 @@ var AcePopup = function(parentNode) { el.style.display = ""; this.renderer.$textLayer.checkForSizeChanges(); - + var left = pos.left; if (left + el.offsetWidth > screenWidth) left = screenWidth - el.offsetWidth; - + el.style.left = left + "px"; - + this._signal("show"); lastMouseEvent = null; popup.isOpen = true; }; - + popup.getTextLeftOffset = function() { return this.$borderSize + this.renderer.$padding + this.$imageSize; }; - + popup.$imageSize = 0; popup.$borderSize = 1; @@ -290,19 +290,24 @@ var AcePopup = function(parentNode) { }; dom.importCssString("\ -.ace_autocomplete.ace-tm .ace_marker-layer .ace_active-line {\ +.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line {\ background-color: #CAD6FA;\ z-index: 1;\ }\ -.ace_autocomplete.ace-tm .ace_line-hover {\ +.ace_editor.ace_autocomplete .ace_line-hover {\ border: 1px solid #abbffe;\ margin-top: -1px;\ background: rgba(233,233,253,0.4);\ }\ -.ace_autocomplete .ace_line-hover {\ +.ace_editor.ace_autocomplete .ace_line-hover {\ position: absolute;\ z-index: 2;\ }\ +.ace_editor.ace_autocomplete .ace_scroller {\ + background: none;\ + border: none;\ + box-shadow: none;\ +}\ .ace_rightAlignedText {\ color: gray;\ display: inline-block;\ @@ -311,11 +316,11 @@ dom.importCssString("\ text-align: right;\ z-index: -1;\ }\ -.ace_autocomplete .ace_completion-highlight{\ +.ace_editor.ace_autocomplete .ace_completion-highlight{\ color: #000;\ text-shadow: 0 0 0.01em;\ }\ -.ace_autocomplete {\ +.ace_editor.ace_autocomplete {\ width: 280px;\ z-index: 200000;\ background: #fbfbfb;\ diff --git a/lib/ace/ext/language_tools.js b/lib/ace/ext/language_tools.js index e0ed0ec7..0f9be6b9 100644 --- a/lib/ace/ext/language_tools.js +++ b/lib/ace/ext/language_tools.js @@ -34,6 +34,7 @@ define(function(require, exports, module) { var snippetManager = require("../snippets").snippetManager; var Autocomplete = require("../autocomplete").Autocomplete; var config = require("../config"); +var util = require("../autocomplete/util"); var textCompleter = require("../autocomplete/text_completer"); var keyWordCompleter = { @@ -114,6 +115,58 @@ var loadSnippetFile = function(id) { }); }; +var doLiveAutocomplete = function(e) { + var editor = e.editor; + var session = editor.getSession(); + var pos = editor.getCursorPosition(); + var line = session.getLine(pos.row); + var hasCompleter = (editor.completer && editor.completer.activated); + + var text = e.args || ""; + + // Is the user entering text + // we only want to automatically show the autocomplete dialog + // whenever the user is typing in text not pasting, deleting, ... + var typing = (e.command.name === "insertstring" && text.length === 1); + + // We don't want to autocomplete with no prefix + if( + e.command.name === 'backspace' && + util.retrievePrecedingIdentifier(line, pos.column) === '' + ) { + if(hasCompleter) editor.completer.detach(); + return; + } + + // we don't want to autocomplete on paste events + if(!typing) { + return; + } + + // The prefix to autocomplete for + var prefix = util.retrievePrecedingIdentifier(line, pos.column); + + // Only autocomplete if there's a prefix that can be matched + if(prefix !== '' && !(hasCompleter)) { + if (!editor.completer) { + // Create new autocompleter + editor.completer = new Autocomplete(); + + // Disable autoInsert + editor.completer.autoInsert = false; + } + + editor.completer.showPopup(editor); + // needed for firefox on mac + editor.completer.cancelContextMenu(); + + } else if(prefix === '' && hasCompleter) { + // When the prefix is empty + // close the autocomplete dialog + editor.completer.detach(); + } +}; + var Editor = require("../editor").Editor; require("../config").defineOptions(Editor.prototype, "editor", { enableBasicAutocompletion: { @@ -127,6 +180,17 @@ require("../config").defineOptions(Editor.prototype, "editor", { }, value: false }, + enableLiveAutocomplete: { + set: function(val) { + if (val) { + // On each change automatically trigger the autocomplete + this.commands.on('afterExec', doLiveAutocomplete); + } else { + this.commands.removeListener('afterExec', doLiveAutocomplete); + } + }, + value: false + }, enableSnippets: { set: function(val) { if (val) {