summaryrefslogtreecommitdiff
path: root/common/content
diff options
context:
space:
mode:
authorDaniel Bainton <dpb@driftaway.org>2008-12-04 07:56:35 +0200
committerDaniel Bainton <dpb@driftaway.org>2008-12-04 07:56:35 +0200
commit729854c749e46bf97a97951614d96cfee302cd9d (patch)
tree6a64a10b3303ff7f5b0d3e4d5398a7544b0f58ff /common/content
parentcc6bdfc2fcaa42186dddac10ed96046e4dc7c5f3 (diff)
downloadpentadactyl-729854c749e46bf97a97951614d96cfee302cd9d.tar.gz
Rename liberator/ to common/
Diffstat (limited to 'common/content')
-rw-r--r--common/content/README.E4X149
-rw-r--r--common/content/bindings.xml37
-rw-r--r--common/content/buffer.js1708
-rw-r--r--common/content/buffer.xhtml9
-rw-r--r--common/content/commands.js892
-rw-r--r--common/content/completion.js1704
-rw-r--r--common/content/editor.js1128
-rw-r--r--common/content/eval.js10
-rw-r--r--common/content/events.js1694
-rw-r--r--common/content/find.js473
-rw-r--r--common/content/help.css149
-rw-r--r--common/content/hints.js811
-rw-r--r--common/content/io.js966
-rw-r--r--common/content/liberator-overlay.js58
-rw-r--r--common/content/liberator.js1360
-rw-r--r--common/content/liberator.xul115
-rw-r--r--common/content/mappings.js414
-rw-r--r--common/content/modes.js294
-rw-r--r--common/content/options.js984
-rw-r--r--common/content/style.js582
-rw-r--r--common/content/tabs.js1000
-rw-r--r--common/content/template.js305
-rw-r--r--common/content/ui.js1860
-rw-r--r--common/content/util.js597
24 files changed, 17299 insertions, 0 deletions
diff --git a/common/content/README.E4X b/common/content/README.E4X
new file mode 100644
index 00000000..3edb7714
--- /dev/null
+++ b/common/content/README.E4X
@@ -0,0 +1,149 @@
+ A terse introduction to E4X
+ Public Domain
+
+The inline XML literals in this code are part of E4X, a standard
+XML processing interface for ECMAScript. In addition to syntax
+for XML literals, E4X provides a new kind of native object,
+"xml", and a syntax, similar to XPath, for accessing and
+modifying the tree. Here is a brief synopsis of the kind of
+usage you'll see herein:
+
+> let xml =
+ <foo bar="baz" baz="qux">
+ <bar>
+ <baz id="1"/>
+ </bar>
+ <baz id="2"/>
+ </foo>;
+
+ // Select all bar elements of the root foo element
+> xml.bar
+ <bar><baz id="1"/></bar>
+
+ // Select all baz elements anywhere beneath the root
+> xml..baz
+ <baz id="1"/>
+ <baz id="2"/>
+
+ // Select all of the immediate children of the root
+> xml.*
+ <bar><baz id="1"/></bar>
+ <baz id="2"/>
+
+ // Select the bar attribute of the root node
+> xml.@bar
+ baz
+
+ // Select all id attributes in the tree
+> xml..@id
+ 1
+ 2
+
+ // Select all attributes of the root node
+> xml.@*
+ baz
+ quz
+
+// Add a quux elemend beneath the first baz
+> xml..baz[0] += <quux/>
+ <baz id="1"/>
+ <quux/>
+> xml
+ <foo bar="baz" baz="qux">
+ <bar>
+ <baz id="1"/>
+ <quux/>
+ </bar>
+ <baz id="2"/>
+ </foo>
+
+ // and beneath the second
+> xml.baz[1] = <quux id="1"/>
+> xml
+ <foo bar="baz" baz="qux">
+ <bar>
+ <baz id="1"/>
+ <quux/>
+ </bar>
+ <baz id="2"/>
+ <quux id="1"/>
+ </foo>
+
+ // Replace bar's subtree with a foo element
+> xml.bar.* = <foo id="1"/>
+> xml
+ <foo bar="baz" baz="qux">
+ <bar>
+ <foo id="1"/>
+ </bar>
+ <baz id="2"/>
+ <quux id="1"/>
+ </foo>
+
+ // Add a bar below bar
+> xml.bar.* += <bar id="1"/>
+ <foo id="1"/>
+ <bar id="1"/>
+> xml
+ <foo bar="baz" baz="qux">
+ <bar>
+ <foo id="1"/>
+ <bar id="1"/>
+ </bar>
+ <baz id="2"/>
+ <quux id="1"/>
+ </foo>
+
+ // Adding a quux attribute to the root
+> xml.@quux = "foo"
+ foo
+> xml
+ <foo bar="baz" baz="qux" quux="foo">
+ <bar>
+ <foo id="1"/>
+ <bar id="1"/>
+ </bar>
+ <baz id="2"/>
+ <quux id="1"/>
+ </foo>
+
+> xml.bar.@id = "0"
+> xml..foo[0] = "Foo"
+ Foo
+> xml..bar[1] = "Bar"
+ Bar
+> xml
+js> xml
+<foo bar="baz" baz="qux" quux="foo" id="0">
+ <bar id="0">
+ <foo id="1">Foo</foo>
+ <bar id="1">Bar</bar>
+ </bar>
+ <baz id="2"/>
+ <quux id="1"/>
+</foo>
+
+ // Selecting all bar elements where id="1"
+> xml..bar.(@id == 1)
+ Bar
+
+ // Literals:
+ // XMLList literal. No root node.
+> <>Foo<br/>Baz</>
+ Foo
+ <br/>
+ Baz
+
+// Interpolation.
+> let x = "<foo/>"
+> <foo bar={x}>{x + "<?>"}</foo>
+ <foo/><?>
+> <foo bar={x}>{x + "<?>"}</foo>.toXMLString()
+ <foo bar="&lt;foo/>">&lt;foo/&gt;&lt;?&gt;</foo>
+
+> let x = <foo/>
+> <foo bar={x}>{x}</foo>.toXMLString()
+ <foo bar="">
+ <foo/>
+ </foo>
+
diff --git a/common/content/bindings.xml b/common/content/bindings.xml
new file mode 100644
index 00000000..8d6474d0
--- /dev/null
+++ b/common/content/bindings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:liberator="http://vimperator.org/namespaces/liberator"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <binding id="frame">
+ <content>
+ <html:div liberator:highlight="FrameIndicator"/>
+ <children/>
+ </content>
+ </binding>
+ <binding id="compitem-td">
+ <!-- No white space. The table is white-space: pre; :( -->
+ <content><html:span class="td-strut"/><html:span class="td-span"><children/></html:span></content>
+ </binding>
+ <binding id="tab" display="xul:hbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tab">
+ <content chromedir="ltr" closetabtext="Close Tab">
+ <xul:stack class="liberator-tab-stack">
+ <xul:image xbl:inherits="validate,src=image" class="tab-icon-image" liberator:highlight="TabIcon"/>
+ <xul:vbox>
+ <xul:spring flex="1"/>
+ <xul:label xbl:inherits="value=ordinal" liberator:highlight="TabIconNumber"/>
+ <xul:spring flex="1"/>
+ </xul:vbox>
+ </xul:stack>
+ <xul:label xbl:inherits="value=ordinal" liberator:highlight="TabNumber"/>
+ <xul:label flex="1" xbl:inherits="value=label,crop,accesskey" class="tab-text" liberator:highlight="TabText"/>
+ <xul:toolbarbutton anonid="close-button" tabindex="-1" class="tab-close-button" liberator:highlight="TabClose"/>
+ </content>
+ </binding>
+</bindings>
+
+<!-- vim:se ft=xbl sw=4 sts=4 tw=0 et: -->
diff --git a/common/content/buffer.js b/common/content/buffer.js
new file mode 100644
index 00000000..4d03b6ce
--- /dev/null
+++ b/common/content/buffer.js
@@ -0,0 +1,1708 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+const Point = new Struct("x", "y");
+
+function Buffer() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+ /* FIXME: This doesn't belong here. */
+ let mainWindowID = config.mainWindowID || "main-window";
+ let fontSize = util.computedStyle(document.getElementById(mainWindowID)).fontSize;
+
+ styles.registerSheet("chrome://liberator/skin/liberator.css");
+ let error = styles.addSheet("font-size", "chrome://liberator/content/buffer.xhtml",
+ "body { font-size: " + fontSize + "; }", true);
+
+ if ("ZoomManager" in window)
+ {
+ const ZOOM_MIN = Math.round(ZoomManager.MIN * 100);
+ const ZOOM_MAX = Math.round(ZoomManager.MAX * 100);
+ }
+
+ function setZoom(value, fullZoom)
+ {
+ if (value < ZOOM_MIN || value > ZOOM_MAX)
+ {
+ liberator.echoerr("Zoom value out of range (" + ZOOM_MIN + " - " + ZOOM_MAX + "%)");
+ return;
+ }
+
+ ZoomManager.useFullZoom = fullZoom;
+ ZoomManager.zoom = value / 100;
+ if ("FullZoom" in window)
+ FullZoom._applySettingToPref();
+ liberator.echo((fullZoom ? "Full" : "Text") + " zoom: " + value + "%");
+ }
+
+ function bumpZoomLevel(steps, fullZoom)
+ {
+ let values = ZoomManager.zoomValues;
+ let i = values.indexOf(ZoomManager.snap(ZoomManager.zoom)) + steps;
+
+ if (i >= 0 && i < values.length)
+ setZoom(Math.round(values[i] * 100), fullZoom);
+ // TODO: I'll leave the behaviour as is for now, but I think this
+ // should probably just take you to the respective bounds -- djk
+ else
+ liberator.beep();
+ }
+
+ function checkScrollYBounds(win, direction)
+ {
+ // NOTE: it's possible to have scrollY > scrollMaxY - FF bug?
+ if (direction > 0 && win.scrollY >= win.scrollMaxY || direction < 0 && win.scrollY == 0)
+ liberator.beep();
+ }
+
+ function findScrollableWindow()
+ {
+ var win = window.document.commandDispatcher.focusedWindow;
+ if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
+ return win;
+
+ win = window.content;
+ if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
+ return win;
+
+ for (let frame in util.Array.iterator(win.frames))
+ if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
+ return frame;
+
+ return win;
+ }
+
+ // both values are given in percent, -1 means no change
+ function scrollToPercentiles(horizontal, vertical)
+ {
+ var win = findScrollableWindow();
+ var h, v;
+
+ if (horizontal < 0)
+ h = win.scrollX;
+ else
+ h = win.scrollMaxX / 100 * horizontal;
+
+ if (vertical < 0)
+ v = win.scrollY;
+ else
+ v = win.scrollMaxY / 100 * vertical;
+
+ win.scrollTo(h, v);
+ }
+
+ // Holds option: [function, title] to generate :pageinfo sections
+ var pageInfo = {};
+ function addPageInfoSection(option, title, fn)
+ {
+ pageInfo[option] = [fn, title];
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["fullscreen", "fs"],
+ "Show the current window fullscreen",
+ "boolean", false,
+ {
+ setter: function (value) window.fullScreen = value,
+ getter: function () window.fullScreen
+ });
+
+ options.add(["nextpattern"],
+ "Patterns to use when guessing the 'next' page in a document sequence",
+ "stringlist", "\\bnext\\b,^>$,^(>>|»)$,^(>|»),(>|»)$,\\bmore\\b");
+
+ options.add(["previouspattern"],
+ "Patterns to use when guessing the 'previous' page in a document sequence",
+ "stringlist", "\\bprev|previous\\b,^<$,^(<<|«)$,^(<|«),(<|«)$");
+
+ options.add(["pageinfo", "pa"], "Desired info on :pa[geinfo]", "charlist", "gfm",
+ {
+ completer: function (filter) [[k, v[1]] for ([k, v] in Iterator(pageInfo))],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["scroll", "scr"],
+ "Number of lines to scroll with <C-u> and <C-d> commands",
+ "number", 0,
+ { validator: function (value) value >= 0 });
+
+ options.add(["showstatuslinks", "ssli"],
+ "Show the destination of the link under the cursor in the status bar",
+ "number", 1,
+ {
+ completer: function (filter) [
+ ["0", "Don't show link destination"],
+ ["1", "Show the link in the status line"],
+ ["2", "Show the link in the command line"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["usermode", "um"],
+ "Show current website with a minimal style sheet to make it easily accessible",
+ "boolean", false,
+ {
+ setter: function (value)
+ {
+ try
+ {
+ window.getMarkupDocumentViewer().authorStyleDisabled = value;
+ }
+ catch (e) {}
+
+ return value;
+ },
+ getter: function ()
+ {
+ try
+ {
+ return window.getMarkupDocumentViewer().authorStyleDisabled;
+ }
+ catch (e) {}
+ }
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var myModes = config.browserModes;
+
+ mappings.add(myModes, ["."],
+ "Repeat the last key event",
+ function (count)
+ {
+ if (mappings.repeat)
+ {
+ for (let i in util.interruptableRange(0, Math.max(count, 1), 100))
+ mappings.repeat();
+ }
+ },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["i", "<Insert>"],
+ "Start caret mode",
+ function ()
+ {
+ // setting this option triggers an observer which takes care of the mode setting
+ options.setPref("accessibility.browsewithcaret", true);
+ });
+
+ mappings.add(myModes, ["<C-c>"],
+ "Stop loading",
+ function () { window.BrowserStop(); });
+
+ // scrolling
+ mappings.add(myModes, ["j", "<Down>", "<C-e>"],
+ "Scroll document down",
+ function (count) { buffer.scrollLines(count > 1 ? count : 1); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["k", "<Up>", "<C-y>"],
+ "Scroll document up",
+ function (count) { buffer.scrollLines(-(count > 1 ? count : 1)); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, liberator.has("mail") ? ["h"] : ["h", "<Left>"],
+ "Scroll document to the left",
+ function (count) { buffer.scrollColumns(-(count > 1 ? count : 1)); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, liberator.has("mail") ? ["l"] : ["l", "<Right>"],
+ "Scroll document to the right",
+ function (count) { buffer.scrollColumns(count > 1 ? count : 1); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["0", "^"],
+ "Scroll to the absolute left of the document",
+ function () { buffer.scrollStart(); });
+
+ mappings.add(myModes, ["$"],
+ "Scroll to the absolute right of the document",
+ function () { buffer.scrollEnd(); });
+
+ mappings.add(myModes, ["gg", "<Home>"],
+ "Go to the top of the document",
+ function (count) { buffer.scrollToPercentile(count > 0 ? count : 0); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["G", "<End>"],
+ "Go to the end of the document",
+ function (count) { buffer.scrollToPercentile(count >= 0 ? count : 100); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["%"],
+ "Scroll to {count} percent of the document",
+ function (count)
+ {
+ if (count > 0 && count <= 100)
+ buffer.scrollToPercentile(count);
+ else
+ liberator.beep();
+ },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["<C-d>"],
+ "Scroll window downwards in the buffer",
+ function (count) { buffer.scrollByScrollSize(count, 1); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["<C-u>"],
+ "Scroll window upwards in the buffer",
+ function (count) { buffer.scrollByScrollSize(count, -1); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["<C-b>", "<PageUp>", "<S-Space>"],
+ "Scroll up a full page",
+ function (count) { buffer.scrollPages(-(count > 1 ? count : 1)); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["<C-f>", "<PageDown>", "<Space>"],
+ "Scroll down a full page",
+ function (count) { buffer.scrollPages(count > 1 ? count : 1); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["]f"],
+ "Focus next frame",
+ function (count) { buffer.shiftFrameFocus(count > 1 ? count : 1, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["[f"],
+ "Focus previous frame",
+ function (count) { buffer.shiftFrameFocus(count > 1 ? count : 1, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["]]"],
+ "Follow the link labeled 'next' or '>' if it exists",
+ function (count) { buffer.followDocumentRelationship("next"); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["[["],
+ "Follow the link labeled 'prev', 'previous' or '<' if it exists",
+ function (count) { buffer.followDocumentRelationship("previous"); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["gf"],
+ "View source",
+ function () { buffer.viewSource(null, false); });
+
+ mappings.add(myModes, ["gF"],
+ "View source with an external editor",
+ function () { buffer.viewSource(null, true); });
+
+ mappings.add(myModes, ["gi"],
+ "Focus last used input field",
+ function (count)
+ {
+ if (count < 1 && buffer.lastInputField)
+ {
+ buffer.lastInputField.focus();
+ }
+ else
+ {
+ var elements = [];
+ var matches = buffer.evaluateXPath(
+ // TODO: type="file"
+ "//input[not(@type) or @type='text' or @type='password'] | //textarea[not(@disabled) and not(@readonly)] |" +
+ "//xhtml:input[not(@type) or @type='text' or @type='password'] | //xhtml:textarea[not(@disabled) and not(@readonly)]"
+ );
+
+ for (match in matches)
+ {
+ let computedStyle = util.computedStyle(match);
+ if (computedStyle.visibility != "hidden" && computedStyle.display != "none")
+ elements.push(match);
+ }
+
+ if (elements.length > 0)
+ {
+ if (count > elements.length)
+ count = elements.length;
+ else if (count < 1)
+ count = 1;
+
+ elements[count - 1].focus();
+ }
+ else
+ {
+ liberator.beep();
+ }
+ }
+ },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["gP"],
+ "Open (put) a URL based on the current clipboard contents in a new buffer",
+ function ()
+ {
+ liberator.open(util.readFromClipboard(),
+ /\bpaste\b/.test(options["activate"]) ?
+ liberator.NEW_BACKGROUND_TAB : liberator.NEW_TAB);
+ });
+
+ mappings.add(myModes, ["p", "<MiddleMouse>"],
+ "Open (put) a URL based on the current clipboard contents in the current buffer",
+ function () { liberator.open(util.readFromClipboard()); });
+
+ mappings.add(myModes, ["P"],
+ "Open (put) a URL based on the current clipboard contents in a new buffer",
+ function ()
+ {
+ liberator.open(util.readFromClipboard(),
+ /\bpaste\b/.test(options["activate"]) ?
+ liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB);
+ });
+
+ // reloading
+ mappings.add(myModes, ["r"],
+ "Reload current page",
+ function () { tabs.reload(getBrowser().mCurrentTab, false); });
+
+ mappings.add(myModes, ["R"],
+ "Reload while skipping the cache",
+ function () { tabs.reload(getBrowser().mCurrentTab, true); });
+
+ // yanking
+ mappings.add(myModes, ["Y"],
+ "Copy selected text or current word",
+ function ()
+ {
+ var sel = buffer.getCurrentWord();
+ if (sel)
+ util.copyToClipboard(sel, true);
+ else
+ liberator.beep();
+ });
+
+ // zooming
+ mappings.add(myModes, ["zi", "+"],
+ "Enlarge text zoom of current web page",
+ function (count) { buffer.zoomIn(count > 1 ? count : 1, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zm"],
+ "Enlarge text zoom of current web page by a larger amount",
+ function (count) { buffer.zoomIn((count > 1 ? count : 1) * 3, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zo", "-"],
+ "Reduce text zoom of current web page",
+ function (count) { buffer.zoomOut(count > 1 ? count : 1, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zr"],
+ "Reduce text zoom of current web page by a larger amount",
+ function (count) { buffer.zoomOut((count > 1 ? count : 1) * 3, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zz"],
+ "Set text zoom value of current web page",
+ function (count) { buffer.textZoom = count > 1 ? count : 100; },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zI"],
+ "Enlarge full zoom of current web page",
+ function (count) { buffer.zoomIn(count > 1 ? count : 1, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zM"],
+ "Enlarge full zoom of current web page by a larger amount",
+ function (count) { buffer.zoomIn((count > 1 ? count : 1) * 3, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zO"],
+ "Reduce full zoom of current web page",
+ function (count) { buffer.zoomOut(count > 1 ? count : 1, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zR"],
+ "Reduce full zoom of current web page by a larger amount",
+ function (count) { buffer.zoomOut((count > 1 ? count : 1) * 3, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["zZ"],
+ "Set full zoom value of current web page",
+ function (count) { buffer.fullZoom = count > 1 ? count : 100; },
+ { flags: Mappings.flags.COUNT });
+
+ // page info
+ mappings.add(myModes, ["<C-g>"],
+ "Print the current file name",
+ function (count) { buffer.showPageInfo(false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add(myModes, ["g<C-g>"],
+ "Print file information",
+ function () { buffer.showPageInfo(true); });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["ha[rdcopy]"],
+ "Print current document",
+ function (args)
+ {
+ var aps = options.getPref("print.always_print_silent");
+ var spp = options.getPref("print.show_print_progress");
+
+ liberator.echo("Sending to printer...");
+ options.setPref("print.always_print_silent", args.bang);
+ options.setPref("print.show_print_progress", !args.bang);
+
+ getBrowser().contentWindow.print();
+
+ options.setPref("print.always_print_silent", aps);
+ options.setPref("print.show_print_progress", spp);
+ liberator.echo("Print job sent.");
+ },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["pa[geinfo]"],
+ "Show various page information",
+ function (args) { buffer.showPageInfo(true, args[0]); },
+ {
+ argCount: "?",
+ completer: function (context)
+ {
+ completion.optionValue(context, "pageinfo", "+", "");
+ context.title = ["Page Info"];
+ }
+ });
+
+ commands.add(["pagest[yle]"],
+ "Select the author style sheet to apply",
+ function (args)
+ {
+ args = args.string;
+
+ var titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
+
+ if (args && titles.indexOf(args) == -1)
+ {
+ liberator.echoerr("E475: Invalid argument: " + args);
+ return;
+ }
+
+ if (options["usermode"])
+ options["usermode"] = false;
+
+ window.stylesheetSwitchAll(window.content, args);
+ },
+ {
+ argCount: "?",
+ completer: function (context) completion.alternateStylesheet(context),
+ literal: 0
+ });
+
+ commands.add(["re[load]"],
+ "Reload current page",
+ function (args) { tabs.reload(getBrowser().mCurrentTab, args.bang); },
+ {
+ bang: true,
+ argCount: "0"
+ });
+
+ // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
+ commands.add(["sav[eas]", "w[rite]"],
+ "Save current document to disk",
+ function (args)
+ {
+ let doc = window.content.document;
+ let chosenData = null;
+ let filename = args[0];
+
+ if (filename)
+ {
+ let file = io.getFile(filename);
+
+ if (file.exists() && !args.bang)
+ {
+ liberator.echoerr("E13: File exists (add ! to override)");
+ return;
+ }
+
+ chosenData = { file: file, uri: makeURI(doc.location.href, doc.characterSet) };
+ }
+
+ // if browser.download.useDownloadDir = false then the "Save As"
+ // dialog is used with this as the default directory
+ // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
+ options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
+
+ try
+ {
+ var contentDisposition = window.content
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .getDocumentMetadata("content-disposition");
+ } catch (e) {}
+
+ window.internalSave(doc.location.href, doc, null, contentDisposition,
+ doc.contentType, false, null, chosenData, doc.referrer ?
+ window.makeURI(doc.referrer) : null, true);
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.file(context)
+ });
+
+ commands.add(["st[op]"],
+ "Stop loading",
+ function () { window.BrowserStop(); },
+ { argCount: "0" });
+
+ commands.add(["vie[wsource]"],
+ "View source code of current document",
+ function (args) { buffer.viewSource(args[0], args.bang); },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.url(context, "bhf")
+ });
+
+ commands.add(["zo[om]"],
+ "Set zoom value of current web page",
+ function (args)
+ {
+ let level;
+
+ if (!args.string)
+ {
+ level = 100;
+ }
+ else if (/^\d+$/.test(args.string))
+ {
+ level = parseInt(args.string, 10);
+ }
+ else if (/^[+-]\d+$/.test(args.string))
+ {
+ if (args.bang)
+ level = buffer.fullZoom + parseInt(args.string, 10);
+ else
+ level = buffer.textZoom + parseInt(args.string, 10);
+
+ // relative args shouldn't take us out of range
+ if (level < ZOOM_MIN)
+ level = ZOOM_MIN;
+ if (level > ZOOM_MAX)
+ level = ZOOM_MAX;
+ }
+ else
+ {
+ liberator.echoerr("E488: Trailing characters");
+ return;
+ }
+
+ if (args.bang)
+ buffer.fullZoom = level;
+ else
+ buffer.textZoom = level;
+ },
+ { bang: true });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PAGE INFO ///////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ addPageInfoSection("f", "Feeds", function (verbose)
+ {
+ var doc = window.content.document;
+
+ const feedTypes = {
+ "application/rss+xml": "RSS",
+ "application/atom+xml": "Atom",
+ "text/xml": "XML",
+ "application/xml": "XML",
+ "application/rdf+xml": "XML"
+ };
+
+ function isValidFeed(data, principal, isFeed)
+ {
+ if (!data || !principal)
+ return false;
+
+ if (!isFeed)
+ {
+ var type = data.type && data.type.toLowerCase();
+ type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+ isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
+ if (!isFeed)
+ {
+ // really slimy: general XML types with magic letters in the title
+ const titleRegex = /(^|\s)rss($|\s)/i;
+ isFeed = ((type == "text/xml" || type == "application/rdf+xml" || type == "application/xml")
+ && titleRegex.test(data.title));
+ }
+ }
+
+ if (isFeed)
+ {
+ try
+ {
+ window.urlSecurityCheck(data.href, principal,
+ Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+ }
+ catch (e)
+ {
+ isFeed = false;
+ }
+ }
+
+ if (type)
+ data.type = type;
+
+ return isFeed;
+ }
+
+ // put feeds rss into pageFeeds[]
+ let nFeed = 0;
+ var linkNodes = doc.getElementsByTagName("link");
+ for (link in util.Array.iterator(linkNodes))
+ {
+ if (!link.href)
+ return;
+
+ var rel = link.rel && link.rel.toLowerCase();
+
+ if (rel == "feed" || (link.type && rel == "alternate"))
+ {
+ var feed = { title: link.title, href: link.href, type: link.type || "" };
+ if (isValidFeed(feed, doc.nodePrincipal, rel == "feed"))
+ {
+ nFeed++;
+ var type = feedTypes[feed.type] || feedTypes["application/rss+xml"];
+ if (verbose)
+ yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
+ }
+ }
+ }
+
+ if (!verbose && nFeed)
+ yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
+ });
+
+ addPageInfoSection("g", "General Info", function (verbose)
+ {
+ let doc = window.content.document;
+
+ // get file size
+ const nsICacheService = Components.interfaces.nsICacheService;
+ const ACCESS_READ = Components.interfaces.nsICache.ACCESS_READ;
+ const cacheService = Components.classes["@mozilla.org/network/cache-service;1"]
+ .getService(nsICacheService);
+ let cacheKey = doc.location.toString().replace(/#.*$/, "");
+
+ for (let proto in util.Array.iterator(["HTTP", "FTP"]))
+ {
+ try
+ {
+ var cacheEntryDescriptor = cacheService.createSession(proto, 0, true)
+ .openCacheEntry(cacheKey, ACCESS_READ, false);
+ break;
+ }
+ catch (e) {}
+ }
+
+ var pageSize = []; // [0] bytes; [1] kbytes
+ if (cacheEntryDescriptor)
+ {
+ pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
+ pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
+ if (pageSize[1] == pageSize[0])
+ pageSize.length = 1; // don't output "xx Bytes" twice
+ }
+
+ var lastModVerbose = new Date(doc.lastModified).toLocaleString();
+ var lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
+ // FIXME: probably not portable across different language versions
+ if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
+ lastModVerbose = lastMod = null;
+
+ if (!verbose)
+ {
+ if (pageSize[0])
+ yield (pageSize[1] || pageSize[0]) + " bytes";
+ yield lastMod;
+ return;
+ }
+
+ yield ["Title", doc.title];
+ yield ["URL", template.highlightURL(doc.location.toString(), true)];
+
+ var ref = "referrer" in doc && doc.referrer;
+ if (ref)
+ yield ["Referrer", template.highlightURL(ref, true)];
+
+ if (pageSize[0])
+ yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
+ : pageSize[0]];
+
+ yield ["Mime-Type", doc.contentType];
+ yield ["Encoding", doc.characterSet];
+ yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
+ if (lastModVerbose)
+ yield ["Last Modified", lastModVerbose];
+ });
+
+ addPageInfoSection("m", "Meta Tags", function (verbose)
+ {
+ // get meta tag data, sort and put into pageMeta[]
+ var metaNodes = window.content.document.getElementsByTagName("meta");
+
+ return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
+ .sort(function (a, b) String.localeCompare(a[0].toLowerCase(), b[0].toLowerCase()));
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ get alternateStyleSheets()
+ {
+ var stylesheets = window.getAllStyleSheets(window.content);
+
+ return stylesheets.filter(
+ function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
+ );
+ },
+
+ get pageInfo() pageInfo,
+
+ // 0 if loading, 1 if loaded or 2 if load failed
+ get loaded()
+ {
+ if (window.content.document.pageIsFullyLoaded !== undefined)
+ return window.content.document.pageIsFullyLoaded;
+ else
+ return 0; // in doubt return "loading"
+ },
+ set loaded(value)
+ {
+ window.content.document.pageIsFullyLoaded = value;
+ },
+
+ // used to keep track of the right field for "gi"
+ get lastInputField()
+ {
+ if (window.content.document.lastInputField)
+ return window.content.document.lastInputField;
+ else
+ return null;
+ },
+ set lastInputField(value)
+ {
+ window.content.document.lastInputField = value;
+ },
+
+ get URL()
+ {
+ return window.content.document.location.href;
+ },
+
+ get pageHeight()
+ {
+ return window.content.innerHeight;
+ },
+
+ get textZoom()
+ {
+ return getBrowser().markupDocumentViewer.textZoom * 100;
+ },
+ set textZoom(value)
+ {
+ setZoom(value, false);
+ },
+
+ get fullZoom()
+ {
+ return getBrowser().markupDocumentViewer.fullZoom * 100;
+ },
+ set fullZoom(value)
+ {
+ setZoom(value, true);
+ },
+
+ get title()
+ {
+ return window.content.document.title;
+ },
+
+ addPageInfoSection: addPageInfoSection,
+
+ // returns an XPathResult object
+ evaluateXPath: function (expression, doc, elem, asIterator)
+ {
+ if (!doc)
+ doc = window.content.document;
+ if (!elem)
+ elem = doc;
+
+ var result = doc.evaluate(expression, elem,
+ function lookupNamespaceURI(prefix)
+ {
+ switch (prefix)
+ {
+ case "xhtml":
+ return "http://www.w3.org/1999/xhtml";
+ case "liberator":
+ return NS.uri;
+ default:
+ return null;
+ }
+ },
+ asIterator ? XPathResult.UNORDERED_NODE_ITERATOR_TYPE : XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+
+ result.__iterator__ = asIterator
+ ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
+ : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); };
+
+ return result;
+ },
+
+ // in contrast to vim, returns the selection if one is made,
+ // otherwise tries to guess the current word under the text cursor
+ // NOTE: might change the selection
+ // FIXME: getSelection() doesn't always preserve line endings, see:
+ // https://www.mozdev.org/bugs/show_bug.cgi?id=19303
+ getCurrentWord: function ()
+ {
+ let selection = window.content.getSelection();
+ if (selection.isCollapsed)
+ {
+ let selController = this.selectionController;
+ let caretmode = selController.getCaretEnabled();
+ selController.setCaretEnabled(true);
+ selController.wordMove(false, false);
+ selController.wordMove(true, true);
+ selController.setCaretEnabled(caretmode);
+ }
+ let range = selection.getRangeAt(0);
+ if (util.computedStyle(range.startContainer).whiteSpace == "pre"
+ && util.computedStyle(range.endContainer).whiteSpace == "pre")
+ return String(range);
+ return String(selection);
+ },
+
+ // more advanced than a simple elem.focus() as it also works for iframes
+ // and image maps
+ // TODO: merge with followLink()?
+ focusElement: function (elem)
+ {
+ var doc = window.content.document;
+ var elemTagName = elem.localName.toLowerCase();
+ if (elemTagName == "frame" || elemTagName == "iframe")
+ {
+ elem.contentWindow.focus();
+ return false;
+ }
+ else
+ {
+ elem.focus();
+ }
+
+ var evt = doc.createEvent("MouseEvents");
+ var x = 0;
+ var y = 0;
+ // for imagemap
+ if (elemTagName == "area")
+ {
+ var coords = elem.getAttribute("coords").split(",");
+ x = Number(coords[0]);
+ y = Number(coords[1]);
+ }
+
+ evt.initMouseEvent("mouseover", true, true, doc.defaultView, 1, x, y, 0, 0, 0, 0, 0, 0, 0, null);
+ elem.dispatchEvent(evt);
+ },
+
+ followDocumentRelationship: function (relationship)
+ {
+ function followFrameRelationship(relationship, parsedFrame)
+ {
+ var regexps;
+ var relText;
+ var patternText;
+ var revString;
+ switch (relationship)
+ {
+ case "next":
+ regexps = options.get("nextpattern").values;
+ revString = "previous";
+ break;
+ case "previous":
+ // TODO: accept prev\%[ious]
+ regexps = options.get("previouspattern").values;
+ revString = "next";
+ break;
+ default:
+ liberator.echoerr("Bad document relationship: " + relationship);
+ }
+
+ relText = new RegExp(relationship, "i");
+ revText = new RegExp(revString, "i");
+ var elems = parsedFrame.document.getElementsByTagName("link");
+ // links have higher priority than normal <a> hrefs
+ for (let i = 0; i < elems.length; i++)
+ {
+ if (relText.test(elems[i].rel) || revText.test(elems[i].rev))
+ {
+ liberator.open(elems[i].href);
+ return true;
+ }
+ }
+
+ // no links? ok, look for hrefs
+ elems = parsedFrame.document.getElementsByTagName("a");
+ for (let i = 0; i < elems.length; i++)
+ {
+ if (relText.test(elems[i].rel) || revText.test(elems[i].rev))
+ {
+ buffer.followLink(elems[i], liberator.CURRENT_TAB);
+ return true;
+ }
+ }
+
+ for (let pattern = 0; pattern < regexps.length; pattern++)
+ {
+ patternText = new RegExp(regexps[pattern], "i");
+ for (let i = 0; i < elems.length; i++)
+ {
+ if (patternText.test(elems[i].textContent))
+ {
+ buffer.followLink(elems[i], liberator.CURRENT_TAB);
+ return true;
+ }
+ else
+ {
+ // images with alt text being href
+ var children = elems[i].childNodes;
+ for (let j = 0; j < children.length; j++)
+ {
+ if (patternText.test(children[j].alt))
+ {
+ buffer.followLink(elems[i], liberator.CURRENT_TAB);
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ var retVal;
+ if (window.content.frames.length != 0)
+ {
+ retVal = followFrameRelationship(relationship, window.content);
+ if (!retVal)
+ {
+ // only loop through frames if the main content didnt match
+ for (let i = 0; i < window.content.frames.length; i++)
+ {
+ retVal = followFrameRelationship(relationship, window.content.frames[i]);
+ if (retVal)
+ break;
+ }
+ }
+ }
+ else
+ {
+ retVal = followFrameRelationship(relationship, window.content);
+ }
+
+ if (!retVal)
+ liberator.beep();
+ },
+
+ // artificially "clicks" a link in order to open it
+ followLink: function (elem, where)
+ {
+ var doc = elem.ownerDocument;
+ var view = doc.defaultView;
+ var offsetX = 1;
+ var offsetY = 1;
+
+ var localName = elem.localName.toLowerCase();
+ if (localName == "frame" || localName == "iframe") // broken?
+ {
+ elem.contentWindow.focus();
+ return false;
+ }
+ else if (localName == "area") // for imagemap
+ {
+ var coords = elem.getAttribute("coords").split(",");
+ offsetX = Number(coords[0]) + 1;
+ offsetY = Number(coords[1]) + 1;
+ }
+
+ var ctrlKey = false, shiftKey = false;
+ switch (where)
+ {
+ case liberator.NEW_TAB:
+ case liberator.NEW_BACKGROUND_TAB:
+ ctrlKey = true;
+ shiftKey = (where == liberator.NEW_BACKGROUND_TAB);
+ break;
+ case liberator.NEW_WINDOW:
+ shiftKey = true;
+ break;
+ case liberator.CURRENT_TAB:
+ break;
+ default:
+ liberator.log("Invalid where argument for followLink()", 0);
+ }
+
+ elem.focus();
+
+ var evt = doc.createEvent("MouseEvents");
+ ["mousedown", "mouseup", "click"].forEach(function (event) {
+ evt.initMouseEvent(event, true, true, view, 1, offsetX, offsetY, 0, 0,
+ ctrlKey, /*altKey*/0, shiftKey, /*metaKey*/ ctrlKey, 0, null);
+ elem.dispatchEvent(evt);
+ });
+ },
+
+ get selectionController() getBrowser().docShell
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsISelectionDisplay)
+ .QueryInterface(Components.interfaces.nsISelectionController),
+
+ saveLink: function (elem, skipPrompt)
+ {
+ var doc = elem.ownerDocument;
+ var url = window.makeURLAbsolute(elem.baseURI, elem.href);
+ var text = elem.textContent;
+
+ try
+ {
+ window.urlSecurityCheck(url, doc.nodePrincipal);
+ // we always want to save that link relative to the current working directory
+ options.setPref("browser.download.lastDir", io.getCurrentDirectory().path);
+ window.saveURL(url, text, null, true, skipPrompt, makeURI(url, doc.characterSet));
+ }
+ catch (e)
+ {
+ liberator.echoerr(e);
+ }
+ },
+
+ scrollBottom: function ()
+ {
+ scrollToPercentiles(-1, 100);
+ },
+
+ scrollColumns: function (cols)
+ {
+ var win = findScrollableWindow();
+ const COL_WIDTH = 20;
+
+ if (cols > 0 && win.scrollX >= win.scrollMaxX || cols < 0 && win.scrollX == 0)
+ liberator.beep();
+
+ win.scrollBy(COL_WIDTH * cols, 0);
+ },
+
+ scrollEnd: function ()
+ {
+ scrollToPercentiles(100, -1);
+ },
+
+ scrollLines: function (lines)
+ {
+ var win = findScrollableWindow();
+ checkScrollYBounds(win, lines);
+ win.scrollByLines(lines);
+ },
+
+ scrollPages: function (pages)
+ {
+ var win = findScrollableWindow();
+ checkScrollYBounds(win, pages);
+ win.scrollByPages(pages);
+ },
+
+ scrollByScrollSize: function (count, direction)
+ {
+ if (count > 0)
+ options["scroll"] = count;
+
+ var win = findScrollableWindow();
+ checkScrollYBounds(win, direction);
+
+ if (options["scroll"] > 0)
+ this.scrollLines(options["scroll"] * direction);
+ else // scroll half a page down in pixels
+ win.scrollBy(0, win.innerHeight / 2 * direction);
+ },
+
+ scrollToPercentile: function (percentage)
+ {
+ scrollToPercentiles(-1, percentage);
+ },
+
+ scrollStart: function ()
+ {
+ scrollToPercentiles(0, -1);
+ },
+
+ scrollTop: function ()
+ {
+ scrollToPercentiles(-1, 0);
+ },
+
+ // TODO: allow callback for filtering out unwanted frames? User defined?
+ shiftFrameFocus: function (count, forward)
+ {
+ if (!window.content.document instanceof HTMLDocument)
+ return;
+
+ count = Math.max(count, 1);
+ var frames = [];
+
+ // find all frames - depth-first search
+ (function (frame) {
+ if (frame.document.body.localName.toLowerCase() == "body")
+ frames.push(frame);
+ Array.forEach(frame.frames, arguments.callee);
+ })(window.content);
+
+ if (frames.length == 0) // currently top is always included
+ return;
+
+ // remove all unfocusable frames
+ // TODO: find a better way to do this - walking the tree is too slow
+ var start = document.commandDispatcher.focusedWindow;
+ frames = frames.filter(function (frame) {
+ frame.focus();
+ return document.commandDispatcher.focusedWindow == frame;
+ });
+ start.focus();
+
+ // find the currently focused frame index
+ // TODO: If the window is a frameset then the first _frame_ should be
+ // focused. Since this is not the current FF behaviour,
+ // we initalize current to -1 so the first call takes us to the
+ // first frame.
+ var current = frames.indexOf(document.commandDispatcher.focusedWindow);
+
+ // calculate the next frame to focus
+ var next = current;
+ if (forward)
+ {
+ next = current + count;
+
+ if (next > frames.length - 1)
+ {
+ if (current == frames.length - 1)
+ liberator.beep();
+ next = frames.length - 1; // still allow the frame indicator to be activated
+ }
+ }
+ else
+ {
+ next = current - count;
+
+ if (next < 0)
+ {
+ if (current == 0)
+ liberator.beep();
+ next = 0; // still allow the frame indicator to be activated
+ }
+ }
+
+ // focus next frame and scroll into view
+ frames[next].focus();
+ if (frames[next] != window.content)
+ frames[next].frameElement.scrollIntoView(false);
+
+ // add the frame indicator
+ let doc = frames[next].document;
+ var indicator = util.xmlToDom(<div highlight="FrameIndicator"/>, doc);
+ doc.body.appendChild(indicator);
+
+ setTimeout(function () { doc.body.removeChild(indicator); }, 500);
+
+ // Doesn't unattach
+ //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
+ //setTimeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
+ },
+
+ // similar to pageInfo
+ // TODO: print more useful information, just like the DOM inspector
+ showElementInfo: function (elem)
+ {
+ liberator.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
+ },
+
+ showPageInfo: function (verbose, sections)
+ {
+ // Ctrl-g single line output
+ if (!verbose)
+ {
+ let file = content.document.location.pathname.split("/").pop() || "[No Name]";
+ let title = content.document.title || "[No Title]";
+
+ let info = template.map("gf", function (opt)
+ template.map(pageInfo[opt][0](), util.identity, ", "),
+ ", ");
+
+ if (bookmarks.isBookmarked(this.URL))
+ info += ", bookmarked";
+
+ var pageInfoText = <>"{file}" [{info}] {title}</>;
+ liberator.echo(pageInfoText, commandline.FORCE_SINGLELINE);
+ return;
+ }
+
+ let option = sections || options["pageinfo"];
+ let list = template.map(option, function (option) {
+ let opt = pageInfo[option];
+ if (opt)
+ return template.table(opt[1], opt[0](true));
+ }, <br/>);
+ liberator.echo(list, commandline.FORCE_MULTILINE);
+ },
+
+ viewSelectionSource: function ()
+ {
+ // copied (and tuned somebit) from browser.jar -> nsContextMenu.js
+ var focusedWindow = document.commandDispatcher.focusedWindow;
+ if (focusedWindow == window)
+ focusedWindow = content;
+
+ var docCharset = null;
+ if (focusedWindow)
+ docCharset = "charset=" + focusedWindow.document.characterSet;
+
+ var reference = null;
+ reference = focusedWindow.getSelection();
+
+ var docUrl = null;
+ window.openDialog("chrome://global/content/viewPartialSource.xul",
+ "_blank", "scrollbars,resizable,chrome,dialog=no",
+ docUrl, docCharset, reference, "selection");
+ },
+
+ viewSource: function (url, useExternalEditor)
+ {
+ url = url || buffer.URL;
+
+ if (useExternalEditor)
+ editor.editFileExternally(url);
+ else
+ liberator.open("view-source:" + url);
+ },
+
+ zoomIn: function (steps, fullZoom)
+ {
+ bumpZoomLevel(steps, fullZoom);
+ },
+
+ zoomOut: function (steps, fullZoom)
+ {
+ bumpZoomLevel(-steps, fullZoom);
+ }
+ };
+ //}}}
+}; //}}}
+
+function Marks() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var localMarks = storage.newMap('local-marks', true);
+ var urlMarks = storage.newMap('url-marks', true);
+
+ var pendingJumps = [];
+ var appContent = document.getElementById("appcontent");
+
+ if (appContent)
+ appContent.addEventListener("load", onPageLoad, true);
+
+ function onPageLoad(event)
+ {
+ var win = event.originalTarget.defaultView;
+ for (let i = 0, length = pendingJumps.length; i < length; i++)
+ {
+ var mark = pendingJumps[i];
+ if (win && win.location.href == mark.location)
+ {
+ win.scrollTo(mark.position.x * win.scrollMaxX, mark.position.y * win.scrollMaxY);
+ pendingJumps.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ function markToString(name, mark)
+ {
+ return name + ", " + mark.location +
+ ", (" + Math.round(mark.position.x * 100) +
+ "%, " + Math.round(mark.position.y * 100) + "%)" +
+ (('tab' in mark) ? ", tab: " + tabs.index(mark.tab) : "");
+ }
+
+ function removeLocalMark(mark)
+ {
+ var localmark = localMarks.get(mark);
+ if (localmark)
+ {
+ var win = window.content;
+ for (let [i,] in Iterator(localmark))
+ {
+ if (localmark[i].location == win.location.href)
+ {
+ liberator.log("Deleting local mark: " + markToString(mark, localmark[i]), 5);
+ localmark.splice(i, 1);
+ if (localmark.length == 0)
+ localMarks.remove(mark);
+ break;
+ }
+ }
+ }
+ }
+
+ function removeURLMark(mark)
+ {
+ var urlmark = urlMarks.get(mark);
+ if (urlmark)
+ {
+ liberator.log("Deleting URL mark: " + markToString(mark, urlmark), 5);
+ urlMarks.remove(mark);
+ }
+ }
+
+ function isLocalMark(mark) /^[a-z]$/.test(mark);
+ function isURLMark(mark) /^[A-Z0-9]$/.test(mark);
+
+ function localMarkIter()
+ {
+ for (let [mark, value] in localMarks)
+ for (let [,val] in Iterator(value))
+ yield [mark, val];
+ }
+
+ function getSortedMarks()
+ {
+ // local marks
+ let location = window.content.location.href;
+ let lmarks = [i for (i in localMarkIter()) if (i[1].location == location)];
+ lmarks.sort();
+
+ // URL marks
+ // FIXME: why does umarks.sort() cause a "Component is not available =
+ // NS_ERROR_NOT_AVAILABLE" exception when used here?
+ let umarks = [i for (i in urlMarks)];
+ umarks.sort(function (a, b) a[0].localeCompare(b[0]));
+
+ return lmarks.concat(umarks);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var myModes = config.browserModes;
+
+ mappings.add(myModes,
+ ["m"], "Set mark at the cursor position",
+ function (arg)
+ {
+ if (/[^a-zA-Z]/.test(arg))
+ {
+ liberator.beep();
+ return;
+ }
+
+ marks.add(arg);
+ },
+ { flags: Mappings.flags.ARGUMENT });
+
+ mappings.add(myModes,
+ ["'", "`"], "Jump to the mark in the current buffer",
+ function (arg) { marks.jumpTo(arg); },
+ { flags: Mappings.flags.ARGUMENT });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["delm[arks]"],
+ "Delete the specified marks",
+ function (args)
+ {
+ let special = args.bang;
+ let args = args.string;
+
+ if (!special && !args)
+ {
+ liberator.echoerr("E471: Argument required");
+ return;
+ }
+ if (special && args)
+ {
+ liberator.echoerr("E474: Invalid argument");
+ return;
+ }
+ var matches;
+ if (matches = args.match(/(?:(?:^|[^a-zA-Z0-9])-|-(?:$|[^a-zA-Z0-9])|[^a-zA-Z0-9 -]).*/))
+ {
+ // NOTE: this currently differs from Vim's behavior which
+ // deletes any valid marks in the arg list, up to the first
+ // invalid arg, as well as giving the error message.
+ liberator.echoerr("E475: Invalid argument: " + matches[0]);
+ return;
+ }
+ // check for illegal ranges - only allow a-z A-Z 0-9
+ if (matches = args.match(/[a-zA-Z0-9]-[a-zA-Z0-9]/g))
+ {
+ for (let i = 0; i < matches.length; i++)
+ {
+ var start = matches[i][0];
+ var end = matches[i][2];
+ if (/[a-z]/.test(start) != /[a-z]/.test(end) ||
+ /[A-Z]/.test(start) != /[A-Z]/.test(end) ||
+ /[0-9]/.test(start) != /[0-9]/.test(end) ||
+ start > end)
+ {
+ liberator.echoerr("E475: Invalid argument: " + args.match(matches[i] + ".*")[0]);
+ return;
+ }
+ }
+ }
+
+ marks.remove(args, special);
+ },
+ { bang: true });
+
+ commands.add(["ma[rk]"],
+ "Mark current location within the web page",
+ function (args)
+ {
+ var mark = args[0];
+ if (mark.length > 1)
+ {
+ liberator.echoerr("E488: Trailing characters");
+ return;
+ }
+ if (!/[a-zA-Z]/.test(mark))
+ {
+ liberator.echoerr("E191: Argument must be a letter or forward/backward quote");
+ return;
+ }
+
+ marks.add(mark);
+ },
+ { argCount: "1" });
+
+ commands.add(["marks"],
+ "Show all location marks of current web page",
+ function (args)
+ {
+ args = args.string;
+
+ // ignore invalid mark characters unless there are no valid mark chars
+ if (args && !/[a-zA-Z]/.test(args))
+ {
+ liberator.echoerr("E283: No marks matching \"" + args + "\"");
+ return;
+ }
+
+ var filter = args.replace(/[^a-zA-Z]/g, "");
+ marks.list(filter);
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ // TODO: add support for frameset pages
+ add: function (mark)
+ {
+ var win = window.content;
+
+ if (win.document.body.localName.toLowerCase() == "frameset")
+ {
+ liberator.echoerr("Marks support for frameset pages not implemented yet");
+ return;
+ }
+
+ var x = win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0;
+ var y = win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0;
+ var position = { x: x, y: y };
+
+ if (isURLMark(mark))
+ {
+ urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() });
+ liberator.log("Adding URL mark: " + markToString(mark, urlMarks.get(mark)), 5);
+ }
+ else if (isLocalMark(mark))
+ {
+ // remove any previous mark of the same name for this location
+ removeLocalMark(mark);
+ if (!localMarks.get(mark))
+ localMarks.set(mark, []);
+ let vals = { location: win.location.href, position: position };
+ localMarks.get(mark).push(vals);
+ liberator.log("Adding local mark: " + markToString(mark, vals), 5);
+ }
+ },
+
+ remove: function (filter, special)
+ {
+ if (special)
+ {
+ // :delmarks! only deletes a-z marks
+ for (let [mark,] in localMarks)
+ removeLocalMark(mark);
+ }
+ else
+ {
+ var pattern = new RegExp("[" + filter.replace(/\s+/g, "") + "]");
+ for (let [mark,] in urlMarks)
+ {
+ if (pattern.test(mark))
+ removeURLMark(mark);
+ }
+ for (let [mark,] in localMarks)
+ {
+ if (pattern.test(mark))
+ removeLocalMark(mark);
+ }
+ }
+ },
+
+ jumpTo: function (mark)
+ {
+ var ok = false;
+
+ if (isURLMark(mark))
+ {
+ let slice = urlMarks.get(mark);
+ if (slice && slice.tab && slice.tab.linkedBrowser)
+ {
+ if (slice.tab.parentNode != getBrowser().tabContainer)
+ {
+ pendingJumps.push(slice);
+ // NOTE: this obviously won't work on generated pages using
+ // non-unique URLs :(
+ liberator.open(slice.location, liberator.NEW_TAB);
+ return;
+ }
+ var index = tabs.index(slice.tab);
+ if (index != -1)
+ {
+ tabs.select(index);
+ var win = slice.tab.linkedBrowser.contentWindow;
+ if (win.location.href != slice.location)
+ {
+ pendingJumps.push(slice);
+ win.location.href = slice.location;
+ return;
+ }
+ liberator.log("Jumping to URL mark: " + markToString(mark, slice), 5);
+ win.scrollTo(slice.position.x * win.scrollMaxX, slice.position.y * win.scrollMaxY);
+ ok = true;
+ }
+ }
+ }
+ else if (isLocalMark(mark))
+ {
+ let win = window.content;
+ let slice = localMarks.get(mark) || [];
+
+ for (let [,lmark] in Iterator(slice))
+ {
+ if (win.location.href == lmark.location)
+ {
+ liberator.log("Jumping to local mark: " + markToString(mark, lmark), 5);
+ win.scrollTo(lmark.position.x * win.scrollMaxX, lmark.position.y * win.scrollMaxY);
+ ok = true;
+ break;
+ }
+ }
+ }
+
+ if (!ok)
+ liberator.echoerr("E20: Mark not set"); // FIXME: move up?
+ },
+
+ list: function (filter)
+ {
+ var marks = getSortedMarks();
+
+ if (marks.length == 0)
+ {
+ liberator.echoerr("No marks set");
+ return;
+ }
+
+ if (filter.length > 0)
+ {
+ marks = marks.filter(function (mark) filter.indexOf(mark[0]) >= 0);
+ if (marks.length == 0)
+ {
+ liberator.echoerr("E283: No marks matching \"" + filter + "\"");
+ return;
+ }
+ }
+
+ let list = template.tabular(["mark", "line", "col", "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 each (mark in marks)));
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/buffer.xhtml b/common/content/buffer.xhtml
new file mode 100644
index 00000000..089b1563
--- /dev/null
+++ b/common/content/buffer.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title/>
+ </head>
+ <body/>
+</html>
diff --git a/common/content/commands.js b/common/content/commands.js
new file mode 100644
index 00000000..f5fff23a
--- /dev/null
+++ b/common/content/commands.js
@@ -0,0 +1,892 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// Do NOT create instances of this class yourself, use the helper method
+// commands.add() instead
+function Command(specs, description, action, extraInfo) //{{{
+{
+ if (!specs || !action)
+ return null;
+
+ if (!extraInfo)
+ extraInfo = {};
+
+ // convert command name abbreviation specs of the form
+ // 'shortname[optional-tail]' to short and long versions Eg. 'abc[def]' ->
+ // 'abc', 'abcdef'
+ function parseSpecs(specs)
+ {
+ // Whoever wrote the following should be ashamed. :(
+ // Good grief! I have no words... -- djk ;-)
+ // let shortNames = longNames = names = [];
+ let names = [];
+ let longNames = [];
+ let shortNames = [];
+
+ for (let [,spec] in Iterator(specs))
+ {
+ let matches = spec.match(/(\w+)\[(\w+)\]/);
+
+ if (matches)
+ {
+ shortNames.push(matches[1]);
+ longNames.push(matches[1] + matches[2]);
+ // order as long1, short1, long2, short2
+ names.push(matches[1] + matches[2]);
+ names.push(matches[1]);
+ }
+ else
+ {
+ longNames.push(spec);
+ names.push(spec);
+ }
+ }
+
+ return { names: names, longNames: longNames, shortNames: shortNames };
+ };
+
+ let expandedSpecs = parseSpecs(specs);
+ this.specs = specs;
+ this.shortNames = expandedSpecs.shortNames;
+ this.longNames = expandedSpecs.longNames;
+
+ // return the primary command name (the long name of the first spec listed)
+ this.name = this.longNames[0];
+ this.names = expandedSpecs.names; // return all command name aliases
+ this.description = description || "";
+ this.action = action;
+ this.argCount = extraInfo.argCount || 0;
+ this.completer = extraInfo.completer || null;
+ this.hereDoc = extraInfo.hereDoc || false;
+ this.options = extraInfo.options || [];
+ this.bang = extraInfo.bang || false;
+ this.count = extraInfo.count || false;
+ this.literal = extraInfo.literal == null ? null : extraInfo.literal;
+ this.serial = extraInfo.serial;
+
+ this.isUserCommand = extraInfo.isUserCommand || false;
+ this.replacementText = extraInfo.replacementText || null;
+};
+
+Command.prototype = {
+
+ execute: function (args, bang, count, modifiers)
+ {
+ // XXX
+ bang = !!bang;
+ count = (count === undefined) ? -1 : count;
+ modifiers = modifiers || {};
+
+ let self = this;
+ function exec(args)
+ {
+ // FIXME: Move to parseCommand?
+ args = self.parseArgs(args);
+ if (!args)
+ return;
+ args.count = count;
+ args.bang = bang;
+ self.action.call(self, args, bang, count, modifiers);
+ }
+
+ if (this.hereDoc)
+ {
+ let matches = args.match(/(.*)<<\s*(\S+)$/);
+ if (matches && matches[2])
+ {
+ commandline.inputMultiline(new RegExp("^" + matches[2] + "$", "m"),
+ function (args) { exec(matches[1] + "\n" + args) });
+ return;
+ }
+ }
+
+ exec(args);
+ },
+
+ hasName: function (name)
+ {
+ for (let [,spec] in Iterator(this.specs))
+ {
+ let fullName = spec.replace(/\[(\w+)]$/, "$1");
+ let index = spec.indexOf("[");
+ let min = index == -1 ? fullName.length : index;
+
+ if (fullName.indexOf(name) == 0 && name.length >= min)
+ return true;
+ }
+
+ return false;
+ },
+
+ parseArgs: function (args, complete) commands.parseArgs(args, this.options, this.argCount, false, this.literal, complete)
+
+}; //}}}
+
+function Commands() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var exCommands = [];
+
+ function parseBool(arg)
+ {
+ if (arg == "true" || arg == "1" || arg == "on")
+ return true;
+ if (arg == "false" || arg == "0" || arg == "off")
+ return false;
+ return NaN;
+ }
+
+ const quoteMap = {
+ "\n": "n",
+ "\t": "t"
+ }
+ function quote(q, list)
+ {
+ let re = RegExp("[" + list + "]", "g");
+ return function (str) q + String.replace(str, re, function ($0) $0 in quoteMap ? quoteMap[$0] : ("\\" + $0)) + q;
+ }
+ const complQuote = { // FIXME
+ '"': ['"', quote("", '\n\t"\\\\'), '"'],
+ "'": ["'", quote("", "\\\\'"), "'"],
+ "": ["", quote("", "\\\\ "), ""]
+ };
+ const quoteArg = {
+ '"': quote('"', '\n\t"\\\\'),
+ "'": quote("'", "\\\\'"),
+ "": quote("", "\\\\ ")
+ }
+
+ const ArgType = new Struct("description", "parse");
+ const argTypes = [
+ null,
+ ["no arg", function (arg) !arg],
+ ["boolean", parseBool],
+ ["string", function (val) val],
+ ["int", parseInt],
+ ["float", parseFloat],
+ ["list", function (arg) arg && arg.split(/\s*,\s*/)]
+ ].map(function (x) x && ArgType.apply(null, x));
+
+ function addCommand(command, isUserCommand, replace)
+ {
+ if (!command) // XXX
+ return false;
+
+ if (exCommands.some(function (c) c.hasName(command.name)))
+ {
+ if (isUserCommand && replace)
+ {
+ commands.removeUserCommand(command.name);
+ }
+ else
+ {
+ liberator.log("Warning: :" + command.name + " already exists, NOT replacing existing command.", 1);
+ return false;
+ }
+ }
+
+ exCommands.push(command);
+
+ return true;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ liberator.registerObserver("load_completion", function ()
+ {
+ completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]);
+ });
+
+ var commandManager = {
+
+ // FIXME: remove later, when our option handler is better
+ OPTION_ANY: 0, // can be given no argument or an argument of any type,
+ // caller is responsible for parsing the return value
+ OPTION_NOARG: 1,
+ OPTION_BOOL: 2,
+ OPTION_STRING: 3,
+ OPTION_INT: 4,
+ OPTION_FLOAT: 5,
+ OPTION_LIST: 6,
+
+ COUNT_NONE: -1,
+ COUNT_ALL: -2, // :%...
+
+ __iterator__: function ()
+ {
+ let sorted = exCommands.sort(function (a, b) a.name > b.name);
+ return util.Array.iterator(sorted);
+ },
+
+ add: function (names, description, action, extra)
+ {
+ return addCommand(new Command(names, description, action, extra), false, false);
+ },
+
+ addUserCommand: function (names, description, action, extra, replace)
+ {
+ extra = extra || {};
+ extra.isUserCommand = true;
+ description = description || "User defined command";
+
+ return addCommand(new Command(names, description, action, extra), true, replace);
+ },
+
+ commandToString: function (args)
+ {
+ let res = [args.command + (args.bang ? "!" : "")];
+ function quote(str) quoteArg[/\s/.test(str) ? '"' : ""](str);
+
+ for (let [opt, val] in Iterator(args.options || {}))
+ {
+ res.push(opt);
+ if (val != null)
+ res.push(quote(val));
+ }
+ for (let [,arg] in Iterator(args.arguments || []))
+ res.push(quote(arg));
+
+ let str = args.literalArg;
+ if (str)
+ res.push(/\n/.test(str) ? "<<EOF\n" + str + "EOF" : str);
+ return res.join(" ");
+ },
+
+ get: function (name)
+ {
+ return exCommands.filter(function (cmd) cmd.hasName(name))[0] || null;
+ },
+
+ getUserCommand: function (name)
+ {
+ return exCommands.filter(function (cmd) cmd.isUserCommand && cmd.hasName(name))[0] || null;
+ },
+
+ getUserCommands: function ()
+ {
+ return exCommands.filter(function (cmd) cmd.isUserCommand);
+ },
+
+ // in '-quoted strings, only ' and \ itself are escaped
+ // in "-quoted strings, also ", \n and \t are translated
+ // in non-quoted strings everything is taken literally apart from "\ " and "\\"
+ //
+ // @param str: something like "-x=foo -opt=bar arg1 arg2"
+ // "options" is an array [name, type, validator, completions] and could look like:
+ // 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"]],
+ // [["-other"], OPTION_ANY]];
+ // @param argCount can be:
+ // "0": no arguments
+ // "1": exactly one argument
+ // "+": one or more arguments
+ // "*": zero or more arguments (default if unspecified)
+ // "?": zero or one arguments
+ // @param allowUnknownOptions: -foo won't result in an error, if -foo isn't
+ // specified in "options"
+ // TODO: should it handle comments?
+ parseArgs: function (str, options, argCount, allowUnknownOptions, literal, complete)
+ {
+ // returns [count, parsed_argument]
+ function getNextArg(str)
+ {
+ var stringDelimiter = null;
+ var escapeNext = false;
+
+ var arg = "";
+
+ outer:
+ for (let i = 0; i < str.length; i++)
+ {
+ inner:
+ switch (str[i])
+ {
+ case '"':
+ case "'":
+ if (escapeNext)
+ {
+ escapeNext = false;
+ break;
+ }
+ switch (stringDelimiter)
+ {
+ case str[i]:
+ stringDelimiter = null;
+ continue outer;
+ case null:
+ stringDelimiter = str[i];
+ continue outer;
+ }
+ break;
+
+ // \ is an escape key for non quoted or "-quoted strings
+ // for '-quoted strings it is taken literally, apart from \' and \\
+ case "\\":
+ if (escapeNext)
+ {
+ escapeNext = false;
+ break;
+ }
+ else
+ {
+ // in non-quoted strings, only escape "\\" and "\ ", otherwise drop "\\"
+ if (!stringDelimiter && str[i + 1] != "\\" && str[i + 1] != " ")
+ continue outer;
+ // in single quoted strings, only escape "\\" and "\'", otherwise keep "\\"
+ if (stringDelimiter == "'" && str[i + 1] != "\\" && str[i + 1] != "'")
+ break;
+ escapeNext = true;
+ continue outer;
+ }
+ break;
+
+ default:
+ if (stringDelimiter == "'")
+ {
+ escapeNext = false;
+ break;
+ }
+ if (escapeNext)
+ {
+ escapeNext = false;
+ switch (str[i])
+ {
+ case "n": arg += "\n"; break;
+ case "t": arg += "\t"; break;
+ default:
+ break inner; // this makes "a\fb" -> afb; wanted or should we return ab? --mst
+ }
+ continue outer;
+ }
+ else if (stringDelimiter != '"' && /\s/.test(str[i]))
+ {
+ return [i, arg];
+ }
+ break;
+ }
+ arg += str[i];
+ }
+
+ // TODO: add parsing of a " comment here:
+ if (stringDelimiter)
+ return [str.length, arg, stringDelimiter];
+ if (escapeNext)
+ return [str.length, arg, "\\"];
+ else
+ return [str.length, arg];
+ }
+
+ if (!options)
+ options = [];
+
+ if (!argCount)
+ argCount = "*";
+
+ var args = []; // parsed options
+ args.__iterator__ = util.Array.iterator2;
+ args.string = str; // for access to the unparsed string
+ args.literalArg = "";
+
+ var invalid = false;
+ // FIXME: best way to specify these requirements?
+ var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found
+ var arg = null;
+ var count = 0; // the length of the argument
+ var i = 0;
+ var completeOpts;
+
+ // XXX
+ 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))];
+ }
+ function resetCompletions()
+ {
+ completeOpts = null;
+ args.completeArg = null;
+ args.completeOpt = null;
+ args.completeFilter = null;
+ args.completeStart = i;
+ args.quote = complQuote[""];
+ }
+ if (complete)
+ {
+ resetCompletions();
+ matchOpts("");
+ args.completeArg = 0;
+ }
+
+ function echoerr(error)
+ {
+ if (complete)
+ complete.message = error;
+ else
+ liberator.echoerr(error);
+ }
+
+ outer:
+ while (i < str.length || complete)
+ {
+ // skip whitespace
+ while (/\s/.test(str[i]) && i < str.length)
+ i++;
+ if (i == str.length && !complete)
+ break;
+
+ if (complete)
+ resetCompletions();
+
+ var sub = str.substr(i);
+ if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub))
+ {
+ onlyArgumentsRemaining = true;
+ i += 2;
+ continue;
+ }
+
+ var optname = "";
+ if (!onlyArgumentsRemaining)
+ {
+ for (let [,opt] in Iterator(options))
+ {
+ for (let [,optname] in Iterator(opt[0]))
+ {
+ 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)
+ {
+ [count, arg, quote] = getNextArg(sub.substr(optname.length + 1));
+
+ // if we add the argument to an option after a space, it MUST not be empty
+ if (sep != "=" && !quote && arg.length == 0)
+ arg = null;
+
+ count++; // to compensate the "=" character
+ }
+ else if (!/\s/.test(sep)) // this isn't really an option as it has trailing characters, parse it as an argument
+ {
+ invalid = true;
+ }
+
+ let context = null;
+ if (!complete && quote)
+ {
+ liberator.echoerr("Invalid argument for option " + optname);
+ return null;
+ }
+
+ if (!invalid)
+ {
+ if (complete && count > 0)
+ {
+ args.completeStart += optname.length + 1;
+ args.completeOpt = opt;
+ args.completeFilter = arg;
+ args.quote = complQuote[quote] || complQuote[""];
+ }
+ let type = argTypes[opt[1]];
+ if (type && (!complete || arg != null))
+ {
+ let orig = arg;
+ arg = type.parse(arg);
+ if (arg == null || (typeof arg == "number" && isNaN(arg)))
+ {
+ if (!complete || orig != "" || args.completeStart != str.length)
+ echoerr("Invalid argument for " + type.description + " option: " + optname);
+ if (complete)
+ complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
+ else
+ return null;
+ }
+ }
+
+ // we have a validator function
+ if (typeof opt[2] == "function")
+ {
+ if (opt[2].call(this, arg) == false)
+ {
+ echoerr("Invalid argument for option: " + optname);
+ if (complete)
+ complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
+ else
+ return null;
+ }
+ }
+
+ args[opt[0][0]] = arg; // always use the first name of the option
+ i += optname.length + count;
+ if (i == str.length)
+ break outer;
+ continue outer;
+ }
+ // if it is invalid, just fall through and try the next argument
+ }
+ }
+ }
+ }
+
+ matchOpts(sub);
+
+ if (complete)
+ {
+ if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
+ complete.highlight(i, sub.length, "SPELLCHECK")
+ }
+
+ if (args.length == literal)
+ {
+ if (complete)
+ args.completeArg = args.length;
+ args.literalArg = sub;
+ args.push(sub);
+ args.quote = null;
+ break;
+ }
+
+ // if not an option, treat this token as an argument
+ var [count, arg, quote] = getNextArg(sub);
+ if (complete)
+ {
+ args.quote = complQuote[quote] || complQuote[""];
+ args.completeFilter = arg || "";
+ }
+ else if (count == -1)
+ {
+ liberator.echoerr("Error parsing arguments: " + arg);
+ return null;
+ }
+ else if (!onlyArgumentsRemaining && /^-/.test(arg))
+ {
+ liberator.echoerr("Invalid option: " + arg);
+ return null;
+ }
+
+ if (arg != null)
+ args.push(arg);
+ if (complete)
+ args.completeArg = args.length - 1;
+
+ i += count;
+ if (count <= 0 || i == str.length)
+ break;
+ }
+
+ if (complete)
+ {
+ if (args.completeOpt)
+ {
+ let opt = args.completeOpt;
+ let context = complete.fork(opt[0][0], args.completeStart);
+ context.filter = args.completeFilter;
+ if (typeof opt[3] == "function")
+ var compl = opt[3](context, args);
+ else
+ compl = opt[3] || [];
+ context.title = [opt[0][0]];
+ context.quote = args.quote;
+ context.completions = compl;
+ }
+ complete.advance(args.completeStart);
+ complete.title = ["Options"];
+ if (completeOpts)
+ complete.completions = completeOpts;
+ }
+
+ // check for correct number of arguments
+ if (args.length == 0 && /^[1+]$/.test(argCount) ||
+ literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || ""))
+ {
+ if (!complete)
+ {
+ liberator.echoerr("E471: Argument required");
+ return null;
+ }
+ }
+ else if (args.length == 1 && (argCount == "0") ||
+ args.length > 1 && /^[01?]$/.test(argCount))
+ {
+ echoerr("E488: Trailing characters");
+ return null;
+ }
+
+ return args;
+ },
+
+ // return [null, null, null, null, heredoc_tag || false];
+ // [count, cmd, special, args] = match;
+ parseCommand: function (str, tag)
+ {
+ // remove comments
+ str.replace(/\s*".*$/, "");
+
+ if (tag) // we already have a multiline heredoc construct
+ {
+ if (str == tag)
+ return [null, null, null, null, false];
+ else
+ return [null, null, null, str, tag];
+ }
+
+ // 0 - count, 1 - cmd, 2 - special, 3 - args, 4 - heredoc tag
+ let matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/);
+ //var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/);
+ if (!matches)
+ return [null, null, null, null, null];
+ let [, count, cmd, special, args, heredoc] = matches;
+
+ // parse count
+ if (count)
+ count = count == "%" ? this.COUNT_ALL: parseInt(count, 10);
+ else
+ count = this.COUNT_NONE;
+
+ if (args)
+ {
+ tag = args.match(/<<\s*(\w+)\s*$/);
+ if (tag && tag[1])
+ heredoc = tag[1];
+ }
+
+ return [count, cmd, !!special, args || "", heredoc];
+ },
+
+ get quoteArg() quoteArg,
+
+ removeUserCommand: function (name)
+ {
+ exCommands = exCommands.filter(function (cmd) !(cmd.isUserCommand && cmd.hasName(name)));
+ },
+
+ // FIXME: still belong here? Also used for autocommand parameters
+ replaceTokens: function replaceTokens(str, tokens)
+ {
+ return str.replace(/<((?:q-)?)([a-zA-Z]+)?>/g, function (match, quote, token)
+ {
+ if (token == "lt") // Don't quote, as in vim (but, why so in vim? You'd think people wouldn't say <q-lt> if they didn't want it)
+ return "<";
+ let res = tokens[token];
+ if (res == undefined) // Ignore anything undefined
+ res = "<" + token + ">";
+ if (quote && typeof res != "number")
+ return quoteArg['"'](res);
+ return res;
+ });
+ }
+ };
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ function userCommand(args, modifiers)
+ {
+ let tokens = {
+ args: this.argCount && args.string,
+ bang: this.bang && args.bang ? "!" : "",
+ count: this.count && args.count
+ };
+
+ liberator.execute(commands.replaceTokens(this.replacementText, tokens));
+ }
+
+ // TODO: offer completion.ex?
+ var completeOptionMap = {
+ altstyle: "alternateStylesheet", bookmark: "bookmark",
+ buffer: "buffer", color: "colorScheme", command: "command",
+ dialog: "dialog", dir: "directory", environment: "environment",
+ event: "autocmdEvent", file: "file", help: "help",
+ highlight: "highlightGroup", javascript: "javascript", macro: "macro",
+ mapping: "userMapping", menu: "menuItem", option: "option",
+ preference: "preference", search: "search",
+ shellcmd: "shellCommand", sidebar: "sidebar", url: "url",
+ usercommand: "userCommand"
+ };
+
+ // TODO: Vim allows commands to be defined without {rep} if there are {attr}s
+ // specified - useful?
+ commandManager.add(["com[mand]"],
+ "List and define commands",
+ function (args)
+ {
+ let cmd = args[0];
+
+ if (cmd != null && /\W/.test(cmd))
+ {
+ liberator.echoerr("E182: Invalid command name");
+ return;
+ }
+
+ if (args.literalArg)
+ {
+ let nargsOpt = args["-nargs"] || "0";
+ let bangOpt = "-bang" in args;
+ let countOpt = "-count" in args;
+ let completeOpt = args["-complete"];
+
+ let completeFunc = null; // default to no completion for user commands
+
+ if (completeOpt)
+ {
+ let func;
+
+ if (/^custom,/.test(completeOpt))
+ func = completeOpt.replace("custom,", "");
+ else
+ func = "completion." + completeOptionMap[completeOpt];
+
+ completeFunc = eval(func);
+ }
+
+ if (!commands.addUserCommand(
+ [cmd],
+ "User defined command",
+ userCommand,
+ {
+ argCount: nargsOpt,
+ bang: bangOpt,
+ count: countOpt,
+ completer: completeFunc,
+ replacementText: args.literalArg
+ },
+ args.bang)
+ )
+ {
+ liberator.echoerr("E174: Command already exists: add ! to replace it");
+ }
+ }
+ else
+ {
+ function completerToString(completer)
+ {
+ if (completer)
+ return [k for ([k, v] in Iterator(completeOptionMap))
+ if (v == completer.name)][0] || "custom";
+ else
+ return "";
+ }
+
+ // TODO: using an array comprehension here generates flakey results across repeated calls
+ // : perhaps we shouldn't allow options in a list call but just ignore them for now
+ let cmds = exCommands.filter(function (c) c.isUserCommand && (!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 each (cmd in cmds)));
+
+ commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ else
+ {
+ liberator.echo("No user-defined commands found");
+ }
+ }
+ },
+ {
+ bang: true,
+ completer: function (context) completion.userCommand(context),
+ options: [
+ [["-nargs"], commandManager.OPTION_STRING,
+ function (arg) /^[01*?+]$/.test(arg), ["0", "1", "*", "?", "+"]],
+ [["-bang"], commandManager.OPTION_NOARG],
+ [["-count"], commandManager.OPTION_NOARG],
+ [["-complete"], commandManager.OPTION_STRING,
+ function (arg) arg in completeOptionMap || /custom,\w+/.test(arg)]
+ ],
+ literal: 1,
+ serial: function () [
+ {
+ command: this.name,
+ bang: true,
+ // Yeah, this is a bit scary. Perhaps I'll fix it when I'm
+ // awake.
+ options: util.Array.assocToObj(
+ util.map({ argCount: "-nargs", bang: "-bang", count: "-count" },
+ function ([k, v]) k in cmd && cmd[k] != "0" && [v, typeof cmd[k] == "boolean" ? null : cmd[k]])
+ .filter(util.identity)),
+ arguments: [cmd.name],
+ literalArg: cmd.replacementText
+ }
+ for ([k, cmd] in Iterator(exCommands))
+ if (cmd.isUserCommand && cmd.replacementText)
+ ]
+ });
+
+ commandManager.add(["comc[lear]"],
+ "Delete all user-defined commands",
+ function ()
+ {
+ commands.getUserCommands().forEach(function (cmd) { commands.removeUserCommand(cmd.name); });
+ },
+ { argCount: "0" });
+
+ commandManager.add(["delc[ommand]"],
+ "Delete the specified user-defined command",
+ function (args)
+ {
+ let name = args[0];
+
+ if (commands.get(name))
+ commands.removeUserCommand(name);
+ else
+ liberator.echoerr("E184: No such user-defined command: " + name);
+ },
+ {
+ argCount: "1",
+ completer: function (context) completion.userCommand(context)
+ });
+
+ //}}}
+
+ return commandManager;
+
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/completion.js b/common/content/completion.js
new file mode 100644
index 00000000..33387b3f
--- /dev/null
+++ b/common/content/completion.js
@@ -0,0 +1,1704 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+function CompletionContext(editor, name, offset)
+{
+ if (!(this instanceof arguments.callee))
+ return new arguments.callee(editor, name, offset);
+ if (!name)
+ name = "";
+
+ let self = this;
+ if (editor instanceof arguments.callee)
+ {
+ let parent = editor;
+ name = parent.name + "/" + name;
+ this.contexts = parent.contexts;
+ if (name in this.contexts)
+ self = this.contexts[name];
+ else
+ self.contexts[name] = this;
+
+ self.parent = parent;
+ ["filters", "keys", "title", "quote"].forEach(function (key)
+ self[key] = parent[key] && util.cloneObject(parent[key]));
+ ["anchored", "compare", "editor", "_filter", "filterFunc", "keys", "_process", "top"].forEach(function (key)
+ self[key] = parent[key]);
+
+ self.__defineGetter__("value", function () this.top.value);
+
+ self.offset = parent.offset;
+ self.advance(offset);
+
+ self.incomplete = false;
+ self.message = null;
+ self.waitingForTab = false;
+ //delete self._filter; // FIXME?
+ delete self._generate;
+ delete self._ignoreCase;
+ if (self != this)
+ return self;
+ ["_caret", "contextList", "maxItems", "onUpdate", "selectionTypes", "tabPressed", "updateAsync", "value"].forEach(function (key) {
+ self.__defineGetter__(key, function () this.top[key]);
+ self.__defineSetter__(key, function (val) this.top[key] = val);
+ });
+ }
+ else
+ {
+ if (typeof editor == "string")
+ this._value = editor;
+ else
+ this.editor = editor;
+ this.compare = function (a, b) String.localeCompare(a.text, b.text);
+
+ this.filterFunc = function (items)
+ {
+ let self = this;
+ return this.filters.
+ reduce(function (res, filter) res.filter(function (item) filter.call(self, item)),
+ items);
+ }
+ this.filters = [function (item) {
+ let text = Array.concat(this.getKey(item, "text"));
+ for (let [i, str] in Iterator(text))
+ {
+ if (this.match(String(str)))
+ {
+ item.text = String(text[i]);
+ return true;
+ }
+ }
+ return false;
+ }];
+ this.contexts = { name: this };
+ this.keys = { text: 0, description: 1, icon: "icon" };
+ this.offset = offset || 0;
+ this.onUpdate = function () true;
+ this.top = this;
+ this.__defineGetter__("incomplete", function () this.contextList.some(function (c) c.parent && c.incomplete));
+ this.__defineGetter__("waitingForTab", function () this.contextList.some(function (c) c.parent && c.waitingForTab));
+ this.reset();
+ }
+ this.cache = {};
+ this.itemCache = {};
+ this.key = "";
+ this.message = null;
+ this.name = name || "";
+ this._completions = []; // FIXME
+ this.getKey = function (item, key) (typeof self.keys[key] == "function") ? self.keys[key].call(this, item.item) :
+ key in self.keys ? item.item[self.keys[key]]
+ : item.item[key];
+}
+CompletionContext.prototype = {
+ // Temporary
+ get allItems()
+ {
+ try
+ {
+ let self = this;
+ let minStart = Math.min.apply(Math, [context.offset for ([k, context] in Iterator(this.contexts)) if (context.items.length && context.hasItems)]);
+ let items = this.contextList.map(function (context) {
+ if (!context.hasItems)
+ return [];
+ let prefix = self.value.substring(minStart, context.offset);
+ return context.items.map(function makeItem(item) ({ text: prefix + item.text, item: item.item }));
+ });
+ return { start: minStart, items: util.Array.flatten(items), longestSubstring: this.longestAllSubstring }
+ }
+ catch (e)
+ {
+ liberator.reportError(e);
+ return { start: 0, items: [], longestAllSubstring: "" }
+ }
+ },
+ // Temporary
+ get allSubstrings()
+ {
+ let contexts = this.contextList.filter(function (c) c.hasItems && c.items.length);
+ let minStart = Math.min.apply(Math, contexts.map(function (c) c.offset));
+ let lists = contexts.map(function (context) {
+ let prefix = context.value.substring(minStart, context.offset);
+ return context.substrings.map(function (s) prefix + s);
+ });
+
+ let substrings = lists.reduce(
+ function (res, list) res.filter(function (str) list.some(function (s) s.substr(0, str.length) == str)),
+ lists.pop());
+ if (!substrings) // FIXME: How is this undefined?
+ return [];
+ return util.Array.uniq(substrings);
+ },
+ // Temporary
+ get longestAllSubstring()
+ {
+ return this.allSubstrings.reduce(function (a, b) a.length > b.length ? a : b, "");
+ },
+
+ get caret() this._caret - this.offset,
+ set caret(val) this._caret = val + this.offset,
+
+ get compare() this._compare || function () 0,
+ set compare(val) this._compare = val,
+
+ get completions() this._completions || [],
+ set completions(items)
+ {
+ // Accept a generator
+ if (!(items instanceof Array))
+ items = [x for (x in Iterator(items))];
+ delete this.cache.filtered;
+ delete this.cache.filter;
+ this.cache.rows = [];
+ this.hasItems = items.length > 0;
+ this._completions = items;
+ let self = this;
+ if (this.updateAsync && !this.noUpdate)
+ liberator.callInMainThread(function () { self.onUpdate.call(self) });
+ },
+
+ get createRow() this._createRow || template.completionRow, // XXX
+ set createRow(createRow) this._createRow = createRow,
+
+ get filterFunc() this._filterFunc || util.identity,
+ set filterFunc(val) this._filterFunc = val,
+
+ get filter() this._filter != null ? this._filter : this.value.substr(this.offset, this.caret),
+ set filter(val)
+ {
+ delete this._ignoreCase;
+ return this._filter = val
+ },
+
+ get format() ({
+ title: this.title,
+ keys: this.keys,
+ process: this.process
+ }),
+ set format(format)
+ {
+ this.title = format.title || this.title;
+ this.keys = format.keys || this.keys;
+ this.process = format.process || this.process;
+ },
+
+ get message() this._message || (this.waitingForTab ? "Waiting for <Tab>" : null),
+ set message(val) this._message = val,
+
+ get regenerate() this._generate && (!this.completions || !this.itemCache[this.key] || this.cache.offset != this.offset),
+ set regenerate(val) { if (val) delete this.itemCache[this.key] },
+
+ get generate() !this._generate ? null : function ()
+ {
+ if (this.offset != this.cache.offset)
+ this.itemCache = {};
+ this.cache.offset = this.offset;
+ if (!this.itemCache[this.key])
+ this.itemCache[this.key] = this._generate.call(this);
+ return this.itemCache[this.key];
+ },
+ set generate(arg)
+ {
+ this.hasItems = true;
+ this._generate = arg;
+ //**/ liberator.dump(this.name + ": set generate()");
+ if (this.background && this.regenerate)
+ {
+ //**/ this.__i = (this.__i || 0) + 1;
+ //**/ let self = this;
+ //**/ function dump(msg) liberator.callInMainThread(function () liberator.dump(self.name + ":" + self.__i + ": " + msg));
+ //**/ dump("set generate() regenerating");
+
+ let lock = {};
+ this.cache.backgroundLock = lock;
+ this.incomplete = true;
+ let thread = this.getCache("backgroundThread", liberator.newThread);
+ //**/ dump(thread);
+ liberator.callAsync(thread, this, function () {
+ //**/ dump("In async");
+ if (this.cache.backgroundLock != lock)
+ {
+ //**/ dump("Lock !ok");
+ return;
+ }
+ let items = this.generate();
+ //**/ dump("Generated");
+ if (this.cache.backgroundLock != lock)
+ {
+ //**/ dump("Lock !ok");
+ return;
+ }
+ this.incomplete = false;
+ //**/ dump("completions=");
+ this.completions = items;
+ //**/ dump("completions==");
+ });
+ }
+ },
+
+ get ignoreCase()
+ {
+ if ("_ignoreCase" in this)
+ return this._ignoreCase;
+ let mode = options["wildcase"];
+ if (mode == "match")
+ return this._ignoreCase = false;
+ if (mode == "ignore")
+ return this._ignoreCase = true;
+ return this._ignoreCase = !/[A-Z]/.test(this.filter);
+ },
+ set ignoreCase(val) this._ignoreCase = val,
+
+ get items()
+ {
+ if (!this.hasItems || this.backgroundLock)
+ return [];
+ if (this.cache.filtered && this.cache.filter == this.filter)
+ return this.cache.filtered;
+ this.cache.rows = [];
+ let items = this.completions;
+ if (this.generate && !this.background)
+ {
+ // XXX
+ this.noUpdate = true;
+ this.completions = items = this.generate();
+ this.noUpdate = false;
+ }
+ this.cache.filter = this.filter;
+ if (items == null)
+ return items;
+
+ let self = this;
+ delete this._substrings;
+
+ let filtered = this.filterFunc(items.map(function (item) ({ text: self.getKey({ item: item }, "text"), item: item })));
+ if (this.maxItems)
+ filtered = filtered.slice(0, this.maxItems);
+
+ if (options.get("wildoptions").has("sort") && this.compare)
+ filtered.sort(this.compare);
+ let quote = this.quote;
+ if (quote)
+ filtered.forEach(function (item) {
+ item.unquoted = item.text;
+ item.text = quote[0] + quote[1](item.text) + quote[2];
+ })
+ return this.cache.filtered = filtered;
+ },
+
+ get process() // FIXME
+ {
+ let self = this;
+ let process = this._process;
+ process = [process[0] || template.icon, process[1] || function (item, k) k];
+ let first = process[0];
+ let filter = this.filter;
+ if (!this.anchored)
+ process[0] = function (item, text) first.call(self, item, template.highlightFilter(item.text, filter));
+ return process;
+ },
+ set process(process)
+ {
+ this._process = process;
+ },
+
+ get substrings()
+ {
+ let items = this.items;
+ if (items.length == 0 || !this.hasItems)
+ return [];
+ if (this._substrings)
+ return this._substrings;
+
+ let fixCase = this.ignoreCase ? String.toLowerCase : util.identity;
+ let text = fixCase(items[0].unquoted || items[0].text);
+ let filter = fixCase(this.filter);
+ if (this.anchored)
+ {
+ function compare (text, s) text.substr(0, s.length) == s;
+ substrings = util.map(util.range(filter.length, text.length + 1),
+ function (end) text.substring(0, end));
+ }
+ else
+ {
+ function compare (text, s) text.indexOf(s) >= 0;
+ substrings = [];
+ let start = 0;
+ let idx;
+ let length = filter.length;
+ while ((idx = text.indexOf(filter, start)) > -1 && idx < text.length)
+ {
+ for (let end in util.range(idx + length, text.length + 1))
+ substrings.push(text.substring(idx, end));
+ start = idx + 1;
+ }
+ }
+ substrings = items.reduce(
+ function (res, item) res.filter(function (str) compare(fixCase(item.unquoted || item.text), str)),
+ substrings);
+ let quote = this.quote;
+ if (quote)
+ substrings = substrings.map(function (str) quote[0] + quote[1](str));
+ return this._substrings = substrings;
+ },
+
+ advance: function advance(count)
+ {
+ delete this._ignoreCase;
+ if (this.quote)
+ {
+ count = this.quote[0].length + this.quote[1](this.filter.substr(0, count)).length;
+ this.quote[0] = "";
+ this.quote[2] = "";
+ }
+ this.offset += count;
+ if (this._filter)
+ this._filter = this._filter.substr(count);
+ },
+
+ getCache: function (key, defVal)
+ {
+ if (!(key in this.cache))
+ this.cache[key] = defVal();
+ return this.cache[key];
+ },
+
+ getItems: function getItems(start, end)
+ {
+ let self = this;
+ let items = this.items;
+ let reverse = start > end;
+ start = Math.max(0, start || 0);
+ end = Math.min(items.length, end ? end : items.length);
+ return util.map(util.range(start, end, reverse), function (i) items[i]);
+ },
+
+ getRows: function getRows(start, end, doc)
+ {
+ let self = this;
+ let items = this.items;
+ let cache = this.cache.rows;
+ let reverse = start > end;
+ start = Math.max(0, start || 0);
+ end = Math.min(items.length, end != null ? end : items.length);
+ for (let i in util.range(start, end, reverse))
+ yield [i, cache[i] = cache[i] || util.xmlToDom(self.createRow(items[i]), doc)];
+ },
+
+ fork: function fork(name, offset, self, completer)
+ {
+ if (typeof completer == "string")
+ completer = self[completer]
+ let context = new CompletionContext(this, name, offset);
+ this.contextList.push(context);
+ if (completer)
+ return completer.apply(self || this, [context].concat(Array.slice(arguments, 4)));
+ return context;
+ },
+
+ getText: function getText(item)
+ {
+ let text = item[self.keys["text"]];
+ if (self.quote)
+ return self.quote(text);
+ return text;
+ },
+
+ highlight: function highlight(start, length, type)
+ {
+ try // Firefox <3.1 doesn't have repaintSelection
+ {
+ this.selectionTypes[type] = null;
+ const selType = Components.interfaces.nsISelectionController["SELECTION_" + type];
+ const editor = this.editor;
+ let sel = editor.selectionController.getSelection(selType);
+ if (length == 0)
+ sel.removeAllRanges();
+ else
+ {
+ let range = editor.selection.getRangeAt(0).cloneRange();
+ range.setStart(range.startContainer, this.offset + start);
+ range.setEnd(range.startContainer, this.offset + start + length);
+ sel.addRange(range);
+ }
+ editor.selectionController.repaintSelection(selType);
+ }
+ catch (e) {}
+ },
+
+ match: function match(str)
+ {
+ let filter = this.filter;
+ if (this.ignoreCase)
+ {
+ filter = filter.toLowerCase();
+ str = str.toLowerCase();
+ }
+ if (this.anchored)
+ return str.substr(0, filter.length) == filter;
+ return str.indexOf(filter) > -1;
+ },
+
+ reset: function reset()
+ {
+ let self = this;
+ if (this.parent)
+ throw Error();
+ // Not ideal.
+ for (let type in this.selectionTypes)
+ this.highlight(0, 0, type);
+ this.contextList = [];
+ this.offset = 0;
+ this.process = [];
+ this.selectionTypes = {};
+ this.tabPressed = false;
+ this.title = ["Completions"];
+ this.waitingForTab = false;
+ this.updateAsync = false;
+ if (this.editor)
+ {
+ this.value = this.editor.selection.focusNode.textContent;
+ this._caret = this.editor.selection.focusOffset;
+ }
+ else
+ {
+ this.value = this._value;
+ this._caret = this.value.length;
+ }
+ //for (let key in (k for ([k, v] in Iterator(self.contexts)) if (v.offset > this.caret)))
+ // delete this.contexts[key];
+ for each (let context in this.contexts)
+ {
+ context.hasItems = false;
+ context.incomplete = false;
+ }
+ },
+
+ wait: function wait(interruptable, timeout)
+ {
+ let end = Date.now() + timeout;
+ while (this.incomplete && (!timeout || Date.now() > end))
+ liberator.threadYield(false, interruptable);
+ return this.incomplete;
+ }
+}
+
+function Completion() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ try
+ {
+ var completionService = Components.classes["@mozilla.org/browser/global-history;2"]
+ .getService(Components.interfaces.nsIAutoCompleteSearch);
+ }
+ catch (e) {}
+
+ const EVAL_TMP = "__liberator_eval_tmp";
+
+ function Javascript()
+ {
+ let json = Components.classes["@mozilla.org/dom/json;1"]
+ .createInstance(Components.interfaces.nsIJSON);
+ const OFFSET = 0, CHAR = 1, STATEMENTS = 2, DOTS = 3, FULL_STATEMENTS = 4, COMMA = 5, FUNCTIONS = 6;
+ let stack = [];
+ let functions = [];
+ let top = []; /* The element on the top of the stack. */
+ let last = ""; /* The last opening char pushed onto the stack. */
+ let lastNonwhite = ""; /* Last non-whitespace character we saw. */
+ let lastChar = ""; /* Last character we saw, used for \ escaping quotes. */
+ let compl = [];
+ let str = "";
+
+ let lastIdx = 0;
+
+ let cacheKey = null;
+
+ this.completers = {};
+
+ this.iter = function iter(obj)
+ {
+ let iterator = (function objIter()
+ {
+ for (let k in obj)
+ {
+ // Some object members are only accessible as function calls
+ try
+ {
+ yield [k, obj[k]];
+ continue;
+ }
+ catch (e) {}
+ yield [k, <>inaccessable</>]
+ }
+ })();
+ try
+ {
+ // The point of 'for k in obj' is to get keys
+ // that are accessible via . or [] notation.
+ // Iterators quite often return values of no
+ // use whatsoever for this purpose, so, we try
+ // this rather dirty hack of getting a standard
+ // object iterator for any object that defines its
+ // own.
+ if ("__iterator__" in obj)
+ {
+ let oldIter = obj.__iterator__;
+ delete obj.__iterator__;
+ iterator = Iterator(obj);
+ obj.__iterator__ = oldIter;
+ }
+ }
+ catch (e) {}
+ return iterator;
+ }
+
+ /* Search the object for strings starting with @key.
+ * If @last is defined, key is a quoted string, it's
+ * wrapped in @last after @offset characters are sliced
+ * off of it and it's quoted.
+ */
+ this.objectKeys = function objectKeys(obj)
+ {
+ // Things we can dereference
+ if (["object", "string", "function"].indexOf(typeof obj) == -1)
+ return [];
+ if (!obj)
+ return [];
+
+ // XPCNativeWrappers, etc, don't show all accessible
+ // members until they're accessed, so, we look at
+ // the wrappedJSObject instead, and return any keys
+ // available in the object itself.
+ let orig = obj;
+
+ // v[0] in orig and orig[v[0]] catch different cases. XPCOM
+ // objects are problematic, to say the least.
+ if (modules.isPrototypeOf(obj))
+ compl = [v for (v in Iterator(obj))];
+ else
+ {
+ if (obj.wrappedJSObject)
+ obj = obj.wrappedJSObject;
+ compl = [v for (v in this.iter(obj))
+ if ((typeof orig == "object" && v[0] in orig) || orig[v[0]] !== undefined)];
+ }
+
+ // And if wrappedJSObject happens to be available,
+ // return that, too.
+ if (orig.wrappedJSObject)
+ compl.push(["wrappedJSObject", obj]);
+
+ // Add keys for sorting later.
+ // Numbers are parsed to ints.
+ // Constants, which should be unsorted, are found and marked null.
+ compl.forEach(function (item) {
+ let key = item[0];
+ if (!isNaN(key))
+ key = parseInt(key);
+ else if (/^[A-Z_]+$/.test(key))
+ key = "";
+ item.key = key;
+ });
+
+ return compl;
+ }
+
+ this.eval = function eval(arg, key, tmp)
+ {
+ let cache = this.context.cache.eval;
+ let context = this.context.cache.evalContext;
+
+ if (!key)
+ key = arg;
+ if (key in cache)
+ return cache[key];
+
+ context[EVAL_TMP] = tmp;
+ try
+ {
+ return cache[key] = liberator.eval(arg, context);
+ }
+ catch (e)
+ {
+ return null;
+ }
+ finally
+ {
+ delete context[EVAL_TMP];
+ }
+ }
+
+ /* Get an element from the stack. If @n is negative,
+ * count from the top of the stack, otherwise, the bottom.
+ * If @m is provided, return the @mth value of element @o
+ * of the stack entey at @n.
+ */
+ let get = function get(n, m, o)
+ {
+ let a = stack[n >= 0 ? n : stack.length + n];
+ if (o != null)
+ a = a[o];
+ if (m == null)
+ return a;
+ return a[a.length - m - 1];
+ }
+
+ function buildStack(filter)
+ {
+ let self = this;
+ /* Push and pop the stack, maintaining references to 'top' and 'last'. */
+ let push = function push(arg)
+ {
+ top = [i, arg, [i], [], [], [], []];
+ last = top[CHAR];
+ stack.push(top);
+ }
+ let pop = function pop(arg)
+ {
+ if (top[CHAR] != arg)
+ {
+ self.context.highlight(top[OFFSET], i - top[OFFSET], "SPELLCHECK");
+ self.context.highlight(top[OFFSET], 1, "FIND");
+ throw new Error("Invalid JS");
+ }
+ if (i == self.context.caret - 1)
+ self.context.highlight(top[OFFSET], 1, "FIND");
+ // The closing character of this stack frame will have pushed a new
+ // statement, leaving us with an empty statement. This doesn't matter,
+ // now, as we simply throw away the frame when we pop it, but it may later.
+ if (top[STATEMENTS][top[STATEMENTS].length - 1] == i)
+ top[STATEMENTS].pop();
+ top = get(-2);
+ last = top[CHAR];
+ let ret = stack.pop();
+ return ret;
+ }
+
+ let i = 0, c = ""; /* Current index and character, respectively. */
+
+ // Reuse the old stack.
+ if (str && filter.substr(0, str.length) == str)
+ {
+ i = str.length;
+ if (this.popStatement)
+ top[STATEMENTS].pop();
+ }
+ else
+ {
+ stack = [];
+ functions = [];
+ push("#root");
+ }
+
+ /* Build a parse stack, discarding entries as opening characters
+ * match closing characters. The stack is walked from the top entry
+ * and down as many levels as it takes us to figure out what it is
+ * that we're completing.
+ */
+ str = filter;
+ let length = str.length;
+ for (; i < length; lastChar = c, i++)
+ {
+ c = str[i];
+ if (last == '"' || last == "'" || last == "/")
+ {
+ if (lastChar == "\\") // Escape. Skip the next char, whatever it may be.
+ {
+ c = "";
+ i++;
+ }
+ else if (c == last)
+ pop(c);
+ }
+ else
+ {
+ // A word character following a non-word character, or simply a non-word
+ // character. Start a new statement.
+ if (/[\w$]/.test(c) && !/[\w\d$]/.test(lastChar) || !/[\w\d\s$]/.test(c))
+ top[STATEMENTS].push(i);
+
+ // A "." or a "[" dereferences the last "statement" and effectively
+ // joins it to this logical statement.
+ if ((c == "." || c == "[") && /[\w\d$\])"']/.test(lastNonwhite)
+ || lastNonwhite == "." && /[\w$]/.test(c))
+ top[STATEMENTS].pop();
+
+ switch (c)
+ {
+ case "(":
+ /* Function call, or if/while/for/... */
+ if (/[\w\d$]/.test(lastNonwhite))
+ {
+ functions.push(i);
+ top[FUNCTIONS].push(i);
+ top[STATEMENTS].pop();
+ }
+ case '"':
+ case "'":
+ case "/":
+ case "{":
+ push(c);
+ break;
+ case "[":
+ push(c);
+ break;
+ case ".":
+ top[DOTS].push(i);
+ break;
+ case ")": pop("("); break;
+ case "]": pop("["); break;
+ case "}": pop("{"); /* Fallthrough */
+ case ";":
+ top[FULL_STATEMENTS].push(i);
+ case ",":
+ top[COMMA];
+ break;
+ }
+
+ if (/\S/.test(c))
+ lastNonwhite = c;
+ }
+ }
+
+ this.popStatement = false;
+ if (!/[\w\d$]/.test(lastChar) && lastNonwhite != ".")
+ {
+ this.popStatement = true;
+ top[STATEMENTS].push(i);
+ }
+
+ lastIdx = i;
+ }
+
+ this.complete = function _complete(context)
+ {
+ this.context = context;
+
+ let self = this;
+ try
+ {
+ buildStack.call(this, context.filter);
+ }
+ catch (e)
+ {
+ if (e.message != "Invalid JS")
+ liberator.reportError(e);
+ lastIdx = 0;
+ return;
+ }
+
+ let cache = this.context.cache;
+ this.context.getCache("eval", Object);
+ this.context.getCache("evalContext", function () ({ __proto__: userContext }));
+
+ /* Okay, have parse stack. Figure out what we're completing. */
+
+ // Find any complete statements that we can eval before we eval our object.
+ // This allows for things like: let doc = window.content.document; let elem = doc.createElement...; elem.<Tab>
+ let prev = 0;
+ for (let [,v] in Iterator(get(0)[FULL_STATEMENTS]))
+ {
+ let key = str.substring(prev, v + 1);
+ if (checkFunction(prev, v, key))
+ return;
+ this.eval(key);
+ prev = v + 1;
+ }
+
+ // Don't eval any function calls unless the user presses tab.
+ function checkFunction(start, end, key)
+ {
+ let res = functions.some(function (idx) idx >= start && idx < end);
+ if (!res || self.context.tabPressed || key in cache.eval)
+ return false;
+ self.context.waitingForTab = true;
+ return true;
+ }
+
+ // For each DOT in a statement, prefix it with TMP, eval it,
+ // and save the result back to TMP. The point of this is to
+ // cache the entire path through an object chain, mainly in
+ // the presence of function calls. There are drawbacks. For
+ // instance, if the value of a variable changes in the course
+ // of inputting a command (let foo=bar; frob(foo); foo=foo.bar; ...),
+ // we'll still use the old value. But, it's worth it.
+ function getObj(frame, stop)
+ {
+ let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
+ let prev = statement;
+ let obj;
+ let cacheKey;
+ for (let [i, dot] in Iterator(get(frame)[DOTS].concat(stop)))
+ {
+ if (dot < statement)
+ continue;
+ if (dot > stop || dot <= prev)
+ break;
+ let s = str.substring(prev, dot);
+
+ if (prev != statement)
+ s = EVAL_TMP + "." + s;
+ cacheKey = str.substring(statement, dot);
+
+ if (checkFunction(prev, dot, cacheKey))
+ return [];
+
+ prev = dot + 1;
+ obj = self.eval(s, cacheKey, obj);
+ }
+ return [[obj, cacheKey]]
+ }
+
+ function getObjKey(frame)
+ {
+ let dot = get(frame, 0, DOTS) || -1; // Last dot in frame.
+ let statement = get(frame, 0, STATEMENTS) || 0; // Current statement.
+ let end = (frame == -1 ? lastIdx : get(frame + 1)[OFFSET]);
+
+ cacheKey = null;
+ let obj = [[cache.evalContext, "Local Variables"], [userContext, "Global Variables"],
+ [modules, "modules"], [window, "window"]]; // Default objects;
+ /* Is this an object dereference? */
+ if (dot < statement) // No.
+ dot = statement - 1;
+ else // Yes. Set the object to the string before the dot.
+ obj = getObj(frame, dot);
+
+ let [, space, key] = str.substring(dot + 1, end).match(/^(\s*)(.*)/);
+ return [dot + 1 + space.length, obj, key];
+ }
+
+ function fill(context, obj, name, compl, anchored, key, last, offset)
+ {
+ context.title = [name];
+ context.anchored = anchored;
+ context.filter = key;
+ context.itemCache = context.parent.itemCache;
+ context.key = name;
+
+ if (last != null)
+ context.quote = [last, function (text) util.escapeString(text.substr(offset), ""), last];
+ else // We're not looking for a quoted string, so filter out anything that's not a valid identifier
+ context.filters.push(function (item) /^[\w$][\w\d$]*$/.test(item.text));
+
+ compl.call(self, context, obj);
+ }
+
+ function complete(objects, key, compl, string, last)
+ {
+ let orig = compl;
+ if (!compl)
+ {
+ compl = function (context, obj)
+ {
+ context.process = [null, function highlight(item, v) template.highlight(v, true)];
+ // Sort in a logical fasion for object keys:
+ // Numbers are sorted as numbers, rather than strings, and appear first.
+ // Constants are unsorted, and appear before other non-null strings.
+ // Other strings are sorted in the default manner.
+ let compare = context.compare;
+ context.compare = function (a, b)
+ {
+ if (!isNaN(a.item.key) && !isNaN(b.item.key))
+ return a.item.key - b.item.key;
+ return isNaN(b.item.key) - isNaN(a.item.key) || compare(a, b);
+ }
+ if (!context.anchored) // We've already listed anchored matches, so don't list them again here.
+ context.filters.push(function (item) util.compareIgnoreCase(item.text.substr(0, this.filter.length), this.filter));
+ if (obj == cache.evalContext)
+ context.regenerate = true;
+ context.generate = function () self.objectKeys(obj);
+ }
+ }
+ // TODO: Make this a generic completion helper function.
+ let filter = key + (string || "");
+ for (let [,obj] in Iterator(objects))
+ {
+ this.context.fork(obj[1], top[OFFSET], this, fill,
+ obj[0], obj[1], compl, compl != orig, filter, last, key.length);
+ }
+ if (orig)
+ return;
+ for (let [,obj] in Iterator(objects))
+ {
+ obj[1] += " (substrings)";
+ this.context.fork(obj[1], top[OFFSET], this, fill,
+ obj[0], obj[1], compl, false, filter, last, key.length);
+ }
+ }
+
+ // In a string. Check if we're dereferencing an object.
+ // Otherwise, do nothing.
+ if (last == "'" || last == '"')
+ {
+ /*
+ * str = "foo[bar + 'baz"
+ * obj = "foo"
+ * key = "bar + ''"
+ */
+
+ // The top of the stack is the sting we're completing.
+ // Wrap it in its delimiters and eval it to process escape sequences.
+ let string = str.substring(get(-1)[OFFSET] + 1, lastIdx);
+ string = eval(last + string + last);
+
+ function getKey()
+ {
+ if (last == "")
+ return "";
+ // After the opening [ upto the opening ", plus '' to take care of any operators before it
+ let key = str.substring(get(-2, 0, STATEMENTS), get(-1, null, OFFSET)) + "''";
+ // Now eval the key, to process any referenced variables.
+ return this.eval(key);
+ }
+
+ /* Is this an object accessor? */
+ if (get(-2)[CHAR] == "[") // Are we inside of []?
+ {
+ /* Stack:
+ * [-1]: "...
+ * [-2]: [...
+ * [-3]: base statement
+ */
+
+ // Yes. If the [ starts at the begining of a logical
+ // statement, we're in an array literal, and we're done.
+ if (get(-3, 0, STATEMENTS) == get(-2)[OFFSET])
+ return;
+
+ // Begining of the statement upto the opening [
+ let obj = getObj(-3, get(-2)[OFFSET]);
+
+ return complete.call(this, obj, getKey(), null, string, last);
+ }
+
+ // Is this a function call?
+ if (get(-2)[CHAR] == "(")
+ {
+ /* Stack:
+ * [-1]: "...
+ * [-2]: (...
+ * [-3]: base statement
+ */
+
+ // Does the opening "(" mark a function call?
+ if (get(-3, 0, FUNCTIONS) != get(-2)[OFFSET])
+ return; // No. We're done.
+
+ let [offset, obj, func] = getObjKey(-3);
+ if (!obj.length)
+ return;
+
+ try
+ {
+ var completer = obj[0][0][func].liberatorCompleter;
+ }
+ catch (e) {}
+ if (!completer)
+ completer = this.completers[func];
+ if (!completer)
+ return;
+
+ // Split up the arguments
+ let prev = get(-2)[OFFSET];
+ let args = [];
+ for (let [i, idx] in Iterator(get(-2)[COMMA]))
+ {
+ let arg = str.substring(prev + 1, idx);
+ prev = idx;
+ args.__defineGetter__(i, function () self.eval(ret));
+ }
+ let key = getKey();
+ args.push(key + string);
+
+ compl = function (context, obj)
+ {
+ let res = completer.call(self, context, func, obj, args);
+ if (res)
+ context.completions = res;
+ }
+
+ obj[0][1] += "." + func + "(... [" + args.length + "]";
+ return complete.call(this, obj, key, compl, string, last);
+ }
+
+ // In a string that's not an obj key or a function arg.
+ // Nothing to do.
+ return;
+ }
+
+ /*
+ * str = "foo.bar.baz"
+ * obj = "foo.bar"
+ * key = "baz"
+ *
+ * str = "foo"
+ * obj = [modules, window]
+ * key = "foo"
+ */
+
+ let [offset, obj, key] = getObjKey(-1);
+
+ // Wait for a keypress before completing the default objects.
+ if (!this.context.tabPressed && key == "" && obj.length > 1)
+ {
+ this.context.waitingForTab = true;
+ this.context.message = "Waiting for key press";
+ return;
+ }
+
+ if (!/^(?:\w[\w\d]*)?$/.test(key))
+ return; /* Not a word. Forget it. Can this even happen? */
+
+ top[OFFSET] = offset;
+ return complete.call(this, obj, key);
+ }
+ };
+ let javascript = new Javascript();
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ let self = {
+
+ setFunctionCompleter: function setFunctionCompleter(funcs, completers)
+ {
+ funcs = Array.concat(funcs);
+ for (let [,func] in Iterator(funcs))
+ {
+ func.liberatorCompleter = function liberatorCompleter(context, func, obj, args) {
+ let completer = completers[args.length - 1];
+ if (!completer)
+ return [];
+ return completer.call(this, context, obj, args);
+ };
+ }
+ },
+
+ // FIXME
+ _runCompleter: function _runCompleter(name, filter, maxItems)
+ {
+ let context = CompletionContext(filter);
+ context.maxItems = maxItems;
+ let res = context.fork.apply(context, ["run", 0, this, name].concat(Array.slice(arguments, 3)));
+ if (res) // FIXME
+ return { items: res.map(function (i) ({ item: i })) };
+ context.wait(true);
+ return context.allItems;
+ },
+
+ runCompleter: function runCompleter(name, filter, maxItems)
+ {
+ return this._runCompleter.apply(this, Array.slice(arguments))
+ .items.map(function (i) i.item);
+ },
+
+ // cancel any ongoing search
+ cancel: function cancel()
+ {
+ if (completionService)
+ completionService.stopSearch();
+ },
+
+ // generic helper function which checks if the given "items" array pass "filter"
+ // items must be an array of strings
+ match: function match(items, filter, caseSensitive)
+ {
+ if (typeof filter != "string" || !items)
+ return false;
+
+ var itemsStr = items.join(" ");
+ if (!caseSensitive)
+ {
+ filter = filter.toLowerCase();
+ itemsStr = itemsStr.toLowerCase();
+ }
+
+ return filter.split(/\s+/).every(function strIndex(str) itemsStr.indexOf(str) > -1);
+ },
+
+ listCompleter: function listCompleter(name, filter, maxItems)
+ {
+ let context = CompletionContext(filter || "");
+ context.maxItems = maxItems;
+ context.fork.apply(context, ["list", 0, completion, name].concat(Array.slice(arguments, 3)));
+ context = context.contexts["/list"];
+ context.wait();
+
+ let list = template.generic(
+ <div highlight="Completions">
+ { 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);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// COMPLETION TYPES ////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ autocmdEvent: function autocmdEvent(context)
+ {
+ context.completions = config.autocommands;
+ },
+
+ bookmark: function bookmark(context, tags)
+ {
+ context.title = ["Bookmark", "Title"];
+ context.format = bookmarks.format;
+ // 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);
+ completion.urls(context, tags);
+ },
+
+ buffer: function buffer(context)
+ {
+ filter = context.filter.toLowerCase();
+ context.title = ["Buffer", "URL"];
+ context.keys = { text: "text", description: "url", icon: "icon" };
+ let process = context.process[0];
+ context.process = [function (item)
+ <>
+ <span highlight="Indicator" style="display: inline-block; width: 1.5em; text-align: center">{item.item.indicator}</span>
+ { process.call(this, item) }
+ </>];
+
+ context.completions = util.map(tabs.browsers, function ([i, browser]) {
+ if (i == tabs.index())
+ indicator = "%"
+ else if (i == tabs.index(tabs.alternate))
+ indicator = "#";
+ else
+ indicator = " ";
+
+ let tab = tabs.getTab(i);
+ i = i + 1;
+ let url = browser.contentDocument.location.href;
+
+ return {
+ text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url],
+ url: url,
+ indicator: indicator,
+ icon: tab.image || DEFAULT_FAVICON
+ };
+ });
+ },
+
+ colorScheme: function colorScheme(context)
+ {
+ options.get("runtimepath").values.forEach(function (path) {
+ context.fork(path, 0, null, function (context) {
+ context.filter = path + "/colors/" + context.filter;
+ completion.file(context, true);
+ context.title = [path + "/colors/"];
+ context.quote = function (text) text.replace(/\.vimp$/, "");
+ });
+ });
+ },
+
+ command: function command(context)
+ {
+ context.title = ["Command"];
+ context.anchored = true;
+ context.keys = { text: "longNames", description: "description" };
+ context.completions = [k for (k in commands)];
+ },
+
+ dialog: function dialog(context)
+ {
+ context.title = ["Dialog"];
+ context.completions = config.dialogs;
+ },
+
+ directory: function directory(context, tail)
+ {
+ this.file(context, tail);
+ context.filters.push(function (item) this.getKey(item, "description") == "Directory");
+ },
+
+ environment: function environment(context)
+ {
+ let command = liberator.has("Win32") ? "set" : "env";
+ let lines = io.system(command).split("\n");
+ lines.pop();
+
+ context.title = ["Environment Variable", "Value"];
+ context.generate = function () lines.map(function (line) (line.match(/([^=]+)=(.+)/) || []).slice(1));
+ },
+
+ // provides completions for ex commands, including their arguments
+ ex: function ex(context)
+ {
+ // if there is no space between the command name and the cursor
+ // then get completions of the command name
+ let [count, cmd, bang, args] = commands.parseCommand(context.filter);
+ let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || [];
+ context.advance(prefix.length)
+ if (!junk)
+ return context.fork("", 0, this, "command");
+
+ // dynamically get completions as specified with the command's completer function
+ let command = commands.get(cmd);
+ if (!command)
+ {
+ context.highlight(0, cmd.length, "SPELLCHECK");
+ return;
+ }
+
+ [prefix] = context.filter.match(/^(?:\w*[\s!]|!)\s*/);
+ let cmdContext = context.fork(cmd, prefix.length);
+ let argContext = context.fork("args", prefix.length);
+ args = command.parseArgs(cmdContext.filter, argContext);
+ if (args)
+ {
+ // FIXME: Move to parseCommand
+ args.count = count;
+ args.bang = bang;
+ if (!args.completeOpt && command.completer)
+ {
+ cmdContext.advance(args.completeStart);
+ cmdContext.quote = args.quote;
+ cmdContext.filter = args.completeFilter;
+ let compObject = command.completer.call(command, cmdContext, args);
+ if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects
+ compObject = { start: compObject[0], items: compObject[1] };
+ if (compObject != null)
+ {
+ cmdContext.advance(compObject.start);
+ cmdContext.filterFunc = null;
+ cmdContext.completions = compObject.items;
+ }
+ }
+ context.updateAsync = true;
+ }
+ },
+
+ // TODO: support file:// and \ or / path separators on both platforms
+ // if "tail" is true, only return names without any directory components
+ file: function file(context, tail)
+ {
+ let [dir] = context.filter.match(/^(?:.*[\/\\])?/);
+ // dir == "" is expanded inside readDirectory to the current dir
+
+ context.title = ["Path", "Type"];
+ if (tail)
+ context.advance(dir.length);
+ context.keys = { text: 0, description: 1, icon: 2 };
+ context.anchored = true;
+ context.background = true;
+ context.key = dir;
+ context.generate = function generate_file()
+ {
+ context.cache.dir = dir;
+
+ try
+ {
+ let files = io.readDirectory(dir);
+
+ if (options["wildignore"])
+ {
+ let wigRegexp = RegExp("(^" + options["wildignore"].replace(",", "|", "g") + ")$");
+ files = files.filter(function (f) f.isDirectory() || !wigRegexp.test(f.leafName))
+ }
+
+ return files.map(
+ function (file) [tail ? file.leafName : dir + file.leafName,
+ file.isDirectory() ? "Directory" : "File",
+ file.isDirectory() ? "resource://gre/res/html/folder.png"
+ : "moz-icon://" + file.leafName]
+ );
+ }
+ catch (e) {}
+ return [];
+ };
+ },
+
+ help: function help(context)
+ {
+ context.title = ["Help"];
+ context.generate = function ()
+ {
+ let res = config.helpFiles.map(function (file) {
+ let resp = util.httpGet("chrome://liberator/locale/" + file);
+ if (!resp)
+ return [];
+ let doc = resp.responseXML;
+ return Array.map(doc.getElementsByClassName("tag"),
+ function (elem) [elem.textContent, file]);
+ });
+ return util.Array.flatten(res);
+ }
+ },
+
+ history: function _history(context, maxItems)
+ {
+ context.format = history.format;
+ context.title = ["History"]
+ context.compare = null;
+ //context.background = true;
+ if (context.maxItems == null)
+ context.maxItems = 100;
+ context.regenerate = true;
+ context.generate = function () history.get(context.filter, this.maxItems);
+ },
+
+ get javascriptCompleter() javascript,
+
+ javascript: function _javascript(context) javascript.complete(context),
+
+ location: function location(context)
+ {
+ if (!completionService)
+ return
+ context.title = ["Smart Completions"];
+ context.keys.icon = 2;
+ context.incomplete = true;
+ context.hasItems = context.completions.length > 0; // XXX
+ context.filterFunc = null;
+ context.compare = null;
+ let timer = new util.Timer(50, 100, function (result) {
+ context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING;
+ context.completions = [
+ [result.getValueAt(i), result.getCommentAt(i), result.getImageAt(i)]
+ for (i in util.range(0, result.matchCount))
+ ];
+ });
+ completionService.stopSearch();
+ completionService.startSearch(context.filter, "", context.result, {
+ onSearchResult: function onSearchResult(search, result) {
+ context.result = result;
+ timer.tell(result);
+ if (result.searchResult <= result.RESULT_SUCCESS)
+ timer.flush();
+ }
+ });
+ },
+
+ macro: function macro(context)
+ {
+ context.title = ["Macro", "Keys"];
+ context.completions = [item for (item in events.getMacros())];
+ },
+
+ menuItem: function menuItem(filter) commands.get("emenu").completer(filter), // XXX
+
+ option: function option(context, scope)
+ {
+ context.title = ["Option"];
+ context.anchored = true;
+ context.keys = { text: "names", description: "description" };
+ context.completions = options;
+ if (scope)
+ context.filters.push(function ({ item: opt }) opt.scope & scope);
+ },
+
+ optionValue: function (context, name, op, curValue)
+ {
+ let opt = options.get(name);
+ let completer = opt.completer;
+ if (!completer)
+ return;
+
+ let curValues = curValue != null ? opt.parseValues(curValue) : opt.values;
+ let newValues = opt.parseValues(context.filter);
+
+ let len = context.filter.length;
+ switch (opt.type)
+ {
+ case "boolean":
+ if (!completer)
+ completer = function () [["true", ""], ["false", ""]];
+ break;
+ case "stringlist":
+ len = newValues.pop().length;
+ break;
+ case "charlist":
+ len = 0;
+ break;
+ }
+ // TODO: Highlight when invalid
+ context.advance(context.filter.length - len);
+
+ context.title = ["Option Value"];
+ let completions = completer(context);
+ if (!completions)
+ return;
+ /* Not vim compatible, but is a significant enough improvement
+ * that it's worth breaking compatibility.
+ */
+ if (newValues instanceof Array)
+ {
+ completions = completions.filter(function (val) newValues.indexOf(val[0]) == -1);
+ switch (op)
+ {
+ case "+":
+ completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1);
+ break;
+ case "-":
+ completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1);
+ break;
+ }
+ }
+ context.completions = completions;
+ },
+
+ preference: function preference(context)
+ {
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ context.title = ["Firefox Preference", "Value"];
+ context.keys = { text: function (item) item, description: function (item) options.getPref(item) };
+ context.completions = prefs.getChildList("", { value: 0 });
+ },
+
+ search: function search(context, noSuggest)
+ {
+ let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/);
+ let keywords = bookmarks.getKeywords();
+ let engines = bookmarks.getSearchEngines();
+
+ context.title = ["Search Keywords"];
+ context.anchored = true;
+ context.completions = keywords.concat(engines);
+ context.keys = { text: 0, description: 1, icon: 2 };
+
+ if (!space || noSuggest)
+ return;
+
+ context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest",
+ keyword, true);
+
+ let item = keywords.filter(function (k) k.keyword == keyword)[0];
+ if (item && item.url.indexOf("%s") > -1)
+ context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
+ context.format = history.format;
+ context.title = [keyword + " Quick Search"];
+ context.anchored = true;
+ context.background = true;
+ context.compare = null;
+ context.generate = function () {
+ let [begin, end] = item.url.split("%s");
+
+ return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) {
+ let rest = item.url.length - end.length;
+ let query = item.url.substring(begin.length, rest);
+ if (item.url.substr(rest) == end && query.indexOf("&") == -1)
+ {
+ item.url = decodeURIComponent(query);
+ return item;
+ }
+ }).filter(util.identity);
+ };
+ });
+ },
+
+ searchEngineSuggest: function searchEngineSuggest(context, engineAliases, kludge)
+ {
+ if (!context.filter)
+ return;
+
+ let ss = Components.classes["@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsIBrowserSearchService);
+ let engineList = (engineAliases || options["suggestengines"] || "google").split(",");
+
+ let completions = [];
+ engineList.forEach(function (name) {
+ let engine = ss.getEngineByAlias(name);
+ if (!engine)
+ return;
+ let [,word] = /^\s*(\S+)/.exec(context.filter) || [];
+ if (!kludge && word == name) // FIXME: Check for matching keywords
+ return;
+ let ctxt = context.fork(name, 0);
+
+ ctxt.title = [engine.description + " Suggestions"];
+ ctxt.compare = null;
+ ctxt.incomplete = true;
+ bookmarks.getSuggestions(name, ctxt.filter, function (compl) {
+ ctxt.incomplete = false;
+ ctxt.completions = compl;
+ });
+ });
+ },
+
+ shellCommand: function shellCommand(context)
+ {
+ context.title = ["Shell Command", "Path"];
+ context.generate = function ()
+ {
+ const environmentService = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+
+ let dirNames = environmentService.get("PATH").split(RegExp(liberator.has("Win32") ? ";" : ":"));
+ let commands = [];
+
+ for (let [,dirName] in Iterator(dirNames))
+ {
+ let dir = io.getFile(dirName);
+ if (dir.exists() && dir.isDirectory())
+ {
+ commands.push([[file.leafName, dir.path] for ([i, file] in Iterator(io.readDirectory(dir)))
+ if (file.isFile() && file.isExecutable())]);
+ }
+ }
+
+ return util.Array.flatten(commands);
+ }
+ },
+
+ sidebar: function sidebar(context)
+ {
+ let menu = document.getElementById("viewSidebarMenu");
+ context.title = ["Sidebar Panel"];
+ context.completions = Array.map(menu.childNodes, function (n) [n.label, ""]);
+ },
+
+ alternateStylesheet: function alternateStylesheet(context)
+ {
+ context.title = ["Stylesheet", "Location"];
+ context.keys = { text: "title", description: function (item) item.href };
+
+ // unify split style sheets
+ let completions = buffer.alternateStyleSheets;
+ completions.forEach(function (stylesheet) {
+ stylesheet.href = stylesheet.href || "inline";
+ completions = completions.filter(function (sheet) {
+ if (stylesheet.title == sheet.title && stylesheet != sheet)
+ {
+ stylesheet.href += ", " + sheet.href;
+ return false;
+ }
+ return true;
+ });
+ });
+ context.completions = completions;
+ },
+
+ // filter a list of urls
+ //
+ // may consist of search engines, filenames, bookmarks and history,
+ // depending on the 'complete' option
+ // if the 'complete' argument is passed like "h", it temporarily overrides the complete option
+ url: function url(context, complete)
+ {
+ var numLocationCompletions = 0; // how many async completions did we already return to the caller?
+ var start = 0;
+ var skip = context.filter.match("^.*" + options["urlseparator"]); // start after the last 'urlseparator'
+ if (skip)
+ context.advance(skip[0].length);
+
+ // Will, and should, throw an error if !(c in opts)
+ Array.forEach(complete || options["complete"],
+ function (c) context.fork(c, 0, completion, completion.urlCompleters[c].completer));
+ },
+
+ urlCompleters: {},
+
+ addUrlCompleter: function addUrlCompleter(opt)
+ {
+ this.urlCompleters[opt] = UrlCompleter.apply(null, Array.slice(arguments));
+ },
+
+ urls: function (context, tags)
+ {
+ let compare = String.localeCompare;
+ let contains = String.indexOf
+ if (context.ignoreCase)
+ {
+ compare = util.compareIgnoreCase;
+ contains = function (a, b) a && a.toLowerCase().indexOf(b.toLowerCase()) > -1;
+ }
+
+ if (tags)
+ context.filters.push(function (item) tags.
+ every(function (tag) (context.getKey(item, "tags") || []).
+ some(function (t) !compare(tag, t))));
+
+ if (!context.title)
+ context.title = ["URL", "Title"];
+
+ context.fork("additional", 0, this, function (context) {
+ context.title[0] += " (additional)";
+ context.filter = context.parent.filter; // FIXME
+ context.completions = context.parent.completions;
+ // For items whose URL doesn't exactly match the filter,
+ // accept them if all tokens match either the URL or the title.
+ // Filter out all directly matching strings.
+ let match = context.filters[0];
+ context.filters[0] = function (item) !match.call(this, item);
+ // and all that don't match the tokens.
+ let tokens = context.filter.split(/\s+/);
+ context.filters.push(function (item) tokens.every(
+ function (tok) contains(context.getKey(item, "url"), tok) ||
+ contains(context.getKey(item, "title"), tok)));
+
+ let re = RegExp(tokens.filter(util.identity).map(util.escapeRegex).join("|"), "g");
+ function highlight(item, text, i) process[i].call(this, item, template.highlightRegexp(text, re));
+ let process = [template.icon, function (item, k) k];
+ context.process = [
+ function (item, text) highlight.call(this, item, item.text, 0),
+ function (item, text) highlight.call(this, item, text, 1)
+ ];
+ });
+ },
+
+ userCommand: function userCommand(context)
+ {
+ context.title = ["User Command", "Definition"];
+ context.keys = { text: "name", description: "replacementText" };
+ context.completions = commands.getUserCommands();
+ },
+
+ userMapping: function userMapping(context, args, modes)
+ {
+ if (args.completeArg == 0)
+ {
+ let maps = [[m.names[0], ""] for (m in mappings.getUserIterator(modes))];
+ context.completions = maps;
+ }
+ }
+ // }}}
+ };
+
+ const UrlCompleter = new Struct("name", "description", "completer");
+ self.addUrlCompleter("S", "Suggest engines", self.searchEngineSuggest);
+ self.addUrlCompleter("b", "Bookmarks", self.bookmark);
+ self.addUrlCompleter("h", "History", self.history);
+ self.addUrlCompleter("f", "Local files", self.file);
+ self.addUrlCompleter("l", "Firefox location bar entries (bookmarks and history sorted in an intelligent way)", self.location);
+ self.addUrlCompleter("s", "Search engines and keyword URLs", self.search);
+
+ return self;
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/editor.js b/common/content/editor.js
new file mode 100644
index 00000000..af52c089
--- /dev/null
+++ b/common/content/editor.js
@@ -0,0 +1,1128 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// command names taken from:
+// http://developer.mozilla.org/en/docs/Editor_Embedding_Guide
+
+function Editor() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ // store our last search with f, F, t or T
+ var lastFindChar = null;
+ var lastFindCharFunc = null;
+ var abbrev = {}; // abbrev["lhr"][0]["{i,c,!}","rhs"]
+
+ function getEditor()
+ {
+ return window.document.commandDispatcher.focusedElement;
+ }
+
+ function getController()
+ {
+ var ed = getEditor();
+ if (!ed || !ed.controllers)
+ return null;
+
+ return ed.controllers.getControllerForCommand("cmd_beginLine");
+ }
+
+ function selectPreviousLine()
+ {
+ editor.executeCommand("cmd_selectLinePrevious");
+ if ((modes.extended & modes.LINE) && !editor.selectedText())
+ editor.executeCommand("cmd_selectLinePrevious");
+ }
+ function selectNextLine()
+ {
+ editor.executeCommand("cmd_selectLineNext");
+ if ((modes.extended & modes.LINE) && !editor.selectedText())
+ editor.executeCommand("cmd_selectLineNext");
+ }
+
+ // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXTAREA mode
+ function addMovementMap(keys, hasCount, caretModeMethod, caretModeArg, textareaCommand, visualTextareaCommand)
+ {
+ var extraInfo = {};
+ if (hasCount)
+ extraInfo.flags = Mappings.flags.COUNT;
+
+ mappings.add([modes.CARET], keys, "",
+ function (count)
+ {
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ var controller = getBrowser().docShell
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsISelectionDisplay)
+ .QueryInterface(Components.interfaces.nsISelectionController);
+
+ while (count--)
+ controller[caretModeMethod](caretModeArg, false);
+ },
+ extraInfo);
+
+ mappings.add([modes.VISUAL], keys, "",
+ function (count)
+ {
+ if (typeof count != "number" || count < 1 || !hasCount)
+ count = 1;
+
+ var controller = getBrowser().docShell
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsISelectionDisplay)
+ .QueryInterface(Components.interfaces.nsISelectionController);
+
+ while (count--)
+ {
+ if (modes.extended & modes.TEXTAREA)
+ {
+ if (typeof visualTextareaCommand == "function")
+ visualTextareaCommand();
+ else
+ editor.executeCommand(visualTextareaCommand);
+ }
+ else
+ controller[caretModeMethod](caretModeArg, true);
+ }
+ },
+ extraInfo);
+
+ mappings.add([modes.TEXTAREA], keys, "",
+ function (count)
+ {
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ editor.executeCommand(textareaCommand, count);
+ },
+ extraInfo);
+ }
+
+ // add mappings for commands like i,a,s,c,etc. in TEXTAREA mode
+ function addBeginInsertModeMap(keys, commands)
+ {
+ mappings.add([modes.TEXTAREA], keys, "",
+ function (count)
+ {
+ commands.forEach(function (cmd)
+ editor.executeCommand(cmd, 1));
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ });
+ }
+
+ function addMotionMap(key)
+ {
+ mappings.add([modes.TEXTAREA], [key],
+ "Motion command",
+ function (motion, count) { editor.executeCommandWithMotion(key, motion, count); },
+ { flags: Mappings.flags.MOTION | Mappings.flags.COUNT });
+ }
+
+ // For the record, some of this code I've just finished throwing
+ // away makes me want to pull someone else's hair out. --Kris
+ function abbrevs()
+ {
+ for (let [lhs, abbr] in Iterator(abbrev))
+ for (let [,rhs] in Iterator(abbr))
+ yield [lhs, rhs];
+ }
+
+ // mode = "i" -> add :iabbrev, :iabclear and :iunabbrev commands
+ function addAbbreviationCommands(ch, modeDescription)
+ {
+ var mode = ch || "!";
+ modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
+
+ commands.add([ch ? ch + "a[bbrev]" : "ab[breviate]"],
+ "Abbreviate a key sequence" + modeDescription,
+ function (args)
+ {
+ let [lhs, rhs] = args;
+ if (rhs)
+ editor.addAbbreviation(mode, lhs, rhs);
+ else
+ editor.listAbbreviations(mode, lhs || "");
+ },
+ {
+ literal: 1,
+ serial: function () [
+ {
+ command: this.name,
+ arguments: [lhs],
+ literalArg: abbr[1]
+ }
+ for ([lhs, abbr] in abbrevs())
+ if (abbr[0] == mode)
+ ]
+ });
+
+ commands.add([ch ? ch + "una[bbrev]" : "una[bbreviate]"],
+ "Remove an abbreviation" + modeDescription,
+ function (args) { editor.removeAbbreviation(mode, args.string); });
+
+ commands.add([ch + "abc[lear]"],
+ "Remove all abbreviations" + modeDescription,
+ function () { editor.removeAllAbbreviations(mode); },
+ { argCount: "0" });
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["editor"],
+ "Set the external text editor",
+ "string", "gvim -f");
+
+ options.add(["insertmode", "im"],
+ "Use Insert mode as the default for text areas",
+ "boolean", true);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var myModes = [modes.INSERT, modes.COMMAND_LINE];
+
+ /* KEYS COUNT CARET TEXTAREA VISUAL_TEXTAREA */
+ addMovementMap(["k", "<Up>"], true, "lineMove", false, "cmd_linePrevious", selectPreviousLine);
+ addMovementMap(["j", "<Down>", "<Return>"], true, "lineMove", true, "cmd_lineNext", selectNextLine);
+ addMovementMap(["h", "<Left>", "<BS>"], true, "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious");
+ addMovementMap(["l", "<Right>", "<Space>"], true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext");
+ addMovementMap(["b", "B", "<C-Left>"], true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious");
+ addMovementMap(["w", "W", "e", "<C-Right>"], true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext");
+ addMovementMap(["<C-f>", "<PageDown>"], true, "pageMove", true, "cmd_movePageDown", "cmd_selectNextPage");
+ addMovementMap(["<C-b>", "<PageUp>"], true, "pageMove", false, "cmd_movePageUp", "cmd_selectPreviousPage");
+ addMovementMap(["gg", "<C-Home>"], false, "completeMove", false, "cmd_moveTop", "cmd_selectTop");
+ addMovementMap(["G", "<C-End>"], false, "completeMove", true, "cmd_moveBottom", "cmd_selectBottom");
+ addMovementMap(["0", "^", "<Home>"], false, "intraLineMove", false, "cmd_beginLine", "cmd_selectBeginLine");
+ addMovementMap(["$", "<End>"], false, "intraLineMove", true, "cmd_endLine" , "cmd_selectEndLine" );
+
+ addBeginInsertModeMap(["i", "<Insert"], []);
+ addBeginInsertModeMap(["a"], ["cmd_charNext"]);
+ addBeginInsertModeMap(["I", "gI"], ["cmd_beginLine"]);
+ addBeginInsertModeMap(["A"], ["cmd_endLine"]);
+ addBeginInsertModeMap(["s"], ["cmd_deleteCharForward"]);
+ addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"]);
+ addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"]);
+
+ addMotionMap("d"); // delete
+ addMotionMap("c"); // change
+ addMotionMap("y"); // yank
+
+ // insert mode mappings
+ mappings.add(myModes,
+ ["<C-o>", "<C-f>", "<C-g>", "<C-n>", "<C-p>"],
+ "Ignore certain " + config.hostApplication + " key bindings",
+ function () { /*liberator.beep();*/ });
+
+ mappings.add(myModes,
+ ["<C-w>"], "Delete previous word",
+ function () { editor.executeCommand("cmd_deleteWordBackward", 1); });
+
+ mappings.add(myModes,
+ ["<C-u>"], "Delete until beginning of current line",
+ function ()
+ {
+ // broken in FF3, deletes the whole line:
+ // editor.executeCommand("cmd_deleteToBeginningOfLine", 1);
+ editor.executeCommand("cmd_selectBeginLine", 1);
+ if (getController().isCommandEnabled("cmd_delete"))
+ editor.executeCommand("cmd_delete", 1);
+ });
+
+ mappings.add(myModes,
+ ["<C-k>"], "Delete until end of current line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); });
+
+ mappings.add(myModes,
+ ["<C-a>"], "Move cursor to beginning of current line",
+ function () { editor.executeCommand("cmd_beginLine", 1); });
+
+ mappings.add(myModes,
+ ["<C-e>"], "Move cursor to end of current line",
+ function () { editor.executeCommand("cmd_endLine", 1); });
+
+ mappings.add(myModes,
+ ["<C-h>"], "Delete character to the left",
+ function () { editor.executeCommand("cmd_deleteCharBackward", 1); });
+
+ mappings.add(myModes,
+ ["<C-d>"], "Delete character to the right",
+ function () { editor.executeCommand("cmd_deleteCharForward", 1); });
+
+ /*mappings.add(myModes,
+ ["<C-Home>"], "Move cursor to beginning of text field",
+ function () { editor.executeCommand("cmd_moveTop", 1); });
+
+ mappings.add(myModes,
+ ["<C-End>"], "Move cursor to end of text field",
+ function () { editor.executeCommand("cmd_moveBottom", 1); });*/
+
+ mappings.add(myModes,
+ ["<S-Insert>"], "Insert clipboard/selection",
+ function () { editor.pasteClipboard(); });
+
+ mappings.add([modes.INSERT, modes.TEXTAREA, modes.COMPOSE],
+ ["<C-i>"], "Edit text field with an external editor",
+ function () { editor.editFieldExternally(); });
+
+ // FIXME: <esc> does not work correctly
+ mappings.add([modes.INSERT],
+ ["<C-t>"], "Edit text field in vi mode",
+ function () { liberator.mode = modes.TEXTAREA; });
+
+ mappings.add([modes.INSERT],
+ ["<Space>", "<Return>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); },
+ { flags: Mappings.flags.ALLOW_EVENT_ROUTING });
+
+ mappings.add([modes.INSERT],
+ ["<Tab>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); document.commandDispatcher.advanceFocus(); });
+
+ mappings.add([modes.INSERT],
+ ["<C-]>", "<C-5>"], "Expand insert mode abbreviation",
+ function () { editor.expandAbbreviation("i"); });
+
+ // textarea mode
+ mappings.add([modes.TEXTAREA],
+ ["u"], "Undo",
+ function (count)
+ {
+ editor.executeCommand("cmd_undo", count);
+ liberator.mode = modes.TEXTAREA;
+ },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA],
+ ["<C-r>"], "Redo",
+ function (count)
+ {
+ editor.executeCommand("cmd_redo", count);
+ liberator.mode = modes.TEXTAREA;
+ },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA],
+ ["D"], "Delete the characters under the cursor until the end of the line",
+ function () { editor.executeCommand("cmd_deleteToEndOfLine"); });
+
+ mappings.add([modes.TEXTAREA],
+ ["o"], "Open line below current",
+ function (count)
+ {
+ editor.executeCommand("cmd_endLine", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ events.feedkeys("<Return>");
+ });
+
+ mappings.add([modes.TEXTAREA],
+ ["O"], "Open line above current",
+ function (count)
+ {
+ editor.executeCommand("cmd_beginLine", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ events.feedkeys("<Return>");
+ editor.executeCommand("cmd_linePrevious", 1);
+ });
+
+ mappings.add([modes.TEXTAREA],
+ ["X"], "Delete character to the left",
+ function (count) { editor.executeCommand("cmd_deleteCharBackward", count); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA],
+ ["x"], "Delete character to the right",
+ function (count) { editor.executeCommand("cmd_deleteCharForward", count); },
+ { flags: Mappings.flags.COUNT });
+
+ // visual mode
+ mappings.add([modes.CARET, modes.TEXTAREA, modes.VISUAL],
+ ["v"], "Start visual mode",
+ function (count) { modes.set(modes.VISUAL, liberator.mode); });
+
+ mappings.add([modes.TEXTAREA],
+ ["V"], "Start visual line mode",
+ function (count)
+ {
+ modes.set(modes.VISUAL, modes.TEXTAREA | modes.LINE);
+ editor.executeCommand("cmd_beginLine", 1);
+ editor.executeCommand("cmd_selectLineNext", 1);
+ });
+
+ mappings.add([modes.VISUAL],
+ ["c", "s"], "Change selected text",
+ function (count)
+ {
+ if (modes.extended & modes.TEXTAREA)
+ {
+ editor.executeCommand("cmd_cut");
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ }
+ else
+ liberator.beep();
+ });
+
+ mappings.add([modes.VISUAL],
+ ["d"], "Delete selected text",
+ function (count)
+ {
+ if (modes.extended & modes.TEXTAREA)
+ {
+ editor.executeCommand("cmd_cut");
+ modes.set(modes.TEXTAREA);
+ }
+ else
+ liberator.beep();
+ });
+
+ mappings.add([modes.VISUAL],
+ ["y"], "Yank selected text",
+ function (count)
+ {
+ if (modes.extended & modes.TEXTAREA)
+ {
+ editor.executeCommand("cmd_copy");
+ modes.set(modes.TEXTAREA);
+ }
+ else
+ {
+ var sel = window.content.document.getSelection();
+ if (sel)
+ util.copyToClipboard(sel, true);
+ else
+ liberator.beep();
+ }
+ });
+
+ mappings.add([modes.VISUAL, modes.TEXTAREA],
+ ["p"], "Paste clipboard contents",
+ function (count)
+ {
+ if (!(modes.extended & modes.CARET))
+ {
+ if (!count) count = 1;
+ while (count--)
+ editor.executeCommand("cmd_paste");
+ liberator.mode = modes.TEXTAREA;
+ }
+ else
+ liberator.beep();
+ });
+
+ // finding characters
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["f"], "Move to a character on the current line after the cursor",
+ function (count, arg)
+ {
+ var pos = editor.findCharForward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos, true, liberator.mode == modes.VISUAL);
+ },
+ { flags: Mappings.flags.ARGUMENT | Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["F"], "Move to a charater on the current line before the cursor",
+ function (count, arg)
+ {
+ var pos = editor.findCharBackward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos, false, liberator.mode == modes.VISUAL);
+ },
+ { flags: Mappings.flags.ARGUMENT | Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["t"], "Move before a character on the current line",
+ function (count, arg)
+ {
+ var pos = editor.findCharForward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos - 1, true, liberator.mode == modes.VISUAL);
+ },
+ { flags: Mappings.flags.ARGUMENT | Mappings.flags.COUNT });
+
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["T"], "Move before a character on the current line, backwards",
+ function (count, arg)
+ {
+ var pos = editor.findCharBackward(arg, count);
+ if (pos >= 0)
+ editor.moveToPosition(pos + 1, false, liberator.mode == modes.VISUAL);
+ },
+ { flags: Mappings.flags.ARGUMENT | Mappings.flags.COUNT });
+
+ // textarea and visual mode
+ mappings.add([modes.TEXTAREA, modes.VISUAL],
+ ["~"], "Switch case of the character under the cursor and move the cursor to the right",
+ function (count)
+ {
+ if (modes.main == modes.VISUAL)
+ {
+ count = getEditor().selectionEnd - getEditor().selectionStart;
+ }
+ if (typeof count != "number" || count < 1)
+ {
+ count = 1;
+ }
+
+ while (count-- > 0)
+ {
+ var text = getEditor().value;
+ var pos = getEditor().selectionStart;
+ if (pos >= text.length)
+ {
+ liberator.beep();
+ return;
+ }
+ var chr = text[pos];
+ getEditor().value = text.substring(0, pos) +
+ (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) +
+ text.substring(pos + 1);
+ editor.moveToPosition(pos + 1, true, false);
+ }
+ modes.set(modes.TEXTAREA);
+ },
+ { flags: Mappings.flags.COUNT });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ addAbbreviationCommands("", "");
+ addAbbreviationCommands("i", "insert");
+ addAbbreviationCommands("c", "command line");
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ line: function ()
+ {
+ var line = 1;
+ var text = getEditor().value;
+ for (let i = 0; i < getEditor().selectionStart; i++)
+ if (text[i] == "\n")
+ line++;
+ return line;
+ },
+
+ col: function ()
+ {
+ var col = 1;
+ var text = getEditor().value;
+ for (let i = 0; i < getEditor().selectionStart; i++)
+ {
+ col++;
+ if (text[i] == "\n")
+ col = 1;
+ }
+ return col;
+ },
+
+ unselectText: function ()
+ {
+ var elt = window.document.commandDispatcher.focusedElement;
+ if (elt && elt.selectionEnd)
+ elt.selectionEnd = elt.selectionStart;
+ },
+
+ selectedText: function ()
+ {
+ var text = getEditor().value;
+ return text.substring(getEditor().selectionStart, getEditor().selectionEnd);
+ },
+
+ pasteClipboard: function ()
+ {
+ var elt = window.document.commandDispatcher.focusedElement;
+
+ if (elt.setSelectionRange && util.readFromClipboard())
+ // readFromClipboard would return 'undefined' if not checked
+ // dunno about .setSelectionRange
+ {
+ var rangeStart = elt.selectionStart; // caret position
+ var rangeEnd = elt.selectionEnd;
+ var tempStr1 = elt.value.substring(0, rangeStart);
+ var tempStr2 = util.readFromClipboard();
+ var tempStr3 = elt.value.substring(rangeEnd);
+ elt.value = tempStr1 + tempStr2 + tempStr3;
+ elt.selectionStart = rangeStart + tempStr2.length;
+ elt.selectionEnd = elt.selectionStart;
+ }
+ },
+
+ // count is optional, defaults to 1
+ executeCommand: function (cmd, count)
+ {
+ var controller = getController();
+ if (!controller || !controller.supportsCommand(cmd) || !controller.isCommandEnabled(cmd))
+ {
+ liberator.beep();
+ return false;
+ }
+
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ var didCommand = false;
+ while (count--)
+ {
+ // some commands need this try/catch workaround, because a cmd_charPrevious triggered
+ // at the beginning of the textarea, would hang the doCommand()
+ // good thing is, we need this code anyway for proper beeping
+ try
+ {
+ controller.doCommand(cmd);
+ didCommand = true;
+ }
+ catch (e)
+ {
+ if (!didCommand)
+ liberator.beep();
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ // cmd = y, d, c
+ // motion = b, 0, gg, G, etc.
+ executeCommandWithMotion: function (cmd, motion, count)
+ {
+ if (!typeof count == "number" || count < 1)
+ count = 1;
+
+ if (cmd == motion)
+ {
+ motion = "j";
+ count--;
+ }
+
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+
+ switch (motion)
+ {
+ case "j":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_selectLineNext", count+1);
+ break;
+ case "k":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_lineNext", 1);
+ this.executeCommand("cmd_selectLinePrevious", count+1);
+ break;
+ case "h":
+ this.executeCommand("cmd_selectCharPrevious", count);
+ break;
+ case "l":
+ this.executeCommand("cmd_selectCharNext", count);
+ break;
+ case "e":
+ case "w":
+ this.executeCommand("cmd_selectWordNext", count);
+ break;
+ case "b":
+ this.executeCommand("cmd_selectWordPrevious", count);
+ break;
+ case "0":
+ case "^":
+ this.executeCommand("cmd_selectBeginLine", 1);
+ break;
+ case "$":
+ this.executeCommand("cmd_selectEndLine", 1);
+ break;
+ case "gg":
+ this.executeCommand("cmd_endLine", 1);
+ this.executeCommand("cmd_selectTop", 1);
+ this.executeCommand("cmd_selectBeginLine", 1);
+ break;
+ case "G":
+ this.executeCommand("cmd_beginLine", 1);
+ this.executeCommand("cmd_selectBottom", 1);
+ this.executeCommand("cmd_selectEndLine", 1);
+ break;
+
+ default:
+ liberator.beep();
+ return false;
+ }
+
+ switch (cmd)
+ {
+ case "d":
+ this.executeCommand("cmd_delete", 1);
+ // need to reset the mode as the visual selection changes it
+ modes.main = modes.TEXTAREA;
+ break;
+ case "c":
+ this.executeCommand("cmd_delete", 1);
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ break;
+ case "y":
+ this.executeCommand("cmd_copy", 1);
+ this.unselectText();
+ break;
+
+ default:
+ liberator.beep();
+ return false;
+ }
+ return true;
+ },
+
+ // This function will move/select up to given "pos"
+ // Simple setSelectionRange() would be better, but we want to maintain the correct
+ // order of selectionStart/End (a firefox bug always makes selectionStart <= selectionEnd)
+ // Use only for small movements!
+ moveToPosition: function (pos, forward, select)
+ {
+ if (!select)
+ {
+ getEditor().setSelectionRange(pos, pos);
+ return;
+ }
+
+ if (forward)
+ {
+ if (pos <= getEditor().selectionEnd || pos > getEditor().value.length)
+ return false;
+
+ do // TODO: test code for endless loops
+ {
+ this.executeCommand("cmd_selectCharNext", 1);
+ }
+ while (getEditor().selectionEnd != pos);
+ }
+ else
+ {
+ if (pos >= getEditor().selectionStart || pos < 0)
+ return false;
+
+ do // TODO: test code for endless loops
+ {
+ this.executeCommand("cmd_selectCharPrevious", 1);
+ }
+ while (getEditor().selectionStart != pos);
+ }
+ },
+
+ // returns the position of char
+ findCharForward: function (ch, count)
+ {
+ if (!getEditor())
+ return -1;
+
+ lastFindChar = ch;
+ lastFindCharFunc = this.findCharForward;
+
+ var text = getEditor().value;
+ if (!typeof count == "number" || count < 1)
+ count = 1;
+
+ for (let i = getEditor().selectionEnd + 1; i < text.length; i++)
+ {
+ if (text[i] == "\n")
+ break;
+ if (text[i] == ch)
+ count--;
+ if (count == 0)
+ return i + 1; // always position the cursor after the char
+ }
+
+ liberator.beep();
+ return -1;
+ },
+
+ // returns the position of char
+ findCharBackward: function (ch, count)
+ {
+ if (!getEditor())
+ return -1;
+
+ lastFindChar = ch;
+ lastFindCharFunc = this.findCharBackward;
+
+ var text = getEditor().value;
+ if (!typeof count == "number" || count < 1)
+ count = 1;
+
+ for (let i = getEditor().selectionStart - 1; i >= 0; i--)
+ {
+ if (text[i] == "\n")
+ break;
+ if (text[i] == ch)
+ count--;
+ if (count == 0)
+ return i;
+ }
+
+ liberator.beep();
+ return -1;
+ },
+
+ editFileExternally: function (path)
+ {
+ // TODO: save return value in v:shell_error
+ let args = commands.parseArgs(options["editor"], [], "*", true);
+
+ if (args.length < 1)
+ {
+ liberator.echoerr("No editor specified");
+ return;
+ }
+
+ args.push(path);
+ liberator.callFunctionInThread(null, io.run, args.shift(), args, true);
+ },
+
+ // TODO: clean up with 2 functions for textboxes and currentEditor?
+ editFieldExternally: function ()
+ {
+ if (!options["editor"])
+ return false;
+
+ var textBox = null;
+ if (!(config.isComposeWindow))
+ textBox = document.commandDispatcher.focusedElement;
+
+ var text = ""; // XXX
+ if (textBox)
+ text = textBox.value;
+ else if (typeof GetCurrentEditor == "function") // Thunderbird composer
+ text = GetCurrentEditor().outputToString("text/plain", 2);
+ else
+ return false;
+
+ try
+ {
+ var tmpfile = io.createTempFile();
+ }
+ catch (e)
+ {
+ liberator.echoerr("Could not create temporary file: " + e.message);
+ return false;
+ }
+ try
+ {
+ io.writeFile(tmpfile, text);
+ }
+ catch (e)
+ {
+ liberator.echoerr("Could not write to temporary file " + tmpfile.path + ": " + e.message);
+ return false;
+ }
+
+ if (textBox)
+ {
+ textBox.setAttribute("readonly", "true");
+ var oldBg = textBox.style.backgroundColor;
+ var tmpBg = "yellow";
+ textBox.style.backgroundColor = "#bbbbbb";
+ }
+
+ this.editFileExternally(tmpfile.path);
+
+ if (textBox)
+ textBox.removeAttribute("readonly");
+
+ // if (v:shell_error != 0)
+ // {
+ // tmpBg = "red";
+ // liberator.echoerr("External editor returned with exit code " + retcode);
+ // }
+ // else
+ // {
+ try
+ {
+ var val = io.readFile(tmpfile);
+ if (textBox)
+ textBox.value = val;
+ else
+ {
+ //document.getElementById("content-frame").contentDocument.designMode = "on";
+ var editor = GetCurrentEditor();
+ var wholeDocRange = editor.document.createRange();
+ var rootNode = editor.rootElement.QueryInterface(Components.interfaces.nsIDOMNode);
+ wholeDocRange.selectNodeContents(rootNode);
+ editor.selection.addRange(wholeDocRange);
+ editor.selection.deleteFromDocument();
+ editor.insertText(val);
+ //setTimeout(function () {
+ // document.getElementById("content-frame").contentDocument.designMode = "off";
+ //}, 100);
+ }
+ }
+ catch (e)
+ {
+ tmpBg = "red";
+ liberator.echoerr("Could not read from temporary file " + tmpfile.path + ": " + e.message);
+ }
+ // }
+
+ // blink the textbox after returning
+ if (textBox)
+ {
+ var timeout = 100;
+ var colors = [tmpBg, oldBg, tmpBg, oldBg];
+ (function () {
+ textBox.style.backgroundColor = colors.shift();
+ if (colors.length > 0)
+ setTimeout(arguments.callee, timeout);
+ })();
+ }
+
+ tmpfile.remove(false);
+ return true;
+ },
+
+ // Abbreviations {{{
+
+ // filter is i, c or "!" (insert or command abbreviations or both)
+ listAbbreviations: function (filter, lhs)
+ {
+ if (lhs) // list only that one
+ {
+ if (abbrev[lhs])
+ {
+ for (let [,abbr] in Iterator(abbrev[lhs]))
+ {
+ if (abbr[0] == filter)
+ liberator.echo(abbr[0] + " " + lhs + " " + abbr[1]);
+ // Is it me, or is this clearly very wrong? --Kris
+ return true;
+ }
+ }
+ liberator.echoerr("No abbreviations found");
+ return false;
+ }
+ else // list all (for that filter {i,c,!})
+ {
+ var searchFilter = (filter == "!") ? "!ci"
+ : filter + "!"; // ! -> list all, on c or i ! matches too)
+
+ let list = [[rhs[0], lhs, rhs[1]] for ([lhs, rhs] in abbrevs()) if (searchFilter.indexOf(rhs[0]) > -1)];
+
+ if (!list.length)
+ return liberator.echoerr("No abbreviations found");
+ list = template.tabular(["", "LHS", "RHS"], [], list);
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ },
+
+ // System for adding abbreviations:
+ //
+ // filter == ! delete all, and set first (END)
+ //
+ // if filter == ! remove all and add it as only END
+ //
+ // variant 1: rhs matches anywere in loop
+ //
+ // 1 mod matches anywhere in loop
+ // a) simple replace and
+ // I) (maybe there's another rhs that matches? not possible)
+ // (when there's another item, it's opposite mod with different rhs)
+ // (so do nothing further but END)
+ //
+ // 2 mod does not match
+ // a) the opposite is there -> make a ! and put it as only and END
+ // (b) a ! is there. do nothing END)
+ //
+ // variant 2: rhs matches *no*were in loop and filter is c or i
+ // everykind of current combo is possible to 1 {c,i,!} or two {c and i}
+ //
+ // 1 mod is ! split into two i + c END
+ // 1 not !: opposite mode (first), add/change 'second' and END
+ // 1 not !: same mode (first), overwrite first this END
+ //
+ addAbbreviation: function (filter, lhs, rhs)
+ {
+ if (!abbrev[lhs])
+ {
+ abbrev[lhs] = [];
+ abbrev[lhs][0] = [filter, rhs];
+ return;
+ }
+
+ if (filter == "!")
+ {
+ if (abbrev[lhs][1])
+ abbrev[lhs][1] = "";
+ abbrev[lhs][0] = [filter, rhs];
+ return;
+ }
+
+ for (let i = 0; i < abbrev[lhs].length; i++)
+ {
+ if (abbrev[lhs][i][1] == rhs)
+ {
+ if (abbrev[lhs][i][0] == filter)
+ {
+ abbrev[lhs][i] = [filter, rhs];
+ return;
+ }
+ else
+ {
+ if (abbrev[lhs][i][0] != "!")
+ {
+ if (abbrev[lhs][1])
+ abbrev[lhs][1] = "";
+ abbrev[lhs][0] = ["!", rhs];
+ return;
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
+ }
+
+ if (abbrev[lhs][0][0] == "!")
+ {
+ var tmpOpp = ("i" == filter) ? "c" : "i";
+ abbrev[lhs][1] = [tmpOpp, abbrev[lhs][0][1]];
+ abbrev[lhs][0] = [filter, rhs];
+ return;
+ }
+
+ if (abbrev[lhs][0][0] != filter)
+ abbrev[lhs][1] = [filter, rhs];
+ else
+ abbrev[lhs][0] = [filter, rhs];
+ },
+
+ removeAbbreviation: function (filter, lhs)
+ {
+ if (!lhs)
+ {
+ liberator.echoerr("E474: Invalid argument");
+ return false;
+ }
+
+ if (abbrev[lhs]) // abbrev exists
+ {
+ if (filter == "!")
+ {
+ abbrev[lhs] = "";
+ return true;
+ }
+ else
+ {
+ if (!abbrev[lhs][1]) // only one exists
+ {
+ if (abbrev[lhs][0][0] == "!") // exists as ! -> no 'full' delete
+ {
+ abbrev[lhs][0][0] = (filter == "i") ? "c" : "i"; // ! - i = c; ! - c = i
+ return true;
+ }
+ else if (abbrev[lhs][0][0] == filter)
+ {
+ abbrev[lhs] = "";
+ return true;
+ }
+ }
+ else // two abbrev's exists ( 'i' or 'c' (filter as well))
+ {
+ if (abbrev[lhs][0][0] == "c" && filter == "c")
+ abbrev[lhs][0] = abbrev[lhs][1];
+
+ abbrev[lhs][1] = "";
+
+ return true;
+ }
+ }
+ }
+
+ liberator.echoerr("E24: No such abbreviation");
+ return false;
+ },
+
+ removeAllAbbreviations: function (filter)
+ {
+ if (filter == "!")
+ {
+ abbrev = {};
+ }
+ else
+ {
+ for (let lhs in abbrev)
+ {
+ for (let i = 0; i < abbrev[lhs].length; i++)
+ {
+ if (abbrev[lhs][i][0] == "!" || abbrev[lhs][i][0] == filter)
+ this.removeAbbreviation(filter, lhs);
+ }
+ }
+ }
+ },
+
+ expandAbbreviation: function (filter) // try to find an candidate and replace accordingly
+ {
+ var textbox = getEditor();
+ if (!textbox)
+ return;
+ var text = textbox.value;
+ var currStart = textbox.selectionStart;
+ var currEnd = textbox.selectionEnd;
+ var foundWord = text.substring(0, currStart).replace(/^(.|\n)*?(\S+)$/m, "$2"); // get last word \b word boundary
+ if (!foundWord)
+ return true;
+
+ for (let lhs in abbrev)
+ {
+ for (let i = 0; i < abbrev[lhs].length; i++)
+ {
+ if (lhs == foundWord && (abbrev[lhs][i][0] == filter || abbrev[lhs][i][0] == "!"))
+ {
+ // if found, replace accordingly
+ var len = foundWord.length;
+ var abbrText = abbrev[lhs][i][1];
+ text = text.substring(0, currStart - len) + abbrText + text.substring(currStart);
+ textbox.value = text;
+ textbox.selectionStart = currStart - len + abbrText.length;
+ textbox.selectionEnd = currEnd - len + abbrText.length;
+ break;
+ }
+ }
+ }
+ return true;
+ }
+ //}}}
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/eval.js b/common/content/eval.js
new file mode 100644
index 00000000..14f5fd95
--- /dev/null
+++ b/common/content/eval.js
@@ -0,0 +1,10 @@
+try { __liberator_eval_result = eval(__liberator_eval_string)
+}
+catch (e)
+{
+ __liberator_eval_error = e;
+}
+// Important: The eval statement *must* remain on the first line
+// in order for line numbering in any errors to remain correct.
+//
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/events.js b/common/content/events.js
new file mode 100644
index 00000000..730ec02f
--- /dev/null
+++ b/common/content/events.js
@@ -0,0 +1,1694 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+function AutoCommands() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var store = [];
+
+ function matchAutoCmd(autoCmd, event, regex)
+ {
+ return (!event || autoCmd.event == event) &&
+ (!regex || autoCmd.pattern.source == regex);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["eventignore", "ei"],
+ "List of autocommand event names which should be ignored",
+ "stringlist", "",
+ {
+ completer: function () config.autocommands.concat([["all", "All events"]]),
+ validator: Option.validateCompleter
+ });
+
+ options.add(["focuscontent", "fc"],
+ "Try to stay in normal mode after loading a web page",
+ "boolean", false);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["au[tocmd]"],
+ "Execute commands automatically on events",
+ function (args)
+ {
+ let [event, regex, cmd] = args;
+ let events = null;
+ if (event)
+ {
+ // NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}|
+ let validEvents = config.autocommands.map(function (event) event[0]);
+ validEvents.push("*");
+
+ events = event.split(",");
+ if (!events.every(function (event) validEvents.indexOf(event) >= 0))
+ {
+ liberator.echoerr("E216: No such group or event: " + event);
+ return;
+ }
+ }
+
+ if (cmd) // add new command, possibly removing all others with the same event/pattern
+ {
+ if (args.bang)
+ autocommands.remove(event, regex);
+ autocommands.add(events, regex, cmd);
+ }
+ else
+ {
+ if (event == "*")
+ event = null;
+ if (args.bang)
+ {
+ // TODO: "*" only appears to work in Vim when there is a {group} specified
+ if (args[0] != "*" || regex)
+ autocommands.remove(event, regex); // remove all
+ }
+ else
+ {
+ autocommands.list(event, regex); // list all
+ }
+ }
+ },
+ {
+ bang: true,
+ completer: function (context) completion.autocmdEvent(context),
+ literal: 2
+ });
+
+ // TODO: expand target to all buffers
+ commands.add(["doauto[all]"],
+ "Apply the autocommands matching the specified URL pattern to all buffers",
+ function (args)
+ {
+ commands.get("doautocmd").action.call(this, args);
+ },
+ {
+ completer: function (context) completion.autocmdEvent(context),
+ literal: 0
+ }
+ );
+
+ // TODO: restrict target to current buffer
+ commands.add(["do[autocmd]"],
+ "Apply the autocommands matching the specified URL pattern to the current buffer",
+ function (args)
+ {
+ args = args.string;
+ if (/^\s*$/.test(args))
+ return liberator.echo("No matching autocommands");
+
+ let [, event, url] = args.match(/^(\S+)(?:\s+(\S+))?$/);
+ url = url || buffer.URL;
+
+ let validEvents = config.autocommands.map(function (e) e[0]);
+
+ if (event == "*")
+ {
+ liberator.echoerr("E217: Can't execute autocommands for ALL events");
+ }
+ else if (validEvents.indexOf(event) == -1)
+ {
+ liberator.echoerr("E216: No such group or event: " + args);
+ }
+ else
+ {
+ // TODO: perhaps trigger could return the number of autocmds triggered
+ // TODO: Perhaps this should take -args to pass to the command?
+ if (!autocommands.get(event).some(function (c) c.pattern.test(url)))
+ liberator.echo("No matching autocommands");
+ else
+ autocommands.trigger(event, { url: url });
+ }
+ },
+ {
+ completer: function (context) completion.autocmdEvent(context),
+ literal: 0
+ }
+ );
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ liberator.registerObserver("load_completion", function ()
+ {
+ completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]);
+ });
+
+
+ return {
+
+ __iterator__: function () util.Array.iterator(store),
+
+ add: function (events, regex, cmd)
+ {
+ if (typeof events == "string")
+ {
+ events = events.split(",");
+ liberator.log("DEPRECATED: the events list arg to autocommands.add() should be an array of event names");
+ }
+ events.forEach(function (event)
+ store.push({ event: event, pattern: RegExp(regex), command: cmd }));
+ },
+
+ get: function (event, regex)
+ {
+ return store.filter(function (autoCmd) matchAutoCmd(autoCmd, event, regex));
+ },
+
+ remove: function (event, regex)
+ {
+ store = store.filter(function (autoCmd) !matchAutoCmd(autoCmd, event, regex));
+ },
+
+ list: function (event, regex)
+ {
+ let cmds = {};
+
+ // XXX
+ store.forEach(function (autoCmd) {
+ if (matchAutoCmd(autoCmd, event, regex))
+ {
+ cmds[autoCmd.event] = cmds[autoCmd.event] || [];
+ cmds[autoCmd.event].push(autoCmd);
+ }
+ });
+
+ var list = template.generic(
+ <table>
+ <tr highlight="Title">
+ <td colspan="2">----- Auto Commands -----</td>
+ </tr>
+ {
+ template.map(cmds, function ([event, items])
+ <tr highlight="Title">
+ <td colspan="2">{event}</td>
+ </tr>
+ +
+ template.map(items, function (item)
+ <tr>
+ <td>&#160;{item.pattern.source}</td>
+ <td>{item.command}</td>
+ </tr>))
+ }
+ </table>);
+
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ trigger: function (event, args)
+ {
+ if (options.get("eventignore").has("all", event))
+ return;
+
+ let autoCmds = store.filter(function (autoCmd) autoCmd.event == event);
+
+ liberator.echomsg("Executing " + event + " Auto commands for \"*\"", 8);
+
+ let lastPattern = null;
+
+ let url = args.url || "";
+ for (let [,autoCmd] in Iterator(autoCmds))
+ {
+ if (autoCmd.pattern.test(url))
+ {
+ if (!lastPattern || lastPattern.source != autoCmd.pattern.source)
+ liberator.echomsg("Executing " + event + " Auto commands for \"" + autoCmd.pattern.source + "\"", 8);
+
+ lastPattern = autoCmd.pattern;
+
+ liberator.echomsg("autocommand " + autoCmd.command, 9);
+ liberator.execute(commands.replaceTokens(autoCmd.command, args));
+ }
+ }
+ }
+ };
+ //}}}
+}; //}}}
+
+function Events() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const input = liberator.input;
+
+ var fullscreen = window.fullScreen;
+
+ var inputBufferLength = 0; // count the number of keys in v.input.buffer (can be different from v.input.buffer.length)
+ var skipMap = false; // while feeding the keys (stored in v.input.buffer | no map found) - ignore mappings
+
+ var macros = storage.newMap('macros', true);
+
+ var currentMacro = "";
+ var lastMacro = "";
+
+ try // not every extension has a getBrowser() method
+ {
+ var tabcontainer = getBrowser().mTabContainer;
+ if (tabcontainer) // not every VIM-like extension has a tab container
+ {
+ tabcontainer.addEventListener("TabMove", function (event)
+ {
+ statusline.updateTabCount();
+ }, false);
+ tabcontainer.addEventListener("TabOpen", function (event)
+ {
+ statusline.updateTabCount();
+ }, false);
+ tabcontainer.addEventListener("TabClose", function (event)
+ {
+ statusline.updateTabCount();
+ }, false);
+ tabcontainer.addEventListener("TabSelect", function (event)
+ {
+ // TODO: is all of that necessary?
+ modes.reset();
+ commandline.clear();
+ modes.show();
+ statusline.updateTabCount();
+ tabs.updateSelectionHistory();
+
+ if (options["focuscontent"])
+ setTimeout(function () { liberator.focusContent(true); }, 10); // just make sure, that no widget has focus
+ }, false);
+ }
+
+ getBrowser().addEventListener("DOMContentLoaded", onDOMContentLoaded, true);
+
+ // this adds an event which is is called on each page load, even if the
+ // page is loaded in a background tab
+ getBrowser().addEventListener("load", onPageLoad, true);
+
+ // called when the active document is scrolled
+ getBrowser().addEventListener("scroll", function (event)
+ {
+ statusline.updateBufferPosition();
+ modes.show();
+ }, null);
+ }
+ catch (e) {}
+
+// getBrowser().addEventListener("submit", function (event)
+// {
+// // reset buffer loading state as early as possible, important for macros
+// buffer.loaded = 0;
+// }, null);
+
+ /////////////////////////////////////////////////////////
+ // track if a popup is open or the menubar is active
+ var activeMenubar = false;
+ function enterPopupMode(event)
+ {
+ if (event.originalTarget.localName == "tooltip" || event.originalTarget.id == "liberator-visualbell")
+ return;
+
+ modes.add(modes.MENU);
+ }
+ function exitPopupMode()
+ {
+ // gContextMenu is set to NULL by Firefox, when a context menu is closed
+ if (typeof gContextMenu != "undefined" && gContextMenu == null && !activeMenubar)
+ modes.remove(modes.MENU);
+ }
+ function enterMenuMode()
+ {
+ activeMenubar = true;
+ modes.add(modes.MENU);
+ }
+ function exitMenuMode()
+ {
+ activeMenubar = false;
+ modes.remove(modes.MENU);
+ }
+ window.addEventListener("popupshown", enterPopupMode, true);
+ window.addEventListener("popuphidden", exitPopupMode, true);
+ window.addEventListener("DOMMenuBarActive", enterMenuMode, true);
+ window.addEventListener("DOMMenuBarInactive", exitMenuMode, true);
+ window.addEventListener("resize", onResize, true);
+
+ // window.document.addEventListener("DOMTitleChanged", function (event)
+ // {
+ // liberator.log("titlechanged");
+ // }, null);
+
+ // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
+ // matters, so use that string as the first item, that you
+ // want to refer to within liberator's source code for
+ // comparisons like if (key == "<Esc>") { ... }
+ var keyTable = [
+ [ KeyEvent.DOM_VK_ESCAPE, ["Esc", "Escape"] ],
+ [ KeyEvent.DOM_VK_LEFT_SHIFT, ["<"] ],
+ [ KeyEvent.DOM_VK_RIGHT_SHIFT, [">"] ],
+ [ KeyEvent.DOM_VK_RETURN, ["Return", "CR", "Enter"] ],
+ [ KeyEvent.DOM_VK_TAB, ["Tab"] ],
+ [ KeyEvent.DOM_VK_DELETE, ["Del"] ],
+ [ KeyEvent.DOM_VK_BACK_SPACE, ["BS"] ],
+ [ KeyEvent.DOM_VK_HOME, ["Home"] ],
+ [ KeyEvent.DOM_VK_INSERT, ["Insert", "Ins"] ],
+ [ KeyEvent.DOM_VK_END, ["End"] ],
+ [ KeyEvent.DOM_VK_LEFT, ["Left"] ],
+ [ KeyEvent.DOM_VK_RIGHT, ["Right"] ],
+ [ KeyEvent.DOM_VK_UP, ["Up"] ],
+ [ KeyEvent.DOM_VK_DOWN, ["Down"] ],
+ [ KeyEvent.DOM_VK_PAGE_UP, ["PageUp"] ],
+ [ KeyEvent.DOM_VK_PAGE_DOWN, ["PageDown"] ],
+ [ KeyEvent.DOM_VK_F1, ["F1"] ],
+ [ KeyEvent.DOM_VK_F2, ["F2"] ],
+ [ KeyEvent.DOM_VK_F3, ["F3"] ],
+ [ KeyEvent.DOM_VK_F4, ["F4"] ],
+ [ KeyEvent.DOM_VK_F5, ["F5"] ],
+ [ KeyEvent.DOM_VK_F6, ["F6"] ],
+ [ KeyEvent.DOM_VK_F7, ["F7"] ],
+ [ KeyEvent.DOM_VK_F8, ["F8"] ],
+ [ KeyEvent.DOM_VK_F9, ["F9"] ],
+ [ KeyEvent.DOM_VK_F10, ["F10"] ],
+ [ KeyEvent.DOM_VK_F11, ["F11"] ],
+ [ KeyEvent.DOM_VK_F12, ["F12"] ],
+ [ KeyEvent.DOM_VK_F13, ["F13"] ],
+ [ KeyEvent.DOM_VK_F14, ["F14"] ],
+ [ KeyEvent.DOM_VK_F15, ["F15"] ],
+ [ KeyEvent.DOM_VK_F16, ["F16"] ],
+ [ KeyEvent.DOM_VK_F17, ["F17"] ],
+ [ KeyEvent.DOM_VK_F18, ["F18"] ],
+ [ KeyEvent.DOM_VK_F19, ["F19"] ],
+ [ KeyEvent.DOM_VK_F20, ["F20"] ],
+ [ KeyEvent.DOM_VK_F21, ["F21"] ],
+ [ KeyEvent.DOM_VK_F22, ["F22"] ],
+ [ KeyEvent.DOM_VK_F23, ["F23"] ],
+ [ KeyEvent.DOM_VK_F24, ["F24"] ]
+ ];
+
+ function getKeyCode(str)
+ {
+ str = str.toLowerCase();
+
+ for (let [,key] in Iterator(keyTable))
+ {
+ for (let [,name] in Iterator(key[1]))
+ {
+ // we don't store lowercase keys in the keyTable, because we
+ // also need to get good looking strings for the reverse action
+ if (name.toLowerCase() == str)
+ return key[0];
+ }
+ }
+
+ return 0;
+ }
+
+ function isFormElemFocused()
+ {
+ var elt = window.document.commandDispatcher.focusedElement;
+ if (elt == null)
+ return false;
+
+ try
+ { // sometimes the elt doesn't have .localName
+ var tagname = elt.localName.toLowerCase();
+ var type = elt.type.toLowerCase();
+
+ if ((tagname == "input" && (type != "image")) ||
+ tagname == "textarea" ||
+ // tagName == "SELECT" ||
+ // tagName == "BUTTON" ||
+ tagname == "isindex") // isindex is a deprecated one-line input box
+ return true;
+ }
+ catch (e)
+ {
+ // FIXME: do nothing?
+ }
+
+ return false;
+ }
+
+ function triggerLoadAutocmd(name, doc)
+ {
+ let args = {
+ url: doc.location.href,
+ title: doc.title
+ };
+
+ if (liberator.has("tabs"))
+ {
+ args.tab = tabs.getContentIndex(doc) + 1;
+ args.doc = "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument";
+ }
+
+ autocommands.trigger(name, args);
+ }
+
+ function onResize(event)
+ {
+ if (window.fullScreen != fullscreen)
+ {
+ fullscreen = window.fullScreen;
+ liberator.triggerObserver("fullscreen", fullscreen);
+ autocommands.trigger("Fullscreen", { state: fullscreen });
+ }
+ }
+
+ function onDOMContentLoaded(event)
+ {
+ if (event.originalTarget instanceof HTMLDocument)
+ triggerLoadAutocmd("DOMLoad", event.originalTarget);
+ }
+
+ // TODO: see what can be moved to onDOMContentLoaded()
+ function onPageLoad(event)
+ {
+ if (event.originalTarget instanceof HTMLDocument)
+ {
+ let doc = event.originalTarget;
+ // document is part of a frameset
+ if (doc.defaultView.frameElement)
+ {
+ // hacky way to get rid of "Transfering data from ..." on sites with frames
+ // when you click on a link inside a frameset, because asyncUpdateUI
+ // is not triggered there (firefox bug?)
+ setTimeout(statusline.updateUrl, 10);
+ return;
+ }
+
+ // code which should happen for all (also background) newly loaded tabs goes here:
+
+ let url = doc.location.href;
+ let title = doc.title;
+
+ // mark the buffer as loaded, we can't use buffer.loaded
+ // since that always refers to the current buffer, while doc can be
+ // any buffer, even in a background tab
+ doc.pageIsFullyLoaded = 1;
+
+ // code which is only relevant if the page load is the current tab goes here:
+ if (doc == getBrowser().contentDocument)
+ {
+ // we want to stay in command mode after a page has loaded
+ // TODO: move somehwere else, as focusing can already happen earlier than on "load"
+ if (options["focuscontent"])
+ {
+ setTimeout(function () {
+ var focused = document.commandDispatcher.focusedElement;
+ if (focused && (focused.value !== undefined) && focused.value.length == 0)
+ focused.blur();
+ }, 100);
+ }
+ }
+ else // background tab
+ {
+ liberator.echomsg("Background tab loaded: " + title || url, 3);
+ }
+
+ triggerLoadAutocmd("PageLoad", doc);
+ }
+ }
+
+ function wrapListener(method)
+ {
+ return function (event)
+ {
+ try
+ {
+ eventManager[method](event);
+ }
+ catch (e)
+ {
+ if (e.message == "Interrupted")
+ liberator.echoerr("Interrupted");
+ else
+ liberator.echoerr("Processing " + event.type + " event: " + (e.echoerr || e));
+ liberator.reportError(e);
+ }
+ };
+ }
+
+ // return true when load successful, or false otherwise
+ function waitForPageLoaded()
+ {
+ liberator.dump("start waiting in loaded state: " + buffer.loaded);
+ liberator.threadYield(true); // clear queue
+
+ if (buffer.loaded == 1)
+ return true;
+
+ let start = Date.now();
+ let end = start + 25000; // maximum time to wait - TODO: add option
+ let now;
+ while (now = Date.now(), now < end)
+ {
+ liberator.threadYield();
+ if ((now - start) % 1000 < 10)
+ liberator.dump("waited: " + (now - start) + " ms");
+
+ if (!events.feedingKeys)
+ return false;
+
+ if (buffer.loaded > 0)
+ {
+ liberator.sleep(250);
+ break;
+ }
+ else
+ liberator.echo("Waiting for page to load...");
+ }
+ modes.show();
+
+ // TODO: allow macros to be continued when page does not fully load with an option
+ var ret = (buffer.loaded == 1);
+ if (!ret)
+ liberator.echoerr("Page did not load completely in " + ms + " milliseconds. Macro stopped.");
+ liberator.dump("done waiting: " + ret);
+
+ // sometimes the input widget had focus when replaying a macro
+ // maybe this call should be moved somewhere else?
+ // liberator.focusContent(true);
+
+ return ret;
+ }
+
+ // load all macros inside ~/.vimperator/macros/
+ // setTimeout needed since io. is loaded after events.
+ setTimeout (function () {
+ // FIXME: largely duplicated for loading plugins
+ try
+ {
+ let dirs = io.getRuntimeDirectories("macros");
+
+ if (dirs.length > 0)
+ {
+ for (let [,dir] in Iterator(dirs))
+ {
+ liberator.echomsg("Searching for \"macros/*\" in \"" + dir.path + "\"", 2);
+
+ liberator.log("Sourcing macros directory: " + dir.path + "...", 3);
+
+ let files = io.readDirectory(dir.path);
+
+ files.forEach(function (file) {
+ if (!file.exists() || file.isDirectory() ||
+ !file.isReadable() || !/^[\w_-]+(\.vimp)?$/i.test(file.leafName))
+ return;
+
+ let name = file.leafName.replace(/\.vimp$/i, "");
+ macros.set(name, io.readFile(file).split("\n")[0]);
+
+ liberator.log("Macro " + name + " added: " + macros.get(name), 5);
+ });
+ }
+ }
+ else
+ {
+ liberator.log("No user macros directory found", 3);
+ }
+ }
+ catch (e)
+ {
+ // thrown if directory does not exist
+ liberator.log("Error sourcing macros directory: " + e, 9);
+ }
+ }, 100);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ mappings.add(modes.all,
+ ["<Esc>", "<C-[>"], "Focus content",
+ function () { events.onEscape(); });
+
+ // add the ":" mapping in all but insert mode mappings
+ mappings.add([modes.NORMAL, modes.VISUAL, modes.HINTS, modes.MESSAGE, modes.COMPOSE, modes.CARET, modes.TEXTAREA],
+ [":"], "Enter command line mode",
+ function () { commandline.open(":", "", modes.EX); });
+
+ // focus events
+ mappings.add([modes.NORMAL, modes.VISUAL, modes.CARET],
+ ["<Tab>"], "Advance keyboard focus",
+ function () { document.commandDispatcher.advanceFocus(); });
+
+ mappings.add([modes.NORMAL, modes.VISUAL, modes.CARET, modes.INSERT, modes.TEXTAREA],
+ ["<S-Tab>"], "Rewind keyboard focus",
+ function () { document.commandDispatcher.rewindFocus(); });
+
+ mappings.add(modes.all,
+ ["<C-z>"], "Temporarily ignore all " + config.name + " key bindings",
+ function () { modes.passAllKeys = true; });
+
+ mappings.add(modes.all,
+ ["<C-v>"], "Pass through next key",
+ function () { modes.passNextKey = true; });
+
+ mappings.add(modes.all,
+ ["<Nop>"], "Do nothing",
+ function () { return; });
+
+ // macros
+ mappings.add([modes.NORMAL, modes.MESSAGE],
+ ["q"], "Record a key sequence into a macro",
+ function (arg) { events.startRecording(arg); },
+ { flags: Mappings.flags.ARGUMENT });
+
+ mappings.add([modes.NORMAL, modes.MESSAGE],
+ ["@"], "Play a macro",
+ function (count, arg)
+ {
+ if (count < 1) count = 1;
+ while (count-- && events.playMacro(arg))
+ ;
+ },
+ { flags: Mappings.flags.ARGUMENT | Mappings.flags.COUNT });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["delmac[ros]"],
+ "Delete macros",
+ function (args)
+ {
+ if (args.bang)
+ args.string = ".*"; // XXX
+
+ events.deleteMacros(args.string);
+ },
+ {
+ bang: true,
+ completer: function (context) completion.macro(context),
+ literal: 0
+ });
+
+ commands.add(["macros"],
+ "List all macros",
+ function (args) { completion.listCompleter("macro", args[0]) },
+ {
+ argCount: "?",
+ completer: function (context) completion.macro(context)
+ });
+
+ commands.add(["pl[ay]"],
+ "Replay a recorded macro",
+ function (args) { events.playMacro(args[0]); },
+ {
+ argCount: "1",
+ completer: function (context) completion.macro(context)
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var eventManager = {
+
+ feedingKeys: false,
+
+ wantsModeReset: true, // used in onFocusChange since Firefox is so buggy here
+
+ destroy: function ()
+ {
+ // removeEventListeners() to avoid mem leaks
+ liberator.dump("TODO: remove all eventlisteners");
+
+ if (typeof getBrowser != "undefined")
+ getBrowser().removeProgressListener(this.progressListener);
+
+ window.removeEventListener("popupshown", enterPopupMode, true);
+ window.removeEventListener("popuphidden", exitPopupMode, true);
+ window.removeEventListener("DOMMenuBarActive", enterMenuMode, true);
+ window.removeEventListener("DOMMenuBarInactive", exitMenuMode, true);
+
+ window.removeEventListener("keypress", this.onKeyPress, true);
+ window.removeEventListener("keydown", this.onKeyDown, true);
+ },
+
+ startRecording: function (macro)
+ {
+ if (!/[a-zA-Z0-9]/.test(macro))
+ {
+ // TODO: ignore this like Vim?
+ liberator.echoerr("E354: Invalid register name: '" + macro + "'");
+ return;
+ }
+
+ modes.isRecording = true;
+
+ if (/[A-Z]/.test(macro)) // uppercase (append)
+ {
+ currentMacro = macro.toLowerCase();
+ if (!macros.get(currentMacro))
+ macros.set(currentMacro, ""); // initialize if it does not yet exist
+ }
+ else
+ {
+ currentMacro = macro;
+ macros.set(currentMacro, "");
+ }
+ },
+
+ playMacro: function (macro)
+ {
+ var res = false;
+ if (!/[a-zA-Z0-9@]/.test(macro) && macro.length == 1)
+ {
+ liberator.echoerr("E354: Invalid register name: '" + macro + "'");
+ return false;
+ }
+
+ if (macro == "@") // use lastMacro if it's set
+ {
+ if (!lastMacro)
+ {
+ liberator.echoerr("E748: No previously used register");
+ return false;
+ }
+ }
+ else
+ {
+ if (macro.length == 1)
+ lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist
+ else
+ lastMacro = macro; // e.g. long names are case sensitive
+ }
+
+ if (macros.get(lastMacro))
+ {
+ // make sure the page is stopped before starting to play the macro
+ try
+ {
+ window.getWebNavigation().stop(nsIWebNavigation.STOP_ALL);
+ }
+ catch (e) {}
+
+ buffer.loaded = 1; // even if not a full page load, assume it did load correctly before starting the macro
+ modes.isReplaying = true;
+ res = events.feedkeys(macros.get(lastMacro), true); // true -> noremap
+ modes.isReplaying = false;
+ }
+ else
+ {
+ if (lastMacro.length == 1)
+ // TODO: ignore this like Vim?
+ liberator.echoerr("Exxx: Register " + lastMacro + " not set");
+ else
+ liberator.echoerr("Exxx: Named macro '" + lastMacro + "' not set");
+ }
+ return res;
+ },
+
+ getMacros: function (filter)
+ {
+ if (!filter)
+ return macros;
+
+ var re = new RegExp(filter);
+ return ([macro, keys] for ([macro, keys] in macros) if (re.test(macro)));
+ },
+
+ deleteMacros: function (filter)
+ {
+ var re = new RegExp(filter);
+
+ for (let [item,] in macros)
+ {
+ if (re.test(item))
+ macros.remove(item);
+ }
+ },
+
+ // This method pushes keys into the event queue from liberator
+ // it is similar to vim's feedkeys() method, but cannot cope with
+ // 2 partially feeded strings, you have to feed one parsable string
+ //
+ // @param keys: a string like "2<C-f>" to pass
+ // if you want < to be taken literally, prepend it with a \\
+ feedkeys: function (keys, noremap, silent)
+ {
+ var doc = window.document;
+ var view = window.document.defaultView;
+ var escapeKey = false; // \ to escape some special keys
+
+ var wasFeeding = this.feedingKeys;
+ this.feedingKeys = true;
+ var wasSilent = commandline.silent;
+ if (silent)
+ commandline.silent = silent;
+
+ try
+ {
+ noremap = !!noremap;
+
+ for (var i = 0; i < keys.length; i++)
+ {
+ let charCode = keys.charCodeAt(i);
+ let keyCode = 0;
+ let shift = false, ctrl = false, alt = false, meta = false;
+ let string = null;
+
+ //if (keys[i] == "\\") // FIXME: support the escape key
+ if (keys[i] == "<" && !escapeKey) // start a complex key
+ {
+ let [match, modifier, keyname] = keys.substr(i).match(/<([CSMA]-)*(.+?)>/i) || [];
+ if (keyname)
+ {
+ if (modifier) // check for modifiers
+ {
+ ctrl = /C-/i.test(modifier);
+ alt = /A-/i.test(modifier);
+ shift = /S-/i.test(modifier);
+ meta = /M-/i.test(modifier);
+ }
+ if (keyname.length == 1)
+ {
+ if (!ctrl && !alt && !shift && !meta)
+ return false; // an invalid key like <a>
+ charCode = keyname.charCodeAt(0);
+ }
+ else if (keyname.toLowerCase() == "space")
+ {
+ charCode = 32;
+ }
+ else if (keyname.toLowerCase() == "nop")
+ {
+ string = "<Nop>";
+ }
+ else if (keyCode = getKeyCode(keyname))
+ {
+ charCode = 0;
+ }
+ else // an invalid key like <A-xxx> was found, stop propagation here (like Vim)
+ {
+ break;
+ }
+
+ i += match.length - 1;
+ }
+ }
+ else // a simple key
+ {
+ // FIXME: does not work for non A-Z keys like Ö,Ä,...
+ shift = (keys[i] >= "A" && keys[i] <= "Z");
+ }
+
+ let elem = window.document.commandDispatcher.focusedElement;
+ if (!elem)
+ elem = window.content;
+
+ let evt = doc.createEvent("KeyEvents");
+ evt.initKeyEvent("keypress", true, true, view, ctrl, alt, shift, meta, keyCode, charCode);
+ evt.noremap = noremap;
+ evt.isMacro = true;
+ if (string)
+ {
+ evt.liberatorString = string;
+ events.onKeyPress(evt);
+ }
+ else
+ {
+ elem.dispatchEvent(evt);
+ }
+ if (!this.feedingKeys)
+ break;
+ // stop feeding keys if page loading failed
+ if (modes.isReplaying && !waitForPageLoaded())
+ break;
+ // else // a short break between keys often helps
+ // liberator.sleep(50);
+ }
+ }
+ finally
+ {
+ this.feedingKeys = wasFeeding;
+ if (silent)
+ commandline.silent = wasSilent;
+ }
+ return i == keys.length;
+ },
+
+ // this function converts the given event to
+ // a keycode which can be used in mappings
+ // e.g. pressing ctrl+n would result in the string "<C-n>"
+ // null if unknown key
+ toString: function (event)
+ {
+ if (!event)
+ return "[object Mappings]";
+
+ if (event.liberatorString)
+ return event.liberatorString;
+
+ let key = null;
+ let modifier = "";
+
+ if (event.ctrlKey)
+ modifier += "C-";
+ if (event.altKey)
+ modifier += "A-";
+ if (event.metaKey)
+ modifier += "M-";
+
+ if (event.type == "keypress")
+ {
+ if (event.charCode == 0)
+ {
+ if (event.shiftKey)
+ modifier += "S-";
+
+ for (let i = 0; i < keyTable.length; i++)
+ {
+ if (keyTable[i][0] == event.keyCode)
+ {
+ key = keyTable[i][1][0];
+ break;
+ }
+ }
+ }
+ // special handling of the Space key
+ else if (event.charCode == 32)
+ {
+ if (event.shiftKey)
+ modifier += "S-";
+ key = "Space";
+ }
+ // a normal key like a, b, c, 0, etc.
+ else if (event.charCode > 0)
+ {
+ key = String.fromCharCode(event.charCode);
+ if (modifier.length == 0)
+ return key;
+ }
+ }
+ else if (event.type == "click" || event.type == "dblclick")
+ {
+ if (event.shiftKey)
+ modifier += "S-";
+ if (event.type == "dblclick")
+ modifier += "2-";
+ // TODO: triple and quadruple click
+
+ switch (event.button)
+ {
+ case 0:
+ key = "LeftMouse";
+ break;
+ case 1:
+ key = "MiddleMouse";
+ break;
+ case 2:
+ key = "RightMouse";
+ break;
+ }
+ }
+
+ if (key == null)
+ return null;
+
+ // a key like F1 is always enclosed in < and >
+ return "<" + modifier + key + ">";
+ },
+
+ isAcceptKey: function (key)
+ {
+ return (key == "<Return>" || key == "<C-j>" || key == "<C-m>");
+ },
+
+ isCancelKey: function (key)
+ {
+ return (key == "<Esc>" || key == "<C-[>" || key == "<C-c>");
+ },
+
+ // argument "event" is delibarately not used, as i don't seem to have
+ // access to the real focus target
+ //
+ // the ugly wantsModeReset is needed, because firefox generates a massive
+ // amount of focus changes for things like <C-v><C-k> (focusing the search field)
+ onFocusChange: function (event)
+ {
+ // command line has it's own focus change handler
+ if (liberator.mode == modes.COMMAND_LINE)
+ return;
+
+ function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument
+
+ var win = window.document.commandDispatcher.focusedWindow;
+ var elem = window.document.commandDispatcher.focusedElement;
+
+ if (win && win.top == content && liberator.has("tabs"))
+ tabs.localStore.focusedFrame = win;
+
+ if (elem && elem.readOnly)
+ return;
+
+ if (elem && (
+ (elem instanceof HTMLInputElement && (elem.type.toLowerCase() == "text" || elem.type.toLowerCase() == "password")) ||
+ (elem instanceof HTMLSelectElement)
+ ))
+ {
+ this.wantsModeReset = false;
+ liberator.mode = modes.INSERT;
+ if (hasHTMLDocument(win))
+ buffer.lastInputField = elem;
+ return;
+ }
+
+ if (elem && elem instanceof HTMLTextAreaElement)
+ {
+ this.wantsModeReset = false;
+ if (options["insertmode"])
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ else if (elem.selectionEnd - elem.selectionStart > 0)
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+ else
+ modes.main = modes.TEXTAREA;
+ if (hasHTMLDocument(win))
+ buffer.lastInputField = elem;
+ return;
+ }
+
+ if (config.name == "Muttator")
+ {
+ // we switch to -- MESSAGE -- mode for muttator, when the main HTML widget gets focus
+ if (hasHTMLDocument(win) || elem instanceof HTMLAnchorElement)
+ {
+ if (config.isComposeWindow)
+ {
+ liberator.dump("Compose editor got focus");
+ modes.set(modes.INSERT, modes.TEXTAREA);
+ }
+ else if (liberator.mode != modes.MESSAGE)
+ liberator.mode = modes.MESSAGE;
+ return;
+ }
+ }
+
+ if (liberator.mode == modes.INSERT ||
+ liberator.mode == modes.TEXTAREA ||
+ liberator.mode == modes.MESSAGE ||
+ liberator.mode == modes.VISUAL)
+ {
+ // FIXME: currently this hack is disabled to make macros work
+ // this.wantsModeReset = true;
+ // setTimeout(function () {
+ // if (events.wantsModeReset)
+ // {
+ // events.wantsModeReset = false;
+ modes.reset();
+ // }
+ // }, 0);
+ }
+ },
+
+ onSelectionChange: function (event)
+ {
+ var couldCopy = false;
+ var controller = document.commandDispatcher.getControllerForCommand("cmd_copy");
+ if (controller && controller.isCommandEnabled("cmd_copy"))
+ couldCopy = true;
+
+ if (liberator.mode != modes.VISUAL)
+ {
+ if (couldCopy)
+ {
+ if ((liberator.mode == modes.TEXTAREA ||
+ (modes.extended & modes.TEXTAREA))
+ && !options["insertmode"])
+ modes.set(modes.VISUAL, modes.TEXTAREA);
+ else if (liberator.mode == modes.CARET)
+ modes.set(modes.VISUAL, modes.CARET);
+ }
+ }
+ // XXX: disabled, as i think automatically starting visual caret mode does more harm than help
+ // else
+ // {
+ // if (!couldCopy && modes.extended & modes.CARET)
+ // liberator.mode = modes.CARET;
+ // }
+ },
+
+ // global escape handler, is called in ALL modes
+ onEscape: function ()
+ {
+ if (!modes.passNextKey)
+ {
+ if (modes.passAllKeys)
+ {
+ modes.passAllKeys = false;
+ return;
+ }
+
+ switch (liberator.mode)
+ {
+ case modes.NORMAL:
+ // clear any selection made
+ var selection = window.content.getSelection();
+ try
+ { // a simple if (selection) does not seem to work
+ selection.collapseToStart();
+ }
+ catch (e) {}
+ commandline.clear();
+
+ modes.reset();
+ liberator.focusContent(true);
+ break;
+
+ case modes.VISUAL:
+ if (modes.extended & modes.TEXTAREA)
+ liberator.mode = modes.TEXTAREA;
+ else if (modes.extended & modes.CARET)
+ liberator.mode = modes.CARET;
+ break;
+
+ case modes.CARET:
+ // setting this option will trigger an observer which will
+ // care about all other details like setting the NORMAL mode
+ options.setPref("accessibility.browsewithcaret", false);
+ break;
+
+ case modes.INSERT:
+ if ((modes.extended & modes.TEXTAREA) && !options["insertmode"])
+ {
+ liberator.mode = modes.TEXTAREA;
+ }
+ else
+ {
+ modes.reset();
+ liberator.focusContent(true);
+ }
+ break;
+
+ default: // HINTS, CUSTOM or COMMAND_LINE
+ modes.reset();
+ break;
+ }
+ }
+ },
+
+ // this keypress handler gets always called first, even if e.g.
+ // the commandline has focus
+ onKeyPress: function (event)
+ {
+ let key = events.toString(event);
+ if (!key)
+ return true;
+
+ //liberator.log(key + " in mode: " + liberator.mode);
+ //liberator.dump(key + " in mode: " + liberator.mode);
+
+ if (modes.isRecording)
+ {
+ if (key == "q") // TODO: should not be hardcoded
+ {
+ modes.isRecording = false;
+ liberator.log("Recorded " + currentMacro + ": " + macros.get(currentMacro), 9);
+ liberator.echo("Recorded macro '" + currentMacro + "'");
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ else if (!mappings.hasMap(liberator.mode, input.buffer + key))
+ {
+ macros.set(currentMacro, macros.get(currentMacro) + key);
+ }
+ }
+
+ if (key == "<C-c>")
+ liberator.interrupted = true;
+
+ // feedingKeys needs to be separate from interrupted so
+ // we can differentiate between a recorded <C-c>
+ // interrupting whatever it's started and a real <C-c>
+ // interrupting our playback.
+ if (events.feedingKeys)
+ {
+ if (key == "<C-c>" && !event.isMacro)
+ {
+ events.feedingKeys = false;
+ setTimeout(function () { liberator.echo("Canceled playback of macro '" + lastMacro + "'") }, 100);
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ }
+
+ let stop = true; // set to false if we should NOT consume this event but let Firefox handle it
+
+ let win = document.commandDispatcher.focusedWindow;
+ if (win && win.document.designMode == "on" && !config.isComposeWindow)
+ return false;
+
+ // menus have their own command handlers
+ if (modes.extended & modes.MENU)
+ return false;
+
+ // handle Escape-one-key mode (Ctrl-v)
+ if (modes.passNextKey && !modes.passAllKeys)
+ {
+ modes.passNextKey = false;
+ return false;
+ }
+ // handle Escape-all-keys mode (Ctrl-q)
+ if (modes.passAllKeys)
+ {
+ if (modes.passNextKey)
+ modes.passNextKey = false; // and then let flow continue
+ else if (key == "<Esc>" || key == "<C-[>" || key == "<C-v>")
+ ; // let flow continue to handle these keys to cancel escape-all-keys mode
+ else
+ return false;
+ }
+
+ // just forward event without checking any mappings when the MOW is open
+ if (liberator.mode == modes.COMMAND_LINE &&
+ (modes.extended & modes.OUTPUT_MULTILINE))
+ {
+ commandline.onMultilineOutputEvent(event);
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ // XXX: ugly hack for now pass certain keys to firefox as they are without beeping
+ // also fixes key navigation in combo boxes, submitting forms, etc.
+ // FIXME: breaks iabbr for now --mst
+ if ((config.name == "Vimperator" && liberator.mode == modes.NORMAL)
+ || liberator.mode == modes.INSERT)
+ {
+ if (key == "<Return>")
+ return false;
+ else if (key == "<Space>" || key == "<Up>" || key == "<Down>")
+ return false;
+ }
+
+ // // FIXME: handle middle click in content area {{{
+ // // alert(event.target.id);
+ // if (/*event.type == 'mousedown' && */event.button == 1 && event.target.id == 'content')
+ // {
+ // //echo("foo " + event.target.id);
+ // //if (document.commandDispatcher.focusedElement == command_line.inputField)
+ // {
+ // //alert(command_line.value.substring(0, command_line.selectionStart));
+ // command_line.value = command_line.value.substring(0, command_line.selectionStart) +
+ // window.readFromClipboard() +
+ // command_line.value.substring(command_line.selectionEnd, command_line.value.length);
+ // alert(command_line.value);
+ // }
+ // //else
+ // // {
+ // // openURLs(window.readFromClipboard());
+ // // }
+ // return true;
+ // } }}}
+
+ if (key != "<Esc>" && key != "<C-[>")
+ {
+ // custom mode...
+ if (liberator.mode == modes.CUSTOM)
+ {
+ plugins.onEvent(event);
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ if (modes.extended & modes.HINTS)
+ {
+ // under HINT mode, certain keys are redirected to hints.onEvent
+ if (key == "<Return>" || key == "<Tab>" || key == "<S-Tab>"
+ || key == mappings.getMapLeader()
+ || (key == "<BS>" && hints.previnput == "number")
+ || (/^[0-9]$/.test(key) && !hints.escNumbers))
+ {
+ hints.onEvent(event);
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ // others are left to generate the 'input' event or handled by firefox
+ return;
+ }
+ }
+
+ // FIXME (maybe): (is an ESC or C-] here): on HINTS mode, it enters
+ // into 'if (map && !skipMap) below. With that (or however) it
+ // triggers the onEscape part, where it resets mode. Here I just
+ // return true, with the effect that it also gets to there (for
+ // whatever reason). if that happens to be correct, well..
+ // XXX: why not just do that as well for HINTS mode actually?
+
+ if (liberator.mode == modes.CUSTOM)
+ return true;
+
+ let countStr = input.buffer.match(/^[0-9]*/)[0];
+ let candidateCommand = (input.buffer + key).replace(countStr, "");
+ let map;
+ if (event.noremap)
+ map = mappings.getDefault(liberator.mode, candidateCommand);
+ else
+ map = mappings.get(liberator.mode, candidateCommand);
+
+ let candidates = mappings.getCandidates(liberator.mode, candidateCommand);
+ if (candidates.length == 0 && !map)
+ {
+ map = input.pendingMap;
+ input.pendingMap = null;
+ }
+
+ // counts must be at the start of a complete mapping (10j -> go 10 lines down)
+ if (/^[1-9][0-9]*$/.test(input.buffer + key))
+ {
+ // no count for insert mode mappings
+ if (liberator.mode == modes.INSERT || liberator.mode == modes.COMMAND_LINE)
+ stop = false;
+ else
+ {
+ input.buffer += key;
+ inputBufferLength++;
+ }
+ }
+ else if (input.pendingArgMap)
+ {
+ input.buffer = "";
+ inputBufferLength = 0;
+ let tmp = input.pendingArgMap; // must be set to null before .execute; if not
+ input.pendingArgMap = null; // v.input.pendingArgMap is still 'true' also for new feeded keys
+ if (key != "<Esc>" && key != "<C-[>")
+ {
+ if (modes.isReplaying && !waitForPageLoaded())
+ return true;
+
+ tmp.execute(null, input.count, key);
+ }
+ }
+ // only follow a map if there isn't a longer possible mapping
+ // (allows you to do :map z yy, when zz is a longer mapping than z)
+ // TODO: map.rhs is only defined for user defined commands, should add a "isDefault" property
+ else if (map && !skipMap && (map.rhs || candidates.length == 0))
+ {
+ input.count = parseInt(countStr, 10);
+ if (isNaN(input.count))
+ input.count = -1;
+ if (map.flags & Mappings.flags.ARGUMENT)
+ {
+ input.pendingArgMap = map;
+ input.buffer += key;
+ inputBufferLength++;
+ }
+ else if (input.pendingMotionMap)
+ {
+ if (key != "<Esc>" && key != "<C-[>")
+ {
+ input.pendingMotionMap.execute(candidateCommand, input.count, null);
+ }
+ input.pendingMotionMap = null;
+ input.buffer = "";
+ inputBufferLength = 0;
+ }
+ // no count support for these commands yet
+ else if (map.flags & Mappings.flags.MOTION)
+ {
+ input.pendingMotionMap = map;
+ input.buffer = "";
+ inputBufferLength = 0;
+ }
+ else
+ {
+ input.buffer = "";
+ inputBufferLength = 0;
+
+ if (modes.isReplaying && !waitForPageLoaded())
+ return true;
+
+ let ret = map.execute(null, input.count);
+ if (map.flags & Mappings.flags.ALLOW_EVENT_ROUTING && ret)
+ stop = false;
+ }
+ }
+ else if (mappings.getCandidates(liberator.mode, candidateCommand).length > 0 && !skipMap)
+ {
+ input.pendingMap = map;
+ input.buffer += key;
+ inputBufferLength++;
+ }
+ else // if the key is neither a mapping nor the start of one
+ {
+ // the mode checking is necessary so that things like g<esc> do not beep
+ if (input.buffer != "" && !skipMap && (liberator.mode == modes.INSERT ||
+ liberator.mode == modes.COMMAND_LINE || liberator.mode == modes.TEXTAREA))
+ {
+ // no map found -> refeed stuff in v.input.buffer (only while in INSERT, CO... modes)
+ skipMap = true; // ignore maps while doing so
+ events.feedkeys(input.buffer, true);
+ }
+ if (skipMap)
+ {
+ if (--inputBufferLength == 0) // inputBufferLength == 0. v.input.buffer refeeded...
+ skipMap = false; // done...
+ }
+
+ input.buffer = "";
+ input.pendingArgMap = null;
+ input.pendingMotionMap = null;
+ input.pendingMap = null;
+
+ if (key != "<Esc>" && key != "<C-[>")
+ {
+ // allow key to be passed to firefox if we can't handle it
+ stop = false;
+
+ if (liberator.mode == modes.COMMAND_LINE)
+ {
+ if (!(modes.extended & modes.INPUT_MULTILINE))
+ commandline.onEvent(event); // reroute event in command line mode
+ }
+ else if (liberator.mode != modes.INSERT && liberator.mode != modes.TEXTAREA)
+ {
+ liberator.beep();
+ }
+ }
+ }
+
+ if (stop)
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ let motionMap = (input.pendingMotionMap && input.pendingMotionMap.names[0]) || "";
+ statusline.updateInputBuffer(motionMap + input.buffer);
+ return false;
+ },
+
+ // this is need for sites like msn.com which focus the input field on keydown
+ onKeyUpOrDown: function (event)
+ {
+ if (modes.passNextKey ^ modes.passAllKeys || isFormElemFocused())
+ return true;
+
+ event.stopPropagation();
+ return false;
+ },
+
+ // TODO: move to buffer.js?
+ progressListener: {
+ QueryInterface: function (aIID)
+ {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsIXULBrowserWindow) || // for setOverLink();
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ // XXX: function may later be needed to detect a canceled synchronous openURL()
+ onStateChange: function (webProgress, request, flags, status)
+ {
+ // 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 & (Components.interfaces.nsIWebProgressListener.STATE_IS_DOCUMENT |
+ Components.interfaces.nsIWebProgressListener.STATE_IS_WINDOW))
+ {
+ // This fires when the load event is initiated
+ // only thrown for the current tab, not when another tab changes
+ if (flags & Components.interfaces.nsIWebProgressListener.STATE_START)
+ {
+ buffer.loaded = 0;
+ statusline.updateProgress(0);
+
+ autocommands.trigger("PageLoadPre", { url: buffer.URL });
+
+ // don't reset mode if a frame of the frameset gets reloaded which
+ // is not the focused frame
+ if (document.commandDispatcher.focusedWindow == webProgress.DOMWindow)
+ {
+ setTimeout(function () { modes.reset(false); },
+ liberator.mode == modes.HINTS ? 500 : 0);
+ }
+ }
+ else if (flags & Components.interfaces.nsIWebProgressListener.STATE_STOP)
+ {
+ buffer.loaded = (status == 0 ? 1 : 2);
+ statusline.updateUrl();
+ }
+ }
+ },
+ // for notifying the user about secure web pages
+ onSecurityChange: function (webProgress, aRequest, aState)
+ {
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ if (aState & nsIWebProgressListener.STATE_IS_INSECURE)
+ statusline.setClass("insecure");
+ else if (aState & nsIWebProgressListener.STATE_IS_BROKEN)
+ statusline.setClass("broken");
+ else if (aState & nsIWebProgressListener.STATE_IS_SECURE)
+ statusline.setClass("secure");
+ },
+ onStatusChange: function (webProgress, request, status, message)
+ {
+ statusline.updateUrl(message);
+ },
+ onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress)
+ {
+ statusline.updateProgress(curTotalProgress/maxTotalProgress);
+ },
+ // happens when the users switches tabs
+ onLocationChange: function ()
+ {
+ statusline.updateUrl();
+ statusline.updateProgress();
+
+ autocommands.trigger("LocationChange", { url: buffer.URL });
+
+ // if this is not delayed we get the position of the old buffer
+ setTimeout(function () { statusline.updateBufferPosition(); }, 100);
+ },
+ // called at the very end of a page load
+ asyncUpdateUI: function ()
+ {
+ setTimeout(statusline.updateUrl, 100);
+ },
+ setOverLink : function (link, b)
+ {
+ var ssli = options["showstatuslinks"];
+ if (link && ssli)
+ {
+ if (ssli == 1)
+ statusline.updateUrl("Link: " + link);
+ else if (ssli == 2)
+ liberator.echo("Link: " + link, commandline.DISALLOW_MULTILINE);
+ }
+
+ if (link == "")
+ {
+ if (ssli == 1)
+ statusline.updateUrl();
+ else if (ssli == 2)
+ modes.show();
+ }
+ },
+
+ // stub functions for the interfaces
+ setJSStatus: function (status) { ; },
+ setJSDefaultStatus: function (status) { ; },
+ setDefaultStatus: function (status) { ; },
+ onLinkIconAvailable: function () { ; }
+ },
+
+ // TODO: move to options.js?
+ prefObserver: {
+ register: function ()
+ {
+ var prefService = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefService);
+ this._branch = prefService.getBranch(""); // better way to monitor all changes?
+ this._branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
+ this._branch.addObserver("", this, false);
+ },
+
+ unregister: function ()
+ {
+ if (this._branch)
+ this._branch.removeObserver("", this);
+ },
+
+ observe: function (aSubject, aTopic, aData)
+ {
+ if (aTopic != "nsPref:changed")
+ return;
+
+ // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
+ // aData is the name of the pref that's been changed (relative to aSubject)
+ switch (aData)
+ {
+ case "accessibility.browsewithcaret":
+ var value = options.getPref("accessibility.browsewithcaret", false);
+ liberator.mode = value ? modes.CARET : modes.NORMAL;
+ break;
+ }
+ }
+ }
+ }; //}}}
+
+ window.XULBrowserWindow = eventManager.progressListener;
+ window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsIDocShellTreeItem).treeOwner
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIXULWindow)
+ .XULBrowserWindow = window.XULBrowserWindow;
+ try
+ {
+ getBrowser().addProgressListener(eventManager.progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
+ }
+ catch (e) {}
+
+ eventManager.prefObserver.register();
+ liberator.registerObserver("shutdown", function () {
+ eventManager.destroy();
+ eventManager.prefObserver.unregister();
+ });
+
+ window.addEventListener("keypress", wrapListener("onKeyPress"), true);
+ window.addEventListener("keydown", wrapListener("onKeyUpOrDown"), true);
+ window.addEventListener("keyup", wrapListener("onKeyUpOrDown"), true);
+
+ return eventManager;
+
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/find.js b/common/content/find.js
new file mode 100644
index 00000000..b4c0ffe6
--- /dev/null
+++ b/common/content/find.js
@@ -0,0 +1,473 @@
+/***** B/GIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// TODO: proper backwards search - implement our own component?
+// : implement our own highlighter?
+// : frameset pages
+// : <ESC> should cancel search highlighting in 'incsearch' mode and jump
+// back to the presearch page location - can probably use the same
+// solution as marks
+// : 'linksearch' searches should highlight link matches only
+// : changing any search settings should also update the search state including highlighting
+// : incremental searches shouldn't permanently update search modifiers
+
+// make sure you only create this object when the "liberator" object is ready
+function Search() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ // FIXME:
+ //var self = this; // needed for callbacks since "this" is the "liberator" object in a callback
+ var found = false; // true if the last search was successful
+ var backwards = false; // currently searching backwards
+ var searchString = ""; // current search string (without modifiers)
+ var searchPattern = ""; // current search string (includes modifiers)
+ var lastSearchPattern = ""; // the last searched pattern (includes modifiers)
+ var lastSearchString = ""; // the last searched string (without modifiers)
+ var lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
+ var caseSensitive = false; // search string is case sensitive
+ var linksOnly = false; // search is limited to link text only
+
+ // Event handlers for search - closure is needed
+ liberator.registerCallback("change", modes.SEARCH_FORWARD, function (command) { search.searchKeyPressed(command); });
+ liberator.registerCallback("submit", modes.SEARCH_FORWARD, function (command) { search.searchSubmitted(command); });
+ liberator.registerCallback("cancel", modes.SEARCH_FORWARD, function () { search.searchCanceled(); });
+ // TODO: allow advanced myModes in register/triggerCallback
+ liberator.registerCallback("change", modes.SEARCH_BACKWARD, function (command) { search.searchKeyPressed(command); });
+ liberator.registerCallback("submit", modes.SEARCH_BACKWARD, function (command) { search.searchSubmitted(command); });
+ liberator.registerCallback("cancel", modes.SEARCH_BACKWARD, function () { search.searchCanceled(); });
+
+ // set searchString, searchPattern, caseSensitive, linksOnly
+ function processUserPattern(pattern)
+ {
+ //// strip off pattern terminator and offset
+ //if (backwards)
+ // pattern = pattern.replace(/\?.*/, "");
+ //else
+ // pattern = pattern.replace(/\/.*/, "");
+
+ searchPattern = pattern;
+
+ // links only search - \l wins if both modifiers specified
+ if (/\\l/.test(pattern))
+ linksOnly = true;
+ else if (/\L/.test(pattern))
+ linksOnly = false;
+ else if (options["linksearch"])
+ linksOnly = true;
+ else
+ linksOnly = false;
+
+ // strip links-only modifiers
+ pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });
+
+ // case sensitivity - \c wins if both modifiers specified
+ if (/\c/.test(pattern))
+ caseSensitive = false;
+ else if (/\C/.test(pattern))
+ caseSensitive = true;
+ else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern))
+ caseSensitive = true;
+ else if (options["ignorecase"])
+ caseSensitive = false;
+ else
+ caseSensitive = true;
+
+ // strip case-sensitive modifiers
+ pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });
+
+ // remove any modifer escape \
+ pattern = pattern.replace(/\\(\\[cClL])/g, "$1");
+
+ searchString = pattern;
+ }
+
+ /* Stolen from toolkit.jar in Firefox, for the time being. The private
+ * methods were unstable, and changed. The new version is not remotely
+ * compatible with what we do.
+ * The following only applies to this object, and may not be
+ * necessary, or accurate, but, just in case:
+ * The Original Code is mozilla.org viewsource frontend.
+ *
+ * The Initial Developer of the Original Code is
+ * Netscape Communications Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2003
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Blake Ross <blake@cs.stanford.edu> (Original Author)
+ * Masayuki Nakano <masayuki@d-toybox.com>
+ * Ben Basson <contact@cusser.net>
+ * Jason Barnabe <jason_barnabe@fastmail.fm>
+ * Asaf Romano <mano@mozilla.com>
+ * Ehsan Akhgari <ehsan.akhgari@gmail.com>
+ * Graeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
+ */
+ var highlightObj = {
+ search: function (aWord, matchCase)
+ {
+ var finder = Components.classes["@mozilla.org/embedcomp/rangefind;1"]
+ .createInstance()
+ .QueryInterface(Components.interfaces.nsIFind);
+ if (matchCase !== undefined)
+ finder.caseSensitive = matchCase;
+
+ var range;
+ while ((range = finder.Find(aWord,
+ this.searchRange,
+ this.startPt,
+ this.endPt)))
+ yield range;
+ },
+
+ highlightDoc: function highlightDoc(win, aWord)
+ {
+ Array.forEach(win.frames, function (frame) highlightObj.highlightDoc(frame, aWord));
+
+ var doc = win.document;
+ if (!doc || !(doc instanceof HTMLDocument))
+ return;
+
+ if (!aWord)
+ {
+ let elems = highlightObj.getSpans(doc);
+ for (let i = elems.snapshotLength; --i >= 0;)
+ {
+ let elem = elems.snapshotItem(i);
+ let docfrag = doc.createDocumentFragment();
+ let next = elem.nextSibling;
+ let parent = elem.parentNode;
+
+ let child;
+ while (child = elem.firstChild)
+ docfrag.appendChild(child);
+
+ parent.removeChild(elem);
+ parent.insertBefore(docfrag, next);
+ parent.normalize();
+ }
+ return;
+ }
+
+ var baseNode = <span highlight="Search"/>
+ baseNode = util.xmlToDom(baseNode, window.content.document);
+
+ var body = doc.body;
+ var count = body.childNodes.length;
+ this.searchRange = doc.createRange();
+ this.startPt = doc.createRange();
+ this.endPt = doc.createRange();
+
+ this.searchRange.setStart(body, 0);
+ this.searchRange.setEnd(body, count);
+
+ this.startPt.setStart(body, 0);
+ this.startPt.setEnd(body, 0);
+ this.endPt.setStart(body, count);
+ this.endPt.setEnd(body, count);
+
+ liberator.interrupted = false;
+ let n = 0;
+ for (let retRange in this.search(aWord, caseSensitive))
+ {
+ // Highlight
+ var nodeSurround = baseNode.cloneNode(true);
+ var node = this.highlight(retRange, nodeSurround);
+ this.startPt = node.ownerDocument.createRange();
+ this.startPt.setStart(node, node.childNodes.length);
+ this.startPt.setEnd(node, node.childNodes.length);
+ if (n++ % 20 == 0)
+ liberator.threadYield(true);
+ if (liberator.interrupted)
+ break;
+ }
+ },
+
+ highlight: function highlight(aRange, aNode)
+ {
+ var startContainer = aRange.startContainer;
+ var startOffset = aRange.startOffset;
+ var endOffset = aRange.endOffset;
+ var docfrag = aRange.extractContents();
+ var before = startContainer.splitText(startOffset);
+ var parent = before.parentNode;
+ aNode.appendChild(docfrag);
+ parent.insertBefore(aNode, before);
+ return aNode;
+ },
+
+ getSpans: function (doc) buffer.evaluateXPath("//*[@liberator:highlight='Search']", doc)
+ };
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["hlsearch", "hls"],
+ "Highlight previous search pattern matches",
+ "boolean", "false",
+ {
+ setter: function (value)
+ {
+ if (value)
+ search.highlight();
+ else
+ search.clear();
+
+ return value;
+ }
+ });
+
+ options.add(["ignorecase", "ic"],
+ "Ignore case in search patterns",
+ "boolean", true);
+
+ options.add(["incsearch", "is"],
+ "Show where the search pattern matches as it is typed",
+ "boolean", true);
+
+ options.add(["linksearch", "lks"],
+ "Limit the search to hyperlink text",
+ "boolean", false);
+
+ options.add(["smartcase", "scs"],
+ "Override the 'ignorecase' option if the pattern contains uppercase characters",
+ "boolean", true);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var myModes = config.browserModes;
+ myModes = myModes.concat([modes.CARET]);
+
+ mappings.add(myModes,
+ ["/"], "Search forward for a pattern",
+ function () { search.openSearchDialog(modes.SEARCH_FORWARD); });
+
+ mappings.add(myModes,
+ ["?"], "Search backwards for a pattern",
+ function () { search.openSearchDialog(modes.SEARCH_BACKWARD); });
+
+ mappings.add(myModes,
+ ["n"], "Find next",
+ function () { search.findAgain(false); });
+
+ mappings.add(myModes,
+ ["N"], "Find previous",
+ function () { search.findAgain(true); });
+
+ mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
+ "Find word under cursor",
+ function ()
+ {
+ search.searchSubmitted(buffer.getCurrentWord(), false);
+ search.findAgain();
+ });
+
+ mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
+ "Find word under cursor backwards",
+ function ()
+ {
+ search.searchSubmitted(buffer.getCurrentWord(), true);
+ search.findAgain();
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["noh[lsearch]"],
+ "Remove the search highlighting",
+ function () { search.clear(); },
+ { argCount: "0" });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ // Called when the search dialog is asked for
+ // If you omit "mode", it will default to forward searching
+ openSearchDialog: function (mode)
+ {
+ if (mode == modes.SEARCH_BACKWARD)
+ {
+ commandline.open("?", "", modes.SEARCH_BACKWARD);
+ backwards = true;
+ }
+ else
+ {
+ commandline.open("/", "", modes.SEARCH_FORWARD);
+ backwards = false;
+ }
+
+ // TODO: focus the top of the currently visible screen
+ },
+
+ // Finds text in a page
+ // TODO: backwards seems impossible i fear :(
+ find: function (str, backwards)
+ {
+ var fastFind = getBrowser().fastFind;
+
+ processUserPattern(str);
+
+ fastFind.caseSensitive = caseSensitive;
+ found = fastFind.find(searchString, linksOnly) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
+
+ if (!found)
+ setTimeout(function () { liberator.echoerr("E486: Pattern not found: " + searchPattern); }, 0);
+
+ return found;
+ },
+
+ // Called when the current search needs to be repeated
+ findAgain: function (reverse)
+ {
+ // this hack is needed to make n/N work with the correct string, if
+ // we typed /foo<esc> after the original search. Since searchString is
+ // readonly we have to call find() again to update it.
+ if (getBrowser().fastFind.searchString != lastSearchString)
+ this.find(lastSearchString, false);
+
+ var up = reverse ? !lastSearchBackwards : lastSearchBackwards;
+ var result = getBrowser().fastFind.findAgain(up, linksOnly);
+
+ if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
+ {
+ liberator.echoerr("E486: Pattern not found: " + lastSearchPattern);
+ }
+ else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
+ {
+ // hack needed, because wrapping causes a "scroll" event which clears
+ // our command line
+ setTimeout(function () {
+ if (up)
+ commandline.echo("search hit TOP, continuing at BOTTOM",
+ commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
+ else
+ commandline.echo("search hit BOTTOM, continuing at TOP",
+ commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
+ }, 0);
+ }
+ else
+ {
+ liberator.echo((up ? "?" : "/") + lastSearchPattern, null, commandline.FORCE_SINGLELINE);
+
+ if (options["hlsearch"])
+ this.highlight(lastSearchString);
+ }
+ },
+
+ // Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set
+ searchKeyPressed: function (command)
+ {
+ if (options["incsearch"])
+ this.find(command, backwards);
+ },
+
+ // Called when the enter key is pressed to trigger a search
+ // use forcedBackward if you call this function directly
+ searchSubmitted: function (command, forcedBackward)
+ {
+ if (typeof forcedBackward === "boolean")
+ backwards = forcedBackward;
+
+ // use the last pattern if none specified
+ if (!command)
+ command = lastSearchPattern;
+
+ if (!options["incsearch"] || !found)
+ {
+ this.clear();
+ this.find(command, backwards);
+ }
+
+ lastSearchBackwards = backwards;
+ //lastSearchPattern = command.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
+ lastSearchPattern = command;
+ lastSearchString = searchString;
+
+ // TODO: move to find() when reverse incremental searching is kludged in
+ // need to find again for reverse searching
+ if (backwards)
+ setTimeout(function () { search.findAgain(false); }, 0);
+
+ if (options["hlsearch"])
+ this.highlight(searchString);
+
+ modes.reset();
+ },
+
+ // Called when the search is canceled - for example if someone presses
+ // escape while typing a search
+ searchCanceled: function ()
+ {
+ this.clear();
+ // TODO: code to reposition the document to the place before search started
+ },
+
+ // FIXME: thunderbird incompatible
+ // this is not dependent on the value of 'hlsearch'
+ highlight: function (text)
+ {
+ if (config.name == "Muttator")
+ return;
+
+ // already highlighted?
+ if (highlightObj.getSpans(content.document).snapshotLength > 0)
+ return;
+
+ if (!text)
+ text = lastSearchString;
+
+ highlightObj.highlightDoc(window.content, text);
+
+ // recreate selection since _highlightDoc collapses the selection backwards
+ getBrowser().fastFind.findAgain(false, linksOnly);
+
+ // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
+ },
+
+ clear: function ()
+ {
+ highlightObj.highlightDoc(window.content);
+ // need to manually collapse the selection if the document is not
+ // highlighted
+ getBrowser().fastFind.collapseSelection();
+ }
+
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/help.css b/common/content/help.css
new file mode 100644
index 00000000..e35994ae
--- /dev/null
+++ b/common/content/help.css
@@ -0,0 +1,149 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+div.main {
+ font-family: -moz-fixed;
+ white-space: -moz-pre-wrap;
+ width: 800px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+h1 {
+ text-align: center;
+}
+
+p.tagline {
+ text-align: center;
+ font-weight: bold;
+}
+
+table.vimperator {
+ border-width: 1px;
+ border-style: dotted;
+ border-color: gray;
+ /*margin-bottom: 2em; /* FIXME: just a quick hack until we have proper pages */
+}
+table.vimperator td {
+ border: none;
+ padding: 3px;
+}
+tr.separator {
+ height: 10px;
+}
+hr {
+ height: 1px;
+ background-color: white;
+ border-style: none;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+td.taglist {
+ text-align: right;
+ vertical-align: top;
+ border-spacing: 13px 10px;
+}
+td.taglist td {
+ width: 100px;
+ padding: 3px 0px;
+}
+tr.taglist code, td.usage code {
+ margin: 0px 2px;
+}
+td.usage code {
+ white-space: nowrap;
+}
+td.taglist code {
+ margin-left: 2em;
+}
+code.tag {
+ font-weight: bold;
+ color: rgb(255, 0, 255); /* magenta */
+ padding-left: 5px;
+}
+tr.description {
+ margin-bottom: 4px;
+}
+table.commands {
+ background-color: rgb(250, 240, 230);
+ color: black;
+}
+table.mappings {
+ background-color: rgb(230, 240, 250);
+ color: black;
+}
+table.options {
+ background-color: rgb(240, 250, 230);
+ color: black;
+}
+
+fieldset.paypal {
+ border: none;
+}
+
+.argument {
+ color: #6A97D4;
+}
+
+.command {
+ font-weight: bold;
+ color: #632610;
+}
+
+.mapping {
+ font-weight: bold;
+ color: #102663;
+}
+
+.option {
+ font-weight: bold;
+ color: #106326;
+}
+
+.code {
+ color: #108826;
+}
+
+.shorthelp {
+ font-weight: bold;
+}
+
+.version {
+ position: absolute;
+ top: 10px;
+ right: 2%;
+ color: #C0C0C0;
+ text-align: right;
+}
+
+.warning {
+ font-weight: bold;
+ color: red;
+}
+
+/* vim: set fdm=marker sw=4 ts=4 et: */
diff --git a/common/content/hints.js b/common/content/hints.js
new file mode 100644
index 00000000..c1f18eb1
--- /dev/null
+++ b/common/content/hints.js
@@ -0,0 +1,811 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+function Hints() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const ELEM = 0, TEXT = 1, SPAN = 2, IMGSPAN = 3;
+
+ var myModes = config.browserModes;
+
+ var hintMode;
+ var submode = ""; // used for extended mode, can be "o", "t", "y", etc.
+ var hintString = ""; // the typed string part of the hint is in this string
+ var hintNumber = 0; // only the numerical part of the hint
+ var usedTabKey = false; // when we used <Tab> to select an element
+ var prevInput = ""; // record previous user input type, "text" || "number"
+
+ // hints[] = [elem, text, span, imgspan, elem.style.backgroundColor, elem.style.color]
+ var pageHints = [];
+ var validHints = []; // store the indices of the "hints" array with valid elements
+
+ var escapeNumbers = false; // escape mode for numbers. true -> treated as hint-text
+ var activeTimeout = null; // needed for hinttimeout > 0
+ var canUpdate = false;
+
+ // keep track of the documents which we generated the hints for
+ // docs = { doc: document, start: start_index in hints[], end: end_index in hints[] }
+ var docs = [];
+
+ const Mode = new Struct("prompt", "action", "tags");
+ Mode.defaultValue("tags", function () function () options.hinttags);
+ function extended() options.extendedhinttags;
+ const hintModes = {
+ ";": Mode("Focus hint", function (elem) buffer.focusElement(elem), extended),
+ a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)),
+ s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)),
+ o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)),
+ t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)),
+ b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)),
+ v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended),
+ V: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, true), extended),
+ w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended),
+
+ "?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended),
+ O: Mode("Open location based on hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)),
+ T: Mode("Open new tab based on hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)),
+ W: Mode("Open new window based on hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)),
+ y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)),
+ Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended),
+ };
+
+ // reset all important variables
+ function reset()
+ {
+ statusline.updateInputBuffer("");
+ hintString = "";
+ hintNumber = 0;
+ usedTabKey = false;
+ prevInput = "";
+ pageHints = [];
+ validHints = [];
+ canUpdate = false;
+ docs = [];
+ escapeNumbers = false;
+
+ if (activeTimeout)
+ clearTimeout(activeTimeout);
+ activeTimeout = null;
+ }
+
+ function updateStatusline()
+ {
+ statusline.updateInputBuffer((escapeNumbers ? mappings.getMapLeader() : "") + (hintNumber || ""));
+ }
+
+ function generate(win)
+ {
+ if (!win)
+ win = window.content;
+
+ var doc = win.document;
+ var height = win.innerHeight;
+ var width = win.innerWidth;
+ var scrollX = doc.defaultView.scrollX;
+ var scrollY = doc.defaultView.scrollY;
+
+ var baseNodeAbsolute = util.xmlToDom(<span highlight="Hint"/>, doc);
+
+ var elem, tagname, text, span, rect;
+ var res = buffer.evaluateXPath(hintMode.tags(), doc, null, true);
+
+ var fragment = util.xmlToDom(<div highlight="hints"/>, doc);
+ var start = pageHints.length;
+ for (let elem in res)
+ {
+ // TODO: for iframes, this calculation is wrong
+ rect = elem.getBoundingClientRect();
+ if (!rect || rect.top > height || rect.bottom < 0 || rect.left > width || rect.right < 0)
+ continue;
+
+ rect = elem.getClientRects()[0];
+ if (!rect)
+ continue;
+
+ var computedStyle = doc.defaultView.getComputedStyle(elem, null);
+ if (computedStyle.getPropertyValue("visibility") == "hidden" || computedStyle.getPropertyValue("display") == "none")
+ continue;
+
+ // TODO: mozilla docs recommend localName instead of tagName
+ tagname = elem.tagName.toLowerCase();
+ if (tagname == "input" || tagname == "textarea")
+ text = elem.value;
+ else if (tagname == "select")
+ {
+ if (elem.selectedIndex >= 0)
+ text = elem.item(elem.selectedIndex).text;
+ else
+ text = "";
+ }
+ else
+ text = elem.textContent.toLowerCase();
+
+ span = baseNodeAbsolute.cloneNode(true);
+ span.style.left = (rect.left + scrollX) + "px";
+ span.style.top = (rect.top + scrollY) + "px";
+ fragment.appendChild(span);
+
+ pageHints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color]);
+ }
+
+ if (doc.body)
+ {
+ doc.body.appendChild(fragment);
+ docs.push({ doc: doc, start: start, end: pageHints.length - 1 });
+ }
+
+ // also generate hints for frames
+ Array.forEach(win.frames, function (frame) { generate(frame); });
+
+ return true;
+ }
+
+ // TODO: make it aware of imgspans
+ function showActiveHint(newID, oldID)
+ {
+ var oldElem = validHints[oldID - 1];
+ if (oldElem)
+ setClass(oldElem, false);
+
+ var newElem = validHints[newID - 1];
+ if (newElem)
+ setClass(newElem, true);
+ }
+
+ function setClass(elem, active)
+ {
+ let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " ";
+ if (active)
+ elem.setAttributeNS(NS.uri, "highlight", prefix + "HintActive");
+ else
+ elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem");
+ }
+
+ function showHints()
+ {
+
+ let elem, tagname, text, rect, span, imgspan;
+ let hintnum = 1;
+ let validHint = hintMatcher(hintString.toLowerCase());
+ let activeHint = hintNumber || 1;
+ validHints = [];
+
+ for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
+ {
+ let scrollX = doc.defaultView.scrollX;
+ let scrollY = doc.defaultView.scrollY;
+
+ inner:
+ for (let i in (util.interruptableRange(start, end + 1, 500)))
+ {
+ let hint = pageHints[i];
+ [elem, text, span, imgspan] = hint;
+
+ if (!validHint(text))
+ {
+ span.style.display = "none";
+ if (imgspan)
+ imgspan.style.display = "none";
+
+ elem.removeAttributeNS(NS.uri, "highlight");
+ continue inner;
+ }
+
+ if (text == "" && elem.firstChild && elem.firstChild.tagName == "IMG")
+ {
+ if (!imgspan)
+ {
+ rect = elem.firstChild.getBoundingClientRect();
+ if (!rect)
+ continue;
+
+ imgspan = util.xmlToDom(<span highlight="Hint"/>, doc);
+ imgspan.setAttributeNS(NS.uri, "class", "HintImage");
+ imgspan.style.left = (rect.left + scrollX) + "px";
+ imgspan.style.top = (rect.top + scrollY) + "px";
+ imgspan.style.width = (rect.right - rect.left) + "px";
+ imgspan.style.height = (rect.bottom - rect.top) + "px";
+ hint[IMGSPAN] = imgspan;
+ span.parentNode.appendChild(imgspan);
+ }
+ setClass(imgspan, activeHint == hintnum)
+ }
+
+ span.setAttribute("number", hintnum++);
+ if (imgspan)
+ imgspan.setAttribute("number", hintnum);
+ else
+ setClass(elem, activeHint == hintnum);
+ validHints.push(elem);
+ }
+ }
+
+ if (options.usermode)
+ {
+ let css = [];
+ // FIXME: Broken for imgspans.
+ for (let [,{ doc: doc }] in Iterator(docs))
+ {
+ for (let elem in buffer.evaluateXPath("//*[@liberator:highlight and @number]", doc))
+ {
+ let group = elem.getAttributeNS(NS.uri, "highlight");
+ css.push(highlight.selector(group) + "[number='" + elem.getAttribute("number") + "'] { " + elem.style.cssText + " }");
+ }
+ }
+ styles.addSheet("hint-positions", "*", css.join("\n"), true, true);
+ }
+
+ return true;
+ }
+
+ function removeHints(timeout)
+ {
+ var firstElem = validHints[0] || null;
+
+ for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
+ {
+ for (let elem in buffer.evaluateXPath("//*[@liberator:highlight='hints']", doc))
+ elem.parentNode.removeChild(elem);
+ for (let i in util.range(start, end + 1))
+ {
+ let hint = pageHints[i];
+ if (!timeout || hint[ELEM] != firstElem)
+ hint[ELEM].removeAttributeNS(NS.uri, "highlight");
+ }
+
+ // animate the disappearance of the first hint
+ if (timeout && firstElem)
+ {
+ // USE THIS FOR MAKING THE SELECTED ELEM RED
+ // firstElem.style.backgroundColor = "red";
+ // firstElem.style.color = "white";
+ // setTimeout(function () {
+ // firstElem.style.backgroundColor = firstElemBgColor;
+ // firstElem.style.color = firstElemColor;
+ // }, 200);
+ // OR USE THIS FOR BLINKING:
+ // var counter = 0;
+ // var id = setInterval(function () {
+ // firstElem.style.backgroundColor = "red";
+ // if (counter % 2 == 0)
+ // firstElem.style.backgroundColor = "yellow";
+ // else
+ // firstElem.style.backgroundColor = "#88FF00";
+ //
+ // if (counter++ >= 2)
+ // {
+ // firstElem.style.backgroundColor = firstElemBgColor;
+ // firstElem.style.color = firstElemColor;
+ // clearTimeout(id);
+ // }
+ // }, 100);
+ setTimeout(function () { firstElem.removeAttributeNS(NS.uri, "highlight") }, timeout);
+ }
+ }
+ styles.removeSheet("hint-positions", null, null, null, true);
+
+ reset();
+ }
+
+ function processHints(followFirst)
+ {
+ if (validHints.length == 0)
+ {
+ liberator.beep();
+ return false;
+ }
+
+ if (options["followhints"] > 0)
+ {
+ if (!followFirst)
+ return false; // no return hit; don't examine uniqueness
+
+ // OK. return hit. But there's more than one hint, and
+ // there's no tab-selected current link. Do not follow in mode 2
+ if (options["followhints"] == 2 && validHints.length > 1 && !hintNumber)
+ return liberator.beep();
+ }
+
+ if (!followFirst)
+ {
+ let firstHref = validHints[0].getAttribute("href") || null;
+ if (firstHref)
+ {
+ if (validHints.some(function (e) e.getAttribute("href") != firstHref))
+ return false;
+ }
+ else if (validHints.length > 1)
+ {
+ return false;
+ }
+ }
+
+ var timeout = followFirst || events.feedingKeys ? 0 : 500;
+ var activeIndex = (hintNumber ? hintNumber - 1 : 0);
+ var elem = validHints[activeIndex];
+ removeHints(timeout);
+
+ if (timeout == 0)
+ // force a possible mode change, based on wheter an input field has focus
+ events.onFocusChange();
+ setTimeout(function () {
+ if (modes.extended & modes.HINTS)
+ modes.reset();
+ hintMode.action(elem, elem.href || "");
+ }, timeout);
+ return true;
+ }
+
+ function onInput (event)
+ {
+ prevInput = "text";
+
+ // clear any timeout which might be active after pressing a number
+ if (activeTimeout)
+ {
+ clearTimeout(activeTimeout);
+ activeTimeout = null;
+ }
+
+ hintNumber = 0;
+ hintString = commandline.getCommand();
+ updateStatusline();
+ showHints();
+ if (validHints.length == 1)
+ processHints(false);
+ }
+
+ function hintMatcher(hintString) //{{{
+ {
+ function containsMatcher(hintString) //{{{
+ {
+ var tokens = hintString.split(/ +/);
+ return function (linkText) tokens.every(function (token) linkText.indexOf(token) >= 0);
+ } //}}}
+
+ function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{
+ {
+ var hintStrings = hintString.split(/ +/);
+ var wordSplitRegex = new RegExp(options["wordseparators"]);
+
+ // What the **** does this do? --Kris
+ function charsAtBeginningOfWords(chars, words, allowWordOverleaping)
+ {
+ var charIdx = 0;
+ var numMatchedWords = 0;
+ for (let [,word] in Iterator(words))
+ {
+ if (word.length == 0)
+ continue;
+
+ let wcIdx = 0;
+ // Check if the current word matches same characters as the previous word.
+ // Each already matched word has matched at least one character.
+ if (charIdx > numMatchedWords)
+ {
+ let matchingStarted = false;
+ for (let i in util.range(numMatchedWords, charIdx))
+ {
+ if (chars[i] == word[wcIdx])
+ {
+ matchingStarted = true;
+ wcIdx++;
+ }
+ else if (matchingStarted)
+ {
+ wcIdx = 0;
+ break;
+ }
+ }
+ }
+
+ // the current word matches same characters as the previous word
+ var prevCharIdx;
+ if (wcIdx > 0)
+ {
+ prevCharIdx = charIdx;
+ // now check if it matches additional characters
+ for (; wcIdx < word.length && charIdx < chars.length; wcIdx++, charIdx++)
+ {
+ if (word[wcIdx] != chars[charIdx])
+ break;
+ }
+
+ // the word doesn't match additional characters, now check if the
+ // already matched characters are equal to the next characters for matching,
+ // if yes, then consume them
+ if (prevCharIdx == charIdx)
+ {
+ for (let i = 0; i < wcIdx && charIdx < chars.length; i++, charIdx++)
+ {
+ if (word[i] != chars[charIdx])
+ break;
+ }
+ }
+
+ numMatchedWords++;
+ }
+ // the current word doesn't match same characters as the previous word, just
+ // try to match the next characters
+ else
+ {
+ prevCharIdx = charIdx;
+ for (let i = 0; i < word.length && charIdx < chars.length; i++, charIdx++)
+ {
+ if (word[i] != chars[charIdx])
+ break;
+ }
+
+ if (prevCharIdx == charIdx)
+ {
+ if (!allowWordOverleaping)
+ return false;
+ }
+ else
+ numMatchedWords++;
+ }
+
+ if (charIdx == chars.length)
+ return true;
+ }
+
+ return (charIdx == chars.length);
+ }
+
+ function stringsAtBeginningOfWords(strings, words, allowWordOverleaping)
+ {
+ var strIdx = 0;
+ for (let [,word] in Iterator(words))
+ {
+ if (word.length == 0)
+ continue;
+
+ let str = strings[strIdx];
+ if (str.length == 0 || word.indexOf(str) == 0)
+ strIdx++;
+ else if (!allowWordOverleaping)
+ return false;
+
+ if (strIdx == strings.length)
+ return true;
+ }
+
+ for (; strIdx < strings.length; strIdx++)
+ {
+ if (strings[strIdx].length != 0)
+ return false;
+ }
+ return true;
+ }
+
+ function wordStartsWith(linkText)
+ {
+ if (hintStrings.length == 1 && hintStrings[0].length == 0)
+ return true;
+
+ let words = linkText.split(wordSplitRegex).map(String.toLowerCase);
+ if (hintStrings.length == 1)
+ return charsAtBeginningOfWords(hintStrings[0], words, allowWordOverleaping);
+ else
+ return stringsAtBeginningOfWords(hintStrings, words, allowWordOverleaping);
+ }
+
+ return wordStartsWith;
+ } //}}}
+
+ var hintMatching = options["hintmatching"];
+ switch (hintMatching)
+ {
+ case "contains" : return containsMatcher(hintString);
+ case "wordstartswith": return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ true);
+ case "firstletters" : return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ false);
+ case "custom" : return liberator.plugins.customHintMatcher(hintString);
+ default : liberator.echoerr("Invalid hintmatching type: " + hintMatching);
+ }
+ return null;
+ } //}}}
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const DEFAULT_HINTTAGS = "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
+ "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
+ "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
+ "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select";
+
+ options.add(["extendedhinttags", "eht"],
+ "XPath string of hintable elements activated by ';'",
+ "string", DEFAULT_HINTTAGS);
+
+ options.add(["hinttags", "ht"],
+ "XPath string of hintable elements activated by 'f' and 'F'",
+ "string", DEFAULT_HINTTAGS);
+
+ options.add(["hinttimeout", "hto"],
+ "Timeout before automatically following a non-unique numerical hint",
+ "number", 0,
+ { validator: function (value) value >= 0 });
+
+ options.add(["followhints", "fh"],
+ // FIXME: this description isn't very clear but I can't think of a
+ // better one right now.
+ "Change the behaviour of <Return> in hint mode",
+ "number", 0,
+ {
+ completer: function () [
+ ["0", "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on [m]<Return>[m]."],
+ ["1", "Follow the selected hint on [m]<Return>[m]."],
+ ["2", "Follow the selected hint on [m]<Return>[m] only it's been [m]<Tab>[m]-selected."]
+ ],
+ validator: function (value) Option.validateCompleter
+ });
+
+ options.add(["hintmatching", "hm"],
+ "How links are matched",
+ "string", "contains",
+ {
+ completer: function (filter)
+ {
+ return [[m, ""] for each (m in ["contains", "wordstartswith", "firstletters", "custom"])];
+ },
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wordseparators", "wsp"],
+ "How words are split for hintmatching",
+ "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]');
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ mappings.add(myModes, ["f"],
+ "Start QuickHint mode",
+ function () { hints.show("o"); });
+
+ mappings.add(myModes, ["F"],
+ "Start QuickHint mode, but open link in a new tab",
+ function () { hints.show("t"); });
+
+ mappings.add(myModes, [";"],
+ "Start an extended hint mode",
+ function (arg) { hints.show(arg); },
+ { flags: Mappings.flags.ARGUMENT });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ addMode: function (mode)
+ {
+ hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1));
+ },
+
+ show: function (minor, filter, win)
+ {
+ hintMode = hintModes[minor];
+ if (!hintMode)
+ {
+ liberator.beep();
+ return;
+ }
+ commandline.input(hintMode.prompt + ":", null, { onChange: onInput });
+ modes.extended = modes.HINTS;
+
+ submode = minor;
+ hintString = filter || "";
+ hintNumber = 0;
+ usedTab = false;
+ prevInput = "";
+ canUpdate = false;
+
+ generate(win);
+
+ // get all keys from the input queue
+ liberator.threadYield(true);
+
+ canUpdate = true;
+ showHints();
+
+ if (validHints.length == 0)
+ {
+ liberator.beep();
+ modes.reset();
+ return false;
+ }
+ else if (validHints.length == 1)
+ {
+ processHints(true);
+ return false;
+ }
+ else // still hints visible
+ return true;
+ },
+
+ hide: function ()
+ {
+ removeHints(0);
+ },
+
+ onEvent: function (event)
+ {
+ var key = events.toString(event);
+ var followFirst = false;
+
+ // clear any timeout which might be active after pressing a number
+ if (activeTimeout)
+ {
+ clearTimeout(activeTimeout);
+ activeTimeout = null;
+ }
+
+ switch (key)
+ {
+ case "<Return>":
+ followFirst = true;
+ break;
+
+ case "<Tab>":
+ case "<S-Tab>":
+ usedTabKey = true;
+ if (hintNumber == 0)
+ hintNumber = 1;
+
+ var oldID = hintNumber;
+ if (key == "<Tab>")
+ {
+ if (++hintNumber > validHints.length)
+ hintNumber = 1;
+ }
+ else
+ {
+ if (--hintNumber < 1)
+ hintNumber = validHints.length;
+ }
+ showActiveHint(hintNumber, oldID);
+ updateStatusline();
+ return;
+
+ case "<BS>":
+ if (hintNumber > 0 && !usedTabKey)
+ {
+ hintNumber = Math.floor(hintNumber / 10);
+ if (hintNumber == 0)
+ prevInput = "text";
+ }
+ else
+ {
+ usedTabKey = false;
+ hintNumber = 0;
+ liberator.beep();
+ return;
+ }
+ break;
+
+ case mappings.getMapLeader():
+ escapeNumbers = !escapeNumbers;
+ if (escapeNumbers && usedTabKey) // hintNumber not used normally, but someone may wants to toggle
+ hintNumber = 0; // <tab>s ? reset. Prevent to show numbers not entered.
+
+ updateStatusline();
+ return;
+
+ default:
+ if (/^\d$/.test(key))
+ {
+ // FIXME: Kludge.
+ if (escapeNumbers)
+ {
+ let cmdline = document.getElementById("liberator-commandline-command");
+ let start = cmdline.selectionStart;
+ let end = cmdline.selectionEnd;
+ cmdline.value = cmdline.value.substr(0, start) + key + cmdline.value.substr(start);
+ cmdline.selectionStart = start + 1;
+ cmdline.selectionEnd = end + 1;
+ return;
+ }
+
+ prevInput = "number";
+
+ var oldHintNumber = hintNumber;
+ if (hintNumber == 0 || usedTabKey)
+ {
+ usedTabKey = false;
+ hintNumber = parseInt(key, 10);
+ }
+ else
+ hintNumber = (hintNumber * 10) + parseInt(key, 10);
+
+ updateStatusline();
+
+ if (!canUpdate)
+ return;
+
+ if (docs.length == 0)
+ {
+ generate();
+ showHints();
+ }
+ showActiveHint(hintNumber, oldHintNumber || 1);
+
+ if (hintNumber == 0 || hintNumber > validHints.length)
+ {
+ liberator.beep();
+ return;
+ }
+
+ // if we write a numeric part like 3, but we have 45 hints, only follow
+ // the hint after a timeout, as the user might have wanted to follow link 34
+ if (hintNumber > 0 && hintNumber * 10 <= validHints.length)
+ {
+ var timeout = options["hinttimeout"];
+ if (timeout > 0)
+ activeTimeout = setTimeout(function () { processHints(true); }, timeout);
+
+ return false;
+ }
+ // we have a unique hint
+ processHints(true);
+ return;
+ }
+ }
+
+ updateStatusline();
+
+ if (canUpdate)
+ {
+ if (docs.length == 0 && hintString.length > 0)
+ generate();
+
+ showHints();
+ processHints(followFirst);
+ }
+ }
+ };
+
+ // FIXME: add resize support
+ // window.addEventListener("resize", onResize, null);
+
+ // function onResize(event)
+ // {
+ // if (event)
+ // doc = event.originalTarget;
+ // else
+ // doc = window.content.document;
+ // }
+
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/io.js b/common/content/io.js
new file mode 100644
index 00000000..5078e898
--- /dev/null
+++ b/common/content/io.js
@@ -0,0 +1,966 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+ Code based on venkman
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+plugins.contexts = {};
+function Script(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])/, function (_0, _1) _1.toUpperCase());
+ this.PATH = file.path;
+ this.__context__ = this;
+
+ // This belongs elsewhere
+ for (let [,dir] in Iterator(io.getRuntimeDirectories("plugin")))
+ {
+ if (dir.contains(file, false))
+ plugins[name] = this.NAME;
+ }
+}
+Script.prototype = plugins;
+
+// TODO: why are we passing around strings rather than file objects?
+function IO() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const WINDOWS = liberator.has("Win32");
+ const EXTENSION_NAME = config.name.toLowerCase(); // "vimperator" or "muttator"
+
+ const ioService = Components.classes['@mozilla.org/network/io-service;1']
+ .getService(Components.interfaces.nsIIOService);
+ const environmentService = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ const directoryService = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties);
+ const downloadManager = Components.classes["@mozilla.org/download-manager;1"]
+ .createInstance(Components.interfaces.nsIDownloadManager);
+
+ var processDir = directoryService.get("CurWorkD", Components.interfaces.nsIFile);
+ var cwd = processDir;
+ var oldcwd = null;
+
+ var lastRunCommand = ""; // updated whenever the users runs a command with :!
+ var scriptNames = [];
+
+ // default option values
+ var cdpath = "," + (environmentService.get("CDPATH").replace(/[:;]/g, ",") || ",");
+ var runtimepath = "~/" + (WINDOWS ? "" : ".") + EXTENSION_NAME;
+ var shell, shellcmdflag;
+
+ if (WINDOWS)
+ {
+ shell = "cmd.exe";
+ // TODO: setting 'shell' to "something containing sh" updates
+ // 'shellcmdflag' appropriately at startup on Windows in Vim
+ shellcmdflag = "/c";
+ }
+ else
+ {
+ shell = environmentService.get("SHELL") || "sh";
+ shellcmdflag = "-c";
+ }
+
+ function expandPathList(list) list.split(",").map(io.expandPath).join(",")
+
+ function getPathsFromPathList(list)
+ {
+ if (!list)
+ return [];
+ else
+ // empty list item means the current directory
+ return list.replace(/,$/, "")
+ .split(",")
+ .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir);
+ }
+
+ function replacePathSep(path)
+ {
+ if (WINDOWS)
+ return path.replace("/", "\\");
+ return path;
+ }
+
+ function joinPaths(head, tail)
+ {
+ let path = ioManager.getFile(head);
+ path.appendRelativePath(ioManager.expandPath(tail)); // FIXME: should only expand env vars and normalise path separators
+ return path;
+ }
+
+ function isAbsolutePath(path)
+ {
+ try
+ {
+ Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile)
+ .initWithPath(path);
+ return true;
+ }
+ catch (e)
+ {
+ return false;
+ }
+ }
+
+ var downloadListener = {
+ onDownloadStateChange: function (state, download)
+ {
+ if (download.state == downloadManager.DOWNLOAD_FINISHED)
+ {
+ let url = download.source.spec;
+ let title = download.displayName;
+ let file = download.targetFile.path;
+ let size = download.size;
+
+ liberator.echomsg("Download of " + title + " to " + file + " finished", 1);
+ autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
+ }
+ },
+ onStateChange: function () {},
+ onProgressChange: function () {},
+ onSecurityChange: function () {}
+ };
+
+ downloadManager.addListener(downloadListener);
+ liberator.registerObserver("shutdown", function () {
+ downloadManager.removeListener(downloadListener);
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["cdpath", "cd"],
+ "List of directories searched when executing :cd",
+ "stringlist", cdpath,
+ { setter: function (value) expandPathList(value) });
+
+ options.add(["runtimepath", "rtp"],
+ "List of directories searched for runtime files",
+ "stringlist", runtimepath,
+ { setter: function (value) expandPathList(value) });
+
+ options.add(["shell", "sh"],
+ "Shell to use for executing :! and :run commands",
+ "string", shell,
+ { setter: function (value) io.expandPath(value) });
+
+ options.add(["shellcmdflag", "shcf"],
+ "Flag passed to shell when executing :! and :run commands",
+ "string", shellcmdflag);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["cd", "chd[ir]"],
+ "Change the current directory",
+ function (args)
+ {
+ args = args.literalArg;
+
+ if (!args)
+ {
+ args = "~";
+ }
+ else if (args == "-")
+ {
+ if (oldcwd)
+ {
+ args = oldcwd.path;
+ }
+ else
+ {
+ liberator.echoerr("E186: No previous directory");
+ return;
+ }
+ }
+
+ args = io.expandPath(args);
+
+ // go directly to an absolute path or look for a relative path
+ // match in 'cdpath'
+ // TODO: handle ../ and ./ paths
+ if (isAbsolutePath(args))
+ {
+ if (io.setCurrentDirectory(args))
+ liberator.echo(io.getCurrentDirectory().path);
+ }
+ else
+ {
+ let dirs = getPathsFromPathList(options["cdpath"]);
+ let found = false;
+
+ for (let [,dir] in Iterator(dirs))
+ {
+ dir = joinPaths(dir, args);
+
+ if (dir.exists() && dir.isDirectory() && dir.isReadable())
+ {
+ io.setCurrentDirectory(dir.path);
+ liberator.echo(io.getCurrentDirectory().path);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ liberator.echoerr("E344: Can't find directory \"" + args + "\" in cdpath"
+ + "\n"
+ + "E472: Command failed");
+ }
+ }
+ },
+ {
+ argCount: "?",
+ completer: function (context) completion.directory(context, true),
+ literal: 0
+ });
+
+ // NOTE: this command is only used in :source
+ commands.add(["fini[sh]"],
+ "Stop sourcing a script file",
+ function () { liberator.echoerr("E168: :finish used outside of a sourced file"); },
+ { argCount: "0" });
+
+ commands.add(["pw[d]"],
+ "Print the current directory name",
+ function () { liberator.echo(io.getCurrentDirectory().path); },
+ { argCount: "0" });
+
+ // "mkv[imperatorrc]" or "mkm[uttatorrc]"
+ commands.add([EXTENSION_NAME.replace(/(.)(.*)/, "mk$1[$2rc]")],
+ "Write current key mappings and changed options to the config file",
+ function (args)
+ {
+ // TODO: "E172: Only one file name allowed"
+ let filename = args[0] || "~/" + (WINDOWS ? "_" : ".") + EXTENSION_NAME + "rc";
+ let file = io.getFile(filename);
+
+ if (file.exists() && !args.bang)
+ {
+ liberator.echoerr("E189: \"" + filename + "\" exists (add ! to override)");
+ return;
+ }
+
+ // FIXME: Use a set/specifiable list here:
+ let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)];
+ lines = util.Array.flatten(lines);
+
+ // :mkvimrc doesn't save autocommands, so we don't either - remove this code at some point
+ // line += "\n\" Auto-Commands\n";
+ // for (let item in autocommands)
+ // line += "autocmd " + item.event + " " + item.pattern.source + " " + item.command + "\n";
+
+ // if (mappings.getMapLeader() != "\\")
+ // line += "\nlet mapleader = \"" + mappings.getMapLeader() + "\"\n";
+
+ // source a user .vimperatorrc file
+ lines.unshift('"' + liberator.version);
+ lines.push("\nsource! " + filename + ".local");
+ lines.push("\n\" vim: set ft=vimperator:");
+
+ try
+ {
+ io.writeFile(file, lines.join("\n"));
+ }
+ catch (e)
+ {
+ liberator.echoerr("E190: Cannot open \"" + filename + "\" for writing");
+ liberator.log("Could not write to " + file.path + ": " + e.message); // XXX
+ }
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.file(context, true)
+ });
+
+ commands.add(["runt[ime]"],
+ "Source the specified file from each directory in 'runtimepath'",
+ function (args) { io.sourceFromRuntimePath(args, args.bang); },
+ {
+ argCount: "+",
+ bang: true
+ }
+ );
+
+ 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(scriptNames)))); // TODO: add colon?
+
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+ { argCount: "0" });
+
+ commands.add(["so[urce]"],
+ "Read Ex commands from a file",
+ function (args)
+ {
+ // FIXME: "E172: Only one file name allowed"
+ io.source(args[0], args.bang);
+ },
+ {
+ argCount: "1",
+ bang: true,
+ completer: function (context) completion.file(context, true)
+ });
+
+ commands.add(["!", "run"],
+ "Run a command",
+ function (args)
+ {
+ let special = args.bang;
+ args = args.string;
+
+ // :!! needs to be treated specially as the command parser sets the
+ // special flag but removes the ! from args
+ if (special)
+ args = "!" + args;
+
+ // replaceable bang and no previous command?
+ if (/((^|[^\\])(\\\\)*)!/.test(args) && !lastRunCommand)
+ {
+ liberator.echoerr("E34: No previous command");
+ return;
+ }
+
+ // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
+ // pass through a raw bang when escaped or substitute the last command
+ args = args.replace(/(\\)*!/g,
+ function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", lastRunCommand)
+ );
+
+ lastRunCommand = args;
+
+ let output = io.system(args);
+ let command = ":" + util.escapeHTML(commandline.getCommand()) + "<br/>";
+
+ liberator.echo(template.generic(<span style="white-space: pre">{output}</span>))
+ liberator.echo(command + util.escapeHTML(output));
+
+ autocommands.trigger("ShellCmdPost", {});
+ },
+ {
+ argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk
+ bang: true,
+ completer: function (context) completion.shellCommand(context),
+ literal: 0
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ liberator.registerObserver("load_completion", function ()
+ {
+ completion.setFunctionCompleter([ioManager.getFile, ioManager.expandPath],
+ [function (context, obj, args) {
+ context.quote[2] = "";
+ completion.file(context, true);
+ }]);
+ });
+
+ var ioManager = {
+
+ MODE_RDONLY: 0x01,
+ MODE_WRONLY: 0x02,
+ MODE_RDWR: 0x04,
+ MODE_CREATE: 0x08,
+ MODE_APPEND: 0x10,
+ MODE_TRUNCATE: 0x20,
+ MODE_SYNC: 0x40,
+ MODE_EXCL: 0x80,
+
+ sourcing: null,
+
+ expandPath: function (path)
+ {
+ // TODO: proper pathname separator translation like Vim - this should be done elsewhere
+ if (WINDOWS)
+ path = path.replace("/", "\\", "g");
+
+ // expand "~" to VIMPERATOR_HOME or HOME (USERPROFILE or HOMEDRIVE\HOMEPATH on Windows if HOME is not set)
+ if (/^~/.test(path))
+ {
+ let home = environmentService.get("VIMPERATOR_HOME");
+
+ if (!home)
+ home = environmentService.get("HOME");
+
+ if (WINDOWS && !home)
+ home = environmentService.get("USERPROFILE") ||
+ environmentService.get("HOMEDRIVE") + environmentService.get("HOMEPATH");
+
+ path = home + path.substr(1);
+ }
+
+ // 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?
+ path = path.replace(
+ RegExp("\\$(\\w+)\\b|\\${(\\w+)}" + (WINDOWS ? "|%(\\w+)%" : ""), "g"),
+ function (m, n1, n2, n3) environmentService.get(n1 || n2 || n3) || m
+ );
+
+ return path.replace("\\ ", " ", "g");
+ },
+
+ // TODO: there seems to be no way, short of a new component, to change
+ // Firefox's CWD - see // https://bugzilla.mozilla.org/show_bug.cgi?id=280953
+ getCurrentDirectory: function ()
+ {
+ let dir = ioManager.getFile(cwd.path);
+
+ // NOTE: the directory could have been deleted underneath us so
+ // fallback to Firefox's CWD
+ if (dir.exists() && dir.isDirectory())
+ return dir;
+ else
+ return processDir;
+ },
+
+ setCurrentDirectory: function (newdir)
+ {
+ newdir = newdir || "~";
+
+ if (newdir == "-")
+ {
+ [cwd, oldcwd] = [oldcwd, this.getCurrentDirectory()];
+ }
+ else
+ {
+ let dir = ioManager.getFile(newdir);
+
+ if (!dir.exists() || !dir.isDirectory())
+ {
+ liberator.echoerr("E344: Can't find directory \"" + dir.path + "\" in path");
+ return null;
+ }
+
+ [cwd, oldcwd] = [dir, this.getCurrentDirectory()];
+ }
+
+ return ioManager.getCurrentDirectory();
+ },
+
+ getRuntimeDirectories: function (specialDirectory)
+ {
+ let dirs = getPathsFromPathList(options["runtimepath"]);
+
+ dirs = dirs.map(function (dir) joinPaths(dir, specialDirectory))
+ .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
+
+ return dirs;
+ },
+
+ getRCFile: function (dir)
+ {
+ dir = dir || "~";
+
+ let rcFile1 = joinPaths(dir, "." + EXTENSION_NAME + "rc");
+ let rcFile2 = joinPaths(dir, "_" + EXTENSION_NAME + "rc");
+
+ if (WINDOWS)
+ [rcFile1, rcFile2] = [rcFile2, rcFile1];
+
+ if (rcFile1.exists() && rcFile1.isFile())
+ return rcFile1;
+ else if (rcFile2.exists() && rcFile2.isFile())
+ return rcFile2;
+ else
+ return null;
+ },
+
+ // return a nsILocalFile for path where you can call isDirectory(), etc. on
+ // caller must check with .exists() if the returned file really exists
+ // also expands relative paths
+ getFile: function (path, noCheckPWD)
+ {
+ let file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ let protocolHandler = Components.classes["@mozilla.org/network/protocol;1?name=file"]
+ .createInstance(Components.interfaces.nsIFileProtocolHandler);
+
+ if (/file:\/\//.test(path))
+ {
+ file = protocolHandler.getFileFromURLSpec(path);
+ }
+ else
+ {
+ let expandedPath = ioManager.expandPath(path);
+
+ if (!isAbsolutePath(expandedPath) && !noCheckPWD)
+ file = joinPaths(ioManager.getCurrentDirectory().path, expandedPath);
+ else
+ file.initWithPath(expandedPath);
+ }
+
+ return file;
+ },
+
+ // TODO: make secure
+ // returns a nsILocalFile or null if it could not be created
+ createTempFile: function ()
+ {
+ let tmpName = EXTENSION_NAME + ".tmp";
+
+ switch (EXTENSION_NAME)
+ {
+ case "muttator":
+ tmpName = "mutt-ator-mail"; // to allow vim to :set ft=mail automatically
+ break;
+ case "vimperator":
+ try
+ {
+ if (window.content.document.location.hostname)
+ tmpName = EXTENSION_NAME + "-" + window.content.document.location.hostname + ".tmp";
+ }
+ catch (e) {}
+ break;
+ }
+
+ let file = directoryService.get("TmpD", Components.interfaces.nsIFile);
+ file.append(tmpName);
+ file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
+
+ if (file.exists())
+ return file;
+ else
+ return null; // XXX
+
+ },
+
+ // file is either a full pathname or an instance of file instanceof nsILocalFile
+ readDirectory: function (file, sort)
+ {
+ if (typeof file == "string")
+ file = ioManager.getFile(file);
+ else if (!(file instanceof Components.interfaces.nsILocalFile))
+ throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
+
+ if (file.isDirectory())
+ {
+ let entries = file.directoryEntries;
+ let array = [];
+ while (entries.hasMoreElements())
+ {
+ let entry = entries.getNext();
+ entry.QueryInterface(Components.interfaces.nsIFile);
+ array.push(entry);
+ }
+ if (sort)
+ return array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
+ return array;
+ }
+ else
+ return []; // XXX: or should it throw an error, probably yes?
+ },
+
+ // file is either a full pathname or an instance of file instanceof nsILocalFile
+ // reads a file in "text" mode and returns the string
+ readFile: function (file)
+ {
+ let ifstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ let icstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterInputStream);
+
+ let charset = "UTF-8";
+ if (typeof file == "string")
+ file = ioManager.getFile(file);
+ else if (!(file instanceof Components.interfaces.nsILocalFile))
+ throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
+
+ ifstream.init(file, -1, 0, 0);
+ const replacementChar = Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
+ icstream.init(ifstream, charset, 4096, replacementChar); // 4096 bytes buffering
+
+ let buffer = "";
+ let str = {};
+ while (icstream.readString(4096, str) != 0)
+ buffer += str.value;
+
+ icstream.close();
+ ifstream.close();
+
+ return buffer;
+ },
+
+ // file is either a full pathname or an instance of file instanceof nsILocalFile
+ // default permission = 0644, only used when creating a new file, does not change permissions if the file exists
+ // mode can be ">" or ">>" in addition to the normal MODE_* flags
+ writeFile: function (file, buf, mode, perms)
+ {
+ let ofstream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Components.interfaces.nsIFileOutputStream);
+ let ocstream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+
+ let charset = "UTF-8"; // Can be any character encoding name that Mozilla supports
+ if (typeof file == "string")
+ file = ioManager.getFile(file);
+ else if (!(file instanceof Components.interfaces.nsILocalFile))
+ throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
+
+ if (mode == ">>")
+ mode = ioManager.MODE_WRONLY | ioManager.MODE_CREATE | ioManager.MODE_APPEND;
+ else if (!mode || mode == ">")
+ mode = ioManager.MODE_WRONLY | ioManager.MODE_CREATE | ioManager.MODE_TRUNCATE;
+
+ if (!perms)
+ perms = 0644;
+
+ ofstream.init(file, mode, perms, 0);
+ ocstream.init(ofstream, charset, 0, 0x0000);
+ ocstream.writeString(buf);
+
+ ocstream.close();
+ ofstream.close();
+ },
+
+ run: function (program, args, blocking)
+ {
+ args = args || [];
+ blocking = !!blocking;
+
+ let file;
+
+ if (isAbsolutePath(program))
+ {
+ file = ioManager.getFile(program, true);
+ }
+ else
+ {
+ let dirs = environmentService.get("PATH").split(WINDOWS ? ";" : ":");
+ // Windows tries the cwd first TODO: desirable?
+ if (WINDOWS)
+ dirs = [io.getCurrentDirectory().path].concat(dirs);
+
+lookup:
+ for (let [,dir] in Iterator(dirs))
+ {
+ file = joinPaths(dir, program);
+ try
+ {
+ if (file.exists())
+ break;
+
+ // TODO: couldn't we just palm this off to the start command?
+ // automatically try to add the executable path extensions on windows
+ if (WINDOWS)
+ {
+ let extensions = environmentService.get("PATHEXT").split(";");
+ for (let [,extension] in Iterator(extensions))
+ {
+ file = joinPaths(dir, program + extension);
+ if (file.exists())
+ break lookup;
+ }
+ }
+ }
+ catch (e) {}
+ }
+ }
+
+ if (!file || !file.exists())
+ {
+ liberator.echoerr("Command not found: " + program);
+ return -1;
+ }
+
+ let process = Components.classes["@mozilla.org/process/util;1"]
+ .createInstance(Components.interfaces.nsIProcess);
+
+ process.init(file);
+ process.run(blocking, args, args.length);
+
+ return process.exitValue;
+ },
+
+ // when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is fixed
+ // is fixed, should use that instead of a tmpfile
+ system: function (command, input)
+ {
+ liberator.echomsg("Calling shell to execute: " + command, 4);
+
+ let stdoutFile = ioManager.createTempFile();
+ let stderrFile = ioManager.createTempFile();
+
+ function escapeQuotes(str) str.replace('"', '\\"', "g");
+
+ if (!stdoutFile || !stderrFile) // FIXME: error reporting
+ return "";
+
+ if (WINDOWS)
+ command = "cd /D " + cwd.path + " && " + command + " > " + stdoutFile.path + " 2> " + stderrFile.path;
+ else
+ // TODO: should we only attempt the actual command conditionally on a successful cd?
+ command = "cd " + escapeQuotes(cwd.path) + "; " + command + " > \"" + escapeQuotes(stdoutFile.path) + "\""
+ + " 2> \"" + escapeQuotes(stderrFile.path) + "\"";
+
+ let stdinFile = null;
+
+ if (input)
+ {
+ stdinFile = ioManager.createTempFile(); // FIXME: no returned file?
+ ioManager.writeFile(stdinFile, input);
+ command += " < \"" + escapeQuotes(stdinFile.path) + "\"";
+ }
+
+ let res = ioManager.run(options["shell"], [options["shellcmdflag"], command], true);
+
+ if (res > 0)
+ var output = ioManager.readFile(stderrFile) + "\nshell returned " + res;
+ else
+ var output = ioManager.readFile(stdoutFile);
+
+ stdoutFile.remove(false);
+ stderrFile.remove(false);
+
+ if (stdinFile)
+ stdinFile.remove(false);
+
+ // if there is only one \n at the end, chop it off
+ if (output && output.indexOf("\n") == output.length - 1)
+ output = output.substr(0, output.length - 1);
+
+ return output;
+ },
+
+ // FIXME: multiple paths?
+ sourceFromRuntimePath: function (paths, all)
+ {
+ let dirs = getPathsFromPathList(options["runtimepath"]);
+ let found = false;
+
+ // FIXME: should use original arg string
+ liberator.echomsg("Searching for \"" + paths.join(" ") + "\" in \"" + options["runtimepath"] + "\"", 2);
+
+ outer:
+ for (let [,dir] in Iterator(dirs))
+ {
+ for (let [,path] in Iterator(paths))
+ {
+ let file = joinPaths(dir, path);
+
+ liberator.echomsg("Searching for \"" + file.path, 3);
+
+ if (file.exists() && file.isFile() && file.isReadable())
+ {
+ io.source(file.path, false);
+ found = true;
+
+ if (!all)
+ break outer;
+ }
+ }
+ }
+
+ if (!found)
+ liberator.echomsg("not found in 'runtimepath': \"" + paths.join(" ") + "\"", 1); // FIXME: should use original arg string
+
+ return found;
+ },
+
+ // files which end in .js are sourced as pure javascript files,
+ // no need (actually forbidden) to add: js <<EOF ... EOF around those files
+ source: function (filename, silent)
+ {
+ let wasSourcing = ioManager.sourcing;
+ try
+ {
+ var file = ioManager.getFile(filename);
+ ioManager.sourcing = {
+ file: file.path,
+ line: 0
+ };
+
+ if (!file.exists() || !file.isReadable() || file.isDirectory())
+ {
+ if (!silent)
+ {
+ if (file.exists() && file.isDirectory())
+ liberator.echomsg("Cannot source a directory: \"" + filename + "\"", 0);
+ else
+ liberator.echomsg("could not source: \"" + filename + "\"", 1);
+
+ liberator.echoerr("E484: Can't open file " + filename);
+ }
+
+ return;
+ }
+
+ liberator.echomsg("sourcing \"" + filename + "\"", 2);
+
+ let str = ioManager.readFile(file);
+ let uri = ioService.newFileURI(file);
+
+ // handle pure javascript files specially
+ if (/\.js$/.test(filename))
+ {
+ try
+ {
+ liberator.loadScript(uri.spec, new Script(file));
+ }
+ catch (e)
+ {
+ let err = new Error();
+ for (let [k, v] in Iterator(e))
+ err[k] = v;
+ err.echoerr = file.path + ":" + e.lineNumber + ": " + e;
+ throw err;
+ }
+ }
+ else if (/\.css$/.test(filename))
+ {
+ storage.styles.registerSheet(uri.spec, !silent, true);
+ }
+ else
+ {
+ let heredoc = "";
+ let heredocEnd = null; // the string which ends the heredoc
+ let lines = str.split("\n");
+
+ for (let [i, line] in Iterator(lines))
+ {
+ if (heredocEnd) // we already are in a heredoc
+ {
+ if (heredocEnd.test(line))
+ {
+ command.execute(heredoc, special, count);
+ heredoc = "";
+ heredocEnd = null;
+ }
+ else
+ {
+ heredoc += line + "\n";
+ }
+ }
+ else
+ {
+ ioManager.sourcing.line = i + 1;
+ // skip line comments and blank lines
+ if (/^\s*(".*)?$/.test(line))
+ continue;
+
+ var [count, cmd, special, args] = commands.parseCommand(line);
+ var command = commands.get(cmd);
+
+ if (!command)
+ {
+ let lineNumber = i + 1;
+
+ // FIXME: messages need to be able to specify
+ // whether they can be cleared/overwritten or
+ // should be appended to and the MOW opened
+ liberator.echoerr("Error detected while processing " + file.path,
+ commandline.FORCE_MULTILINE);
+ commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR,
+ commandline.APPEND_TO_MESSAGES);
+ liberator.echoerr("E492: Not an editor command: " + line);
+ }
+ else
+ {
+ if (command.name == "finish")
+ {
+ break;
+ }
+ else if (command.hereDoc)
+ {
+ // check for a heredoc
+ let matches = args.match(/(.*)<<\s*(\S+)$/);
+
+ if (matches)
+ {
+ args = matches[1];
+ heredocEnd = new RegExp("^" + matches[2] + "$", "m");
+ if (matches[1])
+ heredoc = matches[1] + "\n";
+ }
+ else
+ {
+ command.execute(args, special, count);
+ }
+ }
+ else
+ {
+ // execute a normal liberator command
+ liberator.execute(line);
+ }
+ }
+ }
+ }
+
+ // if no heredoc-end delimiter is found before EOF then
+ // process the heredoc anyway - Vim compatible ;-)
+ if (heredocEnd)
+ command.execute(heredoc, special, count);
+ }
+
+ if (scriptNames.indexOf(file.path) == -1)
+ scriptNames.push(file.path);
+
+ liberator.echomsg("finished sourcing \"" + filename + "\"", 2);
+
+ liberator.log("Sourced: " + file.path, 3);
+ }
+ catch (e)
+ {
+ let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
+ liberator.reportError(e);
+ if (!silent)
+ liberator.echoerr(message);
+ }
+ finally
+ {
+ ioManager.sourcing = wasSourcing;
+ }
+ }
+ }; //}}}
+
+ return ioManager;
+
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/liberator-overlay.js b/common/content/liberator-overlay.js
new file mode 100644
index 00000000..5175b75b
--- /dev/null
+++ b/common/content/liberator-overlay.js
@@ -0,0 +1,58 @@
+
+(function () {
+ const modules = {};
+ const BASE = "chrome://liberator/content/";
+
+ modules.modules = modules;
+
+ const loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader);
+ function load(script)
+ {
+ for (let [i, base] in Iterator(prefix))
+ {
+ try
+ {
+ loader.loadSubScript(base + script, modules)
+ return;
+ }
+ catch (e)
+ {
+ if (i + 1 < prefix.length)
+ continue;
+ if (Components.utils.reportError)
+ Components.utils.reportError(e);
+ dump("liberator: Loading script " + script + ": " + e + "\n");
+ }
+ }
+ }
+
+ Components.utils.import("resource://liberator/storage.jsm", modules);
+
+ let prefix = [BASE];
+
+ ["liberator.js",
+ "config.js",
+ "util.js",
+ "style.js",
+ "buffer.js",
+ "commands.js",
+ "completion.js",
+ "editor.js",
+ "events.js",
+ "find.js",
+ "hints.js",
+ "io.js",
+ "mappings.js",
+ "modes.js",
+ "options.js",
+ "template.js",
+ "ui.js"].forEach(load);
+
+ prefix.unshift("chrome://" + modules.config.name.toLowerCase() + "/content/");
+ if (modules.config.scripts)
+ modules.config.scripts.forEach(load);
+
+})()
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/liberator.js b/common/content/liberator.js
new file mode 100644
index 00000000..9f6472d8
--- /dev/null
+++ b/common/content/liberator.js
@@ -0,0 +1,1360 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const plugins = {};
+plugins.__proto__ = modules;
+
+const EVAL_ERROR = "__liberator_eval_error";
+const EVAL_RESULT = "__liberator_eval_result";
+const EVAL_STRING = "__liberator_eval_string";
+const userContext = {
+ __proto__: modules
+};
+
+const liberator = (function () //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const threadManager = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService(Components.interfaces.nsIThreadManager);
+ function Runnable(self, func, args)
+ {
+ this.self = self;
+ this.func = func;
+ this.args = args;
+ }
+ Runnable.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIRunnable]),
+ run: function () { this.func.apply(this.self, this.args); }
+ };
+
+ var callbacks = [];
+ var observers = [];
+ function registerObserver(type, callback)
+ {
+ observers.push([type, callback]);
+ }
+
+ function loadModule(name, func)
+ {
+ var message = "Loading module " + name + "...";
+ try
+ {
+ liberator.log(message, 0);
+ liberator.dump(message);
+ modules[name] = func();
+ liberator.triggerObserver("load_" + name, name);
+ }
+ catch (e)
+ {
+ window.toJavaScriptConsole();
+ liberator.reportError(e);
+ }
+ }
+
+ // Only general options are added here, which are valid for all vimperator like extensions
+ registerObserver("load_options", function ()
+ {
+ options.add(["errorbells", "eb"],
+ "Ring the bell when an error message is displayed",
+ "boolean", false);
+
+ options.add(["exrc", "ex"],
+ "Allow reading of an RC file in the current directory",
+ "boolean", false);
+
+ const tabopts = [
+ ["n", "Tab number", null, highlight.selector("TabNumber")],
+ ["N", "Tab number over icon", null, highlight.selector("TabIconNumber")]
+ ];
+ options.add(["guioptions", "go"],
+ "Show or hide certain GUI elements like the menu or toolbar",
+ "charlist", config.defaults.guioptions || "",
+ {
+ setter: function (value)
+ {
+ for (let [opt, ids] in Iterator(config.guioptions))
+ {
+ ids.forEach(function (id)
+ {
+ try
+ {
+ document.getElementById(id).collapsed = (value.indexOf(opt) < 0);
+ }
+ catch (e) {}
+ });
+ }
+ let classes = tabopts.filter(function (o) value.indexOf(o[0]) == -1)
+ .map(function (a) a[3])
+ if (!classes.length)
+ {
+ styles.removeSheet("taboptions", null, null, null, true);
+ }
+ else
+ {
+ storage.styles.addSheet("taboptions", "chrome://*", classes.join(",") + "{ display: none; }", true, true);
+ statusline.updateTabCount();
+ }
+
+ return value;
+ },
+ completer: function (filter)
+ {
+ return [
+ ["m", "Menubar"],
+ ["T", "Toolbar"],
+ ["b", "Bookmark bar"]
+ ].concat(!liberator.has("tabs") ? [] : tabopts);
+ },
+ validator: Option.validateCompleter
+ });
+
+ options.add(["helpfile", "hf"],
+ "Name of the main help file",
+ "string", "intro.html");
+
+ options.add(["loadplugins", "lpl"],
+ "Load plugin scripts when starting up",
+ "boolean", true);
+
+ options.add(["verbose", "vbs"],
+ "Define which info messages are displayed",
+ "number", 0,
+ { validator: function (value) value >= 0 && value <= 15 });
+
+ options.add(["visualbell", "vb"],
+ "Use visual bell instead of beeping on errors",
+ "boolean", false,
+ {
+ setter: function (value)
+ {
+ options.setPref("accessibility.typeaheadfind.enablesound", !value);
+ return value;
+ }
+ });
+ });
+
+ registerObserver("load_mappings", function ()
+ {
+ mappings.add(modes.all, ["<F1>"],
+ "Open help window",
+ function () { liberator.help(); });
+
+ if (liberator.has("session"))
+ {
+ mappings.add([modes.NORMAL], ["ZQ"],
+ "Quit and don't save the session",
+ function () { liberator.quit(false); });
+ }
+
+ mappings.add([modes.NORMAL], ["ZZ"],
+ "Quit and save the session",
+ function () { liberator.quit(true); });
+ });
+
+ registerObserver("load_commands", function ()
+ {
+ commands.add(["addo[ns]"],
+ "Manage available Extensions and Themes",
+ function ()
+ {
+ liberator.open("chrome://mozapps/content/extensions/extensions.xul",
+ (options["newtab"] && options.get("newtab").has("all", "addons"))
+ ? liberator.NEW_TAB: liberator.CURRENT_TAB);
+ },
+ { argCount: "0" });
+
+ commands.add(["beep"],
+ "Play a system beep",
+ function () { liberator.beep(); },
+ { argCount: "0" });
+
+ commands.add(["dia[log]"],
+ "Open a " + config.name + " dialog",
+ function (args)
+ {
+ args = args[0];
+
+ try
+ {
+ var dialogs = config.dialogs;
+ for (let i = 0; i < dialogs.length; i++)
+ {
+ if (dialogs[i][0] == args)
+ return dialogs[i][2]();
+ }
+ liberator.echoerr(args ? "Dialog \"" + args + "\" not available" : "E474: Invalid argument");
+ }
+ catch (e)
+ {
+ liberator.echoerr("Error opening '" + args + "': " + e);
+ }
+ },
+ {
+ argCount: "1",
+ bang: true,
+ completer: function (context, args) completion.dialog(context)
+ });
+
+ // TODO: move this
+ function getMenuItems()
+ {
+ function addChildren(node, parent)
+ {
+ for (let [,item] in Iterator(node.childNodes))
+ {
+ if (item.childNodes.length == 0 && item.localName == "menuitem"
+ && !/rdf:http:/.test(item.label)) // FIXME
+ {
+ item.fullMenuPath = parent + item.label;
+ items.push(item);
+ }
+ else
+ {
+ path = parent;
+ if (item.localName == "menu")
+ path += item.label + ".";
+ addChildren(item, path);
+ }
+ }
+ }
+
+ let items = [];
+ addChildren(document.getElementById(config.guioptions["m"]), "");
+ return items;
+ }
+
+ commands.add(["em[enu]"],
+ "Execute the specified menu item from the command line",
+ function (args)
+ {
+ args = args.string;
+ let items = getMenuItems();
+
+ if (!items.some(function (i) i.fullMenuPath == args))
+ {
+ liberator.echoerr("E334: Menu not found: " + args);
+ return;
+ }
+
+ for (let [i, item] in Iterator(items))
+ {
+ if (item.fullMenuPath == args)
+ item.doCommand();
+ }
+ },
+ {
+ argCount: "1",
+ // TODO: add this as a standard menu completion function
+ completer: function (context)
+ {
+ context.title = ["Menu Path", "Label"];
+ context.keys = { text: "fullMenuPath", description: "label" };
+ context.completions = getMenuItems();
+ },
+ literal: 0
+ });
+
+ commands.add(["exe[cute]"],
+ "Execute the argument as an Ex command",
+ // FIXME: this should evaluate each arg separately then join
+ // with " " before executing.
+ // E.g. :execute "source" io.getRCFile().path
+ // Need to fix commands.parseArgs which currently strips the quotes
+ // from quoted args
+ function (args)
+ {
+ try
+ {
+ var cmd = liberator.eval(args.string);
+ liberator.execute(cmd);
+ }
+ catch (e)
+ {
+ liberator.echoerr(e);
+ return;
+ }
+ });
+
+ commands.add(["exu[sage]"],
+ "List all Ex commands with a short description",
+ function (args) { showHelpIndex("ex-cmd-index", commands, args.bang); },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["h[elp]"],
+ "Display help",
+ function (args)
+ {
+ if (args.bang)
+ {
+ liberator.echoerr("E478: Don't panic!");
+ return;
+ }
+
+ liberator.help(args.string);
+ },
+ {
+ argCount: "?",
+ bang: true,
+ completer: function (context) completion.help(context),
+ literal: 0 // FIXME: why literal?
+ });
+
+ commands.add(["javas[cript]", "js"],
+ "Run a JavaScript command through eval()",
+ function (args)
+ {
+ if (args.bang) // open javascript console
+ {
+ liberator.open("chrome://global/content/console.xul",
+ (options["newtab"] && options.get("newtab").has("all", "javascript"))
+ ? liberator.NEW_TAB : liberator.CURRENT_TAB);
+ }
+ else
+ {
+ try
+ {
+ liberator.eval(args.string);
+ }
+ catch (e)
+ {
+ liberator.echoerr(e);
+ }
+ }
+ },
+ {
+ bang: true,
+ completer: function (context) completion.javascript(context),
+ hereDoc: true,
+ literal: 0
+ });
+
+ commands.add(["loadplugins", "lpl"],
+ "Load all plugins immediately",
+ function () { liberator.loadPlugins(); },
+ { argCount: "0" });
+
+ commands.add(["norm[al]"],
+ "Execute Normal mode commands",
+ function (args) { events.feedkeys(args.string, args.bang); },
+ {
+ argCount: "+",
+ bang: true
+ });
+
+ commands.add(["optionu[sage]"],
+ "List all options with a short description",
+ function (args) { showHelpIndex("option-index", options, args.bang); },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["q[uit]"],
+ liberator.has("tabs") ? "Quit current tab" : "Quit application",
+ function (args)
+ {
+ if (liberator.has("tabs"))
+ tabs.remove(getBrowser().mCurrentTab, 1, false, 1);
+ else
+ liberator.quit(false, args.bang);
+ },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["res[tart]"],
+ "Force " + config.name + " to restart",
+ function () { liberator.restart(); },
+ { argCount: "0" });
+
+ commands.add(["time"],
+ "Profile a piece of code or run a command multiple times",
+ function (args)
+ {
+ let count = args.count;
+ let special = args.bang;
+ args = args.string;
+
+ if (args[0] == ":")
+ var method = function () liberator.execute(args);
+ else
+ method = liberator.eval("(function () {" + args + "})");
+
+ try
+ {
+ if (count > 1)
+ {
+ let each, eachUnits, totalUnits;
+ let total = 0;
+
+ for (let i in util.interruptableRange(0, count, 500))
+ {
+ let now = Date.now();
+ method();
+ total += Date.now() - now;
+ }
+
+ if (special)
+ return;
+
+ if (total / count >= 100)
+ {
+ each = total / 1000.0 / count;
+ eachUnits = "sec";
+ }
+ else
+ {
+ each = total / count;
+ eachUnits = "msec";
+ }
+
+ if (total >= 100)
+ {
+ total = total / 1000.0;
+ totalUnits = "sec";
+ }
+ else
+ {
+ totalUnits = "msec";
+ }
+
+ var str = template.generic(
+ <table>
+ <tr highlight="Title" align="left">
+ <th colspan="3">Code execution summary</th>
+ </tr>
+ <tr><td>&#xa0;&#xa0;Executed:</td><td align="right"><span class="times-executed">{count}</span></td><td>times</td></tr>
+ <tr><td>&#xa0;&#xa0;Average time:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
+ <tr><td>&#xa0;&#xa0;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
+ {
+ var beforeTime = Date.now();
+ method();
+
+ if (special)
+ return;
+
+ var afterTime = Date.now();
+
+ if (afterTime - beforeTime >= 100)
+ liberator.echo("Total time: " + ((afterTime - beforeTime) / 1000.0).toFixed(2) + " sec");
+ else
+ liberator.echo("Total time: " + (afterTime - beforeTime) + " msec");
+ }
+ }
+ catch (e)
+ {
+ liberator.echoerr(e);
+ }
+ },
+ {
+ argCount: "+",
+ bang: true,
+ completer: function (context)
+ {
+ if (/^:/.test(context.filter))
+ return completion.ex(context);
+ else
+ return completion.javascript(context);
+ },
+ count: true,
+ literal: 0
+ });
+
+ commands.add(["ve[rsion]"],
+ "Show version information",
+ function (args)
+ {
+ if (args.bang)
+ liberator.open("about:");
+ else
+ liberator.echo(":" + util.escapeHTML(commandline.getCommand()) + "\n" +
+ config.name + " " + liberator.version +
+ " running on:\n" + navigator.userAgent);
+ },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["viu[sage]"],
+ "List all mappings with a short description",
+ function (args) { showHelpIndex("normal-index", mappings, args.bang); },
+ {
+ argCount: "0",
+ bang: true
+ });
+ });
+
+ // initially hide all GUI, it is later restored unless the user has :set go= or something
+ // similar in his config
+ function hideGUI()
+ {
+ var guioptions = config.guioptions;
+ for (let option in guioptions)
+ {
+ guioptions[option].forEach(function (elem) {
+ try
+ {
+ document.getElementById(elem).collapsed = true;
+ }
+ catch (e) {}
+ });
+ }
+ }
+
+ // return the platform normalised to Vim values
+ function getPlatformFeature()
+ {
+ let platform = navigator.platform;
+
+ return /^Mac/.test(platform) ? "MacUnix" : platform == "Win32" ? "Win32" : "Unix";
+ }
+
+ // show a usage index either in the MOW or as a full help page
+ function showHelpIndex(tag, items, inMow)
+ {
+ if (inMow)
+ liberator.echo(template.usage(items), commandline.FORCE_MULTILINE);
+ else
+ liberator.help(tag);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ modules: modules,
+
+ get mode() modes.main,
+ set mode(value) modes.main = value,
+
+ // Global constants
+ CURRENT_TAB: 1,
+ NEW_TAB: 2,
+ NEW_BACKGROUND_TAB: 3,
+ NEW_WINDOW: 4,
+
+ forceNewTab: false,
+
+ // ###VERSION### and ###DATE### are replaced by the Makefile
+ version: "###VERSION### (created: ###DATE###)",
+
+ // TODO: move to events.js?
+ input: {
+ buffer: "", // partial command storage
+ pendingMotionMap: null, // e.g. "d{motion}" if we wait for a motion of the "d" command
+ pendingArgMap: null, // pending map storage for commands like m{a-z}
+ count: -1 // parsed count from the input buffer
+ },
+
+ // @param type can be:
+ // "submit": when the user pressed enter in the command line
+ // "change"
+ // "cancel"
+ // "complete"
+ // TODO: "zoom": if the zoom value of the current buffer changed
+ // TODO: move to ui.js?
+ registerCallback: function (type, mode, func)
+ {
+ // TODO: check if callback is already registered
+ callbacks.push([type, mode, func]);
+ },
+
+ triggerCallback: function (type, mode, data)
+ {
+ // liberator.dump("type: " + type + " mode: " + mode + "data: " + data + "\n");
+ for (let i = 0; i < callbacks.length; i++)
+ {
+ var [thistype, thismode, thisfunc] = callbacks[i];
+ if (mode == thismode && type == thistype)
+ return thisfunc.call(this, data);
+ }
+ return false;
+ },
+
+ registerObserver: registerObserver,
+
+ triggerObserver: function (type, data)
+ {
+ for (let [,[thistype, callback]] in Iterator(observers))
+ {
+ if (thistype == type)
+ callback(data);
+ }
+ },
+
+ beep: function ()
+ {
+ // FIXME: popups clear the command-line
+ if (options["visualbell"])
+ {
+ // flash the visual bell
+ let popup = document.getElementById("liberator-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 = Components.classes["@mozilla.org/sound;1"]
+ .getService(Components.interfaces.nsISound);
+ soundService.beep();
+ }
+ return false; // so you can do: if (...) return liberator.beep();
+ },
+
+ newThread: function () threadManager.newThread(0),
+
+ callAsync: function (thread, self, func)
+ {
+ hread = thread || threadManager.newThread(0);
+ thread.dispatch(new Runnable(self, func, Array.slice(arguments, 2)), thread.DISPATCH_NORMAL);
+ },
+
+ // be sure to call GUI related methods like alert() or dump() ONLY in the main thread
+ callFunctionInThread: function (thread, func)
+ {
+ thread = thread || threadManager.newThread(0);
+
+ // DISPATCH_SYNC is necessary, otherwise strange things will happen
+ thread.dispatch(new Runnable(null, func, Array.slice(arguments, 2)), thread.DISPATCH_SYNC);
+ },
+
+ // NOTE: "browser.dom.window.dump.enabled" preference needs to be set
+ dump: function (message)
+ {
+ if (typeof message == "object")
+ message = util.objectToString(message);
+ else
+ message += "\n";
+ window.dump(("config" in modules && config.name.toLowerCase()) + ": " + message);
+ },
+
+ dumpStack: function (msg, frames)
+ {
+ let stack = Error().stack.replace(/(?:.*\n){2}/, "");
+ if (frames != null)
+ [stack] = stack.match(RegExp("(?:.*\n){0," + frames + "}"));
+ liberator.dump((msg || "Stack") + "\n" + stack);
+ },
+
+ echo: function (str, flags)
+ {
+ commandline.echo(str, commandline.HL_NORMAL, flags);
+ },
+
+ // TODO: Vim replaces unprintable characters in echoerr/echomsg
+ echoerr: function (str, flags)
+ {
+ flags |= commandline.APPEND_TO_MESSAGES;
+
+ if (typeof str == "object" && "echoerr" in str)
+ str = str.echoerr;
+ else if (str instanceof Error)
+ str = str.fileName + ":" + str.lineNumber + ": " + str;
+
+ if (options["errorbells"])
+ liberator.beep();
+
+ commandline.echo(str, commandline.HL_ERRORMSG, flags);
+ },
+
+ // TODO: add proper level constants
+ echomsg: function (str, verbosity, flags)
+ {
+ flags |= commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE;
+
+ if (verbosity == null)
+ verbosity = 0; // verbosity level is exclusionary
+
+ if (options["verbose"] >= verbosity)
+ commandline.echo(str, commandline.HL_INFOMSG, flags);
+ },
+
+ loadScript: function (uri, context)
+ {
+ let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader);
+ loader.loadSubScript(uri, context);
+ },
+
+ eval: function (str, context)
+ {
+ try
+ {
+ if (!context)
+ context = userContext;
+ context[EVAL_ERROR] = null;
+ context[EVAL_STRING] = str;
+ context[EVAL_RESULT] = null;
+ this.loadScript("chrome://liberator/content/eval.js", context);
+ if (context[EVAL_ERROR])
+ throw context[EVAL_ERROR];
+ return context[EVAL_RESULT];
+ }
+ finally
+ {
+ delete context[EVAL_ERROR];
+ delete context[EVAL_RESULT];
+ delete context[EVAL_STRING];
+ }
+ },
+
+ // partial sixth level expression evaluation
+ // TODO: what is that really needed for, and where could it be used?
+ // Or should it be removed? (c) Viktor
+ // Better name? See other liberator.eval()
+ // I agree, the name is confusing, and so is the
+ // description --Kris
+ evalExpression: function (string)
+ {
+ string = string.toString().replace(/^\s*/, "").replace(/\s*$/, "");
+ var matches = string.match(/^&(\w+)/);
+ if (matches)
+ {
+ var opt = this.options.get(matches[1]);
+ if (!opt)
+ {
+ this.echoerr("E113: Unknown option: " + matches[1]);
+ return;
+ }
+ var type = opt.type;
+ var value = opt.getter();
+ if (type != "boolean" && type != "number")
+ value = value.toString();
+ return value;
+ }
+
+ // String
+ else if (matches = string.match(/^(['"])([^\1]*?[^\\]?)\1/))
+ {
+ if (matches)
+ return matches[2].toString();
+ else
+ {
+ this.echoerr("E115: Missing quote: " + string);
+ return;
+ }
+ }
+
+ // Number
+ else if (matches = string.match(/^(\d+)$/))
+ {
+ return parseInt(match[1], 10);
+ }
+
+ var reference = this.variableReference(string);
+ if (!reference[0])
+ this.echoerr("E121: Undefined variable: " + string);
+ else
+ return reference[0][reference[1]];
+
+ return;
+ },
+
+ // Execute an Ex command like str=":zoom 300"
+ execute: function (str, modifiers)
+ {
+ // skip comments and blank lines
+ if (/^\s*("|$)/.test(str))
+ return;
+
+ modifiers = modifiers || {};
+
+ let err = null;
+ let [count, cmd, special, args] = commands.parseCommand(str.replace(/^'(.*)'$/, "$1"));
+ let command = commands.get(cmd);
+
+ if (command === null)
+ {
+ err = "E492: Not a " + config.name.toLowerCase() + " command: " + str;
+ liberator.focusContent();
+ }
+ else if (command.action === null)
+ {
+ err = "E666: Internal error: command.action === null"; // TODO: need to perform this test? -- djk
+ }
+ else if (count != -1 && !command.count)
+ {
+ err = "E481: No range allowed";
+ }
+ else if (special && !command.bang)
+ {
+ err = "E477: No ! allowed";
+ }
+
+ if (!err)
+ command.execute(args, special, count, modifiers);
+ else
+ liberator.echoerr(err);
+ },
+
+ // TODO: move to buffer.focus()?
+ // after pressing Escape, put focus on a non-input field of the browser document
+ // if clearFocusedElement, also blur a focused link
+ focusContent: function (clearFocusedElement)
+ {
+ let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Components.interfaces.nsIWindowWatcher);
+ if (window != ww.activeWindow)
+ return;
+
+ let elem = config.mainWidget || window.content;
+ // TODO: make more generic
+ try
+ {
+ if (liberator.has("mail") && clearFocusedElement && !config.isComposeWindow)
+ {
+ let i = gDBView.selection.currentIndex;
+ if (i == -1 && gDBView.rowCount >= 0)
+ i = 0;
+ gDBView.selection.select(i);
+ }
+
+ if (this.has("tabs"))
+ {
+ let frame = tabs.localStore.focusedFrame;
+ if (frame && frame.top == window.content)
+ elem = frame;
+ }
+ }
+ catch (e) {}
+ if (clearFocusedElement && document.commandDispatcher.focusedElement)
+ document.commandDispatcher.focusedElement.blur();
+ if (elem && (elem != document.commandDispatcher.focusedElement))
+ elem.focus();
+ },
+
+ // does this liberator extension have a certain feature?
+ has: function (feature) config.features.indexOf(feature) >= 0,
+
+ hasExtension: function (name)
+ {
+ let manager = Components.classes["@mozilla.org/extensions/manager;1"]
+ .getService(Components.interfaces.nsIExtensionManager);
+ let extensions = manager.getItemList(Components.interfaces.nsIUpdateItem.TYPE_EXTENSION, {});
+
+ return extensions.some(function (e) e.name == name);
+ },
+
+ help: function (topic)
+ {
+ var where = (options["newtab"] && options.get("newtab").has("all", "help"))
+ ? liberator.NEW_TAB : liberator.CURRENT_TAB;
+
+ if (!topic)
+ {
+ var helpFile = options["helpfile"];
+
+ if (config.helpFiles.indexOf(helpFile) != -1)
+ liberator.open("chrome://liberator/locale/" + helpFile, where);
+ else
+ liberator.echo("Sorry, help file \"" + helpFile + "\" not found");
+
+ return;
+ }
+
+ function jumpToTag(file, tag)
+ {
+ liberator.open("chrome://liberator/locale/" + file, where);
+ // TODO: it would be better to wait for pageLoad
+ setTimeout(function () {
+ let elem = buffer.evaluateXPath('//*[@class="tag" and text()="' + tag + '"]').snapshotItem(0);
+ if (elem)
+ window.content.scrollTo(0, elem.getBoundingClientRect().top - 10); // 10px context
+ else
+ liberator.dump('no element: ' + '@class="tag" and text()="' + tag + '"\n' );
+ }, 500);
+ }
+
+ var items = completion.runCompleter("help", topic);
+ var partialMatch = -1;
+
+ for (let [i, item] in Iterator(items))
+ {
+ if (item[0] == topic)
+ {
+ jumpToTag(item[1], item[0]);
+ return;
+ }
+ else if (partialMatch == -1 && item[0].indexOf(topic) > -1)
+ {
+ partialMatch = i;
+ }
+ }
+
+ if (partialMatch > -1)
+ jumpToTag(items[partialMatch][1], items[partialMatch][0]);
+ else
+ liberator.echoerr("E149: Sorry, no help for " + topic);
+ },
+
+ globalVariables: {},
+
+ loadModule: function (name, func) { loadModule(name, func); },
+
+ loadPlugins: function ()
+ {
+ // FIXME: largely duplicated for loading macros
+ try
+ {
+ let dirs = io.getRuntimeDirectories("plugin");
+
+ if (dirs.length == 0)
+ {
+ liberator.log("No user plugin directory found", 3);
+ return;
+ }
+ for (let [,dir] in Iterator(dirs))
+ {
+ // TODO: search plugins/**/* for plugins
+ liberator.echomsg("Searching for \"plugin/*.{js,vimp}\" in \"" + dir.path + "\"", 2);
+
+ liberator.log("Sourcing plugin directory: " + dir.path + "...", 3);
+
+ let files = io.readDirectory(dir.path, true);
+
+ files.forEach(function (file) {
+ if (!file.isDirectory() && /\.(js|vimp)$/i.test(file.path) && !(file.path in liberator.pluginFiles))
+ {
+ try
+ {
+ io.source(file.path, false);
+ liberator.pluginFiles[file.path] = true;
+ }
+ catch (e)
+ {
+ liberator.reportError(e);
+ }
+ }
+ });
+ }
+ }
+ catch (e)
+ {
+ // thrown if directory does not exist
+ liberator.log("Error sourcing plugin directory: " + e, 9);
+ }
+ },
+
+ // logs a message to the javascript error console
+ // if msg is an object, it is beautified
+ // TODO: add proper level constants
+ log: function (msg, level)
+ {
+ var verbose = 0;
+ if (typeof level != "number") // XXX
+ level = 1;
+
+ // options does not exist at the very beginning
+ if (modules.options)
+ verbose = options.getPref("extensions.liberator.loglevel", 0);
+
+ if (level > verbose)
+ return;
+
+ if (typeof msg == "object")
+ msg = util.objectToString(msg, false);
+
+ var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
+ .getService(Components.interfaces.nsIConsoleService);
+ consoleService.logStringMessage(config.name.toLowerCase() + ": " + msg);
+ },
+
+ // open one or more URLs
+ //
+ // @param urls: either a string or an array of urls
+ // The array can look like this:
+ // ["url1", "url2", "url3", ...] or:
+ // [["url1", postdata1], ["url2", postdata2], ...]
+ // @param where: if ommited, CURRENT_TAB is assumed
+ // but NEW_TAB is set when liberator.forceNewTab is true.
+ // @param force: Don't prompt whether to open more than 20 tabs.
+ // @returns true when load was initiated, or false on error
+ open: function (urls, where, force)
+ {
+ // convert the string to an array of converted URLs
+ // -> see util.stringToURLArray for more details
+ if (typeof urls == "string")
+ urls = util.stringToURLArray(urls);
+
+ if (urls.length > 20 && !force)
+ {
+ commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no])",
+ function (resp) {
+ if (resp && resp.match(/^y(es)?$/i))
+ liberator.open(urls, where, true);
+ });
+ return true;
+ }
+
+ if (urls.length == 0)
+ return false;
+
+ function open(urls, where)
+ {
+ let url = Array.concat(urls)[0];
+ let postdata = Array.concat(urls)[1];
+ let whichwindow = window;
+
+ // decide where to load the first url
+ switch (where)
+ {
+ case liberator.CURRENT_TAB:
+ getBrowser().loadURIWithFlags(url, nsIWebNavigation.LOAD_FLAGS_NONE, null, null, postdata);
+ break;
+
+ case liberator.NEW_BACKGROUND_TAB:
+ case liberator.NEW_TAB:
+ if (!liberator.has("tabs"))
+ return open(urls, liberator.NEW_WINDOW);
+
+ let tab = getBrowser().addTab(url, null, null, postdata);
+ if (where == liberator.NEW_TAB)
+ getBrowser().selectedTab = tab;
+ break;
+
+ case liberator.NEW_WINDOW:
+ const wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ window.open();
+ whichwindow = wm.getMostRecentWindow("navigator:browser");
+ whichwindow.loadURI(url, null, postdata);
+ break;
+
+ default:
+ liberator.echoerr("Exxx: Invalid 'where' directive in liberator.open(...)");
+ return false;
+ }
+ }
+
+ if (liberator.forceNewTab)
+ where = liberator.NEW_TAB;
+ else if (!where)
+ where = liberator.CURRENT_TAB;
+
+ for (let [i, url] in Iterator(urls))
+ {
+ open(url, where);
+ if (i == 0 && !liberator.has("tabs"))
+ break;
+ where = liberator.NEW_BACKGROUND_TAB;
+ }
+
+ return true;
+ },
+
+ pluginFiles: {},
+
+ // namespace for plugins/scripts. Actually (only) the active plugin must/can set a
+ // v.plugins.mode = <str> string to show on v.modes.CUSTOM
+ // v.plugins.stop = <func> hooked on a v.modes.reset()
+ // v.plugins.onEvent = <func> function triggered, on keypresses (unless <esc>) (see events.js)
+ plugins: plugins,
+
+ // quit liberator, no matter how many tabs/windows are open
+ quit: function (saveSession, force)
+ {
+ if (saveSession)
+ options.setPref("browser.startup.page", 3); // start with saved session
+ else
+ options.setPref("browser.startup.page", 1); // start with default homepage session
+
+ const nsIAppStartup = Components.interfaces.nsIAppStartup;
+ if (force)
+ Components.classes["@mozilla.org/toolkit/app-startup;1"]
+ .getService(nsIAppStartup)
+ .quit(nsIAppStartup.eForceQuit);
+ else
+ window.goQuitApplication();
+ },
+
+ reportError: function (error)
+ {
+ if (Components.utils.reportError)
+ Components.utils.reportError(error);
+ try
+ {
+ let obj = {
+ toString: function () error.toString(),
+ stack: <>{error.stack.replace(/^/mg, "\t")}</>
+ };
+ for (let [k, v] in Iterator(error))
+ {
+ if (!(k in obj))
+ obj[k] = v;
+ }
+ liberator.dump(obj);
+ liberator.dump("");
+ }
+ catch (e) {}
+ },
+
+ restart: function ()
+ {
+ const nsIAppStartup = Components.interfaces.nsIAppStartup;
+
+ // notify all windows that an application quit has been requested.
+ var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+ var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ os.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // something aborted the quit process.
+ if (cancelQuit.data)
+ return;
+
+ // notify all windows that an application quit has been granted.
+ os.notifyObservers(null, "quit-application-granted", null);
+
+ // enumerate all windows and call shutdown handlers
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var windows = wm.getEnumerator(null);
+ while (windows.hasMoreElements())
+ {
+ var win = windows.getNext();
+ if (("tryToClose" in win) && !win.tryToClose())
+ return;
+ }
+ Components.classes["@mozilla.org/toolkit/app-startup;1"]
+ .getService(nsIAppStartup)
+ .quit(nsIAppStartup.eRestart | nsIAppStartup.eAttemptQuit);
+ },
+
+ // TODO: move to {muttator,vimperator,...}.js
+ // this function is called when the chrome is ready
+ startup: function ()
+ {
+ let start = Date.now();
+ liberator.log("Initializing liberator object...", 0);
+
+ config.features = config.features || [];
+ config.features.push(getPlatformFeature());
+ config.defaults = config.defaults || {};
+ config.guioptions = config.guioptions || {};
+ config.browserModes = config.browserModes || [modes.NORMAL];
+ config.mailModes = config.mailModes || [modes.NORMAL];
+ // TODO: suitable defaults?
+ //config.mainWidget
+ //config.mainWindowID
+ //config.visualbellWindow
+ //config.styleableChrome
+ config.autocommands = config.autocommands || [];
+ config.dialogs = config.dialogs || [];
+ config.helpFiles = config.helpFiles || [];
+
+ // commands must always be the first module to be initialized
+ loadModule("commands", Commands);
+ loadModule("options", Options);
+ loadModule("mappings", Mappings);
+ loadModule("buffer", Buffer);
+ loadModule("events", Events);
+ loadModule("commandline", CommandLine);
+ loadModule("statusline", StatusLine);
+ loadModule("editor", Editor);
+ loadModule("autocommands", AutoCommands);
+ loadModule("io", IO);
+ loadModule("completion", Completion);
+
+ // add options/mappings/commands which are only valid in this particular extension
+ if (config.init)
+ config.init();
+
+ liberator.log("All modules loaded", 3);
+
+ // first time intro message
+ const firstTime = "extensions." + config.name.toLowerCase() + ".firsttime";
+ if (options.getPref(firstTime, true))
+ {
+ setTimeout(function () {
+ liberator.help();
+ options.setPref(firstTime, false);
+ }, 1000);
+ }
+
+ // always start in normal mode
+ modes.reset();
+
+ // TODO: we should have some class where all this guioptions stuff fits well
+ hideGUI();
+
+ // finally, read a ~/.vimperatorrc and plugin/**.{vimp,js}
+ // make sourcing asynchronous, otherwise commands that open new tabs won't work
+ setTimeout(function () {
+
+ let rcFile = io.getRCFile("~");
+
+ if (rcFile)
+ io.source(rcFile.path, true);
+ else
+ liberator.log("No user RC file found", 3);
+
+ if (options["exrc"])
+ {
+ let localRcFile = io.getRCFile(io.getCurrentDirectory().path);
+ if (localRcFile)
+ io.source(localRcFile.path, true);
+ }
+
+ if (options["loadplugins"])
+ liberator.loadPlugins();
+
+ // after sourcing the initialization files, this function will set
+ // all gui options to their default values, if they have not been
+ // set before by any rc file
+ for (let option in options)
+ {
+ if (option.setter)
+ option.value = option.value;
+ }
+
+ liberator.triggerObserver("enter", null);
+ autocommands.trigger(config.name + "Enter", {});
+ }, 0);
+
+ statusline.update();
+
+ liberator.dump("loaded in " + (Date.now() - start) + " ms");
+ liberator.log(config.name + " fully initialized", 0);
+ },
+
+ shutdown: function ()
+ {
+ autocommands.trigger(config.name + "LeavePre", {});
+
+ storage.saveAll();
+
+ liberator.triggerObserver("shutdown", null);
+
+ liberator.dump("All liberator modules destroyed\n");
+
+ autocommands.trigger(config.name + "Leave", {});
+ },
+
+ sleep: function (delay)
+ {
+ let mainThread = threadManager.mainThread;
+
+ let end = Date.now() + delay;
+ while (Date.now() < end)
+ mainThread.processNextEvent(true);
+ return true;
+ },
+
+ callInMainThread: function (callback)
+ {
+ let mainThread = threadManager.mainThread;
+ if (!threadManager.isMainThread)
+ mainThread.dispatch({ run: callback }, mainThread.DISPATCH_NORMAL);
+ else
+ callback();
+ },
+
+ threadYield: function (flush, interruptable)
+ {
+ let mainThread = threadManager.mainThread;
+ liberator.interrupted = false;
+ do
+ {
+ mainThread.processNextEvent(!flush);
+ if (liberator.interrupted)
+ throw new Error("Interrupted");
+ }
+ while (flush && mainThread.hasPendingEvents());
+ },
+
+ variableReference: function (string)
+ {
+ if (!string)
+ return [null, null, null];
+
+ var matches = string.match(/^([bwtglsv]):(\w+)/);
+ if (matches) // Variable
+ {
+ // Other variables should be implemented
+ if (matches[1] == "g")
+ {
+ if (matches[2] in this.globalVariables)
+ return [this.globalVariables, matches[2], matches[1]];
+ else
+ return [null, matches[2], matches[1]];
+ }
+ }
+ else // Global variable
+ {
+ if (string in this.globalVariables)
+ return [this.globalVariables, string, "g"];
+ else
+ return [null, string, "g"];
+ }
+ },
+
+ get windows()
+ {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var wa = [];
+ var enumerator = wm.getEnumerator("navigator:browser");
+ while (enumerator.hasMoreElements())
+ wa.push(enumerator.getNext());
+ return wa;
+ }
+ };
+ //}}}
+})(); //}}}
+
+window.liberator = liberator;
+
+// called when the chrome is fully loaded and before the main window is shown
+window.addEventListener("load", liberator.startup, false);
+window.addEventListener("unload", liberator.shutdown, false);
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/liberator.xul b/common/content/liberator.xul
new file mode 100644
index 00000000..fe7c63a7
--- /dev/null
+++ b/common/content/liberator.xul
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- ***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK ***** -->
+
+<?xml-stylesheet href="chrome://liberator/skin/liberator.css" type="text/css"?>
+<!DOCTYPE overlay SYSTEM "liberator.dtd" [
+ <!ENTITY liberator.content "chrome://liberator/content/">
+]>
+
+<overlay id="liberator"
+ xmlns:liberator="http://vimperator.org/namespaces/liberator"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/x-javascript;version=1.8" src="&liberator.content;liberator-overlay.js"/>
+
+ <window id="&liberator.mainWindow;">
+
+ <keyset id="mainKeyset">
+ <key id="key_open_vimbar" key=":" oncommand="liberator.modules.commandline.open(':', '', liberator.modules.modes.EX);" modifiers=""/>
+ <key id="key_stop" keycode="VK_ESCAPE" oncommand="liberator.modules.events.onEscape();"/>
+ <!-- other keys are handled inside the event loop in events.js -->
+ </keyset>
+
+ <popupset>
+ <panel id="liberator-visualbell" liberator:highlight="Bell"/>
+ </popupset>
+
+ <!--this notifies us also of focus events in the XUL
+ from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
+ <commandset id="onVimperatorFocus"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="if (typeof liberator.modules.events != 'undefined') liberator.modules.events.onFocusChange(event);"/>
+ <commandset id="onVimperatorSelect"
+ commandupdater="true"
+ events="select"
+ oncommandupdate="if (typeof liberator.modules.events != 'undefined') liberator.modules.events.onSelectionChange(event);"/>
+
+ <!-- As of Firefox 3.1pre, <iframe>.height changes do not seem to have immediate effect,
+ therefore we need to put them into a <vbox> for which that works just fine -->
+ <vbox class="liberator-container" hidden="false" collapsed="true">
+ <iframe id="liberator-multiline-output" src="chrome://liberator/content/buffer.xhtml"
+ flex="1" hidden="false" collapsed="false"
+ onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
+ </vbox>
+
+ <vbox class="liberator-container" hidden="false" collapsed="true">
+ <iframe id="liberator-completions" src="chrome://liberator/content/buffer.xhtml"
+ flex="1" hidden="false" collapsed="false"
+ onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
+ </vbox>
+
+ <hbox id="liberator-commandline" hidden="false" liberator:highlight="Normal">
+ <label class="plain" id="liberator-commandline-prompt" flex="0" crop="end" value="" collapsed="true"/>
+ <textbox class="plain" id="liberator-commandline-command" flex="1" type="timed" timeout="100"
+ oninput="liberator.modules.commandline.onEvent(event);"
+ onfocus="liberator.modules.commandline.onEvent(event);"
+ onblur="liberator.modules.commandline.onEvent(event);"/>
+ </hbox>
+
+ <vbox class="liberator-container" hidden="false" collapsed="false">
+ <textbox id="liberator-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
+ onkeypress="liberator.modules.commandline.onMultilineInputEvent(event);"
+ oninput="liberator.modules.commandline.onMultilineInputEvent(event);"
+ onblur="liberator.modules.commandline.onMultilineInputEvent(event);"/>
+ </vbox>
+
+ </window>
+
+ <statusbar id="status-bar" liberator:highlight="StatusLine">
+ <hbox insertbefore="&liberator.statusBefore;" insertafter="&liberator.statusAfter;"
+ id="liberator-statusline" flex="1" hidden="false" align="center">
+ <textbox class="plain" id="liberator-statusline-field-url" readonly="false" flex="1" crop="end"/>
+ <label class="plain" id="liberator-statusline-field-inputbuffer" flex="0"/>
+ <label class="plain" id="liberator-statusline-field-progress" flex="0"/>
+ <label class="plain" id="liberator-statusline-field-tabcount" flex="0"/>
+ <label class="plain" id="liberator-statusline-field-bufferposition" flex="0"/>
+ </hbox>
+ <!-- just hide them since other elements expect them -->
+ <statusbarpanel id="statusbar-display" hidden="true"/>
+ <statusbarpanel id="statusbar-progresspanel" hidden="true"/>
+ </statusbar>
+
+</overlay>
+
+<!-- vim: set fdm=marker sw=4 ts=4 et: -->
diff --git a/common/content/mappings.js b/common/content/mappings.js
new file mode 100644
index 00000000..b10c4e42
--- /dev/null
+++ b/common/content/mappings.js
@@ -0,0 +1,414 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// Do NOT create instances of this class yourself, use the helper method
+// mappings.add() instead
+function Map(modes, cmds, description, action, extraInfo) //{{{
+{
+ if (!modes || (!cmds || !cmds.length) || !action)
+ return null;
+
+ if (!extraInfo)
+ extraInfo = {};
+
+ this.modes = modes;
+ // only store keysyms with uppercase modifier strings
+ this.names = cmds.map(function (cmd) cmd.replace(/[casm]-/g, String.toUpperCase));
+ this.action = action;
+
+ this.flags = extraInfo.flags || 0;
+ this.description = description || "";
+ this.rhs = extraInfo.rhs || null;
+ this.noremap = extraInfo.noremap || false;
+ this.silent = extraInfo.silent || false;
+};
+
+Map.prototype = {
+
+ hasName: function (name)
+ {
+ return this.names.indexOf(name) >= 0;
+ },
+
+ execute: function (motion, count, argument)
+ {
+ var args = [];
+
+ if (this.flags & Mappings.flags.MOTION)
+ args.push(motion);
+ if (this.flags & Mappings.flags.COUNT)
+ args.push(count);
+ if (this.flags & Mappings.flags.ARGUMENT)
+ args.push(argument);
+
+ let self = this;
+ // FIXME: Kludge.
+ if (this.names[0] != ".")
+ mappings.repeat = function () self.action.apply(self, args);
+
+ return this.action.apply(this, args);
+ }
+
+}; //}}}
+
+function Mappings() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var main = []; // default mappings
+ var user = []; // user created mappings
+
+ for (let mode in modes)
+ {
+ main[mode] = [];
+ user[mode] = [];
+ }
+
+ function addMap(map, userMap)
+ {
+ var where = userMap ? user : main;
+ map.modes.forEach(function (mode) {
+ if (!(mode in where))
+ where[mode] = [];
+ where[mode].push(map);
+ });
+ }
+
+ function getMap(mode, cmd, stack)
+ {
+ var maps = stack[mode] || [];
+
+ for (let [,map] in Iterator(maps))
+ {
+ if (map.hasName(cmd))
+ return map;
+ }
+
+ return null;
+ }
+
+ function removeMap(mode, cmd)
+ {
+ var maps = user[mode] || [];
+ var names;
+
+ for (let [i, map] in Iterator(maps))
+ {
+ for (let [j, name] in Iterator(map.names))
+ {
+ if (name == cmd)
+ {
+ map.names.splice(j, 1);
+ if (map.names.length == 0)
+ maps.splice(i, 1);
+ return;
+ }
+ }
+ }
+ }
+
+ function expandLeader(keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader())
+
+ // Return all mappings present in all @modes
+ function mappingsIterator(modes, stack)
+ {
+ modes = modes.slice();
+ return (map for ([i, map] in Iterator(stack[modes.shift()]))
+ if (modes.every(function (mode) stack[mode].some(
+ function (m) m.rhs == map.rhs && m.names[0] == map.names[0]))))
+ }
+
+ function addMapCommands(ch, modes, modeDescription)
+ {
+ // 0 args -> list all maps
+ // 1 arg -> list the maps starting with args
+ // 2 args -> map arg1 to arg*
+ function map(args, mode, noremap)
+ {
+ if (!args.length)
+ {
+ mappings.list(mode);
+ return;
+ }
+
+ // ?:\s+ <- don't remember; (...)? optional = rhs
+ let [lhs, rhs] = args;
+
+ if (!rhs) // list the mapping
+ {
+ mappings.list(mode, expandLeader(lhs));
+ }
+ else
+ {
+ for (let [,m] in Iterator(mode))
+ {
+ mappings.addUserMap([m], [lhs],
+ "User defined mapping",
+ function (count) { events.feedkeys((count > 1 ? count : "") + this.rhs, this.noremap, this.silent); },
+ {
+ flags: Mappings.flags.COUNT,
+ rhs: rhs,
+ noremap: !!noremap,
+ silent: "<silent>" in args
+ });
+ }
+ }
+ }
+
+ modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
+
+ const opts = {
+ completer: function (context, args) completion.userMapping(context, args, modes),
+ options: [
+ [["<silent>", "<Silent>"], commands.OPTION_NOARG]
+ ],
+ literal: 1,
+ serial: function () {
+ let noremap = this.name.indexOf("noremap") > -1;
+ return [
+ {
+ command: this.name,
+ options: map.silent ? { "<silent>": null } : {},
+ arguments: [map.names[0]],
+ literalArg: map.rhs
+ }
+ for (map in mappingsIterator(modes, user))
+ if (map.rhs && map.noremap == noremap)
+ ]
+ }
+ };
+
+ commands.add([ch ? ch + "m[ap]" : "map"],
+ "Map a key sequence" + modeDescription,
+ function (args) { map(args, modes, false); },
+ opts);
+
+ commands.add([ch + "no[remap]"],
+ "Map a key sequence without remapping keys" + modeDescription,
+ function (args) { map(args, modes, true); },
+ opts);
+
+ commands.add([ch + "mapc[lear]"],
+ "Remove all mappings" + modeDescription,
+ function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
+ { argCount: "0" });
+
+ commands.add([ch + "unm[ap]"],
+ "Remove a mapping" + modeDescription,
+ function (args)
+ {
+ args = args[0];
+
+ let found = false;
+ for (let [,mode] in Iterator(modes))
+ {
+ if (mappings.hasMap(mode, args))
+ {
+ mappings.remove(mode, args);
+ found = true;
+ }
+ }
+ if (!found)
+ liberator.echoerr("E31: No such mapping");
+ },
+ {
+ argCount: "1",
+ completer: function (context, args) completion.userMapping(context, args, modes)
+ });
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ addMapCommands("", [modes.NORMAL], "");
+ addMapCommands("c", [modes.COMMAND_LINE], "command line");
+ addMapCommands("i", [modes.INSERT, modes.TEXTAREA], "insert");
+ if (liberator.has("mail"))
+ addMapCommands("m", [modes.MESSAGE], "message");
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ liberator.registerObserver("load_completion", function ()
+ {
+ completion.setFunctionCompleter(mappings.get,
+ [
+ null,
+ function (context, obj, args)
+ {
+ let mode = args[0]
+ return util.Array.flatten(
+ [
+ [[name, map.description] for ([i, name] in Iterator(map.names))]
+ for ([i, map] in Iterator(user[mode].concat(main[mode])))
+ ])
+ }
+ ]);
+ });
+
+ // FIXME:
+ Mappings.flags = {
+ ALLOW_EVENT_ROUTING: 1 << 0, // if set, return true inside the map command to pass the event further to firefox
+ MOTION: 1 << 1,
+ COUNT: 1 << 2,
+ ARGUMENT: 1 << 3
+ };
+
+ return {
+
+ // NOTE: just normal mode for now
+ __iterator__: function () mappingsIterator([modes.NORMAL], main),
+
+ // used by :mkvimperatorrc to save mappings
+ getUserIterator: function (mode) mappingsIterator(mode, user),
+
+ add: function (modes, keys, description, action, extra)
+ {
+ addMap(new Map(modes, keys, description, action, extra), false);
+ },
+
+ addUserMap: function (modes, keys, description, action, extra)
+ {
+ keys = keys.map(expandLeader);
+ var map = new Map(modes, keys, description || "User defined mapping", action, extra);
+
+ // remove all old mappings to this key sequence
+ for (let [,name] in Iterator(map.names))
+ {
+ for (let [,mode] in Iterator(map.modes))
+ removeMap(mode, name);
+ }
+
+ addMap(map, true);
+ },
+
+ get: function (mode, cmd)
+ {
+ mode = mode || modes.NORMAL;
+ return getMap(mode, cmd, user) || getMap(mode, cmd, main);
+ },
+
+ getDefault: function (mode, cmd)
+ {
+ mode = mode || modes.NORMAL;
+ return getMap(mode, cmd, main);
+ },
+
+ // returns an array of mappings with names which START with "cmd" (but are NOT "cmd")
+ getCandidates: function (mode, cmd)
+ {
+ let mappings = user[mode].concat(main[mode]);
+ let matches = [];
+
+ for (let [,map] in Iterator(mappings))
+ {
+ for (let [,name] in Iterator(map.names))
+ {
+ if (name.indexOf(cmd) == 0 && name.length > cmd.length)
+ {
+ // for < only return a candidate if it doesn't look like a <c-x> mapping
+ if (cmd != "<" || !/^<.+>/.test(name))
+ matches.push(map);
+ }
+ }
+ }
+
+ return matches;
+ },
+
+ getMapLeader: function ()
+ {
+ var leaderRef = liberator.variableReference("mapleader");
+ return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
+ },
+
+ // returns whether the user added a custom user map
+ hasMap: function (mode, cmd)
+ {
+ return user[mode].some(function (map) map.hasName(cmd));
+ },
+
+ remove: function (mode, cmd)
+ {
+ removeMap(mode, cmd);
+ },
+
+ removeAll: function (mode)
+ {
+ user[mode] = [];
+ },
+
+ list: function (modes, filter)
+ {
+ let modeSign = "";
+ modes.forEach(function (mode)
+ {
+ if (mode == modes.NORMAL)
+ modeSign += "n";
+ if ((mode == modes.INSERT || mode == modes.TEXTAREA) && modeSign.indexOf("i") == -1)
+ modeSign += "i";
+ if (mode == modes.COMMAND_LINE)
+ modeSign += "c";
+ if (mode == modes.MESSAGRE)
+ modeSign += "m";
+ });
+
+ let maps = mappingsIterator(modes, user);
+ if (filter)
+ maps = [map for (map in maps) if (map.names[0] == filter)];
+
+ let list = <table>
+ {
+ template.map(maps, function (map)
+ template.map(map.names, function (name)
+ <tr>
+ <td>{modeSign} {name}</td>
+ <td>{map.noremap ? "*" : " "}</td>
+ <td>{map.rhs || "function () { ... }"}</td>
+ </tr>))
+ }
+ </table>;
+
+ // TODO: Move this to an ItemList to show this automatically
+ if (list.*.length() == list.text().length())
+ {
+ liberator.echo(<div highlight="Title">No mappings found</div>, commandline.FORCE_MULTILINE);
+ return;
+ }
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/modes.js b/common/content/modes.js
new file mode 100644
index 00000000..0f94f589
--- /dev/null
+++ b/common/content/modes.js
@@ -0,0 +1,294 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+const modes = (function () //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var main = 1; // NORMAL
+ var extended = 0; // NONE
+
+ var passNextKey = false;
+ var passAllKeys = false;
+ var isRecording = false;
+ var isReplaying = false; // playing a macro
+
+ var modeStack = [];
+
+ function getModeMessage()
+ {
+ if (passNextKey && !passAllKeys)
+ return "-- PASS THROUGH (next) --";
+ else if (passAllKeys && !passNextKey)
+ return "-- PASS THROUGH --";
+
+ // when recording a macro
+ var macromode = "";
+ if (modes.isRecording)
+ macromode = "recording";
+ else if (modes.isReplaying)
+ macromode = "replaying";
+
+ var ext = "";
+ if (extended & modes.MENU) // TODO: desirable?
+ ext += " (menu)";
+ ext += " --" + macromode;
+
+ if (main in modeMap && typeof modeMap[main].display == "function")
+ return "-- " + modeMap[main].display() + ext;
+ return macromode;
+ }
+
+ // NOTE: Pay attention that you don't run into endless loops
+ // Usually you should only indicate to leave a special mode like HINTS
+ // by calling modes.reset() and adding the stuff which is needed
+ // for its cleanup here
+ function handleModeChange(oldMode, newMode)
+ {
+ // TODO: fix v.log() to work with verbosity level
+ //liberator.log("switching from mode " + oldMode + " to mode " + newMode, 7);
+ //liberator.dump("switching from mode " + oldMode + " to mode " + newMode + "\n");
+
+ switch (oldMode)
+ {
+ case modes.TEXTAREA:
+ case modes.INSERT:
+ editor.unselectText();
+ break;
+
+ case modes.VISUAL:
+ if (newMode == modes.CARET)
+ {
+ // clear any selection made
+ var selection = window.content.getSelection();
+ try
+ { // a simple if (selection) does not work
+ selection.collapseToStart();
+ }
+ catch (e) {}
+ }
+ else
+ editor.unselectText();
+ break;
+
+ case modes.CUSTOM:
+ plugins.stop();
+ break;
+
+ case modes.COMMAND_LINE:
+ // clean up for HINT mode
+ if (modes.extended & modes.HINTS)
+ hints.hide();
+ commandline.close();
+ break;
+ }
+
+ if (newMode == modes.NORMAL)
+ {
+ // disable caret mode when we want to switch to normal mode
+ var value = options.getPref("accessibility.browsewithcaret", false);
+ if (value)
+ options.setPref("accessibility.browsewithcaret", false);
+
+ statusline.updateUrl();
+ // Kludge to prevent the input field losing focus on MS/Mac
+ setTimeout(function () {
+ let focus = document.commandDispatcher.focusedElement;
+ let urlbar = document.getElementById("urlbar");
+ if (!urlbar || focus != urlbar.inputField)
+ liberator.focusContent(false);
+ }, 100);
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var self = {
+ NONE: 0,
+
+ __iterator__: function () util.Array.iterator(this.all),
+
+ get all() mainModes.slice(),
+
+ addMode: function (name, extended, display) {
+ let disp = name.replace("_", " ", "g");
+ this[name] = 1 << lastMode++;
+ modeMap[name] = modeMap[this[name]] = {
+ extended: extended,
+ mask: this[name],
+ name: name,
+ display: display || function () disp
+ };
+ if (!extended)
+ mainModes.push(this[name]);
+ },
+
+ // show the current mode string in the command line
+ show: function ()
+ {
+ if (!options["showmode"])
+ return;
+
+ // never show mode messages if we are in command line mode
+ if (main == modes.COMMAND_LINE)
+ return;
+
+ commandline.echo(getModeMessage(), commandline.HL_MODEMSG,
+ commandline.DISALLOW_MULTILINE);
+ },
+
+ // add/remove always work on the extended mode only
+ add: function (mode)
+ {
+ extended |= mode;
+ this.show();
+ },
+
+ // helper function to set both modes in one go
+ // if silent == true, you also need to take care of the mode handling changes yourself
+ set: function (mainMode, extendedMode, silent)
+ {
+ // if a main mode is set, the extended is always cleared
+ if (typeof mainMode === "number")
+ {
+ main = mainMode;
+ if (!extendedMode)
+ extended = modes.NONE;
+
+ if (!silent && mainMode != main)
+ handleModeChange(main, mainMode);
+ }
+ if (typeof extendedMode === "number")
+ extended = extendedMode;
+
+ if (!silent)
+ this.show();
+ },
+
+ push: function (mainMode, extendedMode, silent)
+ {
+ modeStack.push([main, extended]);
+ this.set(mainMode, extendedMode, silent);
+ },
+
+ pop: function (silent)
+ {
+ var a = modeStack.pop();
+ if (a)
+ this.set(a[0], a[1], silent);
+ else
+ this.reset(silent);
+ },
+
+ setCustomMode: function (modestr, oneventfunc, stopfunc)
+ {
+ // TODO this.plugin[id]... ('id' maybe submode or what..)
+ plugins.mode = modestr;
+ plugins.onEvent = oneventfunc;
+ plugins.stop = stopfunc;
+ },
+
+ // keeps recording state
+ reset: function (silent)
+ {
+ modeStack = [];
+ if (config.isComposeWindow)
+ this.set(modes.COMPOSE, modes.NONE, silent);
+ else
+ this.set(modes.NORMAL, modes.NONE, silent);
+ },
+
+ remove: function (mode)
+ {
+ extended &= ~mode;
+ this.show();
+ },
+
+ get passNextKey() passNextKey,
+ set passNextKey(value) { passNextKey = value; this.show(); },
+
+ get passAllKeys() passAllKeys,
+ set passAllKeys(value) { passAllKeys = value; this.show(); },
+
+ get isRecording() isRecording,
+ set isRecording(value) { isRecording = value; this.show(); },
+
+ get isReplaying() isReplaying,
+ set isReplaying(value) { isReplaying = value; this.show(); },
+
+ get main() main,
+ set main(value) {
+ if (value != main)
+ handleModeChange(main, value);
+
+ main = value;
+ // setting the main mode always resets any extended mode
+ extended = modes.NONE;
+ this.show();
+ },
+
+ get extended() extended,
+ set extended(value) { extended = value; this.show(); }
+
+ };
+
+ var mainModes = [self.NONE];
+ var lastMode = 0;
+ var modeMap = {};
+
+ // main modes, only one should ever be active
+ self.addMode("NORMAL", false, -1);
+ self.addMode("INSERT");
+ self.addMode("VISUAL", false, function () "VISUAL" + (extended & modes.LINE ? " LINE" : ""));
+ self.addMode("COMMAND_LINE");
+ self.addMode("CARET"); // text cursor is visible
+ self.addMode("TEXTAREA"); // text cursor is in a HTMLTextAreaElement
+ self.addMode("MESSAGE"); // for now only used in Muttator when the message has focus
+ self.addMode("COMPOSE");
+ self.addMode("CUSTOM", false, function () plugins.mode);
+ // extended modes, can include multiple modes, and even main modes
+ self.addMode("EX", true);
+ self.addMode("HINTS", true);
+ self.addMode("INPUT_MULTILINE", true);
+ self.addMode("OUTPUT_MULTILINE", true);
+ self.addMode("SEARCH_FORWARD", true);
+ self.addMode("SEARCH_BACKWARD", true);
+ self.addMode("MENU", true); // a popupmenu is active
+ self.addMode("LINE", true); // linewise visual mode
+ self.addMode("RECORDING", true);
+ self.addMode("PROMPT", true);
+
+ return self;
+ //}}}
+})(); //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/options.js b/common/content/options.js
new file mode 100644
index 00000000..e376c16e
--- /dev/null
+++ b/common/content/options.js
@@ -0,0 +1,984 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// do NOT create instances of this class yourself, use the helper method
+// options.add() instead
+function Option(names, description, type, defaultValue, extraInfo) //{{{
+{
+ if (!names || !type)
+ return null;
+
+ if (!extraInfo)
+ extraInfo = {};
+
+ this.name = names[0];
+ this.names = names;
+ this.type = type;
+ this.scope = (extraInfo.scope & options.OPTION_SCOPE_BOTH) ||
+ options.OPTION_SCOPE_GLOBAL;
+ // XXX set to BOTH by default someday? - kstep
+ this.description = description || "";
+
+ // "", 0 are valid default values
+ this.defaultValue = (defaultValue === undefined) ? null : defaultValue;
+
+ this.setter = extraInfo.setter || null;
+ this.getter = extraInfo.getter || null;
+ this.completer = extraInfo.completer || null;
+ this.validator = extraInfo.validator || null;
+ this.checkHas = extraInfo.checkHas || null;
+
+ // this property is set to true whenever the option is first set
+ // useful to see whether it was changed by some rc file
+ this.hasChanged = false;
+
+ // add no{option} variant of boolean {option} to this.names
+ if (this.type == "boolean")
+ {
+ this.names = []; // reset since order is important
+ for (let [,name] in Iterator(names))
+ {
+ this.names.push(name);
+ this.names.push("no" + name);
+ }
+ }
+
+ if (this.globalvalue == undefined)
+ this.globalvalue = this.defaultValue;
+}
+Option.prototype = {
+ get globalvalue() options.store.get(this.name),
+ set globalvalue(val) { options.store.set(this.name, val) },
+
+ parseValues: function (value)
+ {
+ if (this.type == "stringlist")
+ return value.split(",");
+ if (this.type == "charlist")
+ return Array.slice(value);
+ return value;
+ },
+
+ joinValues: function (values)
+ {
+ if (this.type == "stringlist")
+ return values.join(",");
+ if (this.type == "charlist")
+ return values.join("");
+ return values;
+ },
+
+ get values() this.parseValues(this.value),
+ set values(values) this.setValues(this.scope, values),
+
+ getValues: function (scope) this.parseValues(this.get(scope)),
+
+ setValues: function (values, scope)
+ {
+ this.set(this.joinValues(values), scope || this.scope);
+ },
+
+ get: function (scope)
+ {
+ if (scope)
+ {
+ if ((scope & this.scope) == 0) // option doesn't exist in this scope
+ return null;
+ }
+ else
+ {
+ scope = this.scope;
+ }
+
+ var aValue;
+
+ if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL))
+ aValue = tabs.options[this.name];
+ if ((scope & options.OPTION_SCOPE_GLOBAL) && (aValue == undefined))
+ aValue = this.globalvalue;
+
+ if (this.getter)
+ this.getter.call(this, aValue);
+
+ return aValue;
+ },
+
+ set: function (newValue, scope)
+ {
+ scope = scope || this.scope;
+ if ((scope & this.scope) == 0) // option doesn't exist in this scope
+ return null;
+
+ if (this.setter)
+ {
+ let tmpValue = newValue;
+ newValue = this.setter.call(this, newValue);
+
+ if (newValue === undefined)
+ {
+ newValue = tmpValue;
+ liberator.log("DEPRECATED: '" + this.name + "' setter should return a value");
+ }
+ }
+
+ if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL))
+ tabs.options[this.name] = newValue;
+ if ((scope & options.OPTION_SCOPE_GLOBAL) && newValue != this.globalValue)
+ this.globalvalue = newValue;
+
+ this.hasChanged = true;
+ },
+
+ get value() this.get(),
+ set value(val) this.set(val),
+
+ has: function ()
+ {
+ let self = this;
+ let test = function (val) values.indexOf(val) >= 0;
+ if (this.checkHas)
+ test = function (val) values.some(function (value) self.checkHas(value, val));
+ let values = this.values;
+ /* Return whether some argument matches */
+ return Array.some(arguments, function (val) test(val))
+ },
+
+ hasName: function (name) this.names.indexOf(name) >= 0,
+
+ isValidValue: function (values)
+ {
+ if (this.validator)
+ return this.validator(values);
+ else
+ return true;
+ },
+
+ reset: function ()
+ {
+ this.value = this.defaultValue;
+ },
+
+ op: function (operator, values, scope, invert)
+ {
+ let newValue = null;
+ let self = this;
+
+ switch (this.type)
+ {
+ case "boolean":
+ if (operator != "=")
+ break;
+
+ if (invert)
+ newValue = !this.value;
+ else
+ newValue = values;
+ break;
+
+ case "number":
+ let value = parseInt(values); // deduce radix
+
+ if (isNaN(value))
+ return "E521: Number required";
+
+ switch (operator)
+ {
+ case "+":
+ newValue = this.value + value;
+ break;
+ case "-":
+ newValue = this.value - value;
+ break;
+ case "^":
+ newValue = this.value * value;
+ break;
+ case "=":
+ newValue = value;
+ break;
+ }
+
+ break;
+
+ case "charlist":
+ case "stringlist":
+ values = Array.concat(values);
+ switch (operator)
+ {
+ case "+":
+ newValue = util.Array.uniq(Array.concat(this.values, values), true);
+ break;
+ case "^":
+ // NOTE: Vim doesn't prepend if there's a match in the current value
+ newValue = util.Array.uniq(Array.concat(values, this.values), true);
+ break;
+ case "-":
+ newValue = this.values.filter(function (item) values.indexOf(item) == -1);
+ break;
+ case "=":
+ newValue = values;
+ if (invert)
+ {
+ let keepValues = this.values.filter(function (item) values.indexOf(item) == -1);
+ let addValues = values.filter(function (item) self.values.indexOf(item) == -1);
+ newValue = addValues.concat(keepValues);
+ }
+ break;
+ }
+
+ break;
+
+ case "string":
+ switch (operator)
+ {
+ case "+":
+ newValue = this.value + values;
+ break;
+ case "-":
+ newValue = this.value.replace(values, "");
+ break;
+ case "^":
+ newValue = values + this.value;
+ break;
+ case "=":
+ newValue = values;
+ break;
+ }
+
+ break;
+
+ default:
+ return "E685: Internal error: option type `" + option.type + "' not supported";
+ }
+
+ if (newValue == null)
+ return "Operator " + operator + " not supported for option type " + this.type;
+ if (!this.isValidValue(newValue))
+ return "E474: Invalid argument: " + values;
+ this.setValues(newValue, scope);
+ }
+};
+ // TODO: Run this by default?
+Option.validateCompleter = function (values)
+{
+ let context = CompletionContext("");
+ let res = context.fork("", 0, this, this.completer);
+ if (!res)
+ res = context.allItems.items.map(function (item) [item.text]);
+ return Array.concat(values).every(
+ function (value) res.some(function (item) item[0] == value));
+}; //}}}
+
+function Options() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var prefService = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var optionHash = {};
+
+ function optionObserver(key, event, option)
+ {
+ // Trigger any setters.
+ let opt = options.get(option);
+ if (event == "change" && opt)
+ opt.set(opt.value, options.OPTION_SCOPE_GLOBAL);
+ }
+
+ storage.newMap("options", false);
+ storage.addObserver("options", optionObserver);
+ liberator.registerObserver("shutdown", function () {
+ storage.removeObserver("options", optionObserver);
+ });
+
+ function storePreference(name, value)
+ {
+ var type = prefService.getPrefType(name);
+ switch (typeof value)
+ {
+ case "string":
+ if (type == prefService.PREF_INVALID || type == prefService.PREF_STRING)
+ prefService.setCharPref(name, value);
+ else if (type == prefService.PREF_INT)
+ liberator.echoerr("E521: Number required after =: " + name + "=" + value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ case "number":
+ if (type == prefService.PREF_INVALID || type == prefService.PREF_INT)
+ prefService.setIntPref(name, value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ case "boolean":
+ if (type == prefService.PREF_INVALID || type == prefService.PREF_BOOL)
+ prefService.setBoolPref(name, value);
+ else if (type == prefService.PREF_INT)
+ liberator.echoerr("E521: Number required after =: " + name + "=" + value);
+ else
+ liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
+ break;
+ default:
+ liberator.echoerr("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")");
+ }
+ }
+
+ function loadPreference(name, forcedDefault, defaultBranch)
+ {
+ var defaultValue = null; // XXX
+ if (forcedDefault != null) // this argument sets defaults for non-user settable options (like extensions.history.comp_history)
+ defaultValue = forcedDefault;
+
+ var branch = defaultBranch ? prefService.getDefaultBranch("") : prefService;
+ var type = prefService.getPrefType(name);
+ try
+ {
+ switch (type)
+ {
+ case prefService.PREF_STRING:
+ var value = branch.getComplexValue(name, Components.interfaces.nsISupportsString).data;
+ // try in case it's a localized string (will throw an exception if not)
+ if (!prefService.prefIsLocked(name) && !prefService.prefHasUserValue(name) &&
+ /^chrome:\/\/.+\/locale\/.+\.properties/.test(value))
+ value = branch.getComplexValue(name, Components.interfaces.nsIPrefLocalizedString).data;
+ return value;
+ case prefService.PREF_INT:
+ return branch.getIntPref(name);
+ case prefService.PREF_BOOL:
+ return branch.getBoolPref(name);
+ default:
+ return defaultValue;
+ }
+ }
+ catch (e)
+ {
+ return defaultValue;
+ }
+ }
+
+ //
+ // firefox preferences which need to be changed to work well with vimperator
+ //
+
+ // work around firefox popup blocker
+ var popupAllowedEvents = loadPreference("dom.popup_allowed_events", "change click dblclick mouseup reset submit");
+ if (!/keypress/.test(popupAllowedEvents))
+ {
+ storePreference("dom.popup_allowed_events", popupAllowedEvents + " keypress");
+ liberator.registerObserver("shutdown", function ()
+ {
+ if (loadPreference("dom.popup_allowed_events", "")
+ == popupAllowedEvents + " keypress")
+ storePreference("dom.popup_allowed_events", popupAllowedEvents);
+ });
+ }
+
+ // TODO: maybe reset in .destroy()?
+ // TODO: move to vim.js or buffer.js
+ // we have our own typeahead find implementation
+ storePreference("accessibility.typeaheadfind.autostart", false);
+ storePreference("accessibility.typeaheadfind", false); // actually the above setting should do it, but has no effect in firefox
+
+ // start with saved session
+ storePreference("browser.startup.page", 3);
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["let"],
+ "Set or list a variable",
+ function (args)
+ {
+ args = args.string;
+
+ if (!args)
+ {
+ var str =
+ <table>
+ {
+ template.map(liberator.globalVariables, function ([i, value]) {
+ let prefix = typeof value == "number" ? "#" :
+ typeof value == "function" ? "*" :
+ " ";
+ return <tr>
+ <td style="width: 200px;">{i}</td>
+ <td>{prefix}{value}</td>
+ </tr>
+ })
+ }
+ </table>;
+ if (str.*.length())
+ liberator.echo(str, commandline.FORCE_MULTILINE);
+ else
+ liberator.echo("No variables found");
+ return;
+ }
+
+ var matches;
+ // 1 - type, 2 - name, 3 - +-., 4 - expr
+ if (matches = args.match(/([$@&])?([\w:]+)\s*([-+.])?=\s*(.+)/))
+ {
+ if (!matches[1])
+ {
+ var reference = liberator.variableReference(matches[2]);
+ if (!reference[0] && matches[3])
+ {
+ liberator.echoerr("E121: Undefined variable: " + matches[2]);
+ return;
+ }
+
+ var expr = liberator.evalExpression(matches[4]);
+ if (expr === undefined)
+ {
+ liberator.echoerr("E15: Invalid expression: " + matches[4]);
+ return;
+ }
+ else
+ {
+ if (!reference[0])
+ {
+ if (reference[2] == "g")
+ reference[0] = liberator.globalVariables;
+ else
+ return; // for now
+ }
+
+ if (matches[3])
+ {
+ if (matches[3] == "+")
+ reference[0][reference[1]] += expr;
+ else if (matches[3] == "-")
+ reference[0][reference[1]] -= expr;
+ else if (matches[3] == ".")
+ reference[0][reference[1]] += expr.toString();
+ }
+ else
+ reference[0][reference[1]] = expr;
+ }
+ }
+ }
+ // 1 - name
+ else if (matches = args.match(/^\s*([\w:]+)\s*$/))
+ {
+ var reference = liberator.variableReference(matches[1]);
+ if (!reference[0])
+ {
+ liberator.echoerr("E121: Undefined variable: " + matches[1]);
+ return;
+ }
+
+ var value = reference[0][reference[1]];
+ let prefix = typeof value == "number" ? "#" :
+ typeof value == "function" ? "*" :
+ " ";
+ liberator.echo(reference[1] + "\t\t" + prefix + value);
+ }
+ });
+
+ commands.add(["pref[erences]", "prefs"],
+ "Show " + config.hostApplication + " preferences",
+ function (args)
+ {
+ if (args.bang) // open Firefox settings GUI dialog
+ {
+ liberator.open("about:config",
+ (options["newtab"] && options.get("newtab").has("all", "prefs"))
+ ? liberator.NEW_TAB : liberator.CURRENT_TAB);
+ }
+ else
+ {
+ window.openPreferences();
+ }
+ },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["setl[ocal]"],
+ "Set local option",
+ function (args)
+ {
+ commands.get("set").execute(args.string, args.bang, args.count, { scope: options.OPTION_SCOPE_LOCAL });
+ },
+ {
+ bang: true,
+ count: true,
+ completer: function (context, args)
+ {
+ return commands.get("set").completer(context.filter, args.bang, args.count, { scope: options.OPTION_SCOPE_LOCAL });
+ },
+ literal: 0
+ }
+ );
+
+ commands.add(["setg[lobal]"],
+ "Set global option",
+ function (args)
+ {
+ commands.get("set").execute(args.string, args.bang, args.count, { scope: options.OPTION_SCOPE_GLOBAL });
+ },
+ {
+ bang: true,
+ count: true,
+ completer: function (context, args)
+ {
+ return commands.get("set").completer(context.filter, args.bang, args.count, { scope: options.OPTION_SCOPE_GLOBAL });
+ },
+ literal: 0
+ }
+ );
+
+ // TODO: support setting multiple options at once
+ commands.add(["se[t]"],
+ "Set an option",
+ function (args, modifiers)
+ {
+ let bang = args.bang;
+ args = args.string;
+ if (bang)
+ {
+ var onlyNonDefault = false;
+ if (!args)
+ {
+ args = "all";
+ onlyNonDefault = true;
+ }
+
+ let [matches, name, postfix, valueGiven, operator, value] =
+ args.match(/^\s*?([a-zA-Z0-9\.\-_{}]+)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
+ let reset = (postfix == "&");
+ let invertBoolean = (postfix == "!");
+
+ if (name == "all" && reset)
+ liberator.echoerr("You can't reset all options, it could make " + config.hostApplication + " unusable.");
+ else if (name == "all")
+ options.listPrefs(onlyNonDefault, "");
+ else if (reset)
+ options.resetPref(name);
+ else if (invertBoolean)
+ options.invertPref(name);
+ else if (valueGiven)
+ {
+ switch (value)
+ {
+ case undefined:
+ value = "";
+ break;
+ case "true":
+ value = true;
+ break;
+ case "false":
+ value = false;
+ break;
+ default:
+ if (/^\d+$/.test(value))
+ value = parseInt(value, 10);
+ }
+ options.setPref(name, value);
+ }
+ else
+ {
+ options.listPrefs(onlyNonDefault, name);
+ }
+ return;
+ }
+
+ let opt = options.parseOpt(args, modifiers);
+ if (!opt)
+ {
+ liberator.echoerr("Error parsing :set command: " + args);
+ return;
+ }
+
+ let option = opt.option;
+ if (option == null && !opt.all)
+ {
+ liberator.echoerr("No such option: " + opt.name);
+ return;
+ }
+
+ // reset a variable to its default value
+ if (opt.reset)
+ {
+ if (opt.all)
+ {
+ for (let option in options)
+ option.reset();
+ }
+ else
+ {
+ option.reset();
+ }
+ }
+ // read access
+ else if (opt.get)
+ {
+ if (opt.all)
+ {
+ options.list(opt.onlyNonDefault, opt.scope);
+ }
+ else
+ {
+ if (option.type == "boolean")
+ liberator.echo((opt.optionValue ? " " : "no") + option.name);
+ else
+ liberator.echo(" " + option.name + "=" + opt.optionValue);
+ }
+ }
+ // write access
+ // NOTE: the behavior is generally Vim compatible but could be
+ // improved. i.e. Vim's behavior is pretty sloppy to no real benefit
+ else
+ {
+ if (opt.option.type == "boolean")
+ {
+ if (opt.valueGiven)
+ {
+ liberator.echoerr("E474: Invalid argument: " + args);
+ return;
+ }
+ opt.values = !opt.unsetBoolean;
+ }
+ let res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert);
+ if (res)
+ liberator.echoerr(res);
+ }
+ },
+ {
+ bang: true,
+ completer: function (context, args, modifiers)
+ {
+ let filter = context.filter;
+ var optionCompletions = [];
+
+ if (args.bang) // list completions for about:config entries
+ {
+ if (filter[filter.length - 1] == "=")
+ {
+ context.advance(filter.length);
+ context.completions = [options.getPref(filter.substr(0, filter.length - 1)), "Current Value"];
+ return;
+ }
+
+ return completion.preference(context);
+ }
+
+ let opt = options.parseOpt(filter, modifiers);
+ let prefix = opt.prefix;
+
+ if (context.filter.indexOf("=") == -1)
+ {
+ if (prefix)
+ context.filters.push(function ({ item: opt }) opt.type == "boolean" || prefix == "inv" && opt.values instanceof Array);
+ return completion.option(context, opt.scope);
+ }
+ else if (prefix == "no")
+ return;
+
+ if (prefix)
+ context.advance(prefix.length);
+
+ let option = opt.option;
+ context.advance(context.filter.indexOf("=") + 1);
+
+ if (!option)
+ {
+ context.message = "No such option: " + opt.name;
+ context.highlight(0, name.length, "SPELLCHECK");
+ }
+
+ if (opt.get || opt.reset || !option || prefix)
+ return;
+
+ if (!opt.value)
+ {
+ context.fork("default", 0, this, function (context) {
+ context.title = ["Extra Completions"];
+ context.completions = [[option.value, "Current value"], [option.defaultValue, "Default value"]].filter(function (f) f[0])
+ });
+ }
+
+ completion.optionValue(context, opt.name, opt.operator);
+ },
+ literal: 0,
+ serial: function () [
+ {
+ command: this.name,
+ literalArg: opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
+ : opt.name + "=" + opt.value
+ }
+ for (opt in options)
+ if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & options.OPTION_SCOPE_GLOBAL))
+ ]
+ });
+
+ commands.add(["unl[et]"],
+ "Delete a variable",
+ function (args)
+ {
+ //var names = args.split(/ /);
+ //if (typeof names == "string") names = [names];
+
+ //var length = names.length;
+ //for (let i = 0, name = names[i]; i < length; name = names[++i])
+ for (let [,name] in args)
+ {
+ var name = args[i];
+ var reference = liberator.variableReference(name);
+ if (!reference[0])
+ {
+ if (!args.bang)
+ liberator.echoerr("E108: No such variable: " + name);
+ return;
+ }
+
+ delete reference[0][reference[1]];
+ }
+ },
+ {
+ argCount: "+",
+ bang: true
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ // TODO: Does this belong elsewhere?
+ liberator.registerObserver("load_completion", function ()
+ {
+ completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
+ completion.setFunctionCompleter([options.getPref, options.setPref, options.resetPref, options.invertPref],
+ [function () Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch)
+ .getChildList("", { value: 0 })
+ .map(function (pref) [pref, ""])]);
+ });
+
+ return {
+
+ OPTION_SCOPE_GLOBAL: 1,
+ OPTION_SCOPE_LOCAL: 2,
+ OPTION_SCOPE_BOTH: 3,
+
+ __iterator__: function ()
+ {
+ let sorted = [o for ([i, o] in Iterator(optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name));
+ return (v for ([k, v] in Iterator(sorted)));
+ },
+
+ add: function (names, description, type, defaultValue, extraInfo)
+ {
+ if (!extraInfo)
+ extraInfo = {};
+
+ let option = new Option(names, description, type, defaultValue, extraInfo);
+
+ if (!option)
+ return false;
+
+ if (option.name in optionHash)
+ {
+ // never replace for now
+ liberator.log("Warning: '" + names[0] + "' already exists, NOT replacing existing option.", 1);
+ return false;
+ }
+
+ // quickly access options with options["wildmode"]:
+ this.__defineGetter__(option.name, function () option.value);
+ this.__defineSetter__(option.name, function (value) { option.value = value; });
+
+ optionHash[option.name] = option;
+ return true;
+ },
+
+ get: function (name, scope)
+ {
+ if (!scope)
+ scope = options.OPTION_SCOPE_BOTH;
+
+ if (name in optionHash)
+ return (optionHash[name].scope & scope) && optionHash[name];
+
+ for (let opt in Iterator(options))
+ {
+ if (opt.hasName(name))
+ return (opt.scope & scope) && opt;
+ }
+
+ return null;
+ },
+
+ list: function (onlyNonDefault, scope)
+ {
+ if (!scope)
+ scope = options.OPTION_SCOPE_BOTH;
+
+ let opts = function (opt) {
+ for (let opt in Iterator(options))
+ {
+ let option = {
+ isDefault: opt.value == opt.defaultValue,
+ name: opt.name,
+ default: opt.defaultValue,
+ pre: "\u00a0\u00a0", /* Unicode nonbreaking space. */
+ value: <></>
+ };
+
+ if (onlyNonDefault && option.isDefault)
+ continue;
+ if (!(opt.scope & scope))
+ continue;
+
+ if (opt.type == "boolean")
+ {
+ if (!opt.value)
+ option.pre = "no";
+ option.default = (option.default ? "" : "no") + opt.name;
+ }
+ else
+ {
+ option.value = <>={template.highlight(opt.value)}</>;
+ }
+ yield option;
+ }
+ };
+
+ let list = template.options("Options", opts());
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ listPrefs: function (onlyNonDefault, filter)
+ {
+ if (!filter)
+ filter = "";
+
+ var prefArray = prefService.getChildList("", { value: 0 });
+ prefArray.sort();
+ let prefs = function () {
+ for each (let pref in prefArray)
+ {
+ var userValue = prefService.prefHasUserValue(pref);
+ if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1)
+ continue;
+
+ value = options.getPref(pref);
+
+ let option = {
+ isDefault: !userValue,
+ default: loadPreference(pref, null, true),
+ value: <>={template.highlight(value, true, 100)}</>,
+ name: pref,
+ pre: "\u00a0\u00a0" /* Unicode nonbreaking space. */
+ };
+
+ yield option;
+ }
+ };
+
+ let list = template.options(config.hostApplication + " Options", prefs());
+ commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ },
+
+ parseOpt: function parseOpt(args, modifiers)
+ {
+ let ret = {};
+ let matches, prefix, postfix, valueGiven;
+
+ [matches, prefix, ret.name, postfix, valueGiven, ret.operator, ret.value] =
+ args.match(/^\s*(no|inv)?([a-z_]*)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
+
+ ret.args = args;
+ ret.onlyNonDefault = false; // used for :set to print non-default options
+ if (!args)
+ {
+ ret.name = "all";
+ ret.onlyNonDefault = true;
+ }
+
+ if (matches)
+ ret.option = options.get(ret.name, ret.scope);
+
+ ret.prefix = prefix;
+ ret.postfix = postfix;
+
+ ret.all = (ret.name == "all");
+ ret.get = (ret.all || postfix == "?" || (ret.option && ret.option.type != "boolean" && !valueGiven));
+ ret.invert = (prefix == "inv" || postfix == "!");
+ ret.reset = (postfix == "&");
+ ret.unsetBoolean = (prefix == "no");
+
+ ret.scope = modifiers && modifiers.scope;
+
+ if (!ret.option)
+ return ret;
+
+ if (ret.value === undefined)
+ ret.value = "";
+
+ ret.optionValue = ret.option.get(ret.scope);
+ ret.optionValues = ret.option.getValues(ret.scope);
+
+ ret.values = ret.option.parseValues(ret.value);
+
+ return ret;
+ },
+
+ get store() storage.options,
+
+ getPref: function (name, forcedDefault)
+ {
+ return loadPreference(name, forcedDefault);
+ },
+
+ setPref: function (name, value)
+ {
+ return storePreference(name, value);
+ },
+
+ resetPref: function (name)
+ {
+ return prefService.clearUserPref(name);
+ },
+
+ // this works only for booleans
+ invertPref: function (name)
+ {
+ if (prefService.getPrefType(name) == prefService.PREF_BOOL)
+ this.setPref(name, !this.getPref(name));
+ else
+ liberator.echoerr("E488: Trailing characters: " + name + "!");
+ }
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/style.js b/common/content/style.js
new file mode 100644
index 00000000..a3d7b12e
--- /dev/null
+++ b/common/content/style.js
@@ -0,0 +1,582 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+ ©2008 Kris Maglione <maglione.k at Gmail>
+ Distributable under the terms of the MIT license, which allows
+ for sublicensing under any compatible license, including the MPL,
+ GPL, and MPL. Anyone who changes this file is welcome to relicense
+ it under any or all of those licenseses.
+}}} ***** END LICENSE BLOCK *****/
+
+Highlights.prototype.CSS = <![CDATA[
+ Boolean color: red;
+ Function color: navy;
+ Null color: blue;
+ Number color: blue;
+ Object color: maroon;
+ String color: green;
+
+ 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;
+
+ CompGroup
+ CompGroup:not(:first-of-type) margin-top: .5em;
+ CompTitle color: magenta; background: white; font-weight: bold;
+ CompTitle>* /* border-bottom: 1px dashed magenta; */
+ 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: #FF6060; /* light-red */
+ StatusLineSecure color: black; background: #B0FF00; /* light-green */
+
+ TabClose
+ TabIcon
+ TabText
+ 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;
+
+ Search,,* {
+ font-size: inherit;
+ padding: 0;
+ color: black;
+ background-color: yellow;
+ padding: 0;
+ }
+ ]]>.toString();
+function Highlights(name, store, serial)
+{
+ var self = this;
+ var highlight = {};
+ var styles = storage.styles;
+
+ const Highlight = Struct("class", "selector", "filter", "default", "value");
+ Highlight.defaultValue("filter", function () "chrome://liberator/content/buffer.xhtml" + "," + config.styleableChrome);
+ Highlight.defaultValue("selector", function () self.selector(this.class));
+ Highlight.defaultValue("value", function () this.default);
+ 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] || new Highlight(key);
+ styles.removeSheet(style.selector, null, null, null, true);
+
+ 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(style.selector, null, null, null, true);
+ return null;
+ }
+ newStyle = style.default;
+ force = true;
+ }
+
+ let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;")
+ .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug
+ css = style.selector + " { " + css + " }";
+
+ let error = styles.addSheet(style.selector, style.filter, css, true, force);
+ if (error)
+ return error;
+ style.value = newStyle;
+ highlight[style.class] = style;
+ }
+
+ this.selector = function (class)
+ {
+ let [, hl, rest] = class.match(/^(\w*)(.*)/);
+ return "[liberator|highlight~=" + hl + "]" + rest
+ };
+
+ this.reload = function ()
+ {
+ this.CSS.replace(/\{((?:.|\n)*?)\}/g, function (_, _1) _1.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*(.*)$/), 1));
+ 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);
+ }
+ }
+}
+
+function Styles(name, store, serial)
+{
+ /* Can't reference liberator or Components inside Styles --
+ * they're members of the window object, which disappear
+ * with this window.
+ */
+ const util = modules.util;
+ const sleep = liberator.sleep;
+ const storage = modules.storage;
+ const consoleService = Components.classes["@mozilla.org/consoleservice;1"]
+ .getService(Components.interfaces.nsIConsoleService);
+ const ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ const sss = Components.classes["@mozilla.org/content/style-sheet-service;1"]
+ .getService(Components.interfaces.nsIStyleSheetService);
+ const namespace = '@namespace html "' + XHTML + '";\n' +
+ '@namespace xul "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";\n' +
+ '@namespace liberator "' + NS.uri + '";\n';
+ const Sheet = new Struct("name", "sites", "css", "ref");
+
+ let cssUri = function (css) "chrome-data:text/css," + encodeURI(css);
+
+ let userSheets = [];
+ let systemSheets = [];
+ let userNames = {};
+ let systemNames = {};
+
+ 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));
+
+ this.addSheet = function (name, filter, css, system, force)
+ {
+ let sheets = system ? systemSheets : userSheets;
+ let names = system ? systemNames : userNames;
+ if (name && name in names)
+ this.removeSheet(name, null, null, null, system);
+
+ let sheet = sheets.filter(function (s) s.sites.join(",") == filter && s.css == css)[0];
+ if (!sheet)
+ sheet = new Sheet(name, filter.split(",").filter(util.identity), css, null);
+
+ if (sheet.ref == null) // Not registered yet
+ {
+ sheet.ref = [];
+ try
+ {
+ this.registerSheet(cssUri(wrapCSS(sheet)), !force);
+ }
+ catch (e)
+ {
+ return e.echoerr || e;
+ }
+ sheets.push(sheet);
+ }
+ if (name)
+ {
+ sheet.ref.push(name);
+ names[name] = sheet;
+ }
+ return null;
+ }
+
+ this.findSheets = function (name, filter, css, index, system)
+ {
+ 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]);
+ };
+
+ this.removeSheet = function (name, filter, css, index, system)
+ {
+ let self = this;
+ 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(name, f, index, system), 0);
+
+ if (filter == undefined)
+ filter = "";
+
+ let matches = this.findSheets(name, filter, css, index, system);
+ if (matches.length == 0)
+ return;
+
+ for (let [,sheet] in Iterator(matches.reverse()))
+ {
+ if (name)
+ {
+ if (sheet.ref.indexOf(name) > -1)
+ sheet.ref.splice(sheet.ref.indexOf(name), 1);
+ delete names[name];
+ }
+ if (!sheet.ref.length)
+ {
+ this.unregisterSheet(cssUri(wrapCSS(sheet)));
+ if (sheets.indexOf(sheet) > -1)
+ sheets.splice(sheets.indexOf(sheet), 1);
+ }
+ if (filter)
+ {
+ let sites = sheet.sites.filter(function (f) f != filter);
+ if (sites.length)
+ this.addSheet(name, sites.join(","), css, system, true);
+ }
+ }
+ return matches.length;
+ }
+
+ this.registerSheet = function (uri, doCheckSyntax, reload)
+ {
+ //dump (uri + "\n\n");
+ if (doCheckSyntax)
+ checkSyntax(uri);
+ if (reload)
+ this.unregisterSheet(uri);
+ uri = ios.newURI(uri, null, null);
+ if (reload || !sss.sheetRegistered(uri, sss.USER_SHEET))
+ sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
+ }
+
+ this.unregisterSheet = function (uri)
+ {
+ uri = ios.newURI(uri, null, null);
+ if (sss.sheetRegistered(uri, sss.USER_SHEET))
+ sss.unregisterSheet(uri, sss.USER_SHEET);
+ }
+
+ function wrapCSS(sheet)
+ {
+ let filter = sheet.sites;
+ let css = sheet.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 + "@-moz-document " + selectors + "{\n" + css + "\n}\n";
+ }
+
+ let queryinterface = XPCOMUtils.generateQI([Components.interfaces.nsIConsoleListener]);
+ /* What happens if more than one thread tries to use this? */
+ let testDoc = document.implementation.createDocument(XHTML, "doc", null);
+ function checkSyntax(uri)
+ {
+ let errors = [];
+ let listener = {
+ QueryInterface: queryinterface,
+ observe: function (message)
+ {
+ try
+ {
+ message = message.QueryInterface(Components.interfaces.nsIScriptError);
+ if (message.sourceName == uri)
+ errors.push(message);
+ }
+ catch (e) {}
+ }
+ };
+
+ try
+ {
+ consoleService.registerListener(listener);
+ if (testDoc.documentElement.firstChild)
+ testDoc.documentElement.removeChild(testDoc.documentElement.firstChild);
+ testDoc.documentElement.appendChild(util.xmlToDom(
+ <html><head><link type="text/css" rel="stylesheet" href={uri}/></head></html>, testDoc));
+
+ while (true)
+ {
+ try
+ {
+ // Throws NS_ERROR_DOM_INVALID_ACCESS_ERR if not finished loading
+ testDoc.styleSheets[0].cssRules.length;
+ break;
+ }
+ catch (e)
+ {
+ if (e.name != "NS_ERROR_DOM_INVALID_ACCESS_ERR")
+ return [e.toString()];
+ sleep(10);
+ }
+ }
+ }
+ finally
+ {
+ consoleService.unregisterListener(listener);
+ }
+ if (errors.length)
+ {
+ let err = new Error("", errors[0].sourceName.replace(/^(chrome-data:text\/css,).*/, "$1..."), errors[0].lineNumber);
+ err.name = "CSSError"
+ err.message = errors.reduce(function (msg, e) msg + "; " + e.lineNumber + ": " + e.errorMessage,
+ errors.shift().errorMessage);
+ err.echoerr = err.fileName + ":" + err.lineNumber + ": " + err.message;
+ throw err;
+ }
+ }
+}
+let (array = util.Array)
+{
+ Styles.prototype = {
+ get sites() array.uniq(array.flatten([v.sites for ([k, v] in this.userSheets)]))
+ };
+}
+
+const styles = storage.newObject("styles", Styles, false);
+const highlight = storage.newObject("highlight", Highlights, false);
+highlight.CSS = Highlights.prototype.CSS;
+highlight.reload();
+
+liberator.triggerObserver("load_styles", "styles");
+liberator.triggerObserver("load_highlight", "highlight");
+
+liberator.registerObserver("load_commands", function ()
+{
+ // TODO: :colo default needs :hi clear
+ commands.add(["colo[rscheme]"],
+ "Load a color scheme",
+ function (args)
+ {
+ let scheme = args[0];
+
+ if (io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]))
+ autocommands.trigger("ColorScheme", { name: scheme });
+ else
+ liberator.echoerr("E185: Cannot find color scheme " + scheme);
+ },
+ {
+ argCount: "1",
+ completer: function (context) completion.colorScheme(context)
+ });
+
+ 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].ref.length)]);
+ let str = template.tabular(["", "Filter", "CSS"],
+ ["padding: 0 1em 0 1ex; vertical-align: top", "padding: 0 1em 0 0; vertical-align: top"],
+ ([k, v[1].join(","), v[2]]
+ for ([i, [k, v]] in Iterator(list))
+ if ((!filter || v[1].indexOf(filter) >= 0) && (!name || v[0] == name))));
+ commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ }
+ else
+ {
+ if ("-append" in args)
+ {
+ let sheet = styles.findSheets(name, null, null, null, false)[0];
+ if (sheet)
+ {
+ filter = sheet.sites.concat(filter).join(",");
+ css = sheet.css.replace(/;?\s*$/, "; " + css);
+ }
+ }
+ let err = styles.addSheet(name, filter, css, false, args.bang);
+ if (err)
+ liberator.echoerr(err);
+ }
+ },
+ {
+ bang: true,
+ completer: function (context, args) {
+ let compl = [];
+ if (args.completeArg == 0)
+ {
+ try
+ {
+ compl.push([content.location.host, "Current Host"]);
+ compl.push([content.location.href, "Current URL"]);
+ }
+ catch (e) {}
+ context.completions = compl.concat([[s, ""] for each (s in styles.sites)])
+ }
+ else if (args.completeArg == 1)
+ {
+ let sheet = styles.findSheets(args["-name"], null, null, null, false)[0];
+ 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)
+ ]
+ });
+
+ commands.add(["dels[tyle]"],
+ "Remove a user stylesheet",
+ function (args) {
+ styles.removeSheet(args["-name"], args[0], args.literalArg, args["-index"], false);
+ },
+ {
+ completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); },
+ literal: 1,
+ options: [[["-index", "-i"], commands.OPTION_INT, null, function () [[i, <>{s.sites.join(",")}: {s.css.replace("\n", "\\n")}</>] for ([i, s] in styles.userSheets)]],
+ [["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]]]
+ });
+
+ 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 [key, css] = args;
+ if (!css && !(key && args.bang))
+ {
+ 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;
+ }
+ let error = highlight.set(key, css, args.bang, "-append" in args);
+ if (error)
+ liberator.echoerr(error);
+ },
+ {
+ bang: true,
+ // TODO: add this as a standard highlight completion function?
+ completer: function (context, args)
+ {
+ if (args.completeArg == 0)
+ context.completions = [[v.class, ""] 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)
+ ]
+ });
+});
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/tabs.js b/common/content/tabs.js
new file mode 100644
index 00000000..80ba66af
--- /dev/null
+++ b/common/content/tabs.js
@@ -0,0 +1,1000 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+// TODO: many methods do not work with Thunderbird correctly yet
+
+function Tabs() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var tabmail;
+ var getBrowser = (function () {
+ if (config.hostApplication == "Thunderbird")
+ {
+ return function ()
+ {
+ if (!tabmail)
+ {
+ tabmail = document.getElementById('tabmail');
+ tabmail.__defineGetter__('mTabContainer', function () this.tabContainer);
+ tabmail.__defineGetter__('mTabs', function () this.tabContainer.childNodes);
+ tabmail.__defineGetter__('mCurrentTab', function () this.tabContainer.selectedItem);
+ tabmail.__defineGetter__('mStrip', function () this.tabStrip);
+ tabmail.__defineGetter__('browsers', function () [browser for (browser in Iterator(this.mTabs))] );
+ }
+ return tabmail;
+ };
+ }
+ else
+ return window.getBrowser;
+ })();
+ var alternates = [getBrowser().mCurrentTab, null];
+
+ // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
+ var lastBufferSwitchArgs = "";
+ var lastBufferSwitchSpecial = true;
+
+ // @param spec can either be:
+ // - an absolute integer
+ // - "" for the current tab
+ // - "+1" for the next tab
+ // - "-3" for the tab, which is 3 positions left of the current
+ // - "$" for the last tab
+ function indexFromSpec(spec, wrap)
+ {
+ var position = getBrowser().mTabContainer.selectedIndex;
+ var length = getBrowser().mTabs.length;
+ var last = length - 1;
+
+ if (spec === undefined || spec === "")
+ return position;
+
+ if (typeof spec === "number")
+ position = spec;
+ else if (spec === "$")
+ position = last;
+ else if (/^[+-]\d+$/.test(spec))
+ position += parseInt(spec, 10);
+ else if (/^\d+$/.test(spec))
+ position = parseInt(spec, 10);
+ else
+ return -1;
+
+ if (position > last)
+ position = wrap ? position % length : last;
+ else if (position < 0)
+ position = wrap ? (position % length) + length : 0;
+
+ return position;
+ }
+
+ function copyTab(to, from)
+ {
+ var ss = Components.classes["@mozilla.org/browser/sessionstore;1"]
+ .getService(Components.interfaces.nsISessionStore);
+
+ if (!from)
+ from = getBrowser().mTabContainer.selectedItem;
+
+ var tabState = ss.getTabState(from);
+ ss.setTabState(to, tabState);
+ }
+
+ // hide tabs initially
+ if (config.name == "Vimperator")
+ getBrowser().mStrip.getElementsByClassName("tabbrowser-tabs")[0].collapsed = true;
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["showtabline", "stal"],
+ "Control when to show the tab bar of opened web pages",
+ "number", config.name == "Vimperator" ? 2 : 0,
+ {
+ setter: function (value)
+ {
+ let tabStrip = tabs.tabStrip;
+
+ if (!tabStrip)
+ return;
+
+ if (value == 0)
+ {
+ tabStrip.collapsed = true;
+ }
+ else
+ {
+ let pref = "browser.tabStrip.autoHide";
+ if (options.getPref(pref) == null)
+ pref = "browser.tabs.autoHide";
+ options.setPref(pref, value == 1);
+ tabStrip.collapsed = false;
+ }
+
+ return value;
+ },
+ completer: function (filter)
+ {
+ return [
+ ["0", "Never show tab bar"],
+ ["1", "Show tab bar only if more than one tab is open"],
+ ["2", "Always show tab bar"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+
+ if (config.name == "Vimperator")
+ {
+ options.add(["activate", "act"],
+ "Define when tabs are automatically activated",
+ "stringlist", "homepage,quickmark,tabopen,paste",
+ {
+ completer: function (filter)
+ {
+ return [
+ ["homepage", "gH mapping"],
+ ["quickmark", "go and gn mappings"],
+ ["tabopen", ":tabopen[!] command"],
+ ["paste", "P and gP mappings"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+
+ options.add(["newtab"],
+ "Define which commands should output in a new tab by default",
+ "stringlist", "",
+ {
+ completer: function (filter)
+ {
+ return [
+ ["all", "All commands"],
+ ["addons", ":addo[ns] command"],
+ ["downloads", ":downl[oads] command"],
+ ["help", ":h[elp] command"],
+ ["javascript", ":javascript! or :js! command"],
+ ["prefs", ":pref[erences]! or :prefs! command"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+
+ options.add(["popups", "pps"],
+ "Where to show requested popup windows",
+ "number", 1,
+ {
+ setter: function (value)
+ {
+ var values = [[0, 1], // always in current tab
+ [0, 3], // in a new tab
+ [2, 3], // in a new window if it has specified sizes
+ [1, 2], // always in new window
+ [2, 1]];// current tab unless it has specified sizes
+
+ options.setPref("browser.link.open_newwindow.restriction", values[value][0]);
+ options.setPref("browser.link.open_newwindow", values[value][1]);
+
+ return value;
+ },
+ completer: function (filter)
+ {
+ return [
+ ["0", "Force to open in the current tab"],
+ ["1", "Always open in a new tab"],
+ ["2", "Open in a new window if it has a specific requested size (default in Firefox)"],
+ ["3", "Always open in a new window"],
+ ["4", "Open in the same tab unless it has a specific requested size"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+ // TODO: Add option, or only apply when go~=[nN]
+ styles.addSheet("tab-binding", "chrome://browser/content/browser.xul",
+ ".tabbrowser-tab { -moz-binding: url(chrome://liberator/content/bindings.xml#tab) !important; }", true);
+
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ mappings.add([modes.NORMAL], ["g0", "g^"],
+ "Go to the first tab",
+ function (count) { tabs.select(0); });
+
+ mappings.add([modes.NORMAL], ["g$"],
+ "Go to the last tab",
+ function (count) { tabs.select("$"); });
+
+ mappings.add([modes.NORMAL], ["gt", "<C-n>", "<C-Tab>", "<C-PageDown>"],
+ "Go to the next tab",
+ function (count) { tabs.select(count > 0 ? count - 1: "+1", count > 0 ? false : true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["gT", "<C-p>", "<C-S-Tab>", "<C-PageUp>"],
+ "Go to previous tab",
+ function (count) { tabs.select("-" + (count < 1 ? 1 : count), true); },
+ { flags: Mappings.flags.COUNT });
+
+ if (config.name == "Vimperator")
+ {
+ mappings.add([modes.NORMAL], ["b"],
+ "Open a prompt to switch buffers",
+ function () { commandline.open(":", "buffer! ", modes.EX); });
+
+ mappings.add([modes.NORMAL], ["B"],
+ "Show buffer list",
+ function () { tabs.list(false); });
+
+ mappings.add([modes.NORMAL], ["d"],
+ "Delete current buffer",
+ function (count) { tabs.remove(getBrowser().mCurrentTab, count, false, 0); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["D"],
+ "Delete current buffer, focus tab to the left",
+ function (count) { tabs.remove(getBrowser().mCurrentTab, count, true, 0); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["gb"],
+ "Repeat last :buffer[!] command",
+ function (count) { tabs.switchTo(null, null, count, false); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["gB"],
+ "Repeat last :buffer[!] command in reverse direction",
+ function (count) { tabs.switchTo(null, null, count, true); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["u"],
+ "Undo closing of a tab",
+ function (count) { commands.get("undo").execute("", false, count); },
+ { flags: Mappings.flags.COUNT });
+
+ mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
+ "Select the alternate tab or the [count]th tab",
+ function (count)
+ {
+ if (count < 1)
+ tabs.selectAlternateTab();
+ else
+ tabs.switchTo(count.toString(), false);
+ },
+ { flags: Mappings.flags.COUNT });
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
+ "Delete current buffer",
+ function (args)
+ {
+ let special = args.bang;
+ let count = args.count;
+ args = args.string;
+
+ if (args)
+ {
+ args = args.toLowerCase();
+ let removed = 0;
+ let matches = args.match(/^(\d+):?/);
+
+ if (matches)
+ {
+ tabs.remove(tabs.getTab(parseInt(matches[1], 10) - 1));
+ removed = 1;
+ }
+ else
+ {
+ let browsers = getBrowser().browsers;
+ for (let i = browsers.length - 1; i >= 0; i--)
+ {
+ let title = browsers[i].contentTitle.toLowerCase() || "";
+ let uri = browsers[i].currentURI.spec.toLowerCase();
+ let host = browsers[i].currentURI.host.toLowerCase();
+
+ if (host.indexOf(args) >= 0 || uri == args ||
+ (special && (title.indexOf(args) >= 0 || uri.indexOf(args) >= 0)))
+ {
+ tabs.remove(tabs.getTab(i));
+ removed++;
+ }
+ }
+ }
+
+ if (removed > 0)
+ liberator.echo(removed + " fewer tab(s)");
+ else
+ liberator.echoerr("E94: No matching tab for " + args);
+ }
+ else // just remove the current tab
+ tabs.remove(getBrowser().mCurrentTab, count > 0 ? count : 1, special, 0);
+ },
+ {
+ bang: true,
+ count: true,
+ completer: function (context) completion.buffer(context),
+ literal: 0
+ });
+
+ // TODO: this should open in a new tab positioned directly after the current one, not at the end
+ commands.add(["tab"],
+ "Execute a command and tell it to output in a new tab",
+ function (args)
+ {
+ liberator.forceNewTab = true;
+ liberator.execute(args.string);
+ liberator.forceNewTab = false;
+ },
+ {
+ argCount: "+",
+ completer: function (context) completion.ex(context),
+ literal: 0
+ });
+
+ commands.add(["tabl[ast]", "bl[ast]"],
+ "Switch to the last tab",
+ function () tabs.select("$", false),
+ { argCount: "0" });
+
+ // TODO: "Zero count" if 0 specified as arg
+ commands.add(["tabp[revious]", "tp[revious]", "tabN[ext]", "tN[ext]", "bp[revious]", "bN[ext]"],
+ "Switch to the previous tab or go [count] tabs back",
+ function (args)
+ {
+ let count = args.count;
+ args = args.string;
+
+ // count is ignored if an arg is specified, as per Vim
+ if (args)
+ {
+ if (/^\d+$/.test(args))
+ tabs.select("-" + args, true); // FIXME: urgh!
+ else
+ liberator.echoerr("E488: Trailing characters");
+ }
+ else if (count > 0)
+ {
+ tabs.select("-" + count, true);
+ }
+ else
+ {
+ tabs.select("-1", true);
+ }
+ },
+ { count: true });
+
+ // TODO: "Zero count" if 0 specified as arg
+ commands.add(["tabn[ext]", "tn[ext]", "bn[ext]"],
+ "Switch to the next or [count]th tab",
+ function (args)
+ {
+ let count = args.count;
+ args = args.string;
+
+ if (args || count > 0)
+ {
+ var index;
+
+ // count is ignored if an arg is specified, as per Vim
+ if (args)
+ {
+ if (/^\d+$/.test(args))
+ {
+ index = args - 1;
+ }
+ else
+ {
+ liberator.echoerr("E488: Trailing characters");
+ return;
+ }
+ }
+ else
+ {
+ index = count - 1;
+ }
+
+ if (index < tabs.count)
+ tabs.select(index, true);
+ else
+ liberator.beep();
+ }
+ else
+ {
+ tabs.select("+1", true);
+ }
+ },
+ { count: true });
+
+ commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
+ "Switch to the first tab",
+ function () { tabs.select(0, false); },
+ { argCount: "0" });
+
+ if (config.name == "Vimperator")
+ {
+ // TODO: "Zero count" if 0 specified as arg, multiple args and count ranges?
+ commands.add(["b[uffer]"],
+ "Switch to a buffer",
+ function (args)
+ {
+ let count = args.count;
+ let special = args.special;
+ args = args.string;
+
+ // if a numeric arg is specified any count is ignored; if a
+ // count and non-numeric arg are both specified then E488
+ if (args && count > 0)
+ {
+ if (/^\d+$/.test(args))
+ tabs.switchTo(args, special);
+ else
+ liberator.echoerr("E488: Trailing characters");
+ }
+ else if (count > 0)
+ {
+ tabs.switchTo(count.toString(), special);
+ }
+ else
+ {
+ tabs.switchTo(args, special);
+ }
+ },
+ {
+ argCount: "?",
+ bang: true,
+ count: true,
+ completer: function (context) completion.buffer(context),
+ literal: 0
+ });
+
+ commands.add(["buffers", "files", "ls", "tabs"],
+ "Show a list of all buffers",
+ function (args) { tabs.list(args.literalArg); },
+ {
+ argCount: "?",
+ literal: 0
+ });
+
+ commands.add(["quita[ll]", "qa[ll]"],
+ "Quit " + config.name,
+ function (args) { liberator.quit(false, args.bang); },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ commands.add(["reloada[ll]"],
+ "Reload all tab pages",
+ function (args) { tabs.reloadAll(args.bang); },
+ {
+ argCount: "0",
+ bang: true
+ });
+
+ // TODO: add count support
+ commands.add(["tabm[ove]"],
+ "Move the current tab after tab N",
+ function (args)
+ {
+ let special = args.bang;
+ args = args.string;
+
+ // FIXME: tabmove! N should probably produce an error
+ if (!/^([+-]?\d+|)$/.test(args))
+ {
+ liberator.echoerr("E488: Trailing characters");
+ return;
+ }
+
+ if (!args)
+ args = "$"; // if not specified, move to the last tab
+
+ tabs.move(getBrowser().mCurrentTab, args, special);
+ },
+ { bang: true });
+
+ commands.add(["tabo[nly]"],
+ "Close all other tabs",
+ function () { tabs.keepOnly(getBrowser().mCurrentTab); },
+ { argCount: "0" });
+
+ commands.add(["tabopen", "t[open]", "tabnew", "tabe[dit]"],
+ "Open one or more URLs in a new tab",
+ function (args)
+ {
+ let special = args.bang;
+ args = args.string;
+
+ var where = special ? liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB;
+ if (/\btabopen\b/.test(options["activate"]))
+ where = special ? liberator.NEW_BACKGROUND_TAB : liberator.NEW_TAB;
+
+ if (args)
+ liberator.open(args, where);
+ else
+ liberator.open("about:blank", where);
+ },
+ {
+ bang: true,
+ completer: function (context) completion.url(context),
+ literal: 0
+ });
+
+ commands.add(["tabde[tach]"],
+ "Detach current tab to its own window",
+ function () { tabs.detachTab(null); },
+ { argCount: "0" });
+
+ commands.add(["tabd[uplicate]"],
+ "Duplicate current tab",
+ function (args)
+ {
+ var tab = tabs.getTab();
+
+ var activate = args.bang ? true : false;
+ if (/\btabopen\b/.test(options["activate"]))
+ activate = !activate;
+
+ for (let i in util.range(0, Math.max(1, args.count)))
+ tabs.cloneTab(tab, activate);
+ },
+ {
+ argCount: "0",
+ bang: true,
+ count: true
+ });
+ }
+
+ if (liberator.has("session"))
+ {
+ // TODO: extract common functionality of "undoall"
+ commands.add(["u[ndo]"],
+ "Undo closing of a tab",
+ function (args)
+ {
+ let count = args.count;
+ args = args[0]
+
+ if (count < 1)
+ count = 1;
+
+ if (args)
+ {
+ count = 0;
+ for (let [i, item] in Iterator(tabs.closedTabs))
+ {
+ if (item.state.entries[0].url == args)
+ {
+ count = i + 1;
+ break;
+ }
+ }
+
+ if (!count)
+ {
+ liberator.echoerr("Exxx: No matching closed tab");
+ return;
+ }
+ }
+
+ window.undoCloseTab(count - 1);
+ },
+ {
+ argCount: "?",
+ completer: function (context)
+ {
+ context.keys = { text: function (item) item.state.entries[0].url, description: "title" };
+ context.completions = tabs.closedTabs;
+ },
+ count: true,
+ literal: 0
+ });
+
+ commands.add(["undoa[ll]"],
+ "Undo closing of all closed tabs",
+ function (args)
+ {
+ for (let i in Itarator(tabs.closedTabs))
+ window.undoCloseTab(0);
+
+ },
+ { argCount: "0" });
+
+ commands.add(["wqa[ll]", "wq", "xa[ll]"],
+ "Save the session and quit",
+ function () { liberator.quit(true); },
+ { argCount: "0" });
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ get alternate() alternates[1],
+
+ get browsers()
+ {
+ let browsers = getBrowser().browsers;
+ for (let i = 0; i < browsers.length; i++)
+ yield [i, browsers[i]];
+ },
+
+ get count() getBrowser().mTabs.length,
+
+ get options()
+ {
+ let store = this.localStore;
+ if (!("options" in store))
+ store.options = {};
+ return store.options;
+ },
+
+ get localStore()
+ {
+ let tab = this.getTab();
+ if (!tab.liberatorStore)
+ tab.liberatorStore = {};
+ return tab.liberatorStore;
+ },
+
+ get tabStrip()
+ {
+ if (config.hostApplication == "Firefox")
+ return getBrowser().mStrip.getElementsByClassName("tabbrowser-tabs")[0];
+ else if (config.hostApplication == "Thunderbird")
+ return getBrowser().mStrip;
+ },
+
+ // @returns the index of the currently selected tab starting with 0
+ index: function (tab)
+ {
+ if (tab)
+ return Array.indexOf(getBrowser().mTabs, tab);
+
+ return getBrowser().mTabContainer.selectedIndex;
+ },
+
+ // TODO: implement filter
+ // @returns an array of tabs which match filter
+ get: function (filter)
+ {
+ var buffers = [];
+ for (let [i, browser] in this.browsers)
+ {
+ var title = browser.contentTitle || "(Untitled)";
+ var uri = browser.currentURI.spec;
+ var number = i + 1;
+ buffers.push([number, title, uri]);
+ }
+ return buffers;
+ },
+
+ getContentIndex: function (content)
+ {
+ for (let [i, browser] in this.browsers)
+ {
+ if (browser.contentWindow == content)
+ return i;
+ if (browser.contentDocument == content)
+ return i;
+ }
+ },
+
+ getTab: function (index)
+ {
+ if (index != undefined)
+ return getBrowser().mTabs[index];
+
+ return getBrowser().mTabContainer.selectedItem;
+ },
+
+ get closedTabs()
+ {
+ const json = Components.classes["@mozilla.org/dom/json;1"]
+ .createInstance(Components.interfaces.nsIJSON);
+ const ss = Components.classes["@mozilla.org/browser/sessionstore;1"]
+ .getService(Components.interfaces.nsISessionStore);
+ return json.decode(ss.getClosedTabData(window));
+ },
+
+ list: function (filter)
+ {
+ completion.listCompleter("buffer", filter);
+ },
+
+ // wrap causes the movement to wrap around the start and end of the tab list
+ // NOTE: position is a 0 based index
+ move: function (tab, spec, wrap)
+ {
+ var index = indexFromSpec(spec, wrap);
+ getBrowser().moveTabTo(tab, index);
+ },
+
+ // quitOnLastTab = 1: quit without saving session
+ // quitOnLastTab = 2: quit and save session
+ remove: function (tab, count, focusLeftTab, quitOnLastTab)
+ {
+ var removeOrBlankTab = {
+ Firefox: function (tab)
+ {
+ if (getBrowser().mTabs.length > 1)
+ getBrowser().removeTab(tab);
+ else
+ {
+ if (buffer.URL != "about:blank" ||
+ window.getWebNavigation().sessionHistory.count > 0)
+ {
+ liberator.open("about:blank", liberator.NEW_BACKGROUND_TAB);
+ getBrowser().removeTab(tab);
+ }
+ else
+ liberator.beep();
+ }
+ },
+ Thunderbird: function (tab)
+ {
+ if (getBrowser().mTabs.length > 1)
+ getBrowser().removeTab(tab);
+ else
+ liberator.beep();
+ }
+ }[config.hostApplication] || function () {};
+
+ if (typeof count != "number" || count < 1)
+ count = 1;
+
+ if (quitOnLastTab >= 1 && getBrowser().mTabs.length <= count)
+ {
+ if (liberator.windows.length > 1)
+ window.close();
+ else
+ liberator.quit(quitOnLastTab == 2);
+
+ return;
+ }
+
+ let index = this.index(tab);
+ if (focusLeftTab)
+ {
+ let lastRemovedTab = 0;
+ for (let i = index; i > index - count && i >= 0; i--)
+ {
+ removeOrBlankTab(this.getTab(i));
+ lastRemovedTab = i > 0 ? i : 1;
+ }
+ getBrowser().mTabContainer.selectedIndex = lastRemovedTab - 1;
+ }
+ else
+ {
+ let i = index + count - 1;
+ if (i >= this.count)
+ i = this.count - 1;
+
+ for (; i >= index; i--)
+ removeOrBlankTab(this.getTab(i));
+ getBrowser().mTabContainer.selectedIndex = index;
+ }
+ },
+
+ keepOnly: function (tab)
+ {
+ getBrowser().removeAllTabsBut(tab);
+ },
+
+ select: function (spec, wrap)
+ {
+ var index = indexFromSpec(spec, wrap);
+ // FIXME:
+ if (index === -1)
+ {
+ liberator.beep(); // XXX: move to ex-handling?
+ return;
+ }
+ getBrowser().mTabContainer.selectedIndex = index;
+ },
+
+ reload: function (tab, bypassCache)
+ {
+ if (bypassCache)
+ {
+ const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+ const flags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ getBrowser().getBrowserForTab(tab).reloadWithFlags(flags);
+ }
+ else
+ {
+ getBrowser().reloadTab(tab);
+ }
+ },
+
+ reloadAll: function (bypassCache)
+ {
+ if (bypassCache)
+ {
+ for (let i = 0; i < getBrowser().mTabs.length; i++)
+ {
+ try
+ {
+ this.reload(getBrowser().mTabs[i], bypassCache);
+ }
+ catch (e)
+ {
+ // FIXME: can we do anything useful here without stopping the
+ // other tabs from reloading?
+ }
+ }
+ }
+ else
+ {
+ getBrowser().reloadAllTabs();
+ }
+ },
+
+ // "buffer" is a string which matches the URL or title of a buffer, if it
+ // is null, the last used string is used again
+ switchTo: function (buffer, allowNonUnique, count, reverse)
+ {
+ if (buffer == "")
+ return;
+
+ if (buffer != null)
+ {
+ // store this command, so it can be repeated with "B"
+ lastBufferSwitchArgs = buffer;
+ lastBufferSwitchSpecial = allowNonUnique;
+ }
+ else
+ {
+ buffer = lastBufferSwitchArgs;
+ if (allowNonUnique === undefined || allowNonUnique == null) // XXX
+ allowNonUnique = lastBufferSwitchSpecial;
+ }
+
+ if (buffer == "#")
+ {
+ tabs.selectAlternateTab();
+ return;
+ }
+
+ if (!count || count < 1)
+ count = 1;
+ if (typeof reverse != "boolean")
+ reverse = false;
+
+ let matches = buffer.match(/^(\d+):?/);
+ if (matches)
+ {
+ tabs.select(parseInt(matches[1], 10) - 1, false); // make it zero-based
+ return;
+ }
+
+ matches = [];
+ let lowerBuffer = buffer.toLowerCase();
+ let first = tabs.index() + (reverse ? 0 : 1);
+ let nbrowsers = getBrowser().browsers.length;
+ for (let [i,] in tabs.browsers)
+ {
+ let index = (i + first) % nbrowsers;
+ let url = getBrowser().getBrowserAtIndex(index).contentDocument.location.href;
+ let title = getBrowser().getBrowserAtIndex(index).contentDocument.title.toLowerCase();
+ if (url == buffer)
+ {
+ tabs.select(index, false);
+ return;
+ }
+
+ if (url.indexOf(buffer) >= 0 || title.indexOf(lowerBuffer) >= 0)
+ matches.push(index);
+ }
+ if (matches.length == 0)
+ liberator.echoerr("E94: No matching buffer for " + buffer);
+ else if (matches.length > 1 && !allowNonUnique)
+ liberator.echoerr("E93: More than one match for " + buffer);
+ else
+ {
+ if (reverse)
+ {
+ index = matches.length - count;
+ while (index < 0)
+ index += matches.length;
+ }
+ else
+ index = (count - 1) % matches.length;
+
+ tabs.select(matches[index], false);
+ }
+ },
+
+ cloneTab: function (tab, activate)
+ {
+ var newTab = getBrowser().addTab();
+ copyTab(newTab, tab);
+
+ if (activate)
+ getBrowser().mTabContainer.selectedItem = newTab;
+
+ return newTab;
+ },
+
+ detachTab: function (tab)
+ {
+ if (!tab)
+ tab = getBrowser().mTabContainer.selectedItem;
+
+ window.open();
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+
+ copyTab(win.getBrowser().mCurrentTab, tab);
+ this.remove(tab, 1, false, 1);
+ },
+
+ selectAlternateTab: function ()
+ {
+ if (tabs.alternate == null || tabs.getTab() == tabs.alternate)
+ {
+ liberator.echoerr("E23: No alternate page");
+ return;
+ }
+
+ // NOTE: this currently relies on v.tabs.index() returning the
+ // currently selected tab index when passed null
+ var index = tabs.index(tabs.alternate);
+
+ // TODO: since a tab close is more like a bdelete for us we
+ // should probably reopen the closed tab when a 'deleted'
+ // alternate is selected
+ if (index == -1)
+ liberator.echoerr("E86: Buffer does not exist"); // TODO: This should read "Buffer N does not exist"
+ else
+ tabs.select(index);
+ },
+
+ // NOTE: when restarting a session FF selects the first tab and then the
+ // tab that was selected when the session was created. As a result the
+ // alternate after a restart is often incorrectly tab 1 when there
+ // shouldn't be one yet.
+ updateSelectionHistory: function ()
+ {
+ alternates = [this.getTab(), alternates[0]];
+ }
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/template.js b/common/content/template.js
new file mode 100644
index 00000000..b3c832fa
--- /dev/null
+++ b/common/content/template.js
@@ -0,0 +1,305 @@
+const template = {
+ add: function add(a, b) a + b,
+ join: function join(c) function (a, b) a + c + b,
+
+ map: function map(iter, fn, sep, interruptable)
+ {
+ if (iter.length) /* Kludge? */
+ iter = util.Array.iterator(iter);
+ let ret = <></>;
+ let n = 0;
+ for each (let i in Iterator(iter))
+ {
+ let val = fn(i);
+ if (val == undefined)
+ continue;
+ if (sep && n++)
+ ret += sep;
+ if (interruptable && n % interruptable == 0)
+ liberator.threadYield(true, true);
+ ret += val;
+ }
+ return ret;
+ },
+
+ maybeXML: function maybeXML(xml)
+ {
+ if (typeof xml == "xml")
+ return xml;
+ try
+ {
+ return new XMLList(xml);
+ }
+ catch (e) {}
+ return <>{xml}</>;
+ },
+
+ completionRow: function completionRow(item, class)
+ {
+ if (typeof icon == "function")
+ icon = icon();
+
+ if (class)
+ {
+ var text = item[0] || "";
+ var desc = item[1] || "";
+ }
+ else
+ {
+ var text = this.process[0].call(this, item, item.text || this.getKey(item, "text"));
+ var desc = this.process[1].call(this, item, this.getKey(item, "description"));
+ }
+
+ return <div highlight={class || "CompItem"} style="white-space: nowrap">
+ <!-- The non-breaking spaces prevent empty elements
+ - from pushing the baseline down and enlarging
+ - the row.
+ -->
+ <li highlight="CompResult">{text}&#160;</li>
+ <li highlight="CompDesc">{desc}&#160;</li>
+ </div>;
+ },
+
+ bookmarkDescription: function (item, text)
+ {
+ let extra = this.getKey(item, "extra");
+ return <>
+ <a href="#" highlight="URL">{text}</a>&#160;
+ {
+ !(extra && extra.length) ? "" :
+ <span class="extra-info">
+ ({
+ template.map(extra, function (e)
+ <>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>,
+ <>&#xa0;</>/* Non-breaking space */)
+ })
+ </span>
+ }
+ </>
+ },
+
+ icon: function (item, text)
+ {
+ let icon = this.getKey(item, "icon");
+ return <><span highlight="CompIcon">{icon ? <img src={icon}/> : <></>}</span><span class="td-strut"/>{text}</>
+ },
+
+ filter: function (str) <span highlight="Filter">{str}</span>,
+
+ // if "processStrings" is true, any passed strings will be surrounded by " and
+ // any line breaks are displayed as \n
+ highlight: function highlight(arg, processStrings, clip)
+ {
+ // some objects like window.JSON or getBrowsers()._browsers need the try/catch
+ let str = clip ? util.clip(String(arg), clip) : String(arg);
+ try
+ {
+ switch (arg == null ? "undefined" : typeof arg)
+ {
+ case "number":
+ return <span highlight="Number">{str}</span>;
+ case "string":
+ if (processStrings)
+ str = str.quote();
+ return <span highlight="String">{str}</span>;
+ case "boolean":
+ return <span highlight="Boolean">{str}</span>;
+ case "function":
+ // Vim generally doesn't like /foo*/, because */ looks like a comment terminator.
+ // Using /foo*(:?)/ instead.
+ if (processStrings)
+ return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>;
+ return <>{arg}</>;
+ case "undefined":
+ return <span highlight="Null">{arg}</span>;
+ case "object":
+ // for java packages value.toString() would crash so badly
+ // that we cannot even try/catch it
+ if (/^\[JavaPackage.*\]$/.test(arg))
+ return <>[JavaPackage]</>;
+ if (processStrings && false)
+ str = template.highlightFilter(str, "\n", function () <span highlight="NonText">^J</span>);
+ return <span highlight="Object">{str}</span>;
+ case "xml":
+ return arg;
+ default:
+ return <![CDATA[<unknown type>]]>;
+ }
+ }
+ catch (e)
+ {
+ return<![CDATA[<unknown>]]>;
+ }
+ },
+
+ 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);
+ },
+
+ highlightRegexp: function highlightRegexp(str, re, highlight)
+ {
+ return this.highlightSubstrings(str, (function ()
+ {
+ let res;
+ while ((res = re.exec(str)) && res[0].length)
+ yield [res.index, res[0].length];
+ })(), highlight || template.filter);
+ },
+
+ highlightSubstrings: function highlightSubstrings(str, iter, highlight)
+ {
+ if (typeof str == "xml")
+ return str;
+ if (str == "")
+ return <>{str}</>;
+
+ str = String(str).replace(" ", "\u00a0");
+ let s = <></>;
+ let start = 0;
+ let n = 0;
+ for (let [i, length] in iter)
+ {
+ if (n++ > 50) // Prevent infinite loops.
+ return s + <>{str.substr(start)}</>;
+ XML.ignoreWhitespace = false;
+ s += <>{str.substring(start, i)}</>;
+ s += highlight(str.substr(i, length));
+ start = i + length;
+ }
+ return s + <>{str.substr(start)}</>;
+ },
+
+ highlightURL: function highlightURL(str, force)
+ {
+ if (force || /^[a-zA-Z]+:\/\//.test(str))
+ return <a highlight="URL" href="#">{str}</a>;
+ else
+ return str;
+ },
+
+ generic: function generic(xml)
+ {
+ return <>:{commandline.getCommand()}<br/></> + xml;
+ },
+
+ // every item must have a .xml property which defines how to draw itself
+ // @param headers is an array of strings, the text for the header columns
+ genericTable: function genericTable(items, format)
+ {
+ this.listCompleter(function (context) {
+ context.filterFunc = null;
+ if (format)
+ context.format = format;
+ context.completions = items;
+ });
+ },
+
+ jumps: function jumps(index, elems)
+ {
+ return this.generic(
+ <table>
+ <tr style="text-align: left;" highlight="Title">
+ <th colspan="2">jump</th><th>title</th><th>URI</th>
+ </tr>
+ {
+ this.map(Iterator(elems), function ([idx, val])
+ <tr>
+ <td class="indicator">{idx == index ? ">" : ""}</td>
+ <td>{Math.abs(idx - index)}</td>
+ <td style="width: 250px; max-width: 500px; overflow: hidden;">{val.title}</td>
+ <td><a href="#" highlight="URL jump-list">{val.URI.spec}</a></td>
+ </tr>)
+ }
+ </table>);
+ },
+
+ options: function options(title, opts)
+ {
+ return this.generic(
+ <table>
+ <tr highlight="Title" align="left">
+ <th>--- {title} ---</th>
+ </tr>
+ {
+ this.map(opts, function (opt)
+ <tr>
+ <td>
+ <span style={opt.isDefault ? "" : "font-weight: bold"}>{opt.pre}{opt.name}</span><span>{opt.value}</span>
+ {opt.isDefault || opt.default == null ? "" : <span class="extra-info"> (default: {opt.default})</span>}
+ </td>
+ </tr>)
+ }
+ </table>);
+ },
+
+ table: function table(title, data, indent)
+ {
+ let table =
+ <table>
+ <tr highlight="Title" align="left">
+ <th colspan="2">{title}</th>
+ </tr>
+ {
+ this.map(data, function (datum)
+ <tr>
+ <td style={"font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex")}>{datum[0]}</td>
+ <td>{template.maybeXML(datum[1])}</td>
+ </tr>)
+ }
+ </table>;
+ if (table.tr.length() > 1)
+ return table;
+ },
+
+ tabular: function tabular(headings, style, iter)
+ {
+ /* This might be mind-bogglingly slow. We'll see. */
+ return this.generic(
+ <table>
+ <tr highlight="Title" align="left">
+ {
+ this.map(headings, function (h)
+ <th>{h}</th>)
+ }
+ </tr>
+ {
+ this.map(iter, function (row)
+ <tr>
+ {
+ template.map(Iterator(row), function ([i, d])
+ <td style={style[i] || ""}>{d}</td>)
+ }
+ </tr>)
+ }
+ </table>);
+ },
+
+ usage: function usage(iter)
+ {
+ return this.generic(
+ <table>
+ {
+ this.map(iter, function (item)
+ <tr>
+ <td highlight="Title" style="padding-right: 20px">{item.name || item.names[0]}</td>
+ <td>{item.description}</td>
+ </tr>)
+ }
+ </table>);
+ }
+};
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/ui.js b/common/content/ui.js
new file mode 100644
index 00000000..5a3dda32
--- /dev/null
+++ b/common/content/ui.js
@@ -0,0 +1,1860 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+/*
+ * This class is used for prompting of user input and echoing of messages
+ *
+ * it consists of a prompt and command field
+ * be sure to only create objects of this class when the chrome is ready
+ */
+function CommandLine() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const UNINITIALIZED = {}; // notifies us, if we need to start history/tab-completion from the beginning
+
+ storage.newArray("history-search", true);
+ storage.newArray("history-command", true);
+
+ var inputHistory = {
+ get mode() (modes.extended == modes.EX) ? "command" : "search",
+
+ get store() storage["history-" + this.mode],
+
+ get length() this.store.length,
+
+ get: function get(index) this.store.get(index),
+
+ add: function add(str)
+ {
+ if (!str)
+ return;
+
+ this.store.mutate('filter', function (line) line != str);
+ this.store.push(str);
+ this.store.truncate(options["history"], true);
+ }
+ };
+
+ var historyIndex = UNINITIALIZED;
+ var historyStart = "";
+
+ var messageHistory = {
+ _messages: [],
+ get messages()
+ {
+ let max = options["messages"];
+
+ // resize if 'messages' has changed
+ if (this._messages.length > max)
+ this._messages = this._messages.splice(this._messages.length - max);
+
+ return this._messages;
+ },
+
+ get length() this._messages.length,
+
+ add: function add(message)
+ {
+ if (!message)
+ return;
+
+ if (this._messages.length >= options["messages"])
+ this._messages.shift();
+
+ this._messages.push(message);
+ }
+ };
+ var lastMowOutput = null;
+
+ var silent = false;
+
+ function Completions(context)
+ {
+ let self = this;
+ context.onUpdate = function ()
+ {
+ self.reset();
+ };
+ this.context = context;
+ this.editor = context.editor;
+ this.selected = null;
+ this.wildmode = options.get("wildmode");
+ this.itemList = completionList;
+ this.itemList.setItems(context);
+ this.reset();
+ }
+ Completions.prototype = {
+ UP: {},
+ DOWN: {},
+ PAGE_UP: {},
+ PAGE_DOWN: {},
+ RESET: null,
+
+ get completion()
+ {
+ let str = commandline.getCommand();
+ return str.substring(this.prefix.length, str.length - this.suffix.length);
+ },
+ set completion set_completion(completion)
+ {
+ this.previewClear();
+
+ // Change the completion text.
+ // The second line is a hack to deal with some substring
+ // preview corner cases.
+ commandWidget.value = this.prefix + completion + this.suffix;
+ this.editor.selection.focusNode.textContent = commandWidget.value;
+
+ // Reset the caret to one position after the completion.
+ this.caret = this.prefix.length + completion.length;
+ },
+
+ get caret() this.editor.selection.focusOffset,
+ set caret(offset)
+ {
+ commandWidget.selectionStart = offset;
+ commandWidget.selectionEnd = offset;
+ },
+
+ get start() this.context.allItems.start,
+
+ get items() this.context.allItems.items,
+
+ get substring() this.context.longestAllSubstring,
+
+ get wildtype() this.wildtypes[this.wildIndex] || "",
+
+ get type() ({
+ list: this.wildmode.checkHas(this.wildtype, "list"),
+ longest: this.wildmode.checkHas(this.wildtype, "longest"),
+ first: this.wildmode.checkHas(this.wildtype, ""),
+ full: this.wildmode.checkHas(this.wildtype, "full")
+ }),
+
+ complete: function (show, tabPressed)
+ {
+ this.context.reset();
+ this.context.tabPressed = tabPressed;
+ liberator.triggerCallback("complete", currentExtendedMode, this.context);
+ this.reset(show, tabPressed);
+ },
+
+ preview: function preview()
+ {
+ if (this.wildtype < 0 || this.suffix || !this.items.length)
+ return;
+ this.previewClear();
+
+ let substring = "";
+ switch (this.wildtype.replace(/.*:/, ""))
+ {
+ case "":
+ substring = this.items[0].text;
+ break;
+ case "longest":
+ if (this.items.length > 1)
+ {
+ substring = this.substring;
+ break;
+ }
+ // Fallthrough
+ case "full":
+ let item = this.items[this.selected != null ? this.selected + 1 : 0];
+ if (item)
+ substring = item.text;
+ break;
+ }
+
+ // Don't show 1-character substrings unless we've just hit backspace
+ if (substring.length < 2 && (!this.lastSubstring || this.lastSubstring.indexOf(substring) != 0))
+ return;
+ this.lastSubstring = substring;
+
+ let value = this.completion;
+ if (util.compareIgnoreCase(value, substring.substr(0, value.length)))
+ return;
+ substring = substring.substr(value.length);
+ this.removeSubstring = substring;
+
+ // highlight="Preview" won't work in the editor.
+ let node = util.xmlToDom(<span style={highlight.get("Preview").value}>{substring}</span>,
+ document);
+ let start = this.caret;
+ this.editor.insertNode(node, this.editor.rootElement, 1);
+ this.caret = start;
+ },
+
+ previewClear: function previewClear()
+ {
+ let node = this.editor.rootElement.firstChild;
+ if (node && node.nextSibling)
+ this.editor.deleteNode(node.nextSibling);
+ else if (this.removeSubstring)
+ {
+ let str = this.removeSubstring;
+ let cmd = commandWidget.value;
+ if (cmd.substr(cmd.length - str.length) == str)
+ commandWidget.value = cmd.substr(0, cmd.length - str.length);
+ }
+ delete this.removeSubstring;
+ },
+
+ reset: function reset(show)
+ {
+ this.wildtypes = this.wildmode.values;
+ this.wildIndex = -1;
+
+ this.prefix = this.context.value.substr(0, this.start);
+ this.value = this.context.value.substr(this.start, this.context.caret);
+ this.suffix = this.context.value.substr(this.context.caret);
+
+ if (show)
+ {
+ this.itemList.reset();
+ this.select(this.RESET);
+ this.wildIndex = 0;
+ }
+
+ this.preview();
+ },
+
+ select: function select(idx)
+ {
+ switch (idx)
+ {
+ case this.UP:
+ if (this.selected == null)
+ idx = this.items.length - 1;
+ else
+ idx = this.selected - 1;
+ break;
+ case this.DOWN:
+ if (this.selected == null)
+ idx = 0;
+ else
+ idx = this.selected + 1;
+ break;
+ case this.RESET:
+ idx = null;
+ break;
+ default: idx = Math.max(0, Math.max(this.items.length - 1, idx));
+ }
+ this.itemList.selectItem(idx);
+ if (idx < 0 || idx >= this.items.length || idx == null)
+ {
+ // Wrapped. Start again.
+ this.selected = null;
+ this.completion = this.value;
+ }
+ else
+ {
+ this.selected = idx;
+ this.completion = this.items[idx].text;
+ }
+ },
+
+ tab: function tab(reverse)
+ {
+ // Check if we need to run the completer.
+ if (this.context.waitingForTab || this.wildIndex == -1)
+ this.complete(true, true);
+
+ if (this.items.length == 0)
+ {
+ // No items. Wait for any unfinished completers.
+ let end = Date.now() + 5000;
+ while (this.context.incomplete && this.items.length == 0 && Date.now() < end)
+ liberator.threadYield();
+
+ if (this.items.length == 0)
+ return liberator.beep();
+ }
+
+ switch (this.wildtype.replace(/.*:/, ""))
+ {
+ case "":
+ this.select(0);
+ break;
+ case "longest":
+ if (this.items.length > 1)
+ {
+ if (this.substring && this.substring != this.completion)
+ {
+ this.completion = this.substring;
+ liberator.triggerCallback("change", currentExtendedMode, commandline.getCommand());
+ }
+ break;
+ }
+ // Fallthrough
+ case "full":
+ this.select(reverse ? this.UP : this.DOWN)
+ break;
+ }
+
+ if (this.type.list)
+ completionList.show();
+
+ this.wildIndex = Math.max(0, Math.min(this.wildtypes.length - 1, this.wildIndex + 1));
+ this.preview();
+
+ statusTimer.tell();
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// TIMERS //////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var statusTimer = new util.Timer(5, 100, function statusTell() {
+ if (completions.selected == null)
+ statusline.updateProgress("");
+ else
+ statusline.updateProgress("match " + (completions.selected + 1) + " of " + completions.items.length);
+ });
+
+ var autocompleteTimer = new util.Timer(201, 300, function autocompleteTell(tabPressed) {
+ if (events.feedingKeys || !completions)
+ return;
+ completions.complete(true, false);
+ completions.itemList.show();
+ });
+
+ var tabTimer = new util.Timer(10, 10, function tabTell(event) {
+ if (completions)
+ completions.tab(event.shiftKey);
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// CALLBACKS ///////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ // callback for prompt mode
+ var promptSubmitCallback = null;
+ var promptChangeCallback = null;
+ var promptCompleter = null;
+
+ liberator.registerCallback("submit", modes.EX, function (command) { liberator.execute(command); });
+ liberator.registerCallback("complete", modes.EX, function (context) {
+ context.fork("ex", 0, completion, "ex");
+ });
+ liberator.registerCallback("change", modes.EX, function (command) {
+ if (options.get("wildoptions").has("auto"))
+ autocompleteTimer.tell(false);
+ else
+ completions.reset();
+ });
+
+ liberator.registerCallback("cancel", modes.PROMPT, closePrompt);
+ liberator.registerCallback("submit", modes.PROMPT, closePrompt);
+ liberator.registerCallback("change", modes.PROMPT, function (str) {
+ if (promptChangeCallback)
+ return promptChangeCallback(str);
+ });
+ liberator.registerCallback("complete", modes.PROMPT, function (context) {
+ if (promptCompleter)
+ promptCompleter(context);
+ });
+
+ function closePrompt(value)
+ {
+ let callback = promptSubmitCallback;
+ promptSubmitCallback = null;
+ currentExtendedMode = null;
+ commandline.clear();
+ if (callback)
+ callback(value);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// VARIABLES ///////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const completionList = new ItemList("liberator-completions");
+ var completions = null;
+
+ var wildIndex = 0; // keep track how often we press <Tab> in a row
+ var startHints = false; // whether we're waiting to start hints mode
+ var lastSubstring = "";
+
+ // the containing box for the promptWidget and commandWidget
+ const commandlineWidget = document.getElementById("liberator-commandline");
+ // the prompt for the current command, for example : or /. Can be blank
+ const promptWidget = document.getElementById("liberator-commandline-prompt");
+ // the command bar which contains the current command
+ const commandWidget = document.getElementById("liberator-commandline-command");
+
+ commandWidget.inputField.QueryInterface(Components.interfaces.nsIDOMNSEditableElement);
+
+ // the widget used for multiline output
+ const multilineOutputWidget = document.getElementById("liberator-multiline-output");
+ const outputContainer = multilineOutputWidget.parentNode;
+
+ multilineOutputWidget.contentDocument.body.id = "liberator-multiline-output-content";
+
+ // the widget used for multiline intput
+ const multilineInputWidget = document.getElementById("liberator-multiline-input");
+
+ // we need to save the mode which were in before opening the command line
+ // this is then used if we focus the command line again without the "official"
+ // way of calling "open"
+ var currentExtendedMode = null; // the extended mode which we last openend the command line for
+ var currentPrompt = null;
+ var currentCommand = null;
+
+ // save the arguments for the inputMultiline method which are needed in the event handler
+ var multilineRegexp = null;
+ var multilineCallback = null;
+
+ function setHighlightGroup(group)
+ {
+ commandlineWidget.setAttributeNS(NS.uri, "highlight", group);
+ }
+
+ // sets the prompt - for example, : or /
+ function setPrompt(pmt, highlightGroup)
+ {
+ promptWidget.value = pmt;
+
+ if (pmt)
+ {
+ promptWidget.size = pmt.length;
+ promptWidget.collapsed = false;
+ }
+ else
+ {
+ promptWidget.collapsed = true;
+ }
+ promptWidget.setAttributeNS(NS.uri, "highlight", highlightGroup || commandline.HL_NORMAL);
+ }
+
+ // sets the command - e.g. 'tabopen', 'open http://example.com/'
+ function setCommand(cmd)
+ {
+ commandWidget.value = cmd;
+ }
+
+ function setLine(str, highlightGroup, forceSingle)
+ {
+ setHighlightGroup(highlightGroup);
+ setPrompt("");
+ setCommand(str);
+ if (!forceSingle &&
+ commandWidget.inputField.editor.rootElement
+ .scrollWidth > commandWidget.inputField.scrollWidth)
+ {
+ setCommand("");
+ setMultiline(<span highlight="Message">{str}</span>, highlightGroup);
+ }
+ }
+
+ // TODO: extract CSS
+ // : resize upon a window resize
+ // : echoed lines longer than v-c-c.width should wrap and use MOW
+ function setMultiline(str, highlightGroup)
+ {
+ //outputContainer.collapsed = true;
+ let doc = multilineOutputWidget.contentDocument;
+ let win = multilineOutputWidget.contentWindow;
+
+ /* If it's already XML, assume it knows what it's doing.
+ * Otherwise, white space is significant.
+ * The problem elsewhere is that E4X tends to insert new lines
+ * after interpolated data.
+ */
+ XML.ignoreWhitespace = typeof str != "xml";
+ let output = util.xmlToDom(<div class={"ex-command-output "} style="white-space: nowrap" highlight={highlightGroup}>{template.maybeXML(str)}</div>, doc);
+ XML.ignoreWhitespace = true;
+
+ lastMowOutput = output;
+
+ // FIXME: need to make sure an open MOW is closed when commands
+ // that don't generate output are executed
+ if (outputContainer.collapsed)
+ doc.body.innerHTML = "";
+
+ doc.body.appendChild(output);
+
+ commandline.updateOutputHeight(true);
+
+ if (options["more"] && win.scrollMaxY > 0)
+ {
+ // start the last executed command's output at the top of the screen
+ let elements = doc.getElementsByClassName("ex-command-output");
+ elements[elements.length - 1].scrollIntoView(true);
+ }
+ else
+ {
+ win.scrollTo(0, doc.height);
+ }
+ commandline.updateMorePrompt();
+
+ win.focus();
+
+ startHints = false;
+ modes.set(modes.COMMAND_LINE, modes.OUTPUT_MULTILINE);
+ }
+
+ function autosizeMultilineInputWidget()
+ {
+ let lines = multilineInputWidget.value.split("\n").length - 1;
+
+ if (lines == 0)
+ lines = 1;
+
+ multilineInputWidget.setAttribute("rows", String(lines));
+ }
+
+ // used for the :echo[err] commands
+ function echoArgumentToString(arg, useColor)
+ {
+ if (!arg)
+ return "";
+
+ try
+ {
+ arg = liberator.eval(arg);
+ }
+ catch (e)
+ {
+ liberator.echoerr(e);
+ return null;
+ }
+
+ if (typeof arg === "object")
+ arg = util.objectToString(arg, useColor);
+ else if (typeof arg != "xml")
+ arg = String(arg);
+
+ return arg;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["history", "hi"],
+ "Number of Ex commands and search patterns to store in the command-line history",
+ "number", 500,
+ { validator: function (value) value >= 0 });
+
+ options.add(["messages", "msgs"],
+ "Number of messages to store in the message history",
+ "number", 100,
+ { validator: function (value) value >= 0 });
+
+ options.add(["more"],
+ "Pause the message list window when more than one screen of listings is displayed",
+ "boolean", true);
+
+ options.add(["showmode", "smd"],
+ "Show the current mode in the command line",
+ "boolean", true);
+
+ options.add(["suggestengines"],
+ "Engine Alias which has a feature of suggest",
+ "stringlist", "google",
+ {
+ completer: function completer(value)
+ {
+ let ss = Components.classes["@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsIBrowserSearchService);
+ let engines = ss.getEngines({})
+ .filter(function (engine) engine.supportsResponseType("application/x-suggestions+json"));
+
+ return engines.map(function (engine) [engine.alias, engine.description]);
+ },
+ validator: Option.validateCompleter
+ });
+
+ // TODO: these belong in ui.js
+ options.add(["complete", "cpt"],
+ "Items which are completed at the :[tab]open prompt",
+ "charlist", "sfl",
+ {
+ completer: function completer(filter) [k for each (k in completion.urlCompleters)],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wildcase", "wic"],
+ "Completion case matching mode",
+ "string", "smart",
+ {
+ completer: function () [
+ ["smart", "Case is significant when capital letters are typed"],
+ ["match", "Case is always significant"],
+ ["ignore", "Case is never significant"]
+ ],
+ validator: Option.validateCompleter
+ });
+
+ options.add(["wildignore", "wig"],
+ "List of file patterns to ignore when completing files",
+ "stringlist", "",
+ {
+ validator: function validator(values)
+ {
+ // TODO: allow for escaping the ","
+ try
+ {
+ RegExp("^(" + values.join("|") + ")$");
+ return true;
+ }
+ catch (e)
+ {
+ return false;
+ }
+ }
+ });
+
+ options.add(["wildmode", "wim"],
+ "Define how command line completion works",
+ "stringlist", "list:full",
+ {
+ completer: function completer(filter)
+ {
+ return [
+ // Why do we need ""?
+ ["", "Complete only the first match"],
+ ["full", "Complete the next full match"],
+ ["longest", "Complete to longest common string"],
+ ["list", "If more than one match, list all matches"],
+ ["list:full", "List all and complete first match"],
+ ["list:longest", "List all and complete common string"]
+ ];
+ },
+ validator: Option.validateCompleter,
+ checkHas: function (value, val)
+ {
+ let [first, second] = value.split(":", 2);
+ return first == val || second == val;
+ }
+ });
+
+ options.add(["wildoptions", "wop"],
+ "Change how command line completion is done",
+ "stringlist", "",
+ {
+ completer: function completer(value)
+ {
+ return [
+ ["auto", "Automatically show completions while you are typing"],
+ ["sort", "Always sort the completion list"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// MAPPINGS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var myModes = [modes.COMMAND_LINE];
+
+ // TODO: move "<Esc>", "<C-[" here from mappings
+ mappings.add(myModes,
+ ["<C-c>"], "Focus content",
+ function () { events.onEscape(); });
+
+ mappings.add(myModes,
+ ["<Space>"], "Expand command line abbreviation",
+ function ()
+ {
+ commandline.resetCompletions();
+ return editor.expandAbbreviation("c");
+ },
+ { flags: Mappings.flags.ALLOW_EVENT_ROUTING });
+
+ mappings.add(myModes,
+ ["<C-]>", "<C-5>"], "Expand command line abbreviation",
+ function () { editor.expandAbbreviation("c"); });
+
+ // FIXME: Should be "g<" but that doesn't work unless it has a non-null
+ // rhs, getCandidates broken?
+ mappings.add([modes.NORMAL],
+ ["g."], "Redisplay the last command output",
+ function ()
+ {
+ if (lastMowOutput)
+ commandline.echo(lastMowOutput,
+ commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
+ else
+ liberator.beep();
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// COMMANDS ////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var echoCommands = [
+ {
+ name: "ec[ho]",
+ description: "Echo the expression",
+ action: liberator.echo
+ },
+ {
+ name: "echoe[rr]",
+ description: "Echo the expression as an error message",
+ action: liberator.echoerr
+ },
+ {
+ name: "echom[sg]",
+ description: "Echo the expression as an informational message",
+ action: liberator.echomsg
+ }
+ ];
+
+ echoCommands.forEach(function (command) {
+ commands.add([command.name],
+ command.description,
+ function (args)
+ {
+ var str = echoArgumentToString(args.string, true);
+ if (str != null)
+ command.action(str);
+ },
+ {
+ completer: function (context) completion.javascript(context),
+ literal: 0
+ });
+ });
+
+ commands.add(["mes[sages]"],
+ "Display previously given messages",
+ function ()
+ {
+ // TODO: are all messages single line? Some display an aggregation
+ // of single line messages at least. E.g. :source
+ // FIXME: I retract my retraction, this command-line/MOW mismatch _is_ really annoying -- djk
+ if (messageHistory.length == 1)
+ {
+ let message = messageHistory.messages[0];
+ commandline.echo(message.str, message.highlight, commandline.FORCE_SINGLELINE);
+ }
+ else if (messageHistory.length > 1)
+ {
+ let list = <></>;
+
+ for (let [,message] in Iterator(messageHistory.messages))
+ list += <div highlight={message.highlight + " Message"}>{message.str}</div>;
+
+ liberator.echo(list, commandline.FORCE_MULTILINE);
+ }
+ },
+ { argCount: "0" });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ HL_NORMAL : "Normal",
+ HL_ERRORMSG : "ErrorMsg",
+ HL_MODEMSG : "ModeMsg",
+ HL_MOREMSG : "MoreMsg",
+ HL_QUESTION : "Question",
+ HL_INFOMSG : "InfoMsg",
+ HL_WARNINGMSG : "WarningMsg",
+ HL_LINENR : "LineNr",
+
+ FORCE_MULTILINE : 1 << 0,
+ FORCE_SINGLELINE : 1 << 1,
+ DISALLOW_MULTILINE : 1 << 2, // if an echo() should try to use the single line
+ // but output nothing when the MOW is open; when also
+ // FORCE_MULTILINE is given, FORCE_MULTILINE takes precedence
+ APPEND_TO_MESSAGES : 1 << 3, // add the string to the message history
+
+ get completionContext() completions.context,
+
+ get mode() (modes.extended == modes.EX) ? "cmd" : "search",
+
+ get silent() silent,
+ set silent(val) {
+ silent = val;
+ if (silent)
+ storage.styles.addSheet("silent-mode", "chrome://*", "#liberator-commandline > * { opacity: 0 }", true, true);
+ else
+ storage.styles.removeSheet("silent-mode", null, null, null, true);
+ },
+
+ getCommand: function getCommand()
+ {
+ try
+ {
+ return commandWidget.inputField.editor.rootElement.firstChild.textContent;
+ }
+ catch (e) {}
+ return commandWidget.value;
+ },
+
+ open: function open(prompt, cmd, extendedMode)
+ {
+ // save the current prompts, we need it later if the command widget
+ // receives focus without calling the this.open() method
+ currentPrompt = prompt || "";
+ currentCommand = cmd || "";
+ currentExtendedMode = extendedMode || null;
+
+ historyIndex = UNINITIALIZED;
+
+ modes.set(modes.COMMAND_LINE, currentExtendedMode);
+ setHighlightGroup(this.HL_NORMAL);
+ setPrompt(currentPrompt);
+ setCommand(currentCommand);
+
+ commandWidget.focus();
+
+ completions = new Completions(CompletionContext(commandWidget.inputField.editor));
+
+ // open the completion list automatically if wanted
+ if (/\s/.test(cmd) &&
+ options.get("wildoptions").has("auto") &&
+ extendedMode == modes.EX)
+ autocompleteTimer.tell(false);
+ },
+
+ // normally used when pressing esc, does not execute a command
+ close: function close()
+ {
+ let res = liberator.triggerCallback("cancel", currentExtendedMode);
+ inputHistory.add(this.getCommand());
+ statusline.updateProgress(""); // we may have a "match x of y" visible
+ this.clear();
+ },
+
+ clear: function clear()
+ {
+ multilineInputWidget.collapsed = true;
+ outputContainer.collapsed = true;
+ completionList.hide();
+ this.resetCompletions();
+
+ setLine("", this.HL_NORMAL);
+ },
+
+ // liberator.echo uses different order of flags as it omits the hightlight group, change v.commandline.echo argument order? --mst
+ echo: function echo(str, highlightGroup, flags)
+ {
+ let focused = document.commandDispatcher.focusedElement;
+ if (focused && focused == commandWidget.inputField || focused == multilineInputWidget.inputField)
+ return false;
+ if (silent)
+ return false;
+ if (modes.main == modes.COMMAND_LINE)
+ return false;
+
+ highlightGroup = highlightGroup || this.HL_NORMAL;
+
+ if (flags & this.APPEND_TO_MESSAGES)
+ messageHistory.add({ str: str, highlight: highlightGroup });
+
+ liberator.callInMainThread(function () {
+ let where = setLine;
+ if (flags & commandline.FORCE_MULTILINE)
+ where = setMultiline;
+ else if (flags & commandline.FORCE_SINGLELINE)
+ where = function () setLine(str, highlightGroup, true);
+ else if (flags & commandline.DISALLOW_MULTILINE)
+ {
+ if (!outputContainer.collapsed)
+ where = null;
+ else
+ where = function () setLine(str, highlightGroup, true);
+ }
+ else if (/\n|<br\/?>/.test(str))
+ where = setMultiline;
+
+ if (where)
+ where(str, highlightGroup);
+
+ currentExtendedMode = null;
+ });
+
+ return true;
+ },
+
+ // this will prompt the user for a string
+ // commandline.input("(s)ave or (o)pen the file?")
+ input: function input(prompt, callback, extra)
+ {
+ extra = extra || {};
+
+ promptSubmitCallback = callback;
+ promptChangeCallback = extra.onChange;
+ promptCompleter = extra.completer;
+ modes.push(modes.COMMAND_LINE, modes.PROMPT);
+ currentExtendedMode = modes.PROMPT;
+ setPrompt(prompt + " ", this.HL_QUESTION);
+ setCommand(extra.default || "");
+ commandWidget.focus();
+ },
+
+ // reads a multi line input and returns the string once the last line matches
+ // @param untilRegexp
+ inputMultiline: function inputMultiline(untilRegexp, callbackFunc)
+ {
+ // save the mode, because we need to restore it
+ modes.push(modes.COMMAND_LINE, modes.INPUT_MULTILINE);
+
+ // save the arguments, they are needed in the event handler onEvent
+ multilineRegexp = untilRegexp;
+ multilineCallback = callbackFunc;
+
+ multilineInputWidget.collapsed = false;
+ multilineInputWidget.value = "";
+ autosizeMultilineInputWidget();
+
+ setTimeout(function () { multilineInputWidget.focus(); }, 10);
+ },
+
+ onEvent: function onEvent(event)
+ {
+ completions.previewClear();
+ let command = this.getCommand();
+
+ if (event.type == "blur")
+ {
+ // prevent losing focus, there should be a better way, but it just didn't work otherwise
+ setTimeout(function () {
+ if (liberator.mode == modes.COMMAND_LINE &&
+ !(modes.extended & modes.INPUT_MULTILINE) &&
+ !(modes.extended & modes.OUTPUT_MULTILINE) &&
+ event.originalTarget == commandWidget.inputField)
+ {
+ commandWidget.inputField.focus();
+ }
+ }, 0);
+ }
+ else if (event.type == "focus")
+ {
+ if (!currentExtendedMode && event.target == commandWidget.inputField)
+ {
+ event.target.blur();
+ liberator.beep();
+ }
+ }
+ else if (event.type == "input")
+ {
+ liberator.triggerCallback("change", currentExtendedMode, command);
+ }
+ else if (event.type == "keypress")
+ {
+ if (!currentExtendedMode)
+ return true;
+
+ let key = events.toString(event);
+ //liberator.log("command line handling key: " + key + "\n");
+
+ // user pressed ENTER to carry out a command
+ // user pressing ESCAPE is handled in the global onEscape
+ // FIXME: <Esc> should trigger "cancel" event
+ if (events.isAcceptKey(key))
+ {
+ let mode = currentExtendedMode; // save it here, as setMode() resets it
+ currentExtendedMode = null; /* Don't let modes.pop trigger "cancel" */
+ inputHistory.add(command);
+ modes.pop(!commandline.silent);
+ this.resetCompletions();
+ completionList.hide();
+ liberator.focusContent(false);
+ statusline.updateProgress(""); // we may have a "match x of y" visible
+ return liberator.triggerCallback("submit", mode, command);
+ }
+ // user pressed UP or DOWN arrow to cycle history completion
+ else if (/^(<Up>|<Down>|<S-Up>|<S-Down>|<PageUp>|<PageDown>)$/.test(key))
+ {
+ function loadHistoryItem(index)
+ {
+ setCommand(inputHistory.get(historyIndex));
+ liberator.triggerCallback("change", currentExtendedMode, commandline.getCommand());
+ }
+
+ let previousItem = /Up/.test(key);
+ let matchCurrent = !/(Page|S-)/.test(key);
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // always reset the tab completion if we use up/down keys
+ completions.select(completions.RESET);
+
+ // save 'start' position for iterating through the history
+ if (historyIndex == UNINITIALIZED)
+ {
+ historyIndex = inputHistory.length;
+ historyStart = command;
+ }
+
+ // search the history for the first item matching the current
+ // commandline string
+ while (historyIndex >= -1 && historyIndex <= inputHistory.length)
+ {
+ previousItem ? historyIndex-- : historyIndex++;
+
+ // user pressed DOWN when there is no newer history item
+ if (historyIndex == inputHistory.length)
+ {
+ setCommand(historyStart);
+ liberator.triggerCallback("change", currentExtendedMode, this.getCommand());
+ break;
+ }
+
+ // cannot go past history start/end
+ if (historyIndex <= -1)
+ {
+ historyIndex = 0;
+ liberator.beep();
+ break;
+ }
+ else if (historyIndex >= inputHistory.length + 1)
+ {
+ historyIndex = inputHistory.length;
+ liberator.beep();
+ break;
+ }
+
+ if (matchCurrent)
+ {
+ if (inputHistory.get(historyIndex).indexOf(historyStart) == 0)
+ {
+ loadHistoryItem(historyIndex);
+ break;
+ }
+ }
+ else
+ {
+ loadHistoryItem(historyIndex);
+ break;
+ }
+ }
+ }
+ // user pressed TAB to get completions of a command
+ else if (key == "<Tab>" || key == "<S-Tab>")
+ {
+ tabTimer.tell(event);
+ // prevent tab from moving to the next field
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+ else if (key == "<BS>")
+ {
+ // reset the tab completion
+ completionIndex = historyIndex = UNINITIALIZED;
+ completions.reset();
+
+ // and blur the command line if there is no text left
+ if (command.length == 0)
+ {
+ liberator.triggerCallback("cancel", currentExtendedMode);
+ modes.pop(); // FIXME: use mode stack
+ }
+ }
+ else // any other key
+ {
+ this.resetCompletions();
+ }
+ return true; // allow this event to be handled by Firefox
+ }
+ },
+
+ onMultilineInputEvent: function onMultilineInputEvent(event)
+ {
+ if (event.type == "keypress")
+ {
+ let key = events.toString(event);
+ if (events.isAcceptKey(key))
+ {
+ let text = multilineInputWidget.value.substr(0, multilineInputWidget.selectionStart);
+ if (text.match(multilineRegexp))
+ {
+ text = text.replace(multilineRegexp, "");
+ modes.pop();
+ multilineInputWidget.collapsed = true;
+ multilineCallback.call(this, text);
+ }
+ }
+ else if (events.isCancelKey(key))
+ {
+ modes.pop();
+ multilineInputWidget.collapsed = true;
+ }
+ }
+ else if (event.type == "blur")
+ {
+ if (modes.extended & modes.INPUT_MULTILINE)
+ setTimeout(function () { multilineInputWidget.inputField.focus(); }, 0);
+ }
+ else if (event.type == "input")
+ {
+ autosizeMultilineInputWidget();
+ }
+ return true;
+ },
+
+ // FIXME: if 'more' is set and the MOW is not scrollable we should still
+ // allow a down motion after an up rather than closing
+ onMultilineOutputEvent: function onMultilineOutputEvent(event)
+ {
+ let win = multilineOutputWidget.contentWindow;
+
+ let showMoreHelpPrompt = false;
+ let showMorePrompt = false;
+ let closeWindow = false;
+ let passEvent = false;
+
+ function isScrollable() !win.scrollMaxY == 0;
+ function atEnd() win.scrollY / win.scrollMaxY >= 1;
+
+ let key = events.toString(event);
+
+ if (startHints)
+ {
+ statusline.updateInputBuffer("");
+ startHints = false;
+ hints.show(key, undefined, win);
+ return;
+ }
+
+ switch (key)
+ {
+ case "<Esc>":
+ closeWindow = true;
+ break; // handled globally in events.js:onEscape()
+
+ case ":":
+ commandline.open(":", "", modes.EX);
+ return;
+
+ // down a line
+ case "j":
+ case "<Down>":
+ if (options["more"] && isScrollable())
+ win.scrollByLines(1);
+ else
+ passEvent = true;
+ break;
+
+ case "<C-j>":
+ case "<C-m>":
+ case "<Return>":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollByLines(1);
+ else
+ closeWindow = true; // don't propagate the event for accept keys
+ break;
+
+ // up a line
+ case "k":
+ case "<Up>":
+ case "<BS>":
+ if (options["more"] && isScrollable())
+ win.scrollByLines(-1);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ // half page down
+ case "d":
+ if (options["more"] && isScrollable())
+ win.scrollBy(0, win.innerHeight / 2);
+ else
+ passEvent = true;
+ break;
+
+ // TODO: <LeftMouse> on the prompt line should scroll one page
+ case "<LeftMouse>":
+ if (event.originalTarget.getAttributeNS(NS.uri, "highlight") == "URL buffer-list")
+ {
+ tabs.select(parseInt(event.originalTarget.parentNode.parentNode.firstChild.textContent, 10) - 1);
+ closeWindow = true;
+ break;
+ }
+ else if (event.originalTarget.localName.toLowerCase() == "a")
+ {
+ liberator.open(event.originalTarget.textContent);
+ break;
+ }
+ case "<A-LeftMouse>": // for those not owning a 3-button mouse
+ case "<MiddleMouse>":
+ if (event.originalTarget.localName.toLowerCase() == "a")
+ {
+ let where = /\btabopen\b/.test(options["activate"]) ?
+ liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB;
+ liberator.open(event.originalTarget.textContent, where);
+ }
+ break;
+
+ // let firefox handle those to select table cells or show a context menu
+ case "<C-LeftMouse>":
+ case "<RightMouse>":
+ case "<C-S-LeftMouse>":
+ break;
+
+ // page down
+ case "f":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(1);
+ else
+ passEvent = true;
+ break;
+
+ case "<Space>":
+ case "<PageDown>":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollByPages(1);
+ else
+ passEvent = true;
+ break;
+
+ // half page up
+ case "u":
+ // if (more and scrollable)
+ if (options["more"] && isScrollable())
+ win.scrollBy(0, -(win.innerHeight / 2));
+ else
+ passEvent = true;
+ break;
+
+ // page up
+ case "b":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(-1);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ case "<PageUp>":
+ if (options["more"] && isScrollable())
+ win.scrollByPages(-1);
+ else
+ passEvent = true;
+ break;
+
+ // top of page
+ case "g":
+ if (options["more"] && isScrollable())
+ win.scrollTo(0, 0);
+ else if (options["more"] && !isScrollable())
+ showMorePrompt = true;
+ else
+ passEvent = true;
+ break;
+
+ // bottom of page
+ case "G":
+ if (options["more"] && isScrollable() && !atEnd())
+ win.scrollTo(0, win.scrollMaxY);
+ else
+ passEvent = true;
+ break;
+
+ // copy text to clipboard
+ case "<C-y>":
+ util.copyToClipboard(win.getSelection());
+ break;
+
+ // close the window
+ case "q":
+ closeWindow = true;
+ break;
+
+ case ";":
+ statusline.updateInputBuffer(";");
+ startHints = true;
+ break;
+
+ // unmapped key
+ default:
+ if (!options["more"] || !isScrollable() || atEnd() || events.isCancelKey(key))
+ passEvent = true;
+ else
+ showMoreHelpPrompt = true;
+ }
+
+ if (passEvent || closeWindow)
+ {
+ // FIXME: use mode stack
+ modes.pop();
+ this.clear();
+
+ if (passEvent)
+ events.onKeyPress(event);
+ }
+ else // set update the prompt string
+ {
+ commandline.updateMorePrompt(showMorePrompt, showMoreHelpPrompt);
+ }
+ },
+
+ updateMorePrompt: function updateMorePrompt(force, showHelp)
+ {
+ if (modes.main == modes.COMMAND_LINE)
+ return;
+ let win = multilineOutputWidget.contentWindow;
+ function isScrollable() !win.scrollMaxY == 0;
+ function atEnd() win.scrollY / win.scrollMaxY >= 1;
+
+ if (showHelp)
+ setLine("-- More -- SPACE/d/j: screen/page/line down, b/u/k: up, q: quit", this.HL_MOREMSG);
+ else if (force || (options["more"] && isScrollable() && !atEnd()))
+ setLine("-- More --", this.HL_MOREMSG);
+ else
+ setLine("Press ENTER or type command to continue", this.HL_QUESTION);
+ },
+
+ updateOutputHeight: function updateOutputHeight(open)
+ {
+ if (!open && outputContainer.collapsed)
+ return;
+
+ let doc = multilineOutputWidget.contentDocument;
+ outputContainer.collapsed = true;
+ let availableHeight = 250;
+ try
+ {
+ availableHeight = getBrowser().mPanelContainer ?
+ getBrowser().mPanelContainer.boxObject.height : getBrowser().boxObject.height;
+ }
+ catch (e) {}
+ doc.body.style.minWidth = commandlineWidget.scrollWidth + "px";
+ outputContainer.height = Math.min(doc.height, availableHeight) + "px";
+ doc.body.style.minWidth = undefined;
+ outputContainer.collapsed = false;
+ },
+
+ // TODO: does that function need to be public?
+ resetCompletions: function resetCompletions()
+ {
+ autocompleteTimer.reset();
+ if (completions)
+ {
+ completions.context.reset();
+ completions.reset();
+ }
+ historyIndex = UNINITIALIZED;
+ removeSuffix = "";
+ }
+ };
+ //}}}
+}; //}}}
+
+/**
+ * The list which is used for the completion box (and QuickFix window in future)
+ *
+ * @param id: the id of the the XUL <iframe> which we want to fill
+ * it MUST be inside a <vbox> (or any other html element,
+ * because otherwise setting the height does not work properly
+ */
+function ItemList(id) //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ const CONTEXT_LINES = 3;
+ var maxItems = 20;
+ var completionElements = [];
+
+ var iframe = document.getElementById(id);
+ if (!iframe)
+ {
+ liberator.log("No iframe with id: " + id + " found, strange things may happen!"); // "The truth is out there..." -- djk
+ return;
+ }
+
+ function dom(xml, map) util.xmlToDom(xml, doc, map);
+ function elemToString(elem) elem.nodeType == elem.TEXT_NODE ? elem.data :
+ "<" + [elem.localName].concat([a.name + "=" + a.value.quote() for (a in util.Array.iterator(elem.attributes))]).join(" ") + ">";
+ var doc = iframe.contentDocument;
+ var container = iframe.parentNode;
+
+ doc.body.id = id + "-content";
+ doc.body.appendChild(doc.createTextNode(""));
+ doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight.
+
+ let gradient =
+ <div highlight="Gradient">
+ <div style="height: 0px">
+ <div highlight="GradientRight Gradient"
+ style="border: 0 !important; margin: 0 !important; padding: 0 !important;"/>
+ </div>
+ <table width="100%" style="height: 100%">
+ <tr>
+ { template.map(util.range(0, 100), function (i)
+ <td highlight="GradientLeft" style={"opacity: " + (1 - i / 100)}/>) }
+ </tr>
+ </table>
+ </div>;
+
+ var items = null;
+ var startIndex = -1; // The index of the first displayed item
+ var endIndex = -1; // The index one *after* the last displayed item
+ var selIndex = -1; // The index of the currently selected element
+ var div = null;
+ var divNodes = {};
+ var minHeight = 0;
+
+ function autoSize()
+ {
+ if (container.collapsed)
+ div.style.minWidth = document.getElementById("liberator-commandline").scrollWidth + "px";
+ minHeight = Math.max(minHeight, divNodes.completions.getBoundingClientRect().bottom);
+ container.height = minHeight;
+ if (container.collapsed)
+ div.style.minWidth = undefined;
+ // FIXME: Belongs elsewhere.
+ commandline.updateOutputHeight(false);
+ }
+
+ function getCompletion(index) completionElements.snapshotItem(index - startIndex);
+
+ function init()
+ {
+ div = dom(
+ <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
+ <div highlight="Completions" key="noCompletions"><span highlight="Title">No Completions</span></div>
+ <div key="completions"/>
+ <div highlight="Completions">
+ {
+ template.map(util.range(0, maxItems * 2), function (i)
+ <span highlight="CompItem">
+ <li highlight="NonText">~</li>
+ </span>)
+ }
+ </div>
+ </div>, divNodes);
+ doc.body.replaceChild(div, doc.body.firstChild);
+
+ items.contextList.forEach(function init_eachContext(context) {
+ delete context.cache.nodes;
+ if (!context.items.length && !context.message && !context.incomplete)
+ return;
+ context.cache.nodes = [];
+ dom(<div key="root" highlight="CompGroup">
+ <div highlight="Completions">
+ { context.createRow(context.title || [], "CompTitle") }
+ </div>
+ { gradient }
+ <div key="message" highlight="CompMsg"/>
+ <div key="up" highlight="CompLess"/>
+ <div key="items" highlight="Completions"/>
+ <div key="waiting" highlight="CompMsg">Waiting...</div>
+ <div key="down" highlight="CompMore"/>
+ </div>, context.cache.nodes);
+ divNodes.completions.appendChild(context.cache.nodes.root);
+ });
+ }
+
+ /**
+ * uses the entries in "items" to fill the listbox
+ * does incremental filling to speed up things
+ *
+ * @param offset: start at this index and show maxItems
+ */
+ function fill(offset)
+ {
+ XML.ignoreWhiteSpace = false;
+ let diff = offset - startIndex;
+ if (items == null || offset == null || diff == 0 || offset < 0)
+ return false;
+
+ startIndex = offset;
+ endIndex = Math.min(startIndex + maxItems, items.allItems.items.length);
+
+ let haveCompletions = false;
+ let off = 0;
+ let end = startIndex + maxItems;
+ function getRows(context)
+ {
+ function fix(n) Math.max(0, Math.min(len, n));
+ end -= context.message + context.incomplete;
+ let len = context.items.length;
+ let start = off;
+ off += len;
+ let res = [fix(offset - start), fix(end - start)];
+ res[2] = (context.incomplete && res[1] >= offset && off - 1 < end);
+ return res;
+ }
+
+ items.contextList.forEach(function fill_eachContext(context) {
+ let nodes = context.cache.nodes;
+ if (!nodes)
+ return;
+ haveCompletions = true;
+
+ let root = nodes.root
+ let items = nodes.items;
+ let [start, end, waiting] = getRows(context);
+
+ if (context.message)
+ nodes.message.textContent = context.message;
+ nodes.message.style.display = context.message ? "block" : "none";
+ nodes.waiting.style.display = waiting ? "block" : "none";
+ nodes.up.style.opacity = "0";
+ nodes.down.style.display = "none";
+
+ for (let [i, row] in Iterator(context.getRows(start, end, doc)))
+ nodes[i] = row;
+ for (let [i, row] in util.Array.iterator2(nodes))
+ {
+ if (!row)
+ continue;
+ let display = (i >= start && i < end);
+ if (display && row.parentNode != items)
+ {
+ do
+ {
+ var next = nodes[++i];
+ if (next && next.parentNode != items)
+ next = null;
+ }
+ while (!next && i < end)
+ items.insertBefore(row, next);
+ }
+ else if (!display && row.parentNode == items)
+ items.removeChild(row);
+ }
+ if (context.items.length == 0)
+ return;
+ nodes.up.style.opacity = (start == 0) ? "0" : "1";
+ if (end != context.items.length)
+ nodes.down.style.display = "block"
+ else
+ nodes.up.style.display = "block"
+ });
+
+ divNodes.noCompletions.style.display = haveCompletions ? "none" : "block";
+
+ completionElements = buffer.evaluateXPath("//xhtml:div[@liberator:highlight='CompItem']", doc);
+
+ autoSize();
+ return true;
+ }
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ clear: function clear() { this.setItems(); doc.body.innerHTML = ""; },
+ hide: function hide() { container.collapsed = true; },
+ show: function show() { container.collapsed = false; },
+ visible: function visible() !container.collapsed,
+
+ reset: function ()
+ {
+ startIndex = endIndex = selIndex = -1;
+ div = null;
+ this.selectItem(-1);
+ },
+
+ // if @param selectedItem is given, show the list and select that item
+ setItems: function setItems(newItems, selectedItem)
+ {
+ if (container.collapsed)
+ minHeight = 0;
+ startIndex = endIndex = selIndex = -1;
+ items = newItems;
+ this.reset();
+ if (typeof selectedItem == "number")
+ {
+ this.selectItem(selectedItem);
+ this.show();
+ }
+ },
+
+ // select index, refill list if necessary
+ selectItem: function selectItem(index)
+ {
+ //if (container.collapsed) // fixme
+ // return;
+
+ //let now = Date.now();
+
+ if (div == null)
+ init();
+
+ let sel = selIndex;
+ let len = items.allItems.items.length;
+ let newOffset = startIndex;
+
+ if (index == -1 || index == null || index == len) // wrapped around
+ {
+ if (selIndex < 0)
+ newOffset = 0;
+ selIndex = -1;
+ index = -1;
+ }
+ else
+ {
+ if (index <= startIndex + CONTEXT_LINES)
+ newOffset = index - CONTEXT_LINES;
+ if (index >= endIndex - CONTEXT_LINES)
+ newOffset = index + CONTEXT_LINES - maxItems + 1;
+
+ newOffset = Math.min(newOffset, len - maxItems);
+ newOffset = Math.max(newOffset, 0);
+
+ selIndex = index;
+ }
+
+ if (sel > -1)
+ getCompletion(sel).removeAttribute("selected");
+ fill(newOffset);
+ if (index >= 0)
+ getCompletion(index).setAttribute("selected", "true");
+
+ //if (index == 0)
+ // this.start = now;
+ //if (index == Math.min(len - 1, 100))
+ // liberator.dump({ time: Date.now() - this.start });
+ },
+
+ onEvent: function onEvent(event) false
+ };
+ //}}}
+}; //}}}
+
+function StatusLine() //{{{
+{
+ ////////////////////////////////////////////////////////////////////////////////
+ ////////////////////// PRIVATE SECTION /////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ var statusBar = document.getElementById("status-bar");
+ statusBar.collapsed = true; // it is later restored unless the user sets laststatus=0
+
+ // our status bar fields
+ var statuslineWidget = document.getElementById("liberator-statusline");
+ var urlWidget = document.getElementById("liberator-statusline-field-url");
+ var inputBufferWidget = document.getElementById("liberator-statusline-field-inputbuffer");
+ var progressWidget = document.getElementById("liberator-statusline-field-progress");
+ var tabCountWidget = document.getElementById("liberator-statusline-field-tabcount");
+ var bufferPositionWidget = document.getElementById("liberator-statusline-field-bufferposition");
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// OPTIONS /////////////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ options.add(["laststatus", "ls"],
+ "Show the status line",
+ "number", 2,
+ {
+ setter: function setter(value)
+ {
+ if (value == 0)
+ document.getElementById("status-bar").collapsed = true;
+ else if (value == 1)
+ liberator.echo("show status line only with > 1 window not implemented yet");
+ else
+ document.getElementById("status-bar").collapsed = false;
+
+ return value;
+ },
+ completer: function completer(filter)
+ {
+ return [
+ ["0", "Never display status line"],
+ ["1", "Display status line only if there are multiple windows"],
+ ["2", "Always display status line"]
+ ];
+ },
+ validator: Option.validateCompleter
+ });
+
+ /////////////////////////////////////////////////////////////////////////////}}}
+ ////////////////////// PUBLIC SECTION //////////////////////////////////////////
+ /////////////////////////////////////////////////////////////////////////////{{{
+
+ return {
+
+ setClass: function setClass(type)
+ {
+ const highlightGroup = {
+ secure: "StatusLineSecure",
+ broken: "StatusLineBroken",
+ insecure: "StatusLine"
+ };
+
+ statusBar.setAttributeNS(NS.uri, "highlight", highlightGroup[type]);
+ },
+
+ // update all fields of the statusline
+ update: function update()
+ {
+ this.updateUrl();
+ this.updateInputBuffer();
+ this.updateProgress();
+ this.updateTabCount();
+ this.updateBufferPosition();
+ },
+
+ // if "url" is ommited, build a usable string for the URL
+ updateUrl: function updateUrl(url)
+ {
+ if (typeof url == "string")
+ {
+ urlWidget.value = url;
+ return;
+ }
+
+ url = buffer.URL;
+
+ // make it even more vim-like
+ if (url == "about:blank")
+ {
+ if (!buffer.title)
+ url = "[No Name]";
+ }
+ else
+ {
+ url = url.replace(RegExp("^chrome://liberator/locale/(\\S+\\.html)$"), "$1 [Help]");
+ }
+
+ // when session information is available, add [+] when we can go backwards
+ if (config.name == "Vimperator")
+ {
+ let sh = window.getWebNavigation().sessionHistory;
+ let modified = "";
+ if (sh.index > 0)
+ modified += "+";
+ if (sh.index < sh.count -1)
+ modified += "-";
+ if (bookmarks.isBookmarked(buffer.URL))
+ modified += "\u2764"; // a heart symbol: ❤
+ //modified += "\u2665"; // a heart symbol: ♥
+
+ if (modified)
+ url += " [" + modified + "]";
+ }
+
+ urlWidget.value = url;
+ },
+
+ updateInputBuffer: function updateInputBuffer(buffer)
+ {
+ if (!buffer || typeof buffer != "string")
+ buffer = "";
+
+ inputBufferWidget.value = buffer;
+ },
+
+ updateProgress: function updateProgress(progress)
+ {
+ if (!progress)
+ progress = "";
+
+ if (typeof progress == "string")
+ {
+ progressWidget.value = progress;
+ }
+ else if (typeof progress == "number")
+ {
+ let progressStr = "";
+ if (progress <= 0)
+ progressStr = "[ Loading... ]";
+ else if (progress < 1)
+ {
+ progress = Math.floor(progress * 20);
+ progressStr = "["
+ + "====================".substr(0, progress)
+ + ">"
+ + "\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0".substr(0, 19 - progress)
+ + "]";
+ }
+ progressWidget.value = progressStr;
+ }
+ },
+
+ // you can omit either of the 2 arguments
+ updateTabCount: function updateTabCount(currentIndex, totalTabs)
+ {
+ if (!liberator.has("tabs"))
+ {
+ tabCountWidget = "";
+ return;
+ }
+
+ // update the ordinal which is used for numbered tabs only when the user has
+ // tab numbers set, and the host application supports it
+ if (config.hostApplication == "Firefox" &&
+ (options.get("guioptions").has("n") || options.get("guioptions").has("N")))
+ {
+ for (let [i, tab] in Iterator(Array.slice(getBrowser().mTabs)))
+ tab.setAttribute("ordinal", i + 1);
+ }
+
+ if (!currentIndex || typeof currentIndex != "number")
+ currentIndex = tabs.index() + 1;
+ if (!totalTabs || typeof currentIndex != "number")
+ totalTabs = tabs.count;
+
+ tabCountWidget.value = "[" + currentIndex + "/" + totalTabs + "]";
+ },
+
+ // percent is given between 0 and 1
+ updateBufferPosition: function updateBufferPosition(percent)
+ {
+ if (!percent || typeof percent != "number")
+ {
+ let win = document.commandDispatcher.focusedWindow;
+ if (!win)
+ return;
+ percent = win.scrollMaxY == 0 ? -1 : win.scrollY / win.scrollMaxY;
+ }
+
+ let bufferPositionStr = "";
+ percent = Math.round(percent * 100);
+ if (percent < 0)
+ bufferPositionStr = "All";
+ else if (percent == 0)
+ bufferPositionStr = "Top";
+ else if (percent < 10)
+ bufferPositionStr = " " + percent + "%";
+ else if (percent >= 100)
+ bufferPositionStr = "Bot";
+ else
+ bufferPositionStr = percent + "%";
+
+ bufferPositionWidget.value = bufferPositionStr;
+ }
+
+ };
+ //}}}
+}; //}}}
+
+// vim: set fdm=marker sw=4 ts=4 et:
diff --git a/common/content/util.js b/common/content/util.js
new file mode 100644
index 00000000..cc961c54
--- /dev/null
+++ b/common/content/util.js
@@ -0,0 +1,597 @@
+/***** BEGIN LICENSE BLOCK ***** {{{
+Version: MPL 1.1/GPL 2.0/LGPL 2.1
+
+The contents of this file are subject to the Mozilla Public License Version
+1.1 (the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis,
+WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+for the specific language governing rights and limitations under the
+License.
+
+(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
+
+Alternatively, the contents of this file may be used under the terms of
+either the GNU General Public License Version 2 or later (the "GPL"), or
+the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+in which case the provisions of the GPL or the LGPL are applicable instead
+of those above. If you wish to allow use of your version of this file only
+under the terms of either the GPL or the LGPL, and not to allow others to
+use your version of this file under the terms of the MPL, indicate your
+decision by deleting the provisions above and replace them with the notice
+and other provisions required by the GPL or the LGPL. If you do not delete
+the provisions above, a recipient may use your version of this file under
+the terms of any one of the MPL, the GPL or the LGPL.
+}}} ***** END LICENSE BLOCK *****/
+
+const XHTML = "http://www.w3.org/1999/xhtml";
+const NS = Namespace("liberator", "http://vimperator.org/namespaces/liberator");
+default xml namespace = XHTML;
+
+const util = { //{{{
+
+ Array: {
+ // [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" }
+ // From Common Lisp, more or less
+ assocToObj: function assocToObj(assoc)
+ {
+ let obj = {};
+ assoc.forEach(function ([k, v]) { obj[k] = v });
+ return obj;
+ },
+
+ // flatten an array: [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"]
+ flatten: function flatten(ary) Array.concat.apply([], ary),
+
+ iterator: function iterator(ary)
+ {
+ let length = ary.length;
+ for (let i = 0; i < length; i++)
+ yield ary[i];
+ },
+
+ iterator2: function (ary)
+ {
+ let length = ary.length;
+ for (let i = 0; i < length; i++)
+ yield [i, ary[i]];
+ },
+
+ uniq: function uniq(ary, unsorted)
+ {
+ let ret = [];
+ if (unsorted)
+ {
+ for (let [,item] in Iterator(ary))
+ if (ret.indexOf(item) == -1)
+ ret.push(item);
+ }
+ else
+ {
+ for (let [,item] in Iterator(ary.sort()))
+ {
+ if (item != last || !ret.length)
+ ret.push(item);
+ var last = item;
+ }
+ }
+ return ret;
+ }
+ },
+
+ // TODO: class could have better variable names/documentation
+ Timer: function Timer(minInterval, maxInterval, callback)
+ {
+ let timer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.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;
+
+ try
+ {
+ callback(this.arg);
+ }
+ finally
+ {
+ this.doneAt = Date.now() + minInterval;
+ }
+ };
+ this.tell = function (arg)
+ {
+ if (arg !== undefined)
+ 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.latest)
+ this.notify();
+ };
+ },
+
+ cloneObject: function cloneObject(obj)
+ {
+ if (obj instanceof Array)
+ return obj.slice();
+ let newObj = {};
+ for (let [k, v] in Iterator(obj))
+ newObj[k] = v;
+ return newObj;
+ },
+
+ clip: function clip(str, length)
+ {
+ return str.length <= length ? str : str.substr(0, length - 3) + "...";
+ },
+
+ compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
+
+ computedStyle: function computedStyle(node)
+ {
+ while (node instanceof Text && node.parentNode)
+ node = node.parentNode;
+ return node.ownerDocument.defaultView.getComputedStyle(node, null);
+ },
+
+ copyToClipboard: function copyToClipboard(str, verbose)
+ {
+ const clipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Components.interfaces.nsIClipboardHelper);
+ clipboardHelper.copyString(str);
+
+ if (verbose)
+ liberator.echo("Yanked " + str, commandline.FORCE_SINGLELINE);
+ },
+
+ createURI: function createURI(str)
+ {
+ const fixup = Components.classes["@mozilla.org/docshell/urifixup;1"]
+ .getService(Components.interfaces.nsIURIFixup);
+ return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+ },
+
+ escapeHTML: function escapeHTML(str)
+ {
+ // XXX: the following code is _much_ slower than a simple .replace()
+ // :history display went down from 2 to 1 second after changing
+ //
+ // var e = window.content.document.createElement("div");
+ // e.appendChild(window.content.document.createTextNode(str));
+ // return e.innerHTML;
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ },
+
+ escapeRegex: function escapeRegex(str)
+ {
+ return str.replace(/([\\{}()[\].?*+])/g, "\\$1");
+ },
+
+ escapeString: function escapeString(str, delimiter)
+ {
+ if (delimiter == undefined)
+ delimiter = '"';
+ return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
+ },
+
+ formatBytes: function formatBytes(num, decimalPlaces, humanReadable)
+ {
+ const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
+ let unitIndex = 0;
+ let tmpNum = parseInt(num, 10) || 0;
+ let strNum = [tmpNum + ""];
+
+ if (humanReadable)
+ {
+ while (tmpNum >= 1024)
+ {
+ tmpNum /= 1024;
+ if (++unitIndex > (unitVal.length - 1))
+ break;
+ }
+
+ let decPower = Math.pow(10, decimalPlaces);
+ strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
+
+ if (!strNum[1])
+ strNum[1] = "";
+
+ while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
+ strNum[1] += "0";
+ }
+
+ for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
+ strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
+
+ if (unitIndex) // decimalPlaces only when > Bytes
+ strNum[0] += "." + strNum[1];
+
+ return strNum[0] + " " + unitVal[unitIndex];
+ },
+
+ // generates an Asciidoc help entry, "command" can also be a mapping
+ generateHelp: function generateHelp(command, extraHelp)
+ {
+ let start = "", end = "";
+ if (command instanceof liberator.Command)
+ start = ":";
+ else if (command instanceof liberator.Option)
+ start = end = "'";
+
+ let ret = "";
+ let longHelp = false;
+ if ((command.help && command.description) && (command.help.length + command.description.length) > 50)
+ longHelp = true;
+
+ // the tags which are printed on the top right
+ for (let j = command.names.length - 1; j >= 0; j--)
+ ret += "|" + start + command.names[j] + end + "| ";
+
+ if (longHelp)
+ ret += "+";
+
+ ret += "\n";
+
+ // the usage information for the command
+ let usage = command.names[0];
+ if (command.specs) // for :commands
+ usage = command.specs[0];
+
+ usage = usage.replace(/{/, "\\\\{").replace(/}/, "\\\\}");
+ usage = usage.replace(/'/, "\\'").replace(/`/, "\\`");
+ ret += "||" + start + usage + end + "||";
+ if (usage.length > 15)
+ ret += " +";
+
+ ret += "\n________________________________________________________________________________\n";
+
+ // the actual help text
+ if (command.description)
+ {
+ ret += command.description + "."; // the help description
+ if (extraHelp)
+ ret += " +\n" + extraHelp;
+ }
+ else
+ ret += "Sorry, no help available";
+
+ // add more space between entries
+ ret += "\n________________________________________________________________________________\n\n\n";
+
+ return ret;
+ },
+
+ httpGet: function httpGet(url, callback)
+ {
+ try
+ {
+ let xmlhttp = new XMLHttpRequest();
+ xmlhttp.mozBackgroundRequest = true;
+ if (callback)
+ {
+ xmlhttp.onreadystatechange = function () {
+ if (xmlhttp.readyState == 4)
+ callback(xmlhttp)
+ }
+ }
+ xmlhttp.open("GET", url, !!callback);
+ xmlhttp.send(null);
+ return xmlhttp;
+ }
+ catch (e)
+ {
+ liberator.log("Error opening " + url + ": " + e, 1);
+ }
+ },
+
+ identity: function identity(k) k,
+
+ map: function map(obj, fn)
+ {
+ let ary = [];
+ for (let i in Iterator(obj))
+ ary.push(fn(i));
+ return ary;
+ },
+
+ // if color = true it uses HTML markup to color certain items
+ objectToString: function objectToString(object, color)
+ {
+ /* Use E4X literals so html is automatically quoted
+ * only when it's asked for. Noone wants to see &lt;
+ * on their console or :map :foo in their buffer
+ * when they expect :map <C-f> :foo.
+ */
+ XML.prettyPrinting = false;
+ XML.ignoreWhitespace = false;
+
+ if (object === null)
+ return "null\n";
+
+ if (typeof object != "object")
+ return false;
+
+ try
+ { // for window.JSON
+ var obj = String(object);
+ }
+ catch (e)
+ {
+ obj = "[Object]";
+ }
+ obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
+ let string = <><span highlight="Title Object">{obj}</span>::<br/>&#xa;</>;
+
+ 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))
+ {
+ object = Iterator(object);
+ hasValue = false;
+ }
+ for (let i in object)
+ {
+ let value = <![CDATA[<no value>]]>;
+ try
+ {
+ value = object[i];
+ }
+ catch (e) {}
+ if (!hasValue)
+ {
+ if (i instanceof Array && i.length == 2)
+ [i, value] = i;
+ else
+ var noVal = true;
+ }
+
+ value = template.highlight(value, true, 150);
+ // FIXME: Inline style.
+ key = <span style="font-weight: bold;">{i}</span>;
+ if (!isNaN(i))
+ i = parseInt(i);
+ else if (/^[A-Z_]+$/.test(i))
+ i = "";
+ keys.push([i, <>{key}{noVal ? "" : <>: {value}</> // Vim /
+ }<br/>&#xa;</>]);
+ }
+ }
+ catch (e) {}
+
+ function compare(a, b)
+ {
+ if (!isNaN(a[0]) && !isNaN(b[0]))
+ return a[0] - b[0];
+ return String.localeCompare(a[0], b[0]);
+ }
+ string += template.map(keys.sort(compare), function (f) f[1]);
+ return color ? string : [s for each (s in string)].join("");
+ },
+
+ range: function range(start, end, reverse)
+ {
+ if (!reverse)
+ {
+ while (start < end)
+ yield start++;
+ }
+ else
+ {
+ while (start > end)
+ yield --start;
+ }
+ },
+
+ interruptableRange: function interruptableRange(start, end, time)
+ {
+ let endTime = Date.now() + time;
+ while (start < end)
+ {
+ if (Date.now() > endTime)
+ {
+ liberator.threadYield(true, true);
+ endTime = Date.now() + time;
+ }
+ yield start++;
+ }
+ },
+
+ // same as Firefox's readFromClipboard function, but needed for apps like Thunderbird
+ readFromClipboard: function readFromClipboard()
+ {
+ let url;
+
+ try
+ {
+ const clipboard = Components.classes['@mozilla.org/widget/clipboard;1']
+ .getService(Components.interfaces.nsIClipboard);
+ const transferable = Components.classes['@mozilla.org/widget/transferable;1']
+ .createInstance(Components.interfaces.nsITransferable);
+
+ transferable.addDataFlavor("text/unicode");
+
+ if (clipboard.supportsSelectionClipboard())
+ clipboard.getData(transferable, clipboard.kSelectionClipboard);
+ else
+ clipboard.getData(transferable, clipboard.kGlobalClipboard);
+
+ let data = {};
+ let dataLen = {};
+
+ transferable.getTransferData("text/unicode", data, dataLen);
+
+ if (data)
+ {
+ data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
+ url = data.data.substring(0, dataLen.value / 2);
+ }
+ }
+ catch (e) {}
+
+ return url;
+ },
+
+ // takes a string like 'google bla, www.osnews.com'
+ // and returns an array ['www.google.com/search?q=bla', 'www.osnews.com']
+ stringToURLArray: function stringToURLArray(str)
+ {
+ let urls = str.split(new RegExp("\s*" + options["urlseparator"] + "\s*"));
+
+ return urls.map(function (url) {
+ try
+ {
+ let file = io.getFile(url);
+ if (file.exists() && file.isReadable())
+ return file.path;
+ }
+ catch (e) {}
+
+ // removes spaces from the string if it starts with http:// or something like that
+ if (/^\w+:\/\//.test(url))
+ url = url.replace(/\s+/g, "");
+
+ // strip each 'URL' - makes things simpler later on
+ url = url.replace(/^\s+|\s+$/, "");
+
+ // if the string doesn't look like a valid URL (i.e. contains a space
+ // or does not contain any of: .:/) try opening it with a search engine
+ // or keyword bookmark
+ if (/\s/.test(url) || !/[.:\/]/.test(url))
+ {
+ // TODO: it would be clearer if the appropriate call to
+ // getSearchURL was made based on whether or not the first word was
+ // indeed an SE alias rather than seeing if getSearchURL can
+ // process the call usefully and trying again if it fails - much
+ // like the comments below ;-)
+
+ // check for a search engine match in the string
+ let searchURL = bookmarks.getSearchURL(url, false);
+ if (searchURL)
+ {
+ return searchURL;
+ }
+ else // no search engine match, search for the whole string in the default engine
+ {
+ searchURL = bookmarks.getSearchURL(url, true);
+ if (searchURL)
+ return searchURL;
+ }
+ }
+ // if we are here let Firefox handle the url and hope it does
+ // something useful with it :)
+ return url;
+ });
+ },
+
+ xmlToDom: function xmlToDom(node, doc, nodes)
+ {
+ XML.prettyPrinting = false;
+ switch (node.nodeKind())
+ {
+ case "text":
+ return doc.createTextNode(node);
+ case "element":
+ let domnode = doc.createElementNS(node.namespace(), node.localName());
+ 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));
+ if (nodes && node.@key)
+ nodes[node.@key] = domnode;
+ return domnode;
+ }
+ }
+}; //}}}
+
+// Struct is really slow, AT LEAST 5 times slower than using structs or simple Objects
+// main reason is the function ConStructor(), which i couldn't get faster.
+// Maybe it's a TraceMonkey problem, for now don't use it for anything which must be fast (like bookmarks or history)
+function Struct()
+{
+ let self = this instanceof Struct ? this : new Struct();
+ if (!arguments.length)
+ return self;
+
+ let args = Array.slice(arguments);
+ self.__defineGetter__("length", function () args.length);
+ self.__defineGetter__("members", function () args.slice());
+ for (let arg in Iterator(args))
+ {
+ let [i, name] = arg;
+ self.__defineGetter__(name, function () this[i]);
+ self.__defineSetter__(name, function (val) { this[i] = val; });
+ }
+ function ConStructor()
+ {
+ let self = this instanceof arguments.callee ? this : new arguments.callee();
+ //for (let [k, v] in Iterator(Array.slice(arguments))) // That is makes using struct twice as slow as the following code:
+ for (let i = 0; i < arguments.length; i++)
+ {
+ if (arguments[i] != undefined)
+ self[i] = arguments[i];
+ }
+
+ return self;
+ }
+ ConStructor.prototype = self;
+ ConStructor.defaultValue = function (key, val)
+ {
+ let i = args.indexOf(key);
+ ConStructor.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0
+ ConStructor.prototype.__defineSetter__(i, function (val) {
+ let value = val;
+ this.__defineGetter__(i, function () value);
+ this.__defineSetter__(i, function (val) { value = val });
+ });
+ };
+ return self.constructor = ConStructor;
+}
+
+Struct.prototype = {
+ clone: function clone()
+ {
+ return this.constructor.apply(null, this.slice());
+ },
+ // Iterator over our named members
+ __iterator__: function ()
+ {
+ let self = this;
+ return ([v, self[v]] for ([k, v] in Iterator(self.members)))
+ }
+}
+
+// Add no-sideeffect array methods. Can't set new Array() as the prototype or
+// get length() won't work.
+for (let [,k] in Iterator(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf",
+ "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"]))
+ Struct.prototype[k] = Array.prototype[k];
+
+// vim: set fdm=marker sw=4 ts=4 et: