From 423d0e9f70415f3578fdb68b42b2889628a87256 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 21 Nov 2008 22:06:33 +0000 Subject: [PATCH] Completion improvements, including basic completion contexts (very incomplete) --- content/completion.js | 157 ++++++++++++++++++++++++++++++++++-------- content/liberator.js | 6 +- content/style.js | 28 ++++---- content/ui.js | 26 ++++--- content/util.js | 2 +- 5 files changed, 160 insertions(+), 59 deletions(-) diff --git a/content/completion.js b/content/completion.js index 159af919..e1c8fc72 100644 --- a/content/completion.js +++ b/content/completion.js @@ -32,6 +32,82 @@ modules._cleanEval = function (__liberator_eval_arg, __liberator_eval_tmp) return window.eval(__liberator_eval_arg); } +function CompletionContext(editor, offset) +{ + if (editor instanceof arguments.callee) + { + let parent = editor; + this.parent = parent; + this.editor = parent.editor; + this.offset = parent.offset + (offset || 0); + this.__defineGetter__("tabPressed", function () this.parent.tabPressed); + this.contexts = this.parent.contexts; + } + else + { + this.editor = editor; + this.offset = offset || 0; + this.tabPressed = false; + this.contexts = {}; + } + this.selectionTypes = {}; +} +CompletionContext.prototype = { + get caret() this.editor.selection.getRangeAt(0).startOffset - this.offset, + + get filter() this.value.substr(this.offset, this.caret), + + get value() this.editor.rootElement.textContent, + + advance: function (count) + { + this.offset += count; + }, + + fork: function (name, offset) + { + if (!(name in this.contexts)) + this.contexts[name] = new CompletionContext(this); + this.contexts[name].offset = this.offset + offset; + return this.contexts[name]; + }, + + highlight: function (start, length, type) + { + try // Firefox <3.1 doesn't have repaintSelection + { + this.selectionTypes[type] = null; + const selType = Components.interfaces.nsISelectionController["SELECTION_" + type]; + const editor = this.editor; + let sel = editor.selectionController.getSelection(selType); + if (length == 0) + { + sel.removeAllRanges(); + editor.selectionController.repaintSelection(selType); + return; + } + + let range = editor.selection.getRangeAt(0).cloneRange(); + range.setStart(range.startContainer, this.offset + start); + range.setEnd(range.startContainer, this.offset + start + length); + sel.addRange(range); + editor.selectionController.repaintSelection(selType); + } + catch (e) {} + }, + + reset: function () + { + // Not ideal. + for (let type in this.selectionTypes) + this.highlight(0, 0, type); + this.selectionTypes = {}; + this.tabPressed = false; + this.offset = 0; + }, + +} + function Completion() //{{{ { //////////////////////////////////////////////////////////////////////////////// @@ -235,6 +311,7 @@ function Completion() //{{{ function buildStack(start) { + let self = this; /* Push and pop the stack, maintaining references to 'top' and 'last'. */ let push = function push(arg) { @@ -246,12 +323,12 @@ function Completion() //{{{ { if (top[CHAR] != arg) { - commandline.highlight(top[OFFSET] + 1, i + 1, "SPELLCHECK"); - commandline.highlight(top[OFFSET], top[OFFSET] + 1, "FIND"); + self.context.highlight(top[OFFSET] + 1, i - top[OFFSET], "SPELLCHECK"); + self.context.highlight(top[OFFSET], 1, "FIND"); throw new Error("Invalid JS"); } if (i == str.length - 1) - commandline.highlight(top[OFFSET], top[OFFSET] + 1, "FIND"); + self.context.highlight(top[OFFSET], 1, "FIND"); // The closing character of this stack frame will have pushed a new // statement, leaving us with an empty statement. This doesn't matter, // now, as we simply throw away the frame when we pop it, but it may later. @@ -354,20 +431,22 @@ function Completion() //{{{ lastIdx = i; } - this.complete = function complete(string) + this.complete = function complete(context) { - commandline.highlight(0, 0, "SPELLCHECK"); - commandline.highlight(0, 0, "FIND"); + this.context = context; + let string = context.filter; let self = this; try { continuing = lastIdx && string.indexOf(str) == 0; str = string; - buildStack(continuing ? lastIdx : 0); + buildStack.call(this, continuing ? lastIdx : 0); } catch (e) { + if (e.message != "Invalid JS") + Components.utils.reportError(e); // liberator.dump(util.escapeString(string) + ": " + e + "\n" + e.stack); lastIdx = 0; return [0, []]; @@ -891,12 +970,12 @@ function Completion() //{{{ return [0, completion.filter(schemes, filter)]; }, - command: function command(filter) + command: function command(context) { - if (!filter) - return [0, [[c.name, c.description] for (c in commands)]]; + if (!context.filter) + return { start: 0, items: [[c.name, c.description] for (c in commands)] }; else - return [0, this.filter([[c.longNames, c.description] for (c in commands)], filter, true)]; + return { start: 0, items: this.filter([[c.longNames, c.description] for (c in commands)], context.filter, true) }; }, dialog: function dialog(filter) [0, this.filter(config.dialogs, filter)], @@ -919,39 +998,57 @@ function Completion() //{{{ }, // provides completions for ex commands, including their arguments - ex: function ex(str) + ex: function ex(context) { this.filterMap = null; this.filterString = ""; - this.parenMatch = null; substrings = []; - if (str.indexOf(cacheFilter["ex"]) != 0) + if (context.filter.indexOf(cacheFilter["ex"]) != 0) { cacheFilter = {}; cacheResults = {}; } - cacheFilter["ex"] = str; + cacheFilter["ex"] = context.filter; // if there is no space between the command name and the cursor // then get completions of the command name - var [count, cmd, special, args] = commands.parseCommand(str); - var matches = str.match(/^(:*\d*)\w*$/); - if (matches) - return { start: matches[1].length, items: this.command(cmd)[1] }; + let [count, cmd, special, args] = commands.parseCommand(context.filter); + let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || []; + context.advance(junk.length) + if (!junk) + return this.command(context); // dynamically get completions as specified with the command's completer function - var compObject = { start: 0, completions: [] }; - var exLength = 0; - var command = commands.get(cmd); + let command = commands.get(cmd); + let compObject = { start: 0, items: [] }; if (command && command.completer) { - matches = str.match(/^:*\d*(?:\w+[\s!]|!)\s*/); - exLength = matches ? matches[0].length : 0; - compObject = command.completer.call(command, args, special); - if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects - compObject = { start: compObject[0], items: compObject[1] }; + [prefix] = context.filter.match(/^(?:\w+[\s!]|!)\s*/); + context.advance((prefix || "").length); + args = command.parseArgs(context.filter, true); + liberator.dump(args); + if (args) + { + // XXX, XXX, XXX + compObject = command.completer.call(command, args.string, special, args, context); + liberator.dump(compObject); + if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects + compObject = { start: compObject[0], items: compObject[1] }; + if (compObject == null) + compObject = { start: context.offset, items: context.items }; + else + compObject.start += context.offset; + if (args.completions) + { + if (!compObject.items.length) + compObject.start = args.completeStart + context.offset; + if (args.completeStart + context.offset == compObject.start) + compObject.items = args.completions.concat(compObject.items); + } + liberator.dump(compObject); + liberator.dump("\n"); + } } - compObject.start += exLength; return compObject; }, @@ -1025,9 +1122,9 @@ function Completion() //{{{ get javascriptCompleter() javascript, - javascript: function _javascript(str) + javascript: function (context) { - return javascript.complete(str); + return javascript.complete(context); }, macro: function macro(filter) diff --git a/content/liberator.js b/content/liberator.js index 4afd8c74..b9636e71 100644 --- a/content/liberator.js +++ b/content/liberator.js @@ -336,7 +336,7 @@ const liberator = (function () //{{{ }, { bang: true, - completer: function (filter) completion.javascript(filter), + completer: function (filter, bang, args, context) completion.javascript(context), hereDoc: true }); @@ -1161,10 +1161,6 @@ const liberator = (function () //{{{ liberator.log("All modules loaded", 3); - // TODO: move elsewhere - liberator.registerCallback("submit", modes.EX, function (command) { liberator.execute(command); }); - liberator.registerCallback("complete", modes.EX, function (str) { return completion.ex(str); }); - // first time intro message const firstTime = "extensions." + config.name.toLowerCase() + ".firsttime"; if (options.getPref(firstTime, true)) diff --git a/content/style.js b/content/style.js index 666fdd38..aabb000b 100644 --- a/content/style.js +++ b/content/style.js @@ -429,20 +429,20 @@ liberator.registerObserver("load_commands", function () { argCount: "2", bang: true, - completer: function (filter) { - let args = this.parseArgs(filter, true); - let compl = args ? args.completions : []; - if (args && args.completeOpt) - return [args.completeStart, compl]; - - try + completer: function (filter, bang, args) { + let compl = []; + if (args.completeArg == 0) { - compl.push([content.location.host, "Current Host"]); - compl.push([content.location.href, "Current URL"]); + try + { + compl.push([content.location.host, "Current Host"]); + compl.push([content.location.href, "Current URL"]); + } + catch (e) {} + comp = compl.concat([[s, ""] for each (s in styles.sites)]) + return [0, completion.filter(compl, args.arguments[0])]; } - catch (e) {} - comp = compl.concat([[s, ""] for each (s in styles.sites)]) - return [0, completion.filter(compl, filter)]; + return [0, []]; }, hereDoc: true, literal: true, @@ -473,8 +473,8 @@ liberator.registerObserver("load_commands", function () .concat([[s, ""] for each (s in styles.sites)]) , filter)], literal: true, - options: [[["-index", "-i"], commands.OPTION_INT], - [["-name", "-n"], commands.OPTION_STRING]] + options: [[["-index", "-i"], commands.OPTION_INT, null, function () [[k, v.name || v.sites.join(",") + " " + v.css] for ([k, v] in Iterator(styles.userNames))]], + [["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]]] }); commands.add(["hi[ghlight]"], diff --git a/content/ui.js b/content/ui.js index 7b4e5e62..481d7631 100644 --- a/content/ui.js +++ b/content/ui.js @@ -98,6 +98,7 @@ function CommandLine() //{{{ 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" @@ -112,11 +113,11 @@ function CommandLine() //{{{ else statusline.updateProgress("match " + (completionIndex + 1) + " of " + completions.items.length); }); - var autocompleteTimer = new util.Timer(201, 300, function (command) { + var autocompleteTimer = new util.Timer(201, 300, function (tabPressed) { if (events.feedingKeys) return; - - commandline.setCompletions(completion.ex(command)); + completionContext.reset(); + commandline.setCompletions(completion.ex(completionContext)); }); // the containing box for the promptWidget and commandWidget @@ -151,10 +152,16 @@ function CommandLine() //{{{ var promptChangeCallback = null; 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; + return completion.ex(completionContext); + }); liberator.registerCallback("change", modes.EX, function (command) { completion.cancel(); // cancel any previous completion function if (options.get("wildoptions").has("auto")) - autocompleteTimer.tell(command); + autocompleteTimer.tell(false); else completionIndex = UNINITIALIZED; }); @@ -209,9 +216,7 @@ function CommandLine() //{{{ setPrompt(""); setCommand(str); if (!forceSingle && - commandWidget.inputField.editor - .selection.getRangeAt(0) - .startContainer.parentNode + commandWidget.inputField.editor.rootElement .scrollWidth > commandWidget.inputField.scrollWidth) { setCommand(""); @@ -493,7 +498,7 @@ function CommandLine() //{{{ if (str != null) command.action(str); }, - { completer: function (filter) completion.javascript(filter) }); + { completer: function (filter, bang, args, context) completion.javascript(context) }); }); commands.add(["mes[sages]"], @@ -542,6 +547,8 @@ 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 mode() (modes.extended == modes.EX) ? "cmd" : "search", get silent() silent, @@ -576,11 +583,12 @@ function CommandLine() //{{{ commandWidget.focus(); + completionContext = new CompletionContext(commandWidget.inputField.editor); // open the completion list automatically if wanted if (/\s/.test(cmd) && options.get("wildoptions").has("auto") && extendedMode == modes.EX) - autocompleteTimer.tell(cmd); + autocompleteTimer.tell(false); }, // normally used when pressing esc, does not execute a command diff --git a/content/util.js b/content/util.js index 64ee3570..015cde54 100644 --- a/content/util.js +++ b/content/util.js @@ -336,7 +336,7 @@ const util = { //{{{ i = parseInt(i); else if (/^[A-Z_]+$/.test(i)) i = ""; - keys.push([i, <>{key}{noVal ? "" : <>:{value}}
]); + keys.push([i, <>{key}{noVal ? "" : <>: {value}}
]); } } catch (e) {}