diff --git a/ace.d.ts b/ace.d.ts index ca8d6e1e83a..33e4cfa8e8d 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -922,6 +922,72 @@ export namespace Ace { prefix: string, callback: CompleterCallback): void; } + + export class AceInline { + show(editor: Editor, completion: Completion, prefix: string): void; + isOpen(): void; + hide(): void; + destroy(): void; + } + + interface CompletionOptions { + matches?: Completion[]; + } + + type CompletionProviderOptions = { + exactMatch?: boolean; + ignoreCaption?: boolean; + } + + type CompletionRecord = { + all: Completion[]; + filtered: Completion[]; + filterText: string; + } | CompletionProviderOptions + + type GatherCompletionRecord = { + prefix: string; + matches: Completion[]; + finished: boolean; + } + + type CompletionCallbackFunction = (err: Error | undefined, data: GatherCompletionRecord) => void; + type CompletionProviderCallback = (err: Error | undefined, completions: CompletionRecord, finished: boolean) => void; + + export class CompletionProvider { + insertByIndex(editor: Editor, index: number, options: CompletionProviderOptions): boolean; + insertMatch(editor: Editor, data: Completion, options: CompletionProviderOptions): boolean; + completions: CompletionRecord; + gatherCompletions(editor: Editor, callback: CompletionCallbackFunction): boolean; + provideCompletions(editor: Editor, options: CompletionProviderOptions, callback: CompletionProviderCallback): void; + detach(): void; + } + + export class Autocomplete { + constructor(); + autoInsert?: boolean; + autoSelect?: boolean; + exactMatch?: boolean; + inlineEnabled?: boolean; + getPopup(): AcePopup; + showPopup(editor: Editor, options: CompletionOptions): void; + detach(): void; + destroy(): void; + } + + type AcePopupNavigation = "up" | "down" | "start" | "end"; + + export class AcePopup { + constructor(parentNode: HTMLElement); + setData(list: Completion[], filterText: string): void; + getData(row: number): Completion; + getRow(): number; + getRow(line: number): void; + hide(): void; + show(pos: Point, lineHeight: number, topdownOnly: boolean): void; + tryShow(pos: Point, lineHeight: number, anchor: "top" | "bottom" | undefined, forceShow?: boolean): boolean; + goTo(where: AcePopupNavigation): void; + } } @@ -944,3 +1010,43 @@ export const Range: { fromPoints(start: Ace.Point, end: Ace.Point): Ace.Range; comparePoints(p1: Ace.Point, p2: Ace.Point): number; }; + + +type InlineAutocompleteAction = "prev" | "next" | "first" | "last"; + +type TooltipCommandEnabledFunction = (editor: Ace.Editor) => boolean; + +interface TooltipCommand extends Ace.Command { + enabled: TooltipCommandEnabledFunction | boolean, + position?: number; +} + +export class InlineAutocomplete { + constructor(); + getInlineRenderer(): Ace.AceInline; + getInlineTooltip(): InlineTooltip; + getCompletionProvider(): Ace.CompletionProvider; + show(editor: Ace.Editor): void; + isOpen(): boolean; + detach(): void; + destroy(): void; + goTo(action: InlineAutocompleteAction): void; + tooltipEnabled: boolean; + commands: Record + getIndex(): number; + setIndex(value: number): void; + getLength(): number; + getData(index?: number): Ace.Completion | undefined; + updateCompletions(options: Ace.CompletionOptions): void; +} + +export class InlineTooltip { + constructor(parentElement: HTMLElement); + setCommands(commands: Record): void; + show(editor: Ace.Editor): void; + updatePosition(): void; + updateButtons(force?: boolean): void; + isShown(): boolean; + detach(): void; + destroy(): void; +} diff --git a/demo/inline_autocompletion.html b/demo/inline_autocompletion.html new file mode 100644 index 00000000000..b514f670e5f --- /dev/null +++ b/demo/inline_autocompletion.html @@ -0,0 +1,48 @@ + + + + + ACE Inline Autocompletion demo + + + + +

+
+
+
+
+
+
+
diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js
index 08c66c910bd..27272d3c4d4 100644
--- a/demo/kitchen-sink/demo.js
+++ b/demo/kitchen-sink/demo.js
@@ -312,6 +312,8 @@ doclist.pickDocument = function(name) {
 var OptionPanel = require("ace/ext/options").OptionPanel;
 var optionsPanel = new OptionPanel(env.editor);
 
+var originalAutocompleteCommand = null;
+
 optionsPanel.add({
     Main: {
         Document: {
@@ -368,6 +370,29 @@ optionsPanel.add({
             path: "showTokenInfo",
             position: 2000
         },
+        "Inline preview for autocomplete": {
+            path: "inlineEnabledForAutocomplete",
+            position: 2000,
+            onchange: function(value) {
+                var Autocomplete = require("ace/autocomplete").Autocomplete;
+                if (value && !originalAutocompleteCommand) {
+                    originalAutocompleteCommand = Autocomplete.startCommand.exec;
+                    Autocomplete.startCommand.exec = function(editor) {
+                        var autocomplete = Autocomplete.for(editor);
+                        autocomplete.inlineEnabled = true;
+                        originalAutocompleteCommand(...arguments);
+                    }
+                } else if (!value) {
+                    var autocomplete = Autocomplete.for(editor);
+                    autocomplete.destroy();
+                    Autocomplete.startCommand.exec = originalAutocompleteCommand;
+                    originalAutocompleteCommand = null;
+                }
+            },
+            getValue: function() {
+                return !!originalAutocompleteCommand;
+            }
+        },
         "Show Textarea Position": devUtil.textPositionDebugger,
         "Text Input Debugger": devUtil.textInputDebugger,
     }
@@ -468,8 +493,10 @@ optionsPanelContainer.insertBefore(
 );
 
 require("ace/ext/language_tools");
+require("ace/ext/inline_autocomplete");
 env.editor.setOptions({
     enableBasicAutocompletion: true,
+    enableInlineAutocompletion: true,
     enableSnippets: true
 });
 
diff --git a/src/autocomplete.js b/src/autocomplete.js
index 079c80c2300..91a35f159b4 100644
--- a/src/autocomplete.js
+++ b/src/autocomplete.js
@@ -2,6 +2,7 @@
 
 var HashHandler = require("./keyboard/hash_handler").HashHandler;
 var AcePopup = require("./autocomplete/popup").AcePopup;
+var AceInline = require("./autocomplete/inline").AceInline;
 var getAriaId = require("./autocomplete/popup").getAriaId;
 var util = require("./autocomplete/util");
 var lang = require("./lib/lang");
@@ -9,11 +10,22 @@ var dom = require("./lib/dom");
 var snippetManager = require("./snippets").snippetManager;
 var config = require("./config");
 
+var destroyCompleter = function(e, editor) {
+    editor.completer && editor.completer.destroy();
+};
+
+
+/**
+ * This object controls the autocompletion components and their lifecycle.
+ * There is an autocompletion popup, an optional inline ghost text renderer and a docuent tooltip popup inside.
+ * @class
+ */
+
 var Autocomplete = function() {
     this.autoInsert = false;
     this.autoSelect = true;
     this.exactMatch = false;
-    this.gatherCompletionsId = 0;
+    this.inlineEnabled = false;
     this.keyboardHandler = new HashHandler();
     this.keyboardHandler.bindKeys(this.commands);
 
@@ -38,68 +50,138 @@ var Autocomplete = function() {
             e.stop();
         }.bind(this));
         this.popup.focus = this.editor.focus.bind(this.editor);
-        this.popup.on("show", this.tooltipTimer.bind(null, null));
-        this.popup.on("select", this.tooltipTimer.bind(null, null));
+        this.popup.on("show", this.$onPopupChange.bind(this));
+        this.popup.on("hide", this.$onHidePopup.bind(this));
+        this.popup.on("select", this.$onPopupChange.bind(this));
         this.popup.on("changeHoverMarker", this.tooltipTimer.bind(null, null));
         return this.popup;
     };
 
+    this.$initInline = function() {
+        if (!this.inlineEnabled || this.inlineRenderer)
+            return;
+        this.inlineRenderer = new AceInline();
+        return this.inlineRenderer;
+    };
+
     this.getPopup = function() {
         return this.popup || this.$init();
     };
 
+    this.$onHidePopup = function() {
+        if (this.inlineRenderer) {
+            this.inlineRenderer.hide();
+        }
+        this.hideDocTooltip();
+    };
+
+    this.$onPopupChange = function(hide) {
+        if (this.inlineRenderer && this.inlineEnabled) {
+            var completion = hide ? null : this.popup.getData(this.popup.getRow());
+            var prefix = util.getCompletionPrefix(this.editor);
+            if (!this.inlineRenderer.show(this.editor, completion, prefix)) {
+                this.inlineRenderer.hide();
+            }
+            this.$updatePopupPosition();
+        }
+        this.tooltipTimer.call(null, null);
+    };
+
+    this.$updatePopupPosition = function() {
+        var editor = this.editor;
+        var renderer = editor.renderer;
+
+        var lineHeight = renderer.layerConfig.lineHeight;
+        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 - editor.renderer.scrollLeft;
+        pos.left += renderer.gutterWidth;
+
+        var posGhostText = {
+            top: pos.top,
+            left: pos.left
+        };
+
+        if (renderer.$ghostText && renderer.$ghostTextWidget) {
+            if (this.base.row === renderer.$ghostText.position.row) {
+                posGhostText.top += renderer.$ghostTextWidget.el.offsetHeight;
+            }
+        }
+
+        // Try to render below ghost text, then above ghost text, then over ghost text
+        if (this.popup.tryShow(posGhostText, lineHeight, "bottom")) {
+            return;
+        }
+
+        if (this.popup.tryShow(pos, lineHeight, "top")) {
+            return;
+        }
+        
+        this.popup.show(pos, lineHeight);
+    };
+
     this.openPopup = function(editor, prefix, keepPopupPosition) {
         if (!this.popup)
             this.$init();
 
+        if (this.inlineEnabled && !this.inlineRenderer)
+            this.$initInline();
+
         this.popup.autoSelect = this.autoSelect;
 
         this.popup.setData(this.completions.filtered, this.completions.filterText);
-        if (this.editor.textInput.setAriaOptions)
-            this.editor.textInput.setAriaOptions({activeDescendant: getAriaId(this.popup.getRow())});
+        if (this.editor.textInput.setAriaOptions) {
+            this.editor.textInput.setAriaOptions({
+                activeDescendant: getAriaId(this.popup.getRow()),
+                inline: this.inlineEnabled
+            });
+        }
 
         editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
         
-        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;
-
-            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 - editor.renderer.scrollLeft;
-            pos.left += renderer.gutterWidth;
-
-            this.popup.show(pos, lineHeight);
+            this.$updatePopupPosition();
+            if (this.tooltipNode) {
+                this.updateDocTooltip();
+            }
         } else if (keepPopupPosition && !prefix) {
             this.detach();
         }
         this.changeTimer.cancel();
     };
 
+    /**
+     * Detaches all elements from the editor, and cleans up the data for the session
+     */
     this.detach = function() {
-        this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
-        this.editor.off("changeSelection", this.changeListener);
-        this.editor.off("blur", this.blurListener);
-        this.editor.off("mousedown", this.mousedownListener);
-        this.editor.off("mousewheel", this.mousewheelListener);
+        if (this.editor) {
+            this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
+            this.editor.off("changeSelection", this.changeListener);
+            this.editor.off("blur", this.blurListener);
+            this.editor.off("mousedown", this.mousedownListener);
+            this.editor.off("mousewheel", this.mousewheelListener);
+        }
         this.changeTimer.cancel();
         this.hideDocTooltip();
 
-        this.gatherCompletionsId += 1;
+        if (this.completionProvider) {
+            this.completionProvider.detach();
+        }
+
         if (this.popup && this.popup.isOpen)
             this.popup.hide();
 
         if (this.base)
             this.base.detach();
         this.activated = false;
-        this.completions = this.base = null;
+        this.completionProvider = this.completions = this.base = null;
     };
 
     this.changeListener = function(e) {
@@ -144,34 +226,14 @@ var Autocomplete = function() {
             data = this.popup.getData(this.popup.getRow());
         if (!data)
             return false;
-
         var completions = this.completions;
-        this.editor.startOperation({command: {name: "insertMatch"}});
-        if (data.completer && data.completer.insertMatch) {
-            data.completer.insertMatch(this.editor, data);
-        } else {
-            // TODO add support for options.deleteSuffix
-            if (!completions)
-                return false;
-            if (completions.filterText) {
-                var ranges = this.editor.selection.getAllRanges();
-                for (var i = 0, range; range = ranges[i]; i++) {
-                    range.start.column -= completions.filterText.length;
-                    this.editor.session.remove(range);
-                }
-            }
-            if (data.snippet)
-                snippetManager.insertSnippet(this.editor, data.snippet);
-            else
-                this.editor.execCommand("insertstring", data.value || data);
-        }
+        var result = this.getCompletionProvider().insertMatch(this.editor, data, completions.filterText, options);
         // detach only if new popup was not opened while inserting match
         if (this.completions == completions)
             this.detach();
-        this.editor.endOperation();
+        return result;
     };
 
-
     this.commands = {
         "Up": function(editor) { editor.completer.goTo("up"); },
         "Down": function(editor) { editor.completer.goTo("down"); },
@@ -193,32 +255,11 @@ var Autocomplete = function() {
         "PageDown": function(editor) { editor.completer.popup.gotoPageDown(); }
     };
 
-    this.gatherCompletions = function(editor, callback) {
-        var session = editor.getSession();
-        var pos = editor.getCursorPosition();
-
-        var prefix = util.getCompletionPrefix(editor);
-
-        this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);
-        this.base.$insertRight = true;
-
-        var matches = [];
-        var total = editor.completers.length;
-        editor.completers.forEach(function(completer, i) {
-            completer.getCompletions(editor, session, pos, prefix, function(err, results) {
-                if (!err && results)
-                    matches = matches.concat(results);
-                // Fetch prefix again, because they may have changed by now
-                callback(null, {
-                    prefix: util.getCompletionPrefix(editor),
-                    matches: matches,
-                    finished: (--total === 0)
-                });
-            });
-        });
-        return true;
-    };
-
+    /**
+     * This is the entry point for the autocompletion class, triggers the actions which collect and display suggestions
+     * @param {Editor} editor
+     * @param {CompletionOptions} options
+     */
     this.showPopup = function(editor, options) {
         if (this.editor)
             this.detach();
@@ -240,6 +281,21 @@ var Autocomplete = function() {
         this.updateCompletions(false, options);
     };
 
+    this.getCompletionProvider = function() {
+        if (!this.completionProvider)
+            this.completionProvider = new CompletionProvider();
+        return this.completionProvider;
+    };
+
+    /**
+     * This method is deprecated, it is only kept for backwards compatibility.
+     * Use the same method include CompletionProvider instead for the same functionality.
+     * @deprecated
+     */
+    this.gatherCompletions = function(editor, callback) {
+        return this.getCompletionProvider().gatherCompletions(editor, callback);
+    };
+
     this.updateCompletions = function(keepPopupPosition, options) {
         if (keepPopupPosition && this.base && this.completions) {
             var pos = this.editor.getCursorPosition();
@@ -265,70 +321,32 @@ var Autocomplete = function() {
             return this.openPopup(this.editor, "", keepPopupPosition);
         }
 
-        // Save current gatherCompletions session, session is close when a match is insert
-        var _id = this.gatherCompletionsId;
-
-        // Only detach if result gathering is finished
-        var detachIfFinished = function(results) {
-            if (!results.finished) return;
-            return this.detach();
-        }.bind(this);
-
-        var processResults = function(results) {
-            var prefix = results.prefix;
-            var matches = results.matches;
-
-            this.completions = new FilteredList(matches);
-
-            if (this.exactMatch)
-                this.completions.exactMatch = true;
-
-            this.completions.setFilter(prefix);
-            var filtered = this.completions.filtered;
-
-            // No results
-            if (!filtered.length)
-                return detachIfFinished(results);
-
-            // One result equals to the prefix
-            if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet)
-                return detachIfFinished(results);
-
-            // Autoinsert if one result
-            if (this.autoInsert && filtered.length == 1 && results.finished)
-                return this.insertMatch(filtered[0]);
-
-            this.openPopup(this.editor, prefix, keepPopupPosition);
-        }.bind(this);
-
-        var isImmediate = true;
-        var immediateResults = null;
-        this.gatherCompletions(this.editor, function(err, results) {
-            var prefix = results.prefix;
-            var matches = results && results.matches;
-
-            if (!matches || !matches.length)
-                return detachIfFinished(results);
-
-            // Wrong prefix or wrong session -> ignore
-            if (prefix.indexOf(results.prefix) !== 0 || _id != this.gatherCompletionsId)
-                return;
-
-            // If multiple completers return their results immediately, we want to process them together
-            if (isImmediate) {
-                immediateResults = results;
-                return;
+        var session = this.editor.getSession();
+        var pos = this.editor.getCursorPosition();
+        var prefix = util.getCompletionPrefix(this.editor);
+        this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);
+        this.base.$insertRight = true;
+        var completionOptions = { exactMatch: this.exactMatch };
+        this.getCompletionProvider().provideCompletions(this.editor, completionOptions, function(err, completions, finished) {
+            var filtered = completions.filtered;
+            var prefix = util.getCompletionPrefix(this.editor);
+
+            if (finished) {
+                // No results
+                if (!filtered.length)
+                    return this.detach();
+
+                // One result equals to the prefix
+                if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet)
+                    return this.detach();
+
+                // Autoinsert if one result
+                if (this.autoInsert && filtered.length == 1)
+                    return this.insertMatch(filtered[0]);
             }
-
-            processResults(results);
+            this.completions = completions;
+            this.openPopup(this.editor, prefix, keepPopupPosition);
         }.bind(this));
-        
-        isImmediate = false;
-        if (immediateResults) {
-            var results = immediateResults;
-            immediateResults = null;
-            processResults(results);
-        }
     };
 
     this.cancelContextMenu = function() {
@@ -437,27 +455,31 @@ var Autocomplete = function() {
             if (el && el.parentNode)
                 el.parentNode.removeChild(el);
         }
-        if (this.editor && this.editor.completer == this)
-            this.editor.completer == null;
-        this.popup = null;
+        if (this.editor && this.editor.completer == this) {
+            this.editor.off("destroy", destroyCompleter);
+            this.editor.completer = null;
+        }
+        this.inlineRenderer = this.popup = this.editor = null;
     };
 
 }).call(Autocomplete.prototype);
 
 
 Autocomplete.for = function(editor) {
-    if (editor.completer) {
+    if (editor.completer instanceof Autocomplete) {
         return editor.completer;
     }
+    if (editor.completer) {
+        editor.completer.destroy();
+        editor.completer = null;
+    }
     if (config.get("sharedPopups")) {
-        if (!Autocomplete.$shared)
+        if (!Autocomplete.$sharedInstance)
             Autocomplete.$sharedInstance = new Autocomplete();
         editor.completer = Autocomplete.$sharedInstance;
     } else {
         editor.completer = new Autocomplete();
-        editor.once("destroy", function(e, editor) {
-            editor.completer.destroy();
-        });
+        editor.once("destroy", destroyCompleter);
     }
     return editor.completer;
 };
@@ -475,11 +497,143 @@ Autocomplete.startCommand = {
     bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
 };
 
+/**
+ * This class is responsible for providing completions and inserting them to the editor
+ * @class
+ */
+
+var CompletionProvider = function() {
+    this.active = true;
+};
+
+(function() {
+    this.insertByIndex = function(editor, index, options) {
+        if (!this.completions || !this.completions.filtered) {
+            return false;
+        }
+        return this.insertMatch(editor, this.completions.filtered[index], options);
+    };
+
+    this.insertMatch = function(editor, data, options) {
+        if (!data)
+            return false;
+
+        editor.startOperation({command: {name: "insertMatch"}});
+        if (data.completer && data.completer.insertMatch) {
+            data.completer.insertMatch(editor, data);
+        } else {
+            // TODO add support for options.deleteSuffix
+            if (!this.completions) {
+                return false;
+            }
+            if (this.completions.filterText) {
+                var ranges = editor.selection.getAllRanges();
+                for (var i = 0, range; range = ranges[i]; i++) {
+                    range.start.column -= this.completions.filterText.length;
+                    editor.session.remove(range);
+                }
+            }
+            if (data.snippet)
+                snippetManager.insertSnippet(editor, data.snippet);
+            else
+                editor.execCommand("insertstring", data.value || data);
+        }
+        editor.endOperation();
+        return true;
+    };
+
+    this.gatherCompletions = function(editor, callback) {
+        var session = editor.getSession();
+        var pos = editor.getCursorPosition();
+    
+        var prefix = util.getCompletionPrefix(editor);
+    
+        var matches = [];
+        var total = editor.completers.length;
+        editor.completers.forEach(function(completer, i) {
+            completer.getCompletions(editor, session, pos, prefix, function(err, results) {
+                if (!err && results)
+                    matches = matches.concat(results);
+                // Fetch prefix again, because they may have changed by now
+                callback(null, {
+                    prefix: util.getCompletionPrefix(editor),
+                    matches: matches,
+                    finished: (--total === 0)
+                });
+            });
+        });
+        return true;
+    };
+
+    /**
+     * This is the entry point to the class, it gathers, then provides the completions asynchronously via callback.
+     * The callback function may be called multiple times, the last invokation is marked with a `finished` flag
+     * @param {Editor} editor
+     * @param {CompletionProviderOptions} options
+     * @param {CompletionProviderCallback} callback
+     */
+    this.provideCompletions = function(editor, options, callback) {
+        var processResults = function(results) {
+            var prefix = results.prefix;
+            var matches = results.matches;
+
+            this.completions = new FilteredList(matches);
+
+            if (options.exactMatch)
+                this.completions.exactMatch = true;
+
+            if (options.ignoreCaption)
+                this.completions.ignoreCaption = true;
+
+            this.completions.setFilter(prefix);
+
+            callback(null, this.completions, results.finished);
+        }.bind(this);
+
+        var isImmediate = true;
+        var immediateResults = null;
+        this.gatherCompletions(editor, function(err, results) {
+            if (!this.active) {
+                return;
+            }
+            if (err) {
+                callback(err, [], true);
+                this.detach();
+            }
+            var prefix = results.prefix;
+
+            // Wrong prefix or wrong session -> ignore
+            if (prefix.indexOf(results.prefix) !== 0)
+                return;
+
+            // If multiple completers return their results immediately, we want to process them together
+            if (isImmediate) {
+                immediateResults = results;
+                return;
+            }
+
+            processResults(results);
+        }.bind(this));
+        
+        isImmediate = false;
+        if (immediateResults) {
+            var results = immediateResults;
+            immediateResults = null;
+            processResults(results);
+        }
+    };
+
+    this.detach = function() {
+        this.active = false;
+    };
+}).call(CompletionProvider.prototype);
+
 var FilteredList = function(array, filterText) {
     this.all = array;
     this.filtered = array;
     this.filterText = filterText || "";
     this.exactMatch = false;
+    this.ignoreCaption = false;
 };
 (function(){
     this.setFilter = function(str) {
@@ -506,12 +660,13 @@ var FilteredList = function(array, filterText) {
 
         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.caption || item.value || item.snippet;
+            var caption = (!this.ignoreCaption && item.caption) || item.value || item.snippet;
             if (!caption) continue;
             var lastIndex = -1;
             var matchMask = 0;
@@ -560,4 +715,5 @@ var FilteredList = function(array, filterText) {
 }).call(FilteredList.prototype);
 
 exports.Autocomplete = Autocomplete;
+exports.CompletionProvider = CompletionProvider;
 exports.FilteredList = FilteredList;
diff --git a/src/autocomplete/inline.js b/src/autocomplete/inline.js
new file mode 100644
index 00000000000..e161b4df5e1
--- /dev/null
+++ b/src/autocomplete/inline.js
@@ -0,0 +1,73 @@
+"use strict";
+
+var snippetManager = require("../snippets").snippetManager;
+
+/**
+ * This object is used to manage inline code completions rendered into an editor with ghost text.
+ * @class
+ */
+
+/**
+ * Creates the inline completion renderer which renders the inline code completions directly in the target editor.
+ * @constructor
+ */
+
+var AceInline = function() {
+    this.editor = null;
+};
+
+(function() {
+
+    /**
+     * Renders the completion as ghost text to the current cursor position
+     * @param {Editor} editor
+     * @param {Completion} completion
+     * @param {string} prefix
+     * @returns {boolean} True if the completion could be rendered to the editor, false otherwise
+     */
+    this.show = function(editor, completion, prefix) {
+        prefix = prefix || "";
+        if (editor && this.editor && this.editor !== editor) {
+            this.hide();
+            this.editor = null;
+        }
+        if (!editor || !completion) {
+            return false;
+        }
+        var displayText = completion.snippet ? snippetManager.getDisplayTextForSnippet(editor, completion.snippet) : completion.value;
+        if (!displayText || !displayText.startsWith(prefix)) {
+            return false;
+        }
+        this.editor = editor;
+        displayText = displayText.slice(prefix.length);
+        if (displayText === "") {
+            editor.removeGhostText();
+        } else {
+            editor.setGhostText(displayText);
+        }
+        return true;
+    };
+
+    this.isOpen = function() {
+        if (!this.editor) {
+            return false;
+        }
+        return !!this.editor.renderer.$ghostText;
+    };
+
+    this.hide = function() {
+        if (!this.editor) {
+            return false;
+        }
+        this.editor.removeGhostText();
+        return true;
+    };
+
+    this.destroy = function() {
+        this.hide();
+        this.editor = null;
+    };
+}).call(AceInline.prototype);
+
+
+exports.AceInline = AceInline;
diff --git a/src/autocomplete/inline_test.js b/src/autocomplete/inline_test.js
new file mode 100644
index 00000000000..18b9e6d7e00
--- /dev/null
+++ b/src/autocomplete/inline_test.js
@@ -0,0 +1,236 @@
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("../test/mockdom");
+}
+
+"use strict";
+
+var assert = require("../test/assertions");
+var AceInline = require("./inline").AceInline;
+var Editor = require("../ace").Editor;
+var EditSession = require("../ace").EditSession;
+var VirtualRenderer = require("../ace").VirtualRenderer;
+
+var editor;
+var editor2;
+var inline;
+
+var textBase = "abc123\n\n    ";
+
+var completions = [
+    {
+        value: "foo",
+        score: 4
+    },
+    {
+        value: "function",
+        score: 3
+    },
+    {
+        value: "foobar",
+        score: 2
+    },
+    {
+        snippet: "function foo() {\n    console.log('test');\n}",
+        score: 1
+    },
+    {
+        snippet: "foobar2",
+        score: 0
+    }
+];
+
+var getAllLines = function(editorOverride) {
+    editorOverride = editorOverride || editor;
+    return editorOverride.renderer.$textLayer.element.childNodes.map(function (node) {
+        return node.textContent;
+    }).join("\n");
+};
+
+var createEditor = function(element) {
+    var renderer = new VirtualRenderer(element);
+    var session = new EditSession("");
+    return new Editor(renderer, session);
+};
+
+module.exports = {
+    setUp: function(done) {
+        var el = document.createElement("div");
+        el.style.left = "20px";
+        el.style.top = "30px";
+        el.style.width = "500px";
+        el.style.height = "500px";
+        document.body.appendChild(el);
+        editor = createEditor(el);
+        editor.execCommand("insertstring", textBase + "f");
+        inline = new AceInline();
+        editor.getSelection().moveCursorFileEnd();
+        editor.renderer.$loop._flush();
+        done();
+    },
+    "test: displays the ghost text in the editor on show": function(done) {
+        inline.show(editor, completions[0], "f");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "foo");
+        done();
+    },
+    "test: replaces the ghost text in the editor with the latest show": function(done) {
+        inline.show(editor, completions[0], "f");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "foo");
+        inline.show(editor, completions[1], "f");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "function");
+        done();
+    },
+    "test: renders multi-line ghost text indentation": function(done) {
+        assert.equal(editor.renderer.$ghostTextWidget, null);
+        inline.show(editor, completions[3], "f");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "function foo() {");
+        assert.strictEqual(editor.renderer.$ghostTextWidget.text, "        console.log('test');\n    }");
+        assert.strictEqual(editor.renderer.$ghostTextWidget.el.textContent, "        console.log('test');\n    }");
+        done();
+    },
+    "test: boundary tests": function(done) {
+        var noRenderTestCases = [
+            [null, null, null],
+            [editor, null, null],
+            [editor, null, ""],
+            [null, completions[3], ""]
+        ];
+        var result;
+        noRenderTestCases.forEach(function(params) {
+            result = inline.show(params[0], params[1], params[2]);
+            editor.renderer.$loop._flush();
+            assert.notOk(result);
+            assert.equal(editor.renderer.$ghostText, null);
+            assert.equal(editor.renderer.$ghostTextWidget, null);
+        });
+
+        var renderTestCases = [
+            [editor, completions[1], undefined],
+            [editor, completions[1], null],
+            [editor, completions[1], ""]
+        ];
+        renderTestCases.forEach(function(params) {
+            result = inline.show(params[0], params[1], params[2]);
+            editor.renderer.$loop._flush();
+            assert.ok(result);
+            assert.strictEqual(editor.renderer.$ghostText.text, "function");
+            assert.strictEqual(getAllLines(), textBase + "ffunction");
+            assert.equal(editor.renderer.$ghostTextWidget, null);
+        });
+
+        result = inline.show(editor, completions[0], "foo");
+        editor.renderer.$loop._flush();
+        assert.ok(result);
+        assert.equal(editor.renderer.$ghostText, null);
+        assert.equal(editor.renderer.$ghostTextWidget, null);
+        
+        done();
+    },
+    "test: only renders the ghost text without the prefix": function(done) {
+        inline.show(editor, completions[1], "fun");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "fction");
+        done();
+    },
+    "test: verify explicit and implicit hide": function(done) {
+        inline.show(editor, completions[1], "f");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "function");
+        assert.strictEqual(inline.isOpen(), true);
+        inline.hide();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "f");
+        assert.strictEqual(inline.isOpen(), false);
+
+        inline.show(editor, completions[1], "function");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "f");
+        assert.strictEqual(inline.isOpen(), false);
+        done();
+    },
+    "test: does not hide previous ghost text if cannot show current one": function(done) {
+        inline.show(editor, completions[1], "f");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "function");
+        assert.strictEqual(inline.isOpen(), true);
+        inline.show(editor, null, "");
+        editor.renderer.$loop._flush();
+        assert.equal(getAllLines(), textBase + "function");
+        assert.strictEqual(inline.isOpen(), true);
+        inline.hide();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "f");
+        assert.strictEqual(inline.isOpen(), false);
+        done();
+    },
+    "test: removes ghost text from previous editor if new valid editor is passed to show function": function(done) {
+        var el = document.createElement("div");
+        el.style.left = "520px";
+        el.style.top = "530px";
+        el.style.width = "500px";
+        el.style.height = "500px";
+        document.body.appendChild(el);
+        editor2 = createEditor(el);
+        var editor2Text = "different text\n\n    f";
+        editor2.execCommand("insertstring", editor2Text);
+
+        inline.show(editor, completions[1], "f");
+        editor.renderer.$loop._flush();
+        editor2.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "function");
+        assert.strictEqual(getAllLines(editor2), editor2Text);
+        assert.strictEqual(inline.isOpen(), true);
+
+        inline.show(editor2, completions[2], "f");
+        editor.renderer.$loop._flush();
+        editor2.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "f");
+        assert.strictEqual(getAllLines(editor2), editor2Text + "oobar");
+        assert.strictEqual(inline.isOpen(), true);
+
+        inline.show(null, completions[2], "f");
+        editor.renderer.$loop._flush();
+        editor2.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "f");
+        assert.strictEqual(getAllLines(editor2), editor2Text + "oobar");
+        assert.strictEqual(inline.isOpen(), true);
+
+        done();
+    },
+    "test: verify destroy": function(done) {
+        inline.show(editor, completions[0], "f");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), textBase + "foo");
+
+        inline.destroy();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(inline.isOpen(), false);
+        assert.strictEqual(getAllLines(), textBase + "f");
+
+        inline.destroy();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(inline.isOpen(), false);
+        assert.strictEqual(getAllLines(), textBase + "f");
+
+        inline.hide();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(inline.isOpen(), false);
+        assert.strictEqual(getAllLines(), textBase + "f");
+        done();
+    },
+    tearDown: function() {
+        inline.destroy();
+        editor.destroy();
+        if (editor2) {
+            editor2.destroy();
+        }
+    }
+};
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}
diff --git a/src/autocomplete/popup.js b/src/autocomplete/popup.js
index 920f4178de0..0f163aa874a 100644
--- a/src/autocomplete/popup.js
+++ b/src/autocomplete/popup.js
@@ -256,27 +256,82 @@ var AcePopup = function(parentNode) {
 
     popup.hide = function() {
         this.container.style.display = "none";
-        this._signal("hide");
-        popup.isOpen = false;
+        popup.anchorPos = null;
+        popup.anchor = null;
+        if (popup.isOpen) {
+            popup.isOpen = false;
+            this._signal("hide");
+        }
     };
-    popup.show = function(pos, lineHeight, topdownOnly) {
+
+    /**
+     * Tries to show the popup anchored to the given position and anchors.
+     * If the anchor is not specified it tries to align to bottom and right as much as possible.
+     * If the popup does not have enough space to be rendered with the given anchors, it returns false without rendering the popup.
+     * The forceShow flag can be used to render the popup in these cases, which slides the popup so it entirely fits on the screen.
+     * @param {Point} pos
+     * @param {number} lineHeight
+     * @param {"top" | "bottom" | undefined} anchor
+     * @param {boolean} forceShow
+     * @returns {boolean}
+     */
+    popup.tryShow = function(pos, lineHeight, anchor, forceShow) {
+        if (!forceShow && popup.isOpen && popup.anchorPos && popup.anchor &&
+            popup.anchorPos.top === pos.top && popup.anchorPos.left === pos.left &&
+            popup.anchor === anchor
+        ) {
+            return true;
+        }
+
         var el = this.container;
         var screenHeight = window.innerHeight;
         var screenWidth = window.innerWidth;
         var renderer = this.renderer;
         // var maxLines = Math.min(renderer.$maxLines, this.session.getLength());
         var maxH = renderer.$maxLines * lineHeight * 1.4;
-        var top = pos.top + this.$borderSize;
-        var allowTopdown = top > screenHeight / 2 && !topdownOnly;
-        if (allowTopdown && top + lineHeight + maxH > screenHeight) {
-            renderer.$maxPixelHeight = top - 2 * this.$borderSize;
+        var dims = { top: 0, bottom: 0, left: 0 };
+
+        var spaceBelow = screenHeight - pos.top - 3 * this.$borderSize - lineHeight;
+        var spaceAbove = pos.top - 3 * this.$borderSize;
+        if (!anchor) {
+            if (spaceAbove <= spaceBelow || spaceBelow >= maxH) {
+                anchor = "bottom";
+            } else {
+                anchor = "top";
+            }
+        }
+
+        if (anchor === "top") {
+            dims.bottom = pos.top - this.$borderSize;
+            dims.top = dims.bottom - maxH;
+        } else if (anchor === "bottom") {
+            dims.top = pos.top + lineHeight + this.$borderSize;
+            dims.bottom = dims.top + maxH;
+        }
+
+        var fitsX = dims.top >= 0 && dims.bottom <= screenHeight;
+
+        if (!forceShow && !fitsX) {
+            return false;
+        }
+
+        if (!fitsX) {
+            if (anchor === "top") {
+                renderer.$maxPixelHeight = spaceAbove;
+            } else {
+                renderer.$maxPixelHeight = spaceBelow;
+            }
+        } else {
+            renderer.$maxPixelHeight = null;
+        }
+
+
+        if (anchor === "top") {
             el.style.top = "";
-            el.style.bottom = screenHeight - top + "px";
+            el.style.bottom = (screenHeight - dims.bottom) + "px";
             popup.isTopdown = false;
         } else {
-            top += lineHeight;
-            renderer.$maxPixelHeight = screenHeight - top - 0.2 * lineHeight;
-            el.style.top = top + "px";
+            el.style.top = dims.top + "px";
             el.style.bottom = "";
             popup.isTopdown = true;
         }
@@ -288,10 +343,22 @@ var AcePopup = function(parentNode) {
             left = screenWidth - el.offsetWidth;
 
         el.style.left = left + "px";
+        el.style.right = "";
 
-        this._signal("show");
-        lastMouseEvent = null;
-        popup.isOpen = true;
+        if (!popup.isOpen) {
+            popup.isOpen = true;
+            this._signal("show");
+            lastMouseEvent = null;
+        }
+
+        popup.anchorPos = pos;
+        popup.anchor = anchor;
+
+        return true;
+    };
+
+    popup.show = function(pos, lineHeight, topdownOnly) {
+        this.tryShow(pos, lineHeight, topdownOnly ? "bottom" : undefined, true);
     };
 
     popup.goTo = function(where) {
diff --git a/src/autocomplete/popup_test.js b/src/autocomplete/popup_test.js
new file mode 100644
index 00000000000..8c4ee962ee4
--- /dev/null
+++ b/src/autocomplete/popup_test.js
@@ -0,0 +1,217 @@
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("../test/mockdom");
+}
+
+"use strict";
+
+var assert = require("../test/assertions");
+var AcePopup = require("./popup").AcePopup;
+
+var popup;
+var lineHeight = 14;
+var renderHeight = 8 * lineHeight;
+var renderWidth = 300;
+var iframe;
+
+var notEnoughSpaceOnRight = window.innerWidth - 50;
+var notEnoughSpaceOnBottom = window.innerHeight - 50;
+
+var originalDocument;
+
+var completions = [];
+
+for (var i = 0; i < 8; i++) {
+    completions.push({ value: "foo" + i, caption: "foo" + i, name: "foo" + i, score: 4 });
+}
+
+var tryShowAndRender = function(pos, lineHeight, anchor, forceShow) {
+    var result = popup.tryShow(pos, lineHeight, anchor, forceShow);
+    popup.renderer.updateFull(true);
+    return result;
+};
+
+
+var setupPopup = function() {
+    popup = new AcePopup(document.body);
+
+    // Mock the CSS behaviour
+    popup.container.style.width = "300px";
+    popup.setData(completions, "");
+};
+
+var tearDown = function(done) {
+    if (popup) {
+        var el = popup.container;
+        if (el && el.parentNode)
+            el.parentNode.removeChild(el);
+    }
+    if (iframe)
+        document.body.removeChild(iframe);
+    if (originalDocument) {
+        window.document = originalDocument;
+        originalDocument = null;
+    }
+    done && done();
+};
+
+module.exports = {
+    "test: verify width and height": function(done) {
+        setupPopup();
+        tryShowAndRender({ top: 0, left: 0 }, lineHeight, "bottom");
+        renderHeight = popup.container.offsetHeight;
+        assert.strictEqual(popup.isOpen, true);
+        assert.strictEqual(renderHeight > 0, true);
+        popup.hide();
+        assert.strictEqual(popup.isOpen, false);
+        done();
+    },
+    "test: tryShow does not display popup if there is not enough space on the anchor side": function(done) {
+        setupPopup();
+        var result = tryShowAndRender({ top: notEnoughSpaceOnBottom, left: 0}, lineHeight, "bottom");
+        assert.strictEqual(result, false);
+        assert.strictEqual(popup.isOpen, false);
+        result = tryShowAndRender({ top: 50, left: 0}, lineHeight, "top");
+        assert.strictEqual(result, false);
+        assert.strictEqual(popup.isOpen, false);
+        done();
+    },
+    "test: tryShow slides popup on the X axis if there are not enough space on the right": function(done) {
+        setupPopup();
+        
+        var result = tryShowAndRender({ top: 0, left: notEnoughSpaceOnRight }, lineHeight, "bottom");
+        assert.strictEqual(result, true);
+        assert.strictEqual(popup.isOpen, true);
+        assert.strictEqual(popup.container.getBoundingClientRect().right, window.innerWidth);
+        assert.strictEqual(Math.abs(popup.container.getBoundingClientRect().width - renderWidth) < 5, true);
+        popup.hide();
+        assert.strictEqual(popup.isOpen, false);
+
+        result = tryShowAndRender({ top: notEnoughSpaceOnBottom, left: notEnoughSpaceOnRight }, lineHeight, "top");
+        assert.strictEqual(result, true);
+        assert.strictEqual(popup.isOpen, true);
+        assert.strictEqual(popup.container.getBoundingClientRect().right, window.innerWidth);
+        assert.strictEqual(Math.abs(popup.container.getBoundingClientRect().width - renderWidth) < 5, true);
+        popup.hide();
+        assert.strictEqual(popup.isOpen, false);
+        done();
+    },
+    "test: tryShow called with forceShow resizes popup height to fit popup": function(done) {
+        setupPopup();
+        
+        var result = tryShowAndRender({ top: notEnoughSpaceOnBottom, left: 0 }, lineHeight, "bottom", true);
+        assert.strictEqual(result, true);
+        assert.strictEqual(popup.isOpen, true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height <= 50, true);
+        assert.strictEqual(popup.container.getBoundingClientRect().top > notEnoughSpaceOnBottom + lineHeight, true);
+        assert.strictEqual(Math.abs(popup.container.getBoundingClientRect().width - renderWidth) < 5, true);
+        popup.hide();
+        assert.strictEqual(popup.isOpen, false);
+
+        result = tryShowAndRender({ top: 50, left: 0 }, lineHeight, "top", true);
+        assert.strictEqual(result, true);
+        assert.strictEqual(popup.isOpen, true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height <= 50, true);
+        assert.strictEqual(popup.container.getBoundingClientRect().bottom <= 50, true);
+        assert.strictEqual(Math.abs(popup.container.getBoundingClientRect().width - renderWidth) < 5, true);
+        popup.hide();
+        assert.strictEqual(popup.isOpen, false);
+        done();
+    },
+    "test: show displays popup in all 4 corners correctly without topdownOnly specified": function(done) {
+        setupPopup();
+        popup.show({ top: 50, left: 0 }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().top >= 50 + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: notEnoughSpaceOnBottom, left: 0 }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().bottom <= notEnoughSpaceOnBottom);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: 50, left: notEnoughSpaceOnRight }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().top >= 50 + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: notEnoughSpaceOnBottom, left: notEnoughSpaceOnRight }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().bottom <= notEnoughSpaceOnBottom);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+        done();
+    },
+    "test: show displays popup in all 4 corners correctly with topdownOnly specified": function(done) {
+        setupPopup();
+        popup.show({ top: 50, left: 0 }, lineHeight, true);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().top >= 50 + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: 50, left: notEnoughSpaceOnRight }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.strictEqual(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().top >= 50 + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: notEnoughSpaceOnBottom, left: 0 }, lineHeight, true);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.ok(popup.container.getBoundingClientRect().height <= 50);
+        assert.ok(popup.container.getBoundingClientRect().top >= notEnoughSpaceOnBottom + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: notEnoughSpaceOnBottom, left: notEnoughSpaceOnRight }, lineHeight, true);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.ok(popup.container.getBoundingClientRect().height <= 50);
+        assert.ok(popup.container.getBoundingClientRect().top >= notEnoughSpaceOnBottom + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+        done();
+    },
+    "test: resets popup size if space is available again": function(done) {
+        setupPopup();
+        popup.show({ top: notEnoughSpaceOnBottom, left: notEnoughSpaceOnRight }, lineHeight, true);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.ok(popup.container.getBoundingClientRect().height <= 50);
+        assert.ok(popup.container.getBoundingClientRect().top >= notEnoughSpaceOnBottom + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+
+        popup.show({ top: 50, left: notEnoughSpaceOnRight }, lineHeight);
+        popup.renderer.updateFull(true);
+        assert.notEqual(popup.container.style.display, "none");
+        assert.ok(popup.container.getBoundingClientRect().height, renderHeight);
+        assert.ok(popup.container.getBoundingClientRect().top >= 50 + lineHeight);
+        popup.hide();
+        assert.strictEqual(popup.container.style.display, "none");
+        done();
+    },
+    tearDown: tearDown
+};
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}
diff --git a/src/css/editor.css.js b/src/css/editor.css.js
index eaad5d28cb7..3d5cc42d31e 100644
--- a/src/css/editor.css.js
+++ b/src/css/editor.css.js
@@ -589,4 +589,5 @@ module.exports = `
 .ace_ghost_text {
     opacity: 0.5;
     font-style: italic;
+    white-space: pre;
 }`;
