diff options
author | Kris Maglione <kris@vimperator.org> | 2010-08-31 21:09:13 -0400 |
---|---|---|
committer | Kris Maglione <kris@vimperator.org> | 2010-08-31 21:09:13 -0400 |
commit | 8b0d9586b23eb166fafb064e75c4956021d73ca1 (patch) | |
tree | cc3a8bdda4e19dc18eadd5ed0edc4aa2131b43e2 /common | |
parent | 5632e14721897b9e7e23d493f95358bb7df73314 (diff) | |
download | pentadactyl-8b0d9586b23eb166fafb064e75c4956021d73ca1.tar.gz |
Merge testing.
--HG--
rename : common/content/base.js => common/modules/base.jsm
rename : common/content/services.js => common/modules/services.jsm
rename : common/content/style.js => common/modules/styles.jsm
rename : common/content/template.js => common/modules/template.jsm
rename : common/content/util.js => common/modules/util.jsm
Diffstat (limited to 'common')
33 files changed, 2144 insertions, 2180 deletions
diff --git a/common/content/autocommands.js b/common/content/autocommands.js index 899019f7..97232f9b 100755 --- a/common/content/autocommands.js +++ b/common/content/autocommands.js @@ -14,8 +14,6 @@ const AutoCommand = Struct("event", "pattern", "command"); * @instance autocommands */ const AutoCommands = Module("autocommands", { - requires: ["config"], - init: function () { this._store = []; }, @@ -83,7 +81,7 @@ const AutoCommands = Module("autocommands", { } }); - let list = template.commandOutput(commandline.command, + commandline.commandOutput( <table> <tr highlight="Title"> <td colspan="2">----- Auto Commands -----</td> @@ -101,8 +99,6 @@ const AutoCommands = Module("autocommands", { </tr>)) } </table>); - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, /** @@ -137,7 +133,6 @@ const AutoCommands = Module("autocommands", { autoCmd.command.call(autoCmd, args); } catch (e) { - dactyl.reportError(e); dactyl.echoerr(e); } } @@ -203,7 +198,12 @@ const AutoCommands = Module("autocommands", { return args["-javascript"] ? completion.javascript(context) : completion.ex(context); }, literal: 2, - options: [[["-javascript", "-js"], commands.OPTION_NOARG]] + options: [ + { + names: ["-javascript", "-js"], + description: "Interperate the action as JavaScript code rather than an ex command", + } + ] }); [ diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index fff84480..095b4c21 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -10,193 +10,13 @@ const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; // also includes methods for dealing with keywords and search engines const Bookmarks = Module("bookmarks", { - requires: ["autocommands", "config", "dactyl", "storage", "services"], - init: function () { - const faviconService = services.get("favicon"); - const bookmarksService = services.get("bookmarks"); - const historyService = services.get("history"); - const tagging = PlacesUtils.tagging; - - this.getFavicon = getFavicon; - function getFavicon(uri) { - try { - return faviconService.getFaviconImageForPage(util.newURI(uri)).spec; - } - catch (e) { - return ""; - } - } - - // Fix for strange Firefox bug: - // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]" - // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" - // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89" - // data: no] - // Source file: file://~firefox/components/nsTaggingService.js - tagging.getTagsForURI(window.makeURI("http://mysterious.bug"), {}); - - const Bookmark = Struct("url", "title", "icon", "keyword", "tags", "id"); - const Keyword = Struct("keyword", "title", "icon", "url"); - Bookmark.defaultValue("icon", function () getFavicon(this.url)); - Bookmark.prototype.__defineGetter__("extra", function () [ - ["keyword", this.keyword, "Keyword"], - ["tags", this.tags.join(", "), "Tag"] - ].filter(function (item) item[1])); - - const storage = modules.storage; - function Cache(name, store) { - const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder]; - const sleep = dactyl.sleep; // Storage objects are global to all windows, 'dactyl' isn't. - - let bookmarks = []; - let self = this; - - this.__defineGetter__("name", function () name); - this.__defineGetter__("store", function () store); - this.__defineGetter__("bookmarks", function () this.load()); - - this.__defineGetter__("keywords", - function () [Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]); - - this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks))); - - function loadBookmark(node) { - if (node.uri == null) // How does this happen? - return false; - let uri = util.newURI(node.uri); - let keyword = bookmarksService.getKeywordForBookmark(node.itemId); - let tags = tagging.getTagsForURI(uri, {}) || []; - let bmark = Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); - - bookmarks.push(bmark); - return bmark; - } - - function readBookmark(id) { - return { - itemId: id, - uri: bookmarksService.getBookmarkURI(id).spec, - title: bookmarksService.getItemTitle(id) - }; - } - - function deleteBookmark(id) { - let length = bookmarks.length; - bookmarks = bookmarks.filter(function (item) item.id != id); - return bookmarks.length < length; - } - - this.findRoot = function findRoot(id) { - do { - var root = id; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return root; - }; - - this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0; - - this.isRegularBookmark = function findRoot(id) { - do { - var root = id; - if (services.get("livemark") && services.get("livemark").isLivemark(id)) - return false; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return rootFolders.indexOf(root) >= 0; - }; - - // since we don't use a threaded bookmark loading (by set preload) - // anymore, is this loading synchronization still needed? --mst - let loading = false; - this.load = function load() { - if (loading) { - while (loading) - sleep(10); - return bookmarks; - } - - // update our bookmark cache - bookmarks = []; - loading = true; - - let folders = rootFolders.slice(); - let query = historyService.getNewQuery(); - let options = historyService.getNewQueryOptions(); - while (folders.length > 0) { - query.setFolders(folders, 1); - folders.shift(); - let result = historyService.executeQuery(query, options); - let folder = result.root; - folder.containerOpen = true; - - // iterate over the immediate children of this folder - for (let i = 0; i < folder.childCount; i++) { - let node = folder.getChild(i); - if (node.type == node.RESULT_TYPE_FOLDER) // folder - folders.push(node.itemId); - else if (node.type == node.RESULT_TYPE_URI) // bookmark - loadBookmark(node); - } - - // close a container after using it! - folder.containerOpen = false; - } - this.__defineGetter__("bookmarks", function () bookmarks); - loading = false; - return bookmarks; - }; - - var observer = { - onBeginUpdateBatch: function onBeginUpdateBatch() {}, - onEndUpdateBatch: function onEndUpdateBatch() {}, - onItemVisited: function onItemVisited() {}, - onItemMoved: function onItemMoved() {}, - onItemAdded: function onItemAdded(itemId, folder, index) { - // dactyl.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n"); - if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) { - if (self.isBookmark(itemId)) { - let bmark = loadBookmark(readBookmark(itemId)); - storage.fireEvent(name, "add", bmark); - } - } - }, - onItemRemoved: function onItemRemoved(itemId, folder, index) { - // dactyl.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n"); - if (deleteBookmark(itemId)) - storage.fireEvent(name, "remove", itemId); - }, - onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { - if (isAnnotation) - return; - // dactyl.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n"); - let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; - if (bookmark) { - if (property == "tags") - value = tagging.getTagsForURI(util.newURI(bookmark.url), {}); - if (property in bookmark) - bookmark[property] = value; - storage.fireEvent(name, "change", itemId); - } - }, - QueryInterface: function QueryInterface(iid) { - if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) - return this; - throw Cr.NS_ERROR_NO_INTERFACE; - } - }; - - bookmarksService.addObserver(observer, false); - } - let bookmarkObserver = function (key, event, arg) { if (event == "add") autocommands.trigger("BookmarkAdd", arg); statusline.updateUrl(); }; - this._cache = storage.newObject("bookmark-cache", Cache, { store: false }); storage.addObserver("bookmark-cache", bookmarkObserver, window); }, @@ -217,7 +37,7 @@ const Bookmarks = Module("bookmarks", { try { let uri = util.createURI(url); if (!force) { - for (let bmark in this._cache) { + for (let bmark in bookmarkcache) { if (bmark[0] == uri.spec) { var id = bmark[5]; if (title) @@ -268,7 +88,7 @@ const Bookmarks = Module("bookmarks", { isBookmarked: function isBookmarked(url) { try { return services.get("bookmarks").getBookmarkIdsForURI(makeURI(url), {}) - .some(this._cache.isRegularBookmark); + .some(bookmarkcache.isRegularBookmark); } catch (e) { return false; @@ -280,7 +100,7 @@ const Bookmarks = Module("bookmarks", { try { let uri = util.newURI(url); let bmarks = services.get("bookmarks").getBookmarkIdsForURI(uri, {}) - .filter(this._cache.isRegularBookmark); + .filter(bookmarkcache.isRegularBookmark); bmarks.forEach(services.get("bookmarks").removeItem); return bmarks.length; } @@ -350,7 +170,7 @@ const Bookmarks = Module("bookmarks", { // format of returned array: // [keyword, helptext, url] getKeywords: function getKeywords() { - return this._cache.keywords; + return bookmarkcache.keywords; }, // full search string including engine name as first word in @param text @@ -451,38 +271,53 @@ const Bookmarks = Module("bookmarks", { "Show jumplist", function () { let sh = history.session; - let list = template.jumps(sh.index, sh); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput(template.jumps(sh.index, sh)); }, { argCount: "0" }); // TODO: Clean this up. - function tags(context, args) { - let filter = context.filter; - let have = filter.split(","); + const tags = { + names: ["-tags", "-T"], + description: "A comma-separated list of tags", + completer: function tags(context, args) { + let filter = context.filter; + let have = filter.split(","); - args.completeFilter = have.pop(); + args.completeFilter = have.pop(); - let prefix = filter.substr(0, filter.length - args.completeFilter.length); - let tags = array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(this._cache.bookmarks))])); + let prefix = filter.substr(0, filter.length - args.completeFilter.length); + let tags = array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(bookmarkcache.bookmarks))])); - return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; - } + return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; + }, + type: CommandOption.LIST + }; - function title(context, args) { - if (!args.bang) - return [[content.document.title, "Current Page Title"]]; - context.keys.text = "title"; - context.keys.description = "url"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); - } + const title = { + names: ["-title", "-t"], + description: "Bookmark page title or description", + completer: function title(context, args) { + if (!args.bang) + return [[content.document.title, "Current Page Title"]]; + context.keys.text = "title"; + context.keys.description = "url"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); + }, + type: CommandOption.STRING + }; - function keyword(context, args) { - if (!args.bang) - return []; - context.keys.text = "keyword"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); - } + const keyword = { + names: ["-keyword", "-k"], + description: "Keyword by which this bookmark may be opened (:open {keyword})", + completer: function keyword(context, args) { + if (!args.bang) + return []; + context.keys.text = "keyword"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); + }, + type: CommandOption.STRING, + validator: function (arg) /^\S+$/.test(arg) + }; commands.add(["bma[rk]"], "Add a bookmark", @@ -503,14 +338,13 @@ const Bookmarks = Module("bookmarks", { bang: true, completer: function (context, args) { if (!args.bang) { + context.title = ["Page URL"]; context.completions = [[content.document.documentURI, "Current Location"]]; return; } completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); }, - options: [[["-title", "-t"], commands.OPTION_STRING, null, title], - [["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]] + options: [title, tags, keyword] }); commands.add(["bmarks"], @@ -525,22 +359,26 @@ const Bookmarks = Module("bookmarks", { context.filter = args.join(" "); completion.bookmark(context, args["-tags"]); }, - options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-max", "-m"], commands.OPTION_INT]] + options: [tags, + { + names: ["-max", "-m"], + description: "The maximum number of items to list or open", + type: CommandOption.INT + } + ] }); commands.add(["delbm[arks]"], "Delete a bookmark", function (args) { - if (args.bang) { + if (args.bang) commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", function (resp) { if (resp && resp.match(/^y(es)?$/i)) { - bookmarks._cache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); }); + bookmarkcache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); }); dactyl.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE); } }); - } else { let url = args.string || buffer.URL; let deletedCount = bookmarks.remove(url); @@ -621,9 +459,7 @@ const Bookmarks = Module("bookmarks", { if (v) context.filters.push(function (item) this._match(v, item[k])); } - // Need to make a copy because set completions() checks instanceof Array, - // and this may be an Array from another window. - context.completions = Array.slice(storage["bookmark-cache"].bookmarks); + context.completions = bookmarkcache.bookmarks; completion.urls(context, tags); }; diff --git a/common/content/buffer.js b/common/content/buffer.js index cb50f16f..ed050a00 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -8,8 +8,6 @@ /** @scope modules */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", modules); - const Point = Struct("x", "y"); /** @@ -19,8 +17,6 @@ const Point = Struct("x", "y"); * @instance buffer */ const Buffer = Module("buffer", { - requires: ["config", "util"], - init: function () { this.evaluateXPath = util.evaluateXPath; this.pageInfo = {}; @@ -146,10 +142,6 @@ const Buffer = Module("buffer", { }, destroy: function () { - try { - config.browser.removeProgressListener(this.progressListener); - } - catch (e) {} // Why? --djk }, _triggerLoadAutocmd: function _triggerLoadAutocmd(name, doc) { @@ -214,14 +206,10 @@ const Buffer = Module("buffer", { /** * @property {Object} The document loading progress listener. */ - progressListener: { - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsIXULBrowserWindow - ]), - + progressListener: update({ __proto__: window.XULBrowserWindow }, { // XXX: function may later be needed to detect a canceled synchronous openURL() - onStateChange: function (webProgress, request, flags, status) { + onStateChange: function onStateChange(webProgress, request, flags, status) { + onStateChange.superapply(this, arguments); // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also // receive statechange events for loading images and other parts of the web page if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) { @@ -247,7 +235,8 @@ const Buffer = Module("buffer", { } }, // for notifying the user about secure web pages - onSecurityChange: function (webProgress, request, state) { + onSecurityChange: function onSecurityChange(webProgress, request, state) { + onSecurityChange.superapply(this, arguments); // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) statusline.setClass("insecure"); @@ -258,14 +247,17 @@ const Buffer = Module("buffer", { else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH) statusline.setClass("secure"); }, - onStatusChange: function (webProgress, request, status, message) { + onStatusChange: function onStatusChange(webProgress, request, status, message) { + onStatusChange.superapply(this, arguments); statusline.updateUrl(message); }, - onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { + onProgressChange: function onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { + onProgressChange.superapply(this, arguments); statusline.updateProgress(curTotalProgress/maxTotalProgress); }, // happens when the users switches tabs - onLocationChange: function () { + onLocationChange: function onLocationChange() { + onLocationChange.superapply(this, arguments); statusline.updateUrl(); statusline.updateProgress(); @@ -279,10 +271,12 @@ const Buffer = Module("buffer", { }, 500); }, // called at the very end of a page load - asyncUpdateUI: function () { + asyncUpdateUI: function asyncUpdateUI() { + asyncUpdateUI.superapply(this, arguments); setTimeout(function () { statusline.updateUrl(); }, 100); }, - setOverLink: function (link, b) { + setOverLink: function setOverLink(link, b) { + setOverLink.superapply(this, arguments); let ssli = options["showstatuslinks"]; if (link && ssli) { if (ssli == 1) @@ -298,15 +292,7 @@ const Buffer = Module("buffer", { modes.show(); } }, - - // nsIXULBrowserWindow stubs - setJSDefaultStatus: function (status) {}, - setJSStatus: function (status) {}, - - // Stub for something else, presumably. Not in any documented - // interface. - onLinkIconAvailable: function () {} - }, + }), /** * @property {Array} The alternative style sheets for the current @@ -321,19 +307,6 @@ const Buffer = Module("buffer", { }, /** - * @property {Array[Window]} All frames in the current buffer. - */ - get allFrames() { - let frames = []; - (function (frame) { - if (frame.document.body instanceof HTMLBodyElement) - frames.push(frame); - Array.forEach(frame.frames, arguments.callee); - })(window.content); - return frames; - }, - - /** * @property {Object} A map of page info sections to their * content generating functions. */ @@ -448,6 +421,19 @@ const Buffer = Module("buffer", { }, /** + * Returns a list of all frames in the given current buffer. + */ + allFrames: function (win) { + let frames = []; + (function rec(frame) { + if (frame.document.body instanceof HTMLBodyElement) + frames.push(frame); + Array.forEach(frame.frames, rec); + })(win || window.content); + return frames; + }, + + /** * Returns the currently selected word. If the selection is * null, it tries to guess the word that the caret is * positioned in. @@ -581,7 +567,7 @@ const Buffer = Module("buffer", { let ret = followFrame(window.content); if (!ret) // only loop through frames if the main content didn't match - ret = Array.some(buffer.allFrames.frames, followFrame); + ret = Array.some(buffer.allFrames().frames, followFrame); if (!ret) dactyl.beep(); @@ -805,7 +791,7 @@ const Buffer = Module("buffer", { return; count = Math.max(count, 1); - let frames = buffer.allFrames; + let frames = buffer.allFrames(); if (frames.length == 0) // currently top is always included return; @@ -1367,7 +1353,11 @@ const Buffer = Module("buffer", { }; }, events: function () { - /* + try { + config.browser.removeProgressListener(window.XULBrowserWindow); + } + catch (e) {} // Why? --djk + config.browser.addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_ALL); window.XULBrowserWindow = this.progressListener; window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) @@ -1376,12 +1366,6 @@ const Buffer = Module("buffer", { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIXULWindow) .XULBrowserWindow = this.progressListener; - */ - - try { - config.browser.addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_ALL); - } - catch (e) {} // Why? --djk let appContent = document.getElementById("appcontent"); if (appContent) { diff --git a/common/content/commandline.js b/common/content/commandline.js index 974d7446..fc4c2075 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -15,8 +15,6 @@ * this class when the chrome is ready. */ const CommandLine = Module("commandline", { - requires: ["config", "dactyl", "modes", "services", "storage", "template", "util"], - init: function () { const self = this; @@ -95,7 +93,7 @@ const CommandLine = Module("commandline", { ////////////////////// TIMERS ////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - this._statusTimer = new Timer(5, 100, function statusTell() { + this._statusTimer = Timer(5, 100, function statusTell() { if (self._completions == null) return; if (self._completions.selected == null) @@ -104,7 +102,7 @@ const CommandLine = Module("commandline", { statusline.updateProgress("match " + (self._completions.selected + 1) + " of " + self._completions.items.length); }); - this._autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) { + this._autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) { if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { self._completions.complete(true, false); self._completions.itemList.show(); @@ -114,8 +112,8 @@ const CommandLine = Module("commandline", { // This timer just prevents <Tab>s from queueing up when the // system is under load (and, thus, giving us several minutes of // the completion list scrolling). Multiple <Tab> presses are - // still processed normally, as the time is flushed on "keyup". - this._tabTimer = new Timer(0, 0, function tabTell(event) { + // still processed normally, as the timer is flushed on "keyup". + this._tabTimer = Timer(0, 0, function tabTell(event) { if (self._completions) self._completions.tab(event.shiftKey); }); @@ -464,6 +462,18 @@ const CommandLine = Module("commandline", { }, /** + * Displays the multi-line output of a command, preceded by the last + * executed ex command string. + * + * @param {XML} xml The output as an E4X XML object. + */ + commandOutput: function (xml) { + XML.ignoreWhitespace = false; + XML.prettyPrinting = false; + this.echo(<>:{this.command}<br/>{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE); + }, + + /** * Hides the command line, and shows any status messages that * are under it. */ @@ -492,7 +502,7 @@ const CommandLine = Module("commandline", { * commandline.FORCE_MULTILINE - Forces the message to appear in * the MOW. */ - echo: function echo(str, highlightGroup, flags) { + echo: requiresMainThread(function echo(str, highlightGroup, flags) { // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst if (this._silent) return; @@ -506,37 +516,34 @@ const CommandLine = Module("commandline", { services.get("windowWatcher").activeWindow.dactyl) return; - // The DOM isn't threadsafe. It must only be accessed from the main thread. - util.callInMainThread(function () { - if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed) - return; + if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed) + return; - let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); - let action = this._echoLine; + let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); + let action = this._echoLine; - // TODO: this is all a bit convoluted - clean up. - // assume that FORCE_MULTILINE output is fully styled - if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this.widgets.message.value == this._lastEcho)) { - highlightGroup += " Message"; - action = this._echoMultiline; - } + // TODO: this is all a bit convoluted - clean up. + // assume that FORCE_MULTILINE output is fully styled + if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this.widgets.message.value == this._lastEcho)) { + highlightGroup += " Message"; + action = this._echoMultiline; + } - if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) - action = this._echoMultiline; + if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) + action = this._echoMultiline; - if (single) - this._lastEcho = null; - else { - if (this.widgets.message.value == this._lastEcho) - this._echoMultiline(<span highlight="Message">{this._lastEcho}</span>, - this.widgets.message.getAttributeNS(NS.uri, "highlight")); - this._lastEcho = (action == this._echoLine) && str; - } + if (single) + this._lastEcho = null; + else { + if (this.widgets.message.value == this._lastEcho) + this._echoMultiline(<span highlight="Message">{this._lastEcho}</span>, + this.widgets.message.getAttributeNS(NS.uri, "highlight")); + this._lastEcho = (action == this._echoLine) && str; + } - if (action) - action.call(this, str, highlightGroup, single); - }, this); - }, + if (action) + action.call(this, str, highlightGroup, single); + }), /** * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may @@ -675,7 +682,7 @@ const CommandLine = Module("commandline", { modes.pop(); } } - else { // any other key + else { //this.resetCompletions(); } // allow this event to be handled by the host app @@ -1097,7 +1104,7 @@ const CommandLine = Module("commandline", { } let hist = this.store.get(this.index); - // user pressed DOWN when there is no newer this._history item + // user pressed DOWN when there is no newer history item if (!hist) hist = this.original; else @@ -1165,13 +1172,6 @@ const CommandLine = Module("commandline", { get wildtype() this.wildtypes[this.wildIndex] || "", - get type() ({ - list: this.wildmode.checkHas(this.wildtype, "list"), - longest: this.wildmode.checkHas(this.wildtype, "longest"), - first: this.wildmode.checkHas(this.wildtype, ""), - full: this.wildmode.checkHas(this.wildtype, "full") - }), - complete: function complete(show, tabPressed) { this.context.reset(); this.context.tabPressed = tabPressed; @@ -1181,6 +1181,9 @@ const CommandLine = Module("commandline", { this.wildIndex = 0; }, + haveType: function (type) + this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type), + preview: function preview() { this.previewClear(); if (this.wildIndex < 0 || this.suffix || !this.items.length) @@ -1364,7 +1367,7 @@ const CommandLine = Module("commandline", { break; } - if (this.type.list) + if (this.haveType("list")) this.itemList.show(); this.wildIndex = Math.constrain(this.wildIndex + 1, 0, this.wildtypes.length - 1); @@ -1549,17 +1552,14 @@ const ItemList = Class("ItemList", { this._completionElements = []; var iframe = document.getElementById(id); - if (!iframe) { - dactyl.log("No iframe with id: " + id + " found, strange things may happen!"); // "The truth is out there..." -- djk - return; // XXX - } this._doc = iframe.contentDocument; + this._win = iframe.contentWindow; this._container = iframe.parentNode; this._doc.body.id = id + "-content"; this._doc.body.appendChild(this._doc.createTextNode("")); - this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where this._completions/MOW are shown at once, or ls=0. Should use :highlight. + this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight. this._gradient = template.gradient("GradientLeft", "GradientRight"); @@ -1578,7 +1578,8 @@ const ItemList = Class("ItemList", { if (this._container.collapsed) this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px"; - this._minHeight = Math.max(this._minHeight, this._divNodes.completions.getBoundingClientRect().bottom); + this._minHeight = Math.max(this._minHeight, + this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom); this._container.height = this._minHeight; if (this._container.collapsed) diff --git a/common/content/commands.js b/common/content/commands.js index 1cda7784..cb7cc3ae 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -12,6 +12,75 @@ // commands.add() instead /** + * A structure representing the options available for a command. + * + * @property {[string]} names An array of option names. The first name + * is the canonical option name. + * @property {number} type The option's value type. This is one of: + * (@link CommandOption.NOARG), + * (@link CommandOption.STRING), + * (@link CommandOption.BOOL), + * (@link CommandOption.INT), + * (@link CommandOption.FLOAT), + * (@link CommandOption.LIST), + * (@link CommandOption.ANY) + * @property {function} validator A validator function + * @property {function (CompletionContext, object)} completer A list of + * completions, or a completion function which will be passed a + * {@link CompletionContext} and an object like that returned by + * {@link commands.parseArgs} with the following additional keys: + * completeOpt - The name of the option currently being completed. + * @property {boolean} multiple Whether this option can be specified multiple times + * @property {string} description A description of the option + */ +const CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description"); +CommandOption.defaultValue("description", function () ""); +CommandOption.defaultValue("type", function () CommandOption.NOARG); +CommandOption.defaultValue("multiple", function () false); +update(CommandOption, { + // FIXME: remove later, when our option handler is better + /** + * @property {number} The option argument is unspecified. Any argument + * is accepted and caller is responsible for parsing the return + * value. + * @final + */ + ANY: 0, + + /** + * @property {number} The option doesn't accept an argument. + * @final + */ + NOARG: 1, + /** + * @property {number} The option accepts a boolean argument. + * @final + */ + BOOL: 2, + /** + * @property {number} The option accepts a string argument. + * @final + */ + STRING: 3, + /** + * @property {number} The option accepts an integer argument. + * @final + */ + INT: 4, + /** + * @property {number} The option accepts a float argument. + * @final + */ + FLOAT: 5, + /** + * @property {number} The option accepts a string list argument. + * E.g. "foo,bar" + * @final + */ + LIST: 6 +}); + +/** * A class representing Ex commands. Instances are created by * the {@link Commands} class. * @@ -35,8 +104,6 @@ * @private */ const Command = Class("Command", { - requires: ["config"], - init: function (specs, description, action, extraInfo) { specs = Array.concat(specs); // XXX let parsedSpecs = Command.parseSpecs(specs); @@ -51,6 +118,8 @@ const Command = Class("Command", { if (extraInfo) update(this, extraInfo); + if (this.options) + this.options = this.options.map(CommandOption.fromArray, CommandOption); }, /** @@ -191,7 +260,7 @@ const Command = Class("Command", { * invocation which should be restored on subsequent @dactyl * startups. */ - serial: null, + serialize: null, /** * @property {boolean} When true, invocations of this command * may contain private data which should be purged from @@ -240,47 +309,6 @@ const Commands = Module("commands", { this._exMap = {}; }, - // FIXME: remove later, when our option handler is better - /** - * @property {number} The option argument is unspecified. Any argument - * is accepted and caller is responsible for parsing the return - * value. - * @final - */ - OPTION_ANY: 0, - - /** - * @property {number} The option doesn't accept an argument. - * @final - */ - OPTION_NOARG: 1, - /** - * @property {number} The option accepts a boolean argument. - * @final - */ - OPTION_BOOL: 2, - /** - * @property {number} The option accepts a string argument. - * @final - */ - OPTION_STRING: 3, - /** - * @property {number} The option accepts an integer argument. - * @final - */ - OPTION_INT: 4, - /** - * @property {number} The option accepts a float argument. - * @final - */ - OPTION_FLOAT: 5, - /** - * @property {number} The option accepts a string list argument. - * E.g. "foo,bar" - * @final - */ - OPTION_LIST: 6, - /** * @property Indicates that no count was specified for this * command invocation. @@ -442,29 +470,8 @@ const Commands = Module("commands", { * * @param {string} str The Ex command-line string to parse. E.g. * "-x=foo -opt=bar arg1 arg2" - * @param {Array} options The options accepted. These are specified as - * an array [names, type, validator, completions, multiple]. - * names - an array of option names. The first name is the - * canonical option name. - * type - the option's value type. This is one of: - * (@link Commands#OPTION_NOARG), - * (@link Commands#OPTION_STRING), - * (@link Commands#OPTION_BOOL), - * (@link Commands#OPTION_INT), - * (@link Commands#OPTION_FLOAT), - * (@link Commands#OPTION_LIST), - * (@link Commands#OPTION_ANY) - * validator - a validator function - * completer - a list of completions, or a completion function - * multiple - whether this option can be specified multiple times - * E.g. - * options = [[["-force"], OPTION_NOARG], - * [["-fullscreen", "-f"], OPTION_BOOL], - * [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]], - * [["-speed"], OPTION_INT], - * [["-acceleration"], OPTION_FLOAT], - * [["-accessories"], OPTION_LIST, null, ["foo", "bar"], true], - * [["-other"], OPTION_ANY]]; + * @param {[CommandOption]} options The options accepted. These are specified + * as an array of {@link CommandOption} structures. * @param {string} argCount The number of arguments accepted. * "0": no arguments * "1": exactly one argument @@ -518,7 +525,7 @@ const Commands = Module("commands", { function matchOpts(arg) { // Push possible option matches into completions if (complete && !onlyArgumentsRemaining) - completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))]; + completeOpts = options.filter(function (opt) opt.multiple || !(opt.names[0] in args)); } function resetCompletions() { completeOpts = null; @@ -562,14 +569,14 @@ const Commands = Module("commands", { var optname = ""; if (!onlyArgumentsRemaining) { for (let [, opt] in Iterator(options)) { - for (let [, optname] in Iterator(opt[0])) { + for (let [, optname] in Iterator(opt.names)) { if (sub.indexOf(optname) == 0) { invalid = false; arg = null; quote = null; count = 0; let sep = sub[optname.length]; - if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG) { + if (sep == "=" || /\s/.test(sep) && opt.type != CommandOption.NOARG) { [count, arg, quote, error] = getNextArg(sub.substr(optname.length + 1)); dactyl.assert(!error, error); @@ -595,7 +602,7 @@ const Commands = Module("commands", { args.completeFilter = arg; args.quote = Commands.complQuote[quote] || Commands.complQuote[""]; } - let type = Commands.argTypes[opt[1]]; + let type = Commands.argTypes[opt.type]; if (type && (!complete || arg != null)) { let orig = arg; arg = type.parse(arg); @@ -610,8 +617,8 @@ const Commands = Module("commands", { } // we have a validator function - if (typeof opt[2] == "function") { - if (opt[2].call(this, arg) == false) { + if (typeof opt.validator == "function") { + if (opt.validator.call(this, arg) == false) { echoerr("Invalid argument for option: " + optname); if (complete) complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); @@ -620,11 +627,13 @@ const Commands = Module("commands", { } } + matchOpts(sub); + // option allowed multiple times - if (!!opt[4]) - args[opt[0][0]] = (args[opt[0][0]] || []).concat(arg); + if (opt.multiple) + args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg); else - args[opt[0][0]] = opt[1] == this.OPTION_NOARG || arg; + args[opt.names[0]] = opt.type == this.OPTION_NOARG || arg; i += optname.length + count; if (i == str.length) @@ -683,17 +692,18 @@ const Commands = Module("commands", { if (complete) { if (args.completeOpt) { let opt = args.completeOpt; - let context = complete.fork(opt[0][0], args.completeStart); + let context = complete.fork(opt.names[0], args.completeStart); context.filter = args.completeFilter; - if (typeof opt[3] == "function") - var compl = opt[3](context, args); + if (typeof opt.completer == "function") + var compl = opt.completer(context, args); else - compl = opt[3] || []; - context.title = [opt[0][0]]; + compl = opt.completer || []; + context.title = [opt.names[0]]; context.quote = args.quote; context.completions = compl; } complete.advance(args.completeStart); + complete.keys = { text: "names", description: "description" }; complete.title = ["Options"]; if (completeOpts) complete.completions = completeOpts; @@ -1012,18 +1022,16 @@ const Commands = Module("commands", { // : No, array comprehensions are fine, generator statements aren't. --Kris let cmds = this._exCommands.filter(function (c) c.user && (!cmd || c.name.match("^" + cmd))); - if (cmds.length > 0) { - let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"], - ([cmd.bang ? "!" : " ", - cmd.name, - cmd.argCount, - cmd.count ? "0c" : "", - completerToString(cmd.completer), - cmd.replacementText || "function () { ... }"] - for ([, cmd] in Iterator(cmds)))); - - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } + if (cmds.length > 0) + commandline.commandOutput( + template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"]), + ([cmd.bang ? "!" : " ", + cmd.name, + cmd.argCount, + cmd.count ? "0c" : "", + completerToString(cmd.completer), + cmd.replacementText || "function () { ... }"] + for ([, cmd] in Iterator(cmds)))); else dactyl.echomsg("No user-defined commands found"); } @@ -1036,23 +1044,33 @@ const Commands = Module("commands", { completion.ex(context); }, options: [ - [["-nargs"], commands.OPTION_STRING, - function (arg) /^[01*?+]$/.test(arg), - [["0", "No arguments are allowed (default)"], - ["1", "One argument is allowed"], - ["*", "Zero or more arguments are allowed"], - ["?", "Zero or one argument is allowed"], - ["+", "One or more arguments is allowed"]]], - [["-bang"], commands.OPTION_NOARG], - [["-count"], commands.OPTION_NOARG], - [["-description"], commands.OPTION_STRING], - // TODO: "E180: invalid complete value: " + arg - [["-complete"], commands.OPTION_STRING, - function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), - function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))]] + { names: ["-bang"], description: "Command may be proceeded by a !" }, + { names: ["-count"], description: "Command may be preceeded by a count" }, + { + names: ["-description"], + description: "A user-visible description of the command", + type: CommandOption.STRING + }, { + // TODO: "E180: invalid complete value: " + arg + names: ["-complete"], + description: "The argument completion function", + completer: function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))], + type: CommandOption.STRING, + validator: function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), + }, { + names: ["-nargs"], + description: "The allowed number of arguments", + completer: [["0", "No arguments are allowed (default)"], + ["1", "One argument is allowed"], + ["*", "Zero or more arguments are allowed"], + ["?", "Zero or one argument is allowed"], + ["+", "One or more arguments are allowed"]], + type: CommandOption.STRING, + validator: function (arg) /^[01*?+]$/.test(arg) + }, ], literal: 1, - serial: function () [ { + serialize: function () [ { command: this.name, bang: true, options: util.Array.toObject( diff --git a/common/content/completion.js b/common/content/completion.js index 9a9268a0..1434b38e 100755 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -297,7 +297,7 @@ const CompletionContext = Class("CompletionContext", { let [k, v] = i; let _k = "_" + k; if (typeof v == "string" && /^[.[]/.test(v)) - v = eval("(function (i) i" + v + ")"); + v = Function("i", "return i" + v); if (typeof v == "function") res.__defineGetter__(k, function () _k in this ? this[_k] : (this[_k] = v(this.item))); else @@ -325,7 +325,7 @@ const CompletionContext = Class("CompletionContext", { let lock = {}; this.cache.backgroundLock = lock; this.incomplete = true; - let thread = this.getCache("backgroundThread", dactyl.newThread); + let thread = this.getCache("backgroundThread", util.newThread); util.callAsync(thread, this, function () { if (this.cache.backgroundLock != lock) return; @@ -510,7 +510,7 @@ const CompletionContext = Class("CompletionContext", { 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))); + return completer.apply(self || this, [context].concat(Array.slice(arguments, fork.length))); if (completer) return null; @@ -668,14 +668,13 @@ const Completion = Module("completion", { context = context.contexts["/list"]; context.wait(); - let list = template.commandOutput(commandline.command, + commandline.commandOutput( <div highlight="Completions"> { template.map(context.contextList.filter(function (c) c.hasItems), function (context) template.completionRow(context.title, "CompTitle") + template.map(context.items, function (item) context.createRow(item), null, 100)) } </div>); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, //////////////////////////////////////////////////////////////////////////////// @@ -765,13 +764,11 @@ const Completion = Module("completion", { commands.add(["contexts"], "List the completion contexts used during completion of an ex command", function (args) { - commandline.echo(template.commandOutput(commandline.command, + commandline.commandOutput( <div highlight="Completions"> { template.completionRow(["Context", "Title"], "CompTitle") } { template.map(completion.contextList || [], function (item) template.completionRow(item, "CompItem")) } - </div>), - null, commandline.FORCE_MULTILINE); - + </div>); }, { argCount: "1", diff --git a/common/content/configbase.js b/common/content/configbase.js index 9005f122..5fb75d29 100644 --- a/common/content/configbase.js +++ b/common/content/configbase.js @@ -229,13 +229,13 @@ const ConfigBase = Class(ModuleBase, { HelpOptionalArg color: #6A97D4; HelpBody display: block; margin: 1em auto; max-width: 100ex; - HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; + HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; HelpCode display: block; white-space: pre; margin-left: 2em; font-family: Terminus, Fixed, monospace; HelpDefault margin-right: 1ex; white-space: pre; HelpDescription display: block; - HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; + HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; HelpEx display: inline-block; color: #527BBD; font-weight: bold; @@ -250,21 +250,21 @@ const ConfigBase = Class(ModuleBase, { HelpKey color: #102663; - HelpLink,html|a,dactyl://help/* text-decoration: none; + HelpLink,html|a,dactyl://help/* text-decoration: none; HelpLink[href]:hover text-decoration: underline; - HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; - HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; - HelpListItem,html|li,dactyl://help/* display: list-item; + HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; + HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; + HelpListItem,html|li,dactyl://help/* display: list-item; HelpNote color: red; font-weight: bold; HelpOpt color: #106326; HelpOptInfo display: inline-block; margin-bottom: 1ex; - HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; + HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; HelpParagraph:first-child margin-top: 0; - HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; + HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; margin-right: 2em; HelpString display: inline-block; color: green; font-weight: normal; vertical-align: text-top; HelpString::before content: '"'; diff --git a/common/content/dactyl-overlay.js b/common/content/dactyl-overlay.js index 32f23e9a..af0cc3be 100644 --- a/common/content/dactyl-overlay.js +++ b/common/content/dactyl-overlay.js @@ -1,4 +1,4 @@ -// Copyright (c) 2008-2008 Kris Maglione <maglione.k at Gmail> +// Copyright (c) 2008-2010 Kris Maglione <maglione.k at Gmail> // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. @@ -11,55 +11,61 @@ modules.modules = modules; const loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Components.interfaces.mozIJSSubScriptLoader); - function load(script) { + .getService(Components.interfaces.mozIJSSubScriptLoader); + + modules.load = function load(script) { for (let [i, base] in Iterator(prefix)) { try { - loader.loadSubScript(base + script, modules); + loader.loadSubScript(base + script + ".js", modules); return; } catch (e) { - if (i + 1 < prefix.length) - continue; - if (Components.utils.reportError) - Components.utils.reportError(e); - dump("dactyl: Loading script " + script + ": " + e + "\n"); - dump(e.stack + "\n"); + if (e !== "Error opening input stream (invalid filename?)") + dump("dactyl: Trying: " + (base + script + ".js") + ": " + e + "\n" + e.stack); } } - } + try { + Components.utils.import("resource://dactyl/" + script + ".jsm", modules); + } + catch (e) { + dump("dactyl: Loading script " + script + ": " + e.result + " " + e + "\n"); + dump(Error().stack + "\n"); + } + }; let prefix = [BASE]; - ["base.js", - "modules.js", - "autocommands.js", - "buffer.js", - "commandline.js", - "commands.js", - "completion.js", - "configbase.js", - "config.js", - "dactyl.js", - "editor.js", - "events.js", - "finder.js", - "hints.js", - "io.js", - "javascript.js", - "mappings.js", - "marks.js", - "modes.js", - "options.js", - "services.js", - "statusline.js", - "style.js", - "template.js", - "util.js", - ].forEach(load); + ["base", + "modules", + "storage", + "util", + "autocommands", + "buffer", + "commandline", + "commands", + "completion", + "configbase", + "config", + "dactyl", + "editor", + "events", + "finder", + "highlight", + "hints", + "io", + "javascript", + "mappings", + "marks", + "modes", + "options", + "services", + "statusline", + "styles", + "template", + ].forEach(modules.load); prefix.unshift("chrome://" + modules.Config.prototype.name.toLowerCase() + "/content/"); - modules.Config.prototype.scripts.forEach(load); + modules.Config.prototype.scripts.forEach(modules.load); })(); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 320d6304..b56548b1 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -8,7 +8,9 @@ /** @scope modules */ -Cu.import("resource://gre/modules/XPCOMUtils.jsm", modules); +default xml namespace = XHTML; +XML.ignoreWhitespace = false; +XML.prettyPrinting = false; const plugins = { __proto__: modules }; const userContext = { __proto__: modules }; @@ -17,27 +19,6 @@ const EVAL_ERROR = "__dactyl_eval_error"; const EVAL_RESULT = "__dactyl_eval_result"; const EVAL_STRING = "__dactyl_eval_string"; -// Move elsewhere? -const Storage = Module("storage", { - requires: ["services"], - - init: function () { - Components.utils.import("resource://dactyl/storage.jsm", this); - modules.Timer = this.Timer; // Fix me, please. - - try { - let infoPath = services.create("file"); - infoPath.initWithPath(File.expandPath(IO.runtimePath.replace(/,.*/, ""))); - infoPath.append("info"); - infoPath.append(dactyl.profileName); - this.storage.infoPath = infoPath; - } - catch (e) {} - - return this.storage; - } -}); - const FailedAssertion = Class("FailedAssertion", Error, { init: function (message) { this.message = message; @@ -45,8 +26,6 @@ const FailedAssertion = Class("FailedAssertion", Error, { }); const Dactyl = Module("dactyl", { - requires: ["config", "services"], - init: function () { window.dactyl = this; window.liberator = this; @@ -61,8 +40,6 @@ const Dactyl = Module("dactyl", { // without explicitly selecting a profile. /** @property {string} The name of the current user profile. */ this.profileName = services.get("directory").get("ProfD", Ci.nsIFile).leafName.replace(/^.+?\./, ""); - - config.features.push(Dactyl.getPlatformFeature()); }, destroy: function () { @@ -139,29 +116,26 @@ const Dactyl = Module("dactyl", { * bell may be either audible or visual depending on the value of the * 'visualbell' option. */ - beep: function () { + beep: requiresMainThread(function () { // FIXME: popups clear the command line if (options["visualbell"]) { - util.callInMainThread(function () { - // flash the visual bell - let popup = document.getElementById("dactyl-visualbell"); - let win = config.visualbellWindow; - let rect = win.getBoundingClientRect(); - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - - // NOTE: this doesn't seem to work in FF3 with full box dimensions - popup.openPopup(win, "overlap", 1, 1, false, false); - popup.sizeTo(width - 2, height - 2); - setTimeout(function () { popup.hidePopup(); }, 20); - }); + // flash the visual bell + let popup = document.getElementById("dactyl-visualbell"); + let win = config.visualbellWindow; + let rect = win.getBoundingClientRect(); + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + // NOTE: this doesn't seem to work in FF3 with full box dimensions + popup.openPopup(win, "overlap", 1, 1, false, false); + popup.sizeTo(width - 2, height - 2); + setTimeout(function () { popup.hidePopup(); }, 20); } else { let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound); soundService.beep(); } - return false; // so you can do: if (...) return dactyl.beep(); - }, + }), /** * Prints a message to the console. If <b>msg</b> is an object it is @@ -217,9 +191,11 @@ const Dactyl = Module("dactyl", { echoerr: function (str, flags) { flags |= commandline.APPEND_TO_MESSAGES; + if (isinstance(str, ["Error", "Exception"])) + dactyl.reportError(str); if (typeof str == "object" && "echoerr" in str) str = str.echoerr; - else if (str instanceof Error) + else if (isinstance(str, ["Error"])) str = str.fileName + ":" + str.lineNumber + ": " + str; if (options["errorbells"]) @@ -504,7 +480,7 @@ const Dactyl = Module("dactyl", { // Process plugin help entries. XML.ignoreWhiteSpace = false; - XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil. + XML.prettyPrinting = false; XML.prettyIndent = 4; let body = XML(); @@ -594,7 +570,7 @@ const Dactyl = Module("dactyl", { data.push(">"); if (node instanceof HTMLHeadElement) data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString()); - Array.map(node.childNodes, arguments.callee); + Array.map(node.childNodes, fix); data.push("</"); data.push(node.localName); data.push(">"); } break; @@ -1105,6 +1081,9 @@ const Dactyl = Module("dactyl", { dactyl.help(tag); } }, { + config: function () { + config.features.push(Dactyl.getPlatformFeature()); + }, // Only general options are added here, which are valid for all Dactyl extensions options: function () { @@ -1502,30 +1481,25 @@ const Dactyl = Module("dactyl", { 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({ icon: e.iconURL }, e.name), - e.version, - (e.isActive ? <span highlight="Enabled">enabled</span> - : <span highlight="Disabled">disabled</span>) + - ((e.userDisabled || e.appDisabled) == !e.isActive ? XML() : - <> ({e.userDisabled || e.appDisabled - ? <span highlight="Disabled">disabled</span> - : <span highlight="Enabled">enabled</span>} - on restart) - </>), - e.description] for ([, e] in Iterator(extensions))) - ); - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - else { - if (filter) - dactyl.echoerr("Exxx: No extension matching " + filter.quote()); - else - dactyl.echoerr("No extensions installed"); - } + if (extensions.length > 0) + commandline.commandOutput( + template.tabular(["Name", "Version", "Status", "Description"], [], + ([template.icon({ icon: e.iconURL }, e.name), + e.version, + (e.isActive ? <span highlight="Enabled">enabled</span> + : <span highlight="Disabled">disabled</span>) + + ((e.userDisabled || e.appDisabled) == !e.isActive ? XML() : + <> ({e.userDisabled || e.appDisabled + ? <span highlight="Disabled">disabled</span> + : <span highlight="Enabled">enabled</span>} + on restart) + </>), + e.description] + for ([, e] in Iterator(extensions))))); + else if (filter) + dactyl.echoerr("Exxx: No extension matching " + filter.quote()); + else + dactyl.echoerr("No extensions installed"); }); }, { argCount: "?" }); @@ -1696,7 +1670,7 @@ const Dactyl = Module("dactyl", { else totalUnits = "msec"; - let str = template.commandOutput(commandline.command, + commandline.commandOutput( <table> <tr highlight="Title" align="left"> <th colspan="3">Code execution summary</th> @@ -1705,7 +1679,6 @@ const Dactyl = Module("dactyl", { <tr><td>  Average time:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr> <tr><td>  Total time:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr> </table>); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); } else { let beforeTime = Date.now(); @@ -1767,8 +1740,9 @@ const Dactyl = Module("dactyl", { if (args.bang) dactyl.open("about:"); else - dactyl.echo(template.commandOutput(commandline.command, - <>{config.name} {dactyl.version} running on:<br/>{navigator.userAgent}</>)); + commandline.commandOutput(<> + {config.name} {dactyl.version} running on:<br/>{navigator.userAgent} + </>); }, { argCount: "0", bang: true diff --git a/common/content/editor.js b/common/content/editor.js index ee77772f..6a2c7197 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -11,8 +11,6 @@ /** @instance editor */ const Editor = Module("editor", { - requires: ["config"], - init: function () { // store our last search with f, F, t or T // @@ -408,10 +406,10 @@ const Editor = Module("editor", { // blink the textbox after returning if (textBox) { let colors = [tmpBg, oldBg, tmpBg, oldBg]; - (function () { + (function next() { textBox.style.backgroundColor = colors.shift(); if (colors.length > 0) - setTimeout(arguments.callee, 100); + setTimeout(next, 100); })(); } @@ -577,10 +575,8 @@ const Editor = Module("editor", { dactyl.echo(mode + " " + lhs + " " + rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces } - else { - list = template.tabular(["", "LHS", "RHS"], [], list); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } + else + commandline.commandOutput(template.tabular(["", "LHS", "RHS"], [], list)); }, /** @@ -670,7 +666,7 @@ const Editor = Module("editor", { }, { completer: function (context, args) completion.abbreviation(context, args, mode), literal: 0, - serial: function () [ { + serialize: function () [ { command: this.name, arguments: [lhs], literalArg: abbr[1] diff --git a/common/content/events.js b/common/content/events.js index 574dd3f9..455bd0e0 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -12,8 +12,6 @@ * @instance events */ const Events = Module("events", { - requires: ["autocommands", "config"], - init: function () { const self = this; @@ -737,12 +735,10 @@ const Events = Module("events", { // TODO: Merge with onFocusChange 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) + if (Events.isContentNode(elem) && !buffer.focusAllowed(win) && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) elem.blur(); }, @@ -1107,6 +1103,13 @@ const Events = Module("events", { editableInputs: set(["date", "datetime", "datetime-local", "email", "file", "month", "number", "password", "range", "search", "tel", "text", "time", "url", "week"]), + isContentNode: function (node) { + let win = (node.ownerDocument || node).defaultView; + for (; win; win = win.parent != win && win.parent) + if (win == window.content) + return true; + return false; + }, isInputElemFocused: function () { let elem = dactyl.focus; return elem instanceof HTMLInputElement && set.has(Events.editableInputs, elem.type) || diff --git a/common/content/finder.js b/common/content/finder.js index 463506ff..21650c83 100644 --- a/common/content/finder.js +++ b/common/content/finder.js @@ -8,8 +8,6 @@ /** @instance rangefinder */ const RangeFinder = Module("rangefinder", { - requires: ["config"], - init: function () { this.lastSearchPattern = ""; }, @@ -128,7 +126,7 @@ const RangeFinder = Module("rangefinder", { set rangeFind(val) buffer.localStore.rangeFind = val, /** - * Highlights all occurances of <b>str</b> in the buffer. + * Highlights all occurrences of <b>str</b> in the buffer. * * @param {string} str The string to highlight. */ @@ -146,6 +144,11 @@ const RangeFinder = Module("rangefinder", { } }, { }, { + modes: function () { + /* Must come before commandline. */ + modes.addMode("FIND_FORWARD", true); + modes.addMode("FIND_BACKWARD", true); + }, commandline: function () { // Event handlers for search - closure is needed commandline.registerCallback("change", modes.FIND_FORWARD, this.closure.onKeyPress); @@ -197,10 +200,6 @@ const RangeFinder = Module("rangefinder", { }); }, - modes: function () { - modes.addMode("FIND_FORWARD", true); - modes.addMode("FIND_BACKWARD", true); - }, options: function () { options.safeSetPref("accessibility.typeaheadfind.autostart", false); // The above should be sufficient, but: https://bugzilla.mozilla.org/show_bug.cgi?id=348187 @@ -256,8 +255,8 @@ const RangeFinder = Module("rangefinder", { * 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 + * beginning of the first frame in the case of frameset documents, + * and cycles through all frames from beginning 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). @@ -302,8 +301,8 @@ const RangeFind = Class("RangeFind", { }, compareRanges: function (r1, r2) - this.backward ? r1.compareBoundaryPoints(Range.END_TO_START, r2) - : -r1.compareBoundaryPoints(Range.START_TO_END, r2), + this.backward ? r1.compareBoundaryPoints(r1.END_TO_START, r2) + : -r1.compareBoundaryPoints(r1.START_TO_END, r2), findRange: function (range) { let doc = range.startContainer.ownerDocument; @@ -331,7 +330,7 @@ const RangeFind = Class("RangeFind", { this.lastRange.commonAncestorContainer).snapshotItem(0); if (node) { node.focus(); - // Rehighlight collapsed selection + // Re-highlight collapsed selection this.selectedRange = this.lastRange; } }, @@ -358,7 +357,7 @@ const RangeFind = Class("RangeFind", { let string = this.lastString; for (let r in this.iter(string)) { let controller = this.range.selectionController; - for (let node=r.startContainer; node; node=node.parentNode) + for (let node = r.startContainer; node; node = node.parentNode) if (node instanceof Ci.nsIDOMNSEditableElement) { controller = node.editor.selectionController; break; @@ -370,11 +369,30 @@ const RangeFind = Class("RangeFind", { this.selections.push(sel); } this.highlighted = this.lastString; - this.selectedRange = this.lastRange; + if (this.lastRange) + this.selectedRange = this.lastRange; this.addListeners(); } }, + indexIter: function (private_) { + let idx = this.range.index; + if (this.backward) + var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)]; + else + var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)]; + + for (let i in groups[0]) + yield i; + + if (!private_) { + this.wrapped = true; + this.lastRange = null; + for (let i in groups[1]) + yield i; + } + }, + iter: function (word) { let saved = ["range", "lastRange", "lastString"].map(function (s) [s, this[s]], this); try { @@ -382,7 +400,7 @@ const RangeFind = Class("RangeFind", { this.lastRange = null; this.lastString = word; var res; - while ((res = this.search(null, this.reverse, true))) + while (res = this.search(null, this.reverse, true)) yield res; } finally { @@ -504,25 +522,8 @@ const RangeFind = Class("RangeFind", { if (word == "") var range = this.startRange; - else { - function indices() { - let idx = this.range.index; - if (this.backward) - var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)]; - else - var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)]; - - for (let i in groups[0]) - yield i; - - if (!private_) { - this.wrapped = true; - this.lastRange = null; - for (let i in groups[1]) - yield i; - } - } - for (let i in indices.call(this)) { + else + for (let i in this.indexIter(private_)) { if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) { this.range.descroll(); this.range.deselect(); @@ -540,7 +541,6 @@ const RangeFind = Class("RangeFind", { if (range) break; } - } if (range) this.lastRange = range.cloneRange(); @@ -559,11 +559,11 @@ const RangeFind = Class("RangeFind", { }, addListeners: function () { - for (let range in values(this.ranges)) + for (let range in array.itervalues(this.ranges)) range.window.addEventListener("unload", this.closure.onUnload, true); }, purgeListeners: function () { - for (let range in values(this.ranges)) + for (let range in array.itervalues(this.ranges)) range.window.removeEventListener("unload", this.closure.onUnload, true); }, onUnload: function (event) { @@ -591,8 +591,8 @@ const RangeFind = Class("RangeFind", { }, intersects: function (range) - this.range.compareBoundaryPoints(Range.START_TO_END, range) >= 0 && - this.range.compareBoundaryPoints(Range.END_TO_START, range) <= 0, + this.range.compareBoundaryPoints(range.START_TO_END, range) >= 0 && + this.range.compareBoundaryPoints(range.END_TO_START, range) <= 0, save: function () { this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset); @@ -625,8 +625,8 @@ const RangeFind = Class("RangeFind", { }), contains: function (range, r) - range.compareBoundaryPoints(Range.START_TO_END, r) >= 0 && - range.compareBoundaryPoints(Range.END_TO_START, r) <= 0, + range.compareBoundaryPoints(range.START_TO_END, r) >= 0 && + range.compareBoundaryPoints(range.END_TO_START, r) <= 0, endpoint: function (range, before) { range = range.cloneRange(); range.collapse(before); @@ -634,7 +634,7 @@ const RangeFind = Class("RangeFind", { }, equal: function (r1, r2) { try { - return !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2) + return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2) } catch (e) {} return false; diff --git a/common/content/hints.js b/common/content/hints.js index 4ffe9ca7..2c6b8efc 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -10,8 +10,6 @@ /** @instance hints */ const Hints = Module("hints", { - requires: ["config"], - init: function () { this._hintMode = null; diff --git a/common/content/history.js b/common/content/history.js index abed5893..d8962567 100644 --- a/common/content/history.js +++ b/common/content/history.js @@ -7,8 +7,6 @@ "use strict"; const History = Module("history", { - requires: ["config"], - get format() bookmarks.format, get service() services.get("history"), @@ -196,7 +194,7 @@ const History = Module("history", { bang: true, completer: function (context) { context.quote = null; completion.history(context); }, // completer: function (filter) completion.history(filter) - options: [[["-max", "-m"], commands.OPTION_INT]] + options: [{ names: ["-max", "-m"], description: "The maximum number of items to list", type: CommandOption.INT }] }); }, completion: function () { diff --git a/common/content/io.js b/common/content/io.js index 51a00e37..ebe86d6a 100755 --- a/common/content/io.js +++ b/common/content/io.js @@ -10,311 +10,27 @@ /** @scope modules */ plugins.contexts = {}; -const Script = Class("Script", { - init: function (file) { - let self = plugins.contexts[file.path]; - if (self) { - if (self.onUnload) - self.onUnload(); - return self; - } - plugins.contexts[file.path] = this; - this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase()); - this.PATH = file.path; - this.toString = this.toString; - this.__context__ = this; - this.__proto__ = plugins; - - // This belongs elsewhere - for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) { - if (dir.contains(file, false)) - plugins[this.NAME] = this; - } - return this; - } -}); - -/** - * @class File A class to wrap nsIFile objects and simplify operations - * thereon. - * - * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} - * @param {boolean} checkPWD Whether to allow expansion relative to the - * current directory. @default true - */ -const File = Class("File", { - init: function (path, checkPWD) { - if (arguments.length < 2) - checkPWD = true; - - let file = services.create("file"); - - if (path instanceof Ci.nsIFile) - file = path; - else if (/file:\/\//.test(path)) - file = services.create("file:").getFileFromURLSpec(path); - else { - let expandedPath = File.expandPath(path); - - if (!File.isAbsolutePath(expandedPath) && checkPWD) - file = File.joinPaths(io.getCurrentDirectory().path, expandedPath); - else - file.initWithPath(expandedPath); - } - let self = XPCSafeJSObjectWrapper(file); - self.__proto__ = File.prototype; +function Script(file) { + let self = plugins.contexts[file.path]; + if (self) { + if (self.onUnload) + self.onUnload(); return self; - }, - - /** - * Iterates over the objects in this directory. - */ - iterDirectory: function () { - if (!this.isDirectory()) - throw Error("Not a directory"); - let entries = this.directoryEntries; - while (entries.hasMoreElements()) - yield File(entries.getNext().QueryInterface(Ci.nsIFile)); - }, - /** - * Returns the list of files in this directory. - * - * @param {boolean} sort Whether to sort the returned directory - * entries. - * @returns {nsIFile[]} - */ - readDirectory: function (sort) { - if (!this.isDirectory()) - throw Error("Not a directory"); - - let array = [e for (e in this.iterDirectory())]; - if (sort) - array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); - return array; - }, - - /** - * Reads this file's entire contents in "text" mode and returns the - * content as a string. - * - * @param {string} encoding The encoding from which to decode the file. - * @default options["fileencoding"] - * @returns {string} - */ - read: function (encoding) { - let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - if (!encoding) - encoding = options["fileencoding"]; - - ifstream.init(this, -1, 0, 0); - icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering - - let buffer = []; - let str = {}; - while (icstream.readString(4096, str) != 0) - buffer.push(str.value); - - icstream.close(); - ifstream.close(); - return buffer.join(""); - }, - - /** - * Writes the string <b>buf</b> to this file. - * - * @param {string} buf The file content. - * @param {string|number} mode The file access mode, a bitwise OR of - * the following flags: - * {@link #MODE_RDONLY}: 0x01 - * {@link #MODE_WRONLY}: 0x02 - * {@link #MODE_RDWR}: 0x04 - * {@link #MODE_CREATE}: 0x08 - * {@link #MODE_APPEND}: 0x10 - * {@link #MODE_TRUNCATE}: 0x20 - * {@link #MODE_SYNC}: 0x40 - * Alternatively, the following abbreviations may be used: - * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} - * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} - * @default ">" - * @param {number} perms The file mode bits of the created file. This - * is only used when creating a new file and does not change - * permissions if the file exists. - * @default 0644 - * @param {string} encoding The encoding to used to write the file. - * @default options["fileencoding"] - */ - write: function (buf, mode, perms, encoding) { - let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - function getStream(defaultChar) { - let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - stream.init(ofstream, encoding, 0, defaultChar); - return stream; - } - - if (!encoding) - encoding = options["fileencoding"]; - - if (mode == ">>") - mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND; - else if (!mode || mode == ">") - mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; - - if (!perms) - perms = parseInt('0644', 8); - - ofstream.init(this, mode, perms, 0); - let ocstream = getStream(0); - try { - ocstream.writeString(buf); - } - catch (e) { - dactyl.dump(e); - if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { - ocstream = getStream("?".charCodeAt(0)); - ocstream.writeString(buf); - return false; - } - else - throw e; - } - finally { - try { - ocstream.close(); - } - catch (e) {} - ofstream.close(); - } - return true; - } -}, { - /** - * @property {number} Open for reading only. - * @final - */ - MODE_RDONLY: 0x01, - - /** - * @property {number} Open for writing only. - * @final - */ - MODE_WRONLY: 0x02, - - /** - * @property {number} Open for reading and writing. - * @final - */ - MODE_RDWR: 0x04, - - /** - * @property {number} If the file does not exist, the file is created. - * If the file exists, this flag has no effect. - * @final - */ - MODE_CREATE: 0x08, - - /** - * @property {number} The file pointer is set to the end of the file - * prior to each write. - * @final - */ - MODE_APPEND: 0x10, - - /** - * @property {number} If the file exists, its length is truncated to 0. - * @final - */ - MODE_TRUNCATE: 0x20, - - /** - * @property {number} If set, each write will wait for both the file - * data and file status to be physically updated. - * @final - */ - MODE_SYNC: 0x40, - - /** - * @property {number} With MODE_CREATE, if the file does not exist, the - * file is created. If the file already exists, no action and NULL - * is returned. - * @final - */ - MODE_EXCL: 0x80, - - expandPathList: function (list) list.map(this.expandPath), - - expandPath: function (path, relative) { - - // expand any $ENV vars - this is naive but so is Vim and we like to be compatible - // TODO: Vim does not expand variables set to an empty string (and documents it). - // Kris reckons we shouldn't replicate this 'bug'. --djk - // TODO: should we be doing this for all paths? - function expand(path) path.replace( - !dactyl.has("Win32") ? /\$(\w+)\b|\${(\w+)}/g - : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g, - function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m - ); - path = expand(path); - - // expand ~ - // Yuck. - if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(IO.PATH_SEP) + "])").test(path)) { - // Try $HOME first, on all systems - let home = services.get("environment").get("HOME"); - - // Windows has its own idiosyncratic $HOME variables. - if (!home && dactyl.has("Win32")) - home = services.get("environment").get("USERPROFILE") || - services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH"); - - path = home + path.substr(1); - } - - // TODO: Vim expands paths twice, once before checking for ~, once - // after, but doesn't document it. Is this just a bug? --Kris - path = expand(path); - return path.replace("/", IO.PATH_SEP, "g"); - }, - - getPathsFromPathList: function (list) { - if (!list) - return []; - // empty list item means the current directory - return list.replace(/,$/, "").split(",") - .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); - }, - - replacePathSep: function (path) path.replace("/", IO.PATH_SEP, "g"), - - joinPaths: function (head, tail) { - let path = this(head); - try { - path.appendRelativePath(this.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators - // TODO: This code breaks the external editor at least in ubuntu - // because /usr/bin/gvim becomes /usr/bin/vim.gnome normalized and for - // some strange reason it will start without a gui then (which is not - // optimal if you don't start firefox from a terminal ;) - // Why do we need this code? - // if (path.exists() && path.normalize) - // path.normalize(); - } - catch (e) { - return { exists: function () false, __noSuchMethod__: function () { throw e; } }; - } - return path; - }, - - isAbsolutePath: function (path) { - try { - services.create("file").initWithPath(path); - return true; - } - catch (e) { - return false; - } + } + self = { __proto__: plugins }; + plugins.contexts[file.path] = self; + self.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase()); + self.PATH = file.path; + self.__context__ = self; + self.__proto__ = plugins; + + // This belongs elsewhere + for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) { + if (dir.contains(file, false)) + plugins[self.NAME] = self; } -}); + return self; +} // TODO: why are we passing around strings rather than file objects? /** @@ -322,8 +38,6 @@ const File = Class("File", { * @instance io */ const IO = Module("io", { - requires: ["config", "services"], - init: function () { this._processDir = services.get("directory").get("CurWorkD", Ci.nsIFile); this._cwd = this._processDir; @@ -363,7 +77,10 @@ const IO = Module("io", { * @property {function} File class. * @final */ - File: File, + File: Class("File", File, { + init: function init(path, checkCWD) + init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.getCurrentDirectory()) + }), /** * @property {Object} The current file sourcing context. As a file is @@ -423,7 +140,7 @@ const IO = Module("io", { if (newDir == "-") [this._cwd, this._oldcwd] = [this._oldcwd, this.getCurrentDirectory()]; else { - let dir = File(newDir); + let dir = io.File(newDir); if (!dir.exists() || !dir.isDirectory()) { dactyl.echoerr("E344: Can't find directory " + dir.path.quote()); @@ -489,7 +206,7 @@ const IO = Module("io", { file.append(config.tempFile); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt('0600', 8)); - return File(file); + return io.File(file); }, /** @@ -507,7 +224,7 @@ const IO = Module("io", { let file; if (File.isAbsolutePath(program)) - file = File(program, true); + file = io.File(program, true); else { let dirs = services.get("environment").get("PATH").split(dactyl.has("Win32") ? ";" : ":"); // Windows tries the CWD first TODO: desirable? @@ -600,7 +317,7 @@ lookup: dactyl.dump("sourcing " + filename); let time = Date.now(); try { - var file = File(filename); + var file = io.File(filename); this.sourcing = { file: file.path, line: 0 @@ -803,12 +520,7 @@ lookup: /** * @property {string} The current platform's path seperator. */ - get PATH_SEP() { - delete this.PATH_SEP; - let f = services.get("directory").get("CurProcD", Ci.nsIFile); - f.append("foo"); - return this.PATH_SEP = f.path.substr(f.parent.path.length, 1); - } + PATH_SEP: File.PATH_SEP }, { commands: function () { commands.add(["cd", "chd[ir]"], @@ -876,13 +588,13 @@ lookup: dactyl.assert(args.length <= 1, "E172: Only one file name allowed"); let filename = args[0] || io.getRCFile(null, true).path; - let file = File(filename); + let file = io.File(filename); dactyl.assert(!file.exists() || args.bang, "E189: " + filename.quote() + " exists (add ! to override)"); // TODO: Use a set/specifiable list here: - let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)]; + let lines = [cmd.serialize().map(commands.commandToString) for (cmd in commands) if (cmd.serialize)]; lines = util.Array.flatten(lines); // source a user .pentadactylrc file @@ -922,10 +634,9 @@ lookup: commands.add(["scrip[tnames]"], "List all sourced script names", function () { - let list = template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"], - ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))); // TODO: add colon and remove column titles for pedantic Vim compatibility? - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"], + ([i + 1, file] for ([i, file] in Iterator(io._scriptNames))))); // TODO: add colon and remove column titles for pedantic Vim compatibility? }, { argCount: "0" }); @@ -967,8 +678,7 @@ lookup: let output = io.system(arg); commandline.command = "!" + arg; - commandline.echo(template.commandOutput(commandline.command, - <span highlight="CmdOutput">{output}</span>)); + commandline.commandOutput(<span highlight="CmdOutput">{output}</span>); autocommands.trigger("ShellCmdPost", {}); }, { @@ -1034,7 +744,7 @@ lookup: context.key = dir; context.generate = function generate_file() { try { - return File(dir).readDirectory(); + return io.File(dir).readDirectory(); } catch (e) {} return []; @@ -1065,7 +775,7 @@ lookup: }); }, javascript: function () { - JavaScript.setCompleter([this.File, File.expandPath], + JavaScript.setCompleter([File, File.expandPath], [function (context, obj, args) { context.quote[2] = ""; completion.file(context, true); @@ -1088,7 +798,9 @@ lookup: options.add(["fileencoding", "fenc"], "Sets the character encoding of read and written files", "string", "UTF-8", { - completer: function (context) completion.charset(context) + completer: function (context) completion.charset(context), + getter: function () File.defaultEncoding, + setter: function (value) (File.defaultEncoding = value) }); options.add(["cdpath", "cd"], "List of directories searched when executing :cd", diff --git a/common/content/javascript.js b/common/content/javascript.js index 91c18a72..ea730d2a 100644 --- a/common/content/javascript.js +++ b/common/content/javascript.js @@ -26,7 +26,6 @@ const JavaScript = Module("javascript", { // Some object members are only accessible as function calls getKey: function (obj, key) { try { - // if (!Object.prototype.__lookupGetter__.call(obj, key)) return obj[key]; } catch (e) {} @@ -35,20 +34,18 @@ const JavaScript = Module("javascript", { iter: function iter(obj, toplevel) { "use strict"; - const self = this; if (obj == null) return; - let orig = obj; - if(!options["jsdebugger"]) - function value(key) self.getKey(orig, key); - else { - let top = services.get("debugger").wrapValue(obj); - function value(key) top.getProperty(key).value.getWrappedValue(); + let seen = {}; + for (let key in properties(obj, !toplevel)) { + set.add(seen, key); + yield [key, this.getKey(obj, key)]; } - for (let key in properties(obj, !toplevel)) - yield [key, value(key)]; + for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel)) + if (!set.has(seen, key)) + yield [key, this.getKey(obj, key)]; }, objectKeys: function objectKeys(obj, toplevel) { diff --git a/common/content/mappings.js b/common/content/mappings.js index eb595c05..da646b9b 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -35,6 +35,7 @@ const Map = Class("Map", { this.modes = modes; this.names = keys.map(events.canonicalKeys); + this.name = this.names[0]; this.action = action; this.description = description; @@ -122,8 +123,6 @@ const Map = Class("Map", { * @instance mappings */ const Mappings = Module("mappings", { - requires: ["modes"], - init: function () { this._main = []; // default mappings this._user = []; // user created mappings @@ -177,9 +176,9 @@ const Mappings = Module("mappings", { // Return all mappings present in all @modes _mappingsIterator: function (modes, stack) { modes = modes.slice(); - return (map for ([i, map] in Iterator(stack[modes.shift()])) + return (map for ([i, map] in Iterator(stack[modes.shift()].sort(function (m1, m2) String.localeCompare(m1.name, m2.name)))) if (modes.every(function (mode) stack[mode].some( - function (m) m.rhs == map.rhs && m.names[0] == map.names[0])))) + function (mapping) m.rhs == map.rhs && m.name == map.name)))) }, // NOTE: just normal mode for now @@ -363,11 +362,10 @@ const Mappings = Module("mappings", { </table>; // TODO: Move this to an ItemList to show this automatically - if (list.*.length() == list.text().length()) { + if (list.*.length() == list.text().length()) dactyl.echomsg("No mapping found"); - return; - } - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + else + commandline.commandOutput(list); } }, { }, { @@ -413,11 +411,9 @@ const Mappings = Module("mappings", { const opts = { completer: function (context, args) completion.userMapping(context, args, modes), - options: [ - [["<silent>", "<Silent>"], commands.OPTION_NOARG] - ], literal: 1, - serial: function () { + options: [{ names: ["<silent>", "<Silent>"] }], + serialize: function () { let noremap = this.name.indexOf("noremap") > -1; return [ { diff --git a/common/content/marks.js b/common/content/marks.js index 2fd1f0b5..f6ee4755 100644 --- a/common/content/marks.js +++ b/common/content/marks.js @@ -11,8 +11,6 @@ * @instance marks */ const Marks = Module("marks", { - requires: ["config", "storage"], - init: function init() { this._localMarks = storage.newMap("local-marks", { store: true, privateData: true }); this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true }); @@ -176,15 +174,15 @@ const Marks = Module("marks", { dactyl.assert(marks.length > 0, "E283: No marks matching " + filter.quote()); } - let list = template.tabular( - ["Mark", "Line", "Column", "File"], - ["", "text-align: right", "text-align: right", "color: green"], - ([mark[0], - Math.round(mark[1].position.x * 100) + "%", - Math.round(mark[1].position.y * 100) + "%", - mark[1].location] - for ([, mark] in Iterator(marks)))); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.tabular( + ["Mark", "Line", "Column", "File"], + ["", "text-align: right", "text-align: right", "color: green"], + ([mark[0], + Math.round(mark[1].position.x * 100) + "%", + Math.round(mark[1].position.y * 100) + "%", + mark[1].location] + for ([, mark] in Iterator(marks))))); }, _onPageLoad: function _onPageLoad(event) { diff --git a/common/content/modes.js b/common/content/modes.js index fd7611b4..d9aa994b 100644 --- a/common/content/modes.js +++ b/common/content/modes.js @@ -9,8 +9,6 @@ /** @scope modules */ const Modes = Module("modes", { - requires: ["config", "util"], - init: function () { this._main = 1; // NORMAL this._extended = 0; // NONE @@ -49,8 +47,6 @@ const Modes = Module("modes", { this.addMode("MENU", true); // a popupmenu is active this.addMode("LINE", true); // linewise visual mode this.addMode("PROMPT", true); - - config.modes.forEach(function (mode) { this.addMode.apply(this, mode); }, this); }, _getModeMessage: function () { diff --git a/common/content/modules.js b/common/content/modules.js index b485157b..840f87c5 100644 --- a/common/content/modules.js +++ b/common/content/modules.js @@ -53,12 +53,17 @@ const ModuleBase = Class("ModuleBase", { * * @returns {function} The constructor for the resulting module. */ -function Module(name, prototype, classProperties, moduleInit) { +function Module(name) { + let args = Array.slice(arguments); + var base = ModuleBase; - if (callable(prototype)) - base = Array.splice(arguments, 1, 1)[0]; + if (callable(args[1])) + base = args.splice(1, 1)[0]; + let [, prototype, classProperties, moduleInit] = args; const module = Class(name, base, prototype, classProperties); + module.INIT = moduleInit || {}; + module.prototype.INIT = module.INIT; module.requires = prototype.requires || []; Module.list.push(module); Module.constructors[name] = module; @@ -67,67 +72,82 @@ function Module(name, prototype, classProperties, moduleInit) { Module.list = []; Module.constructors = {}; -window.addEventListener("load", function () { - window.removeEventListener("load", arguments.callee, false); +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + + Module.list.forEach(function(module) { + modules.__defineGetter__(module.name, function() { + delete modules[module.name]; + return load(module.name, null, Components.stack.caller); + }); + }); function dump(str) window.dump(String.replace(str, /\n?$/, "\n").replace(/^/m, Config.prototype.name.toLowerCase() + ": ")); const start = Date.now(); const deferredInit = { load: [] }; const seen = set(); - const loaded = []; + const loaded = set(["init"]); + + function init(module) { + function init(func) + function () func.call(module, dactyl, modules, window); + + set.add(loaded, module.constructor.name); + for (let [mod, func] in Iterator(module.INIT)) { + if (mod in loaded) + init(func)(); + else { + deferredInit[mod] = deferredInit[mod] || []; + deferredInit[mod].push(init(func)); + } + } + } + defmodule.modules.map(init); + + function load(module, prereq, frame) { + if (isstring(module)) { + if (!Module.constructors.hasOwnProperty(module)) + modules.load(module); + module = Module.constructors[module]; + } - function load(module, prereq) { try { - if (module.name in modules) + if (module.name in loaded) return; if (module.name in seen) throw Error("Module dependency loop."); set.add(seen, module.name); for (let dep in values(module.requires)) - load(Module.constructors[dep], module.name, dep); + load(Module.constructors[dep], module.name); dump("Load" + (isstring(prereq) ? " " + prereq + " dependency: " : ": ") + module.name); + if (frame && frame.filename) + dump(" from: " + frame.filename + ":" + frame.lineNumber); + + delete modules[module.name]; modules[module.name] = module(); - loaded.push(module.name); - - function init(mod, module) - function () module.INIT[mod].call(modules[module.name], modules[mod]); - for (let mod in values(loaded)) { - try { - if (mod in module.INIT) - init(mod, module)(); - delete module.INIT[mod]; - } - catch (e) { - if (modules.dactyl) - dactyl.reportError(e); - } - } - for (let mod in values(Object.keys(module.INIT))) { - deferredInit[mod] = deferredInit[mod] || []; - deferredInit[mod].push(init(mod, module)); - } + + init(modules[module.name]); for (let [, fn] in iter(deferredInit[module.name] || [])) fn(); } catch (e) { - dump("Loading " + (module && module.name) + ": " + e + "\n"); + dump("Loading " + (module && module.name) + ": " + e); if (e.stack) dump(e.stack); } + return modules[module.name]; } + Module.list.forEach(load); deferredInit["load"].forEach(call); - for (let module in values(Module.list)) - delete module.INIT; - - dump("Loaded in " + (Date.now() - start) + "ms\n"); + dump("Loaded in " + (Date.now() - start) + "ms"); }, false); -window.addEventListener("unload", function () { - window.removeEventListener("unload", arguments.callee, false); +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload, false); for (let [, mod] in iter(modules)) if (mod instanceof ModuleBase && "destroy" in mod) mod.destroy(); diff --git a/common/content/options.js b/common/content/options.js index d60a773b..8f28e7be 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -238,8 +238,8 @@ const Option = Class("Option", { * "string" - String, e.g., "Pentadactyl" * "charlist" - Character list, e.g., "rb" * "regexlist" - Regex list, e.g., "^foo,bar$" - * "stringmap" - String map, e.g., "key=v,foo=bar" - * "regexmap" - Regex map, e.g., "^key=v,foo$=bar" + * "stringmap" - String map, e.g., "key:v,foo:bar" + * "regexmap" - Regex map, e.g., "^key:v,foo$:bar" */ type: null, @@ -351,9 +351,9 @@ const Option = Class("Option", { boolean: function (value) value == "true" || value == true ? true : false, charlist: function (value) Array.slice(value), stringlist: function (value) (value === "") ? [] : value.split(","), - stringmap: function (value) array(v.split("=") for (v in values(value.split(",")))).toObject(), + stringmap: function (value) array(v.split(":") for (v in values(value.split(",")))).toObject(), regexlist: function (value) (value === "") ? [] : value.split(",").map(Option.parseRegex), - regexmap: function (value) value.split(",").map(function (v) v.split("=")) + regexmap: function (value) value.split(",").map(function (v) v.split(":")) .map(function ([k, v]) v != null ? Option.parseRegex(k, v) : Option.parseRegex('.?', k)) }, @@ -478,8 +478,6 @@ Option.ops["regexmap"] = Option.ops["stringlist"]; * @instance options */ const Options = Module("options", { - requires: ["config", "highlight", "storage"], - init: function () { this._optionHash = {}; this._prefContexts = []; @@ -661,8 +659,7 @@ const Options = Module("options", { } }; - let list = template.options("Options", opts()); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput(template.options("Options", opts())); }, /** @@ -687,7 +684,7 @@ const Options = Module("options", { if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1) continue; - value = options.getPref(pref); + let value = options.getPref(pref); let option = { isDefault: !userValue, @@ -701,8 +698,8 @@ const Options = Module("options", { } }; - let list = template.options(config.hostApplication + " Options", prefs()); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.options(config.hostApplication + " Options", prefs())); }, /** @@ -783,15 +780,7 @@ const Options = Module("options", { return this._loadPreference(name, forcedDefault); }, - /** - * Sets the preference <b>name</b> to </b>value</b> but warns the user - * if the value is changed from its default. - * - * @param {string} name The preference name. - * @param {value} value The new preference value. - */ - // FIXME: Well it used to. I'm looking at you mst! --djk - safeSetPref: function (name, value, message) { + _checkPrefSafe: function (name, message) { let curval = this._loadPreference(name, null, false); let defval = this._loadPreference(name, null, true); let saved = this._loadPreference(Options.SAVED + name); @@ -802,6 +791,30 @@ const Options = Module("options", { msg += " " + message; dactyl.echomsg(msg); } + }, + + /** + * Resets the preference <b>name</b> to </b>value</b> but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeResetPref: function (name, message) { + this._checkPrefSafe(name, message); + this.resetPref(name); + this.resetPref(Options.SAVED + name); + }, + + /** + * Sets the preference <b>name</b> to </b>value</b> but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeSetPref: function (name, value, message) { + this._checkPrefSafe(name, message); this._storePreference(name, value); this._storePreference(Options.SAVED + name, value); }, @@ -825,9 +838,7 @@ const Options = Module("options", { try { services.get("pref").clearUserPref(name); } - catch (e) { - // ignore - thrown if not a user set value - } + catch (e) {} // ignore - thrown if not a user set value }, /** @@ -1238,7 +1249,7 @@ const Options = Module("options", { completer: function (context, args) { return setCompleter(context, args); }, - serial: function () [ + serialize: function () [ { command: this.name, arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name diff --git a/common/content/quickmarks.js b/common/content/quickmarks.js index fc44eb79..70dc1b92 100644 --- a/common/content/quickmarks.js +++ b/common/content/quickmarks.js @@ -12,8 +12,6 @@ * @instance quickmarks */ const QuickMarks = Module("quickmarks", { - requires: ["config", "storage"], - init: function () { this._qmarks = storage.newMap("quickmarks", { store: true }); }, diff --git a/common/content/sanitizer.js b/common/content/sanitizer.js index 68b5ed69..83d93aaa 100644 --- a/common/content/sanitizer.js +++ b/common/content/sanitizer.js @@ -15,8 +15,6 @@ // - finish 1.9.0 support if we're going to support sanitizing in Xulmus const Sanitizer = Module("sanitizer", { - requires: ["dactyl"], - init: function () { const self = this; dactyl.loadScript("chrome://browser/content/sanitize.js", Sanitizer); @@ -144,10 +142,13 @@ const Sanitizer = Module("sanitizer", { context.completions = options.get("sanitizeitems").completer(); }, options: [ - [["-timespan", "-t"], - commands.OPTION_INT, - function (arg) /^[0-4]$/.test(arg), - function () options.get("sanitizetimespan").completer()] + { + names: ["-timespan", "-t"], + description: "Timespan for which to sanitize items", + completer: function () options.get("sanitizetimespan").completer(), + type: CommandOption.INT, + validator: function (arg) /^[0-4]$/.test(arg) + } ] }); }, diff --git a/common/content/style.js b/common/content/style.js deleted file mode 100644 index 778d1a5d..00000000 --- a/common/content/style.js +++ /dev/null @@ -1,832 +0,0 @@ -// Copyright (c) 2008-2009 by Kris Maglione <maglione.k at Gmail> -// -// 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 */ - -/** - * @constant - * @property {string} The default highlighting rules. They have the - * form: - * rule ::= selector space space+ css - * selector ::= group - * | group "," css-selector - * | group "," css-selector "," scope - * group ::= groupname - * | groupname css-selector - */ -// <css> -Highlights.prototype.CSS = <![CDATA[ - Boolean color: red; - Function color: navy; - Null color: blue; - Number color: blue; - Object color: maroon; - String color: green; - - Key font-weight: bold; - - Enabled color: blue; - Disabled color: red; - - Normal color: black; background: white; - ErrorMsg color: white; background: red; font-weight: bold; - InfoMsg color: black; background: white; - ModeMsg color: black; background: white; - MoreMsg color: green; background: white; - WarningMsg color: red; background: white; - Message white-space: normal; min-width: 100%; padding-left: 2em; text-indent: -2em; display: block; - NonText color: blue; min-height: 16px; padding-left: 2px; - Preview color: gray; - - CmdLine,>* font-family: monospace; padding: 1px; - CmdOutput white-space: pre; - - CompGroup - CompGroup:not(:first-of-type) margin-top: .5em; - CompTitle color: magenta; background: white; font-weight: bold; - CompTitle>* padding: 0 .5ex; - CompMsg font-style: italic; margin-left: 16px; - CompItem - CompItem[selected] background: yellow; - CompItem>* padding: 0 .5ex; - CompIcon width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex; - CompIcon>img max-width: 16px; max-height: 16px; vertical-align: middle; - CompResult width: 45%; overflow: hidden; - CompDesc color: gray; width: 50%; - CompLess text-align: center; height: 0; line-height: .5ex; padding-top: 1ex; - CompLess::after content: "\2303" /* Unicode up arrowhead */ - CompMore text-align: center; height: .5ex; line-height: .5ex; margin-bottom: -.5ex; - CompMore::after content: "\2304" /* Unicode down arrowhead */ - - Gradient height: 1px; margin-bottom: -1px; margin-top: -1px; - GradientLeft background-color: magenta; - GradientRight background-color: white; - - Indicator color: blue; - Filter font-weight: bold; - - Keyword color: red; - Tag color: blue; - - LineNr color: orange; background: white; - Question color: green; background: white; font-weight: bold; - - StatusLine color: white; background: black; - StatusLineBroken color: black; background: #FFa0a0 /* light-red */ - StatusLineSecure color: black; background: #a0a0FF /* light-blue */ - StatusLineExtended color: black; background: #a0FFa0 /* light-green */ - - TabClose,.tab-close-button - TabIcon,.tab-icon - TabText,.tab-text - TabNumber font-weight: bold; margin: 0px; padding-right: .3ex; - TabIconNumber { - font-weight: bold; - color: white; - text-align: center; - text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px; - } - - Title color: magenta; background: white; font-weight: bold; - URL text-decoration: none; color: green; background: inherit; - URL:hover text-decoration: underline; cursor: pointer; - - FrameIndicator,,* { - background-color: red; - opacity: 0.5; - z-index: 999; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - } - - Bell border: none; background-color: black; - Hint,,* { - font-family: monospace; - font-size: 10px; - font-weight: bold; - color: white; - background-color: red; - border-color: ButtonShadow; - border-width: 0px; - border-style: solid; - padding: 0px 1px 0px 1px; - } - Hint::after,,* content: attr(number); - HintElem,,* background-color: yellow; color: black; - HintActive,,* background-color: #88FF00; color: black; - HintImage,,* opacity: .5; - - Help font-size: 8pt; line-height: 1.4em; font-family: -moz-fixed; - - HelpArg color: #6A97D4; - HelpOptionalArg color: #6A97D4; - - HelpBody display: block; margin: 1em auto; max-width: 100ex; - HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; - HelpCode display: block; white-space: pre; margin-left: 2em; font-family: Terminus, Fixed, monospace; - - HelpDefault margin-right: 1ex; white-space: pre; - - HelpDescription display: block; - HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; - - HelpEx display: inline-block; color: #527BBD; font-weight: bold; - - HelpExample display: block; margin: 1em 0; - HelpExample::before content: "Example: "; font-weight: bold; - - HelpInfo display: block; width: 20em; margin-left: auto; - HelpInfoLabel display: inline-block; width: 6em; color: magenta; font-weight: bold; vertical-align: text-top; - HelpInfoValue display: inline-block; width: 14em; text-decoration: none; vertical-align: text-top; - - HelpItem display: block; margin: 1em 1em 1em 10em; clear: both; - - HelpKey color: #102663; - - HelpLink,html|a,dactyl://help/* text-decoration: none; - HelpLink[href]:hover text-decoration: underline; - - HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; - HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; - HelpListItem,html|li,dactyl://help/* display: list-item; - - HelpNote color: red; font-weight: bold; - - HelpOpt color: #106326; - HelpOptInfo display: inline-block; margin-bottom: 1ex; - - HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; - HelpParagraph:first-child margin-top: 0; - HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; - - HelpString display: inline-block; color: green; font-weight: normal; vertical-align: text-top; - HelpString::before content: '"'; - HelpString::after content: '"'; - HelpString[delim]::before content: attr(delim); - HelpString[delim]::after content: attr(delim); - - HelpHead,html|h1,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - border-bottom-width: 1px; - font-size: 2em; - font-weight: bold; - color: #527BBD; - clear: both; - } - HelpSubhead,html|h2,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - border-bottom-width: 1px; - font-size: 1.2em; - font-weight: bold; - color: #527BBD; - clear: both; - } - HelpSubsubhead,html|h3,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - font-size: 1.1em; - font-weight: bold; - color: #527BBD; - clear: both; - } - - HelpTOC - HelpTOC>ol ol margin-left: -1em; - - HelpTab,html|dl,dactyl://help/* display: table; width: 100%; margin: 1em 0; border-bottom-width: 1px; border-top-width: 1px; padding: .5ex 0; table-layout: fixed; - HelpTabColumn,html|column,dactyl://help/* display: table-column; - HelpTabColumn:first-child width: 25%; - HelpTabTitle,html|dt,dactyl://help/* display: table-cell; padding: .1ex 1ex; font-weight: bold; - HelpTabDescription,html|dd,dactyl://help/* display: table-cell; padding: .1ex 1ex; border-width: 0px; - HelpTabRow,html|dl>html|tr,dactyl://help/* display: table-row; - - HelpTag display: inline-block; color: #527BBD; margin-left: 1ex; font-size: 8pt; font-weight: bold; - HelpTags display: block; float: right; clear: right; - HelpTopic color: #102663; - HelpType margin-right: 2ex; - - HelpWarning color: red; font-weight: bold; - - Logo - - Search,,* { - font-size: inherit; - padding: 0; - color: black; - background-color: yellow; - } - ]]>.toString(); - -/** - * A class to manage highlighting rules. The parameters are the - * standard parameters for any {@link Storage} object. - * - * @author Kris Maglione <maglione.k@gmail.com> - */ -function Highlights(name, store) { - let self = this; - let highlight = {}; - let styles = storage.styles; - - const Highlight = Struct("class", "selector", "filter", "default", "value", "base"); - Highlight.defaultValue("filter", function () - this.base ? this.base.filter : - ["chrome://dactyl/*", - "dactyl:*", - "file://*"].concat(config.styleableChrome).join(",")); - Highlight.defaultValue("selector", function () self.selector(this.class)); - Highlight.defaultValue("value", function () this.default); - Highlight.defaultValue("base", function () { - let base = this.class.match(/^(\w*)/)[0]; - return base != this.class && base in highlight ? highlight[base] : null; - }); - Highlight.prototype.toString = function () "Highlight(" + this.class + ")\n\t" + [k + ": " + util.escapeString(v || "undefined") for ([k, v] in this)].join("\n\t"); - - function keys() [k for ([k, v] in Iterator(highlight))].sort(); - - this.__iterator__ = function () (highlight[v] for ([k, v] in Iterator(keys()))); - - this.get = function (k) highlight[k]; - this.set = function (key, newStyle, force, append) { - let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/); - - if (!(class_ in highlight)) - return "Unknown highlight keyword: " + class_; - - let style = highlight[key] || Highlight(key); - styles.removeSheet(true, style.selector); - - if (append) - newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); - if (/^\s*$/.test(newStyle)) - newStyle = null; - if (newStyle == null) { - if (style.default == null) { - delete highlight[style.class]; - styles.removeSheet(true, style.selector); - return null; - } - newStyle = style.default; - force = true; - } - - let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") - .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug - if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { - css = style.selector + " { " + css + " }"; - - let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); - if (error) - return error; - } - style.value = newStyle; - highlight[style.class] = style; - return null; - }; - - /** - * Gets a CSS selector given a highlight group. - * - * @param {string} class - */ - this.selector = function (class_) { - let [, hl, rest] = class_.match(/^(\w*)(.*)/); - let pattern = "[dactyl|highlight~=" + hl + "]" - if (highlight[hl] && highlight[hl].class != class_) - pattern = highlight[hl].selector; - return pattern + rest; - }; - - /** - * Clears all highlighting rules. Rules with default values are - * reset. - */ - this.clear = function () { - for (let [k, v] in Iterator(highlight)) - this.set(k, null, true); - }; - - /** - * Bulk loads new CSS rules. - * - * @param {string} css The rules to load. See {@link Highlights#css}. - */ - this.loadCSS = function (css) { - css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " ")) - .split("\n").filter(function (s) /\S/.test(s)) - .forEach(function (style) { - style = Highlight.apply(Highlight, Array.slice(style.match(/^\s*((?:[^,\s]|\s\S)+)(?:,((?:[^,\s]|\s\S)+)?)?(?:,((?:[^,\s]|\s\S)+))?\s*(.*)$/), 1)); - if (/^[>+ ]/.test(style.selector)) - style.selector = self.selector(style.class) + style.selector; - - let old = highlight[style.class]; - highlight[style.class] = style; - if (old && old.value != old.default) - style.value = old.value; - }); - for (let [class_, hl] in Iterator(highlight)) { - if (hl.value == hl.default) - this.set(class_); - } - }; - this.loadCSS(this.CSS); -} - -/** - * Manages named and unnamed user style sheets, which apply to both - * chrome and content pages. The parameters are the standard - * parameters for any {@link Storage} object. - * - * @author Kris Maglione <maglione.k@gmail.com> - */ -function Styles(name, store) { - // Can't reference dactyl or Components inside Styles -- - // they're members of the window object, which disappear - // with this window. - const self = this; - const util = modules.util; - const sleep = dactyl.sleep; - const storage = modules.storage; - const ios = services.get("io"); - const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService); - const namespace = "@namespace html " + XHTML.uri.quote() + ";\n" + - "@namespace xul " + XUL.uri.quote() + ";\n" + - "@namespace dactyl " + NS.uri.quote() + ";\n"; - - const Sheet = Struct("name", "id", "sites", "css", "system", "agent"); - Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() { - let filter = this.sites; - let css = this.css; - if (filter[0] == "*") - return namespace + css; - let selectors = filter.map(function (part) (/[*]$/.test(part) ? "url-prefix" : - /[\/:]/.test(part) ? "url" - : "domain") - + '("' + part.replace(/"/g, "%22").replace(/[*]$/, "") + '")') - .join(", "); - return namespace + "/* Dactyl style #" + this.id + " */ @-moz-document " + selectors + "{\n" + css + "\n}\n"; - }); - Sheet.prototype.__defineGetter__("enabled", function () this._enabled); - Sheet.prototype.__defineSetter__("enabled", function (on) { - this._enabled = Boolean(on); - if (on) { - self.registerSheet(cssUri(this.fullCSS)); - if (this.agent) - self.registerSheet(cssUri(this.fullCSS), true); - } - else { - self.unregisterSheet(cssUri(this.fullCSS)); - self.unregisterSheet(cssUri(this.fullCSS), true); - } - }); - - let cssUri = function (css) "chrome-data:text/css," + window.encodeURI(css); - - let userSheets = []; - let systemSheets = []; - let userNames = {}; - let systemNames = {}; - - let id = 0; - - this.__iterator__ = function () Iterator(userSheets.concat(systemSheets)); - this.__defineGetter__("systemSheets", function () Iterator(systemSheets)); - this.__defineGetter__("userSheets", function () Iterator(userSheets)); - this.__defineGetter__("systemNames", function () Iterator(systemNames)); - this.__defineGetter__("userNames", function () Iterator(userNames)); - - /** - * Add a new style sheet. - * - * @param {boolean} system Declares whether this is a system or - * user sheet. System sheets are used internally by - * @dactyl. - * @param {string} name The name given to the style sheet by - * which it may be later referenced. - * @param {string} filter The sites to which this sheet will - * apply. Can be a domain name or a URL. Any URL ending in - * "*" is matched as a prefix. - * @param {string} css The CSS to be applied. - */ - this.addSheet = function (system, name, filter, css, agent) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - if (name && name in names) - this.removeSheet(system, name); - - let sheet = Sheet(name, id++, filter.split(",").filter(util.identity), String(css), null, system, agent); - - try { - sheet.enabled = true; - } - catch (e) { - return e.echoerr || e; - } - sheets.push(sheet); - - if (name) - names[name] = sheet; - return null; - }; - - /** - * Get a sheet with a given name or index. - * - * @param {boolean} system - * @param {string or number} sheet The sheet to retrieve. Strings indicate - * sheet names, while numbers indicate indices. - */ - this.get = function get(system, sheet) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - if (typeof sheet === "number") - return sheets[sheet]; - return names[sheet]; - }; - - /** - * Find sheets matching the parameters. See {@link #addSheet} - * for parameters. - * - * @param {boolean} system - * @param {string} name - * @param {string} filter - * @param {string} css - * @param {number} index - */ - this.findSheets = function (system, name, filter, css, index) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - - // Grossly inefficient. - let matches = [k for ([k, v] in Iterator(sheets))]; - if (index) - matches = String(index).split(",").filter(function (i) i in sheets); - if (name) - matches = matches.filter(function (i) sheets[i] == names[name]); - if (css) - matches = matches.filter(function (i) sheets[i].css == css); - if (filter) - matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0); - return matches.map(function (i) sheets[i]); - }; - - /** - * Remove a style sheet. See {@link #addSheet} for parameters. - * In cases where <b>filter</b> is supplied, the given filters - * are removed from matching sheets. If any remain, the sheet is - * left in place. - * - * @param {boolean} system - * @param {string} name - * @param {string} filter - * @param {string} css - * @param {number} index - */ - this.removeSheet = function (system, name, filter, css, index) { - let self = this; - if (arguments.length == 1) { - var matches = [system]; - system = matches[0].system; - } - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - - if (filter && filter.indexOf(",") > -1) - return filter.split(",").reduce( - function (n, f) n + self.removeSheet(system, name, f, index), 0); - - if (filter == undefined) - filter = ""; - - if (!matches) - matches = this.findSheets(system, name, filter, css, index); - if (matches.length == 0) - return null; - - for (let [, sheet] in Iterator(matches.reverse())) { - sheet.enabled = false; - if (name) - delete names[name]; - if (sheets.indexOf(sheet) > -1) - sheets.splice(sheets.indexOf(sheet), 1); - - /* Re-add if we're only changing the site filter. */ - if (filter) { - let sites = sheet.sites.filter(function (f) f != filter); - if (sites.length) - this.addSheet(system, name, sites.join(","), css, sheet.agent); - } - } - return matches.length; - }; - - /** - * Register a user style sheet at the given URI. - * - * @param {string} uri The URI of the sheet to register. - * @param {boolean} agent If true, sheet is registered as an agent sheet. - * @param {boolean} reload Whether to reload any sheets that are - * already registered. - */ - this.registerSheet = function (uri, agent, reload) { - if (reload) - this.unregisterSheet(uri, agent); - uri = ios.newURI(uri, null, null); - if (reload || !sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) - sss.loadAndRegisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); - }; - - /** - * Unregister a sheet at the given URI. - * - * @param {string} uri The URI of the sheet to unregister. - */ - this.unregisterSheet = function (uri, agent) { - uri = ios.newURI(uri, null, null); - if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) - sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); - }; -} - -Module("styles", { - requires: ["config", "dactyl", "storage", "util"], - - init: function () { - let (array = util.Array) { - update(Styles.prototype, { - get sites() array([v.sites for ([k, v] in this.userSheets)]).flatten().uniq().__proto__, - completeSite: function (context, content) { - context.anchored = false; - try { - context.fork("current", 0, this, function (context) { - context.title = ["Current Site"]; - context.completions = [ - [content.location.host, "Current Host"], - [content.location.href, "Current URL"] - ]; - }); - } - catch (e) {} - context.fork("others", 0, this, function (context) { - context.title = ["Site"]; - context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))]; - }); - } - }); - } - return storage.newObject("styles", Styles, { store: false }); - } -}, { -}, { - commands: function () { - commands.add(["sty[le]"], - "Add or list user styles", - function (args) { - let [filter, css] = args; - let name = args["-name"]; - - if (!css) { - let list = Array.concat([i for (i in styles.userNames)], - [i for (i in styles.userSheets) if (!i[1].name)]); - let str = template.tabular(["", "Name", "Filter", "CSS"], - ["min-width: 1em; text-align: center; color: red; font-weight: bold;", - "padding: 0 1em 0 1ex; vertical-align: top;", - "padding: 0 1em 0 0; vertical-align: top;"], - ([sheet.enabled ? "" : "\u00d7", - key, - sheet.sites.join(","), - sheet.css] - for ([i, [key, sheet]] in Iterator(list)) - if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name)))); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - else { - if ("-append" in args) { - let sheet = styles.get(false, name); - if (sheet) { - filter = sheet.sites.concat(filter).join(","); - css = sheet.css + " " + css; - } - } - let err = styles.addSheet(false, name, filter, css); - if (err) - dactyl.echoerr(err); - } - }, - { - bang: true, - completer: function (context, args) { - let compl = []; - if (args.completeArg == 0) - styles.completeSite(context, content); - else if (args.completeArg == 1) { - let sheet = styles.get(false, args["-name"]); - if (sheet) - context.completions = [[sheet.css, "Current Value"]]; - } - }, - hereDoc: true, - literal: 1, - options: [[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]], - [["-append", "-a"], commands.OPTION_NOARG]], - serial: function () [ - { - command: this.name, - bang: true, - options: sty.name ? { "-name": sty.name } : {}, - arguments: [sty.sites.join(",")], - literalArg: sty.css - } for ([k, sty] in styles.userSheets) - ] - }); - - [ - { - name: ["stylee[nable]", "stye[nable]"], - desc: "Enable a user style sheet", - action: function (sheet) sheet.enabled = true, - filter: function (sheet) !sheet.enabled - }, - { - name: ["styled[isable]", "styd[isable]"], - desc: "Disable a user style sheet", - action: function (sheet) sheet.enabled = false, - filter: function (sheet) sheet.enabled - }, - { - name: ["stylet[oggle]", "styt[oggle]"], - desc: "Toggle a user style sheet", - action: function (sheet) sheet.enabled = !sheet.enabled - }, - { - name: ["dels[tyle]"], - desc: "Remove a user style sheet", - action: function (sheet) styles.removeSheet(sheet) - } - ].forEach(function (cmd) { - commands.add(cmd.name, cmd.desc, - function (args) { - styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"]) - .forEach(cmd.action); - }, - { - completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); }, - literal: 1, - options: [[["-index", "-i"], commands.OPTION_INT, null, - function (context) { - context.compare = CompletionContext.Sort.number; - return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}</>] - for ([i, sheet] in styles.userSheets) - if (!cmd.filter || cmd.filter(sheet))]; - }], - [["-name", "-n"], commands.OPTION_STRING, null, - function () [[name, sheet.css] - for ([name, sheet] in Iterator(styles.userNames)) - if (!cmd.filter || cmd.filter(sheet))]]] - }); - }); - }, - completion: function () { - JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), - [ // Prototype: (system, name, filter, css, index) - null, - function (context, obj, args) args[0] ? this.systemNames : this.userNames, - function (context, obj, args) this.completeSite(context, content), - null, - function (context, obj, args) args[0] ? this.systemSheets : this.userSheets - ]); - } -}); - -Module("highlight", { - requires: ["config", "styles"], - - init: function () { - const self = storage.newObject("highlight", Highlights, { store: false }); - - if (self.CSS != Highlights.prototype.CSS) { - self.CSS = Highlights.prototype.CSS; - self.loadCSS(self.CSS); - } - return self; - } -}, { -}, { - commands: function () { - commands.add(["colo[rscheme]"], - "Load a color scheme", - function (args) { - let scheme = args[0]; - - if (scheme == "default") - highlight.clear(); - else - dactyl.assert(io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]), - "E185: Cannot find color scheme " + scheme); - autocommands.trigger("ColorScheme", { name: scheme }); - }, - { - argCount: "1", - completer: function (context) completion.colorScheme(context) - }); - - commands.add(["hi[ghlight]"], - "Set the style of certain display elements", - function (args) { - let style = <![CDATA[ - ; - display: inline-block !important; - position: static !important; - margin: 0px !important; padding: 0px !important; - width: 3em !important; min-width: 3em !important; max-width: 3em !important; - height: 1em !important; min-height: 1em !important; max-height: 1em !important; - overflow: hidden !important; - ]]>; - let clear = args[0] == "clear"; - if (clear) - args.shift(); - - let [key, css] = args; - dactyl.assert(!(clear && css), "E488: Trailing characters"); - - if (!css && !clear) { - // List matching keys - let str = template.tabular(["Key", "Sample", "CSS"], - ["padding: 0 1em 0 0; vertical-align: top", - "text-align: center"], - ([h.class, - <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>, - template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)] - for (h in highlight) - if (!key || h.class.indexOf(key) > -1))); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - return; - } - if (!key && clear) - highlight.clear(); - else { - let error = highlight.set(key, css, clear, "-append" in args); - if (error) - dactyl.echoerr(error); - } - }, - { - // TODO: add this as a standard highlight completion function? - completer: function (context, args) { - // Complete a highlight group on :hi clear ... - if (args.completeArg > 0 && args[0] == "clear") - args.completeArg = args.completeArg > 1 ? -1 : 0; - - if (args.completeArg == 0) - context.completions = [[v.class, v.value] for (v in highlight)]; - else if (args.completeArg == 1) { - let hl = highlight.get(args[0]); - if (hl) - context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]]; - } - }, - hereDoc: true, - literal: 1, - options: [[["-append", "-a"], commands.OPTION_NOARG]], - serial: function () [ - { - command: this.name, - arguments: [k], - literalArg: v - } - for ([k, v] in Iterator(highlight)) - if (v.value != v.default) - ] - }); - }, - completion: function () { - completion.colorScheme = function colorScheme(context) { - context.title = ["Color Scheme", "Runtime Path"]; - context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" }; - context.completions = util.Array.flatten( - io.getRuntimeDirectories("colors").map( - function (dir) dir.readDirectory().filter( - function (file) /\.vimp$/.test(file.leafName)))) - - }; - - completion.highlightGroup = function highlightGroup(context) { - context.title = ["Highlight Group", "Value"]; - context.completions = [[v.class, v.value] for (v in highlight)]; - }; - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/tabs.js b/common/content/tabs.js index a1b53949..efba231c 100644 --- a/common/content/tabs.js +++ b/common/content/tabs.js @@ -14,8 +14,6 @@ * @instance tabs */ const Tabs = Module("tabs", { - requires: ["config"], - init: function () { this._alternates = [config.tabbrowser.mCurrentTab, null]; @@ -187,7 +185,7 @@ const Tabs = Module("tabs", { this._groups = this._groups = iframe ? iframe.contentWindow : null; if (this._groups) while (!this._groups.TabItems) - dactyl.threadYield(false, true); + util.threadYield(false, true); return this._groups; }, @@ -196,14 +194,15 @@ const Tabs = Module("tabs", { * selected tab if <b>index</b> is not specified. This is a 0-based * index. * - * @param {number} index The index of the tab required. + * @param {number|Node} index The index of the tab required or the tab itself * @returns {Object} */ getTab: function (index) { + if (index instanceof Node) + return index; if (index != null) return config.tabbrowser.mTabs[index]; - else - return config.tabbrowser.mCurrentTab; + return config.tabbrowser.mCurrentTab; }, /** diff --git a/common/content/base.js b/common/modules/base.jsm index e4a58bbb..20b22715 100644 --- a/common/content/base.js +++ b/common/modules/base.jsm @@ -9,6 +9,67 @@ const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let use = {}; +let loaded = {}; +let currentModule; +function defmodule(name, module, params) { + module.NAME = name; + module.EXPORTED_SYMBOLS = params.exports || []; + dump("defmodule " + name + "\n"); + for(let [, mod] in Iterator(params.require || [])) + require(module, mod); + + for(let [, mod] in Iterator(params.use || [])) + if (loaded.hasOwnProperty(mod)) + require(module, mod, "use"); + else { + use[mod] = use[mod] || []; + use[mod].push(module); + } + currentModule = module; +} +defmodule.modules = []; + +function endmodule() { + dump("endmodule " + currentModule.NAME + "\n"); + loaded[currentModule.NAME] = 1; + for(let [, mod] in Iterator(use[currentModule.NAME] || [])) + require(mod, currentModule.NAME, "use"); +} + +function require(obj, name, from) { + try { + dump((from || "require") + ": loading " + name + " into " + obj.NAME + "\n"); + Cu.import("resource://dactyl/" + name + ".jsm", obj); + } + catch (e) { + dump("loading " + String.quote("resource://dactyl/" + name + ".jsm") + "\n"); + dump(" " + e.fileName + ":" + e.lineNumber + ": " + e +"\n"); + } +} + +defmodule("base", this, { + // sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt + exports: [ + "Cc", "Ci", "Class", "Cr", "Cu", "Module", "Object", "Runnable", + "Struct", "StructBase", "Timer", "allkeys", "array", "call", + "callable", "curry", "debuggerProperties", "defmodule", "dict", + "endmodule", "extend", "foreach", "isarray", "isgenerator", + "isinstance", "isobject", "isstring", "issubclass", "iter", "memoize", + "properties", "requiresMainThread", "set", "update", "values", + ], + use: ["services"] +}); + +function Runnable(self, func, args) { + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRunnable]), + run: function () { func.apply(self, args || []); } + }; +} + function allkeys(obj) { let ret = {}; try { @@ -37,7 +98,7 @@ function allkeys(obj) { } function debuggerProperties(obj) { - if (modules.services && services.get("debugger").isOn) { + if (loaded.services && services.get("debugger").isOn) { let ret = {}; services.get("debugger").wrapValue(obj).getProperties(ret, {}); return ret.value; @@ -58,10 +119,17 @@ if (!Object.getOwnPropertyNames) function properties(obj, prototypes) { let orig = obj; let seen = {}; - for (; obj; obj = prototypes && obj.__proto__) - for (let key in values(Object.getOwnPropertyNames(obj))) + for (; obj; obj = prototypes && obj.__proto__) { + try { + var iter = values(Object.getOwnPropertyNames(obj)); + } + catch (e) { + iter = (prop.name.stringValue for (prop in values(debuggerProperties(obj)))); + } + for (let key in iter) if (!prototypes || !set.add(seen, key) && obj != orig) yield key + } } function values(obj) { @@ -141,13 +209,17 @@ function isinstance(targ, src) { } src = Array.concat(src); for (var i = 0; i < src.length; i++) { - if (targ instanceof src[i]) - return true; - if (typeof src[i] == "string") - return Object.prototype.toString(targ) == "[object " + src[i] + "]"; - var type = types[typeof targ]; - if (type && issubclass(src[i], type)) - return true; + if (typeof src[i] == "string") { + if (Object.prototype.toString.call(targ) == "[object " + src[i] + "]") + return true; + } + else { + if (targ instanceof src[i]) + return true; + var type = types[typeof targ]; + if (type && issubclass(src[i], type)) + return true; + } } return false; } @@ -240,12 +312,12 @@ function curry(fn, length, self, acc) { if (acc == null) acc = []; - return function () { + return function curried() { let args = acc.concat(Array.slice(arguments)); // The curried result should preserve 'this' if (arguments.length == 0) - return close(self || this, arguments.callee); + return close(self || this, curried); if (args.length >= length) return fn.apply(self || this, args); @@ -255,6 +327,22 @@ function curry(fn, length, self, acc) { } /** + * Wraps a function so that when called it will always run synchronously + * in the main thread. Return values are not preserved. + * + * @param {function} + * @returns {function} + */ +function requiresMainThread(callback) + function wrapper() { + let mainThread = services.get("threadManager").mainThread; + if (services.get("threadManager").isMainThread) + callback.apply(this, arguments); + else + mainThread.dispatch(Runnable(this, callback, arguments), mainThread.DISPATCH_NORMAL); + } + +/** * Updates an object with the properties of another object. Getters * and setters are copied as expected. Moreover, any function * properties receive new 'supercall' and 'superapply' properties, @@ -263,7 +351,7 @@ function curry(fn, length, self, acc) { * * let a = { foo: function (arg) "bar " + arg } * let b = { __proto__: a } - * update(b, { foo: function () arguments.callee.supercall(this, "baz") }); + * update(b, { foo: function foo() foo.supercall(this, "baz") }); * * a.foo("foo") -> "bar foo" * b.foo() -> "bar baz" @@ -393,7 +481,18 @@ function Class() { }); return Constructor; } -Class.toString = function () "[class " + this.constructor.name + "]", +if (Object.defineProperty) + Class.replaceProperty = function (obj, prop, value) { + Object.defineProperty(obj, prop, { configurable: true, enumerable: true, value: value, writable: true }); + return value; + }; +else + Class.replaceProperty = function (obj, prop, value) { + obj.__defineGetter__(prop, function () value); + obj.__defineSetter__(prop, function (val) { value = val; }); + return value; + }; +Class.toString = function () "[class " + this.name + "]"; Class.prototype = { /** * Initializes new instances of this class. Called automatically @@ -423,6 +522,25 @@ Class.prototype = { }; /** + * Constructs a mew Module class and instantiates an instance into the current + * module global object. + * + * @param {string} name The name of the instance. + * @param {Object} prototype The instance prototype. + * @param {Object} classProperties Properties to be applied to the class constructor. + * @return {Class} + */ +function Module(name, prototype, classProperties, init) { + const module = Class(name, prototype, classProperties); + let instance = module(); + module.name = name.toLowerCase(); + instance.INIT = init || {}; + currentModule[module.name] = instance; + defmodule.modules.push(instance); + return module; +} + +/** * @class Struct * * Creates a new Struct constructor, used for creating objects with @@ -439,7 +557,7 @@ Class.prototype = { */ function Struct() { let args = Array.slice(arguments); - const Struct = Class("Struct", StructBase, { + const Struct = Class("Struct", Struct_Base, { length: args.length, members: args }); @@ -449,7 +567,7 @@ function Struct() { }); return Struct; } -const StructBase = Class("StructBase", { +let Struct_Base = Class("StructBase", Array, { init: function () { for (let i = 0; i < arguments.length; i++) if (arguments[i] != undefined) @@ -464,6 +582,11 @@ const StructBase = Class("StructBase", { return ([k, self[k]] for (k in values(self.members))) } }, { + fromArray: function (ary) { + ary.__proto__ = this.prototype; + return ary; + }, + /** * Sets a lazily constructed default value for a member of * the struct. The value is constructed once, the first time @@ -477,17 +600,65 @@ const StructBase = Class("StructBase", { defaultValue: function (key, val) { let i = this.prototype.members.indexOf(key); this.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0 - this.prototype.__defineSetter__(i, function (value) { - this.__defineGetter__(i, function () value); - this.__defineSetter__(i, function (val) { value = val; }); - }); + this.prototype.__defineSetter__(i, function (value) + Class.replaceProperty(this, i, value)); + } +}); + +const Timer = Class("Timer", { + init: function (minInterval, maxInterval, callback) { + this._timer = services.create("timer"); + this.callback = callback; + this.minInterval = minInterval; + this.maxInterval = maxInterval; + this.doneAt = 0; + this.latest = 0; + }, + + notify: function (timer) { + this._timer.cancel(); + this.latest = 0; + // minInterval is the time between the completion of the command and the next firing + this.doneAt = Date.now() + this.minInterval; + + try { + this.callback(this.arg); + } + finally { + this.doneAt = Date.now() + this.minInterval; + } + }, + + tell: function (arg) { + if (arguments.length > 0) + this.arg = arg; + + let now = Date.now(); + if (this.doneAt == -1) + this._timer.cancel(); + + let timeout = this.minInterval; + if (now > this.doneAt && this.doneAt > -1) + timeout = 0; + else if (this.latest) + timeout = Math.min(timeout, this.latest - now); + else + this.latest = now + this.maxInterval; + + this._timer.initWithCallback(this, Math.max(timeout, 0), this._timer.TYPE_ONE_SHOT); + this.doneAt = -1; + }, + + reset: function () { + this._timer.cancel(); + this.doneAt = 0; + }, + + flush: function () { + if (this.doneAt == -1) + this.notify(); } }); -// Add no-sideeffect array methods. Can't set new Array() as the prototype or -// get length() won't work. -for (let k in values(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", - "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"])) - StructBase.prototype[k] = Array.prototype[k]; /** * Array utility methods. @@ -605,9 +776,8 @@ const array = Class("util.Array", Array, { }, /** - * Zips the contents of two arrays. The resulting array is twice the - * length of ary1, with any shortcomings of ary2 replaced with null - * strings. + * Zips the contents of two arrays. The resulting array is the length of + * ary1, with any shortcomings of ary2 replaced with null strings. * * @param {Array} ary1 * @param {Array} ary2 @@ -616,9 +786,13 @@ const array = Class("util.Array", Array, { zip: function zip(ary1, ary2) { let res = [] for(let [i, item] in Iterator(ary1)) - res.push(item, i in ary2 ? ary2[i] : ""); + res.push([item, i in ary2 ? ary2[i] : ""]); return res; } }); -// vim: set fdm=marker sw=4 ts=4 et: +endmodule(); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/bookmarkcache.jsm b/common/modules/bookmarkcache.jsm new file mode 100644 index 00000000..8fe022ee --- /dev/null +++ b/common/modules/bookmarkcache.jsm @@ -0,0 +1,162 @@ +// Copyright ©2008-2010 Kris Maglione <maglione.k at Gmail> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("bookmarkcache", this, { + exports: ["Bookmark", "BookmarkCache", "Keyword", "bookmarkcache"], + require: ["services", "util"] +}); + + +const Bookmark = Struct("url", "title", "icon", "keyword", "tags", "id"); +const Keyword = Struct("keyword", "title", "icon", "url"); +Bookmark.defaultValue("icon", function () BookmarkCache.getFavicon(this.url)); +Bookmark.prototype.__defineGetter__("extra", function () [ + ["keyword", this.keyword, "Keyword"], + ["tags", this.tags.join(", "), "Tag"] + ].filter(function (item) item[1])); + +const bookmarks = services.get("bookmarks"); +const history = services.get("history"); +const tagging = services.get("tagging"); +const name = "bookmark-cache"; + +const BookmarkCache = Module("BookmarkCache", { + init: function init() { + + bookmarks.addObserver(this, false); + }, + + __iterator__: function () (val for ([, val] in Iterator(self.bookmarks))), + + get bookmarks() Class.replaceProperty(this, "bookmarks", this.load()), + + rootFolders: ["toolbarFolder", "bookmarksMenuFolder", "unfiledBookmarksFolder"] + .map(function (s) bookmarks[s]), + + _deleteBookmark: function deleteBookmark(id) { + let length = bookmarks.length; + bookmarks = bookmarks.filter(function (item) item.id != id); + return bookmarks.length < length; + }, + + _loadBookmark: function loadBookmark(node) { + if (node.uri == null) // How does this happen? + return false; + let uri = util.newURI(node.uri); + let keyword = bookmarks.getKeywordForBookmark(node.itemId); + let tags = tagging.getTagsForURI(uri, {}) || []; + return Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); + }, + + readBookmark: function readBookmark(id) { + return { + itemId: id, + uri: bookmarks.getBookmarkURI(id).spec, + title: bookmarks.getItemTitle(id) + }; + }, + + findRoot: function findRoot(id) { + do { + var root = id; + id = bookmarks.getFolderIdForItem(id); + } while (id != bookmarks.placesRoot && id != root); + return root; + }, + + isBookmark: function (id) this.rootFolders.indexOf(this.findRoot(id)) >= 0, + + isRegularBookmark: function isRegularBookmark(id) { + do { + var root = id; + if (services.get("livemark") && services.get("livemark").isLivemark(id)) + return false; + id = bookmarks.getFolderIdForItem(id); + } while (id != bookmarks.placesRoot && id != root); + return this.rootFolders.indexOf(root) >= 0; + }, + + get keywords() [Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(this.bookmarks)) if (k.keyword)], + + // Should be made thread safe. + load: function load() { + let bookmarks = []; + + let folders = this.rootFolders.slice(); + let query = history.getNewQuery(); + let options = history.getNewQueryOptions(); + while (folders.length > 0) { + query.setFolders(folders, 1); + folders.shift(); + let result = history.executeQuery(query, options); + let folder = result.root; + folder.containerOpen = true; + + // iterate over the immediate children of this folder + for (let i = 0; i < folder.childCount; i++) { + let node = folder.getChild(i); + if (node.type == node.RESULT_TYPE_FOLDER) // folder + folders.push(node.itemId); + else if (node.type == node.RESULT_TYPE_URI) // bookmark + bookmarks.push(this._loadBookmark(node)); + } + + // close a container after using it! + folder.containerOpen = false; + } + + return bookmarks; + }, + + onBeginUpdateBatch: function onBeginUpdateBatch() {}, + onEndUpdateBatch: function onEndUpdateBatch() {}, + onItemVisited: function onItemVisited() {}, + onItemMoved: function onItemMoved() {}, + onItemAdded: function onItemAdded(itemId, folder, index) { + if (bookmarks.getItemType(itemId) == bookmarks.TYPE_BOOKMARK) { + if (self.isBookmark(itemId)) { + let bmark = this._loadBookmark(this.readBookmark(itemId)); + this.bookmarks.push(bmark); + storage.fireEvent(name, "add", bmark); + } + } + }, + onItemRemoved: function onItemRemoved(itemId, folder, index) { + if (this._deleteBookmark(itemId)) + storage.fireEvent(name, "remove", itemId); + }, + onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { + if (isAnnotation) + return; + let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; + if (bookmark) { + if (property == "tags") + value = tagging.getTagsForURI(util.newURI(bookmark.url), {}); + if (property in bookmark) + bookmark[property] = value; + storage.fireEvent(name, "change", itemId); + } + }, + QueryInterface: function QueryInterface(iid) { + if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}, { + getFavicon: function getFavicon(uri) { + try { + return service.get("favicon").getFaviconImageForPage(util.newURI(uri)).spec; + } + catch (e) { + return ""; + } + } +}); + +endmodule(); + +// vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/highlight.jsm b/common/modules/highlight.jsm new file mode 100644 index 00000000..fa184d0e --- /dev/null +++ b/common/modules/highlight.jsm @@ -0,0 +1,241 @@ +// Copyright (c) 2008-2010 by Kris Maglione <maglione.k at Gmail> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("highlight", this, { + exports: ["Highlight", "Highlights", "highlight"], + require: ["services", "styles"], + use: ["template"] +}); + +const Highlight = Struct("class", "selector", "filter", "default", "value", "base"); + +Highlight.defaultValue("filter", function () + this.base ? this.base.filter : + ["chrome://dactyl/*", + "dactyl:*", + "file://*"].concat(highlight.styleableChrome).join(",")); +Highlight.defaultValue("selector", function () highlight.selector(this.class)); +Highlight.defaultValue("value", function () this.default); +Highlight.defaultValue("base", function () { + let base = /^(\w*)/.exec(this.class)[0]; + return (base != this.class && base in highlight.highlight) ? highlight.highlight[base] : null; +}); +Highlight.prototype.toString = function () + "Highlight(" + this.class + ")\n\t" + + [k + ": " + String.quote(v) for ([k, v] in this)] + .join("\n\t"); + +/** + * A class to manage highlighting rules. The parameters are the + * standard parameters for any {@link Storage} object. + * + * @author Kris Maglione <maglione.k@gmail.com> + */ +const Highlights = Module("Highlight", { + init: function () { + this.highlight = {}; + }, + + keys: function keys() Object.keys(this.highlight).sort(), + + __iterator__: function () values(this.highlight), + + get: function (k) this.highlight[k], + set: function (key, newStyle, force, append) { + let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/); + + if (!(class_ in this.highlight)) + return "Unknown highlight keyword: " + class_; + + let style = this.highlight[key] || Highlight(key); + styles.removeSheet(true, style.selector); + + if (append) + newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); + if (/^\s*$/.test(newStyle)) + newStyle = null; + if (newStyle == null) { + if (style.default == null) { + delete this.highlight[style.class]; + styles.removeSheet(true, style.selector); + return null; + } + newStyle = style.default; + force = true; + } + + let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") + .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug + if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { + css = style.selector + " { " + css + " }"; + + let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); + if (error) + return error; + } + style.value = newStyle; + this.highlight[style.class] = style; + return null; + }, + + /** + * Gets a CSS selector given a highlight group. + * + * @param {string} class + */ + selector: function (class_) { + let [, hl, rest] = class_.match(/^(\w*)(.*)/); + let pattern = "[dactyl|highlight~=" + hl + "]" + if (this.highlight[hl] && this.highlight[hl].class != class_) + pattern = this.highlight[hl].selector; + return pattern + rest; + }, + + /** + * Clears all highlighting rules. Rules with default values are + * reset. + */ + clear: function () { + for (let [k, v] in Iterator(this.highlight)) + this.set(k, null, true); + }, + + /** + * Bulk loads new CSS rules. + * + * @param {string} css The rules to load. See {@link Highlights#css}. + */ + loadCSS: function (css) { + css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " ")) + .split("\n").filter(function (s) /\S/.test(s)) + .forEach(function (style) { + style = Highlight.apply(Highlight, + Array.slice(style.match(/^\s*((?:[^,\s]|\s\S)+)(?:,((?:[^,\s]|\s\S)+)?)?(?:,((?:[^,\s]|\s\S)+))?\s*(.*)$/), + 1)); + if (/^[>+ ]/.test(style.selector)) + style.selector = this.selector(style.class) + style.selector; + + let old = this.highlight[style.class]; + this.highlight[style.class] = style; + if (old && old.value != old.default) + style.value = old.value; + }, this); + for (let [class_, hl] in Iterator(this.highlight)) + if (hl.value == hl.default) + this.set(class_); + } +}, { +}, { + commands: function (dactyl, modules) { + const commands = modules.commands; + commands.add(["colo[rscheme]"], + "Load a color scheme", + function (args) { + let scheme = args[0]; + + if (scheme == "default") + highlight.clear(); + else + dactyl.assert(modules.io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]), + "E185: Cannot find color scheme " + scheme); + modules.autocommands.trigger("ColorScheme", { name: scheme }); + }, + { + argCount: "1", + completer: function (context) completion.colorScheme(context) + }); + + commands.add(["hi[ghlight]"], + "Set the style of certain display elements", + function (args) { + let style = <![CDATA[ + ; + display: inline-block !important; + position: static !important; + margin: 0px !important; padding: 0px !important; + width: 3em !important; min-width: 3em !important; max-width: 3em !important; + height: 1em !important; min-height: 1em !important; max-height: 1em !important; + overflow: hidden !important; + ]]>; + let clear = args[0] == "clear"; + if (clear) + args.shift(); + + let [key, css] = args; + dactyl.assert(!(clear && css), "E488: Trailing characters"); + + if (!css && !clear) + modules.commandline.commandOutput( + template.tabular(["Key", "Sample", "CSS"], + ["padding: 0 1em 0 0; vertical-align: top", + "text-align: center"], + ([h.class, + <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>, + template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)] + for (h in highlight) + if (!key || h.class.indexOf(key) > -1)))); + else if (!key && clear) + highlight.clear(); + else { + let error = highlight.set(key, css, clear, "-append" in args); + if (error) + dactyl.echoerr(error); + } + }, + { + // TODO: add this as a standard highlight completion function? + completer: function (context, args) { + // Complete a highlight group on :hi clear ... + if (args.completeArg > 0 && args[0] == "clear") + args.completeArg = args.completeArg > 1 ? -1 : 0; + + if (args.completeArg == 0) + context.completions = [[v.class, v.value] for (v in highlight)]; + else if (args.completeArg == 1) { + let hl = highlight.get(args[0]); + if (hl) + context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]]; + } + }, + hereDoc: true, + literal: 1, + options: [{ names: ["-append", "-a"], description: "Append new CSS to the existing value" }], + serialize: function () [ + { + command: this.name, + arguments: [k], + literalArg: v + } + for ([k, v] in Iterator(highlight)) + if (v.value != v.default) + ] + }); + }, + completion: function (dactyl, modules) { + const completion = modules.completion; + completion.colorScheme = function colorScheme(context) { + context.title = ["Color Scheme", "Runtime Path"]; + context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" }; + context.completions = util.Array.flatten( + modules.io.getRuntimeDirectories("colors").map( + function (dir) dir.readDirectory().filter( + function (file) /\.vimp$/.test(file.leafName)))) + + }; + + completion.highlightGroup = function highlightGroup(context) { + context.title = ["Highlight Group", "Value"]; + context.completions = [[v.class, v.value] for (v in highlight)]; + }; + } +}); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} + +endmodule(); + +// vim:se fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/content/services.js b/common/modules/services.jsm index 19cbfb09..afef4cd8 100644 --- a/common/content/services.js +++ b/common/modules/services.jsm @@ -4,14 +4,12 @@ // given in the LICENSE.txt file included with this file. "use strict"; -/** @scope modules */ +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("services", this, { + exports: ["Services", "services"] +}); -/** - * Cached XPCOM services and classes. - * - * @constructor - */ -const Services = Module("services", { +const Services = Module("Services", { init: function () { this.classes = {}; this.services = {}; @@ -54,9 +52,6 @@ const Services = Module("services", { this.addClass("timer", "@mozilla.org/timer;1", Ci.nsITimer); this.addClass("xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest); this.addClass("zipWriter", "@mozilla.org/zipwriter;1", Ci.nsIZipWriter); - - if (!this.get("extensionManager")) - Components.utils.import("resource://gre/modules/AddonManager.jsm", modules); }, _create: function (classes, ifaces, meth) { @@ -120,10 +115,19 @@ const Services = Module("services", { */ create: function (name) this.classes[name]() }, { +}, { + init: function (dactyl, modules) { + if (!this.get("extensionManager")) + Components.utils.import("resource://gre/modules/AddonManager.jsm", modules); + }, javascript: function (dactyl, modules) { - JavaScript.setCompleter(this.get, [function () services.services]); - JavaScript.setCompleter(this.create, [function () [[c, ""] for (c in services.classes)]]); + modules.JavaScript.setCompleter(this.get, [function () services.services]); + modules.JavaScript.setCompleter(this.create, [function () [[c, ""] for (c in services.classes)]]); } }); -// vim: set fdm=marker sw=4 ts=4 et: +endmodule(); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +// vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/storage.jsm b/common/modules/storage.jsm index f0fd006e..4d09d380 100644 --- a/common/modules/storage.jsm +++ b/common/modules/storage.jsm @@ -21,107 +21,22 @@ }}} ***** END LICENSE BLOCK *****/ "use strict"; -var EXPORTED_SYMBOLS = ["storage", "Timer"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -// XXX: does not belong here -function Timer(minInterval, maxInterval, callback) { - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.doneAt = 0; - this.latest = 0; - this.notify = function (aTimer) { - timer.cancel(); - this.latest = 0; - // minInterval is the time between the completion of the command and the next firing - this.doneAt = Date.now() + minInterval; +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("storage", this, { + exports: ["File", "storage"], + require: ["services", "util"] +}); - try { - callback(this.arg); - } - finally { - this.doneAt = Date.now() + minInterval; - } - }; - this.tell = function (arg) { - if (arguments.length > 0) - this.arg = arg; - - let now = Date.now(); - if (this.doneAt == -1) - timer.cancel(); - - let timeout = minInterval; - if (now > this.doneAt && this.doneAt > -1) - timeout = 0; - else if (this.latest) - timeout = Math.min(timeout, this.latest - now); - else - this.latest = now + maxInterval; - - timer.initWithCallback(this, Math.max(timeout, 0), timer.TYPE_ONE_SHOT); - this.doneAt = -1; - }; - this.reset = function () { - timer.cancel(); - this.doneAt = 0; - }; - this.flush = function () { - if (this.doneAt == -1) - this.notify(); - }; -} +var prefService = services.get("pref").getBranch("extensions.dactyl.datastore."); + +const win32 = services.get("runtime").OS == "Win32"; function getFile(name) { let file = storage.infoPath.clone(); file.append(name); - return file; -} - -function readFile(file) { - let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - let stream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - try { - fileStream.init(file, -1, 0, 0); - stream.init(fileStream, "UTF-8", 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering - - let hunks = []; - let res = {}; - while (stream.readString(4096, res) != 0) - hunks.push(res.value); - - stream.close(); - fileStream.close(); - - return hunks.join(""); - } - catch (e) {} -} - -function writeFile(file, data) { - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, parseInt('0600', 8)); - - let fileStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - - fileStream.init(file, 0x20 | 0x08 | 0x02, parseInt('0600', 8), 0); // PR_TRUNCATE | PR_CREATE | PR_WRITE - stream.init(fileStream, "UTF-8", 0, 0); - - stream.writeString(data); - - stream.close(); - fileStream.close(); + return File(file); } -var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); -var prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService) - .getBranch("extensions.dactyl.datastore."); -var json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - function getCharPref(name) { try { return prefService.getComplexValue(name, Ci.nsISupportsString).data; @@ -140,9 +55,9 @@ function loadPref(name, store, type) { if (store) var pref = getCharPref(name); if (!pref && storage.infoPath) - var file = readFile(getFile(name)); + var file = getFile(name).read(); if (pref || file) - var result = json.decode(pref || file); + var result = services.get("json").decode(pref || file); if (pref) { prefService.clearUserPref(name); savePref({ name: name, store: true, serial: pref }); @@ -157,122 +72,110 @@ function savePref(obj) { if (obj.privateData && storage.privateMode) return; if (obj.store && storage.infoPath) - writeFile(getFile(obj.name), obj.serial); + getFile(obj.name).write(obj.serial); } -var prototype = { +const StoreBase = Class("StoreBase", { OPTIONS: ["privateData"], fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); }, + get serial() services.get("json").encode(this._object), save: function () { savePref(this); }, - init: function (name, store, data, options) { + init: function (name, store, load, options) { + this._load = load; + this.__defineGetter__("store", function () store); this.__defineGetter__("name", function () name); for (let [k, v] in Iterator(options)) if (this.OPTIONS.indexOf(k) >= 0) this[k] = v; this.reload(); - } -}; - -function ObjectStore(name, store, load, options) { - var object = {}; - - this.reload = function reload() { - object = load() || {}; + }, + reload: function reload() { + this._object = this._load() || this._constructor(); this.fireEvent("change", null); - }; + } +}); - this.init.apply(this, arguments); - this.__defineGetter__("serial", function () json.encode(object)); +const ObjectStore = Class("ObjectStore", StoreBase, { + _constructor: Object, - this.set = function set(key, val) { - var defined = key in object; - var orig = object[key]; - object[key] = val; + set: function set(key, val) { + var defined = key in this._object; + var orig = this._object[key]; + this._object[key] = val; if (!defined) this.fireEvent("add", key); else if (orig != val) this.fireEvent("change", key); - }; + }, - this.remove = function remove(key) { - var ret = object[key]; - delete object[key]; + remove: function remove(key) { + var ret = this._object[key]; + delete this._object[key]; this.fireEvent("remove", key); return ret; - }; - - this.get = function get(val, default_) val in object ? object[val] : default_; + }, - this.clear = function () { - object = {}; - }; + get: function get(val, default_) val in this._object ? this._object[val] : default_, - this.__iterator__ = function () Iterator(object); -} -ObjectStore.prototype = prototype; + clear: function () { + this._object = {}; + }, -function ArrayStore(name, store, load, options) { - var array = []; + __iterator__: function () Iterator(this._object), +}); - this.reload = function reload() { - array = load() || []; - this.fireEvent("change", null); - }; +const ArrayStore = Class("ArrayStore", StoreBase, { + _constructor: Array, - this.init.apply(this, arguments); - this.__defineGetter__("serial", function () json.encode(array)); - this.__defineGetter__("length", function () array.length); + get length() this._object.length, - this.set = function set(index, value) { - var orig = array[index]; - array[index] = value; + set: function set(index, value) { + var orig = this._object[index]; + this._object[index] = value; this.fireEvent("change", index); - }; + }, - this.push = function push(value) { - array.push(value); - this.fireEvent("push", array.length); - }; + push: function push(value) { + this._object.push(value); + this.fireEvent("push", this._object.length); + }, - this.pop = function pop(value) { - var ret = array.pop(); - this.fireEvent("pop", array.length); + pop: function pop(value) { + var ret = this._object.pop(); + this.fireEvent("pop", this._object.length); return ret; - }; + }, - this.truncate = function truncate(length, fromEnd) { - var ret = array.length; - if (array.length > length) { + truncate: function truncate(length, fromEnd) { + var ret = this._object.length; + if (this._object.length > length) { if (fromEnd) - array.splice(0, array.length - length); - array.length = length; + this._object.splice(0, this._object.length - length); + this._object.length = length; this.fireEvent("truncate", length); } return ret; - }; + }, // XXX: Awkward. - this.mutate = function mutate(aFuncName) { - var funcName = aFuncName; - arguments[0] = array; - array = Array[funcName].apply(Array, arguments); + mutate: function mutate(funcName) { + var _funcName = funcName; + arguments[0] = this._object; + this._object = Array[_funcName].apply(Array, arguments); this.fireEvent("change", null); - }; + }, - this.get = function get(index) { - return index >= 0 ? array[index] : array[array.length + index]; - }; + get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index], - this.__iterator__ = function () Iterator(array); -} -ArrayStore.prototype = prototype; + __iterator__: function () Iterator(this._object), +}); var keys = {}; var observers = {}; var timers = {}; -var storage = { +const Storage = Module("Storage", { alwaysReload: {}, newObject: function newObject(key, constructor, params) { if (!(key in keys) || params.reload || this.alwaysReload[key]) { @@ -363,6 +266,304 @@ var storage = { this.load(key); return this._privateMode = Boolean(val); } -}; +}, { +}, { + init: function (dactyl, modules) { + let infoPath = services.create("file"); + infoPath.initWithPath(File.expandPath(modules.IO.runtimePath.replace(/,.*/, ""))); + infoPath.append("info"); + infoPath.append(dactyl.profileName); + storage.infoPath = infoPath; + } +}); + +/** + * @class File A class to wrap nsIFile objects and simplify operations + * thereon. + * + * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} + * @param {boolean} checkPWD Whether to allow expansion relative to the + * current directory. @default true + */ +const File = Class("File", { + init: function (path, checkPWD) { + let file = services.create("file"); + + if (path instanceof Ci.nsIFile) + file = path; + else if (/file:\/\//.test(path)) + file = services.create("file:").getFileFromURLSpec(path); + else { + let expandedPath = File.expandPath(path); + + if (!File.isAbsolutePath(expandedPath) && checkPWD) + file = File.joinPaths(checkPWD, expandedPath); + else + file.initWithPath(expandedPath); + } + let self = XPCSafeJSObjectWrapper(file); + self.__proto__ = File.prototype; + return self; + }, + + /** + * Iterates over the objects in this directory. + */ + iterDirectory: function () { + if (!this.isDirectory()) + throw Error("Not a directory"); + let entries = this.directoryEntries; + while (entries.hasMoreElements()) + yield File(entries.getNext().QueryInterface(Ci.nsIFile)); + }, + + /** + * Reads this file's entire contents in "text" mode and returns the + * content as a string. + * + * @param {string} encoding The encoding from which to decode the file. + * @default options["fileencoding"] + * @returns {string} + */ + read: function (encoding) { + let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); + + if (!encoding) + encoding = File.defaultEncoding; + + ifstream.init(this, -1, 0, 0); + icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering + + let buffer = []; + let str = {}; + while (icstream.readString(4096, str) != 0) + buffer.push(str.value); + + icstream.close(); + ifstream.close(); + return buffer.join(""); + }, + + /** + * Returns the list of files in this directory. + * + * @param {boolean} sort Whether to sort the returned directory + * entries. + * @returns {nsIFile[]} + */ + readDirectory: function (sort) { + if (!this.isDirectory()) + throw Error("Not a directory"); + + let array = [e for (e in this.iterDirectory())]; + if (sort) + array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); + return array; + }, + + /** + * Writes the string <b>buf</b> to this file. + * + * @param {string} buf The file content. + * @param {string|number} mode The file access mode, a bitwise OR of + * the following flags: + * {@link #MODE_RDONLY}: 0x01 + * {@link #MODE_WRONLY}: 0x02 + * {@link #MODE_RDWR}: 0x04 + * {@link #MODE_CREATE}: 0x08 + * {@link #MODE_APPEND}: 0x10 + * {@link #MODE_TRUNCATE}: 0x20 + * {@link #MODE_SYNC}: 0x40 + * Alternatively, the following abbreviations may be used: + * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} + * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} + * @default ">" + * @param {number} perms The file mode bits of the created file. This + * is only used when creating a new file and does not change + * permissions if the file exists. + * @default 0644 + * @param {string} encoding The encoding to used to write the file. + * @default options["fileencoding"] + */ + write: function (buf, mode, perms, encoding) { + let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + function getStream(defaultChar) { + let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); + stream.init(ofstream, encoding, 0, defaultChar); + return stream; + } + + if (!encoding) + encoding = File.defaultEncoding; + + if (mode == ">>") + mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND; + else if (!mode || mode == ">") + mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; + + if (!perms) + perms = parseInt('0644', 8); + + ofstream.init(this, mode, perms, 0); + let ocstream = getStream(0); + try { + ocstream.writeString(buf); + } + catch (e) { + if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { + ocstream = getStream("?".charCodeAt(0)); + ocstream.writeString(buf); + return false; + } + else + throw e; + } + finally { + try { + ocstream.close(); + } + catch (e) {} + ofstream.close(); + } + return true; + } +}, { + /** + * @property {number} Open for reading only. + * @final + */ + MODE_RDONLY: 0x01, + + /** + * @property {number} Open for writing only. + * @final + */ + MODE_WRONLY: 0x02, + + /** + * @property {number} Open for reading and writing. + * @final + */ + MODE_RDWR: 0x04, + + /** + * @property {number} If the file does not exist, the file is created. + * If the file exists, this flag has no effect. + * @final + */ + MODE_CREATE: 0x08, + + /** + * @property {number} The file pointer is set to the end of the file + * prior to each write. + * @final + */ + MODE_APPEND: 0x10, + + /** + * @property {number} If the file exists, its length is truncated to 0. + * @final + */ + MODE_TRUNCATE: 0x20, + + /** + * @property {number} If set, each write will wait for both the file + * data and file status to be physically updated. + * @final + */ + MODE_SYNC: 0x40, + + /** + * @property {number} With MODE_CREATE, if the file does not exist, the + * file is created. If the file already exists, no action and NULL + * is returned. + * @final + */ + MODE_EXCL: 0x80, + + /** + * @property {string} The current platform's path seperator. + */ + get PATH_SEP() { + delete this.PATH_SEP; + let f = services.get("directory").get("CurProcD", Ci.nsIFile); + f.append("foo"); + return this.PATH_SEP = f.path.substr(f.parent.path.length, 1); + }, + + defaultEncoding: "UTF-8", + + expandPath: function (path, relative) { + + // expand any $ENV vars - this is naive but so is Vim and we like to be compatible + // TODO: Vim does not expand variables set to an empty string (and documents it). + // Kris reckons we shouldn't replicate this 'bug'. --djk + // TODO: should we be doing this for all paths? + function expand(path) path.replace( + !win32 ? /\$(\w+)\b|\${(\w+)}/g + : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g, + function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m + ); + path = expand(path); + + // expand ~ + // Yuck. + if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(File.PATH_SEP) + "])").test(path)) { + // Try $HOME first, on all systems + let home = services.get("environment").get("HOME"); + + // Windows has its own idiosyncratic $HOME variables. + if (!home && win32) + home = services.get("environment").get("USERPROFILE") || + services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH"); + + path = home + path.substr(1); + } + + // TODO: Vim expands paths twice, once before checking for ~, once + // after, but doesn't document it. Is this just a bug? --Kris + path = expand(path); + return path.replace("/", File.PATH_SEP, "g"); + }, + + expandPathList: function (list) list.map(this.expandPath), + + getPathsFromPathList: function (list) { + if (!list) + return []; + // empty list item means the current directory + return list.replace(/,$/, "").split(",") + .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); + }, + + isAbsolutePath: function (path) { + try { + services.create("file").initWithPath(path); + return true; + } + catch (e) { + return false; + } + }, + + joinPaths: function (head, tail) { + let path = this(head); + try { + // FIXME: should only expand env vars and normalise path separators + path.appendRelativePath(this.expandPath(tail, true)); + } + catch (e) { + return { exists: function () false, __noSuchMethod__: function () { throw e; } }; + } + return path; + }, + + replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g") +}); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +endmodule(); // vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/styles.jsm b/common/modules/styles.jsm new file mode 100644 index 00000000..8b8b1c1c --- /dev/null +++ b/common/modules/styles.jsm @@ -0,0 +1,373 @@ +// Copyright (c) 2008-2010 by Kris Maglione <maglione.k at Gmail> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("styles", this, { + exports: ["Style", "Styles", "styles"], + require: ["services", "util"] +}); + +const sss = services.get("stylesheet"); +function cssUri(css) "chrome-data:text/css," + encodeURI(css); +const namespace = "@namespace html " + XHTML.uri.quote() + ";\n" + + "@namespace xul " + XUL.uri.quote() + ";\n" + + "@namespace dactyl " + NS.uri.quote() + ";\n"; + +const Sheet = Struct("name", "id", "sites", "css", "system", "agent"); +Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() { + let filter = this.sites; + let css = this.css; + if (filter[0] == "*") + return namespace + css; + + let selectors = filter.map(function (part) + (/[*]$/.test(part) ? "url-prefix" : + /[\/:]/.test(part) ? "url" + : "domain") + + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")') + .join(", "); + return "/* Dactyl style #" + this.id + " */ " + namespace + " @-moz-document " + selectors + "{\n" + css + "\n}\n"; +}); +Sheet.prototype.__defineGetter__("enabled", function () this._enabled); +Sheet.prototype.__defineSetter__("enabled", function (on) { + this._enabled = Boolean(on); + let meth = on ? "registerSheet" : "unregisterSheet"; + + styles[meth](cssUri(this.fullCSS)); + if (this.agent) + styles[meth](cssUri(this.fullCSS), true); +}); + +/** + * Manages named and unnamed user style sheets, which apply to both + * chrome and content pages. The parameters are the standard + * parameters for any {@link Storage} object. + * + * @author Kris Maglione <maglione.k@gmail.com> + */ +const Styles = Module("Styles", { + init: function() { + this._id = 0; + this.userSheets = []; + this.systemSheets = []; + this.userNames = {}; + this.systemNames = {}; + }, + + get sites() array(this.userSheets).map(function (s) s.sites).flatten().uniq().__proto__, + + __iterator__: function () Iterator(this.userSheets.concat(this.systemSheets)), + + /** + * Add a new style sheet. + * + * @param {boolean} system Declares whether this is a system or + * user sheet. System sheets are used internally by + * @dactyl. + * @param {string} name The name given to the style sheet by + * which it may be later referenced. + * @param {string} filter The sites to which this sheet will + * apply. Can be a domain name or a URL. Any URL ending in + * "*" is matched as a prefix. + * @param {string} css The CSS to be applied. + */ + addSheet: function addSheet(system, name, filter, css, agent) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + if (name && name in names) + this.removeSheet(system, name); + + let sheet = Sheet(name, this._id++, filter.split(",").filter(util.identity), String(css), null, system, agent); + + try { + sheet.enabled = true; + } + catch (e) { + return e.echoerr || e; + } + sheets.push(sheet); + + if (name) + names[name] = sheet; + return null; + }, + + /** + * Get a sheet with a given name or index. + * + * @param {boolean} system + * @param {string or number} sheet The sheet to retrieve. Strings indicate + * sheet names, while numbers indicate indices. + */ + get: function getget(system, sheet) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + if (typeof sheet === "number") + return sheets[sheet]; + return names[sheet]; + }, + + /** + * Find sheets matching the parameters. See {@link #addSheet} + * for parameters. + * + * @param {boolean} system + * @param {string} name + * @param {string} filter + * @param {string} css + * @param {number} index + */ + findSheets: function findSheets(system, name, filter, css, index) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + + // Grossly inefficient. + let matches = [k for ([k, v] in Iterator(sheets))]; + if (index) + matches = String(index).split(",").filter(function (i) i in sheets); + if (name) + matches = matches.filter(function (i) sheets[i] == names[name]); + if (css) + matches = matches.filter(function (i) sheets[i].css == css); + if (filter) + matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0); + return matches.map(function (i) sheets[i]); + }, + + /** + * Remove a style sheet. See {@link #addSheet} for parameters. + * In cases where <b>filter</b> is supplied, the given filters + * are removed from matching sheets. If any remain, the sheet is + * left in place. + * + * @param {boolean} system + * @param {string} name + * @param {string} filter + * @param {string} css + * @param {number} index + */ + removeSheet: function removeSheet(system, name, filter, css, index) { + let self = this; + if (arguments.length == 1) { + var matches = [system]; + system = matches[0].system; + } + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + + if (filter && filter.indexOf(",") > -1) + return filter.split(",").reduce( + function (n, f) n + self.removeSheet(system, name, f, index), 0); + + if (filter == undefined) + filter = ""; + + if (!matches) + matches = this.findSheets(system, name, filter, css, index); + if (matches.length == 0) + return null; + + for (let [, sheet] in Iterator(matches.reverse())) { + sheet.enabled = false; + if (name) + delete names[name]; + if (sheets.indexOf(sheet) > -1) + sheets.splice(sheets.indexOf(sheet), 1); + + /* Re-add if we're only changing the site filter. */ + if (filter) { + let sites = sheet.sites.filter(function (f) f != filter); + if (sites.length) + this.addSheet(system, name, sites.join(","), css, sheet.agent); + } + } + return matches.length; + }, + + /** + * Register a user style sheet at the given URI. + * + * @param {string} url The URI of the sheet to register. + * @param {boolean} agent If true, sheet is registered as an agent sheet. + * @param {boolean} reload Whether to reload any sheets that are + * already registered. + */ + registerSheet: function registerSheet(url, agent, reload) { + let uri = services.get("io").newURI(url, null, null); + if (reload) + this.unregisterSheet(url, agent); + if (reload || !sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) + sss.loadAndRegisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); + }, + + /** + * Unregister a sheet at the given URI. + * + * @param {string} url The URI of the sheet to unregister. + * @param {boolean} agent If true, sheet is registered as an agent sheet. + */ + unregisterSheet: function unregisterSheet(url, agent) { + let uri = services.get("io").newURI(url, null, null); + if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) + sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); + }, +}, { + completeSite: function (context, content) { + context.anchored = false; + try { + context.fork("current", 0, this, function (context) { + context.title = ["Current Site"]; + context.completions = [ + [content.location.host, "Current Host"], + [content.location.href, "Current URL"] + ]; + }); + } + catch (e) {} + context.fork("others", 0, this, function (context) { + context.title = ["Site"]; + context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))]; + }); + } +}, { + commands: function (dactyl, modules, window) { + const commands = modules.commands; + commands.add(["sty[le]"], + "Add or list user styles", + function (args) { + let [filter, css] = args; + let name = args["-name"]; + + if (!css) { + let list = Array.concat([i for (i in Iterator(styles.userNames))], + [i for (i in Iterator(styles.userSheets)) if (!i[1].name)]); + modules.commandline.commandOutput( + template.tabular(["", "Name", "Filter", "CSS"], + ["min-width: 1em; text-align: center; color: red; font-weight: bold;", + "padding: 0 1em 0 1ex; vertical-align: top;", + "padding: 0 1em 0 0; vertical-align: top;"], + ([sheet.enabled ? "" : "\u00d7", + key, + sheet.sites.join(","), + sheet.css] + for ([i, [key, sheet]] in Iterator(list)) + if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name))))); + } + else { + if ("-append" in args) { + let sheet = styles.get(false, name); + if (sheet) { + filter = sheet.sites.concat(filter).join(","); + css = sheet.css + " " + css; + } + } + let err = styles.addSheet(false, name, filter, css); + if (err) + dactyl.echoerr(err); + } + }, + { + bang: true, + completer: function (context, args) { + let compl = []; + if (args.completeArg == 0) + Styles.completeSite(context, window.content); + else if (args.completeArg == 1) { + let sheet = styles.get(false, args["-name"]); + if (sheet) + context.completions = [[sheet.css, "Current Value"]]; + } + }, + hereDoc: true, + literal: 1, + options: [ + { + names: ["-name", "-n"], + description: "The name of this stylesheet", + completer: function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))], + type: modules.CommandOption.STRING + }, + { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" } + ], + serialize: function () [ + { + command: this.name, + arguments: [sty.sites.join(",")], + bang: true, + literalArg: sty.css, + options: sty.name ? { "-name": sty.name } : {} + } for ([k, sty] in Iterator(styles.userSheets)) + ] + }); + + [ + { + name: ["stylee[nable]", "stye[nable]"], + desc: "Enable a user style sheet", + action: function (sheet) sheet.enabled = true, + filter: function (sheet) !sheet.enabled + }, + { + name: ["styled[isable]", "styd[isable]"], + desc: "Disable a user style sheet", + action: function (sheet) sheet.enabled = false, + filter: function (sheet) sheet.enabled + }, + { + name: ["stylet[oggle]", "styt[oggle]"], + desc: "Toggle a user style sheet", + action: function (sheet) sheet.enabled = !sheet.enabled + }, + { + name: ["dels[tyle]"], + desc: "Remove a user style sheet", + action: function (sheet) styles.removeSheet(sheet) + } + ].forEach(function (cmd) { + commands.add(cmd.name, cmd.desc, + function (args) { + styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"]) + .forEach(cmd.action); + }, + { + completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); }, + literal: 1, + options: [ + { + names: ["-index", "-i"], + type: modules.CommandOption.INT, + completer: function (context) { + context.compare = CompletionContext.Sort.number; + return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}</>] + for ([i, sheet] in styles.userSheets) + if (!cmd.filter || cmd.filter(sheet))]; + }, + }, { + names: ["-name", "-n"], + type: modules.CommandOption.STRING, + completer: function () [[name, sheet.css] + for ([name, sheet] in Iterator(styles.userNames)) + if (!cmd.filter || cmd.filter(sheet))] + } + ] + }); + }); + }, + javascript: function (dactyl, modules, window) { + modules.JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), + [ // Prototype: (system, name, filter, css, index) + null, + function (context, obj, args) args[0] ? this.systemNames : this.userNames, + function (context, obj, args) Styles.completeSite(context, window.content), + null, + function (context, obj, args) args[0] ? this.systemSheets : this.userSheets + ]); + } +}); + +endmodule(); + +// vim:se fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/content/template.js b/common/modules/template.jsm index ffe9ea1e..f444c184 100644 --- a/common/content/template.js +++ b/common/modules/template.jsm @@ -4,9 +4,17 @@ // given in the LICENSE.txt file included with this file. "use strict"; -/** @scope modules */ +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("template", this, { + exports: ["Template", "template"], + require: ["util"] +}); + +default xml namespace = XHTML; +XML.ignoreWhiteSpace = true; +XML.prettyPrinting = false; -const Template = Module("template", { +const Template = Module("Template", { add: function add(a, b) a + b, join: function join(c) function (a, b) a + c + b, @@ -38,6 +46,23 @@ const Template = Module("template", { return <>{xml}</>; }, + bookmarkDescription: function (item, text) + <> + <a href={item.item.url} highlight="URL">{text}</a>  + { + !(item.extra && item.extra.length) ? "" : + <span class="extra-info"> + ({ + template.map(item.extra, function (e) + <>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>, + <> </>/* Non-breaking space */) + }) + </span> + } + </>, + + filter: function (str) <span highlight="Filter">{str}</span>, + completionRow: function completionRow(item, highlightGroup) { if (typeof icon == "function") icon = icon(); @@ -63,27 +88,15 @@ const Template = Module("template", { // </e4x> }, - bookmarkDescription: function (item, text) - <> - <a href={item.item.url} highlight="URL">{text}</a>  - { - !(item.extra && item.extra.length) ? "" : - <span class="extra-info"> - ({ - template.map(item.extra, function (e) - <>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>, - <> </>/* Non-breaking space */) - }) - </span> - } - </>, - - icon: function (item, text) { - return <><span highlight="CompIcon">{item.icon ? <img src={item.icon}/> : <></>}</span><span class="td-strut"/>{text}</> + genericTable: function genericTable(items, format) { + completion.listCompleter(function (context) { + context.filterFunc = null; + if (format) + context.format = format; + context.completions = items; + }); }, - filter: function (str) <span highlight="Filter">{str}</span>, - gradient: function (left, right) <div highlight="Gradient"> <div style="height: 0px"> @@ -192,23 +205,13 @@ const Template = Module("template", { return str; }, - commandOutput: function generic(command, xml) { - return <>:{command}<br/>{xml}</>; - }, - - genericTable: function genericTable(items, format) { - completion.listCompleter(function (context) { - context.filterFunc = null; - if (format) - context.format = format; - context.completions = items; - }); - }, + icon: function (item, text) <> + <span highlight="CompIcon">{item.icon ? <img src={item.icon}/> : <></>}</span><span class="td-strut"/>{text} + </>, jumps: function jumps(index, elems) { // <e4x> - return this.commandOutput( - <table> + return <table> <tr style="text-align: left;" highlight="Title"> <th colspan="2">jump</th><th>title</th><th>URI</th> </tr> @@ -221,14 +224,13 @@ const Template = Module("template", { <td><a href={val.URI.spec} highlight="URL jump-list">{val.URI.spec}</a></td> </tr>) } - </table>); + </table>; // </e4x> }, options: function options(title, opts) { // <e4x> - return this.commandOutput( - <table> + return <table> <tr highlight="Title" align="left"> <th>--- {title} ---</th> </tr> @@ -241,7 +243,7 @@ const Template = Module("template", { </td> </tr>) } - </table>); + </table>; // </e4x> }, @@ -269,8 +271,7 @@ const Template = Module("template", { tabular: function tabular(headings, style, iter) { // TODO: This might be mind-bogglingly slow. We'll see. // <e4x> - return this.commandOutput( - <table> + return <table> <tr highlight="Title" align="left"> { this.map(headings, function (h) @@ -286,14 +287,13 @@ const Template = Module("template", { } </tr>) } - </table>); + </table>; // </e4x> }, usage: function usage(iter) { // <e4x> - return this.commandOutput( - <table> + return <table> { this.map(iter, function (item) <tr> @@ -301,9 +301,11 @@ const Template = Module("template", { <td>{item.description}</td> </tr>) } - </table>); + </table>; // </e4x> } }); -// vim: set fdm=marker sw=4 ts=4 et: +endmodule(); + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/content/util.js b/common/modules/util.jsm index a0ae92c7..5d854b44 100644 --- a/common/content/util.js +++ b/common/modules/util.jsm @@ -6,25 +6,71 @@ // given in the LICENSE.txt file included with this file. "use strict"; -/** @scope modules */ +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("util", this, { + exports: ["Math", "NS", "Util", "XHTML", "XUL", "util"], + require: ["services"], + use: ["template"] +}); const XHTML = Namespace("html", "http://www.w3.org/1999/xhtml"); const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); const NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator"); default xml namespace = XHTML; -const Util = Module("util", { +const Util = Module("Util", { init: function () { this.Array = array; }, get activeWindow() services.get("windowWatcher").activeWindow, + dactyl: { + __noSuchMethod__: function (meth, args) { + let win = util.activeWindow; + if(win && win.dactyl) + return win.dactyl[meth].apply(win.dactyl, args); + return null; + } + }, + callInMainThread: function (callback, self) { let mainThread = services.get("threadManager").mainThread; - if (!services.get("threadManager").isMainThread) - mainThread.dispatch({ run: callback.call(self) }, mainThread.DISPATCH_NORMAL); - else + if (services.get("threadManager").isMainThread) callback.call(self); + else + mainThread.dispatch(Runnable(self, callback), mainThread.DISPATCH_NORMAL); + }, + + /** + * Calls a function asynchronously on a new thread. + * + * @param {nsIThread} thread The thread to call the function on. If no + * thread is specified a new one is created. + * @optional + * @param {Object} self The 'this' object used when executing the + * function. + * @param {function} func The function to execute. + * + */ + callAsync: function (thread, self, func) { + thread = thread || services.get("threadManager").newThread(0); + thread.dispatch(Runnable(self, func, Array.slice(arguments, 3)), thread.DISPATCH_NORMAL); + }, + + /** + * Calls a function synchronously on a new thread. + * + * NOTE: Be sure to call GUI related methods like alert() or dump() + * ONLY in the main thread. + * + * @param {nsIThread} thread The thread to call the function on. If no + * thread is specified a new one is created. + * @optional + * @param {function} func The function to execute. + */ + callInThread: function (thread, func) { + thread = thread || services.get("threadManager").newThread(0); + thread.dispatch(Runnable(null, func, Array.slice(arguments, 2)), thread.DISPATCH_SYNC); }, /** @@ -88,7 +134,7 @@ const Util = Module("util", { clipboardHelper.copyString(str); if (verbose) - dactyl.echo("Yanked " + str, commandline.FORCE_SINGLELINE); + util.dactyl.echomsg("Yanked " + str); }, /** @@ -112,7 +158,10 @@ const Util = Module("util", { * @param {string} pattern The pattern to deglob. * @returns [string] The resulting strings. */ - debrace: function deglobBrace(pattern) { + debrace: function debrace(pattern) { + if (pattern.indexOf("{") == -1) + return [pattern]; + function split(pattern, re, fn, dequote) { let end = 0, match, res = []; while (match = re.exec(pattern)) { @@ -132,7 +181,7 @@ const Util = Module("util", { }, "{}"); function rec(acc) { if (acc.length == patterns.length) - res.push(util.Array.zip(substrings, acc).join("")); + res.push(array(substrings).zip(acc).flatten().join("")); else for (let [, pattern] in Iterator(patterns[acc.length])) rec(acc.concat(pattern)); @@ -203,7 +252,7 @@ const Util = Module("util", { */ evaluateXPath: function (expression, doc, elem, asIterator) { if (!doc) - doc = content.document; + doc = util.activeWindow.content.document; if (!elem) elem = doc; if (isarray(expression)) @@ -247,20 +296,6 @@ const Util = Module("util", { }, /** - * Returns the selection controller for the given window. - * - * @param {Window} window - * @returns {nsISelectionController} - */ - selectionController: function (win) - win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsISelectionDisplay) - .QueryInterface(Ci.nsISelectionController), - - /** * Converts <b>bytes</b> to a pretty printed data size string. * * @param {number} bytes The number of bytes. @@ -326,7 +361,7 @@ const Util = Module("util", { return xmlhttp; } catch (e) { - dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); + util.dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); return null; } }, @@ -396,6 +431,8 @@ const Util = Module("util", { */ memoize: memoize, + newThread: function () services.get("threadManager").newThread(0), + /** * Converts a URI string into a URI object. * @@ -471,13 +508,11 @@ const Util = Module("util", { let keys = []; try { // window.content often does not want to be queried with "var i in object" - let hasValue = !("__iterator__" in object); - /* - if (modules.isPrototypeOf(object)) { + let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"])); + if (object.dactyl && object.modules && object.modules.modules == object.modules) { object = Iterator(object); hasValue = false; } - */ for (let i in object) { let value = <![CDATA[<no value>]]>; try { @@ -604,6 +639,28 @@ const Util = Module("util", { elem.scrollIntoView(); }, + /** + * Returns the selection controller for the given window. + * + * @param {Window} window + * @returns {nsISelectionController} + */ + selectionController: function (win) + win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController), + + /** + * Suspend execution for at least 'delay' milliseconds. Functions by + * yielding execution to the next item in the main event queue, and + * so may lead to unexpected call graphs, and long delays if another + * handler yields execution while waiting. + * + * @param {number} delay The time period for which to sleep in milliseconds. + */ sleep: function (delay) { let mainThread = services.get("threadManager").mainThread; @@ -613,6 +670,44 @@ const Util = Module("util", { return true; }, + highlightFilter: function highlightFilter(str, filter, highlight) { + return this.highlightSubstrings(str, (function () { + if (filter.length == 0) + return; + let lcstr = String.toLowerCase(str); + let lcfilter = filter.toLowerCase(); + let start = 0; + while ((start = lcstr.indexOf(lcfilter, start)) > -1) { + yield [start, filter.length]; + start += filter.length; + } + })(), highlight || template.filter); + }, + + /** + * Behaves like String.split, except that when 'limit' is reached, + * the trailing element contains the entire trailing portion of the + * string. + * + * util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"] + * @param {string} str The string to split. + * @param {RegExp|string} re The regular expression on which to split the string. + * @param {number} limit The maximum number of elements to return. + * @returns {[string]} + */ + split: function (str, re, limit) { + if (!re.global) + re = RegExp(re.source || re, "g"); + let match, start = 0, res = []; + while ((match = re.exec(str)) && --limit && match[0].length) { + res.push(str.substring(start, match.index)); + start = match.index + match[0].length; + } + if (limit) + res.push(str.substring(start)); + return res; + }, + /** * Split a string on literal occurrences of a marker. * @@ -670,7 +765,7 @@ const Util = Module("util", { if (node.length() != 1) { let domnode = doc.createDocumentFragment(); for each (let child in node) - domnode.appendChild(arguments.callee(child, doc, nodes)); + domnode.appendChild(xmlToDom(child, doc, nodes)); return domnode; } switch (node.nodeKind()) { @@ -681,7 +776,7 @@ const Util = Module("util", { for each (let attr in node.@*) domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr)); for each (let child in node.*) - domnode.appendChild(arguments.callee(child, doc, nodes)); + domnode.appendChild(xmlToDom(child, doc, nodes)); if (nodes && node.@key) nodes[node.@key] = domnode; return domnode; @@ -697,8 +792,9 @@ const Util = Module("util", { * Math utility methods. * @singleton */ +const GlobalMath = Math; var Math = { - __proto__: window.Math, + __proto__: GlobalMath, /** * Returns the specified <b>value</b> constrained to the range <b>min</b> - @@ -712,4 +808,8 @@ var Math = { constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max) }; -// vim: set fdm=marker sw=4 ts=4 et: +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +endmodule(); + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: |