diff --git a/__tests__/terminal.spec.js b/__tests__/terminal.spec.js
index f7e91b72b..b5b0de244 100644
--- a/__tests__/terminal.spec.js
+++ b/__tests__/terminal.spec.js
@@ -172,7 +172,7 @@ require('../js/jquery.terminal-src')(global.$);
require('../js/unix_formatting')(global.$);
require('../js/pipe')(global.$);
require('../js/echo_newline')(global.$);
-//require('../js/autocomplete_menu')(global.$);
+require('../js/autocomplete_menu')(global.$);
jest.setTimeout(10000);
@@ -2266,6 +2266,84 @@ describe('extensions', function() {
expect(term.get_prompt()).toEqual(prompt);
});
});
+ describe('autocomplete_menu', function() {
+ function completion(term) {
+ return find_menu(term).find('li').map(function() {
+ return a0($(this).text());
+ }).get();
+ }
+ function find_menu(term) {
+ return term.find('.cmd-cursor-line .cursor-wrapper .cmd-cursor + ul');
+ }
+ function menu_visible(term) {
+ var menu = find_menu(term);
+ expect(menu.length).toEqual(1);
+ expect(menu.is(':visible')).toBeTruthy();
+ }
+ function complete(term, text) {
+ term.focus().insert(text);
+ shortcut(false, false, false, 9, 'tab');
+ return delay(5);
+ }
+ it('should display menu from function with Promise', async function() {
+ var term = $('
').terminal($.noop, {
+ autocompleteMenu: true,
+ completion: function(string) {
+ if (!string.match(/_/) && string.length > 3) {
+ return Promise.resolve([string + '_foo', string + '_bar']);
+ }
+ }
+ });
+ await complete(term, 'hello');
+ menu_visible(term);
+ expect(term.get_command()).toEqual('hello_');
+ expect(completion(term)).toEqual(['foo', 'bar']);
+ term.destroy();
+ });
+ it('should display menu from array', async function() {
+ var term = $('').terminal($.noop, {
+ autocompleteMenu: true,
+ completion: ['hello_foo', 'hello_bar']
+ });
+ await complete(term, 'hello');
+ menu_visible(term);
+ expect(term.get_command()).toEqual('hello_');
+ expect(completion(term)).toEqual(['foo', 'bar']);
+ term.destroy();
+ });
+ it('should display menu from Promise', async function() {
+ var term = $('').terminal($.noop, {
+ autocompleteMenu: true,
+ completion: async function() {
+ await delay(10);
+ return ['hello_foo', 'hello_bar'];
+ }
+ });
+ complete(term, 'hello');
+ await delay(100);
+ menu_visible(term);
+ expect(term.get_command()).toEqual('hello_');
+ expect(completion(term)).toEqual(['foo', 'bar']);
+ term.destroy();
+ });
+ it('should display menu with one element', async function() {
+ var term = $('').terminal($.noop, {
+ autocompleteMenu: true,
+ completion: ['hello_foo', 'hello_bar']
+ });
+ await complete(term, 'hello');
+ enter_text('f');
+ await delay(5);
+ menu_visible(term);
+ expect(term.get_command()).toEqual('hello_f');
+ expect(completion(term)).toEqual(['oo']);
+ shortcut(false, false, false, 9, 'tab');
+ await delay(5);
+ expect(completion(term)).toEqual([]);
+ expect(term.get_command()).toEqual('hello_foo');
+ term.destroy();
+ });
+ });
});
describe('sub plugins', function() {
describe('text_length', function() {
diff --git a/js/autocomplete_menu.js b/js/autocomplete_menu.js
index 47ac1e7c8..887ab1295 100644
--- a/js/autocomplete_menu.js
+++ b/js/autocomplete_menu.js
@@ -13,7 +13,7 @@
* Released under the MIT license
*
*/
-/* global define, global, require, module, setTimeout */
+/* global define, global, require, module, setTimeout, clearTimeout */
(function(factory) {
var root = typeof window !== 'undefined' ? window : global;
if (typeof define === 'function' && define.amd) {
@@ -52,20 +52,47 @@
})(function($) {
var jquery_terminal = $.fn.terminal;
$.fn.terminal = function(interpreter, options) {
- return jquery_terminal.call(this, interpreter, autocomplete_menu(options));
+ function init(node) {
+ return jquery_terminal.call(node, interpreter, autocomplete_menu(options));
+ }
+ if (this.length > 1) {
+ return this.each(init.bind(null, this));
+ } else {
+ return init(this);
+ }
};
+ // -----------------------------------------------------------------------------------
+ // :: cancableble task for usage in comletion menu to ignore previous async completion
+ // -----------------------------------------------------------------------------------
+ function Task(fn) {
+ this._fn = fn;
+ }
+ Task.prototype.invoke = function() {
+ if (!this._cancel) {
+ this._fn.apply(null, arguments);
+ }
+ };
+ Task.prototype.cancel = function() {
+ this._cancel = true;
+ };
+ // -----------------------------------------------------------------------------------
+ // :: function return patched terminal settings
+ // -----------------------------------------------------------------------------------
function autocomplete_menu(options) {
if (options && !options.autocompleteMenu) {
return options;
}
var settings = options || {};
- function complete_menu(term, e, list) {
+ var last_task;
+ // -------------------------------------------------------------------------------
+ // :: function that do actuall matching and displaying of completion menu
+ // -------------------------------------------------------------------------------
+ function complete_menu(term, e, word, list) {
var matched = [];
- var word = term.before_cursor(true);
var regex = new RegExp('^' + $.terminal.escape_regex(word));
for (var i = list.length; i--;) {
if (regex.test(list[i])) {
- matched.push(list[i]);
+ matched.unshift(list[i]);
}
}
if (e.which === 9) {
@@ -93,6 +120,7 @@
delete settings.completion;
settings.onInit = function(term) {
onInit.call(this, term);
+ // init html menu element
var wrapper = this.cmd().find('.cmd-cursor').
wrap('').parent().addClass('cursor-wrapper');
ul = $('').appendTo(wrapper);
@@ -101,21 +129,33 @@
ul.empty();
});
};
+ var timer;
settings.keydown = function(e, term) {
// setTimeout because terminal is adding characters in keypress
// we use keydown because we need to prevent default action
// for tab and still execute custom code
- setTimeout(function() {
+ clearTimeout(timer);
+ timer = setTimeout(function() {
ul.empty();
+ var word = term.before_cursor(true);
+ if (last_task) {
+ last_task.cancel(); // ignore previous completion task
+ }
+ // we save task in closure for callbacks and promise::then
+ var task = last_task = new Task(complete_menu);
if (typeof completion === 'function') {
- var ret = completion.call(term);
+ var ret = completion.call(term, word, function(list) {
+ task.invoke(term, e, word, list);
+ });
if (ret && typeof ret.then === 'function') {
- ret.then(complete_menu.bind(null, term, e));
+ ret.then(function(completion) {
+ task.invoke(term, e, word, completion);
+ });
} else if (ret instanceof Array) {
- complete_menu(term, e, ret);
+ task.invoke(term, e, word, ret);
}
} else if (completion instanceof Array) {
- complete_menu(term, e, completion);
+ task.invoke(term, e, word, completion);
}
}, 0);
var ret = keydown.call(this, e, term);