diff --git a/src/ext/inline_autocomplete.js b/src/ext/inline_autocomplete.js
new file mode 100644
index 00000000000..5c855da7cb3
--- /dev/null
+++ b/src/ext/inline_autocomplete.js
@@ -0,0 +1,610 @@
+"use strict";
+
+var HashHandler = require("../keyboard/hash_handler").HashHandler;
+var AceInline = require("../autocomplete/inline").AceInline;
+var FilteredList = require("../autocomplete").FilteredList;
+var CompletionProvider = require("../autocomplete").CompletionProvider;
+var Editor = require("../editor").Editor;
+var util = require("../autocomplete/util");
+var lang = require("../lib/lang");
+var dom = require("../lib/dom");
+var useragent = require("../lib/useragent");
+var snippetCompleter = require("./language_tools").snippetCompleter;
+var textCompleter = require("./language_tools").textCompleter;
+var keyWordCompleter = require("./language_tools").keyWordCompleter;
+
+var destroyCompleter = function(e, editor) {
+    editor.completer && editor.completer.destroy();
+};
+
+var minPosition = function (posA, posB) {
+    if (posB.row > posA.row) {
+        return posA;
+    } else if (posB.row === posA.row && posB.column > posA.column) {
+        return posA;
+    }
+    return posB;
+};
+
+
+/**
+ * This class controls the inline-only autocompletion components and their lifecycle.
+ * This is more lightweight than the popup-based autocompletion, as it can only work with exact prefix matches.
+ * There is an inline ghost text renderer and an optional command bar tooltip inside.
+ * @class
+ */
+
+var InlineAutocomplete = function(editor) {
+    this.editor = editor;
+    this.tooltipEnabled = true;
+    this.keyboardHandler = new HashHandler(this.commands);
+    this.$index = -1;
+
+    this.blurListener = this.blurListener.bind(this);
+    this.changeListener = this.changeListener.bind(this);
+    this.mousewheelListener = this.mousewheelListener.bind(this);
+
+    this.changeTimer = lang.delayedCall(function() {
+        this.updateCompletions();
+    }.bind(this));
+};
+
+(function() {
+    this.getInlineRenderer = function() {
+        if (!this.inlineRenderer)
+            this.inlineRenderer = new AceInline();
+        return this.inlineRenderer;
+    };
+
+    this.getInlineTooltip = function() {
+        if (!this.inlineTooltip) {
+            this.inlineTooltip = new InlineTooltip(this.editor, document.body || document.documentElement);
+            this.inlineTooltip.setCommands(this.commands);
+        }
+        return this.inlineTooltip;
+    };
+
+
+    /**
+     * This function is the entry point to the class. This triggers the gathering of the autocompletion and displaying the results;
+     * @param {Editor} editor
+     * @param {CompletionOptions} options
+     */
+    this.show = function(options) {
+        this.activated = true;
+
+        if (this.editor.completer !== this) {
+            if (this.editor.completer)
+                this.editor.completer.detach();
+            this.editor.completer = this;
+        }
+
+        this.editor.on("changeSelection", this.changeListener);
+        this.editor.on("blur", this.blurListener);
+        this.editor.on("mousewheel", this.mousewheelListener);
+
+        this.updateCompletions(options);
+    };
+
+    this.$open = function() {
+        if (this.editor.textInput.setAriaOptions) {
+            this.editor.textInput.setAriaOptions({});
+        }
+
+        if (this.tooltipEnabled) {
+            this.getInlineTooltip().show(this.editor);
+        } else if (this.tooltipEnabled === "hover") {
+        }
+
+        this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
+
+        if (this.$index === -1) {
+            this.setIndex(0);
+        } else {
+            this.$showCompletion();
+        }
+        
+        this.changeTimer.cancel();
+    };
+    
+    this.insertMatch = function() {
+        var result = this.getCompletionProvider().insertByIndex(this.editor, this.$index);
+        this.detach();
+        return result;
+    };
+
+    this.commands = {
+        "Previous": {
+            bindKey: "Alt-[",
+            name: "Previous",
+            exec: function(editor) {
+                editor.completer.goTo("prev");
+            },
+            enabled: function(editor) {
+                return editor.completer.getIndex() > 0;
+            },
+            position: 10
+        },
+        "Next": {
+            bindKey: "Alt-]",
+            name: "Next",
+            exec: function(editor) {
+                editor.completer.goTo("next");
+            },
+            enabled: function(editor) {
+                return editor.completer.getIndex() < editor.completer.getLength() - 1;
+            },
+            position: 20
+        },
+        "Accept": {
+            bindKey: { win: "Tab|Ctrl-Right", mac: "Tab|Cmd-Right" },
+            name: "Accept",
+            exec: function(editor) {
+                return editor.completer.insertMatch();
+            },
+            enabled: function(editor) {
+                return editor.completer.getIndex() >= 0;
+            },
+            position: 30
+        },
+        "Close": {
+            bindKey: "Esc",
+            name: "Close",
+            exec: function(editor) {
+                editor.completer.detach();
+            },
+            enabled: true,
+            position: 40
+        }
+    };
+
+    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
+            this.detach();
+    };
+
+    this.blurListener = function(e) {
+        this.detach();
+    };
+
+    this.mousewheelListener = function(e) {
+        if (this.inlineTooltip && this.inlineTooltip.isShown()) {
+            this.inlineTooltip.updatePosition();
+        }
+    };
+
+    this.goTo = function(where) {
+        if (!this.completions || !this.completions.filtered) {
+            return;
+        }
+        switch(where.toLowerCase()) {
+            case "prev":
+                this.setIndex(Math.max(0, this.$index - 1));
+                break;
+            case "next":
+                this.setIndex(this.$index + 1);
+                break;
+            case "first":
+                this.setIndex(0);
+                break;
+            case "last":
+                this.setIndex(this.completions.filtered.length - 1);
+                break;
+        }
+    };
+
+    this.getLength = function() {
+        if (!this.completions || !this.completions.filtered) {
+            return 0;
+        }
+        return this.completions.filtered.length;
+    };
+
+    this.getData = function(index) {
+        if (index == undefined || index === null) {
+            return this.completions.filtered[this.$index];
+        } else {
+            return this.completions.filtered[index];
+        }
+    };
+
+    this.getIndex = function() {
+        return this.$index;
+    };
+
+    this.isOpen = function() {
+        return this.$index >= 0;
+    };
+
+    this.setIndex = function(value) {
+        if (!this.completions || !this.completions.filtered) {
+            return;
+        }
+        var newIndex = Math.max(-1, Math.min(this.completions.filtered.length - 1, value));
+        if (newIndex !== this.$index) {
+            this.$index = newIndex;
+            this.$showCompletion();
+        }
+    };
+
+    this.getCompletionProvider = function() {
+        if (!this.completionProvider)
+            this.completionProvider = new CompletionProvider();
+        return this.completionProvider;
+    };
+
+    this.$showCompletion = function() {
+        if (!this.getInlineRenderer().show(this.editor, this.completions.filtered[this.$index], this.completions.filterText)) {
+            // Not able to show the completion, hide the previous one
+            this.getInlineRenderer().hide();
+        }
+        if (this.inlineTooltip && this.inlineTooltip.isShown()) {
+            this.inlineTooltip.updateButtons();
+        }
+    };
+
+    this.$updatePrefix = function() {
+        var pos = this.editor.getCursorPosition();
+        var prefix = this.editor.session.getTextRange({start: this.base, end: pos});
+        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.$open(this.editor, prefix);
+        return prefix;
+    };
+
+    this.updateCompletions = function(options) {
+        var prefix = "";
+        
+        if (options && options.matches) {
+            var pos = this.editor.getSelectionRange().start;
+            this.base = this.editor.session.doc.createAnchor(pos.row, pos.column);
+            this.base.$insertRight = true;
+            this.completions = new FilteredList(options.matches);
+            return this.$open(this.editor, "");
+        }
+
+        if (this.base && this.completions) {
+            prefix = this.$updatePrefix();
+        }
+
+        var session = this.editor.getSession();
+        var pos = this.editor.getCursorPosition();
+        var prefix = util.getCompletionPrefix(this.editor);
+        this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);
+        this.base.$insertRight = true;
+        var options = {
+            exactMatch: true,
+            ignoreCaption: true
+        };
+        this.getCompletionProvider().provideCompletions(this.editor, options, function(err, completions, finished) {
+            var filtered = completions.filtered;
+            var prefix = util.getCompletionPrefix(this.editor);
+
+            if (finished) {
+                // No results
+                if (!filtered.length)
+                    return this.detach();
+
+                // One result equals to the prefix
+                if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet)
+                    return this.detach();
+            }
+            this.completions = completions;
+            this.$open(this.editor, prefix);
+        }.bind(this));
+    };
+
+    this.detach = function() {
+        if (this.editor) {
+            this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
+            this.editor.off("changeSelection", this.changeListener);
+            this.editor.off("blur", this.blurListener);
+            this.editor.off("mousewheel", this.mousewheelListener);
+        }
+        this.changeTimer.cancel();
+        if (this.inlineTooltip) {
+            this.inlineTooltip.detach();
+        }
+        
+        this.setIndex(-1);
+
+        if (this.completionProvider) {
+            this.completionProvider.detach();
+        }
+
+        if (this.inlineRenderer && this.inlineRenderer.isOpen()) {
+            this.inlineRenderer.hide();
+        }
+
+        if (this.base)
+            this.base.detach();
+        this.activated = false;
+        this.completionProvider = this.completions = this.base = null;
+    };
+
+    this.destroy = function() {
+        this.detach();
+        if (this.inlineRenderer)
+            this.inlineRenderer.destroy();
+        if (this.inlineTooltip)
+            this.inlineTooltip.destroy();
+        if (this.editor && this.editor.completer == this) {
+            this.editor.off("destroy", destroyCompleter);
+            this.editor.completer = null;
+        }
+        this.inlineTooltip = this.editor = this.inlineRenderer = null;
+    };
+
+}).call(InlineAutocomplete.prototype);
+
+InlineAutocomplete.for = function(editor) {
+    if (editor.completer instanceof InlineAutocomplete) {
+        return editor.completer;
+    }
+    if (editor.completer) {
+        editor.completer.destroy();
+        editor.completer = null;
+    }
+
+    editor.completer = new InlineAutocomplete(editor);
+    editor.once("destroy", destroyCompleter);
+    return editor.completer;
+};
+
+InlineAutocomplete.startCommand = {
+    name: "startInlineAutocomplete",
+    exec: function(editor, options) {
+        var completer = InlineAutocomplete.for(editor);
+        completer.show(options);
+    },
+    bindKey: { win: "Alt-C", mac: "Option-C" }
+};
+
+
+var completers = [snippetCompleter, textCompleter, keyWordCompleter];
+
+require("../config").defineOptions(Editor.prototype, "editor", {
+    enableInlineAutocompletion: {
+        set: function(val) {
+            if (val) {
+                if (!this.completers)
+                    this.completers = Array.isArray(val)? val : completers;
+                this.commands.addCommand(InlineAutocomplete.startCommand);
+            } else {
+                this.commands.removeCommand(InlineAutocomplete.startCommand);
+            }
+        },
+        value: false
+    }
+});
+
+/**
+ * Displays a command tooltip above the selection, with clickable elements.
+ * @class
+ */
+
+/**
+ * Creates the inline command tooltip helper which displays the available keyboard commands for the user.
+ * @param {HTMLElement} parentElement
+ * @constructor
+ */
+
+var ENTRY_CLASS_NAME = 'inline_autocomplete_tooltip_entry';
+var BUTTON_CLASS_NAME = 'inline_autocomplete_tooltip_button';
+var TOOLTIP_CLASS_NAME = 'ace_tooltip ace_inline_autocomplete_tooltip';
+var TOOLTIP_ID = 'inline_autocomplete_tooltip';
+
+function InlineTooltip(editor, parentElement) {
+    this.editor = editor;
+    this.htmlElement = document.createElement('div');
+    var el = this.htmlElement;
+    el.style.display = 'none';
+    if (parentElement) {
+        parentElement.appendChild(el);
+    }
+    el.id = TOOLTIP_ID;
+    el.style['pointer-events'] = 'auto';
+    el.className = TOOLTIP_CLASS_NAME;
+    this.commands = {};
+    this.buttons = {};
+    this.eventListeners = {};
+}
+
+(function() {
+
+    var captureMousedown = function(e) {
+        e.preventDefault();
+    };
+
+    /**
+     * This function sets the commands. Note that it is advised to call this before calling show, otherwise there are no buttons to render
+     * @param {Record} commands
+     */
+    this.setCommands = function(commands) {
+        if (!commands || !this.htmlElement) {
+            return;
+        }
+        this.detach();
+        var el = this.htmlElement;
+        while (el.hasChildNodes()) {
+            el.removeChild(el.firstChild);
+        }
+
+        this.commands = commands;
+        this.buttons = {};
+        this.eventListeners = {};
+    
+        Object.keys(commands)
+            .map(function(key) { return [key, commands[key]]; })
+            .filter(function (entry) { return entry[1].position > 0; })
+            .sort(function (a, b) { return a[1].position - b[1].position; })
+            .forEach(function (entry) {
+                var key = entry[0];
+                var command = entry[1];
+                dom.buildDom(["div", { class: ENTRY_CLASS_NAME }, [['div', { class: BUTTON_CLASS_NAME, ref: key }, this.buttons]]], el, this.buttons);
+                var bindKey = command.bindKey;
+                if (typeof bindKey === 'object') {
+                    bindKey = useragent.isMac ? bindKey.mac : bindKey.win;
+                }
+                bindKey = bindKey.replace("|", " / ");
+                var buttonText = dom.createTextNode([command.name, "(", bindKey, ")"].join(" "));
+                this.buttons[key].appendChild(buttonText);
+            }.bind(this));
+    };
+
+    /**
+     * Displays the clickable command bar tooltip
+     * @param {Editor} editor
+     */
+    this.show = function() {
+        this.detach();
+
+        this.htmlElement.style.display = '';
+        this.htmlElement.addEventListener('mousedown', captureMousedown.bind(this));
+
+        this.updatePosition();
+        this.updateButtons(true);
+    };
+
+    this.isShown = function() {
+        return !!this.htmlElement && window.getComputedStyle(this.htmlElement).display !== "none";
+    };
+
+    /**
+     * Updates the position of the command bar tooltip. It aligns itself above the topmost selection in the editor.
+     */
+    this.updatePosition = function() {
+        if (!this.editor) {
+            return;
+        }
+        var renderer = this.editor.renderer;
+
+        var ranges;
+        if (this.editor.selection.getAllRanges) {
+            ranges = this.editor.selection.getAllRanges();
+        } else {
+            ranges = [this.editor.getSelection()];
+        }
+        if (!ranges.length) {
+            return;
+        }
+        var minPos = minPosition(ranges[0].start, ranges[0].end);
+        for (var i = 0, range; range = ranges[i]; i++) {
+            minPos = minPosition(minPos, minPosition(range.start, range.end));
+        }
+
+        var pos = renderer.$cursorLayer.getPixelPosition(minPos, true);
+
+        var el = this.htmlElement;
+        var screenWidth = window.innerWidth;
+        var rect = this.editor.container.getBoundingClientRect();
+
+        pos.top += rect.top - renderer.layerConfig.offset;
+        pos.left += rect.left - this.editor.renderer.scrollLeft;
+        pos.left += renderer.gutterWidth;
+
+        var top = pos.top - el.offsetHeight;
+
+        el.style.top = top + "px";
+        el.style.bottom = "";
+        el.style.left = Math.min(screenWidth - el.offsetWidth, pos.left) + "px";
+    };
+
+    /**
+     * Updates the buttons in the command bar tooltip. Should be called every time when any of the buttons can become disabled or enabled.
+     */
+    this.updateButtons = function(force) {
+        Object.keys(this.buttons).forEach(function(key) {
+            var commandEnabled = this.commands[key].enabled;
+            if (typeof commandEnabled === 'function') {
+                commandEnabled = commandEnabled(this.editor);
+            }
+
+            if (commandEnabled && (force || !this.eventListeners[key])) {
+                this.buttons[key].className = BUTTON_CLASS_NAME;
+                this.buttons[key].ariaDisabled = this.buttons[key].disabled = false;
+                this.buttons[key].removeAttribute("disabled");
+                var eventListener = function(e) {
+                    this.commands[key].exec(this.editor);
+                    e.preventDefault();
+                }.bind(this);
+                this.eventListeners[key] = eventListener;
+                this.buttons[key].addEventListener('mousedown', eventListener);
+            }
+            if (!commandEnabled && (force || this.eventListeners[key])) {
+                this.buttons[key].className = BUTTON_CLASS_NAME + "_disabled";
+                this.buttons[key].ariaDisabled = this.buttons[key].disabled = true;
+                this.buttons[key].setAttribute("disabled", "");
+                this.buttons[key].removeEventListener('mousedown', this.eventListeners[key]);
+                delete this.eventListeners[key];
+            }
+        }.bind(this));
+    };
+    
+    this.detach = function() {
+        var listenerKeys = Object.keys(this.eventListeners);
+        if (this.eventListeners && listenerKeys.length) {
+            listenerKeys.forEach(function(key) {
+                this.buttons[key].removeEventListener('mousedown', this.eventListeners[key]);
+                delete this.eventListeners[key];
+            }.bind(this));
+        }
+        if (this.htmlElement) {
+            this.htmlElement.removeEventListener('mousedown', captureMousedown.bind(this));
+            this.htmlElement.style.display = 'none';
+        }
+    };
+
+    this.destroy = function() {
+        this.detach();
+        if (this.htmlElement) {
+            this.htmlElement.parentNode.removeChild(this.htmlElement);
+        }
+        this.editor = null;
+        this.buttons = null;
+        this.htmlElement = null;
+        this.controls = null;
+    };
+}).call(InlineTooltip.prototype);
+
+dom.importCssString(`
+.ace_inline_autocomplete_tooltip {
+    display: inline-block;
+}
+.${ENTRY_CLASS_NAME} {
+    display: inline-block;
+    padding: 0 5px;
+}
+
+.${BUTTON_CLASS_NAME} {
+    display: inline-block;
+    cursor: pointer;
+    padding: 5px;
+}
+
+.${BUTTON_CLASS_NAME}:hover {
+    background-color: rgba(0, 0, 0, 0.1);
+}
+
+div.${BUTTON_CLASS_NAME}_disabled {
+    display: inline-block;
+    padding: 5px;
+    cursor: default;
+    color: #777;
+}`, "inlinetooltip.css", false);
+
+exports.InlineAutocomplete = InlineAutocomplete;
+exports.InlineTooltip = InlineTooltip;
+exports.TOOLTIP_ID = TOOLTIP_ID;
+exports.BUTTON_CLASS_NAME = BUTTON_CLASS_NAME;
diff --git a/src/ext/inline_autocomplete_test.js b/src/ext/inline_autocomplete_test.js
new file mode 100644
index 00000000000..3bdcea986e1
--- /dev/null
+++ b/src/ext/inline_autocomplete_test.js
@@ -0,0 +1,285 @@
+
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("../test/mockdom");
+}
+
+"use strict";
+
+var Editor = require("../editor").Editor;
+var EditSession = require("../edit_session").EditSession;
+var InlineAutocomplete = require("./inline_autocomplete").InlineAutocomplete;
+var assert = require("../test/assertions");
+var type = require("../test/user").type;
+var VirtualRenderer = require("../virtual_renderer").VirtualRenderer;
+
+var editor;
+var autocomplete;
+
+var getAllLines = function() {
+    var text = Array.from(editor.renderer.$textLayer.element.childNodes).map(function (node) {
+        return node.textContent;
+    }).join("\n");
+    if (editor.renderer.$ghostTextWidget) {
+        return text + "\n" + editor.renderer.$ghostTextWidget.text;
+    }
+    return text;
+};
+
+var typeAndChange = function(...args) {
+    assert.equal(autocomplete.changeTimer.isPending(), null);
+    type(args);
+    assert.ok(autocomplete.changeTimer.isPending);
+    autocomplete.changeTimer.call();
+};
+
+var completions = [
+    {
+        value: "foo"
+    },
+    {
+        value: "foobar"
+    },
+    {
+        value: "function"
+    },
+    {
+        value: "fundraiser"
+    },
+    {
+        snippet: "function foo() {\n    console.log('test');\n}",
+        caption: "func"
+    },
+    {
+        value: "foobar2"
+    }
+];
+
+var mockCompleter = {
+    getCompletions: function(_1, _2, _3, _4, callback) {
+        callback(null, completions);
+    }
+};
+
+module.exports = {
+    setUp: function(done) {
+        var el = document.createElement("div");
+        el.style.left = "20px";
+        el.style.top = "30px";
+        el.style.width = "500px";
+        el.style.height = "500px";
+        document.body.appendChild(el);
+        var renderer = new VirtualRenderer(el);
+        var session = new EditSession("");
+        editor = new Editor(renderer, session);
+        editor.execCommand("insertstring", "f");
+        editor.getSelection().moveCursorFileEnd();
+        editor.renderer.$loop._flush();
+        editor.completers = [mockCompleter];
+        autocomplete = InlineAutocomplete.for(editor);
+        editor.focus();
+        done();
+    },
+    "test: autocomplete completion shows up": function(done) {
+        autocomplete.show(editor);
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(autocomplete.getData().value, "foo");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(getAllLines(), "foo");
+        done();
+    },
+    "test: autocomplete tooltip is shown according to the selected option": function(done) {
+        assert.equal(autocomplete.inlineTooltip, null);
+
+        autocomplete.show(editor);
+        assert.strictEqual(autocomplete.inlineTooltip.isShown(), true);
+
+        autocomplete.detach();
+        assert.strictEqual(autocomplete.inlineTooltip.isShown(), false);
+        done();
+    },
+    "test: autocomplete navigation works": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(autocomplete.getData().value, "foo");
+        assert.equal(getAllLines(), "foo");
+
+        type("Alt-]");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 1);
+        assert.strictEqual(autocomplete.getData().value, "foobar");
+        assert.equal(getAllLines(), "foobar");
+
+        type("Alt-[");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(autocomplete.getData().value, "foo");
+        assert.equal(getAllLines(), "foo");
+        done();
+    },
+    "test: verify goTo commands": function(done) {
+        autocomplete.show(editor);
+        autocomplete.setIndex(1);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getData().value, "foobar");
+        assert.equal(getAllLines(), "foobar");
+
+        autocomplete.goTo("next");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 2);
+        assert.strictEqual(autocomplete.getData().value, "foobar2");
+        assert.strictEqual(getAllLines(), "foobar2");
+
+        autocomplete.goTo("prev");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 1);
+        assert.strictEqual(autocomplete.getData().value, "foobar");
+        assert.strictEqual(getAllLines(), "foobar");
+
+        autocomplete.goTo("last");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 5);
+        assert.strictEqual(autocomplete.getData().value, "fundraiser");
+        assert.strictEqual(getAllLines(), "fundraiser");
+
+        autocomplete.goTo("next");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 5);
+        assert.strictEqual(autocomplete.getData().value, "fundraiser");
+        assert.strictEqual(getAllLines(), "fundraiser");
+
+        autocomplete.goTo("first");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(autocomplete.getData().value, "foo");
+        assert.strictEqual(getAllLines(), "foo");
+
+        autocomplete.goTo("prev");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(autocomplete.getData().value, "foo");
+        assert.strictEqual(getAllLines(), "foo");
+        done();
+    },
+    "test: set index to negative value hides suggestions": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(getAllLines(), "foo");
+
+        autocomplete.setIndex(-1);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.strictEqual(getAllLines(), "f");
+        done();
+    },
+    "test: autocomplete can be closed": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.equal(getAllLines(), "foo");
+
+        type("Escape");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.equal(getAllLines(), "f");
+        done();
+    },
+    "test: autocomplete can be accepted": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.ok(document.querySelectorAll(".ace_ghost_text").length > 0);
+        assert.strictEqual(getAllLines(), "foo");
+
+        type("Tab");
+        editor.renderer.$loop._flush();
+        assert.equal(autocomplete.inlineCompleter, null);
+        assert.equal(autocomplete.inlineTooltip.isShown(), false);
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.equal(editor.renderer.$ghostText, null);
+        assert.equal(editor.renderer.$ghostTextWidget, null);
+        assert.strictEqual(document.querySelectorAll(".ace_ghost_text").length, 0);
+        assert.strictEqual(getAllLines(), "foo");
+        done();
+    },
+    "test: incremental typing filters results": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.equal(getAllLines(), "foo");
+
+        typeAndChange("u", "n");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.equal(getAllLines(), "function foo() {\n    console.log('test');\n}");
+
+        typeAndChange("d");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.equal(getAllLines(), "fundraiser");
+
+        typeAndChange("Backspace", "Backspace", "Backspace");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), true);
+        assert.equal(getAllLines(), "foo");
+
+        typeAndChange("r");
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.equal(getAllLines(), "fr");
+
+        done();
+    },
+    "test: verify detach": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(getAllLines(), "foo");
+
+        autocomplete.detach();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), -1);
+        assert.strictEqual(autocomplete.getLength(), 0);
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.equal(autocomplete.base, null);
+        assert.equal(autocomplete.completions, null);
+        assert.equal(autocomplete.completionProvider, null);
+        assert.strictEqual(getAllLines(), "f");
+        done();
+    },
+    "test: verify destroy": function(done) {
+        autocomplete.show(editor);
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), 0);
+        assert.strictEqual(getAllLines(), "foo");
+        assert.strictEqual(editor.completer, autocomplete);
+
+        autocomplete.destroy();
+        editor.renderer.$loop._flush();
+        assert.strictEqual(autocomplete.getIndex(), -1);
+        assert.strictEqual(autocomplete.getLength(), 0);
+        assert.strictEqual(autocomplete.isOpen(), false);
+        assert.equal(autocomplete.base, null);
+        assert.equal(autocomplete.completions, null);
+        assert.equal(autocomplete.completionProvider, null);
+        assert.equal(autocomplete.editor, null);
+        assert.equal(autocomplete.inlineTooltip, null);
+        assert.equal(autocomplete.inlineRenderer, null);
+        assert.strictEqual(editor.completer, null);
+        assert.strictEqual(getAllLines(), "f");
+
+        autocomplete.destroy();
+        editor.renderer.$loop._flush();
+        done();
+    },
+    tearDown: function() {
+        autocomplete.destroy();
+        editor.destroy();
+    }
+};
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}
diff --git a/src/ext/inline_autocomplete_tooltip_test.js b/src/ext/inline_autocomplete_tooltip_test.js
new file mode 100644
index 00000000000..bee40330e0c
--- /dev/null
+++ b/src/ext/inline_autocomplete_tooltip_test.js
@@ -0,0 +1,174 @@
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("../test/mockdom");
+}
+
+"use strict";
+
+var TOOLTIP_ID = require("./inline_autocomplete").TOOLTIP_ID;
+var BUTTON_CLASS_NAME = require("./inline_autocomplete").BUTTON_CLASS_NAME;
+var InlineTooltip = require("./inline_autocomplete").InlineTooltip;
+var Editor = require("../ace").Editor;
+var EditSession = require("../ace").EditSession;
+var VirtualRenderer = require("../ace").VirtualRenderer;
+var assert = require("../test/assertions");
+
+function mousedown(node) {
+    node.dispatchEvent(new window.CustomEvent("mousedown", { bubbles: true }));
+}
+
+var editor;
+var counters = {};
+var inlineTooltip;
+var testCommand2Enabled = true;
+var commands = {
+    "testCommand1": {
+        name: "testCommand1",
+        bindKey: "Alt-K",
+        exec: function(editor) {
+            if (!editor) {
+                return;
+            }
+            if (!counters["testCommand1"]) {
+            counters["testCommand1"] = 0;
+            }
+            counters["testCommand1"]++;
+        },
+        enabled: function(editor) {
+            if (!editor) {
+                return;
+            }
+            if (!counters["testEnabled1"]) {
+                counters["testEnabled1"] = 0;
+            }
+            counters["testEnabled1"]++;
+            return true;
+        },
+        position: 10
+    },
+    "testCommand2": {
+        name: "testCommand2",
+        bindKey: "Alt-L",
+        exec: function(editor) {
+            if (!editor) {
+                return;
+            }
+            if (!counters["testCommand2"]) {
+            counters["testCommand2"] = 0;
+            }
+            counters["testCommand2"]++;
+        },
+        enabled: function(editor) {
+            if (!editor) {
+                return;
+            }
+            if (!counters["testEnabled2"]) {
+                counters["testEnabled2"] = 0;
+            }
+            counters["testEnabled2"]++;
+            return testCommand2Enabled;
+        },
+        position: 20
+    }
+};
+
+module.exports = {
+    setUp: function() {
+        var el = document.createElement("div");
+        el.style.left = "20px";
+        el.style.top = "30px";
+        el.style.width = "500px";
+        el.style.height = "500px";
+        document.body.appendChild(el);
+        var renderer = new VirtualRenderer(el);
+        var session = new EditSession("abc123\n\nfunc");
+        editor = new Editor(renderer, session);
+        counters = {};
+        inlineTooltip = new InlineTooltip(editor, document.body);
+        inlineTooltip.setCommands(commands);
+        testCommand2Enabled = true;
+        editor.getSelection().moveCursorFileEnd();
+        editor.renderer.$loop._flush();
+    },
+    "test: displays inline tooltip above cursor with commands": function(done) {
+        var tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.strictEqual(inlineTooltip.isShown(), false);
+        assert.strictEqual(window.getComputedStyle(tooltipDomElement).display, "none");
+
+        inlineTooltip.show(editor);
+        tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.strictEqual(window.getComputedStyle(tooltipDomElement).display, "");
+        assert.strictEqual(inlineTooltip.isShown(), true);
+        done();
+    },
+    "test: commands are clickable": function(done) {
+        inlineTooltip.show(editor);
+        assert.strictEqual(inlineTooltip.isShown(), true);
+        assert.strictEqual(counters["testCommand1"], undefined);
+        var buttonElements = Array.from(document.querySelectorAll("." + BUTTON_CLASS_NAME));
+        assert.strictEqual(buttonElements.length, 2);
+        mousedown(buttonElements[0]);
+        mousedown(buttonElements[1]);
+        assert.strictEqual(counters["testCommand1"], 1);
+        done();
+    },
+    "test: commands are disabled when enable check is falsy": function(done) {
+        inlineTooltip.show(editor);
+        var buttonElements = Array.from(document.querySelectorAll("." + BUTTON_CLASS_NAME));
+        var disabledButtonElements = Array.from(document.querySelectorAll("." + BUTTON_CLASS_NAME + "_disabled"));
+        assert.strictEqual(buttonElements.length, 2);
+        assert.strictEqual(disabledButtonElements.length, 0);
+        assert.strictEqual(buttonElements.filter(function (button) { return !button.disabled; }).length, 2);
+        assert.strictEqual(counters["testEnabled1"], 1);
+        assert.strictEqual(counters["testEnabled2"], 1);
+
+        testCommand2Enabled = false;
+        inlineTooltip.updateButtons();
+        buttonElements = Array.from(document.querySelectorAll("." + BUTTON_CLASS_NAME));
+        disabledButtonElements = Array.from(document.querySelectorAll("." + BUTTON_CLASS_NAME + "_disabled"));
+        assert.strictEqual(buttonElements.length, 1);
+        assert.strictEqual(disabledButtonElements.length, 1);
+        assert.strictEqual(disabledButtonElements.filter(function (button) { return button.disabled; }).length, 1);
+        assert.strictEqual(buttonElements.filter(function (button) { return !button.disabled; }).length, 1);
+        assert.strictEqual(counters["testEnabled1"], 2);
+        assert.strictEqual(counters["testEnabled2"], 2);
+        done();
+    },
+    "test: verify detach": function(done) {
+        inlineTooltip.show(editor);
+        var tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.strictEqual(inlineTooltip.isShown(), true);
+
+        inlineTooltip.detach();
+        assert.strictEqual(inlineTooltip.isShown(), false);
+        var tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.strictEqual(window.getComputedStyle(tooltipDomElement).display, "none");
+        done();
+    },
+    "test: verify destroy": function(done) {
+        inlineTooltip.show(editor);
+        var tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.strictEqual(inlineTooltip.isShown(), true);
+        assert.ok(tooltipDomElement);
+
+        inlineTooltip.destroy();
+        assert.strictEqual(inlineTooltip.isShown(), false);
+        tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.equal(tooltipDomElement, null);
+
+        // Intentionally called twice
+        inlineTooltip.destroy();
+        assert.strictEqual(inlineTooltip.isShown(), false);
+        tooltipDomElement = document.getElementById(TOOLTIP_ID);
+        assert.equal(tooltipDomElement, null);
+        done();
+    },
+    tearDown: function() {
+        inlineTooltip.destroy();
+        editor.destroy();
+    }
+};
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}
diff --git a/src/keyboard/textinput.js b/src/keyboard/textinput.js
index 1ee56031e2c..8953353d78c 100644
--- a/src/keyboard/textinput.js
+++ b/src/keyboard/textinput.js
@@ -52,7 +52,7 @@ var TextInput = function(parentNode, host) {
     this.setAriaOptions = function(options) {
         if (options.activeDescendant) {
             text.setAttribute("aria-haspopup", "true");
-            text.setAttribute("aria-autocomplete", "list");
+            text.setAttribute("aria-autocomplete", options.inline ? "both" : "list");
             text.setAttribute("aria-activedescendant", options.activeDescendant);
         } else {
             text.setAttribute("aria-haspopup", "false");
diff --git a/src/snippets.js b/src/snippets.js
index d54e3ead65c..7947a49d877 100644
--- a/src/snippets.js
+++ b/src/snippets.js
@@ -349,7 +349,7 @@ var SnippetManager = function() {
         return result;
     };
 
-    this.insertSnippetForSelection = function(editor, snippetText) {
+    var processSnippetText = function(editor, snippetText) {
         var cursor = editor.getCursorPosition();
         var line = editor.session.getLine(cursor.row);
         var tabString = editor.session.getTabString();
@@ -466,12 +466,28 @@ var SnippetManager = function() {
                     t.end = {row: row, column: column};
             }
         });
+
+        return {
+            text,
+            tabstops,
+            tokens
+        };
+    };
+
+    this.getDisplayTextForSnippet = function(editor, snippetText) {
+        var processedSnippet = processSnippetText.call(this, editor, snippetText);
+        return processedSnippet.text;
+    };
+
+    this.insertSnippetForSelection = function(editor, snippetText) {
+        var processedSnippet = processSnippetText.call(this, editor, snippetText);
+        
         var range = editor.getSelectionRange();
-        var end = editor.session.replace(range, text);
+        var end = editor.session.replace(range, processedSnippet.text);
 
         var tabstopManager = new TabstopManager(editor);
         var selectionId = editor.inVirtualSelectionMode && editor.selection.index;
-        tabstopManager.addTabstops(tabstops, range.start, end, selectionId);
+        tabstopManager.addTabstops(processedSnippet.tabstops, range.start, end, selectionId);
     };
     
     this.insertSnippet = function(editor, snippetText) {
diff --git a/src/test/all_browser.js b/src/test/all_browser.js
index 76f32465fa7..944889212b2 100644
--- a/src/test/all_browser.js
+++ b/src/test/all_browser.js
@@ -13,6 +13,7 @@ var el = document.createElement.bind(document);
 var testNames = [
     "ace/ace_test",
     "ace/anchor_test",
+    "ace/autocomplete/popup_test",
     "ace/autocomplete_test",
     "ace/background_tokenizer_test",
     "ace/commands/command_manager_test",
@@ -25,6 +26,8 @@ var testNames = [
     "ace/editor_text_edit_test",
     "ace/editor_commands_test",
     "ace/ext/hardwrap_test",
+    "ace/ext/inline_autocomplete_test",
+    "ace/ext/inline_autocomplete_tooltip_test",
     "ace/ext/static_highlight_test",
     "ace/ext/whitespace_test",
     "ace/ext/error_marker_test",
diff --git a/src/test/mockdom.js b/src/test/mockdom.js
index e0c65ddddae..fa81e9d692c 100644
--- a/src/test/mockdom.js
+++ b/src/test/mockdom.js
@@ -375,7 +375,7 @@ function Node(name) {
             else
                 width = rect.width - right - left;
             
-            if (this.style.width)
+            if (this.style.height)
                 height = parseCssLength(this.style.height || "100%", rect.height);
             else
                 height = rect.height - top - bottom;
diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js
index 76d99adf144..f73f4271273 100644
--- a/src/virtual_renderer.js
+++ b/src/virtual_renderer.js
@@ -1662,7 +1662,7 @@ var VirtualRenderer = function(container, theme) {
         session.bgTokenizer.lines[row] = null;
         var newToken = {type: type, value: text};
         var tokens = session.getTokens(row);
-        if (column == null) {
+        if (column == null || !tokens.length) {
             tokens.push(newToken);
         } else {
             var l = 0;
@@ -1683,6 +1683,7 @@ var VirtualRenderer = function(container, theme) {
     };
 
     this.removeExtraToken = function(row, column) {
+        this.session.bgTokenizer.lines[row] = null;
         this.updateLines(row, row);
     };
 
diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js
index e08e4f916dc..aaf35d49cd6 100644
--- a/src/virtual_renderer_test.js
+++ b/src/virtual_renderer_test.js
@@ -320,6 +320,11 @@ module.exports = {
 
         editor.renderer.$loop._flush();
         assert.equal(editor.renderer.content.textContent, "abcdefGhost");
+
+        editor.removeGhostText();
+
+        editor.renderer.$loop._flush();
+        assert.equal(editor.renderer.content.textContent, "abcdef");
     },
 
     "test multiline ghost text": function() {
@@ -332,6 +337,13 @@ module.exports = {
         assert.equal(editor.renderer.content.textContent, "abcdefGhost1");
         
         assert.equal(editor.session.lineWidgets[0].el.textContent, "Ghost2\nGhost3");
+
+        editor.removeGhostText();
+
+        editor.renderer.$loop._flush();
+        assert.equal(editor.renderer.content.textContent, "abcdef");
+        
+        assert.equal(editor.session.lineWidgets, null);
     },
     "test: brackets highlighting": function (done) {
         var renderer = editor.renderer;