diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js index 487aeb11b..6f98738cd 100644 --- a/js/ui/runDialog.js +++ b/js/ui/runDialog.js @@ -2,6 +2,7 @@ const Big = imports.gi.Big; const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Lang = imports.lang; const Mainloop = imports.mainloop; @@ -24,6 +25,144 @@ const DIALOG_WIDTH = 320; const DIALOG_PADDING = 6; const ICON_SIZE = 24; const ICON_BOX_SIZE = 36; +const MAX_FILE_DELETED_BEFORE_INVALID = 10; + +function CommandCompleter() { + this._init(); +} + +CommandCompleter.prototype = { + _init : function() { + this._changedCount = 0; + this._paths = GLib.getenv('PATH').split(':'); + this._valid = false; + this._updateInProgress = false; + this._childs = new Array(this._paths.length); + this._monitors = new Array(this._paths.length); + for (let i = 0; i < this._paths.length; i++) { + this._childs[i] = []; + let file = Gio.file_new_for_path(this._paths[i]); + let info = file.query_info(Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE, null); + + if (info.get_attribute_uint32(Gio.FILE_ATTRIBUTE_STANDARD_TYPE) != Gio.FileType.DIRECTORY) + continue; + + this._paths[i] = file.get_path(); + this._monitors[i] = file.monitor_directory(Gio.FileMonitorFlags.NONE, null); + if (this._monitors[i] != null) { + this._monitors[i].connect("changed", Lang.bind(this, this._onChanged)); + } + } + this._update(0); + }, + + _onGetEnumerateComplete : function(obj, res) { + this._enumerator = obj.enumerate_children_finish(res); + this._enumerator.next_files_async(100, GLib.PRIORITY_LOW, null, Lang.bind(this, this._onNextFileComplete), null); + }, + + _onNextFileComplete : function(obj, res) { + let files = obj.next_files_finish(res); + for (let i = 0; i < files.length; i++) { + this._childs[this._i].push(files[i].get_name()); + } + if (files.length) { + this._enumerator.next_files_async(100, GLib.PRIORITY_LOW, null, Lang.bind(this, this._onNextFileComplete), null); + } else { + this._enumerator.close(null); + this._enumerator = null; + this._update(this._i + 1); + } + }, + + update : function() { + if (this._valid) + return; + this._update(0); + }, + + _update : function(i) { + if (i == 0 && this._updateInProgress) + return; + this._updateInProgress = true; + this._changedCount = 0; + this._i = i; + if (i >= this._paths.length) { + this._valid = true; + this._updateInProgress = false; + return; + } + let file = Gio.file_new_for_path(this._paths[i]); + this._childs[this._i] = []; + file.enumerate_children_async(Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_LOW, null, Lang.bind(this, this._onGetEnumerateComplete), null); + }, + + _onChanged : function(m, f, of, type) { + if (!this._valid) + return; + let path = f.get_parent().get_path(); + let k = undefined; + for (let i = 0; i < this._paths.length; i++) { + if (this._paths[i] == path) + k = i; + } + if (k === undefined) { + return; + } + if (type == Gio.FileMonitorEvent.CREATED) { + this._childs[k].push(f.get_basename()); + } + if (type == Gio.FileMonitorEvent.DELETED) { + this._changedCount++; + if (this._changedCount > MAX_FILE_DELETED_BEFORE_INVALID) { + this._valid = false; + } + let name = f.get_basename(); + this._childs[k] = this._childs[k].filter(function(e) { + return e != name; + }); + } + if (type == Gio.FileMonitorEvent.UNMOUNTED) { + this._childs[k] = []; + } + }, + + getCompletion: function(text) { + let common = ""; + let notInit = true; + if (!this._valid) { + this._update(0); + return common; + } + function _getCommon(s1, s2) { + let k = 0; + for (; k < s1.length && k < s2.length; k++) { + if (s1[k] != s2[k]) + break; + } + if (k == 0) + return ""; + return s1.substr(0, k); + } + function _hasPrefix(s1, prefix) { + return s1.indexOf(prefix) == 0; + } + for (let i = 0; i < this._childs.length; i++) { + for (let k = 0; k < this._childs[i].length; k++) { + if (!_hasPrefix(this._childs[i][k], text)) + continue; + if (notInit) { + common = this._childs[i][k]; + notInit = false; + } + common = _getCommon(common, this._childs[i][k]); + } + } + if (common.length) + return common.substr(text.length); + return common; + } +}; function RunDialog() { this._init(); @@ -137,16 +276,55 @@ RunDialog.prototype = { this.close(); })); + this._pathCompleter = new Gio.FilenameCompleter(); + this._commandCompleter = new CommandCompleter(); + this._group.connect('notify::visible', Lang.bind(this._commandCompleter, this._commandCompleter.update)); this._entry.connect('key-press-event', Lang.bind(this, function(o, e) { let symbol = e.get_key_symbol(); if (symbol == Clutter.Escape) { this.close(); return true; } + if (symbol == Clutter.slash) { + // Need preload data before get completion. GFilenameCompleter load content of parent directory. + // Parent directory for /usr/include/ is /usr/. So need to add fake name('a'). + let text = o.get_text().concat('/a'); + let prefix; + if (text.lastIndexOf(' ') == -1) + prefix = text; + else + prefix = text.substr(text.lastIndexOf(' ') + 1); + this._getCompletion(prefix); + return false; + } + if (symbol == Clutter.Tab) { + let text = o.get_text(); + let prefix; + if (text.lastIndexOf(' ') == -1) + prefix = text; + else + prefix = text.substr(text.lastIndexOf(' ') + 1); + let postfix = this._getCompletion(prefix); + if (postfix != null && postfix.length > 0) { + o.insert_text(postfix, -1); + o.set_cursor_position(text.length + postfix.length); + if (postfix[postfix.length - 1] == '/') + this._getCompletion(text + postfix + 'a'); + } + return true; + } return false; })); }, + _getCompletion : function(text) { + if (text.indexOf('/') != -1) { + return this._pathCompleter.get_completion_suffix(text); + } else { + return this._commandCompleter.getCompletion(text); + } + }, + _run : function(command) { this._commandError = false; let f;