diff --git a/liberator/content/completion.js b/liberator/content/completion.js index cd23f003..33387b3f 100644 --- a/liberator/content/completion.js +++ b/liberator/content/completion.js @@ -225,13 +225,14 @@ CompletionContext.prototype = { { this.hasItems = true; this._generate = arg; - liberator.dump(this.name + ": set generate()"); + //**/ liberator.dump(this.name + ": set generate()"); if (this.background && this.regenerate) { //**/ this.__i = (this.__i || 0) + 1; //**/ let self = this; //**/ function dump(msg) liberator.callInMainThread(function () liberator.dump(self.name + ":" + self.__i + ": " + msg)); //**/ dump("set generate() regenerating"); + let lock = {}; this.cache.backgroundLock = lock; this.incomplete = true; @@ -837,8 +838,6 @@ function Completion() //{{{ let res = functions.some(function (idx) idx >= start && idx < end); if (!res || self.context.tabPressed || key in cache.eval) return false; - liberator.dump(cache.eval[key]); - liberator.dumpStack(); self.context.waitingForTab = true; return true; } diff --git a/liberator/content/liberator.js b/liberator/content/liberator.js index 52319744..1260e44e 100644 --- a/liberator/content/liberator.js +++ b/liberator/content/liberator.js @@ -676,9 +676,12 @@ const liberator = (function () //{{{ window.dump(("config" in modules && config.name.toLowerCase()) + ": " + message); }, - dumpStack: function (msg) + dumpStack: function (msg, frames) { - liberator.dump((msg || "") + (new Error()).stack); + let stack = Error().stack.replace(/(?:.*\n){2}/, ""); + if (frames != null) + [stack] = stack.match(RegExp("(?:.*\n){0," + frames + "}")); + liberator.dump((msg || "Stack") + "\n" + stack); }, echo: function (str, flags) @@ -965,7 +968,10 @@ const liberator = (function () //{{{ io.source(file.path, false); liberator.pluginFiles[file.path] = true; } - catch (e) {}; + catch (e) + { + liberator.reportError(e); + } } }); } diff --git a/liberator/content/ui.js b/liberator/content/ui.js index 81d62148..5878ab26 100644 --- a/liberator/content/ui.js +++ b/liberator/content/ui.js @@ -96,133 +96,196 @@ function CommandLine() //{{{ var silent = false; + function Completions(context) + { + let self = this; + context.onUpdate = function () + { + self.reset(); + }; + this.context = context; + this.selected = null; + this.wildmode = options.get("wildmode"); + this.itemList = completionList; + this.itemList.setItems(context); + this.reset(); + } + Completions.prototype = { + UP: {}, + DOWN: {}, + PAGE_UP: {}, + PAGE_DOWN: {}, + RESET: null, + + get completion() + { + let str = commandline.getCommand(); + return str.substring(this.prefix.length, str.length - this.suffix.length); + }, + set completion set_completion(completion) + { + previewClear(); + let editor = commandWidget.inputField.editor; + + // Change the completion text. + // The second line is a hack to deal with some substring + // preview corner cases. + commandWidget.value = this.prefix + completion + this.suffix; + editor.selection.focusNode.textContent = commandWidget.value; + + // Reset the caret to one position after the completion. + let range = editor.selection.getRangeAt(0); + range.setStart(range.startContainer, this.prefix.length + completion.length); + range.collapse(true); + }, + + get start() this.context.allItems.start, + + get items() this.context.allItems.items, + + get substring() this.context.longestAllSubstring, + + get wildtype() this.wildtypes[this.wildIndex] || "", + + get type() ({ + list: this.wildmode.checkHas(this.wildtype, "list"), + longest: this.wildmode.checkHas(this.wildtype, "longest"), + first: this.wildmode.checkHas(this.wildtype, ""), + full: this.wildmode.checkHas(this.wildtype, "full") + }), + + reset: function reset(show) + { + this.wildtypes = this.wildmode.values; + this.wildIndex = -1; + + this.prefix = this.context.value.substr(0, this.start); + this.value = this.context.value.substr(this.start, this.context.caret); + this.suffix = this.context.value.substr(this.context.caret); + + if (show) + { + this.itemList.reset(); + this.select(this.RESET); + this.itemList.show(); + this.wildIndex = 0; + } + + previewSubstring(); + }, + + select: function select(idx) + { + switch (idx) + { + case this.UP: + if (this.selected == null) + idx = this.items.length - 1; + else + idx = this.selected - 1; + break; + case this.DOWN: + if (this.selected == null) + idx = 0; + else + idx = this.selected + 1; + break; + case this.RESET: + idx = null; + break; + default: idx = Math.max(0, Math.max(this.items.length - 1, idx)); + } + this.itemList.selectItem(idx); + if (idx < 0 || idx >= this.items.length || idx == null) + { + // Wrapped. Start again. + this.selected = null; + this.completion = this.value; + } + else + { + this.selected = idx; + this.completion = this.items[idx].text; + } + }, + + tab: function tab(reverse) + { + // Check if we need to run the completer. + if (this.context.waitingForTab || this.wildIndex == -1) + { + this.context.reset(); + this.context.tabPressed = true; + liberator.triggerCallback("complete", currentExtendedMode, this.context); + this.reset(true); + } + + if (this.items.length == 0) + { + // No items. Wait for any unfinished completers. + let end = Date.now() + 5000; + while (this.context.incomplete && this.items.length == 0 && Date.now() < end) + liberator.threadYield(); + + if (this.items.length == 0) + return liberator.beep(); + } + + switch (this.wildtype.replace(/.*:/, "")) + { + case "": + this.select(0); + break; + case "longest": + if (this.items.length > 1) + { + if (this.substring && this.substring != this.completion) + { + this.completion = this.substring; + liberator.triggerCallback("change", currentExtendedMode, commandline.getCommand()); + } + break; + } + // Fallthrough + case "full": + this.select(reverse ? this.UP : this.DOWN) + break; + } + + if (this.type.list) + completionList.show(); + + this.wildIndex = Math.max(0, Math.min(this.wildtypes.length - 1, this.wildIndex + 1)); + + statusTimer.tell(); + } + } + var completionList = new ItemList("liberator-completions"); - var completions = { start: 0, items: [] }; - var completionContext = null; - // for the example command "open sometext| othertext" (| is the cursor pos): - var completionPrefix = ""; // will be: "open sometext" - var completionPostfix = ""; // will be: " othertext" - var completionIndex = UNINITIALIZED; + var completions = null; var wildIndex = 0; // keep track how often we press in a row var startHints = false; // whether we're waiting to start hints mode var lastSubstring = ""; var statusTimer = new util.Timer(5, 100, function statusTell() { - if (completionIndex >= completions.items.length) + if (completions.selected == null) statusline.updateProgress(""); else - statusline.updateProgress("match " + (completionIndex + 1) + " of " + completions.items.length); + statusline.updateProgress("match " + (completions.selected + 1) + " of " + completions.items.length); }); var autocompleteTimer = new util.Timer(201, 300, function autocompleteTell(tabPressed) { - if (events.feedingKeys) + if (events.feedingKeys || !completions) return; - completionContext.reset(); - completionContext.fork("ex", 0, completion, "ex"); - commandline.setCompletions(completionContext.allItems); + completions.context.reset(); + liberator.triggerCallback("complete", currentExtendedMode, completions.context); + completions.reset(true); + completions.itemList.show(); }); var tabTimer = new util.Timer(10, 10, function tabTell(event) { - - let command = commandline.getCommand(); - - // always reset our completion history so up/down keys will start with new values - historyIndex = UNINITIALIZED; - - // TODO: call just once, and not on each - let wildmode = options.get("wildmode"); - let wildType = wildmode.values[Math.min(wildIndex++, wildmode.values.length - 1)]; - - let first = wildmode.value == ""; - let hasList = wildmode.checkHas(wildType, "list"); - let longest = wildmode.checkHas(wildType, "longest"); - let full = first || !longest && wildmode.checkHas(wildType, "full"); - - // we need to build our completion list first - if (completionIndex == UNINITIALIZED || completionContext.waitingForTab) - { - completionIndex = -1; - completionPrefix = command.substring(0, commandWidget.selectionStart); - completionPostfix = command.substring(commandWidget.selectionStart); - completions = liberator.triggerCallback("complete", currentExtendedMode, completionPrefix); - - completionList.setItems(completionContext); - } - - if (completions.items.length == 0) - { - // Wait for items to come available - // TODO: also use that code when we DO have completions but too few - let end = Date.now() + 5000; - while (completionContext.incomplete && completions.items.length == 0 && Date.now() < end) - { - liberator.threadYield(); - completions = completionContext.allItems; - } - - if (completions.items.length == 0) // still not more matches - { - liberator.beep(); - return; - } - } - - if (first) - completionIndex = 0; - else if (full) - { - if (event.shiftKey) - completionIndex--; - else - completionIndex++; - completionIndex = Math.max(0, Math.min(completions.items.length - 1, completionIndex)); - - statusTimer.tell(); - } - - // the following line is not inside if (hasList) for list:longest,full - completionList.selectItem(completionIndex); - if (hasList) - completionList.show(); - - if ((completionIndex == -1 || completionIndex >= completions.items.length) && !longest) // wrapped around matches, reset command line - { - if (full) - setCommand(completionPrefix + completionPostfix); - } - else - { - let compl = null; - if (longest && completions.items.length > 1) - compl = completions.longestSubstring; - else if (full) - compl = completions.items[completionIndex].text; - else if (completions.items.length == 1) - compl = completions.items[0].text; - - if (compl) - { - previewClear(); - let editor = commandWidget.inputField.editor; - - commandWidget.value = command.substring(0, completions.start) + compl + completionPostfix; - editor.selection.focusNode.textContent = commandWidget.value; - - let range = editor.selection.getRangeAt(0); - range.setStart(range.startContainer, completions.start + compl.length); - range.collapse(true); - - if (longest) - liberator.triggerCallback("change", currentExtendedMode, commandline.getCommand()); - - // Start a new completion in the next iteration. Useful for commands like :source - // RFC: perhaps the command can indicate whether the completion should be restarted - // -> should be doable now, since the completion items are objects - // Needed for :source to grab another set of completions after a file/directory has been filled out - // if (completions.length == 1 && !full) - // completionIndex = UNINITIALIZED; - } - } + if (completions) + completions.tab(event.shiftKey); }); // the containing box for the promptWidget and commandWidget @@ -258,18 +321,14 @@ function CommandLine() //{{{ var promptCompleter = null; liberator.registerCallback("submit", modes.EX, function (command) { liberator.execute(command); }); - liberator.registerCallback("complete", modes.EX, function (str) { - completionContext.reset(); - completionContext.tabPressed = true; - completionContext.fork("ex", 0, completion, "ex"); - return completionContext.allItems; + liberator.registerCallback("complete", modes.EX, function (context) { + context.fork("ex", 0, completion, "ex"); }); liberator.registerCallback("change", modes.EX, function (command) { - completion.cancel(); // cancel any previous completion function if (options.get("wildoptions").has("auto")) autocompleteTimer.tell(false); else - completionIndex = UNINITIALIZED; + completions.selected = completions.RESET; }); function closePrompt(value) @@ -286,7 +345,7 @@ function CommandLine() //{{{ liberator.registerCallback("change", modes.PROMPT, function (str) { if (promptChangeCallback) return promptChangeCallback(str); }); liberator.registerCallback("complete", modes.PROMPT, - function (str) { if (promptCompleter) return promptCompleter(str); }); + function (context) { if (promptCompleter) promptCompleter(context); }); function setHighlightGroup(group) { @@ -322,21 +381,28 @@ function CommandLine() //{{{ } function previewSubstring() { - if (!options.get("wildoptions").has("auto") || !completionContext) + // This will only work with autocomplete. + if (!options.get("wildoptions").has("auto") || !completions) return; + let editor = commandWidget.inputField.editor; - let wildmode = options.get("wildmode"); - let wildType = wildmode.values[Math.min(wildIndex, wildmode.values.length - 1)]; - if (wildmode.checkHas(wildType, "longest") && commandWidget.selectionStart == commandWidget.value.length) + + if (completions.type.longest && !completions.suffix) { - // highlight= won't work here. let start = commandWidget.selectionStart; - let substring = completionContext.longestAllSubstring.substr(start - completionContext.allItems.start); - if (substring.length < 2 && substring != lastSubstring.substr(Math.max(0, lastSubstring.length - substring.length))) + let substring = completions.substring; + + // Don't show 1-character substrings unless we've just hit backspace + if (substring.length < 2 && lastSubstring.indexOf(substring) != 0) return; lastSubstring = substring; - let node = {substring} - editor.insertNode(util.xmlToDom(node, document), editor.rootElement, 1); + // Chop off the bits we already have. + substring = substring.substr(completions.value.length); + + // highlight="Preview" won't work in the editor. + let node = util.xmlToDom({substring}, + document); + editor.insertNode(node, editor.rootElement, 1); commandWidget.selectionStart = commandWidget.selectionEnd = start; } } @@ -527,6 +593,7 @@ function CommandLine() //{{{ completer: function completer(filter) { return [ + // Why do we need ""? ["", "Complete only the first match"], ["full", "Complete the next full match"], ["longest", "Complete to longest common string"], @@ -677,7 +744,7 @@ function CommandLine() //{{{ // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence APPEND_TO_MESSAGES : 1 << 3, // add the string to the message history - get completionContext() completionContext, + get completionContext() completions.context, get mode() (modes.extended == modes.EX) ? "cmd" : "search", @@ -692,7 +759,12 @@ function CommandLine() //{{{ getCommand: function getCommand() { - return commandWidget.inputField.editor.rootElement.firstChild.textContent; + try + { + return commandWidget.inputField.editor.rootElement.firstChild.textContent; + } + catch (e) {} + return commandWidget.value; }, open: function open(prompt, cmd, extendedMode) @@ -704,7 +776,6 @@ function CommandLine() //{{{ currentExtendedMode = extendedMode || null; historyIndex = UNINITIALIZED; - completionIndex = UNINITIALIZED; modes.set(modes.COMMAND_LINE, currentExtendedMode); setHighlightGroup(this.HL_NORMAL); @@ -713,11 +784,8 @@ function CommandLine() //{{{ commandWidget.focus(); - completionContext = CompletionContext(commandWidget.inputField.editor); - completionContext.onUpdate = function () - { - commandline.setCompletions(this.allItems); - }; + completions = new Completions(CompletionContext(commandWidget.inputField.editor)); + // open the completion list automatically if wanted if (/\s/.test(cmd) && options.get("wildoptions").has("auto") && @@ -889,7 +957,7 @@ function CommandLine() //{{{ event.stopPropagation(); // always reset the tab completion if we use up/down keys - completionIndex = UNINITIALIZED; + completions.select(completions.RESET); // save 'start' position for iterating through the history if (historyIndex == UNINITIALIZED) @@ -954,6 +1022,7 @@ function CommandLine() //{{{ { // reset the tab completion completionIndex = historyIndex = UNINITIALIZED; + completions.reset(); // and blur the command line if there is no text left if (command.length == 0) @@ -1239,55 +1308,16 @@ function CommandLine() //{{{ outputContainer.collapsed = false; }, - // to allow asynchronous adding of completions - setCompletions: function setCompletions(newCompletions) - { - if (liberator.mode != modes.COMMAND_LINE) - return; - - // don't show an empty result, if we are just waiting for data to arrive - // FIXME: Maybe. CompletionContext - //if (newCompletions.incompleteResult && newCompletions.items.length == 0) - // return; - - completionList.setItems(completionContext); - - // try to keep the old item selected - if (completionIndex >= 0 && completionIndex < newCompletions.items.length && completionIndex < completions.items.length) - { - if (newCompletions.items[completionIndex][0] != completions.items[completionIndex][0]) - completionIndex = -1; - } - else - completionIndex = -1; - - let oldStart = completions.start; - completions = newCompletions; - if (typeof completions.start != "number") - completions.start = oldStart; - - completionList.selectItem(completionIndex); - if (options.get("wildoptions").has("auto")) - completionList.show(); - - // why do we have to set that here? Without that, we lose the - // prefix when wrapping around searches - // with that, we SOMETIMES have problems with followed by in :open completions - - previewSubstring(); - let command = this.getCommand(); - completionPrefix = command.substring(0, commandWidget.selectionStart); - completionPostfix = command.substring(commandWidget.selectionStart); - }, - // TODO: does that function need to be public? resetCompletions: function resetCompletions() { autocompleteTimer.reset(); - completion.cancel(); - completions = { start: completions.start, items: [] }; - completionIndex = historyIndex = UNINITIALIZED; - wildIndex = 0; + if (completions) + { + completions.context.reset(); + completions.reset(); + } + historyIndex = UNINITIALIZED; removeSuffix = ""; } }; @@ -1509,12 +1539,19 @@ function ItemList(id) //{{{ }, visible: function visible() !container.collapsed, + reset: function () + { + startIndex = endIndex = selIndex = -1; + div = null; + this.selectItem(-1); + }, + // if @param selectedItem is given, show the list and select that item setItems: function setItems(newItems, selectedItem) { startIndex = endIndex = selIndex = -1; items = newItems; - init(); + this.reset(); if (typeof selectedItem == "number") { this.selectItem(selectedItem); @@ -1530,11 +1567,14 @@ function ItemList(id) //{{{ //let now = Date.now(); + if (div == null) + init(); + let sel = selIndex; let len = items.allItems.items.length; let newOffset = startIndex; - if (index == -1 || index == len) // wrapped around + if (index == -1 || index == null || index == len) // wrapped around { if (selIndex < 0) newOffset = 0;