diff --git a/.hgignore b/.hgignore index 40d211a3..93349ece 100644 --- a/.hgignore +++ b/.hgignore @@ -11,6 +11,8 @@ syntax: glob */locale/*/*.html */chrome */contrib/vim/*.vba +*/bak/* +downloads/* ## Editor backup and swap files *~ diff --git a/common/Makefile b/common/Makefile index a918fc96..95441a2e 100644 --- a/common/Makefile +++ b/common/Makefile @@ -99,8 +99,8 @@ dist: $(XPI) $(RDF): $(RDF_IN) Makefile @echo "Preparing release..." - $(SED) -e "s,###VERSION###,$(VERSION),g" \ - -e "s,###DATE###,$(BUILD_DATE),g" \ + $(SED) -e "s,@VERSION@,$(VERSION),g" \ + -e "s,@DATE@,$(BUILD_DATE),g" \ < $< > $@ @echo "SUCCESS: $@" diff --git a/common/components/protocols.js b/common/components/protocols.js index 450c346e..fd6378d1 100644 --- a/common/components/protocols.js +++ b/common/components/protocols.js @@ -2,7 +2,7 @@ // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /* Adds support for data: URIs with chrome privileges * and fragment identifiers. @@ -27,7 +27,7 @@ let channel = Components.classesByID["{61ba33c0-3031-11d3-8cd0-0060b0fc14a3}"] .QueryInterface(Ci.nsIRequest); const systemPrincipal = channel.owner; channel.cancel(NS_BINDING_ABORTED); -delete channel; +channel = null; function dataURL(type, data) "data:" + (type || "application/xml;encoding=UTF-8") + "," + escape(data); function makeChannel(url, orig) { @@ -152,8 +152,9 @@ Liberator.prototype = { } }; -var components = [ChromeData, Liberator]; - -function NSGetModule(compMgr, fileSpec) XPCOMUtils.generateModule(components) +if (XPCOMUtils.generateNSGetFactory) + const NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeData, Liberator]); +else + const NSGetModule = XPCOMUtils.generateNSGetModule([ChromeData, Liberator]); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/autocommands.js b/common/content/autocommands.js index c0ce9252..e8eb47f7 100755 --- a/common/content/autocommands.js +++ b/common/content/autocommands.js @@ -1,8 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -274,9 +276,9 @@ const AutoCommands = Module("autocommands", { completer: function () config.autocommands.concat([["all", "All events"]]) }); - options.add(["focuscontent", "fc"], - "Try to stay in normal mode after loading a web page", - "boolean", false); + options.add(["strictfocus", "sf"], + "Prevent scripts from focusing input elements without user intervention", + "boolean", true); } }); diff --git a/common/content/base.js b/common/content/base.js index 16a6422d..4eab72e7 100644 --- a/common/content/base.js +++ b/common/content/base.js @@ -2,6 +2,7 @@ // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; const Cc = Components.classes; const Ci = Components.interfaces; diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index 077c1e8c..eea59c69 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -1,8 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; @@ -364,7 +366,6 @@ const Bookmarks = Module("bookmarks", { // ripped from Firefox function getShortcutOrURI(url) { - var shortcutURL = null; var keyword = url; var param = ""; var offset = url.indexOf(" "); @@ -379,7 +380,7 @@ const Bookmarks = Module("bookmarks", { return [submission.uri.spec, submission.postData]; } - [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); + let [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); if (!shortcutURL) return [url, null]; @@ -598,6 +599,18 @@ const Bookmarks = Module("bookmarks", { context.completions = [["", "Don't perform searches by default"]].concat(context.completions); } }); + + 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]); + } + }); }, completion: function () { completion.bookmark = function bookmark(context, tags, extra) { @@ -643,8 +656,11 @@ const Bookmarks = Module("bookmarks", { let rest = item.url.length - end.length; let query = item.url.substring(begin.length, rest); if (item.url.substr(rest) == end && query.indexOf("&") == -1) { - item.url = decodeURIComponent(query.replace(/#.*/, "")); - return item; + try { + item.url = decodeURIComponent(query.replace(/#.*/, "")); + return item; + } + catch (e) {} } return null; }).filter(util.identity); diff --git a/common/content/browser.js b/common/content/browser.js index aec8bf25..1ceb716c 100644 --- a/common/content/browser.js +++ b/common/content/browser.js @@ -1,10 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2009 by Doug Kearns // Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ diff --git a/common/content/buffer.js b/common/content/buffer.js index d70de3e5..be2362bf 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -1,10 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2009 by Doug Kearns // Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -19,9 +19,10 @@ const Point = Struct("x", "y"); * @instance buffer */ const Buffer = Module("buffer", { - requires: ["config"], + requires: ["config", "util"], init: function () { + this.evaluateXPath = util.evaluateXPath; this.pageInfo = {}; this.addPageInfoSection("f", "Feeds", function (verbose) { @@ -168,7 +169,7 @@ const Buffer = Module("buffer", { // called when the active document is scrolled _updateBufferPosition: function _updateBufferPosition() { statusline.updateBufferPosition(); - modes.show(); + modes.show(); // Clear the status line. }, onDOMContentLoaded: function onDOMContentLoaded(event) { @@ -199,19 +200,7 @@ const Buffer = Module("buffer", { // any buffer, even in a background tab doc.pageIsFullyLoaded = 1; - // code which is only relevant if the page load is the current tab goes here: - if (doc == config.browser.contentDocument) { - // we want to stay in command mode after a page has loaded - // TODO: move somewhere else, as focusing can already happen earlier than on "load" - if (options["focuscontent"]) { - setTimeout(function () { - let focused = liberator.focus; - if (focused && (focused.value != null) && focused.value.length == 0) - focused.blur(); - }, 0); - } - } - else // background tab + if (doc != config.browser.contentDocument) liberator.echomsg("Background tab loaded: " + doc.title || doc.location.href, 3); this._triggerLoadAutocmd("PageLoad", doc); @@ -279,7 +268,11 @@ const Buffer = Module("buffer", { autocommands.trigger("LocationChange", { url: buffer.URL }); // if this is not delayed we get the position of the old buffer - setTimeout(function () { statusline.updateBufferPosition(); }, 500); + setTimeout(function () { + statusline.updateBufferPosition(); + statusline.updateZoomLevel(); + modes.show(); // Clear the status line. + }, 500); }, // called at the very end of a page load asyncUpdateUI: function () { @@ -384,19 +377,18 @@ const Buffer = Module("buffer", { get pageHeight() window.content.innerHeight, /** - * @property {number} The current browser's text zoom level, as a - * percentage with 100 as 'normal'. Only affects text size. + * @property {number} The current browser's zoom level, as a + * percentage with 100 as 'normal'. */ - get textZoom() config.browser.markupDocumentViewer.textZoom * 100, - set textZoom(value) { Buffer.setZoom(value, false); }, + get zoomLevel() config.browser.markupDocumentViewer[this.fullZoom ? "textZoom" : "fullZoom"] * 100, + set zoomLevel(value) { Buffer.setZoom(value, this.fullZoom); }, /** - * @property {number} The current browser's text zoom level, as a - * percentage with 100 as 'normal'. Affects text size, as well as - * image size and block size. + * @property {boolean} Whether the current browser is using full + * zoom, as opposed to text zoom. */ - get fullZoom() config.browser.markupDocumentViewer.fullZoom * 100, - set fullZoom(value) { Buffer.setZoom(value, true); }, + get fullZoom() ZoomManager.useFullZoom, + set fullZoom(value) { Buffer.setZoom(this.zoomLevel, value); }, /** * @property {string} The current document's title. @@ -470,6 +462,18 @@ const Buffer = Module("buffer", { return String(selection); }, + /** + * Returns true if a scripts are allowed to focus the given input + * element or input elements in the given window. + * + * @param {Node|Window} + * @returns {boolean} + */ + focusAllowed: function (elem) { + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + return !options["strictfocus"] || elem.liberatorFocusAllowed; + }, + /** * Focuses the given element. In contrast to a simple * elem.focus() call, this function works for iframes and @@ -479,7 +483,10 @@ const Buffer = Module("buffer", { */ focusElement: function (elem) { let doc = window.content.document; - if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement) + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + win.liberatorFocusAllowed = true; + + if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement])) elem.contentWindow.focus(); else if (elem instanceof HTMLInputElement && elem.type == "file") { Buffer.openUploadPrompt(elem); @@ -960,14 +967,20 @@ const Buffer = Module("buffer", { liberator.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX, "Zoom value out of range (" + Buffer.ZOOM_MIN + " - " + Buffer.ZOOM_MAX + "%)"); - ZoomManager.useFullZoom = fullZoom; + if (fullZoom !== undefined) + ZoomManager.useFullZoom = fullZoom; ZoomManager.zoom = value / 100; + if ("FullZoom" in window) FullZoom._applySettingToPref(); - liberator.echomsg((fullZoom ? "Full" : "Text") + " zoom: " + value + "%"); + + statusline.updateZoomLevel(value, ZoomManager.useFullZoom); }, bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) { + if (fullZoom === undefined) + fullZoom = ZoomManager.useFullZoom; + let values = ZoomManager.zoomValues; let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom)); let i = util.Math.constrain(cur + steps, 0, values.length - 1); @@ -1138,8 +1151,8 @@ const Buffer = Module("buffer", { }, { argCount: "?", - literal: 0, - bang: true + bang: true, + literal: 0 }); commands.add(["pa[geinfo]"], @@ -1178,8 +1191,8 @@ const Buffer = Module("buffer", { "Reload the current web page", function (args) { tabs.reload(config.browser.mCurrentTab, args.bang); }, { - bang: true, - argCount: "0" + argCount: "0", + bang: true }); // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional? @@ -1470,16 +1483,19 @@ const Buffer = Module("buffer", { if (count < 1 && buffer.lastInputField) buffer.focusElement(buffer.lastInputField); else { - let xpath = ["input[not(@type) or @type='text' or @type='password' or @type='file']", - "textarea[not(@disabled) and not(@readonly)]"]; + let xpath = ["input", "textarea[not(@disabled) and not(@readonly)]"]; - let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (match) { - let computedStyle = util.computedStyle(match); + let elements = [m for (m in util.evaluateXPath(xpath))].filter(function (elem) { + if (elem.readOnly || elem instanceof HTMLInputElement && ["file", "search", "text", "password"].indexOf(elem.type) < 0) + return false; + let computedStyle = util.computedStyle(elem); return computedStyle.visibility != "hidden" && computedStyle.display != "none"; }); liberator.assert(elements.length > 0); - buffer.focusElement(elements[util.Math.constrain(count, 1, elements.length) - 1]); + let elem = elements[util.Math.constrain(count, 1, elements.length) - 1]; + buffer.focusElement(elem); + util.scrollIntoView(elem); } }, { count: true }); @@ -1504,7 +1520,7 @@ const Buffer = Module("buffer", { function () { let url = util.readFromClipboard(); liberator.assert(url); - liberator.open(url, { from: "activate", where: liberator.NEW_TAB }); + liberator.open(url, { from: "paste", where: liberator.NEW_TAB }); }); // reloading @@ -1551,27 +1567,27 @@ const Buffer = Module("buffer", { function (count) { buffer.textZoom = count > 1 ? count : 100; }, { count: true }); - mappings.add(myModes, ["zI"], + mappings.add(myModes, ["ZI", "zI"], "Enlarge full zoom of current web page", function (count) { buffer.zoomIn(Math.max(count, 1), true); }, { count: true }); - mappings.add(myModes, ["zM"], + mappings.add(myModes, ["ZM", "zM"], "Enlarge full zoom of current web page by a larger amount", function (count) { buffer.zoomIn(Math.max(count, 1) * 3, true); }, { count: true }); - mappings.add(myModes, ["zO"], + mappings.add(myModes, ["ZO", "zO"], "Reduce full zoom of current web page", function (count) { buffer.zoomOut(Math.max(count, 1), true); }, { count: true }); - mappings.add(myModes, ["zR"], + mappings.add(myModes, ["ZR", "zR"], "Reduce full zoom of current web page by a larger amount", function (count) { buffer.zoomOut(Math.max(count, 1) * 3, true); }, { count: true }); - mappings.add(myModes, ["zZ"], + mappings.add(myModes, ["ZZ", "zZ"], "Set full zoom value of current web page", function (count) { buffer.fullZoom = count > 1 ? count : 100; }, { count: true }); @@ -1599,7 +1615,7 @@ const Buffer = Module("buffer", { "Desired info in the :pageinfo output", "charlist", "gfm", { - completer: function (context) [[k, v[1]] for ([k, v] in Iterator(this.pageInfo))] + completer: function (context) [[k, v[1]] for ([k, v] in Iterator(buffer.pageInfo))] }); options.add(["scroll", "scr"], diff --git a/common/content/commandline.js b/common/content/commandline.js index 79a4b4f2..e4c29de2 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1,7 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; /** @scope modules */ @@ -102,7 +105,7 @@ const CommandLine = Module("commandline", { }); this._autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) { - if (!events.feedingKeys && self._completions && options.get("wildoptions").has("auto")) { + if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { self._completions.complete(true, false); self._completions.itemList.show(); } @@ -329,10 +332,11 @@ const CommandLine = Module("commandline", { FORCE_MULTILINE : 1 << 0, FORCE_SINGLELINE : 1 << 1, - DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line + 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 this._history + APPEND_TO_MESSAGES : 1 << 3, // Add the string to the message this._history. + ACTIVE_WINDOW : 1 << 4, // Only echo in active window. get completionContext() this._completions.context, @@ -499,6 +503,10 @@ const CommandLine = Module("commandline", { if (flags & this.APPEND_TO_MESSAGES) this._messageHistory.add({ str: str, highlight: highlightGroup }); + if ((flags & this.ACTIVE_WINDOW) && + window != services.get("windowWatcher").activeWindow && + services.get("windowWatcher").activeWindow.liberator) + return; // The DOM isn't threadsafe. It must only be accessed from the main thread. liberator.callInMainThread(function () { @@ -754,12 +762,12 @@ const CommandLine = Module("commandline", { case "": case "": case "": - openLink(liberator.NEW_BACKGROUND_TAB); + openLink({ where: liberator.NEW_TAB, background: true }); break; case "": case "": case "": - openLink(liberator.NEW_TAB); + openLink({ where: liberator.NEW_TAB, background: false }); break; case "": openLink(liberator.NEW_WINDOW); @@ -964,7 +972,7 @@ const CommandLine = Module("commandline", { let doc = this._multilineOutputWidget.contentDocument; - availableHeight = config.outputHeight; + let availableHeight = config.outputHeight; if (!this._outputContainer.collapsed) availableHeight += parseFloat(this._outputContainer.height); doc.body.style.minWidth = this._commandlineWidget.scrollWidth + "px"; @@ -1032,7 +1040,7 @@ const CommandLine = Module("commandline", { sanitize: function (timespan) { let range = [0, Number.MAX_VALUE]; if (liberator.has("sanitizer") && (timespan || options["sanitizetimespan"])) - range = sanitizer.getClearRange(timespan || options["sanitizetimespan"]); + range = Sanitizer.getClearRange(timespan || options["sanitizetimespan"]); const self = this; this.store.mutate("filter", function (item) { @@ -1107,7 +1115,7 @@ const CommandLine = Module("commandline", { */ Completions: Class("Completions", { init: function (input) { - this.context = CompletionContext(input.editor); + this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor); this.context.onUpdate = this.closure._reset; this.editor = input.editor; this.selected = null; @@ -1127,7 +1135,7 @@ const CommandLine = Module("commandline", { let str = commandline.command; return str.substring(this.prefix.length, str.length - this.suffix.length); }, - set completion set_completion(completion) { + set completion(completion) { this.previewClear(); // Change the completion text. @@ -1516,84 +1524,6 @@ const CommandLine = Module("commandline", { 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]); - } - }); - - options.add(["complete", "cpt"], - "Items which are completed at the :open prompts", - "charlist", typeof(config.defaults["complete"]) == "string" ? config.defaults["complete"] : "slf", - { - completer: function (context) array(values(completion.urlCompleters)) - }); - - 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"] - ] - }); - - 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 (context) [ - // 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"] - ], - 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 this._completions while you are typing"], - ["sort", "Always sort the completion list"] - ]; - } - }); }, styles: function () { let fontSize = util.computedStyle(document.getElementById(config.mainWindowId)).fontSize; diff --git a/common/content/commands.js b/common/content/commands.js index 0b15f55e..c774bc28 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -1,10 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2009 by Doug Kearns // Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -73,9 +73,9 @@ const Command = Class("Command", { modifiers = modifiers || {}; let self = this; - function exec(args) { + function exec(command) { // FIXME: Move to parseCommand? - args = self.parseArgs(args); + args = self.parseArgs(command); if (!args) return; args.count = count; @@ -237,6 +237,7 @@ const ArgType = Struct("description", "parse"); const Commands = Module("commands", { init: function () { this._exCommands = []; + this._exMap = {}; }, // FIXME: remove later, when our option handler is better @@ -304,7 +305,7 @@ const Commands = Module("commands", { repeat: null, _addCommand: function (command, replace) { - if (this._exCommands.some(function (c) c.hasName(command.name))) { + if (command.name in this._exMap) { if (command.user && replace) commands.removeUserCommand(command.name); else { @@ -314,6 +315,8 @@ const Commands = Module("commands", { } this._exCommands.push(command); + for(let [,name] in Iterator(command.names)) + this._exMap[name] = command; return true; }, @@ -387,7 +390,7 @@ const Commands = Module("commands", { * @returns {Command} */ get: function (name) { - return this._exCommands.filter(function (cmd) cmd.hasName(name))[0] || null; + return this._exMap[name] || this._exCommands.filter(function (cmd) cmd.hasName(name))[0] || null; }, /** @@ -762,6 +765,10 @@ const Commands = Module("commands", { * any of the command's names. */ removeUserCommand: function (name) { + for(let [,cmd] in Iterator(this._exCommands)) + if(cmd.user && cmd.hasName(name)) + for(let [,name] in Iterator(cmd.names)) + delete this._exMap[name]; this._exCommands = this._exCommands.filter(function (cmd) !(cmd.user && cmd.hasName(name))); }, @@ -894,10 +901,10 @@ const Commands = Module("commands", { } [prefix] = context.filter.match(/^(?:\w*[\s!]|!)\s*/); - let cmdContext = context.fork(cmd, prefix.length); + let cmdContext = context.fork(command.name, prefix.length); let argContext = context.fork("args", prefix.length); args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang }); - if (args) { + if (args && !cmdContext.waitingForTab) { // FIXME: Move to parseCommand args.count = count; args.bang = bang; diff --git a/common/content/completion.js b/common/content/completion.js index ed7e3cfd..d86d6a18 100755 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -1,9 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott -// Copyright (c) 2008-2009 by Kris Maglione +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -34,6 +35,11 @@ const CompletionContext = Class("CompletionContext", { if (editor instanceof this.constructor) { let parent = editor; name = parent.name + "/" + name; + + this.autoComplete = options.get("autocomplete").getKey(name); + this.sortResults = options.get("wildsort").getKey(name); + this.wildcase = options.get("wildcase").getKey(name); + this.contexts = parent.contexts; if (name in this.contexts) self = this.contexts[name]; @@ -146,6 +152,8 @@ const CompletionContext = Class("CompletionContext", { this.top = this; this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete)); this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab)); + this.__defineSetter__("incomplete", function (val) {}); + this.__defineSetter__("waitingForTab", function (val) {}); this.reset(); } /** @@ -243,7 +251,7 @@ const CompletionContext = Class("CompletionContext", { get completions() this._completions || [], set completions(items) { // Accept a generator - if ({}.toString.call(items) != '[object Array]') + if (!isarray(items)) items = [x for (x in Iterator(items))]; delete this.cache.filtered; delete this.cache.filter; @@ -333,7 +341,7 @@ const CompletionContext = Class("CompletionContext", { get ignoreCase() { if ("_ignoreCase" in this) return this._ignoreCase; - let mode = options["wildcase"]; + let mode = this.wildcase; if (mode == "match") return this._ignoreCase = false; if (mode == "ignore") @@ -367,7 +375,7 @@ const CompletionContext = Class("CompletionContext", { if (this.maxItems) filtered = filtered.slice(0, this.maxItems); - if (options.get("wildoptions").has("sort") && this.compare) + if (this.sortResults && this.compare) filtered.sort(this.compare); let quote = this.quote; if (quote) @@ -404,12 +412,12 @@ const CompletionContext = Class("CompletionContext", { let filter = fixCase(this.filter); if (this.anchored) { var compare = function compare(text, s) text.substr(0, s.length) == s; - substrings = util.map(util.range(filter.length, text.length + 1), + var substrings = util.map(util.range(filter.length, text.length + 1), function (end) text.substring(0, end)); } else { var compare = function compare(text, s) text.indexOf(s) >= 0; - substrings = []; + var substrings = []; let start = 0; let idx; let length = filter.length; @@ -498,8 +506,14 @@ const CompletionContext = Class("CompletionContext", { completer = self[completer]; let context = CompletionContext(this, name, offset); this.contextList.push(context); - if (completer) + + if (!context.autoComplete && !context.tabPressed && context.editor) + context.waitingForTab = true; + else if (completer) return completer.apply(self || this, [context].concat(Array.slice(arguments, arguments.callee.length))); + + if (completer) + return null; return context; }, @@ -682,7 +696,7 @@ const Completion = Module("completion", { if (skip) context.advance(skip[0].length); - if (typeof complete === "undefined") + if (complete == null) complete = options["complete"]; // Will, and should, throw an error if !(c in opts) @@ -744,6 +758,80 @@ const Completion = Module("completion", { //}}} }, { UrlCompleter: Struct("name", "description", "completer") +}, { + commands: function () { + commands.add(["contexts"], + "List the completion contexts used during completion of an ex command", + function (args) { + commandline.echo(template.commandOutput( +
+ { template.completionRow(["Context", "Title"], "CompTitle") } + { template.map(completion.contextList || [], function (item) template.completionRow(item, "CompItem")) } +
), + null, commandline.FORCE_MULTILINE); + + }, + { + argCount: "1", + completer: function (context, args) { + let PREFIX = "/ex/contexts"; + context.fork("ex", 0, completion, "ex"); + completion.contextList = [[k.substr(PREFIX.length), v.title[0]] for ([k, v] in iter(context.contexts)) if (k.substr(0, PREFIX.length) == PREFIX)]; + }, + literal: 0 + }); + }, + options: function () { + options.add(["autocomplete", "au"], + "Automatically update the completion list on any key press", + "regexlist", ".*"); + + options.add(["complete", "cpt"], + "Items which are completed at the :open prompts", + "charlist", typeof(config.defaults["complete"]) == "string" ? config.defaults["complete"] : "slf", + { + completer: function (context) array(values(completion.urlCompleters)) + }); + + options.add(["wildcase", "wic"], + "Completion case matching mode", + "regexmap", "smart", + { + completer: function () [ + ["smart", "Case is significant when capital letters are typed"], + ["match", "Case is always significant"], + ["ignore", "Case is never significant"] + ] + }); + + options.add(["wildmode", "wim"], + "Define how command line completion works", + "stringlist", "list:full", + { + completer: function (context) [ + // Why do we need ""? + // Because its description is useful during completion. --Kris + ["", "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"] + ], + checkHas: function (value, val) { + let [first, second] = value.split(":", 2); + return first == val || second == val; + }, + has: function () { + test = function (val) this.values.some(function (value) this.checkHas(value, val), this); + return Array.some(arguments, test, this); + } + }); + + options.add(["wildsort", "wis"], + "Regexp list of which contexts to sort", + "regexlist", ".*"); + } }); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/configbase.js b/common/content/configbase.js index 7cb438b2..478a1417 100644 --- a/common/content/configbase.js +++ b/common/content/configbase.js @@ -1,7 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; const ConfigBase = Class(ModuleBase, { /** diff --git a/common/content/editor.js b/common/content/editor.js index 70414226..0307c98a 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -2,7 +2,7 @@ // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ diff --git a/common/content/events.js b/common/content/events.js index cfab6641..4ec214d2 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -1,10 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2009 by Doug Kearns // Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -103,32 +103,16 @@ const Events = Module("events", { } }, 100); - function wrapListener(method) { - return function (event) { - try { - self[method](event); - } - catch (e) { - if (e.message == "Interrupted") - liberator.echoerr("Interrupted"); - else - liberator.echoerr("Processing " + event.type + " event: " + (e.echoerr || e)); - liberator.reportError(e); - } - }; - } - - this._wrappedOnKeyPress = wrapListener("onKeyPress"); - this._wrappedOnKeyUpOrDown = wrapListener("onKeyUpOrDown"); - this.addSessionListener(window, "keypress", this.closure._wrappedOnKeyPress, true); - this.addSessionListener(window, "keydown", this.closure._wrappedOnKeyUpOrDown, true); - this.addSessionListener(window, "keyup", this.closure._wrappedOnKeyUpOrDown, true); - this._activeMenubar = false; - this.addSessionListener(window, "popupshown", this.closure.onPopupShown, true); - this.addSessionListener(window, "popuphidden", this.closure.onPopupHidden, true); this.addSessionListener(window, "DOMMenuBarActive", this.closure.onDOMMenuBarActive, true); this.addSessionListener(window, "DOMMenuBarInactive", this.closure.onDOMMenuBarInactive, true); + this.addSessionListener(window, "focus", this.wrapListener(this.closure.onFocus), true); + this.addSessionListener(window, "keydown", this.wrapListener(this.closure.onKeyUpOrDown), true); + this.addSessionListener(window, "keypress", this.wrapListener(this.closure.onKeyPress), true); + this.addSessionListener(window, "keyup", this.wrapListener(this.closure.onKeyUpOrDown), true); + this.addSessionListener(window, "mousedown", this.wrapListener(this.closure.onMouseDown), true); + this.addSessionListener(window, "popuphidden", this.closure.onPopupHidden, true); + this.addSessionListener(window, "popupshown", this.closure.onPopupShown, true); this.addSessionListener(window, "resize", this.closure.onResize, true); }, @@ -151,10 +135,28 @@ const Events = Module("events", { */ addSessionListener: function (target, event, callback, capture) { let args = Array.slice(arguments, 0); - target.addEventListener.apply(target, args.slice(1)); + target.addEventListener.apply(args[0], args.slice(1)); this.sessionListeners.push(args); }, + /** + * Wraps an event listener to ensure that errors are reported. + */ + wrapListener: function wrapListener(method, self) { + return function (event) { + try { + method.apply(self, arguments); + } + catch (e) { + if (e.message == "Interrupted") + liberator.echoerr("Interrupted"); + else + liberator.echoerr("Processing " + event.type + " event: " + (e.echoerr || e)); + liberator.reportError(e); + } + }; + }, + /** * @property {boolean} Whether synthetic key events are currently being * processed. @@ -651,89 +653,14 @@ const Events = Module("events", { return ret; }, - // argument "event" is deliberately not used, as i don't seem to have - // access to the real focus target - // Huh? --djk - onFocusChange: function (event) { - // command line has it's own focus change handler - if (liberator.mode == modes.COMMAND_LINE) - return; - - function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument - - let win = window.document.commandDispatcher.focusedWindow; - let elem = window.document.commandDispatcher.focusedElement; - - if (win && win.top == content && liberator.has("tabs")) - tabs.localStore.focusedFrame = win; - - try { - if (elem && elem.readOnly) - return; - - if ((elem instanceof HTMLInputElement && /^(text|password)$/.test(elem.type)) || - (elem instanceof HTMLSelectElement)) { - liberator.mode = modes.INSERT; - if (hasHTMLDocument(win)) - buffer.lastInputField = elem; - return; - } - if (elem instanceof HTMLEmbedElement || elem instanceof HTMLObjectElement) { - liberator.mode = modes.EMBED; - return; - } - - if (elem instanceof HTMLTextAreaElement || (elem && elem.contentEditable == "true")) { - if (options["insertmode"]) - modes.set(modes.INSERT); - else if (elem.selectionEnd - elem.selectionStart > 0) - modes.set(modes.VISUAL, modes.TEXTAREA); - else - modes.main = modes.TEXTAREA; - if (hasHTMLDocument(win)) - buffer.lastInputField = elem; - return; - } - - if (config.focusChange) { - config.focusChange(win); - return; - } - - let urlbar = document.getElementById("urlbar"); - if (elem == null && urlbar && urlbar.inputField == this._lastFocus) - liberator.threadYield(true); - - if (liberator.mode & (modes.EMBED | modes.INSERT | modes.TEXTAREA | modes.VISUAL)) - modes.reset(); - } - finally { - this._lastFocus = elem; - } + onDOMMenuBarActive: function () { + this._activeMenubar = true; + modes.add(modes.MENU); }, - onSelectionChange: function (event) { - let couldCopy = false; - let controller = document.commandDispatcher.getControllerForCommand("cmd_copy"); - if (controller && controller.isCommandEnabled("cmd_copy")) - couldCopy = true; - - if (liberator.mode != modes.VISUAL) { - if (couldCopy) { - if ((liberator.mode == modes.TEXTAREA || - (modes.extended & modes.TEXTAREA)) - && !options["insertmode"]) - modes.set(modes.VISUAL, modes.TEXTAREA); - else if (liberator.mode == modes.CARET) - modes.set(modes.VISUAL, modes.CARET); - } - } - // XXX: disabled, as i think automatically starting visual caret mode does more harm than help - // else - // { - // if (!couldCopy && modes.extended & modes.CARET) - // liberator.mode = modes.CARET; - // } + onDOMMenuBarInactive: function () { + this._activeMenubar = false; + modes.remove(modes.MENU); }, /** @@ -808,6 +735,78 @@ const Events = Module("events", { } }, + onFocus: function (event) { + function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument + + let elem = event.originalTarget; + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + + if (hasHTMLDocument(win) && !buffer.focusAllowed(win)) + elem.blur(); + }, + + // argument "event" is deliberately not used, as i don't seem to have + // access to the real focus target + // Huh? --djk + onFocusChange: function (event) { + // command line has it's own focus change handler + if (liberator.mode == modes.COMMAND_LINE) + return; + + function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument + + let win = window.document.commandDispatcher.focusedWindow; + let elem = window.document.commandDispatcher.focusedElement; + + if (win && win.top == content && liberator.has("tabs")) + tabs.localStore.focusedFrame = win; + + try { + if (elem && elem.readOnly) + return; + + if ((elem instanceof HTMLInputElement && /^(search|text|password)$/.test(elem.type)) || + (elem instanceof HTMLSelectElement)) { + liberator.mode = modes.INSERT; + if (hasHTMLDocument(win)) + buffer.lastInputField = elem; + return; + } + + if(isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) { + liberator.mode = modes.EMBED; + return; + } + + if (elem instanceof HTMLTextAreaElement || (elem && elem.contentEditable == "true")) { + if (options["insertmode"]) + modes.set(modes.INSERT); + else if (elem.selectionEnd - elem.selectionStart > 0) + modes.set(modes.VISUAL, modes.TEXTAREA); + else + modes.main = modes.TEXTAREA; + if (hasHTMLDocument(win)) + buffer.lastInputField = elem; + return; + } + + if (config.focusChange) { + config.focusChange(win); + return; + } + + let urlbar = document.getElementById("urlbar"); + if (elem == null && urlbar && urlbar.inputField == this._lastFocus) + liberator.threadYield(true); + + if (liberator.mode & (modes.EMBED | modes.INSERT | modes.TEXTAREA | modes.VISUAL)) + modes.reset(); + } + finally { + this._lastFocus = elem; + } + }, + // this keypress handler gets always called first, even if e.g. // the commandline has focus // TODO: ...help me...please... @@ -1022,7 +1021,9 @@ const Events = Module("events", { if (liberator.mode == modes.COMMAND_LINE) { if (!(modes.extended & modes.INPUT_MULTILINE)) - commandline.onEvent(event); // reroute event in command line mode + liberator.trapErrors(function () { + commandline.onEvent(event); // reroute event in command line mode + }); } else if (!modes.mainMode.input) liberator.beep(); @@ -1050,6 +1051,13 @@ const Events = Module("events", { event.stopPropagation(); }, + onMouseDown: function (event) { + let elem = event.target; + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + for(; win; win = win != win.parent && win.parent) + win.liberatorFocusAllowed = true; + }, + onPopupShown: function (event) { if (event.originalTarget.localName == "tooltip" || event.originalTarget.id == "liberator-visualbell") return; @@ -1062,22 +1070,36 @@ const Events = Module("events", { modes.remove(modes.MENU); }, - onDOMMenuBarActive: function () { - this._activeMenubar = true; - modes.add(modes.MENU); - }, - - onDOMMenuBarInactive: function () { - this._activeMenubar = false; - modes.remove(modes.MENU); - }, - onResize: function (event) { if (window.fullScreen != this._fullscreen) { this._fullscreen = window.fullScreen; liberator.triggerObserver("fullscreen", this._fullscreen); autocommands.trigger("Fullscreen", { state: this._fullscreen }); } + }, + + onSelectionChange: function (event) { + let couldCopy = false; + let controller = document.commandDispatcher.getControllerForCommand("cmd_copy"); + if (controller && controller.isCommandEnabled("cmd_copy")) + couldCopy = true; + + if (liberator.mode != modes.VISUAL) { + if (couldCopy) { + if ((liberator.mode == modes.TEXTAREA || + (modes.extended & modes.TEXTAREA)) + && !options["insertmode"]) + modes.set(modes.VISUAL, modes.TEXTAREA); + else if (liberator.mode == modes.CARET) + modes.set(modes.VISUAL, modes.CARET); + } + } + // XXX: disabled, as i think automatically starting visual caret mode does more harm than help + // else + // { + // if (!couldCopy && modes.extended & modes.CARET) + // liberator.mode = modes.CARET; + // } } }, { isInputElemFocused: function () { diff --git a/common/content/finder.js b/common/content/finder.js index 158ce928..b8bad27e 100644 --- a/common/content/finder.js +++ b/common/content/finder.js @@ -1,465 +1,12 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2008-2010 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; /** @scope modules */ -// TODO: proper backwards search - implement our own component? -// : implement our own highlighter? -// : should cancel search highlighting in 'incsearch' mode and jump -// back to the presearch page location - can probably use the same -// solution as marks -// : 'linksearch' searches should highlight link matches only -// : changing any search settings should also update the search state including highlighting -// : incremental searches shouldn't permanently update search modifiers -// -// TODO: Clean up this rat's nest. --Kris - -/** - * @instance finder - */ -const Finder = Module("finder", { - requires: ["config"], - - init: function () { - const self = this; - - this._found = false; // true if the last search was successful - this._backwards = false; // currently searching backwards - this._searchString = ""; // current search string (without modifiers) - this._searchPattern = ""; // current search string (includes modifiers) - this._lastSearchPattern = ""; // the last searched pattern (includes modifiers) - this._lastSearchString = ""; // the last searched string (without modifiers) - this._lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with this is not set - this._caseSensitive = false; // search string is case sensitive - this._linksOnly = false; // search is limited to link text only - - /* Stolen from toolkit.jar in Firefox, for the time being. The private - * methods were unstable, and changed. The new version is not remotely - * compatible with what we do. - * The following only applies to this object, and may not be - * necessary, or accurate, but, just in case: - * The Original Code is mozilla.org viewsource frontend. - * - * The Initial Developer of the Original Code is - * Netscape Communications Corporation. - * Portions created by the Initial Developer are Copyright (c) 2003 - * by the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Blake Ross (Original Author) - * Masayuki Nakano - * Ben Basson - * Jason Barnabe - * Asaf Romano - * Ehsan Akhgari - * Graeme McCutcheon - */ - this._highlighter = { - - doc: null, - - spans: [], - - search: function (aWord, matchCase) { - var finder = services.create("find"); - if (matchCase !== undefined) - self._caseSensitive = matchCase; - - var range; - while ((range = finder.Find(aWord, this.searchRange, this.startPt, this.endPt))) - yield range; - }, - - highlightDoc: function highlightDoc(win, aWord) { - this.doc = content.document; // XXX - Array.forEach(win.frames, function (frame) this.highlightDoc(frame, aWord), this); - - var doc = win.document; - if (!doc || !(doc instanceof HTMLDocument)) - return; - - if (!aWord) { - let elems = this._highlighter.spans; - for (let i = elems.length; --i >= 0;) { - let elem = elems[i]; - let docfrag = doc.createDocumentFragment(); - let next = elem.nextSibling; - let parent = elem.parentNode; - - let child; - while ((child = elem.firstChild)) - docfrag.appendChild(child); - - parent.removeChild(elem); - parent.insertBefore(docfrag, next); - parent.normalize(); - } - return; - } - - var baseNode = ; - baseNode = util.xmlToDom(baseNode, window.content.document); - - var body = doc.body; - var count = body.childNodes.length; - this.searchRange = doc.createRange(); - this.startPt = doc.createRange(); - this.endPt = doc.createRange(); - - this.searchRange.setStart(body, 0); - this.searchRange.setEnd(body, count); - - this.startPt.setStart(body, 0); - this.startPt.setEnd(body, 0); - this.endPt.setStart(body, count); - this.endPt.setEnd(body, count); - - liberator.interrupted = false; - let n = 0; - for (let retRange in this.search(aWord, this._caseSensitive)) { - // Highlight - var nodeSurround = baseNode.cloneNode(true); - var node = this.highlight(retRange, nodeSurround); - this.startPt = node.ownerDocument.createRange(); - this.startPt.setStart(node, node.childNodes.length); - this.startPt.setEnd(node, node.childNodes.length); - if (n++ % 20 == 0) - liberator.threadYield(true); - if (liberator.interrupted) - break; - } - }, - - highlight: function highlight(aRange, aNode) { - var startContainer = aRange.startContainer; - var startOffset = aRange.startOffset; - var endOffset = aRange.endOffset; - var docfrag = aRange.extractContents(); - var before = startContainer.splitText(startOffset); - var parent = before.parentNode; - aNode.appendChild(docfrag); - parent.insertBefore(aNode, before); - this.spans.push(aNode); - return aNode; - }, - - /** - * Clears all search highlighting. - */ - clear: function () { - this.spans.forEach(function (span) { - if (span.parentNode) { - let el = span.firstChild; - while (el) { - span.removeChild(el); - span.parentNode.insertBefore(el, span); - el = span.firstChild; - } - span.parentNode.removeChild(span); - } - }); - this.spans = []; - }, - - isHighlighted: function (doc) this.doc == doc && this.spans.length > 0 - }; - }, - - // set searchString, searchPattern, caseSensitive, linksOnly - _processUserPattern: function (pattern) { - //// strip off pattern terminator and offset - //if (backwards) - // pattern = pattern.replace(/\?.*/, ""); - //else - // pattern = pattern.replace(/\/.*/, ""); - - this._searchPattern = pattern; - - // links only search - \l wins if both modifiers specified - if (/\\l/.test(pattern)) - this._linksOnly = true; - else if (/\L/.test(pattern)) - this._linksOnly = false; - else if (options["linksearch"]) - this._linksOnly = true; - else - this._linksOnly = false; - - // strip links-only modifiers - pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; }); - - // case sensitivity - \c wins if both modifiers specified - if (/\c/.test(pattern)) - this._caseSensitive = false; - else if (/\C/.test(pattern)) - this._caseSensitive = true; - else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern)) - this._caseSensitive = true; - else if (options["ignorecase"]) - this._caseSensitive = false; - else - this._caseSensitive = true; - - // strip case-sensitive modifiers - pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; }); - - // remove any modifier escape \ - pattern = pattern.replace(/\\(\\[cClL])/g, "$1"); - - this._searchString = pattern; - }, - - /** - * Called when the search dialog is requested. - * - * @param {number} mode The search mode, either modes.SEARCH_FORWARD or - * modes.SEARCH_BACKWARD. - * @default modes.SEARCH_FORWARD - */ - openPrompt: function (mode) { - this._backwards = mode == modes.SEARCH_BACKWARD; - commandline.open(this._backwards ? "?" : "/", "", mode); - // TODO: focus the top of the currently visible screen - }, - - // TODO: backwards seems impossible i fear - /** - * Searches the current buffer for str. - * - * @param {string} str The string to find. - */ - find: function (str) { - let fastFind = config.browser.fastFind; - - this._processUserPattern(str); - fastFind.caseSensitive = this._caseSensitive; - this._found = fastFind.find(this._searchString, this._linksOnly) != Ci.nsITypeAheadFind.FIND_NOTFOUND; - - if (!this._found) - this.setTimeout(function () liberator.echoerr("E486: Pattern not found: " + this._searchPattern, commandline.FORCE_SINGLELINE), 0); - }, - - /** - * Searches the current buffer again for the most recently used search - * string. - * - * @param {boolean} reverse Whether to search forwards or backwards. - * @default false - */ - findAgain: function (reverse) { - // This hack is needed to make n/N work with the correct string, if - // we typed /foo after the original search. Since searchString is - // readonly we have to call find() again to update it. - if (config.browser.fastFind.searchString != this._lastSearchString) - this.find(this._lastSearchString); - - let up = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards; - let result = config.browser.fastFind.findAgain(up, this._linksOnly); - - if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND) - liberator.echoerr("E486: Pattern not found: " + this._lastSearchPattern, commandline.FORCE_SINGLELINE); - else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED) { - // hack needed, because wrapping causes a "scroll" event which clears - // our command line - setTimeout(function () { - let msg = up ? "search hit TOP, continuing at BOTTOM" : "search hit BOTTOM, continuing at TOP"; - commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE); - }, 0); - } - else { - commandline.echo((up ? "?" : "/") + this._lastSearchPattern, null, commandline.FORCE_SINGLELINE); - - if (options["hlsearch"]) - this.highlight(this._lastSearchString); - } - }, - - /** - * Called when the user types a key in the search dialog. Triggers a - * search attempt if 'incsearch' is set. - * - * @param {string} str The search string. - */ - onKeyPress: function (str) { - if (options["incsearch"]) - this.find(str); - }, - - /** - * Called when the key is pressed to trigger a search. - * - * @param {string} str The search string. - * @param {boolean} forcedBackward Whether to search forwards or - * backwards. This overrides the direction set in - * (@link #openPrompt). - * @default false - */ - onSubmit: function (str, forcedBackward) { - if (typeof forcedBackward === "boolean") - this._backwards = forcedBackward; - - if (str) - var pattern = str; - else { - liberator.assert(this._lastSearchPattern, "E35: No previous search pattern"); - pattern = this._lastSearchPattern; - } - - this.clear(); - - if (!options["incsearch"] || !str || !this._found) { - // prevent any current match from matching again - if (!window.content.getSelection().isCollapsed) - window.content.getSelection().getRangeAt(0).collapse(this._backwards); - - this.find(pattern); - } - - this._lastSearchBackwards = this._backwards; - //lastSearchPattern = pattern.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX - this._lastSearchPattern = pattern; - this._lastSearchString = this._searchString; - - // TODO: move to find() when reverse incremental searching is kludged in - // need to find again for reverse searching - if (this._backwards) - this.setTimeout(function () { this.findAgain(false); }, 0); - - if (options["hlsearch"]) - this.highlight(this._searchString); - - modes.reset(); - }, - - /** - * Called when the search is canceled. For example, if someone presses - * while typing a search. - */ - onCancel: function () { - // TODO: code to reposition the document to the place before search started - }, - - /** - * Highlights all occurances of str in the buffer. - * - * @param {string} str The string to highlight. - */ - highlight: function (str) { - // FIXME: Thunderbird incompatible - if (config.name == "Muttator") - return; - - if (this._highlighter.isHighlighted(content.document)) - return; - - if (!str) - str = this._lastSearchString; - - this._highlighter.highlightDoc(window.content, str); - - // recreate selection since highlightDoc collapses the selection - if (window.content.getSelection().isCollapsed) - config.browser.fastFind.findAgain(this._backwards, this._linksOnly); - - // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"]) - }, - - /** - * Clears all search highlighting. - */ - clear: function () { - this._highlighter.clear(); - } -}, { -}, { - commandline: function () { - // Event handlers for search - closure is needed - commandline.registerCallback("change", modes.SEARCH_FORWARD, this.closure.onKeyPress); - commandline.registerCallback("submit", modes.SEARCH_FORWARD, this.closure.onSubmit); - commandline.registerCallback("cancel", modes.SEARCH_FORWARD, this.closure.onCancel); - // TODO: allow advanced myModes in register/triggerCallback - commandline.registerCallback("change", modes.SEARCH_BACKWARD, this.closure.onKeyPress); - commandline.registerCallback("submit", modes.SEARCH_BACKWARD, this.closure.onSubmit); - commandline.registerCallback("cancel", modes.SEARCH_BACKWARD, this.closure.onCancel); - - }, - commands: function () { - commands.add(["noh[lsearch]"], - "Remove the search highlighting", - function () { finder.clear(); }, - { argCount: "0" }); - }, - mappings: function () { - var myModes = config.browserModes; - myModes = myModes.concat([modes.CARET]); - - mappings.add(myModes, - ["/"], "Search forward for a pattern", - function () { finder.openPrompt(modes.SEARCH_FORWARD); }); - - mappings.add(myModes, - ["?"], "Search backwards for a pattern", - function () { finder.openPrompt(modes.SEARCH_BACKWARD); }); - - mappings.add(myModes, - ["n"], "Find next", - function () { finder.findAgain(false); }); - - mappings.add(myModes, - ["N"], "Find previous", - function () { finder.findAgain(true); }); - - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"], - "Find word under cursor", - function () { - this._found = false; - finder.onSubmit(buffer.getCurrentWord(), false); - }); - - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"], - "Find word under cursor backwards", - function () { - this._found = false; - finder.onSubmit(buffer.getCurrentWord(), true); - }); - }, - options: function () { - options.add(["hlsearch", "hls"], - "Highlight previous search pattern matches", - "boolean", "false", { - setter: function (value) { - try { - if (value) - finder.highlight(); - else - finder.clear(); - } - catch (e) {} - - return value; - } - }); - - options.add(["ignorecase", "ic"], - "Ignore case in search patterns", - "boolean", true); - - options.add(["incsearch", "is"], - "Show where the search pattern matches as it is typed", - "boolean", true); - - options.add(["linksearch", "lks"], - "Limit the search to hyperlink text", - "boolean", false); - - options.add(["smartcase", "scs"], - "Override the 'ignorecase' option if the pattern contains uppercase characters", - "boolean", true); - } -}); - +/** @instance rangefinder */ const RangeFinder = Module("rangefinder", { requires: ["config"], @@ -471,6 +18,7 @@ const RangeFinder = Module("rangefinder", { let backwards = mode == modes.FIND_BACKWARD; commandline.open(backwards ? "?" : "/", "", mode); + this.rangeFind = null; this.find("", backwards); }, @@ -537,6 +85,7 @@ const RangeFinder = Module("rangefinder", { if (options["hlsearch"]) this.highlight(); + this.rangeFind.focus(); }, // Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set @@ -557,6 +106,7 @@ const RangeFinder = Module("rangefinder", { if (options["hlsearch"]) this.highlight(); + this.rangeFind.focus(); modes.reset(); }, @@ -603,48 +153,106 @@ const RangeFinder = Module("rangefinder", { }, commands: function () { + commands.add(["noh[lsearch]"], + "Remove the search highlighting", + function () { rangefinder.clear(); }, + { argCount: "0" }); }, mappings: function () { var myModes = config.browserModes.concat([modes.CARET]); mappings.add(myModes, - ["g/"], "Search forward for a pattern", + ["/"], "Search forward for a pattern", function () { rangefinder.openPrompt(modes.FIND_FORWARD); }); mappings.add(myModes, - ["g?"], "Search backwards for a pattern", + ["?"], "Search backwards for a pattern", function () { rangefinder.openPrompt(modes.FIND_BACKWARD); }); mappings.add(myModes, - ["g."], "Find next", + ["n"], "Find next", function () { rangefinder.findAgain(false); }); mappings.add(myModes, - ["g,"], "Find previous", + ["N"], "Find previous", function () { rangefinder.findAgain(true); }); - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["g*"], + mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"], "Find word under cursor", function () { rangefinder._found = false; rangefinder.onSubmit(buffer.getCurrentWord(), false); }); - mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["g#"], + mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"], "Find word under cursor backwards", function () { rangefinder._found = false; rangefinder.onSubmit(buffer.getCurrentWord(), true); }); + }, modes: function () { modes.addMode("FIND_FORWARD", true); modes.addMode("FIND_BACKWARD", true); }, options: function () { + options.add(["hlsearch", "hls"], + "Highlight previous search pattern matches", + "boolean", "false", { + setter: function (value) { + try { + if (value) + rangefinder.highlight(); + else + rangefinder.clear(); + } + catch (e) {} + + return value; + } + }); + + options.add(["ignorecase", "ic"], + "Ignore case in search patterns", + "boolean", true); + + options.add(["incsearch", "is"], + "Show where the search pattern matches as it is typed", + "boolean", true); + + options.add(["linksearch", "lks"], + "Limit the search to hyperlink text", + "boolean", false); + + options.add(["smartcase", "scs"], + "Override the 'ignorecase' option if the pattern contains uppercase characters", + "boolean", true); } }); +/** + * @class RangeFind + * + * A fairly sophisticated typeahead-find replacement. It supports + * incremental search very much as the builtin component. + * Additionally, it supports several features impossible to + * implement using the standard component. Incremental searching + * works both forwards and backwards. Erasing characters during an + * incremental search moves the selection back to the first + * available match for the shorter term. The selection and viewport + * are restored when the search is canceled. + * + * Also, in addition to full support for frames and iframes, this + * implementation will begin searching from the position of the + * caret in the last active frame. This is contrary to the behavior + * of the builtin component, which always starts a search from the + * begining of the first frame in the case of frameset documents, + * and cycles through all frames from begining to end. This makes it + * impossible to choose the starting point of a search for such + * documents, and represents a major detriment to productivity where + * large amounts of data are concerned (e.g., for API documents). + */ const RangeFind = Class("RangeFind", { init: function (matchCase, backward, elementPath) { this.window = Cu.getWeakReference(window); @@ -655,20 +263,28 @@ const RangeFind = Class("RangeFind", { this.finder.caseSensitive = this.matchCase; this.ranges = this.makeFrameList(content); - this.range = RangeFind.Range(tabs.localStore.focusedFrame || content); - this.startRange = (this.range.selection.rangeCount ? this.range.selection.getRangeAt(0) : this.ranges[0].range).cloneRange(); - this.startRange.collapse(!backward); - this.range = this.findRange(this.startRange); - this.ranges.first = this.range; + this.reset(); this.highlighted = null; this.lastString = ""; - this.lastRange = null; this.forward = null; this.found = false; }, + get selectedRange() { + let range = RangeFind.Range(tabs.localStore.focusedFrame || content); + return (range.selection.rangeCount ? range.selection.getRangeAt(0) : this.ranges[0].range).cloneRange(); + }, + + reset: function () { + this.startRange = this.selectedRange; + this.startRange.collapse(!this.reverse); + this.lastRange = this.selectedRange; + this.range = this.findRange(this.startRange); + this.ranges.first = this.range; + }, + sameDocument: function (r1, r2) r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument, compareRanges: function (r1, r2) @@ -699,6 +315,16 @@ const RangeFind = Class("RangeFind", { } }, + focus: function() { + if(this.lastRange) + var node = util.evaluateXPath(RangeFind.selectNodePath, this.range.document, + this.lastRange.commonAncestorContainer).snapshotItem(0); + if(node) { + node.focus(); + this.search(null, false); // Rehighlight collapsed range + } + }, + makeFrameList: function (win) { const self = this; win = win.top; @@ -706,15 +332,21 @@ const RangeFind = Class("RangeFind", { let backup = null; function pushRange(start, end) { + function push(r) { + r = RangeFind.Range(r, frames.length); + if (r) + frames.push(r); + } + let range = start.startContainer.ownerDocument.createRange(); range.setStart(start.startContainer, start.startOffset); range.setEnd(end.startContainer, end.startOffset); if (!self.elementPath) - frames.push(RangeFind.Range(range, frames.length)); + push(range); else for (let r in self.findSubRanges(range)) - frames.push(RangeFind.Range(r, frames.length)); + push(r); } function rec(win) { let doc = win.document; @@ -741,8 +373,8 @@ const RangeFind = Class("RangeFind", { // This doesn't work yet. resetCaret: function () { - let equal = function (r1, r2) !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2); - letselection = this.win.getSelection(); + let equal = RangeFind.equal; + let selection = this.win.getSelection(); if (selection.rangeCount == 0) selection.addRange(this.pageStart); function getLines() { @@ -792,6 +424,9 @@ const RangeFind = Class("RangeFind", { }, search: function (word, reverse, private_) { + if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange)) + this.reset(); + this.wrapped = false; this.finder.findBackwards = reverse ? !this.reverse : this.reverse; let again = word == null; @@ -886,6 +521,7 @@ const RangeFind = Class("RangeFind", { parent.insertBefore(node, before); range.selectNode(node); } + function unhighlight(range) { let elem = range.startContainer; while (!(elem instanceof Element) && elem.parentNode) @@ -913,7 +549,7 @@ const RangeFind = Class("RangeFind", { else { this.highlighted = this.lastString; this.addListeners(); - this.search(null, false); + this.search(null, false); // Rehighlight collapsed range } }, @@ -952,6 +588,9 @@ const RangeFind = Class("RangeFind", { this.window = this.document.defaultView; this.range = range; + if (this.selection == null) + return false; + this.save(); }, @@ -989,12 +628,27 @@ const RangeFind = Class("RangeFind", { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) .QueryInterface(Ci.nsISelectionController), - get selection() this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL) + get selection() { + try { + return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL) + } catch (e) { + return null; + }} + }), + selectNodePath: ["ancestor-or-self::" + s for ([i, s] in Iterator( + ["a", "xhtml:a", "*[@onclick]"]))].join(" | "), endpoint: function (range, before) { range = range.cloneRange(); range.collapse(before); return range; + }, + equal: function (r1, r2) { + try { + return !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2) + } + catch (e) {} + return false; } }); diff --git a/common/content/help.js b/common/content/help.js index 67fbba37..ed1dd22a 100644 --- a/common/content/help.js +++ b/common/content/help.js @@ -2,7 +2,7 @@ // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; function checkFragment() { document.title = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "title")[0].textContent; diff --git a/common/content/help.xsl b/common/content/help.xsl index 1ad7ee76..e9766465 100644 --- a/common/content/help.xsl +++ b/common/content/help.xsl @@ -202,7 +202,7 @@ (default: - + @@ -351,9 +351,13 @@ - - - + + + + # + + + diff --git a/common/content/hints.js b/common/content/hints.js index 48974c7e..ed4b4e9c 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -1,8 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ /** @instance hints */ @@ -40,12 +42,12 @@ const Hints = Module("hints", { "?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended), s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)), a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)), - f: Mode("Focus frame", function (elem) elem.ownerDocument.defaultView.focus(), function () util.makeXPath(["body"])), + f: Mode("Focus frame", function (elem) elem.ownerDocument.defaultView.focus(), function () ["body"]), o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)), t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)), b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)), w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended), - F: Mode("Open multiple hints in tabs", followAndReshow), + F: Mode("Open multiple hints in tabs", function (elem) { buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB); hints.show("F") }), O: Mode("Generate an ':open URL' using hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)), T: Mode("Generate a ':tabopen URL' using hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)), W: Mode("Generate a ':winopen URL' using hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)), @@ -57,21 +59,6 @@ const Hints = Module("hints", { i: Mode("Show image", function (elem) liberator.open(elem.src), images), I: Mode("Show image in a new tab", function (elem) liberator.open(elem.src, liberator.NEW_TAB), images) }; - - /** - * Follows the specified hint and then reshows all hints. Used to open - * multiple hints in succession. - * - * @param {Node} elem The selected hint. - */ - function followAndReshow(elem) { - buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB); - - // TODO: Maybe we find a *simple* way to keep the hints displayed rather than - // showing them again, or is this short flash actually needed as a "usability - // feature"? --mst - hints.show("F"); - } }, /** @@ -127,7 +114,7 @@ const Hints = Module("hints", { let type = elem.type; - if (elem instanceof HTMLInputElement && /(submit|button|this._reset)/.test(type)) + if (elem instanceof HTMLInputElement && /(submit|button|reset)/.test(type)) return [elem.value, false]; else { for (let [, option] in Iterator(options["hintinputs"].split(","))) { @@ -266,14 +253,10 @@ const Hints = Module("hints", { let hint = { elem: elem, showText: false }; // TODO: for iframes, this calculation is wrong - rect = elem.getBoundingClientRect(); + let rect = elem.getBoundingClientRect(); if (!rect || rect.top > height || rect.bottom < 0 || rect.left > width || rect.right < 0) continue; - rect = elem.getClientRects()[0]; - if (!rect) - continue; - let computedStyle = doc.defaultView.getComputedStyle(elem, null); if (computedStyle.getPropertyValue("visibility") != "visible" || computedStyle.getPropertyValue("display") == "none") continue; @@ -370,7 +353,7 @@ const Hints = Module("hints", { if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof HTMLImageElement) { if (!hint.imgSpan) { - rect = hint.elem.firstChild.getBoundingClientRect(); + var rect = hint.elem.firstChild.getBoundingClientRect(); if (!rect) continue; @@ -394,11 +377,11 @@ const Hints = Module("hints", { } } - if (config.browser.markupDocumentViewer.authorStyleDisabled) { + if (options["usermode"]) { let css = []; // FIXME: Broken for imgspans. for (let [, { doc: doc }] in Iterator(this._docs)) { - for (let elem in util.evaluateXPath(" {//*[@liberator:highlight and @number]", doc)) { + for (let elem in util.evaluateXPath("//*[@liberator:highlight and @number]", doc)) { let group = elem.getAttributeNS(NS.uri, "highlight"); css.push(highlight.selector(group) + "[number=" + elem.getAttribute("number").quote() + "] { " + elem.style.cssText + " }"); } @@ -1058,8 +1041,8 @@ const Hints = Module("hints", { }, options: function () { const DEFAULT_HINTTAGS = - util.makeXPath(["input[not(@type='hidden')]", "a", "area", "iframe", "textarea", "button", "select"]) - + " | //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @role='link']"; + util.makeXPath(["input[not(@type='hidden')]", "a", "area", "iframe", "textarea", "button", "select", + "*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @role='link']"]); function checkXPath(val) { try { diff --git a/common/content/history.js b/common/content/history.js index 8f3ae5c8..dbab5522 100644 --- a/common/content/history.js +++ b/common/content/history.js @@ -1,7 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; const History = Module("history", { requires: ["config"], diff --git a/common/content/io.js b/common/content/io.js index 07f15453..7c85d331 100755 --- a/common/content/io.js +++ b/common/content/io.js @@ -1,9 +1,11 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // Some code based on Venkman // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -160,7 +162,7 @@ const File = Class("File", { mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; if (!perms) - perms = 0644; + perms = parseInt('0644', 8); ofstream.init(this, mode, perms, 0); let ocstream = getStream(0); @@ -240,7 +242,7 @@ const File = Class("File", { */ MODE_EXCL: 0x80, - expandPathList: function (list) list.split(",").map(this.expandPath).join(","), + expandPathList: function (list) list.map(this.expandPath), expandPath: function (path, relative) { @@ -338,7 +340,7 @@ const IO = Module("io", { let file = download.targetFile.path; let size = download.size; - liberator.echomsg("Download of " + title + " to " + file + " finished", 1); + liberator.echomsg("Download of " + title + " to " + file + " finished", 1, commandline.ACTIVE_WINDOW); autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size }); } }, @@ -485,7 +487,7 @@ const IO = Module("io", { let file = services.get("directory").get("TmpD", Ci.nsIFile); file.append(config.tempFile); - file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt('0600', 8)); return File(file); }, @@ -595,6 +597,8 @@ lookup: */ source: function (filename, silent) { let wasSourcing = this.sourcing; + liberator.dump("sourcing " + filename); + let time = Date.now(); try { var file = File(filename); this.sourcing = { @@ -624,8 +628,7 @@ lookup: if (/\.js$/.test(filename)) { try { liberator.loadScript(uri.spec, Script(file)); - if (liberator.initialized) - liberator.initHelp(); + liberator.helpInitialized = false; } catch (e) { let err = new Error(); @@ -713,6 +716,7 @@ lookup: liberator.echoerr(message); } finally { + liberator.dump("done sourcing " + filename + ": " + (Date.now() - time) + "ms"); this.sourcing = wasSourcing; } }, @@ -1028,8 +1032,8 @@ lookup: b.isdir - a.isdir || String.localeCompare(a.text, b.text); if (options["wildignore"]) { - let wigRegexp = RegExp("(^" + options.get("wildignore").values.join("|") + ")$"); - context.filters.push(function ({item: f}) f.isDirectory() || !wigRegexp.test(f.leafName)); + let wig = options.get("wildignore"); + context.filters.push(function ({item: f}) f.isDirectory() || !wig.getKey(this.name)); } // context.background = true; @@ -1061,7 +1065,10 @@ lookup: }; }; - completion.addUrlCompleter("f", "Local files", completion.file); + completion.addUrlCompleter("f", "Local files", function (context, full) { + if (!/^\.?\//.test(context.filter)) + completion.file(context, full); + }); }, options: function () { var shell, shellcmdflag; @@ -1099,6 +1106,10 @@ lookup: options.add(["shellcmdflag", "shcf"], "Flag passed to shell when executing :! and :run commands", "string", shellcmdflag); + + options.add(["wildignore", "wig"], + "List of file patterns to ignore when completing files", + "regexlist", ""); } }); diff --git a/common/content/javascript.js b/common/content/javascript.js index 98804f7c..1c337424 100644 --- a/common/content/javascript.js +++ b/common/content/javascript.js @@ -2,6 +2,7 @@ // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; // TODO: Clean this up. @@ -33,17 +34,18 @@ const JavaScript = Module("javascript", { }, iter: function iter(obj, toplevel) { + "use strict"; toplevel = !!toplevel; let seen = {}; let ret = {}; - try { + if(obj == null) + return; + + if(options["jsdebugger"]) { let orig = obj; let top = services.get("debugger").wrapValue(obj); - if (!toplevel) - obj = obj.__proto__; - for (; obj; obj = !toplevel && obj.__proto__) { services.get("debugger").wrapValue(obj).getProperties(ret, {}); for (let prop in values(ret.value)) { @@ -51,18 +53,27 @@ const JavaScript = Module("javascript", { if (name in seen) continue; seen[name] = 1; - yield [prop.name.stringValue, top.getProperty(prop.name.stringValue).value.getWrappedValue()] + if (toplevel || obj !== orig) + yield [prop.name.stringValue, top.getProperty(prop.name.stringValue).value.getWrappedValue()] } } // The debugger doesn't list some properties. I can't guess why. - for (let k in orig) - if (k in orig && !('|' + k in seen) && obj.hasOwnProperty(k) == toplevel) - yield [k, this.getKey(orig, k)] + // This only lists ENUMERABLE properties. + try { + for (let k in orig) + if (k in orig && !('|' + k in seen) + && Object.hasOwnProperty(orig, k) == toplevel) + yield [k, this.getKey(orig, k)] + } + catch(e) {} } - catch(e) { - for (k in allkeys(obj)) - if (obj.hasOwnProperty(k) == toplevel) - yield [k, this.getKey(obj, k)]; + else { + for (let k in allkeys(obj)) + try { + if (Object.hasOwnProperty(obj, k) == toplevel) + yield [k, this.getKey(obj, k)]; + } + catch (e) {} } }, @@ -101,7 +112,7 @@ const JavaScript = Module("javascript", { return completions; }, - eval: function eval(arg, key, tmp) { + eval: function evalstr(arg, key, tmp) { let cache = this.context.cache.eval; let context = this.context.cache.evalContext; @@ -230,8 +241,6 @@ const JavaScript = Module("javascript", { case "'": case "/": case "{": - this._push(this._c); - break; case "[": this._push(this._c); break; @@ -341,6 +350,10 @@ const JavaScript = Module("javascript", { _complete: function (objects, key, compl, string, last) { const self = this; + + if(!options["jsdebugger"] && !this.context.message) + this.context.message = "For better completion data, please enable the JavaScript debugger (:set jsdebugger)"; + let orig = compl; if (!compl) { compl = function (context, obj, recurse) { @@ -427,7 +440,8 @@ const JavaScript = Module("javascript", { // Okay, have parse stack. Figure out what we're completing. // Find any complete statements that we can eval before we eval our object. - // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem. + // This allows for things like: + // let doc = window.content.document; let elem = doc.createEle ... let prev = 0; for (let [, v] in Iterator(this._get(0).fullStatements)) { let key = this._str.substring(prev, v + 1); @@ -437,14 +451,13 @@ const JavaScript = Module("javascript", { prev = v + 1; } - // In a string. Check if we're dereferencing an object. - // Otherwise, do nothing. + // In a string. Check if we're dereferencing an object or + // completing a function argument. Otherwise, do nothing. if (this._last == "'" || this._last == '"') { - // + // str = "foo[bar + 'baz" // obj = "foo" // key = "bar + ''" - // // The top of the stack is the sting we're completing. // Wrap it in its delimiters and eval it to process escape sequences. @@ -497,15 +510,15 @@ const JavaScript = Module("javascript", { // Split up the arguments let prev = this._get(-2).offset; let args = []; - for (let [, idx] in Iterator(this._get(-2).comma)) { + for (let [i, idx] in Iterator(this._get(-2).comma)) { let arg = this._str.substring(prev + 1, idx); prev = idx; - util.memoize(args, this._i, function () self.eval(arg)); + util.memoize(args, i, function () self.eval(arg)); } let key = this._getKey(); args.push(key + string); - compl = function (context, obj) { + let compl = function (context, obj) { let res = completer.call(self, context, func, obj, args); if (res) context.completions = res; @@ -520,7 +533,7 @@ const JavaScript = Module("javascript", { return null; } - // + // str = "foo.bar.baz" // obj = "foo.bar" // key = "baz" @@ -528,11 +541,11 @@ const JavaScript = Module("javascript", { // str = "foo" // obj = [modules, window] // key = "foo" - // + let [offset, obj, key] = this._getObjKey(-1); - // Wait for a keypress before completing the default objects. + // Wait for a keypress before completing when there's no key if (!this.context.tabPressed && key == "" && obj.length > 1) { this.context.waitingForTab = true; this.context.message = "Waiting for key press"; @@ -589,7 +602,7 @@ const JavaScript = Module("javascript", { let completer = completers[args.length - 1]; if (!completer) return []; - return completer.call(this, context, obj, args); + return completer.call(obj, context, obj, args); }; } } diff --git a/common/content/liberator-overlay.js b/common/content/liberator-overlay.js index 478ecd9e..e9461df4 100644 --- a/common/content/liberator-overlay.js +++ b/common/content/liberator-overlay.js @@ -1,7 +1,8 @@ -// Copyright (c) 2008-2009 Kris Maglione +// Copyright (c) 2008-2008 Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. +"use strict"; (function () { const modules = {}; diff --git a/common/content/liberator.js b/common/content/liberator.js index a4b00ded..8214b461 100644 --- a/common/content/liberator.js +++ b/common/content/liberator.js @@ -1,8 +1,10 @@ -// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. - +"use strict"; /** @scope modules */ @@ -134,7 +136,7 @@ const Liberator = Module("liberator", { forceNewWindow: false, /** @property {string} The Liberator version string. */ - version: "###VERSION### (created: ###DATE###)", // these VERSION and DATE tokens are replaced by the Makefile + version: "@VERSION@ (created: @DATE@)", // these VERSION and DATE tokens are replaced by the Makefile /** * @property {Object} The map of command-line options. These are @@ -268,7 +270,7 @@ const Liberator = Module("liberator", { let stack = Error().stack.replace(/(?:.*\n){2}/, ""); if (frames != null) [stack] = stack.match(RegExp("(?:.*\n){0," + frames + "}")); - liberator.dump((msg || "Stack") + "\n" + stack); + liberator.dump((msg || "Stack") + "\n" + stack + "\n"); }, /** @@ -329,7 +331,7 @@ const Liberator = Module("liberator", { // you don't like them you can set verbose=0, or use :silent when // someone adds it. I reckon another flag and 'class' of messages // is just going to unnecessarily complicate things. --djk - flags |= commandline.APPEND_TO_MESSAGES | commandline.DISALLOW_MULTILINE; + flags |= commandline.APPEND_TO_MESSAGES; // | commandline.DISALLOW_MULTILINE; if (verbosity == null) verbosity = 0; // verbosity level is exclusionary @@ -542,85 +544,88 @@ const Liberator = Module("liberator", { * Initialize the help system. */ initHelp: function () { - let namespaces = [config.name.toLowerCase(), "liberator"]; - services.get("liberator:").init({}); + if(!this.helpInitialized) { + let namespaces = [config.name.toLowerCase(), "liberator"]; + services.get("liberator:").init({}); - let tagMap = services.get("liberator:").HELP_TAGS; - let fileMap = services.get("liberator:").FILE_MAP; - let overlayMap = services.get("liberator:").OVERLAY_MAP; + let tagMap = services.get("liberator:").HELP_TAGS; + let fileMap = services.get("liberator:").FILE_MAP; + let overlayMap = services.get("liberator:").OVERLAY_MAP; - // Left as an XPCOM instantiation so it can easilly be moved - // into XPCOM code. - function XSLTProcessor(sheet) { - let xslt = Cc["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Ci.nsIXSLTProcessor); - xslt.importStylesheet(util.httpGet(sheet).responseXML); - return xslt; - } - - // Find help and overlay files with the given name. - function findHelpFile(file) { - let result = []; - for (let [, namespace] in Iterator(namespaces)) { - let url = ["chrome://", namespace, "/locale/", file, ".xml"].join(""); - let res = util.httpGet(url); - if (res) { - if (res.responseXML.documentElement.localName == "document") - fileMap[file] = url; - if (res.responseXML.documentElement.localName == "overlay") - overlayMap[file] = url; - result.push(res.responseXML); - } + // Left as an XPCOM instantiation so it can easilly be moved + // into XPCOM code. + function XSLTProcessor(sheet) { + let xslt = Cc["@mozilla.org/document-transformer;1?type=xslt"].createInstance(Ci.nsIXSLTProcessor); + xslt.importStylesheet(util.httpGet(sheet).responseXML); + return xslt; } - return result; - } - // Find the tags in the document. - function addTags(file, doc) { - doc = XSLT.transformToDocument(doc); - for (let elem in util.evaluateXPath("//xhtml:a/@id", doc)) - tagMap[elem.value] = file; - } - const XSLT = XSLTProcessor("chrome://liberator/content/help-single.xsl"); + // Find help and overlay files with the given name. + function findHelpFile(file) { + let result = []; + for (let [, namespace] in Iterator(namespaces)) { + let url = ["chrome://", namespace, "/locale/", file, ".xml"].join(""); + let res = util.httpGet(url); + if (res) { + if (res.responseXML.documentElement.localName == "document") + fileMap[file] = url; + if (res.responseXML.documentElement.localName == "overlay") + overlayMap[file] = url; + result.push(res.responseXML); + } + } + return result; + } + // Find the tags in the document. + function addTags(file, doc) { + doc = XSLT.transformToDocument(doc); + for (let elem in util.evaluateXPath("//xhtml:a/@id", doc)) + tagMap[elem.value] = file; + } - // Scrape the list of help files from all.xml - // Always process main and overlay files, since XSLTProcessor and - // XMLHttpRequest don't allow access to chrome documents. - tagMap.all = "all"; - let files = findHelpFile("all").map(function (doc) - [f.value for (f in util.evaluateXPath( - "//liberator:include/@href", doc))]); + const XSLT = XSLTProcessor("chrome://liberator/content/help-single.xsl"); - // Scrape the tags from the rest of the help files. - util.Array.flatten(files).forEach(function (file) { - findHelpFile(file).forEach(function (doc) { - addTags(file, doc); + // Scrape the list of help files from all.xml + // Always process main and overlay files, since XSLTProcessor and + // XMLHttpRequest don't allow access to chrome documents. + tagMap.all = "all"; + let files = findHelpFile("all").map(function (doc) + [f.value for (f in util.evaluateXPath( + "//liberator:include/@href", doc))]); + + // Scrape the tags from the rest of the help files. + util.Array.flatten(files).forEach(function (file) { + findHelpFile(file).forEach(function (doc) { + addTags(file, doc); + }); }); - }); - // Process plugin help entries. - XML.ignoreWhiteSpace = false; - XML.prettyPrinting = false; - XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil. - XML.prettyIndent = 4; + // Process plugin help entries. + XML.ignoreWhiteSpace = false; + XML.prettyPrinting = false; + XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil. + XML.prettyIndent = 4; - let body = XML(); - for (let [, context] in Iterator(plugins.contexts)) - if (context.INFO instanceof XML) - body +=

{context.INFO.@summary}

+ - context.INFO; + let body = XML(); + for (let [, context] in Iterator(plugins.contexts)) + if (context.INFO instanceof XML) + body +=

{context.INFO.@summary}

+ + context.INFO; - let help = '\n' + - '\n' + - '' + - -

Using Plugins

+ let help = '\n' + + '\n' + + '' + + +

Using Plugins

- {body} -
.toXMLString(); - fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help]; + {body} +
.toXMLString(); + fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help]; - addTags("plugins", util.httpGet("liberator://help/plugins").responseXML); + addTags("plugins", util.httpGet("liberator://help/plugins").responseXML); + this.helpInitialized = true; + } }, /** @@ -632,6 +637,7 @@ const Liberator = Module("liberator", { * @returns {string} */ help: function (topic, unchunked) { + liberator.initHelp(); if (!topic) { let helpFile = unchunked ? "all" : options["helpfile"]; if (helpFile in services.get("liberator:").FILE_MAP) @@ -737,22 +743,12 @@ const Liberator = Module("liberator", { */ open: function (urls, params, force) { // convert the string to an array of converted URLs - // -> see util.stringToURLArray for more details + // -> see liberator.stringToURLArray for more details // // This is strange. And counterintuitive. Is it really // necessary? --Kris - if (typeof urls == "string") { - // rather switch to the tab instead of opening a new url in case of "12: Tab Title" like "urls" - if (liberator.has("tabs")) { - let matches = urls.match(/^(\d+):/); - if (matches) { - tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based - return; - } - } - - urls = util.stringToURLArray(urls); - } + if (typeof urls == "string") + urls = liberator.stringToURLArray(urls); if (urls.length > 20 && !force) { commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no]) ", @@ -765,7 +761,7 @@ const Liberator = Module("liberator", { let flags = 0; params = params || {}; - if (params instanceof Array) + if (isarray(params)) params = { where: params }; for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" })) @@ -773,15 +769,11 @@ const Liberator = Module("liberator", { flags |= Ci.nsIWebNavigation["LOAD_FLAGS_" + flag]; let where = params.where || liberator.CURRENT_TAB; + let background = ("background" in params) ? params.background : params.where == liberator.NEW_BACKGROUND_TAB; if ("from" in params && liberator.has("tabs")) { if (!('where' in params) && options.get("newtab").has("all", params.from)) - where = liberator.NEW_BACKGROUND_TAB; - if (options.get("activate").has("all", params.from)) { - if (where == liberator.NEW_TAB) - where = liberator.NEW_BACKGROUND_TAB; - else if (where == liberator.NEW_BACKGROUND_TAB) - where = liberator.NEW_TAB; - } + where = liberator.NEW_TAB; + background = !options.get("activate").has("all", params.from); } if (urls.length == 0) @@ -799,7 +791,6 @@ const Liberator = Module("liberator", { browser.loadURIWithFlags(url, flags, null, null, postdata); break; - case liberator.NEW_BACKGROUND_TAB: case liberator.NEW_TAB: if (!liberator.has("tabs")) { open(urls, liberator.NEW_WINDOW); @@ -808,7 +799,7 @@ const Liberator = Module("liberator", { options.withContext(function () { options.setPref("browser.tabs.loadInBackground", true); - browser.loadOneTab(url, null, null, postdata, where == liberator.NEW_BACKGROUND_TAB); + browser.loadOneTab(url, null, null, postdata, background); }); break; @@ -832,7 +823,7 @@ const Liberator = Module("liberator", { for (let [, url] in Iterator(urls)) { open(url, where); - where = liberator.NEW_BACKGROUND_TAB; + background = true; } }, @@ -865,6 +856,64 @@ const Liberator = Module("liberator", { window.goQuitApplication(); }, + /** + * Returns an array of URLs parsed from str. + * + * Given a string like 'google bla, www.osnews.com' return an array + * ['www.google.com/search?q=bla', 'www.osnews.com'] + * + * @param {string} str + * @returns {string[]} + */ + stringToURLArray: function stringToURLArray(str) { + let urls; + + if (options["urlseparator"]) + urls = util.splitLiteral(str, RegExp("\\s*" + options["urlseparator"] + "\\s*")); + else + urls = [str]; + + return urls.map(function (url) { + if (/^\.?\//.test(url)) { + try { + // Try to find a matching file. + let file = io.File(url); + if (file.exists() && file.isReadable()) + return services.get("io").newFileURI(file).spec; + } + catch (e) {} + } + + // strip each 'URL' - makes things simpler later on + url = url.replace(/^\s+|\s+$/, ""); + + // Look for a valid protocol + let proto = url.match(/^([-\w]+):/); + if (proto && Cc["@mozilla.org/network/protocol;1?name=" + proto[1]]) + // Handle as URL, but remove spaces. Useful for copied/'p'asted URLs. + return url.replace(/\s*\n+\s*/g, ""); + + // Ok, not a valid proto. If it looks like URL-ish (foo.com/bar), + // let Gecko figure it out. + if (/^[a-zA-Z0-9-.]+(?:\/|$)/.test(url) && /[.\/]/.test(url) && !/\s/.test(url) || /^[a-zA-Z0-9-.]+:\d+(?:\/|$)/.test(url)) + return url; + + // TODO: it would be clearer if the appropriate call to + // getSearchURL was made based on whether or not the first word was + // indeed an SE alias rather than seeing if getSearchURL can + // process the call usefully and trying again if it fails + + // check for a search engine match in the string, then try to + // search for the whole string in the default engine + let searchURL = bookmarks.getSearchURL(url, false) || bookmarks.getSearchURL(url, true); + if (searchURL) + return searchURL; + + // Hmm. No defsearch? Let the host app deal with it, then. + return url; + }); + }, + /* * Tests a condition and throws a FailedAssertion error on * failure. @@ -1271,17 +1320,8 @@ const Liberator = Module("liberator", { let arg = args[0]; try { - // TODO: why are these sorts of properties arrays? --djk - let dialogs = config.dialogs; - - for (let [, dialog] in Iterator(dialogs)) { - if (util.compareIgnoreCase(arg, dialog[0]) == 0) { - dialog[2](); - return; - } - } - - liberator.echoerr("E475: Invalid argument: " + arg); + liberator.assert(args[0] in config.dialogs, "E475: Invalid argument: " + arg); + config.dialogs[args[0]][1](); } catch (e) { liberator.echoerr("Error opening " + arg.quote() + ": " + e); @@ -1331,19 +1371,89 @@ const Liberator = Module("liberator", { } }); + /////////////////////////////////////////////////////////////////////////// + + if (typeof AddonManager == "undefined") { + modules.AddonManager = { + getInstallForFile: function (file, callback, mimetype) { + callback({ + install: function () { + services.get("extensionManager").installItemFromFile(file, "app-profile"); + } + }); + }, + getAddonById: function (id, callback) { + let addon = id; + if (!isobject(addon)) + addon = services.get("extensionManager").getItemForID(id); + if (!addon) + return callback(null); + + function getRdfProperty(item, property) { + let resource = services.get("rdf").GetResource("urn:mozilla:item:" + item.id); + let value = ""; + + if (resource) { + let target = services.get("extensionManager").datasource.GetTarget(resource, + services.get("rdf").GetResource("http://www.mozilla.org/2004/em-rdf#" + property), true); + if (target && target instanceof Ci.nsIRDFLiteral) + value = target.Value; + } + + return value; + } + + ["aboutURL", "creator", "description", "developers", + "homepageURL", "iconURL", "installDate", "name", + "optionsURL", "releaseNotesURI", "updateDate"].forEach(function (item) { + addon[item] = getRdfProperty(addon, item); + }); + addon.isActive = getRdfProperty(addon, "isDisabled") != "true"; + + addon.uninstall = function () { + services.get("extensionManager").uninstallItem(this.id); + }; + addon.appDisabled = false; + addon.__defineGetter("userDisabled", function() getRdfProperty("userDisabled") == "true"); + addon.__defineSetter__("userDisabled", function(val) { + services.get("extensionManager")[val ? "enableItem" : "disableItem"](this.id); + }); + + return callback(addon); + }, + getAddonsByTypes: function (types, callback) { + let res = []; + for (let [,type] in Iterator(types)) + for (let [,item] in Iterator(services.get("extensionManager") + .getItemList(Ci.nsIUpdateItem["TYPE_" + type.toUpperCase()], {}))) + res.append(this.getAddonById(item)); + return res; + } + }; + + } + + /////////////////////////////////////////////////////////////////////////// + + function callResult(method) { + let args = Array.slice(arguments, 1); + return function (result) { result[method].apply(result, args) }; + } + commands.add(["exta[dd]"], "Install an extension", function (args) { - let file = io.File(args[0]); - - if (file.exists() && file.isReadable() && file.isFile()) - services.get("extensionManager").installItemFromFile(file, "app-profile"); - else { - if (file.exists() && file.isDirectory()) - liberator.echomsg("Cannot install a directory: \"" + file.path + "\"", 0); + let url = args[0]; + let file = io.File(url); + if (!file.exists()) + AddonManager.getInstallForURL(url, callResult("install"), "application/x-xpinstall"); + else if (file.isReadable() && file.isFile()) + AddonManager.getInstallForFile(file, callResult("install"), "application/x-xpinstall"); + else if (file.isDirectory()) + liberator.echomsg("Cannot install a directory: \"" + file.path + "\"", 0); + else liberator.echoerr("E484: Can't open file " + file.path); - } }, { argCount: "1", completer: function (context) { @@ -1357,38 +1467,35 @@ const Liberator = Module("liberator", { { name: "extde[lete]", description: "Uninstall an extension", - action: "uninstallItem" + action: callResult("uninstall") }, { name: "exte[nable]", description: "Enable an extension", - action: "enableItem", - filter: function ({ item: e }) !e.enabled + action: function (addon) addon.userDisabled = false, + filter: function ({ item: e }) e.userDisabled }, { name: "extd[isable]", description: "Disable an extension", - action: "disableItem", - filter: function ({ item: e }) e.enabled + action: function (addon) addon.userDisabled = true, + filter: function ({ item: e }) !e.userDisabled } ].forEach(function (command) { commands.add([command.name], command.description, function (args) { let name = args[0]; - function action(e) { services.get("extensionManager")[command.action](e.id); }; - if (args.bang) - liberator.extensions.forEach(function (e) { action(e); }); - else { - liberator.assert(name, "E471: Argument required"); // XXX + liberator.assert(!name, "E488: Trailing characters"); + else + liberator.assert(name, "E471: Argument required"); - let extension = liberator.getExtension(name); - if (extension) - action(extension); - else - liberator.echoerr("E474: Invalid argument"); - } + AddonManager.getAddonsByTypes(["extension"], function (list) { + if (!args.bang) + list = list.filter(function (extension) extension.name == name); + list.forEach(command.action); + }); }, { argCount: "?", // FIXME: should be "1" bang: true, @@ -1404,51 +1511,64 @@ const Liberator = Module("liberator", { commands.add(["exto[ptions]", "extp[references]"], "Open an extension's preference dialog", function (args) { - let extension = liberator.getExtension(args[0]); - liberator.assert(extension && extension.options, - "E474: Invalid argument"); - if (args.bang) - window.openDialog(extension.options, "_blank", "chrome"); - else - liberator.open(extension.options, { from: "extoptions" }); + AddonManager.getAddonsByTypes(["extension"], function (list) { + list = list.filter(function (extension) extension.name == args[0]); + if (!list.length || !list[0].optionsURL) + liberator.echoerr("E474: Invalid argument"); + else if (args.bang) + window.openDialog(list[0].optionsURL, "_blank", "chrome"); + else + liberator.open(list[0].optionsURL, { from: "extoptions" }); + }); }, { argCount: "1", bang: true, completer: function (context) { completion.extension(context); - context.filters.push(function ({ item: e }) e.options); + context.filters.push(function ({ item: e }) e.isActive && e.optionsURL); }, literal: 0 }); // TODO: maybe indicate pending status too? - commands.add(["extens[ions]"], + commands.add(["extens[ions]", "exts"], "List available extensions", function (args) { - let filter = args[0] || ""; - let extensions = liberator.extensions.filter(function (e) e.name.indexOf(filter) >= 0); + AddonManager.getAddonsByTypes(["extension"], function (extensions) { + if (args[0]) + extensions = extensions.filter(function (extension) extension.name.indexOf(args[0]) >= 0); + extensions.sort(function (a, b) String.localeCompare(a.name, b.name)); - if (extensions.length > 0) { - let list = template.tabular( - ["Name", "Version", "Status", "Description"], [], - ([template.icon(e, e.name), - e.version, - e.enabled ? enabled - : disabled, - e.description] for ([, e] in Iterator(extensions))) - ); + if (extensions.length > 0) { + let list = template.tabular( + ["Name", "Version", "Status", "Description"], [], + ([template.icon({ icon: e.iconURL }, e.name), + e.version, + (e.isActive ? enabled + : disabled) + + ((e.userDisabled || e.appDisabled) == !e.isActive ? XML() : + <> ({e.userDisabled || e.appDisabled + ? disabled + : enabled} + on restart) + ), + e.description] for ([, e] in Iterator(extensions))) + ); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - else { - if (filter) - liberator.echoerr("Exxx: No extension matching \"" + filter + "\""); - else - liberator.echoerr("No extensions installed"); - } + commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + } + else { + if (filter) + liberator.echoerr("Exxx: No extension matching \"" + filter + "\""); + else + liberator.echoerr("No extensions installed"); + } + }); }, { argCount: "?" }); + /////////////////////////////////////////////////////////////////////////// + commands.add(["exu[sage]"], "List all Ex commands with a short description", function (args) { Liberator.showHelpIndex("ex-cmd-index", commands, args.bang); }, { @@ -1704,17 +1824,22 @@ const Liberator = Module("liberator", { completion: function () { completion.dialog = function dialog(context) { context.title = ["Dialog"]; - context.completions = config.dialogs; + context.completions = [[k, v[0]] for ([k, v] in Iterator(config.dialogs))]; }; completion.extension = function extension(context) { context.title = ["Extension"]; context.anchored = false; - context.keys = { text: "name", description: "description", icon: "icon" }, - context.completions = liberator.extensions; + context.keys = { text: "name", description: "description", icon: "iconURL" }, + context.incomplete = true; + AddonManager.getAddonsByTypes(["extension"], function (addons) { + context.incomplete = false; + context.completions = addons; + }); }; completion.help = function help(context, unchunked) { + liberator.initHelp(); context.title = ["Help"]; context.anchored = false; context.completions = services.get("liberator:").HELP_TAGS; @@ -1729,6 +1854,7 @@ const Liberator = Module("liberator", { context.completions = liberator.menuItems; }; + var toolbox = document.getElementById("navigator-toolbox"); completion.toolbar = function toolbar(context) { context.title = ["Toolbar"]; context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" }; @@ -1817,8 +1943,6 @@ const Liberator = Module("liberator", { if (options["loadplugins"]) liberator.loadPlugins(); - liberator.initHelp(); - // after sourcing the initialization files, this function will set // all gui options to their default values, if they have not been // set before by any RC file @@ -1841,6 +1965,7 @@ const Liberator = Module("liberator", { statusline.update(); liberator.log(config.name + " fully initialized", 0); + liberator.initialized = true; } }); diff --git a/common/content/liberator.xul b/common/content/liberator.xul index c1acf2bb..674970d6 100644 --- a/common/content/liberator.xul +++ b/common/content/liberator.xul @@ -67,7 +67,7 @@