/***** BEGIN LICENSE BLOCK ***** {{{ Version: MPL 1.1/GPL 2.0/LGPL 2.1 The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. (c) 2006-2008: Martin Stubenschrott Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. }}} ***** END LICENSE BLOCK *****/ /** @scope modules */ /** * This class is used for prompting of user input and echoing of messages * * it consists of a prompt and command field * be sure to only create objects of this class when the chrome is ready */ function CommandLine() //{{{ { //////////////////////////////////////////////////////////////////////////////// ////////////////////// PRIVATE SECTION ///////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ storage.newArray("history-search", true); storage.newArray("history-command", true); var messageHistory = { _messages: [], get messages() { let max = options["messages"]; // resize if 'messages' has changed if (this._messages.length > max) this._messages = this._messages.splice(this._messages.length - max); return this._messages; }, get length() this._messages.length, add: function add(message) { if (!message) return; if (this._messages.length >= options["messages"]) this._messages.shift(); this._messages.push(message); } }; var lastMowOutput = null; var silent = false; var keepCommand = false; var lastEcho = null; /** * A class for managing the history of an inputField * * @param {Object} inputField * @param {string} mode */ function History(inputField, mode) { if (!(this instanceof arguments.callee)) return new arguments.callee(inputField, mode); this.input = inputField; this.store = storage["history-" + mode]; this.reset(); } History.prototype = { /** * Empties the history. */ reset: function () { this.index = null; }, /** * Permanently save the history */ save: function () { let str = this.input.value; if (/^\s*$/.test(str)) return; this.store.mutate("filter", function (line) line != str); this.store.push(str); this.store.truncate(options["history"], true); }, /** * Set the current match to val * * @param {string} val */ replace: function (val) { this.input.value = val; liberator.triggerCallback("change", currentExtendedMode, val); }, /** * move up or (if backward) down in the history * * @param {boolean} backward * @param {boolean} matchCurrent XXX: what? */ select: function (backward, matchCurrent) { // always reset the tab completion if we use up/down keys completions.reset(); let diff = backward ? -1 : 1; if (this.index == null) { this.original = this.input.value; this.index = this.store.length; } // search the history for the first item matching the current // commandline string while (true) { this.index += diff; if (this.index < 0 || this.index > this.store.length) { this.index = Math.max(0, Math.min(this.store.length, this.index)); liberator.beep(); // I don't know why this kludge is needed. It // prevents the caret from moving to the end of // the input field. if (this.input.value == "") { this.input.value = " "; this.input.value = ""; } break; } let hist = this.store.get(this.index); // user pressed DOWN when there is no newer history item if (hist == null) hist = this.original; if (!matchCurrent || hist.substr(0, this.original.length) == this.original) { this.replace(hist); break; } } } }; /** * A class for tab completions on an input field * * @param {Object} input */ function Completions(input) { if (!(this instanceof arguments.callee)) return new arguments.callee(input); let self = this; this.context = CompletionContext(input.editor); this.context.onUpdate = function () { self._reset(); }; this.editor = input.editor; this.selected = null; this.wildmode = options.get("wildmode"); this.itemList = completionList; this.itemList.setItems(this.context); this.reset(); } Completions.prototype = { UP: {}, DOWN: {}, PAGE_UP: {}, PAGE_DOWN: {}, RESET: null, get completion() { let str = commandline.command; return str.substring(this.prefix.length, str.length - this.suffix.length); }, set completion set_completion(completion) { this.previewClear(); // 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; this.editor.selection.focusNode.textContent = commandWidget.value; // Reset the caret to one position after the completion. this.caret = this.prefix.length + completion.length; }, get caret() this.editor.selection.focusOffset, set caret(offset) { commandWidget.selectionStart = offset; commandWidget.selectionEnd = offset; }, 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") }), complete: function complete(show, tabPressed) { this.context.reset(); this.context.tabPressed = tabPressed; liberator.triggerCallback("complete", currentExtendedMode, this.context); this.context.updateAsync = true; this.reset(show, tabPressed); this.wildIndex = 0; }, preview: function preview() { this.previewClear(); if (this.wildIndex < 0 || this.suffix || !this.items.length) return; let substring = ""; switch (this.wildtype.replace(/.*:/, "")) { case "": substring = this.items[0].text; break; case "longest": if (this.items.length > 1) { substring = this.substring; break; } // Fallthrough case "full": let item = this.items[this.selected != null ? this.selected + 1 : 0]; if (item) substring = item.text; break; } // Don't show 1-character substrings unless we've just hit backspace if (substring.length < 2 && (!this.lastSubstring || this.lastSubstring.indexOf(substring) != 0)) return; this.lastSubstring = substring; let value = this.completion; if (util.compareIgnoreCase(value, substring.substr(0, value.length))) return; substring = substring.substr(value.length); this.removeSubstring = substring; let node = util.xmlToDom({substring}, document); let start = this.caret; this.editor.insertNode(node, this.editor.rootElement, 1); this.caret = start; }, previewClear: function previewClear() { let node = this.editor.rootElement.firstChild; if (node && node.nextSibling) this.editor.deleteNode(node.nextSibling); else if (this.removeSubstring) { let str = this.removeSubstring; let cmd = commandWidget.value; if (cmd.substr(cmd.length - str.length) == str) commandWidget.value = cmd.substr(0, cmd.length - str.length); } delete this.removeSubstring; }, reset: function reset(show) { this.wildIndex = -1; this.prefix = this.context.value.substring(0, this.start); this.value = this.context.value.substring(this.start, this.caret); this.suffix = this.context.value.substring(this.caret); if (show) { this.itemList.reset(); this.selected = null; this.wildIndex = 0; } this.wildtypes = this.wildmode.values; this.preview(); }, _reset: function _reset() { this.prefix = this.context.value.substring(0, this.start); this.value = this.context.value.substring(this.start, this.caret); this.suffix = this.context.value.substring(this.caret); this.itemList.reset(); this.itemList.selectItem(this.selected); this.preview(); }, 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.min(this.items.length - 1, idx)); break; } 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) { autocompleteTimer.flush(); // Check if we need to run the completer. if (this.context.waitingForTab || this.wildIndex == -1) this.complete(true, true); // Would prefer to only do this check when no completion // is available, but there are complications. if (this.items.length == 0 || this.context.incomplete) { // 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(true, true); 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; 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)); this.preview(); statusTimer.tell(); } } /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// TIMERS ////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var statusTimer = new Timer(5, 100, function statusTell() { if (completions.selected == null) statusline.updateProgress(""); else statusline.updateProgress("match " + (completions.selected + 1) + " of " + completions.items.length); }); var autocompleteTimer = new Timer(201, 500, function autocompleteTell(tabPressed) { if (events.feedingKeys || !completions) return; completions.complete(true, false); completions.itemList.show(); }); var tabTimer = new Timer(0, 0, function tabTell(event) { if (completions) completions.tab(event.shiftKey); }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// CALLBACKS /////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ // callback for prompt mode var promptSubmitCallback = null; var promptChangeCallback = null; var promptCompleter = null; liberator.registerCallback("submit", modes.EX, function (command) { liberator.execute(command); }); liberator.registerCallback("complete", modes.EX, function (context) { context.fork("ex", 0, completion, "ex"); }); liberator.registerCallback("change", modes.EX, function (command) { if (options.get("wildoptions").has("auto")) autocompleteTimer.tell(false); }); liberator.registerCallback("cancel", modes.PROMPT, closePrompt); liberator.registerCallback("submit", modes.PROMPT, closePrompt); liberator.registerCallback("change", modes.PROMPT, function (str) { liberator.triggerCallback("change", modes.EX, str); if (promptChangeCallback) return promptChangeCallback.call(commandline, str); }); liberator.registerCallback("complete", modes.PROMPT, function (context) { if (promptCompleter) context.fork("input", 0, commandline, promptCompleter); }); function closePrompt(value) { let callback = promptSubmitCallback; promptSubmitCallback = null; if (callback) callback.call(commandline, value == null ? commandline.command : value); } /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// VARIABLES /////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ const completionList = new ItemList("liberator-completions"); var completions = null; var history = null; var startHints = false; // whether we're waiting to start hints mode var lastSubstring = ""; // the containing box for the promptWidget and commandWidget const commandlineWidget = document.getElementById("liberator-commandline"); // the prompt for the current command, for example : or /. Can be blank const promptWidget = document.getElementById("liberator-commandline-prompt"); // the command bar which contains the current command const commandWidget = document.getElementById("liberator-commandline-command"); const messageBox = document.getElementById("liberator-message"); commandWidget.inputField.QueryInterface(Ci.nsIDOMNSEditableElement); messageBox.inputField.QueryInterface(Ci.nsIDOMNSEditableElement); // the widget used for multiline output const multilineOutputWidget = document.getElementById("liberator-multiline-output"); const outputContainer = multilineOutputWidget.parentNode; multilineOutputWidget.contentDocument.body.id = "liberator-multiline-output-content"; // the widget used for multiline intput const multilineInputWidget = document.getElementById("liberator-multiline-input"); // we need to save the mode which were in before opening the command line // this is then used if we focus the command line again without the "official" // way of calling "open" var currentExtendedMode = null; // the extended mode which we last openend the command line for // modules.__defineGetter__("currentExtendedMode", function () _currentExtendedMode) // modules.__defineSetter__("currentExtendedMode", function (val) (liberator.dumpStack("currentExtendedMode = " + (val && modes.getMode(val).name)), // _currentExtendedMode = val)) var currentPrompt = null; var currentCommand = null; // save the arguments for the inputMultiline method which are needed in the event handler var multilineRegexp = null; var multilineCallback = null; /** * @private - highlight the messageBox according to group */ function setHighlightGroup(group) { messageBox.setAttributeNS(NS.uri, "highlight", group); } /** * @private - Determines whether the command line should be visible. * * @return {boolean} */ function commandShown() modes.main == modes.COMMAND_LINE && !(modes.extended & (modes.INPUT_MULTILINE | modes.OUTPUT_MULTILINE)); /** * @private - set the prompt to val styled with highlightGroup * * @param {string} val * @param {string} highlightGroup */ function setPrompt(val, highlightGroup) { promptWidget.value = val; promptWidget.size = val.length; promptWidget.collapsed = (val == ""); promptWidget.setAttributeNS(NS.uri, "highlight", highlightGroup || commandline.HL_NORMAL); } /** * @private - set the command to cmd and move the user's cursor to the end. * * @param {string} cmd */ function setCommand(cmd) { commandWidget.value = cmd; commandWidget.selectionStart = cmd.length; commandWidget.selectionEnd = cmd.length; } /** * @private - display a message styled with highlightGroup * and, if forceSingle is true, ensure it takes only one line. * * @param {string} str * @param {string} highlightGroup * @param {boolean} forceSingle */ function echoLine(str, highlightGroup, forceSingle) { setHighlightGroup(highlightGroup); messageBox.value = str; liberator.triggerObserver("echoLine", str, highlightGroup, forceSingle); if (!commandShown()) commandline.hide(); let field = messageBox.inputField; if (!forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) echoMultiline({str}, highlightGroup); } /** * Display a multiline message, possible through a "more" like interface * * TODO: resize upon a window resize * * @param {string} str * @param {string} highlightGroup */ function echoMultiline(str, highlightGroup) { let doc = multilineOutputWidget.contentDocument; let win = multilineOutputWidget.contentWindow; liberator.triggerObserver("echoMultiline", str, highlightGroup); // If it's already XML, assume it knows what it's doing. // Otherwise, white space is significant. // The problem elsewhere is that E4X tends to insert new lines // after interpolated data. XML.ignoreWhitespace = typeof str != "xml"; lastMowOutput =
{template.maybeXML(str)}
; let output = util.xmlToDom(lastMowOutput, doc); XML.ignoreWhitespace = true; // FIXME: need to make sure an open MOW is closed when commands // that don't generate output are executed if (outputContainer.collapsed) doc.body.innerHTML = ""; doc.body.appendChild(output); commandline.updateOutputHeight(true); if (options["more"] && win.scrollMaxY > 0) { // start the last executed command's output at the top of the screen let elements = doc.getElementsByClassName("ex-command-output"); elements[elements.length - 1].scrollIntoView(true); } else { win.scrollTo(0, doc.height); } win.focus(); startHints = false; modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE); commandline.updateMorePrompt(); } /** * @private - ensure that the Multiline input widget is the * correct size. */ function autosizeMultilineInputWidget() { let lines = multilineInputWidget.value.split("\n").length - 1; multilineInputWidget.setAttribute("rows", Math.max(lines, 1)); } /** * @private - eval()s a javascript expression * and returns a string suitable to be echo'd. * * If useColor is true, util.objectToString will * colorize object output. * * @param {string} arg * @param {boolean} useColor */ function echoArgumentToString(arg, useColor) { if (!arg) return ""; try { arg = liberator.eval(arg); } catch (e) { liberator.echoerr(e); return null; } if (typeof arg === "object") arg = util.objectToString(arg, useColor); else if (typeof arg == "string" && /\n/.test(arg)) arg = {arg}; else arg = String(arg); return arg; } /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// OPTIONS ///////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ options.add(["history", "hi"], "Number of Ex commands and search patterns to store in the command-line history", "number", 500, { validator: function (value) value >= 0 }); options.add(["messages", "msgs"], "Number of messages to store in the message history", "number", 100, { validator: function (value) value >= 0 }); options.add(["more"], "Pause the message list window when more than one screen of listings is displayed", "boolean", true); options.add(["showmode", "smd"], "Show the current mode in the command line", "boolean", true); options.add(["suggestengines"], "Engine Alias which has a feature of suggest", "stringlist", "google", { completer: function completer(value) { let engines = services.get("browserSearch").getEngines({}) .filter(function (engine) engine.supportsResponseType("application/x-suggestions+json")); return engines.map(function (engine) [engine.alias, engine.description]); }, validator: Option.validateCompleter }); // TODO: these belong in ui.js options.add(["complete", "cpt"], "Items which are completed at the :[tab]open prompt", "charlist", "sfl", { completer: function completer(filter) [k for each (k in completion.urlCompleters)], validator: Option.validateCompleter }); options.add(["wildcase", "wic"], "Completion case matching mode", "string", "smart", { completer: function () [ ["smart", "Case is significant when capital letters are typed"], ["match", "Case is always significant"], ["ignore", "Case is never significant"] ], validator: Option.validateCompleter }); options.add(["wildignore", "wig"], "List of file patterns to ignore when completing files", "stringlist", "", { validator: function validator(values) { // TODO: allow for escaping the "," try { RegExp("^(" + values.join("|") + ")$"); return true; } catch (e) { return false; } } }); options.add(["wildmode", "wim"], "Define how command line completion works", "stringlist", "list:full", { 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"], ["list", "If more than one match, list all matches"], ["list:full", "List all and complete first match"], ["list:longest", "List all and complete common string"] ]; }, validator: Option.validateCompleter, checkHas: function (value, val) { let [first, second] = value.split(":", 2); return first == val || second == val; } }); options.add(["wildoptions", "wop"], "Change how command line completion is done", "stringlist", "", { completer: function completer(value) { return [ ["", "Default completion that won't show or sort the results"], ["auto", "Automatically show completions while you are typing"], ["sort", "Always sort the completion list"] ]; }, validator: Option.validateCompleter }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// MAPPINGS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var myModes = [modes.COMMAND_LINE]; // TODO: move "", ""], "Focus content", function () { events.onEscape(); }); mappings.add(myModes, [""], "Expand command line abbreviation", function () { commandline.resetCompletions(); return editor.expandAbbreviation("c"); }, { flags: Mappings.flags.ALLOW_EVENT_ROUTING }); mappings.add(myModes, ["", ""], "Expand command line abbreviation", function () { editor.expandAbbreviation("c"); }); mappings.add([modes.NORMAL], ["g<"], "Redisplay the last command output", function () { if (lastMowOutput) echoMultiline(lastMowOutput, commandline.HL_NORMAL); else liberator.beep(); }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMMANDS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var echoCommands = [ { name: "ec[ho]", description: "Echo the expression", action: liberator.echo }, { name: "echoe[rr]", description: "Echo the expression as an error message", action: liberator.echoerr }, { name: "echom[sg]", description: "Echo the expression as an informational message", action: liberator.echomsg } ]; echoCommands.forEach(function (command) { commands.add([command.name], command.description, function (args) { let str = echoArgumentToString(args.string, true); if (str != null) command.action(str); }, { completer: function (context) completion.javascript(context), literal: 0 }); }); commands.add(["mes[sages]"], "Display previously given messages", function () { // TODO: are all messages single line? Some display an aggregation // of single line messages at least. E.g. :source // FIXME: I retract my retraction, this command-line/MOW mismatch _is_ really annoying -- djk if (messageHistory.length == 1) { let message = messageHistory.messages[0]; commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE); } else if (messageHistory.length > 1) { XML.ignoreWhitespace = false; let list = template.map(messageHistory.messages, function (message)
{message.str}
); liberator.echo(list, commandline.FORCE_MULTILINE); } }, { argCount: "0" }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ return { HL_NORMAL : "Normal", HL_ERRORMSG : "ErrorMsg", HL_MODEMSG : "ModeMsg", HL_MOREMSG : "MoreMsg", HL_QUESTION : "Question", HL_INFOMSG : "InfoMsg", HL_WARNINGMSG : "WarningMsg", HL_LINENR : "LineNr", FORCE_MULTILINE : 1 << 0, FORCE_SINGLELINE : 1 << 1, DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line // but output nothing when the MOW is open; when also // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence APPEND_TO_MESSAGES : 1 << 3, // add the string to the message history get completionContext() completions.context, get mode() (modes.extended == modes.EX) ? "cmd" : "search", get silent() silent, set silent(val) { silent = val; if (silent) storage.styles.addSheet(true, "silent-mode", "chrome://*", "#liberator-commandline > * { opacity: 0 }"); else storage.styles.removeSheet(true, "silent-mode"); }, /** * XXX: This function is not used! */ runSilently: function (fn, self) { let wasSilent = this.silent; this.silent = true; try { fn.call(self); } finally { this.silent = wasSilent; } }, get command() { try { return commandWidget.inputField.editor.rootElement.firstChild.textContent; } catch (e) {} return commandWidget.value; }, set command(cmd) commandWidget.value = cmd, get message() messageBox.value, /** * Changes the command line to display the following prompt (usually ":") * followed by the command, in the given mode. Valid modes are * attributes of the "modes" variable, and modes.EX is probably * a good choice. * * @param {string} prompt * @param {string} cmd * @param {number} mode */ open: function open(prompt, cmd, extendedMode) { // save the current prompts, we need it later if the command widget // receives focus without calling the this.open() method currentPrompt = prompt || ""; currentCommand = cmd || ""; currentExtendedMode = extendedMode || null; keepCommand = false; setPrompt(currentPrompt); setCommand(currentCommand); commandlineWidget.collapsed = false; modes.set(modes.COMMAND_LINE, currentExtendedMode); commandWidget.focus(); history = History(commandWidget.inputField, (modes.extended == modes.EX) ? "command" : "search"); completions = Completions(commandWidget.inputField); // open the completion list automatically if wanted liberator.triggerCallback("change", currentExtendedMode, cmd); }, /** * Removes any input from the command line, without executing its * contents. Removes any "More" windows or other such output. * Pressing in EX mode normally has this effect. */ close: function close() { let mode = currentExtendedMode; currentExtendedMode = null; liberator.triggerCallback("cancel", mode); if (history) history.save(); this.resetCompletions(); // cancels any asynchronous completion still going on, must be before completions = null completions = null; history = null; statusline.updateProgress(""); // we may have a "match x of y" visible liberator.focusContent(false); multilineInputWidget.collapsed = true; completionList.hide(); if (!keepCommand || this.silent) { outputContainer.collapsed = true; commandline.updateMorePrompt(); this.hide(); } if (!outputContainer.collapsed) { modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE); commandline.updateMorePrompt(); } keepCommand = false; }, /** * Hide any auto-completion/More-ing that is happening. */ hide: function hide() { commandlineWidget.collapsed = true; }, /** * Output the given string onto the command line coloured * using the rules according to highlightGroup. If not * given higlightGroup defaults to commandline.HL_NORMAL * and other possibe values are at commandline.HL_[A-Z]*. * * Flags can be any of: * commandline.APPEND_TO_MESSAGES (causes message to be added to the messagesHistory) * commandline.FORCE_SINGLELINE | commandline.DISALLOW_MULTILINE * commandline.FORCE_MULTILINE * * @param {string} str * @param {string} highlightGroup * @param {number} flags */ echo: function echo(str, highlightGroup, flags) { // liberator.echo uses different order of flags as it omits the highlight group, change v.commandline.echo argument order? --mst if (silent) return false; highlightGroup = highlightGroup || this.HL_NORMAL; if (flags & this.APPEND_TO_MESSAGES) messageHistory.add({ str: str, highlight: highlightGroup }); // The DOM isn't threadsafe. It must only be accessed from the main thread. liberator.callInMainThread(function () { let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); let action = echoLine; if (!single && (!outputContainer.collapsed || messageBox.value == lastEcho)) { highlightGroup += " Message"; action = echoMultiline; } if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) action = echoMultiline; if ((flags & this.DISALLOW_MULTILINE) && !outputContainer.collapsed) return; if (single) lastEcho = null; else { if (messageBox.value == lastEcho) echoMultiline({lastEcho}, messageBox.getAttributeNS(NS.uri, "highlight")); lastEcho = (action == echoLine) && str; } if (action) action(str, highlightGroup, single); }, this); return true; }, /** * Prompt the user for a string and execute the given * callback with that as the only argument on * extra can have any of the following attributes: * * onChange: A function to be called with the current input every time it changes * completer: A function called with a ?context? when the user tries to tabcomplete * promptHighlight: The HighlightGroup to use (default commandline.HL_QUESTION, others * can be found at commandline.HL_[A-Z]*) * * This function sets the mode to modes.COMMAND_LINE, and thus popping the mode will * stop further input from being waited for (useful for stopping onChange) * * @param {string} prompt * @param {function(string)} callback * @param {Object} extra */ input: function input(prompt, callback, extra) { extra = extra || {}; promptSubmitCallback = callback; promptChangeCallback = extra.onChange; promptCompleter = extra.completer; modes.push(modes.COMMAND_LINE, modes.PROMPT); currentExtendedMode = modes.PROMPT; setPrompt(prompt, extra.promptHighlight || this.HL_QUESTION); setCommand(extra.default || ""); commandlineWidget.collapsed = false; commandWidget.focus(); completions = Completions(commandWidget.inputField); }, /** * Get a multiline input from a user, up to but not including * the line which matches the given regular expression. Then * execute the callback with that string as a parameter. * * @param {RegExp} untilRegexp * @param {function(string)} callbackFunc */ inputMultiline: function inputMultiline(untilRegexp, callbackFunc) { // Kludge. let cmd = !commandWidget.collapsed && this.command; modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE); if (cmd != false) echoLine(cmd, this.HL_NORMAL); // save the arguments, they are needed in the event handler onEvent multilineRegexp = untilRegexp; multilineCallback = callbackFunc; multilineInputWidget.collapsed = false; multilineInputWidget.value = ""; autosizeMultilineInputWidget(); setTimeout(function () { multilineInputWidget.focus(); }, 10); }, /** * Handle events, the come from liberator when liberator.mode = modes.COMMAND_LINE * but also takes blur/focus/input events raw from #liberator-commandline-command * in the XUL * * @param {Event} event */ onEvent: function onEvent(event) { let command = this.command; if (event.type == "blur") { // prevent losing focus, there should be a better way, but it just didn't work otherwise setTimeout(function () { if (commandShown() && event.originalTarget == commandWidget.inputField) commandWidget.inputField.focus(); }, 0); } else if (event.type == "focus") { if (!commandShown() && event.target == commandWidget.inputField) { event.target.blur(); liberator.beep(); } } else if (event.type == "input") { //this.resetCompletions(); -> already handled by "keypress" below (hopefully), so don't do it twice liberator.triggerCallback("change", currentExtendedMode, command); } else if (event.type == "keypress") { if (completions) completions.previewClear(); if (!currentExtendedMode) return true; let key = events.toString(event); //liberator.log("command line handling key: " + key + "\n"); // user pressed ENTER to carry out a command // user pressing ESCAPE is handled in the global onEscape // FIXME: should trigger "cancel" event if (events.isAcceptKey(key)) { let mode = currentExtendedMode; // save it here, as modes.pop() resets it keepCommand = true; currentExtendedMode = null; // Don't let modes.pop trigger "cancel" modes.pop(!this.silent); return liberator.triggerCallback("submit", mode, command); } // user pressed UP or DOWN arrow to cycle history completion else if (/^(|||||)$/.test(key)) { // prevent tab from moving to the next field event.preventDefault(); event.stopPropagation(); history.select(/Up/.test(key), !/(Page|S-)/.test(key)); return false; } // user pressed TAB to get completions of a command else if (key == "" || key == "") { // prevent tab from moving to the next field event.preventDefault(); event.stopPropagation(); tabTimer.tell(event); return false; } else if (key == "") { // reset the tab completion this.resetCompletions(); // and blur the command line if there is no text left if (command.length == 0) { liberator.triggerCallback("cancel", currentExtendedMode); modes.pop(); } } else // any other key { this.resetCompletions(); } return true; // allow this event to be handled by Firefox } else if (event.type == "keyup") { if (key == "" || key == "") tabTimer.flush(); } }, /** * Multiline input events, they will come straight from * #liberator-multiline-input in the XUL. * * @param {Event} event */ onMultilineInputEvent: function onMultilineInputEvent(event) { if (event.type == "keypress") { let key = events.toString(event); if (events.isAcceptKey(key)) { let text = multilineInputWidget.value.substr(0, multilineInputWidget.selectionStart); if (text.match(multilineRegexp)) { text = text.replace(multilineRegexp, ""); modes.pop(); multilineInputWidget.collapsed = true; multilineCallback.call(this, text); } } else if (events.isCancelKey(key)) { modes.pop(); multilineInputWidget.collapsed = true; } } else if (event.type == "blur") { if (modes.extended & modes.INPUT_MULTILINE) setTimeout(function () { multilineInputWidget.inputField.focus(); }, 0); } else if (event.type == "input") { autosizeMultilineInputWidget(); } return true; }, /** * Handle events when we are in multiline output mode, * these come from liberator when modes.extended & modes.MULTILINE_OUTPUT * and also from #liberator-multiline-output in the XUL * * FIXME: if 'more' is set and the MOW is not scrollable we should still * allow a down motion after an up rather than closing * * @param {Event} event */ onMultilineOutputEvent: function onMultilineOutputEvent(event) { let win = multilineOutputWidget.contentWindow; let showMoreHelpPrompt = false; let showMorePrompt = false; let closeWindow = false; let passEvent = false; function isScrollable() !win.scrollMaxY == 0; function atEnd() win.scrollY / win.scrollMaxY >= 1; if (event.type == "click") { if (event.target instanceof HTMLAnchorElement && event.button < 2) { event.preventDefault(); let target = event.button == 0 ? liberator.CURRENT_TAB : liberator.NEW_TAB; if (event.target.href == "#") liberator.open(String(event.target), target); else liberator.open(event.target.href, target); } return; } let key = events.toString(event); if (startHints) { statusline.updateInputBuffer(""); startHints = false; hints.show(key, undefined, win); return; } switch (key) { case "": closeWindow = true; break; // handled globally in events.js:onEscape() case ":": commandline.open(":", "", modes.EX); return; // down a line case "j": case "": if (options["more"] && isScrollable()) win.scrollByLines(1); else passEvent = true; break; case "": case "": case "": if (options["more"] && isScrollable() && !atEnd()) win.scrollByLines(1); else closeWindow = true; // don't propagate the event for accept keys break; // up a line case "k": case "": case "": if (options["more"] && isScrollable()) win.scrollByLines(-1); else if (options["more"] && !isScrollable()) showMorePrompt = true; else passEvent = true; break; // half page down case "d": if (options["more"] && isScrollable()) win.scrollBy(0, win.innerHeight / 2); else passEvent = true; break; // TODO: on the prompt line should scroll one page case "": if (event.originalTarget.getAttributeNS(NS.uri, "highlight") == "URL buffer-list") { tabs.select(parseInt(event.originalTarget.parentNode.parentNode.firstChild.textContent, 10) - 1); closeWindow = true; break; } else if (event.originalTarget.localName.toLowerCase() == "a") { liberator.open(event.originalTarget.textContent); break; } case "": // for those not owning a 3-button mouse case "": if (event.originalTarget.localName.toLowerCase() == "a") { let where = /\btabopen\b/.test(options["activate"]) ? liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB; liberator.open(event.originalTarget.textContent, where); } break; // let Firefox handle those to select table cells or show a context menu case "": case "": case "": break; // page down case "f": if (options["more"] && isScrollable()) win.scrollByPages(1); else passEvent = true; break; case "": case "": if (options["more"] && isScrollable() && !atEnd()) win.scrollByPages(1); else passEvent = true; break; // half page up case "u": // if (more and scrollable) if (options["more"] && isScrollable()) win.scrollBy(0, -(win.innerHeight / 2)); else passEvent = true; break; // page up case "b": if (options["more"] && isScrollable()) win.scrollByPages(-1); else if (options["more"] && !isScrollable()) showMorePrompt = true; else passEvent = true; break; case "": if (options["more"] && isScrollable()) win.scrollByPages(-1); else passEvent = true; break; // top of page case "g": if (options["more"] && isScrollable()) win.scrollTo(0, 0); else if (options["more"] && !isScrollable()) showMorePrompt = true; else passEvent = true; break; // bottom of page case "G": if (options["more"] && isScrollable() && !atEnd()) win.scrollTo(0, win.scrollMaxY); else passEvent = true; break; // copy text to clipboard case "": util.copyToClipboard(win.getSelection()); break; // close the window case "q": closeWindow = true; break; case ";": statusline.updateInputBuffer(";"); startHints = true; break; // unmapped key default: if (!options["more"] || !isScrollable() || atEnd() || events.isCancelKey(key)) passEvent = true; else showMoreHelpPrompt = true; } if (passEvent || closeWindow) { modes.pop(); if (passEvent) events.onKeyPress(event); } else // set update the prompt string { commandline.updateMorePrompt(showMorePrompt, showMoreHelpPrompt); } }, /** * Refresh or remove the prompt that displays when in multiline mode. * showHelp will cause the possible key-options to be displayed, * force will cause a display of the default message even if it * could be at the end of the output. * * @param {boolean} force * @param {boolean} showHelp */ updateMorePrompt: function updateMorePrompt(force, showHelp) { if (outputContainer.collapsed) return echoLine("", this.HL_NORMAL); let win = multilineOutputWidget.contentWindow; function isScrollable() !win.scrollMaxY == 0; function atEnd() win.scrollY / win.scrollMaxY >= 1; if (showHelp) echoLine("-- More -- SPACE/d/j: screen/page/line down, b/u/k: up, q: quit", this.HL_MOREMSG, true); else if (force || (options["more"] && isScrollable() && !atEnd())) echoLine("-- More --", this.HL_MOREMSG, true); else echoLine("Press ENTER or type command to continue", this.HL_QUESTION, true); }, /** * Changes the height of the multilineOutputWidget to fit * its contents, if open is true, it will cause the * widget to uncollapse, if not it will leave the widget * closed. * * @param {boolean} open */ updateOutputHeight: function updateOutputHeight(open) { if (!open && outputContainer.collapsed) return; let doc = multilineOutputWidget.contentDocument; // The container needs to be collapsed for this calculation to work. outputContainer.collapsed = true; let availableHeight = 250; try { availableHeight = getBrowser().mPanelContainer ? getBrowser().mPanelContainer.boxObject.height : getBrowser().boxObject.height; } catch (e) {} doc.body.style.minWidth = commandlineWidget.scrollWidth + "px"; outputContainer.height = Math.min(doc.height, availableHeight) + "px"; doc.body.style.minWidth = ""; outputContainer.collapsed = false; }, /** * Disable any active completion functions by calling their cancelFunc's * Will also remove the completions preview window. */ resetCompletions: function resetCompletions() { autocompleteTimer.reset(); // liberator.dump("Resetting completions..."); if (completions) { completions.context.cancelAll(); completions.wildIndex = -1; completions.previewClear(); } if (history) history.reset(); } }; //}}} }; //}}} /** * The list which is used for the completion box (and QuickFix window in future) * * @param {string} id The id of the XUL