From eeed0be1a8abf7e3c97f43b63c1d595e940fef21 Mon Sep 17 00:00:00 2001 From: Michael Schutte Date: Wed, 20 Jul 2011 16:55:01 +0200 Subject: [PATCH] Initial import of 1.0~b6 --- .gitignore | 32 + .hg_archival.txt | 4 + .hgignore | 34 + .hgtags | 24 + BREAKING_CHANGES | 1 + HACKING | 152 ++ LICENSE.txt | 23 + Makefile | 13 + README.E4X | 149 ++ common/Makefile | 203 ++ common/bootstrap.js | 262 ++ common/chrome.manifest | 22 + common/components/commandline-handler.js | 62 + common/components/protocols.js | 385 +++ common/content/abbreviations.js | 320 +++ common/content/about.xul | 32 + common/content/autocommands.js | 293 +++ common/content/bindings.xml | 73 + common/content/bookmarks.js | 676 +++++ common/content/browser.js | 244 ++ common/content/buffer.js | 1861 ++++++++++++++ common/content/buffer.xhtml | 9 + common/content/commandline.js | 1818 ++++++++++++++ common/content/dactyl.js | 2185 +++++++++++++++++ common/content/disable-acr.jsm | 72 + common/content/editor.js | 884 +++++++ common/content/eval.js | 12 + common/content/events.js | 1647 +++++++++++++ common/content/help.css | 120 + common/content/help.js | 27 + common/content/help.xsl | 682 +++++ common/content/hints.js | 1272 ++++++++++ common/content/history.js | 297 +++ common/content/mappings.js | 766 ++++++ common/content/marks.js | 304 +++ common/content/modes.js | 554 +++++ common/content/mow.js | 395 +++ common/content/preferences.xul | 13 + common/content/quickmarks.js | 207 ++ common/content/statusline.js | 392 +++ common/content/tabs.js | 1076 ++++++++ common/contrib/fix_symlinks.py | 20 + common/javascript.vim | 343 +++ common/locale/en-US/all.xml | 43 + common/locale/en-US/autocommands.xml | 110 + common/locale/en-US/browsing.xml | 572 +++++ common/locale/en-US/buffer.xml | 529 ++++ common/locale/en-US/cmdline.xml | 295 +++ common/locale/en-US/developer.xml | 246 ++ common/locale/en-US/editing.xml | 42 + common/locale/en-US/eval.xml | 251 ++ common/locale/en-US/faq.xml | 178 ++ common/locale/en-US/gui.xml | 298 +++ common/locale/en-US/hints.xml | 143 ++ common/locale/en-US/index.xml | 41 + common/locale/en-US/intro.xml | 70 + common/locale/en-US/map.xml | 714 ++++++ common/locale/en-US/marks.xml | 472 ++++ common/locale/en-US/message.xml | 110 + common/locale/en-US/messages.properties | 198 ++ common/locale/en-US/options.xml | 1637 ++++++++++++ common/locale/en-US/pattern.xml | 135 + common/locale/en-US/print.xml | 53 + common/locale/en-US/privacy.xml | 156 ++ common/locale/en-US/repeat.xml | 469 ++++ common/locale/en-US/starting.xml | 180 ++ common/locale/en-US/styling.xml | 242 ++ common/locale/en-US/tabs.xml | 386 +++ common/locale/en-US/various.xml | 272 ++ common/make_jar.sh | 99 + common/modules/addons.jsm | 608 +++++ common/modules/base.jsm | 1550 ++++++++++++ common/modules/bookmarkcache.jsm | 235 ++ common/modules/bootstrap.jsm | 120 + common/modules/commands.jsm | 1649 +++++++++++++ common/modules/completion.jsm | 1083 ++++++++ common/modules/config.jsm | 790 ++++++ common/modules/contexts.jsm | 646 +++++ common/modules/downloads.jsm | 350 +++ common/modules/finder.jsm | 741 ++++++ common/modules/highlight.jsm | 437 ++++ common/modules/io.jsm | 1079 ++++++++ common/modules/javascript.jsm | 902 +++++++ common/modules/messages.jsm | 148 ++ common/modules/options.jsm | 1444 +++++++++++ common/modules/overlay.jsm | 330 +++ common/modules/prefs.jsm | 357 +++ common/modules/sanitizer.jsm | 679 +++++ common/modules/services.jsm | 187 ++ common/modules/storage.jsm | 620 +++++ common/modules/styles.jsm | 752 ++++++ common/modules/template.jsm | 511 ++++ common/modules/util.jsm | 1838 ++++++++++++++ common/process_manifest.awk | 20 + common/skin/dactyl.css | 229 ++ common/tests/functional/dactyl.jsm | 518 ++++ common/tests/functional/data/find.html | 13 + .../tests/functional/shared-modules/addons.js | 1284 ++++++++++ .../functional/shared-modules/dom-utils.js | 685 ++++++ .../functional/shared-modules/downloads.js | 411 ++++ .../functional/shared-modules/localization.js | 307 +++ .../functional/shared-modules/modal-dialog.js | 236 ++ .../functional/shared-modules/performance.js | 208 ++ .../tests/functional/shared-modules/places.js | 192 ++ .../tests/functional/shared-modules/prefs.js | 384 +++ .../shared-modules/private-browsing.js | 237 ++ .../functional/shared-modules/readme.txt | 10 + .../functional/shared-modules/screenshot.js | 131 + .../tests/functional/shared-modules/search.js | 836 +++++++ .../functional/shared-modules/sessionstore.js | 318 +++ .../shared-modules/software-update.js | 530 ++++ .../tests/functional/shared-modules/tabs.js | 503 ++++ .../functional/shared-modules/tabview.js | 629 +++++ .../functional/shared-modules/toolbars.js | 509 ++++ .../tests/functional/shared-modules/utils.js | 445 ++++ .../functional/shared-modules/widgets.js | 82 + common/tests/functional/testAboutPage.js | 18 + common/tests/functional/testCommands.js | 904 +++++++ common/tests/functional/testEchoCommands.js | 78 + common/tests/functional/testFindCommands.js | 49 + common/tests/functional/testHelpCommands.js | 87 + common/tests/functional/testOptions.js | 40 + common/tests/functional/testShellCommands.js | 37 + common/tests/functional/testVersionCommand.js | 38 + common/tests/functional/utils.js | 5 + common/tests/functional/utils.jsm | 48 + melodactyl/AUTHORS | 6 + melodactyl/Makefile | 11 + melodactyl/NEWS | 18 + melodactyl/TODO | 25 + melodactyl/chrome.manifest | 1 + melodactyl/components/commandline-handler.js | 1 + melodactyl/components/protocols.js | 1 + melodactyl/content/config.js | 329 +++ melodactyl/content/library.js | 64 + melodactyl/content/logo.png | Bin 0 -> 1082 bytes melodactyl/content/player.js | 832 +++++++ melodactyl/contrib/vim/Makefile | 9 + .../contrib/vim/ftdetect/melodactyl.vim | 1 + melodactyl/contrib/vim/mkvimball.txt | 2 + melodactyl/defaults/preferences/dactyl.js | 6 + melodactyl/install.rdf | 25 + melodactyl/locale/en-US/all.xml | 14 + melodactyl/locale/en-US/autocommands.xml | 51 + melodactyl/locale/en-US/browsing.xml | 17 + melodactyl/locale/en-US/gui.xml | 55 + melodactyl/locale/en-US/intro.xml | 144 ++ melodactyl/locale/en-US/map.xml | 51 + melodactyl/locale/en-US/player.xml | 328 +++ melodactyl/locale/en-US/tabs.xml | 16 + melodactyl/skin/icon.png | Bin 0 -> 465 bytes pentadactyl/AUTHORS | 43 + pentadactyl/Donors | 139 ++ pentadactyl/Makefile | 12 + pentadactyl/NEWS | 206 ++ pentadactyl/TODO | 70 + pentadactyl/bootstrap.js | 1 + pentadactyl/chrome.manifest | 1 + pentadactyl/components/commandline-handler.js | 1 + pentadactyl/components/protocols.js | 1 + pentadactyl/content/config.js | 371 +++ pentadactyl/content/logo.png | Bin 0 -> 3659 bytes pentadactyl/contrib/vim/Makefile | 9 + .../contrib/vim/ftdetect/pentadactyl.vim | 1 + pentadactyl/contrib/vim/mkvimball.txt | 2 + pentadactyl/install.rdf | 40 + pentadactyl/locale/en-US/all.xml | 14 + pentadactyl/locale/en-US/autocommands.xml | 47 + pentadactyl/locale/en-US/gui.xml | 41 + pentadactyl/locale/en-US/intro.xml | 150 ++ pentadactyl/locale/en-US/map.xml | 48 + pentadactyl/locale/en-US/tutorial.xml | 389 +++ pentadactyl/skin/about.css | 37 + pentadactyl/skin/icon.png | Bin 0 -> 464 bytes teledactyl/AUTHORS | 11 + teledactyl/Makefile | 12 + teledactyl/NEWS | 0 teledactyl/TODO | 17 + teledactyl/chrome.manifest | 1 + teledactyl/components/commandline-handler.js | 1 + teledactyl/components/protocols.js | 1 + teledactyl/content/addressbook.js | 155 ++ teledactyl/content/compose/compose.js | 83 + teledactyl/content/compose/compose.xul | 19 + teledactyl/content/compose/dactyl.xul | 98 + teledactyl/content/config.js | 217 ++ teledactyl/content/logo.png | Bin 0 -> 3731 bytes teledactyl/content/mail.js | 935 +++++++ teledactyl/contrib/vim/Makefile | 9 + .../contrib/vim/ftdetect/teledactyl.vim | 1 + teledactyl/contrib/vim/mkvimball.txt | 2 + teledactyl/defaults/preferences/dactyl.js | 6 + teledactyl/install.rdf | 24 + teledactyl/locale/en-US/Makefile | 3 + teledactyl/locale/en-US/all.xml | 11 + teledactyl/locale/en-US/autocommands.xml | 37 + teledactyl/locale/en-US/gui.xml | 30 + teledactyl/locale/en-US/intro.xml | 138 ++ teledactyl/locale/en-US/map.xml | 47 + teledactyl/skin/icon.png | Bin 0 -> 408 bytes 200 files changed, 58466 insertions(+) create mode 100644 .gitignore create mode 100644 .hg_archival.txt create mode 100644 .hgignore create mode 100644 .hgtags create mode 100644 BREAKING_CHANGES create mode 100644 HACKING create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 README.E4X create mode 100644 common/Makefile create mode 100755 common/bootstrap.js create mode 100644 common/chrome.manifest create mode 100644 common/components/commandline-handler.js create mode 100644 common/components/protocols.js create mode 100644 common/content/abbreviations.js create mode 100644 common/content/about.xul create mode 100644 common/content/autocommands.js create mode 100644 common/content/bindings.xml create mode 100644 common/content/bookmarks.js create mode 100644 common/content/browser.js create mode 100644 common/content/buffer.js create mode 100644 common/content/buffer.xhtml create mode 100644 common/content/commandline.js create mode 100644 common/content/dactyl.js create mode 100644 common/content/disable-acr.jsm create mode 100644 common/content/editor.js create mode 100644 common/content/eval.js create mode 100644 common/content/events.js create mode 100644 common/content/help.css create mode 100644 common/content/help.js create mode 100644 common/content/help.xsl create mode 100644 common/content/hints.js create mode 100644 common/content/history.js create mode 100644 common/content/mappings.js create mode 100644 common/content/marks.js create mode 100644 common/content/modes.js create mode 100644 common/content/mow.js create mode 100644 common/content/preferences.xul create mode 100644 common/content/quickmarks.js create mode 100644 common/content/statusline.js create mode 100644 common/content/tabs.js create mode 100644 common/contrib/fix_symlinks.py create mode 100644 common/javascript.vim create mode 100644 common/locale/en-US/all.xml create mode 100644 common/locale/en-US/autocommands.xml create mode 100644 common/locale/en-US/browsing.xml create mode 100644 common/locale/en-US/buffer.xml create mode 100644 common/locale/en-US/cmdline.xml create mode 100644 common/locale/en-US/developer.xml create mode 100644 common/locale/en-US/editing.xml create mode 100644 common/locale/en-US/eval.xml create mode 100644 common/locale/en-US/faq.xml create mode 100644 common/locale/en-US/gui.xml create mode 100644 common/locale/en-US/hints.xml create mode 100644 common/locale/en-US/index.xml create mode 100644 common/locale/en-US/intro.xml create mode 100644 common/locale/en-US/map.xml create mode 100644 common/locale/en-US/marks.xml create mode 100644 common/locale/en-US/message.xml create mode 100644 common/locale/en-US/messages.properties create mode 100644 common/locale/en-US/options.xml create mode 100644 common/locale/en-US/pattern.xml create mode 100644 common/locale/en-US/print.xml create mode 100644 common/locale/en-US/privacy.xml create mode 100644 common/locale/en-US/repeat.xml create mode 100644 common/locale/en-US/starting.xml create mode 100644 common/locale/en-US/styling.xml create mode 100644 common/locale/en-US/tabs.xml create mode 100644 common/locale/en-US/various.xml create mode 100644 common/make_jar.sh create mode 100644 common/modules/addons.jsm create mode 100644 common/modules/base.jsm create mode 100644 common/modules/bookmarkcache.jsm create mode 100644 common/modules/bootstrap.jsm create mode 100644 common/modules/commands.jsm create mode 100644 common/modules/completion.jsm create mode 100644 common/modules/config.jsm create mode 100644 common/modules/contexts.jsm create mode 100644 common/modules/downloads.jsm create mode 100644 common/modules/finder.jsm create mode 100644 common/modules/highlight.jsm create mode 100644 common/modules/io.jsm create mode 100644 common/modules/javascript.jsm create mode 100644 common/modules/messages.jsm create mode 100644 common/modules/options.jsm create mode 100644 common/modules/overlay.jsm create mode 100644 common/modules/prefs.jsm create mode 100644 common/modules/sanitizer.jsm create mode 100644 common/modules/services.jsm create mode 100644 common/modules/storage.jsm create mode 100644 common/modules/styles.jsm create mode 100644 common/modules/template.jsm create mode 100644 common/modules/util.jsm create mode 100644 common/process_manifest.awk create mode 100644 common/skin/dactyl.css create mode 100644 common/tests/functional/dactyl.jsm create mode 100644 common/tests/functional/data/find.html create mode 100644 common/tests/functional/shared-modules/addons.js create mode 100644 common/tests/functional/shared-modules/dom-utils.js create mode 100644 common/tests/functional/shared-modules/downloads.js create mode 100644 common/tests/functional/shared-modules/localization.js create mode 100644 common/tests/functional/shared-modules/modal-dialog.js create mode 100644 common/tests/functional/shared-modules/performance.js create mode 100644 common/tests/functional/shared-modules/places.js create mode 100644 common/tests/functional/shared-modules/prefs.js create mode 100644 common/tests/functional/shared-modules/private-browsing.js create mode 100644 common/tests/functional/shared-modules/readme.txt create mode 100644 common/tests/functional/shared-modules/screenshot.js create mode 100644 common/tests/functional/shared-modules/search.js create mode 100644 common/tests/functional/shared-modules/sessionstore.js create mode 100644 common/tests/functional/shared-modules/software-update.js create mode 100644 common/tests/functional/shared-modules/tabs.js create mode 100644 common/tests/functional/shared-modules/tabview.js create mode 100644 common/tests/functional/shared-modules/toolbars.js create mode 100644 common/tests/functional/shared-modules/utils.js create mode 100644 common/tests/functional/shared-modules/widgets.js create mode 100644 common/tests/functional/testAboutPage.js create mode 100644 common/tests/functional/testCommands.js create mode 100644 common/tests/functional/testEchoCommands.js create mode 100644 common/tests/functional/testFindCommands.js create mode 100644 common/tests/functional/testHelpCommands.js create mode 100644 common/tests/functional/testOptions.js create mode 100644 common/tests/functional/testShellCommands.js create mode 100644 common/tests/functional/testVersionCommand.js create mode 100644 common/tests/functional/utils.js create mode 100644 common/tests/functional/utils.jsm create mode 100644 melodactyl/AUTHORS create mode 100644 melodactyl/Makefile create mode 100755 melodactyl/NEWS create mode 100644 melodactyl/TODO create mode 120000 melodactyl/chrome.manifest create mode 120000 melodactyl/components/commandline-handler.js create mode 120000 melodactyl/components/protocols.js create mode 100644 melodactyl/content/config.js create mode 100644 melodactyl/content/library.js create mode 100644 melodactyl/content/logo.png create mode 100644 melodactyl/content/player.js create mode 100644 melodactyl/contrib/vim/Makefile create mode 100644 melodactyl/contrib/vim/ftdetect/melodactyl.vim create mode 100644 melodactyl/contrib/vim/mkvimball.txt create mode 100644 melodactyl/defaults/preferences/dactyl.js create mode 100644 melodactyl/install.rdf create mode 100644 melodactyl/locale/en-US/all.xml create mode 100644 melodactyl/locale/en-US/autocommands.xml create mode 100644 melodactyl/locale/en-US/browsing.xml create mode 100644 melodactyl/locale/en-US/gui.xml create mode 100644 melodactyl/locale/en-US/intro.xml create mode 100644 melodactyl/locale/en-US/map.xml create mode 100644 melodactyl/locale/en-US/player.xml create mode 100644 melodactyl/locale/en-US/tabs.xml create mode 100644 melodactyl/skin/icon.png create mode 100644 pentadactyl/AUTHORS create mode 100644 pentadactyl/Donors create mode 100644 pentadactyl/Makefile create mode 100644 pentadactyl/NEWS create mode 100644 pentadactyl/TODO create mode 120000 pentadactyl/bootstrap.js create mode 120000 pentadactyl/chrome.manifest create mode 120000 pentadactyl/components/commandline-handler.js create mode 120000 pentadactyl/components/protocols.js create mode 100644 pentadactyl/content/config.js create mode 100644 pentadactyl/content/logo.png create mode 100644 pentadactyl/contrib/vim/Makefile create mode 100644 pentadactyl/contrib/vim/ftdetect/pentadactyl.vim create mode 100644 pentadactyl/contrib/vim/mkvimball.txt create mode 100644 pentadactyl/install.rdf create mode 100644 pentadactyl/locale/en-US/all.xml create mode 100644 pentadactyl/locale/en-US/autocommands.xml create mode 100644 pentadactyl/locale/en-US/gui.xml create mode 100644 pentadactyl/locale/en-US/intro.xml create mode 100644 pentadactyl/locale/en-US/map.xml create mode 100644 pentadactyl/locale/en-US/tutorial.xml create mode 100644 pentadactyl/skin/about.css create mode 100644 pentadactyl/skin/icon.png create mode 100644 teledactyl/AUTHORS create mode 100644 teledactyl/Makefile create mode 100644 teledactyl/NEWS create mode 100644 teledactyl/TODO create mode 120000 teledactyl/chrome.manifest create mode 120000 teledactyl/components/commandline-handler.js create mode 120000 teledactyl/components/protocols.js create mode 100644 teledactyl/content/addressbook.js create mode 100644 teledactyl/content/compose/compose.js create mode 100644 teledactyl/content/compose/compose.xul create mode 100644 teledactyl/content/compose/dactyl.xul create mode 100644 teledactyl/content/config.js create mode 100644 teledactyl/content/logo.png create mode 100644 teledactyl/content/mail.js create mode 100644 teledactyl/contrib/vim/Makefile create mode 100644 teledactyl/contrib/vim/ftdetect/teledactyl.vim create mode 100644 teledactyl/contrib/vim/mkvimball.txt create mode 100644 teledactyl/defaults/preferences/dactyl.js create mode 100644 teledactyl/install.rdf create mode 100644 teledactyl/locale/en-US/Makefile create mode 100644 teledactyl/locale/en-US/all.xml create mode 100644 teledactyl/locale/en-US/autocommands.xml create mode 100644 teledactyl/locale/en-US/gui.xml create mode 100644 teledactyl/locale/en-US/intro.xml create mode 100644 teledactyl/locale/en-US/map.xml create mode 100644 teledactyl/skin/icon.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50e5bc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +## To see if new rules exclude any existing files, run +## +## git ls-files -i --exclude-standard +## +## after modifying this file. + +## Generated by the build process +*.xpi +*/locale/*/*.html +*/chrome +*/contrib/vim/*.vba + +## Editor backup and swap files +*~ +.\#* +\#**\# +.*.sw[op] +.sw[op] + +## Generated by Mac filesystem +.DS_Store + +## For rejects +*.orig +*.rej +*.ancestor +*.current +*.patched + +## Generated by StGit +patches-* +.stgit-*.txt diff --git a/.hg_archival.txt b/.hg_archival.txt new file mode 100644 index 0000000..a19df6b --- /dev/null +++ b/.hg_archival.txt @@ -0,0 +1,4 @@ +repo: 373f1649c80dea9be7b5bc9c57e8395f94f93ab1 +node: b83bb8e6d273f71b1278c4ee03376594ad6dd039 +branch: pentadactyl-1.0b6 +tag: pentadactyl-1.0b6 diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..9cb94cb --- /dev/null +++ b/.hgignore @@ -0,0 +1,34 @@ +## To see if new rules exclude any existing files, run +## +## hg status -i +## +## after modifying this file. + +syntax: glob + +## Generated by the build process +*.xpi +*/locale/*/*.html +*/chrome +*/contrib/vim/*.vba +*/bak/* +downloads/* +.git/* + +*.py[co] + +## Editor backup and swap files +*~ +.#* +\#**\# +.*.sw[op] +.sw[op] + +## Generated by Mac filesystem +.DS_Store + +syntax: regexp + +## For rejects +\.(orig|rej|bak|diff)$ + diff --git a/.hgtags b/.hgtags new file mode 100644 index 0000000..e33bca1 --- /dev/null +++ b/.hgtags @@ -0,0 +1,24 @@ +c5d6d5cd0dd426585f8e5afa314183208a610e59 muttator-0.5 +fe9ffcfb48c7050f5e6872285ff0197eb54cf213 vimperator-0.4.1 +d063e0e23d668dcf807e2032f032a3d23486ec48 vimperator-0.5 +ce28bfd79dcc119abf59e9318d524144f45d53f5 vimperator-0.5-branch-HEAD-merge-1 +019506ce1fa1a647f27fb4aed293b6e0fe629d31 vimperator-0.5.1 +605db9f5c6cb5a1d65c95211f78d0a480a1566fd vimperator-0.5.2 +3b8c0bcbbf8663fde9f959efbc1ad5fda3d9d1f6 vimperator-0.5.3 +c9273973fc735678c3930bc15fb4915414907d23 vimperator-1.0 +96b65677b4c5a29525fd791aaadbc51a4ba7b7c2 vimperator-1.1 +ff456886599eb7ad2cdd718eac498b6c8a9efc75 vimperator-1.2 +e34e4c4a9c24488f570d08200182542134e05f29 vimperator-2.0 +e34e4c4a9c24488f570d08200182542134e05f29 vimperator-2.0 +f65383ca4fc4872827f3f23350ae503c93cd66a4 vimperator-2.0a1 +75873caffbe7327bfd1c27e615378d2a196a9286 vimperator-2.1 +7777c3e4d29893701a64f46adbd60b3b06cda7a6 vimperator-2.2b1 +d4c91b705d551c63c5a0d929248ec873e978a188 vimperator-2.2 +ca14d185b1506c77b748925770731b66d3dddfbe xulmus-0.1 +a82034b6ed8fe0c5bc43123969b655423f95511d pentadactyl-1.0b1 +15d6abbda4221b30181f8459a3627e00b42f93d6 pentadactyl-1.0b2 +ffa3aaf7882250fb665d8ead4375f529f3e99070 pentadactyl-1.0b3 +2c21fc6135f832c7bbadf43586d2ffe585f02f60 pentadactyl-1.0b4 +1f1342f58d8e7d3972928f193e3f40cbf4425230 pentadactyl-1.0b4.1 +962d8a1e823d0855e14aecaa294ef02e718401b1 pentadactyl-1.0b4.2 +d783bcace8c6a24bee7bd22ead22fe44baaebe90 pentadactyl-1.0b4.3 diff --git a/BREAKING_CHANGES b/BREAKING_CHANGES new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/BREAKING_CHANGES @@ -0,0 +1 @@ + diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..6b4ca69 --- /dev/null +++ b/HACKING @@ -0,0 +1,152 @@ += Hacking = + +If you've taken to hacking Pentadactyl source code, we hope that you'll share +your changes. In case you do, please keep the following in mind, and we'll be +happy to accept your patches. + +== Documentation == + +First of all, all new features and all user-visible changes to existing +features need to be documented. That means editing the appropriate help files +and adding a NEWS entry where appropriate. When editing the NEWS file, you +should add your change to the top of the list of changes. If your change +alters an interface (key binding, command) and is likely to cause trouble, +prefix it with 'IMPORTANT:', otherwise, place it below the other 'IMPORTANT' +entries. If you're not sure if your change merits a news entry, or if it's +important, please ask. + +== Coding Style == + +In general: Just look at the existing source code! + +=== The most important style issues are: === + +* Use 4 spaces to indent things, no tabs, not 2, nor 8 spaces. If you use Vim, + this should be taken care of automatically by the modeline (like the + one below). + +* No trailing whitespace. + +* Use " for enclosing strings instead of ', unless using ' avoids escaping of lots of " + Example: alert("foo") instead of alert('foo'); + +* Use // regexp literals rather than RegExp constructors, unless + you're constructing an expression on the fly, or RegExp + constructors allow you to escape less /s than the additional + escaping of special characters required by string quoting. + + Good: /application\/xhtml\+xml/ + Bad: RegExp("application/xhtml\\+xml") + Good: RegExp("http://(www\\.)vimperator.org/(.*)/(.*)") + Bad: /http:\/\/(www\.)vimperator.org\/(.*)\/(.*)/ + +* Exactly one space after if/for/while/catch etc. and after a comma, but none + after a parenthesis or after a function call: + for (pre; condition; post) + but: + alert("foo"); + +* Bracing is formatted as follows: + function myFunction () { + if (foo) + return bar; + else { + baz = false; + return baz; + } + } + var quux = frob("you", + { + a: 1, + b: 42, + c: { + hoopy: "frood" + } + }); + + When in doubt, look for similar code. + +* No braces for one-line conditional statements: + Right: + if (foo) + frob(); + else + unfrob(); + +* Prefer lambda-style functions where suitable: + Right: list.filter(function (elem) elem.good != elem.BAD); + Wrong: list.filter(function (elem) { return elem.good != elem.BAD }); + Right: list.forEach(function (elem) { window.alert(elem); }); + Wrong: list.forEach(function (elem) window.alert(elem)); + +* Anonymous function definitions should be formatted with a space after the + keyword "function". Example: function () {}, not function() {}. + +* Prefer the use of let over var i.e. only use var when required. + For more details, see + https://developer.mozilla.org/en/New_in_JavaScript_1.7#Block_scope_with_let + +* Reuse common local variable names E.g. "elem" is generally used for element, + "win" for windows, "func" for functions, "ret" for return values etc. + +* Prefer // over /* */ comments (exceptions for big comments are usually OK) + Right: if (HACK) // TODO: remove hack + Wrong: if (HACK) /* TODO: remove hack */ + +* Documentation comment blocks use /** ... */ Wrap these lines at 80 + characters. + +* Only wrap lines if it makes the code obviously clearer. Lines longer than 132 + characters should probably be broken up rather than wrapped anyway. + +* Use UNIX new lines (\n), not windows (\r\n) or old Mac ones (\r) + +* Use Iterators or Array#forEach to iterate over arrays. for (let i + in ary) and for each (let i in ary) include members in an + Array.prototype, which some extensions alter. + Right: + for (let [,elem] in Iterator(ary)) + for (let [k, v] in Iterator(obj)) + ary.forEach(function (elem) { ... + Wrong: + for each (let elem in ary) + + The exceptions to this rule are for objects with __iterator__ set, + and for XML objects (see README.E4X). + +* Avoid using 'new' with constructors where possible, and use [] and + {} rather than new Array/new Object. + Right: + RegExp("^" + foo + "$") + Function(code) + new Date + Wrong: + new RegExp("^" + foo + "$") + new Function(code) + Date() // Right if you want a string-representation of the date + +* Don't use abbreviations for public methods + Right: + function splitString()... + let commands = ...; + let cmds = ...; // Since it's only used locally, abbreviations are ok, but so are the full names + Wrong: + function splitStr() + +== Testing == + +Functional tests are implemented using the Mozmill automated testing framework +-- https://developer.mozilla.org/en/Mozmill_Tests. + +A fresh profile is created for the duration of the test run, however, passing +arguments to the host application won't be supported until Mozmill 1.5.2, the +next release, so any user RC and plugin files should be temporarily disabled. +This can be done by adding the following to the head of the RC file: +set loadplugins= +finish + +The host application binary tested can be overridden via the HOSTAPP_PATH +makefile variable. E.g., +$ HOSTAPP_PATH=/path/to/firefox make -e -C pentadactyl test + +// vim: fdm=marker sw=4 ts=4 et ai: diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d8fcf81 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright (c) 2006-2009 by Martin Stubenschrott + 2007-2009 by Doug Kearns + 2008-2010 by Kris Maglione + +For a full list of authors, refer the AUTHORS file. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..637f33c --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +DIRS = teledactyl pentadactyl melodactyl +TARGETS = clean distclean doc help info jar release xpi +.SILENT: + +all: xpi ; + +$(TARGETS:%=\%.%): + echo MAKE $@ + $(MAKE) -C $* $(@:$*.%=%) + +$(TARGETS): + $(MAKE) $(DIRS:%=%.$@) + diff --git a/README.E4X b/README.E4X new file mode 100644 index 0000000..5e0b8a9 --- /dev/null +++ b/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 = + + + + + + ; + + // Select all bar elements of the root foo element +> xml.bar + + + // Select all baz elements anywhere beneath the root +> xml..baz + + + + // Select all of the immediate children of the root +> xml.* + + + + // 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] += + + +> xml + + + + + + + + + // and beneath the second +> xml.baz[1] = +> xml + + + + + + + + + + // Replace bar's subtree with a foo element +> xml.bar.* = +> xml + + + + + + + + + // Add a bar below bar +> xml.bar.* += + + +> xml + + + + + + + + + + // Adding a quux attribute to the root +> xml.@quux = "foo" + foo +> xml + + + + + + + + + +> xml.bar.@id = "0" +> xml..foo[0] = "Foo" + Foo +> xml..bar[1] = "Bar" + Bar +> xml +js> xml + + + Foo + Bar + + + + + + // Selecting all bar elements where id="1" +> xml..bar.(@id == 1) + Bar + + // Literals: + // XMLList literal. No root node. +> <>Foo
Baz + Foo +
+ Baz + +// Interpolation. +> let x = "" +> {x + ""} + +> {x + ""}.toXMLString() + <foo/><?> + +> let x = +> {x}.toXMLString() + + + + diff --git a/common/Makefile b/common/Makefile new file mode 100644 index 0000000..955d358 --- /dev/null +++ b/common/Makefile @@ -0,0 +1,203 @@ +#### configuration + +TOP = $(shell pwd) +OS = $(shell uname -s) +BUILD_DATE = $(shell date "+%Y/%m/%d %H:%M:%S") +BASE = $(TOP)/../common +GOOGLE_PROJ = dactyl +GOOGLE = https://$(GOOGLE_PROJ).googlecode.com/files +VERSION ?= $(shell sed -n 's/.*em:version\(>\|="\)\(.*\)["<].*/\2/p' $(TOP)/install.rdf | sed 1q) +UUID := $(shell sed -n 's/.*em:id\(>\|="\)\(.*\)["<].*/\2/p' $(TOP)/install.rdf | sed 1q) +MANGLE := $(shell date '+%s' | awk '{ printf "%x", $$1 }') +MOZMILL = mozmill +HOSTAPP_PATH = $(shell which $(HOSTAPP)) +TEST_DIR = $(BASE)/tests/functional +TEST_LOG = $(TEST_DIR)/log + +IDNAME := $(shell echo "$(NAME)" | tr a-z A-Z) + +LOCALEDIR = locale +DOC_FILES = $(wildcard $(LOCALEDIR)/*/*.xml) + +export VERSION BUILD_DATE +MAKE_JAR = sh $(BASE)/make_jar.sh + +# TODO: specify source files manually? +JAR_BASES = $(TOP) $(BASE) +JAR_DIRS = content skin locale modules +JAR_TEXTS = js jsm css dtd xml xul html xhtml xsl properties +JAR_BINS = png + +CHROME = $(MANGLE)/ +JAR = $(CHROME)$(NAME).jar + +XPI_BASES = $(JAR_BASES) $(TOP)/.. +XPI_FILES = bootstrap.js TODO AUTHORS Donors NEWS LICENSE.txt +XPI_DIRS = components $(MANGLE) defaults +XPI_TEXTS = js jsm $(JAR_TEXTS) +XPI_BINS = $(JAR_BINS) + +XPI_NAME = $(NAME)-$(VERSION) +XPI = ../downloads/$(XPI_NAME).xpi +XPI_PATH = $(TOP)/$(XPI:%.xpi=%) + +RDF = ../downloads/update.rdf +RDF_IN = $(RDF).in + +BUILD_DIR = build.$(VERSION).$(OS) + +AWK ?= awk +B64ENCODE ?= base64 +CURL ?= curl +SED := $(shell if [ "xoo" = x$$(echo foo | sed -E 's/f(o)/\1/' 2>/dev/null) ]; \ + then echo sed -E; else echo sed -r; \ + fi) + +.SILENT: + +#### rules + +TARGETS = all help info jar xpi install clean distclean install installxpi test $(CHROME) $(JAR) +$(TARGETS:%=\%.%): + echo MAKE $* $(@:$*.%=%) + $(MAKE) -C $* $(@:$*.%=%) + +.PHONY: $(TARGETS) +all: help + +help: + @echo "$(NAME) $(VERSION) build" + @echo + @echo " make help - display this help" + @echo " make info - show some info about the system" + @echo " make jar - build a JAR ($(JAR))" + @echo " make xpi - build an XPI ($(XPI_NAME))" + @echo " make installxpi - build an XPI and install it to your profile" + @echo " make install - installs this source tree directly to your $(HOSTAPP) profile" + @echo ' set $$PROFILE to select a profile by name and $$PROFILEPATHS' + @echo ' to change the directory where profiles are searched' + @echo " make release - updates update.rdf (this is not for you)" + @echo " make dist - uploads to Google Code (this is not for you)" + @echo " make clean - clean up" + @echo " make distclean - clean up more" + @echo " make test - run functional tests" + @echo + @echo "running some commands with V=1 will show more build details" + +info: + @echo "version $(VERSION)" + @echo "release file $(XPI)" + @echo "doc files $(DOC_FILES)" + @echo "xpi files $(XPI_FILES)" + +jar: $(JAR) + +release: $(XPI) $(RDF) + +# This is not for you! +dist: $(XPI) + @echo DIST $(XPI) $(GOOGLE) + set -e; \ + \ + proj=$$(echo -n $(NAME) | sed 's/\(.\).*/\1/' | tr a-z A-Z); \ + proj="$$proj$$(echo $(NAME) | sed 's/.//')"; \ + [ -z "$$summary" ] && summary="$$proj $(VERSION) Release"; \ + labels="Project-$$proj,$(labels)"; \ + [ -n "$(featured)" ] && labels="$$labels,Featured"; \ + \ + IFS=,; for l in $$labels; do \ + set -- "$$@" --form-string "label=$$l"; \ + done; \ + auth=$$(echo -n "$(GOOGLE_USER):$(GOOGLE_PASS)" | $(B64ENCODE)); \ + $(CURL) "$$@" --form-string "summary=$$summary" \ + -F "filename=@$(XPI)" \ + -H "Authorization: Basic $$auth" \ + -i "$(GOOGLE)" | sed -n '/^Location/{p;q;}' + +install: + export dir; \ + for dir in $(PROFILEPATHS); do \ + test -f "$$dir/profiles.ini" && break; \ + done; \ + \ + profile=$$(sed 's/^$$/\#/' "$$dir/profiles.ini" | \ + awk -v"profile=$(PROFILE)" \ + 'BEGIN { RS="#" } \ + index($$0, "\nName=" profile "\n") { print; exit } \ + !profile && /\nName=default\n/ { args["name=default"] = $$0 } \ + !profile && /\nDefault=1/ { args["default=1"] = $$0 } \ + END { \ + if (args["default=1"]) print args["default=1"]; \ + else print args["name=default"] \ + }' | \ + awk -F= '{ args[$$1] = $$2 } \ + END { \ + if (args["IsRelative"]) print ENVIRON["dir"] "/" args["Path"]; \ + else print args["Path"] \ + }'); \ + \ + if ! test -d "$$profile"; then \ + echo >&2 "Can't locate profile directory"; \ + exit 1; \ + fi; \ + \ + ext="$$profile/extensions/$(UUID)"; \ + mkdir -p "$$(dirname "$$ext")"; \ + rm -rf "$$ext.xpi" "$$ext"; \ + echo "Installing to $$ext"; \ + if which cygpath >/dev/null 2>&1; \ + then cygpath -wa .; \ + else pwd; \ + fi >"$$ext" +installxpi: xpi + $(HOSTAPP) $(XPI) + +$(RDF): $(RDF_IN) Makefile + @echo "Preparing release..." + $(SED) -e "s,@VERSION@,$(VERSION),g" \ + -e "s,@DATE@,$(BUILD_DATE),g" \ + < $< > $@ + @echo "SUCCESS: $@" + +clean: + @echo "General $(NAME) cleanup..." + rm -f $(JAR) $(XPI) + +distclean: + @echo "More $(NAME) cleanup..." + rm -rf $(BUILD_DIR) + +test: xpi + @echo "Running $(NAME) functional tests..." + $(IDNAME)_INIT="set loadplugins=" \ + $(MOZMILL) --show-all -l $(TEST_LOG) -b $(HOSTAPP_PATH) --addons $(XPI) -t $(TEST_DIR) + +#### xpi + +xpi: $(CHROME) + @echo "Building XPI..." + mkdir -p "$(XPI_PATH)" + + $(AWK) -v 'name=$(NAME)' -v 'suffix=$(MANGLE)' \ + -f $(BASE)/process_manifest.awk \ + "$(TOP)/chrome.manifest" >"$(XPI_PATH)/chrome.manifest" + + version="$(VERSION)"; \ + hg root >/dev/null 2>&1 && \ + case "$$version" in \ + *pre) version="$$version-hg$$(hg log -r . --template '{rev}')-$$(hg branch)";; \ + esac; \ + $(SED) -e 's/(em:version(>|="))([^"<]+)/\1'"$$version/" \ + <"$(TOP)/install.rdf" >"$(XPI_PATH)/install.rdf" + + $(MAKE_JAR) "$(XPI)" "$(XPI_BASES)" "$(XPI_DIRS)" "$(XPI_TEXTS)" "$(XPI_BINS)" "$(XPI_FILES)" + rm -r -- $(CHROME) + @echo "Built XPI: $(XPI)" + +#### jar + +$(CHROME) $(JAR): + @echo "Packaging chrome..." + $(MAKE_JAR) -r "$(@)" "$(JAR_BASES)" "$(JAR_DIRS)" "$(JAR_TEXTS)" "$(JAR_BINS)" "$(JAR_FILES)" + @echo "SUCCESS: $@" + diff --git a/common/bootstrap.js b/common/bootstrap.js new file mode 100755 index 0000000..b17cba4 --- /dev/null +++ b/common/bootstrap.js @@ -0,0 +1,262 @@ +// Copyright (c) 2010-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +// +// See https://wiki.mozilla.org/Extension_Manager:Bootstrapped_Extensions +// for details. + +const NAME = "bootstrap"; +const global = this; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +function module(uri) { + let obj = {}; + Cu.import(uri, obj); + return obj; +} + +const { AddonManager } = module("resource://gre/modules/AddonManager.jsm"); +const { XPCOMUtils } = module("resource://gre/modules/XPCOMUtils.jsm"); +const { Services } = module("resource://gre/modules/Services.jsm"); + +const resourceProto = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); +const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); +const manager = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + +const BOOTSTRAP_CONTRACT = "@dactyl.googlecode.com/base/bootstrap"; +JSMLoader = JSMLoader || BOOTSTRAP_CONTRACT in Cc && Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader; + +var JSMLoader = BOOTSTRAP_CONTRACT in Components.classes && + Components.classes[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader; + +// Temporary migration code. +if (!JSMLoader && "@mozilla.org/fuel/application;1" in Components.classes) + JSMLoader = Components.classes["@mozilla.org/fuel/application;1"] + .getService(Components.interfaces.extIApplication) + .storage.get("dactyl.JSMLoader", null); + + +function reportError(e) { + dump("\ndactyl: bootstrap: " + e + "\n" + (e.stack || Error().stack) + "\n"); + Cu.reportError(e); +} + +function httpGet(url) { + let xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); + xmlhttp.overrideMimeType("text/plain"); + xmlhttp.open("GET", url, false); + xmlhttp.send(null); + return xmlhttp; +} + +let initialized = false; +let addon = null; +let addonData = null; +let basePath = null; +let categories = []; +let components = {}; +let resources = []; +let getURI = null; + +function updateVersion() { + try { + function isDev(ver) /^hg|pre$/.test(ver); + if (typeof require === "undefined" || addon === addonData) + return; + + require(global, "config"); + require(global, "prefs"); + config.lastVersion = localPrefs.get("lastVersion", null); + + localPrefs.set("lastVersion", addon.version); + + if (!config.lastVersion || isDev(config.lastVersion) != isDev(addon.version)) + addon.applyBackgroundUpdates = AddonManager[isDev(addon.version) ? "AUTOUPDATE_DISABLE" : "AUTOUPDATE_DEFAULT"]; + } + catch (e) { + reportError(e); + } +} + +function startup(data, reason) { + dump("dactyl: bootstrap: startup " + reasonToString(reason) + "\n"); + basePath = data.installPath; + + if (!initialized) { + initialized = true; + + dump("dactyl: bootstrap: init" + " " + data.id + "\n"); + + addonData = data; + addon = data; + AddonManager.getAddonByID(addon.id, function (a) { + addon = a; + updateVersion(); + }); + + if (basePath.isDirectory()) + getURI = function getURI(path) { + let uri = Services.io.newFileURI(basePath); + uri.path += path; + return Services.io.newFileURI(uri.QueryInterface(Ci.nsIFileURL).file); + }; + else + getURI = function getURI(path) + Services.io.newURI("jar:" + Services.io.newFileURI(basePath).spec + "!/" + path, null, null); + + try { + init(); + } + catch (e) { + reportError(e); + } + } +} + +function FactoryProxy(url, classID) { + this.url = url; + this.classID = Components.ID(classID); +} +FactoryProxy.prototype = { + QueryInterface: XPCOMUtils.generateQI(Ci.nsIFactory), + register: function () { + dump("dactyl: bootstrap: register: " + this.classID + " " + this.contractID + "\n"); + + JSMLoader.registerFactory(this); + }, + get module() { + dump("dactyl: bootstrap: create module: " + this.contractID + "\n"); + + Object.defineProperty(this, "module", { value: {}, enumerable: true }); + JSMLoader.load(this.url, this.module); + return this.module; + }, + createInstance: function (iids) { + return let (factory = this.module.NSGetFactory(this.classID)) + factory.createInstance.apply(factory, arguments); + } +} + +function init() { + dump("dactyl: bootstrap: init\n"); + + let manifestURI = getURI("chrome.manifest"); + let manifest = httpGet(manifestURI.spec) + .responseText + .replace(/^\s*|\s*$|#.*/g, "") + .replace(/^\s*\n/gm, ""); + + let suffix = "-"; + let chars = "0123456789abcdefghijklmnopqrstuv"; + for (let n = Date.now(); n; n = Math.round(n / chars.length)) + suffix += chars[n % chars.length]; + suffix = ""; + + for each (let line in manifest.split("\n")) { + let fields = line.split(/\s+/); + switch(fields[0]) { + case "category": + categoryManager.addCategoryEntry(fields[1], fields[2], fields[3], false, true); + categories.push([fields[1], fields[2]]); + break; + case "component": + components[fields[1]] = new FactoryProxy(getURI(fields[2]).spec, fields[1]); + break; + case "contract": + components[fields[2]].contractID = fields[1]; + break; + + case "resource": + resources.push(fields[1], fields[1] + suffix); + resourceProto.setSubstitution(fields[1], getURI(fields[2])); + resourceProto.setSubstitution(fields[1] + suffix, getURI(fields[2])); + } + } + + try { + module("resource://dactyl-content/disable-acr.jsm").init(addon.id); + } + catch (e) { + reportError(e); + } + + if (JSMLoader && JSMLoader.bump !== 4) // Temporary hack + Services.scriptloader.loadSubScript("resource://dactyl" + suffix + "/bootstrap.jsm", + Cu.import("resource://dactyl/bootstrap.jsm", global)); + + if (!JSMLoader || JSMLoader.bump !== 4) + Cu.import("resource://dactyl/bootstrap.jsm", global); + + JSMLoader.bootstrap = this; + + JSMLoader.load("resource://dactyl/bootstrap.jsm", global); + + JSMLoader.init(suffix); + JSMLoader.load("base.jsm", global); + + if (!(BOOTSTRAP_CONTRACT in Cc)) + manager.registerFactory(Components.ID("{f541c8b0-fe26-4621-a30b-e77d21721fb5}"), + String("{f541c8b0-fe26-4621-a30b-e77d21721fb5}"), + BOOTSTRAP_CONTRACT, { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]), + instance: { + QueryInterface: XPCOMUtils.generateQI([]), + contractID: BOOTSTRAP_CONTRACT, + wrappedJSObject: {} + }, + createInstance: function () this.instance + }) + + Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader = JSMLoader; + + for each (let component in components) + component.register(); + + Services.obs.notifyObservers(null, "dactyl-rehash", null); + updateVersion(); + require(global, "overlay"); +} + +function shutdown(data, reason) { + dump("dactyl: bootstrap: shutdown " + reasonToString(reason) + "\n"); + if (reason != APP_SHUTDOWN) { + try { + module("resource://dactyl-content/disable-acr.jsm").cleanup(); + } + catch (e) { + reportError(e); + } + + if ([ADDON_UPGRADE, ADDON_DOWNGRADE, ADDON_UNINSTALL].indexOf(reason) >= 0) + Services.obs.notifyObservers(null, "dactyl-purge", null); + + Services.obs.notifyObservers(null, "dactyl-cleanup", null); + Services.obs.notifyObservers(null, "dactyl-cleanup-modules", null); + + JSMLoader.purge(); + for each (let [category, entry] in categories) + categoryManager.deleteCategoryEntry(category, entry, false); + for each (let resource in resources) + resourceProto.setSubstitution(resource, null); + } +} + +function reasonToString(reason) { + for each (let name in ["disable", "downgrade", "enable", + "install", "shutdown", "startup", + "uninstall", "upgrade"]) + if (reason == global["ADDON_" + name.toUpperCase()] || + reason == global["APP_" + name.toUpperCase()]) + return name; +} + +function install(data, reason) { dump("dactyl: bootstrap: install " + reasonToString(reason) + "\n"); } +function uninstall(data, reason) { dump("dactyl: bootstrap: uninstall " + reasonToString(reason) + "\n"); } + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/chrome.manifest b/common/chrome.manifest new file mode 100644 index 0000000..4995b0e --- /dev/null +++ b/common/chrome.manifest @@ -0,0 +1,22 @@ +resource dactyl-local-content content/ +resource dactyl-local-skin skin/ +resource dactyl-local-locale locale/ + +resource dactyl ../common/modules/ +resource dactyl-content ../common/content/ +resource dactyl-skin ../common/skin/ +resource dactyl-locale ../common/locale/ + +content dactyl ../common/content/ + +component {16dc34f7-6d22-4aa4-a67f-2921fb5dcb69} components/commandline-handler.js +contract @mozilla.org/commandlinehandler/general-startup;1?type=dactyl {16dc34f7-6d22-4aa4-a67f-2921fb5dcb69} +category command-line-handler m-dactyl @mozilla.org/commandlinehandler/general-startup;1?type=dactyl + +component {c1b67a07-18f7-4e13-b361-2edcc35a5a0d} components/protocols.js +contract @mozilla.org/network/protocol;1?name=chrome-data {c1b67a07-18f7-4e13-b361-2edcc35a5a0d} +component {9c8f2530-51c8-4d41-b356-319e0b155c44} components/protocols.js +contract @mozilla.org/network/protocol;1?name=dactyl {9c8f2530-51c8-4d41-b356-319e0b155c44} +component {f4506a17-5b4d-4cd9-92d4-2eb4630dc388} components/protocols.js +contract @dactyl.googlecode.com/base/xpc-interface-shim {f4506a17-5b4d-4cd9-92d4-2eb4630dc388} + diff --git a/common/components/commandline-handler.js b/common/components/commandline-handler.js new file mode 100644 index 0000000..918d7bf --- /dev/null +++ b/common/components/commandline-handler.js @@ -0,0 +1,62 @@ +// Copyright (c) 2009 by Doug Kearns +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +function reportError(e) { + dump("dactyl: command-line-handler: " + e + "\n" + (e.stack || Error().stack)); + Cu.reportError(e); +} + +var global = this; +var NAME = "command-line-handler"; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function CommandLineHandler() { + this.wrappedJSObject = this; + + Cu.import("resource://dactyl/base.jsm"); + require(global, "util"); + require(global, "config"); +} +CommandLineHandler.prototype = { + + classDescription: "Dactyl Command-line Handler", + + classID: Components.ID("{16dc34f7-6d22-4aa4-a67f-2921fb5dcb69}"), + + contractID: "@mozilla.org/commandlinehandler/general-startup;1?type=dactyl", + + _xpcom_categories: [{ + category: "command-line-handler", + entry: "m-dactyl" + }], + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler]), + + handle: function (commandLine) { + + // TODO: handle remote launches differently? + try { + this.optionValue = commandLine.handleFlagWithParam(config.name, false); + } + catch (e) { + util.dump("option '-" + config.name + "' requires an argument\n"); + } + }, + + get helpInfo() " -" + config.name + " " + " Additional options for " + config.appName + " startup\n".substr(config.name.length) +}; + +if (XPCOMUtils.generateNSGetFactory) + var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]); +else + var NSGetModule = XPCOMUtils.generateNSGetModule([CommandLineHandler]); +var EXPORTED_SYMBOLS = ["NSGetFactory", "global"]; + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/components/protocols.js b/common/components/protocols.js new file mode 100644 index 0000000..d14b3e4 --- /dev/null +++ b/common/components/protocols.js @@ -0,0 +1,385 @@ +// Copyright (c) 2008-2010 Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; +function reportError(e) { + dump("dactyl: protocols: " + e + "\n" + (e.stack || Error().stack)); + Cu.reportError(e); +} + +/* Adds support for data: URIs with chrome privileges + * and fragment identifiers. + * + * "chrome-data:" [; ]* "," [] + * + * By Kris Maglione, ideas from Ed Anuff's nsChromeExtensionHandler. + */ + +var NAME = "protocols"; +var global = this; +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal); + +var DNE = "resource://dactyl/content/does/not/exist"; +var _DNE; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +function makeChannel(url, orig) { + try { + if (url == null) + return fakeChannel(orig); + + if (typeof url === "function") + return let ([type, data] = url(orig)) StringChannel(data, type, orig); + + if (isArray(url)) + return let ([type, data] = url) StringChannel(data, type, orig); + + let uri = ioService.newURI(url, null, null); + return (new XMLChannel(uri)).channel; + } + catch (e) { + util.reportError(e); + throw e; + } +} +function fakeChannel(orig) { + let channel = ioService.newChannel(DNE, null, null); + channel.originalURI = orig; + return channel; +} +function redirect(to, orig, time) { + let html = .toXMLString(); + return StringChannel(html, "text/html", ioService.newURI(to, null, null)); +} + +function Factory(clas) ({ + __proto__: clas.prototype, + createInstance: function (outer, iid) { + try { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + if (!clas.instance) + clas.instance = new clas(); + return clas.instance.QueryInterface(iid); + } + catch (e) { + reportError(e); + throw e; + } + } +}); + +function ChromeData() {} +ChromeData.prototype = { + contractID: "@mozilla.org/network/protocol;1?name=chrome-data", + classID: Components.ID("{c1b67a07-18f7-4e13-b361-2edcc35a5a0d}"), + classDescription: "Data URIs with chrome privileges", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]), + _xpcom_factory: Factory(ChromeData), + + scheme: "chrome-data", + defaultPort: -1, + allowPort: function (port, scheme) false, + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE + | Ci.nsIProtocolHandler.URI_NOAUTH + | Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE, + + newURI: function (spec, charset, baseURI) { + var uri = Components.classes["@mozilla.org/network/standard-url;1"] + .createInstance(Components.interfaces.nsIStandardURL) + .QueryInterface(Components.interfaces.nsIURI); + uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, charset, null); + return uri; + }, + + newChannel: function (uri) { + try { + if (uri.scheme == this.scheme) { + let channel = ioService.newChannel(uri.spec.replace(/^.*?:\/*(.*)(?:#.*)?/, "data:$1"), + null, null); + channel.contentCharset = "UTF-8"; + channel.owner = systemPrincipal; + channel.originalURI = uri; + return channel; + } + } + catch (e) {} + return fakeChannel(uri); + } +}; + +function Dactyl() { + // Kill stupid validator warning. + this["wrapped" + "JSObject"] = this; + + this.HELP_TAGS = {}; + this.FILE_MAP = {}; + this.OVERLAY_MAP = {}; + + this.pages = {}; + this.providers = {}; + + Cu.import("resource://dactyl/bootstrap.jsm"); + if (!JSMLoader.initialized) + JSMLoader.init(); + JSMLoader.load("base.jsm", global); + require(global, "config"); + require(global, "services"); + require(global, "util"); + _DNE = ioService.newChannel(DNE, null, null).name; + + // Doesn't belong here: + AboutHandler.prototype.register(); +} +Dactyl.prototype = { + contractID: "@mozilla.org/network/protocol;1?name=dactyl", + classID: Components.ID("{9c8f2530-51c8-4d41-b356-319e0b155c44}"), + classDescription: "Dactyl utility protocol", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIProtocolHandler]), + _xpcom_factory: Factory(Dactyl), + + init: function (obj) { + for each (let prop in ["HELP_TAGS", "FILE_MAP", "OVERLAY_MAP"]) { + this[prop] = this[prop].constructor(); + for (let [k, v] in Iterator(obj[prop] || {})) + this[prop][k] = v; + } + this.initialized = true; + }, + + scheme: "dactyl", + defaultPort: -1, + allowPort: function (port, scheme) false, + protocolFlags: 0 + | Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE + | Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE, + + newURI: function newURI(spec, charset, baseURI) { + var uri = Cc["@mozilla.org/network/standard-url;1"] + .createInstance(Ci.nsIStandardURL) + .QueryInterface(Ci.nsIURI); + if (baseURI && baseURI.host === "data") + baseURI = null; + uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, charset, baseURI); + return uri; + }, + + newChannel: function newChannel(uri) { + try { + if (/^help/.test(uri.host) && !("all" in this.FILE_MAP)) + return redirect(uri.spec, uri, 1); + + if (uri.host in this.providers) + return makeChannel(this.providers[uri.host](uri), uri); + + let path = decodeURIComponent(uri.path.replace(/^\/|#.*/g, "")); + switch(uri.host) { + case "content": + return makeChannel(this.pages[path] || "resource://dactyl-content/" + path, uri); + case "data": + try { + var channel = ioService.newChannel(uri.path.replace(/^\/(.*)(?:#.*)?/, "data:$1"), + null, null); + } + catch (e) { + var error = e; + break; + } + channel.contentCharset = "UTF-8"; + channel.owner = systemPrincipal; + channel.originalURI = uri; + return channel; + case "help": + return makeChannel(this.FILE_MAP[path], uri); + case "help-overlay": + return makeChannel(this.OVERLAY_MAP[path], uri); + case "help-tag": + let tag = decodeURIComponent(uri.path.substr(1)); + if (tag in this.FILE_MAP) + return redirect("dactyl://help/" + tag, uri); + if (tag in this.HELP_TAGS) + return redirect("dactyl://help/" + this.HELP_TAGS[tag] + "#" + tag.replace(/#/g, encodeURIComponent), uri); + break; + case "locale": + return LocaleChannel("dactyl-locale", path, uri); + case "locale-local": + return LocaleChannel("dactyl-local-locale", path, uri); + } + } + catch (e) { + util.reportError(e); + } + if (error) + throw error; + return fakeChannel(uri); + }, + + // FIXME: Belongs elsewhere + _xpcom_categories: [{ + category: "profile-after-change", + entry: "m-dactyl" + }], + + observe: function observe(subject, topic, data) { + if (topic === "profile-after-change") { + Cu.import("resource://dactyl/bootstrap.jsm"); + JSMLoader.init(); + require(global, "overlay"); + } + } +}; + +function LocaleChannel(pkg, path, orig) { + for each (let locale in [config.locale, "en-US"]) + for each (let sep in "-/") { + var channel = makeChannel(["resource:/", pkg + sep + config.locale, path].join("/"), orig); + if (channel.name !== _DNE) + return channel; + } + return channel; +} + +function StringChannel(data, contentType, uri) { + let channel = services.StreamChannel(uri); + channel.contentStream = services.StringStream(data); + if (contentType) + channel.contentType = contentType; + channel.contentCharset = "UTF-8"; + channel.owner = systemPrincipal; + if (uri) + channel.originalURI = uri; + return channel; +} + +function XMLChannel(uri, contentType) { + try { + var channel = services.io.newChannelFromURI(uri); + var channelStream = channel.open(); + } + catch (e) { + this.channel = fakeChannel(uri); + return; + } + + this.uri = uri; + this.sourceChannel = services.io.newChannelFromURI(uri); + this.pipe = services.Pipe(true, true, 0, 0, null); + this.writes = []; + + this.channel = services.StreamChannel(uri); + this.channel.contentStream = this.pipe.inputStream; + this.channel.contentType = contentType || channel.contentType; + this.channel.contentCharset = "UTF-8"; + this.channel.owner = systemPrincipal; + + let stream = services.InputStream(channelStream); + let [, pre, doctype, url, open, post] = util.regexp(, "x").exec(stream.read(4096)); + this.writes.push(pre); + if (doctype) { + this.writes.push(doctype + "[\n"); + try { + this.writes.push(services.io.newChannel(url, null, null).open()) + } + catch (e) {} + if (!open) + this.writes.push("\n]"); + this.writes.push(post) + } + this.writes.push(channelStream); + + this.writeNext(); +} +XMLChannel.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver]), + writeNext: function () { + try { + if (!this.writes.length) + this.pipe.outputStream.close(); + else { + let stream = this.writes.shift(); + if (isString(stream)) + stream = services.StringStream(stream); + + services.StreamCopier(stream, this.pipe.outputStream, null, + false, true, 4096, true, false) + .asyncCopy(this, null); + } + } + catch (e) { + util.reportError(e); + } + }, + + onStartRequest: function (request, context) {}, + onStopRequest: function (request, context, statusCode) { + this.writeNext(); + } +}; + +function AboutHandler() {} +AboutHandler.prototype = { + register: function () { + try { + JSMLoader.registerFactory(Factory(AboutHandler)); + } + catch (e) { + util.reportError(e); + } + }, + + get classDescription() "About " + config.appName + " Page", + + classID: Components.ID("81495d80-89ee-4c36-a88d-ea7c4e5ac63f"), + + get contractID() "@mozilla.org/network/protocol/about;1?what=" + config.name, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + + newChannel: function (uri) { + let channel = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService) + .newChannel("dactyl://content/about.xul", null, null); + channel.originalURI = uri; + return channel; + }, + + getURIFlags: function (uri) Ci.nsIAboutModule.ALLOW_SCRIPT, +}; + +// A hack to get information about interfaces. +// Doesn't belong here. +function Shim() {} +Shim.prototype = { + contractID: "@dactyl.googlecode.com/base/xpc-interface-shim", + classID: Components.ID("{f4506a17-5b4d-4cd9-92d4-2eb4630dc388}"), + classDescription: "XPCOM empty interface shim", + QueryInterface: function (iid) { + if (iid.equals(Ci.nsISecurityCheckedComponent)) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + }, + getHelperForLanguage: function () null, + getInterfaces: function (count) { count.value = 0; } +}; + +if (XPCOMUtils.generateNSGetFactory) + var NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeData, Dactyl, Shim]); +else + var NSGetModule = XPCOMUtils.generateNSGetModule([ChromeData, Dactyl, Shim]); +var EXPORTED_SYMBOLS = ["NSGetFactory", "global"]; + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/abbreviations.js b/common/content/abbreviations.js new file mode 100644 index 0000000..9a76fa3 --- /dev/null +++ b/common/content/abbreviations.js @@ -0,0 +1,320 @@ +// Copyright (c) 2006-2009 by Martin Stubenschrott +// Copyright (c) 2010 by anekos +// Copyright (c) 2010-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +var Abbreviation = Class("Abbreviation", { + init: function (modes, lhs, rhs) { + this.modes = modes.sort(); + this.lhs = lhs; + this.rhs = rhs; + }, + + equals: function (other) this.lhs == other.lhs && this.rhs == other.rhs, + + expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs), + + modesEqual: function (modes) array.equals(this.modes, modes), + + inMode: function (mode) this.modes.some(function (_mode) _mode == mode), + + inModes: function (modes) modes.some(function (mode) this.inMode(mode), this), + + removeMode: function (mode) { + this.modes = this.modes.filter(function (m) m != mode).sort(); + }, + + get modeChar() Abbreviation.modeChar(this.modes) +}, { + modeChar: function (_modes) { + let result = array.uniq(_modes.map(function (m) m.char)).join(""); + if (result == "ci") + result = "!"; + return result; + } +}); + +var AbbrevHive = Class("AbbrevHive", Contexts.Hive, { + init: function init(group) { + init.superapply(this, arguments); + this._store = {}; + }, + + get empty() !values(this._store).nth(util.identity, 0), + + /** + * Adds a new abbreviation. + * + * @param {Abbreviation} abbr The abbreviation to add. + */ + add: function (abbr) { + if (!(abbr instanceof Abbreviation)) + abbr = Abbreviation.apply(null, arguments); + + for (let [, mode] in Iterator(abbr.modes)) { + if (!this._store[mode]) + this._store[mode] = {}; + this._store[mode][abbr.lhs] = abbr; + } + }, + + /** + * Returns the abbreviation with *lhs* in the given *mode*. + * + * @param {Mode} mode The mode of the abbreviation. + * @param {string} lhs The LHS of the abbreviation. + */ + get: function (mode, lhs) { + let abbrevs = this._store[mode]; + return abbrevs && set.has(abbrevs, lhs) ? abbrevs[lhs] : null; + }, + + /** + * @property {Abbreviation[]} The list of the abbreviations merged from + * each mode. + */ + get merged() { + // Wth? --Kris; + let map = values(this._store).map(Iterator).map(iter.toArray) + .flatten().toObject(); + return Object.keys(map).sort().map(function (k) map[k]); + }, + + /** + * Remove the specified abbreviations. + * + * @param {Array} modes List of modes. + * @param {string} lhs The LHS of the abbreviation. + * @returns {boolean} Did the deleted abbreviation exist? + */ + remove: function (modes, lhs) { + let result = false; + for (let [, mode] in Iterator(modes)) { + if ((mode in this._store) && (lhs in this._store[mode])) { + result = true; + this._store[mode][lhs].removeMode(mode); + delete this._store[mode][lhs]; + } + } + return result; + }, + + /** + * Removes all abbreviations specified in *modes*. + * + * @param {Array} modes List of modes. + */ + clear: function (modes) { + for (let mode in values(modes)) { + for (let abbr in values(this._store[mode])) + abbr.removeMode(mode); + delete this._store[mode]; + } + } +}); + +var Abbreviations = Module("abbreviations", { + init: function () { + + // (summarized from Vim's ":help abbreviations") + // + // There are three types of abbreviations. + // + // full-id: Consists entirely of keyword characters. + // ("foo", "g3", "-1") + // + // end-id: Ends in a keyword character, but all other + // are not keyword characters. + // ("#i", "..f", "$/7") + // + // non-id: Ends in a non-keyword character, but the + // others can be of any type other than space + // and tab. + // ("def#", "4/7$") + // + // Example strings that cannot be abbreviations: + // "a.b", "#def", "a b", "_$r" + // + // For now, a keyword character is anything except for \s, ", or ' + // (i.e., whitespace and quotes). In Vim, a keyword character is + // specified by the 'iskeyword' setting and is a much less inclusive + // list. + // + // TODO: Make keyword definition closer to Vim's default keyword + // definition (which differs across platforms). + + let params = { // This is most definitely not Vim compatible. + keyword: /[^\s"']/, + nonkeyword: /[ "']/ + }; + + this._match = util.regexp(<>) (+ )$ | // full-id + (^ | \s | ) (+ )$ | // end-id + (^ | \s ) (\S* )$ // non-id + ]]>, "x", params); + this._check = util.regexp(<>+ | // full-id + + | // end-id + \S* // non-id + ) $ + ]]>, "x", params); + }, + + get: deprecated("group.abbrevs.get", { get: function get() this.user.closure.get }), + set: deprecated("group.abbrevs.set", { get: function set() this.user.closure.set }), + remove: deprecated("group.abbrevs.remove", { get: function remove() this.user.closure.remove }), + removeAll: deprecated("group.abbrevs.clear", { get: function removeAll() this.user.closure.clear }), + + /** + * Returns the abbreviation for the given *mode* if *text* matches the + * abbreviation expansion criteria. + * + * @param {Mode} mode The mode to search. + * @param {string} text The string to test against the expansion criteria. + * + * @returns {Abbreviation} + */ + match: function (mode, text) { + let match = this._match.exec(text); + if (match) + return this.hives.map(function (h) h.get(mode, match[2] || match[4] || match[6])).nth(util.identity, 0); + return null; + }, + + /** + * Lists all abbreviations matching *modes* and *lhs*. + * + * @param {Array} modes List of modes. + * @param {string} lhs The LHS of the abbreviation. + */ + list: function (modes, lhs) { + let hives = contexts.allGroups.abbrevs.filter(function (h) !h.empty); + + function abbrevs(hive) + hive.merged.filter(function (abbr) (abbr.inModes(modes) && abbr.lhs.indexOf(lhs) == 0)); + + let list = + + + + + + + { + template.map(hives, function (hive) let (i = 0) + + + template.map(abbrevs(hive), function (abbrev) + + + + + + ) + + ) + } +
+ ModeAbbrevReplacement
{!i++ ? hive.name : ""}{abbrev.modeChar}{abbrev.lhs}{abbrev.rhs}
; + + // TODO: Move this to an ItemList to show this automatically + if (list.*.length() === list.text().length() + 2) + dactyl.echomsg(_("abbrev.none")); + else + commandline.commandOutput(list); + } + +}, { +}, { + contexts: function initContexts(dactyl, modules, window) { + update(Abbreviations.prototype, { + hives: contexts.Hives("abbrevs", AbbrevHive), + user: contexts.hives.abbrevs.user + }); + }, + completion: function () { + completion.abbreviation = function abbreviation(context, modes, group) { + group = group || abbreviations.user; + let fn = modes ? function (abbr) abbr.inModes(modes) : util.identity; + context.keys = { text: "lhs" , description: "rhs" }; + context.completions = group.merged.filter(fn); + }; + }, + + commands: function () { + function addAbbreviationCommands(modes, ch, modeDescription) { + modes.sort(); + modeDescription = modeDescription ? " in " + modeDescription + " mode" : ""; + + commands.add([ch ? ch + "a[bbreviate]" : "ab[breviate]"], + "Abbreviate a key sequence" + modeDescription, + function (args) { + let [lhs, rhs] = args; + dactyl.assert(!args.length || abbreviations._check.test(lhs), + _("error.invalidArgument")); + + if (!rhs) + abbreviations.list(modes, lhs || ""); + else { + if (args["-javascript"]) + rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]); + args["-group"].add(modes, lhs, rhs); + } + }, { + completer: function (context, args) { + if (args.length == 1) + return completion.abbreviation(context, modes, args["-group"]); + else if (args["-javascript"]) + return completion.javascript(context); + }, + hereDoc: true, + literal: 1, + options: [ + contexts.GroupFlag("abbrevs"), + { + names: ["-javascript", "-js", "-j"], + description: "Expand this abbreviation by evaluating its right-hand-side as JavaScript" + } + ], + serialize: function () [ + { + command: this.name, + arguments: [abbr.lhs], + literalArg: abbr.rhs, + options: callable(abbr.rhs) ? {"-javascript": null} : {} + } + for ([, abbr] in Iterator(abbreviations.user.merged)) + if (abbr.modesEqual(modes)) + ] + }); + + commands.add([ch + "una[bbreviate]"], + "Remove an abbreviation" + modeDescription, + function (args) { + util.assert(args.bang ^ !!args[0], _("error.argumentOrBang")); + + if (args.bang) + args["-group"].clear(modes); + else if (!args["-group"].remove(modes, args[0])) + return dactyl.echoerr(_("abbrev.noSuch")); + }, { + argCount: "?", + bang: true, + completer: function (context, args) completion.abbreviation(context, modes, args["-group"]), + literal: 0, + options: [contexts.GroupFlag("abbrevs")] + }); + } + + addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", ""); + addAbbreviationCommands([modes.INSERT], "i", "insert"); + addAbbreviationCommands([modes.COMMAND_LINE], "c", "command line"); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/about.xul b/common/content/about.xul new file mode 100644 index 0000000..433402c --- /dev/null +++ b/common/content/about.xul @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/common/content/autocommands.js b/common/content/autocommands.js new file mode 100644 index 0000000..2d960c8 --- /dev/null +++ b/common/content/autocommands.js @@ -0,0 +1,293 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +var AutoCommand = Struct("event", "filter", "command"); +update(AutoCommand.prototype, { + eventName: Class.memoize(function () this.event.toLowerCase()), + + match: function (event, pattern) { + return (!event || this.eventName == event.toLowerCase()) && (!pattern || String(this.filter) === pattern); + } +}); + +var AutoCmdHive = Class("AutoCmdHive", Contexts.Hive, { + init: function init(group) { + init.supercall(this, group); + this._store = []; + }, + + __iterator__: function () array.iterValues(this._store), + + /** + * Adds a new autocommand. *cmd* will be executed when one of the specified + * *events* occurs and the URL of the applicable buffer matches *regexp*. + * + * @param {Array} events The array of event names for which this + * autocommand should be executed. + * @param {string} pattern The URL pattern to match against the buffer URL. + * @param {string} cmd The Ex command to run. + */ + add: function (events, pattern, cmd) { + if (!callable(pattern)) + pattern = Group.compileFilter(pattern); + + for (let event in values(events)) + this._store.push(AutoCommand(event, pattern, cmd)); + }, + + /** + * Returns all autocommands with a matching *event* and *regexp*. + * + * @param {string} event The event name filter. + * @param {string} pattern The URL pattern filter. + * @returns {AutoCommand[]} + */ + get: function (event, pattern) { + return this._store.filter(function (autoCmd) autoCmd.match(event, regexp)); + }, + + /** + * Deletes all autocommands with a matching *event* and *regexp*. + * + * @param {string} event The event name filter. + * @param {string} regexp The URL pattern filter. + */ + remove: function (event, regexp) { + this._store = this._store.filter(function (autoCmd) !autoCmd.match(event, regexp)); + }, +}); + +/** + * @instance autocommands + */ +var AutoCommands = Module("autocommands", { + init: function () { + update(this, { + hives: contexts.Hives("autocmd", AutoCmdHive), + user: contexts.hives.autocmd.user, + allHives: contexts.allGroups.autocmd, + matchingHives: function matchingHives(uri, doc) contexts.matchingGroups(uri, doc).autocmd + }); + }, + + get activeHives() contexts.allGroups.autocmd.filter(function (h) h._store.length), + + add: deprecated("group.autocmd.add", { get: function add() autocommands.user.closure.add }), + get: deprecated("group.autocmd.get", { get: function get() autocommands.user.closure.get }), + remove: deprecated("group.autocmd.remove", { get: function remove() autocommands.user.closure.remove }), + + /** + * Lists all autocommands with a matching *event* and *regexp*. + * + * @param {string} event The event name filter. + * @param {string} regexp The URL pattern filter. + */ + list: function (event, regexp) { + + function cmds(hive) { + let cmds = {}; + hive._store.forEach(function (autoCmd) { + if (autoCmd.match(event, regexp)) { + cmds[autoCmd.event] = cmds[autoCmd.event] || []; + cmds[autoCmd.event].push(autoCmd); + } + }); + return cmds; + } + + commandline.commandOutput( + + + + + { + template.map(this.activeHives, function (hive) + + + + + + + template.map(cmds(hive), function ([event, items]) + + + template.map(items, function (item, i) + + + + + ) + + ) + + ) + } +
----- Auto Commands -----
{hive.name}
{i == 0 ? event : ""}{item.filter.toXML ? item.filter.toXML() : item.filter}{item.command}
); + }, + + /** + * Triggers the execution of all autocommands registered for *event*. A map + * of *args* is passed to each autocommand when it is being executed. + * + * @param {string} event The event to fire. + * @param {Object} args The args to pass to each autocommand. + */ + trigger: function (event, args) { + if (options.get("eventignore").has(event)) + return; + + dactyl.echomsg(_("autocmd.executing", event, "*".quote()), 8); + + let lastPattern = null; + var { url, doc } = args; + if (url) + uri = util.newURI(url); + else + var { uri, doc } = buffer; + + event = event.toLowerCase(); + for (let hive in values(this.matchingHives(uri, doc))) { + let args = update({}, + hive.argsExtra(arguments[1]), + arguments[1]); + + for (let autoCmd in values(hive._store)) + if (autoCmd.eventName === event && autoCmd.filter(uri, doc)) { + if (!lastPattern || lastPattern !== String(autoCmd.filter)) + dactyl.echomsg(_("autocmd.executing", event, autoCmd.filter), 8); + + lastPattern = String(autoCmd.filter); + dactyl.echomsg(_("autocmd.autocommand", autoCmd.command), 9); + + dactyl.trapErrors(autoCmd.command, autoCmd, args); + } + } + } +}, { +}, { + commands: function () { + commands.add(["au[tocmd]"], + "Execute commands automatically on events", + function (args) { + let [event, regexp, cmd] = args; + let events = []; + + if (event) { + // NOTE: event can only be a comma separated list for |:au {event} {pat} {cmd}| + let validEvents = Object.keys(config.autocommands).map(String.toLowerCase); + validEvents.push("*"); + + events = Option.parse.stringlist(event); + dactyl.assert(events.every(function (event) validEvents.indexOf(event.toLowerCase()) >= 0), + _("autocmd.noGroup", event)); + } + + if (args.length > 2) { // add new command, possibly removing all others with the same event/pattern + if (args.bang) + args["-group"].remove(event, regexp); + cmd = contexts.bindMacro(args, "-ex", function (params) params); + args["-group"].add(events, regexp, 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] != "*" || args.length > 1) + args["-group"].remove(event, regexp); // remove all + } + else + autocommands.list(event, regexp); // list all + } + }, { + bang: true, + completer: function (context, args) { + if (args.length == 1) + return completion.autocmdEvent(context); + if (args.length == 3) + return args["-javascript"] ? completion.javascript(context) : completion.ex(context); + }, + hereDoc: true, + keepQuotes: true, + literal: 2, + options: [ + contexts.GroupFlag("autocmd"), + { + names: ["-javascript", "-js"], + description: "Interpret the action as JavaScript code rather than an Ex command" + } + ] + }); + + [ + { + name: "do[autocmd]", + description: "Apply the autocommands matching the specified URL pattern to the current buffer" + }, { + name: "doautoa[ll]", + description: "Apply the autocommands matching the specified URL pattern to all buffers" + } + ].forEach(function (command) { + commands.add([command.name], + command.description, + // TODO: Perhaps this should take -args to pass to the command? + function (args) { + // Vim compatible + if (args.length == 0) + return void dactyl.echomsg(_("autocmd.noMatching")); + + let [event, url] = args; + let defaultURL = url || buffer.uri.spec; + let validEvents = Object.keys(config.autocommands); + + // TODO: add command validators + dactyl.assert(event != "*", + _("autocmd.cantExecuteAll")); + dactyl.assert(validEvents.indexOf(event) >= 0, + _("autocmd.noGroup", args)); + dactyl.assert(autocommands.get(event).some(function (c) c.patterns.some(function (re) re.test(defaultURL) ^ !re.result)), + _("autocmd.noMatching")); + + if (this.name == "doautoall" && dactyl.has("tabs")) { + let current = tabs.index(); + + for (let i = 0; i < tabs.count; i++) { + tabs.select(i); + // if no url arg is specified use the current buffer's URL + autocommands.trigger(event, { url: url || buffer.uri.spec }); + } + + tabs.select(current); + } + else + autocommands.trigger(event, { url: defaultURL }); + }, { + argCount: "*", // FIXME: kludged for proper error message should be "1". + completer: function (context) completion.autocmdEvent(context), + keepQuotes: true + }); + }); + }, + completion: function () { + completion.autocmdEvent = function autocmdEvent(context) { + context.completions = Iterator(config.autocommands); + }; + }, + javascript: function () { + JavaScript.setCompleter(autocommands.user.get, [function () Iterator(config.autocommands)]); + }, + options: function () { + options.add(["eventignore", "ei"], + "List of autocommand event names which should be ignored", + "stringlist", "", + { + values: iter(update({ all: "All Events" }, config.autocommands)).toArray(), + has: Option.has.toggleAll + }); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/bindings.xml b/common/content/bindings.xml new file mode 100644 index 0000000..800230d --- /dev/null +++ b/common/content/bindings.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js new file mode 100644 index 0000000..ade210d --- /dev/null +++ b/common/content/bookmarks.js @@ -0,0 +1,676 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +var DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; + +// also includes methods for dealing with keywords and search engines +var Bookmarks = Module("bookmarks", { + init: function () { + storage.addObserver("bookmark-cache", function (key, event, arg) { + if (["add", "change", "remove"].indexOf(event) >= 0) + autocommands.trigger("Bookmark" + event[0].toUpperCase() + event.substr(1), + iter({ + bookmark: { + toString: function () "bookmarkcache.bookmarks[" + arg.id + "]", + valueOf: function () arg + } + }, arg)); + statusline.updateStatus(); + }, window); + }, + + get format() ({ + anchored: false, + title: ["URL", "Info"], + keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" }, + process: [template.icon, template.bookmarkDescription] + }), + + // TODO: why is this a filter? --djk + get: function get(filter, tags, maxItems, extra) { + return completion.runCompleter("bookmark", filter, maxItems, tags, extra); + }, + + /** + * Adds a new bookmark. The first parameter should be an object with + * any of the following properties: + * + * @param {boolean} unfiled If true, the bookmark is added to the + * Unfiled Bookmarks Folder. + * @param {string} title The title of the new bookmark. + * @param {string} url The URL of the new bookmark. + * @param {string} keyword The keyword of the new bookmark. + * @optional + * @param {[string]} tags The tags for the new bookmark. + * @optional + * @param {boolean} force If true, a new bookmark is always added. + * Otherwise, if a bookmark for the given URL exists it is + * updated instead. + * @optional + * @returns {boolean} True if the bookmark was added or updated + * successfully. + */ + add: function add(unfiled, title, url, keyword, tags, force) { + // FIXME + if (isObject(unfiled)) + var { unfiled, title, url, keyword, tags, post, charset, force } = unfiled; + + try { + let uri = util.createURI(url); + if (!force && bookmarkcache.isBookmarked(uri)) + for (var bmark in bookmarkcache) + if (bmark.url == uri.spec) { + if (title) + bmark.title = title; + break; + } + + if (tags) { + PlacesUtils.tagging.untagURI(uri, null); + PlacesUtils.tagging.tagURI(uri, tags); + } + if (bmark == undefined) + bmark = bookmarkcache.bookmarks[ + services.bookmarks.insertBookmark( + services.bookmarks[unfiled ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"], + uri, -1, title || url)]; + if (!bmark) + return false; + + if (charset !== undefined) + bmark.charset = charset; + if (post !== undefined) + bmark.post = post; + if (keyword) + bmark.keyword = keyword; + } + catch (e) { + util.reportError(e); + return false; + } + + return true; + }, + + /** + * Opens the command line in Ex mode pre-filled with a :bmark + * command to add a new search keyword for the given form element. + * + * @param {Element} elem A form element for which to add a keyword. + */ + addSearchKeyword: function (elem) { + if (elem instanceof HTMLFormElement || elem.form) + var [url, post, charset] = util.parseForm(elem); + else + var [url, post, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet]; + + let options = { "-title": "Search " + elem.ownerDocument.title }; + if (post != null) + options["-post"] = post; + if (charset != null && charset !== "UTF-8") + options["-charset"] = charset; + + CommandExMode().open( + commands.commandToString({ command: "bmark", options: options, arguments: [url] }) + " -keyword "); + }, + + /** + * Toggles the bookmarked state of the given URL. If the URL is + * bookmarked, all bookmarks for said URL are removed. + * If it is not, a new bookmark is added to the Unfiled Bookmarks + * Folder. The new bookmark has the title of the current buffer if + * its URL is identical to *url*, otherwise its title will be the + * value of *url*. + * + * @param {string} url The URL of the bookmark to toggle. + */ + toggle: function toggle(url) { + if (!url) + return; + + let count = this.remove(url); + if (count > 0) + dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.removed", url) }); + else { + let title = buffer.uri.spec == url && buffer.title || url; + let extra = ""; + if (title != url) + extra = " (" + title + ")"; + this.add({ unfiled: true, title: title, url: url }); + dactyl.echomsg({ domains: [util.getHost(url)], message: _("bookmark.added", url + extra) }); + } + }, + + isBookmarked: deprecated("bookmarkcache.isBookmarked", { get: function isBookmarked() bookmarkcache.closure.isBookmarked }), + + /** + * Remove a bookmark or bookmarks. If *ids* is an array, removes the + * bookmarks with those IDs. If it is a string, removes all + * bookmarks whose URLs match that string. + * + * @param {string|[number]} ids The IDs or URL of the bookmarks to + * remove. + * @returns {number} The number of bookmarks removed. + */ + remove: function remove(ids) { + try { + if (!isArray(ids)) { + let uri = util.newURI(ids); + ids = services.bookmarks + .getBookmarkIdsForURI(uri, {}) + .filter(bookmarkcache.closure.isRegularBookmark); + } + ids.forEach(function (id) { + let bmark = bookmarkcache.bookmarks[id]; + if (bmark) { + PlacesUtils.tagging.untagURI(bmark.uri, null); + bmark.charset = null; + } + services.bookmarks.removeItem(id); + }); + return ids.length; + } + catch (e) { + dactyl.reportError(e, true); + return 0; + } + }, + + getSearchEngines: deprecated("bookmarks.searchEngines", function getSearchEngines() this.searchEngines), + /** + * Returns a list of all visible search engines in the search + * services, augmented with keyword, title, and icon properties for + * use in completion functions. + */ + get searchEngines() { + let searchEngines = []; + let aliases = {}; + return iter(services.browserSearch.getVisibleEngines({})).map(function ([, engine]) { + let alias = engine.alias; + if (!alias || !/^[a-z0-9-]+$/.test(alias)) + alias = engine.name.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/, "").toLowerCase(); + if (!alias) + alias = "search"; // for search engines which we can't find a suitable alias + + if (set.has(aliases, alias)) + alias += ++aliases[alias]; + else + aliases[alias] = 0; + + return [alias, { keyword: alias, __proto__: engine, title: engine.description, icon: engine.iconURI && engine.iconURI.spec }]; + }).toObject(); + }, + + /** + * Retrieves a list of search suggestions from the named search + * engine based on the given *query*. The results are always in the + * form of an array of strings. If *callback* is provided, the + * request is executed asynchronously and *callback* is called on + * completion. Otherwise, the request is executed synchronously and + * the results are returned. + * + * @param {string} engineName The name of the search engine from + * which to request suggestions. + * @param {string} query The query string for which to request + * suggestions. + * @param {function([string])} callback The function to call when + * results are returned. + * @returns {[string] | null} + */ + getSuggestions: function getSuggestions(engineName, query, callback) { + const responseType = "application/x-suggestions+json"; + + let engine = set.has(this.searchEngines, engineName) && this.searchEngines[engineName]; + if (engine && engine.supportsResponseType(responseType)) + var queryURI = engine.getSubmission(query, responseType).uri.spec; + if (!queryURI) + return (callback || util.identity)([]); + + function process(req) { + let results = []; + try { + results = JSON.parse(req.responseText)[1].filter(isString); + } + catch (e) {} + if (callback) + return callback(results); + return results; + } + + let req = util.httpGet(queryURI, callback && process); + if (callback) + return req; + return process(req); + }, + + /** + * Returns an array containing a search URL and POST data for the + * given search string. If *useDefsearch* is true, the string is + * always passed to the default search engine. If it is not, the + * search engine name is retrieved from the first space-separated + * token of the given string. + * + * Returns null if no search engine is found for the passed string. + * + * @param {string} text The text for which to retrieve a search URL. + * @param {boolean} useDefsearch Whether to use the default search + * engine. + * @returns {[string, string | null] | null} + */ + getSearchURL: function getSearchURL(text, useDefsearch) { + let query = (useDefsearch ? options["defsearch"] + " " : "") + text; + + // ripped from Firefox + var keyword = query; + var param = ""; + var offset = query.indexOf(" "); + if (offset > 0) { + keyword = query.substr(0, offset); + param = query.substr(offset + 1); + } + + var engine = set.has(bookmarks.searchEngines, keyword) && bookmarks.searchEngines[keyword]; + if (engine) { + if (engine.searchForm && !param) + return engine.searchForm; + let submission = engine.getSubmission(param, null); + return [submission.uri.spec, submission.postData]; + } + + let [url, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); + if (!url) + return null; + + let data = window.unescape(postData || ""); + if (/%s/i.test(url) || /%s/i.test(data)) { + var charset = ""; + var matches = url.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/); + if (matches) + [, url, charset] = matches; + else + try { + charset = services.history.getCharsetForURI(util.newURI(url)); + } + catch (e) {} + + if (charset) + var encodedParam = escape(window.convertFromUnicode(charset, param)); + else + encodedParam = bookmarkcache.keywords[keyword].encodeURIComponent(param); + + url = url.replace(/%s/g, encodedParam).replace(/%S/g, param); + if (/%s/i.test(data)) + postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded"); + } + else if (param) + postData = null; + + if (postData) + return [url, postData]; + return url; + }, + + /** + * Lists all bookmarks whose URLs match *filter*, tags match *tags*, + * and other properties match the properties of *extra*. If + * *openItems* is true, the items are opened in tabs rather than + * listed. + * + * @param {string} filter A URL filter string which the URLs of all + * matched items must contain. + * @param {[string]} tags An array of tags each of which all matched + * items must contain. + * @param {boolean} openItems If true, items are opened rather than + * listed. + * @param {object} extra Extra properties which must be matched. + */ + list: function list(filter, tags, openItems, maxItems, extra) { + // FIXME: returning here doesn't make sense + // Why the hell doesn't it make sense? --Kris + // Because it unconditionally bypasses the final error message + // block and does so only when listing items, not opening them. In + // short it breaks the :bmarks command which doesn't make much + // sense to me but I'm old-fashioned. --djk + if (!openItems) + return completion.listCompleter("bookmark", filter, maxItems, tags, extra); + let items = completion.runCompleter("bookmark", filter, maxItems, tags, extra); + + if (items.length) + return dactyl.open(items.map(function (i) i.url), dactyl.NEW_TAB); + + if (filter.length > 0 && tags.length > 0) + dactyl.echoerr(_("bookmark.noMatching", tags.map(String.quote), filter.quote())); + else if (filter.length > 0) + dactyl.echoerr(_("bookmark.noMatchingString", filter.quote())); + else if (tags.length > 0) + dactyl.echoerr(_("bookmark.noMatchingTags", tags.map(String.quote))); + else + dactyl.echoerr(_("bookmark.none")); + return null; + } +}, { +}, { + commands: function () { + commands.add(["ju[mps]"], + "Show jumplist", + function () { + let sh = history.session; + commandline.commandOutput(template.jumps(sh.index, sh)); + }, + { argCount: "0" }); + + // TODO: Clean this up. + const tags = { + names: ["-tags", "-T"], + description: "A comma-separated list of tags", + completer: function tags(context, args) { + context.generate = function () array(b.tags for (b in bookmarkcache) if (b.tags)).flatten().uniq().array; + context.keys = { text: util.identity, description: util.identity }; + }, + type: CommandOption.LIST + }; + + const title = { + names: ["-title", "-t"], + description: "Bookmark page title or description", + completer: function title(context, args) { + let frames = buffer.allFrames(); + if (!args.bang) + return [ + [win.document.title, frames.length == 1 ? "Current Location" : "Frame: " + win.location.href] + for ([, win] in Iterator(frames))]; + context.keys.text = "title"; + context.keys.description = "url"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); + }, + type: CommandOption.STRING + }; + + const post = { + names: ["-post", "-p"], + description: "Bookmark POST data", + completer: function post(context, args) { + context.keys.text = "post"; + context.keys.description = "url"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], post: context.filter }); + }, + type: CommandOption.STRING + }; + + const keyword = { + names: ["-keyword", "-k"], + description: "Keyword by which this bookmark may be opened (:open {keyword})", + completer: function keyword(context, args) { + context.keys.text = "keyword"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); + }, + type: CommandOption.STRING, + validator: function (arg) /^\S+$/.test(arg) + }; + + commands.add(["bma[rk]"], + "Add a bookmark", + function (args) { + let opts = { + force: args.bang, + unfiled: false, + keyword: args["-keyword"] || null, + charset: args["-charset"], + post: args["-post"], + tags: args["-tags"] || [], + title: args["-title"] || (args.length === 0 ? buffer.title : null), + url: args.length === 0 ? buffer.uri.spec : args[0] + }; + + if (bookmarks.add(opts)) { + let extra = (opts.title == opts.url) ? "" : " (" + opts.title + ")"; + dactyl.echomsg({ domains: [util.getHost(opts.url)], message: _("bookmark.added", opts.url + extra) }, + 1, commandline.FORCE_SINGLELINE); + } + else + dactyl.echoerr(_("bookmark.cantAdd", opts.title.quote())); + }, { + argCount: "?", + bang: true, + completer: function (context, args) { + if (!args.bang) { + context.title = ["Page URL"]; + let frames = buffer.allFrames(); + context.completions = [ + [win.document.documentURI, frames.length == 1 ? "Current Location" : "Frame: " + win.document.title] + for ([, win] in Iterator(frames))]; + return; + } + completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); + }, + options: [keyword, title, tags, post, + { + names: ["-charset", "-c"], + description: "The character encoding of the bookmark", + type: CommandOption.STRING, + completer: function (context) completion.charset(context), + validator: Option.validateCompleter + } + ] + }); + + commands.add(["bmarks"], + "List or open multiple bookmarks", + function (args) { + bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"], + { keyword: args["-keyword"], title: args["-title"] }); + }, + { + bang: true, + completer: function completer(context, args) { + context.filter = args.join(" "); + completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); + }, + options: [tags, keyword, title, + { + names: ["-max", "-m"], + description: "The maximum number of items to list or open", + type: CommandOption.INT + } + ] + // Not privateData, since we don't treat bookmarks as private + }); + + commands.add(["delbm[arks]"], + "Delete a bookmark", + function (args) { + if (args.bang) + commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", + function (resp) { + if (resp && resp.match(/^y(es)?$/i)) { + bookmarks.remove(Object.keys(bookmarkcache.bookmarks)); + dactyl.echomsg(_("bookmark.allGone")); + } + }); + else { + if (!(args.length || args["-tags"] || args["-keyword"] || args["-title"])) + var deletedCount = bookmarks.remove(buffer.uri.spec); + else { + let context = CompletionContext(args.join(" ")); + context.fork("bookmark", 0, completion, "bookmark", + args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); + var deletedCount = bookmarks.remove(context.allItems.items.map(function (item) item.item.id)); + } + + dactyl.echomsg({ message: _("bookmark.deleted", deletedCount) }); + } + + }, + { + argCount: "?", + bang: true, + completer: function completer(context, args) + completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }), + domains: function (args) array.compact(args.map(util.getHost)), + literal: 0, + options: [tags, title, keyword], + privateData: true + }); + }, + mappings: function () { + var myModes = config.browserModes; + + mappings.add(myModes, ["a"], + "Open a prompt to bookmark the current URL", + function () { + let options = {}; + + let url = buffer.uri.spec; + let bmarks = bookmarks.get(url).filter(function (bmark) bmark.url == url); + + if (bmarks.length == 1) { + let bmark = bmarks[0]; + + options["-title"] = bmark.title; + if (bmark.charset) + options["-charset"] = bmark.charset; + if (bmark.keyword) + options["-keyword"] = bmark.keyword; + if (bmark.post) + options["-post"] = bmark.post; + if (bmark.tags.length > 0) + options["-tags"] = bmark.tags; + } + else { + if (buffer.title != buffer.uri.spec) + options["-title"] = buffer.title; + if (content.document.characterSet !== "UTF-8") + options["-charset"] = content.document.characterSet; + } + + CommandExMode().open( + commands.commandToString({ command: "bmark", options: options, arguments: [buffer.uri.spec] })); + }); + + mappings.add(myModes, ["A"], + "Toggle bookmarked state of current URL", + function () { bookmarks.toggle(buffer.uri.spec); }); + }, + options: function () { + options.add(["defsearch", "ds"], + "The default search engine", + "string", "google", + { + completer: function completer(context) { + completion.search(context, true); + context.completions = [{ keyword: "", title: "Don't perform searches by default" }].concat(context.completions); + } + }); + + options.add(["suggestengines"], + "Search engines used for search suggestions", + "stringlist", "google", + { completer: function completer(context) completion.searchEngine(context, true), }); + }, + + completion: function () { + completion.bookmark = function bookmark(context, tags, extra) { + context.title = ["Bookmark", "Title"]; + context.format = bookmarks.format; + iter(extra || {}).forEach(function ([k, v]) { + if (v != null) + context.filters.push(function (item) item.item[k] != null && this.matchString(v, item.item[k])); + }); + context.generate = function () values(bookmarkcache.bookmarks); + completion.urls(context, tags); + }; + + completion.search = function search(context, noSuggest) { + let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); + let keywords = bookmarkcache.keywords; + let engines = bookmarks.searchEngines; + + context.title = ["Search Keywords"]; + context.completions = iter(values(keywords), values(engines)); + context.keys = { text: "keyword", description: "title", icon: "icon" }; + + if (!space || noSuggest) + return; + + context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", + keyword, true); + + let item = keywords[keyword]; + 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.background = true; + context.compare = CompletionContext.Sort.unsorted; + context.generate = function () { + let [begin, end] = item.url.split("%s"); + + return history.get({ uri: util.newURI(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) + try { + item.url = decodeURIComponent(query.replace(/#.*/, "").replace(/\+/g, " ")); + return item; + } + catch (e) {} + return null; + }).filter(util.identity); + }; + }); + }; + + completion.searchEngine = function searchEngine(context, suggest) { + context.title = ["Suggest Engine", "Description"]; + context.keys = { text: "keyword", description: "title", icon: "icon" }; + context.completions = values(bookmarks.searchEngines); + if (suggest) + context.filters.push(function ({ item }) item.supportsResponseType("application/x-suggestions+json")); + + }; + + completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) { + if (!context.filter) + return; + + let engineList = (engineAliases || options["suggestengines"].join(",") || "google").split(","); + + engineList.forEach(function (name) { + let engine = bookmarks.searchEngines[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.keys = { text: util.identity, description: function () "" }; + ctxt.compare = CompletionContext.Sort.unsorted; + ctxt.filterFunc = null; + + let words = ctxt.filter.toLowerCase().split(/\s+/g); + ctxt.completions = ctxt.completions.filter(function (i) words.every(function (w) i.toLowerCase().indexOf(w) >= 0)); + + ctxt.hasItems = ctxt.completions.length; + ctxt.incomplete = true; + ctxt.cache.request = bookmarks.getSuggestions(name, ctxt.filter, function (compl) { + ctxt.incomplete = false; + ctxt.completions = array.uniq(ctxt.completions.filter(function (c) compl.indexOf(c) >= 0) + .concat(compl), true); + }); + }); + }; + + completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest); + completion.addUrlCompleter("b", "Bookmarks", completion.bookmark); + completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/browser.js b/common/content/browser.js new file mode 100644 index 0000000..746b46e --- /dev/null +++ b/common/content/browser.js @@ -0,0 +1,244 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +/** + * @instance browser + */ +var Browser = Module("browser", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { + init: function init() { + this.cleanupProgressListener = util.overlayObject(window.XULBrowserWindow, + this.progressListener); + util.addObserver(this); + }, + + destroy: function () { + this.cleanupProgressListener(); + this.observe.unregister(); + }, + + observers: { + "chrome-document-global-created": function (win, uri) { this.observe(win, "content-document-global-created", uri); }, + "content-document-global-created": function (win, uri) { + let top = util.topWindow(win); + + if (top == window) + this._triggerLoadAutocmd("PageLoadPre", win.document, win.location.href != "null" ? window.location.href : uri); + } + }, + + _triggerLoadAutocmd: function _triggerLoadAutocmd(name, doc, uri) { + if (!(uri || doc.location)) + return; + + uri = isObject(uri) ? uri : util.newURI(uri || doc.location.href); + let args = { + url: { toString: function () uri.spec, valueOf: function () uri }, + title: doc.title + }; + + if (dactyl.has("tabs")) { + args.tab = tabs.getContentIndex(doc) + 1; + args.doc = { + valueOf: function () doc, + toString: function () "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument" + }; + } + + autocommands.trigger(name, args); + }, + + events: { + DOMContentLoaded: function onDOMContentLoaded(event) { + let doc = event.originalTarget; + if (doc instanceof HTMLDocument) + this._triggerLoadAutocmd("DOMLoad", doc); + }, + + // TODO: see what can be moved to onDOMContentLoaded() + // event listener which is is called on each page load, even if the + // page is loaded in a background tab + load: function onLoad(event) { + let doc = event.originalTarget; + if (doc instanceof Document) + dactyl.initDocument(doc); + + if (doc instanceof HTMLDocument) { + if (doc.defaultView.frameElement) { + // document is part of a frameset + + // hacky way to get rid of "Transferring data from ..." on sites with frames + // when you click on a link inside a frameset, because asyncUpdateUI + // is not triggered there (Gecko bug?) + this.timeout(function () { statusline.updateStatus(); }, 10); + } + else { + // code which should happen for all (also background) newly loaded tabs goes here: + if (doc != config.browser.contentDocument) + dactyl.echomsg({ domains: [util.getHost(doc.location)], message: _("buffer.backgroundLoaded", (doc.title || doc.location.href)) }, 3); + + this._triggerLoadAutocmd("PageLoad", doc); + } + } + } + }, + + /** + * @property {Object} The document loading progress listener. + */ + progressListener: { + // XXX: function may later be needed to detect a canceled synchronous openURL() + onStateChange: util.wrapCallback(function onStateChange(webProgress, request, flags, status) { + onStateChange.superapply(this, arguments); + // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also + // receive statechange events for loading images and other parts of the web page + if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) { + dactyl.applyTriggerObserver("browser.stateChange", arguments); + // This fires when the load event is initiated + // only thrown for the current tab, not when another tab changes + if (flags & Ci.nsIWebProgressListener.STATE_START) { + while (document.commandDispatcher.focusedWindow == webProgress.DOMWindow + && modes.have(modes.INPUT)) + modes.pop(); + + } + else if (flags & Ci.nsIWebProgressListener.STATE_STOP) { + // Workaround for bugs 591425 and 606877, dactyl bug #81 + config.browser.mCurrentBrowser.collapsed = false; + if (!dactyl.focusedElement || dactyl.focusedElement === document.documentElement) + dactyl.focusContent(); + } + } + }), + onSecurityChange: util.wrapCallback(function onSecurityChange(webProgress, request, state) { + onSecurityChange.superapply(this, arguments); + dactyl.applyTriggerObserver("browser.securityChange", arguments); + }), + onStatusChange: util.wrapCallback(function onStatusChange(webProgress, request, status, message) { + onStatusChange.superapply(this, arguments); + dactyl.applyTriggerObserver("browser.statusChange", arguments); + }), + onProgressChange: util.wrapCallback(function onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { + onProgressChange.superapply(this, arguments); + dactyl.applyTriggerObserver("browser.progressChange", arguments); + }), + // happens when the users switches tabs + onLocationChange: util.wrapCallback(function onLocationChange(webProgress, request, uri) { + onLocationChange.superapply(this, arguments); + + dactyl.applyTriggerObserver("browser.locationChange", arguments); + + let win = webProgress.DOMWindow; + if (win && uri) { + let oldURI = win.document.dactylURI; + if (win.document.dactylLoadIdx === webProgress.loadedTransIndex + || !oldURI || uri.spec.replace(/#.*/, "") !== oldURI.replace(/#.*/, "")) + for (let frame in values(buffer.allFrames(win))) + frame.document.dactylFocusAllowed = false; + win.document.dactylURI = uri.spec; + win.document.dactylLoadIdx = webProgress.loadedTransIndex; + } + + // Workaround for bugs 591425 and 606877, dactyl bug #81 + let collapse = uri && uri.scheme === "dactyl" && webProgress.isLoadingDocument; + if (collapse) + dactyl.focus(document.documentElement); + config.browser.mCurrentBrowser.collapsed = collapse; + + util.timeout(function () { + browser._triggerLoadAutocmd("LocationChange", + (win || content).document, + uri); + }); + }), + // called at the very end of a page load + asyncUpdateUI: util.wrapCallback(function asyncUpdateUI() { + asyncUpdateUI.superapply(this, arguments); + util.timeout(function () { statusline.updateStatus(); }, 100); + }), + setOverLink: util.wrapCallback(function setOverLink(link, b) { + setOverLink.superapply(this, arguments); + dactyl.triggerObserver("browser.overLink", link); + }), + } +}, { +}, { + events: function initEvents(dactyl, modules, window) { + events.listen(config.browser, browser, "events", true); + }, + commands: function initCommands(dactyl, modules, window) { + commands.add(["o[pen]"], + "Open one or more URLs in the current tab", + function (args) { dactyl.open(args[0] || "about:blank"); }, + { + completer: function (context) completion.url(context), + domains: function (args) array.compact(dactyl.parseURLs(args[0] || "").map( + function (url) util.getHost(url))), + literal: 0, + privateData: true + }); + + commands.add(["redr[aw]"], + "Redraw the screen", + function () { + window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils) + .redraw(); + statusline.updateStatus(); + commandline.clear(); + }, + { argCount: "0" }); + }, + mappings: function initMappings(dactyl, modules, window) { + // opening websites + mappings.add([modes.NORMAL], + ["o"], "Open one or more URLs", + function () { CommandExMode().open("open "); }); + + mappings.add([modes.NORMAL], ["O"], + "Open one or more URLs, based on current location", + function () { CommandExMode().open("open " + buffer.uri.spec); }); + + mappings.add([modes.NORMAL], ["t"], + "Open one or more URLs in a new tab", + function () { CommandExMode().open("tabopen "); }); + + mappings.add([modes.NORMAL], ["T"], + "Open one or more URLs in a new tab, based on current location", + function () { CommandExMode().open("tabopen " + buffer.uri.spec); }); + + mappings.add([modes.NORMAL], ["w"], + "Open one or more URLs in a new window", + function () { CommandExMode().open("winopen "); }); + + mappings.add([modes.NORMAL], ["W"], + "Open one or more URLs in a new window, based on current location", + function () { CommandExMode().open("winopen " + buffer.uri.spec); }); + + mappings.add([modes.NORMAL], ["~"], + "Open home directory", + function () { dactyl.open("~"); }); + + mappings.add([modes.NORMAL], ["gh"], + "Open homepage", + function () { BrowserHome(); }); + + mappings.add([modes.NORMAL], ["gH"], + "Open homepage in a new tab", + function () { + let homepages = gHomeButton.getHomePage(); + dactyl.open(homepages, { from: "homepage", where: dactyl.NEW_TAB }); + }); + + mappings.add([modes.MAIN], [""], + "Redraw the screen", + function () { ex.redraw(); }); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/buffer.js b/common/content/buffer.js new file mode 100644 index 0000000..af099e7 --- /dev/null +++ b/common/content/buffer.js @@ -0,0 +1,1861 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +/** + * A class to manage the primary web content buffer. The name comes + * from Vim's term, 'buffer', which signifies instances of open + * files. + * @instance buffer + */ +var Buffer = Module("buffer", { + init: function init() { + this.evaluateXPath = util.evaluateXPath; + this.pageInfo = {}; + + this.addPageInfoSection("f", "Feeds", function (verbose) { + 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 = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 || + // really slimy: general XML types with magic letters in the title + type in feedTypes && /\brss\b/i.test(data.title); + } + + if (isFeed) { + try { + window.urlSecurityCheck(data.href, principal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } + catch (e) { + isFeed = false; + } + } + + if (type) + data.type = type; + + return isFeed; + } + + let nFeed = 0; + for (let [i, win] in Iterator(buffer.allFrames())) { + let doc = win.document; + + for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) { + let rel = link.rel.toLowerCase(); + let feed = { title: link.title, href: link.href, type: link.type || "" }; + if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) { + nFeed++; + let type = feedTypes[feed.type] || "RSS"; + if (verbose) + yield [feed.title, template.highlightURL(feed.href, true) +  ({type})]; + } + } + + } + + if (!verbose && nFeed) + yield nFeed + " feed" + (nFeed > 1 ? "s" : ""); + }); + + this.addPageInfoSection("g", "General Info", function (verbose) { + let doc = buffer.focusedFrame.document; + + // get file size + const ACCESS_READ = Ci.nsICache.ACCESS_READ; + let cacheKey = doc.documentURI; + + for (let proto in array.iterValues(["HTTP", "FTP"])) { + try { + var cacheEntryDescriptor = services.cache.createSession(proto, 0, true) + .openCacheEntry(cacheKey, ACCESS_READ, false); + break; + } + catch (e) {} + } + + let 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 + } + + let lastModVerbose = new Date(doc.lastModified).toLocaleString(); + let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X"); + + 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.href, true)]; + + let 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]; + }); + + this.addPageInfoSection("m", "Meta Tags", function (verbose) { + // get meta tag data, sort and put into pageMeta[] + let metaNodes = buffer.focusedFrame.document.getElementsByTagName("meta"); + + return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)]) + .sort(function (a, b) util.compareIgnoreCase(a[0], b[0])); + }); + + dactyl.commands["buffer.viewSource"] = function (event) { + let elem = event.originalTarget; + buffer.viewSource([elem.getAttribute("href"), Number(elem.getAttribute("line"))]); + }; + }, + + // called when the active document is scrolled + _updateBufferPosition: function _updateBufferPosition() { + statusline.updateBufferPosition(); + commandline.clear(true); + }, + + /** + * @property {Array} The alternative style sheets for the current + * buffer. Only returns style sheets for the 'screen' media type. + */ + get alternateStyleSheets() { + let stylesheets = window.getAllStyleSheets(this.focusedFrame); + + return stylesheets.filter( + function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title) + ); + }, + + climbUrlPath: function climbUrlPath(count) { + let url = buffer.documentURI.clone(); + dactyl.assert(url instanceof Ci.nsIURL); + + while (count-- && url.path != "/") + url.path = url.path.replace(/[^\/]+\/*$/, ""); + + dactyl.assert(!url.equals(buffer.documentURI)); + dactyl.open(url.spec); + }, + + incrementURL: function incrementURL(count) { + let matches = buffer.uri.spec.match(/(.*?)(\d+)(\D*)$/); + dactyl.assert(matches); + let oldNum = matches[2]; + + // disallow negative numbers as trailing numbers are often proceeded by hyphens + let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0)); + if (/^0/.test(oldNum)) + while (newNum.length < oldNum.length) + newNum = "0" + newNum; + + matches[2] = newNum; + dactyl.open(matches.slice(1).join("")); + }, + + /** + * @property {Object} A map of page info sections to their + * content generating functions. + */ + pageInfo: null, + + /** + * @property {number} True when the buffer is fully loaded. + */ + get loaded() Math.min.apply(null, + this.allFrames() + .map(function (frame) ["loading", "interactive", "complete"] + .indexOf(frame.document.readyState))), + + /** + * @property {Object} The local state store for the currently selected + * tab. + */ + get localStore() { + if (!content.document.dactylStore) + content.document.dactylStore = {}; + return content.document.dactylStore; + }, + + /** + * @property {Node} The last focused input field in the buffer. Used + * by the "gi" key binding. + */ + get lastInputField() { + let field = this.localStore.lastInputField && this.localStore.lastInputField.get(); + let doc = field && field.ownerDocument; + let win = doc && doc.defaultView; + return win && doc === win.document ? field : null; + }, + set lastInputField(value) { this.localStore.lastInputField = value && Cu.getWeakReference(value); }, + + /** + * @property {nsIURI} The current top-level document. + */ + get doc() window.content.document, + + /** + * @property {nsIURI} The current top-level document's URI. + */ + get uri() util.newURI(content.location.href), + + /** + * @property {nsIURI} The current top-level document's URI, sans any + * fragment identifier. + */ + get documentURI() let (doc = content.document) doc.documentURIObject || util.newURI(doc.documentURI), + + /** + * @property {string} The current top-level document's URL. + */ + get URL() update(new String(content.location.href), util.newURI(content.location.href)), + + /** + * @property {number} The buffer's height in pixels. + */ + get pageHeight() content.innerHeight, + + /** + * @property {number} The current browser's zoom level, as a + * percentage with 100 as 'normal'. + */ + get zoomLevel() config.browser.markupDocumentViewer[this.fullZoom ? "fullZoom" : "textZoom"] * 100, + set zoomLevel(value) { this.setZoom(value, this.fullZoom); }, + + /** + * @property {boolean} Whether the current browser is using full + * zoom, as opposed to text zoom. + */ + get fullZoom() ZoomManager.useFullZoom, + set fullZoom(value) { this.setZoom(this.zoomLevel, value); }, + + /** + * @property {string} The current document's title. + */ + get title() content.document.title, + + /** + * @property {number} The buffer's horizontal scroll percentile. + */ + get scrollXPercent() { + let elem = this.findScrollable(0, true); + if (elem.scrollWidth - elem.clientWidth === 0) + return 0; + return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth); + }, + + /** + * @property {number} The buffer's vertical scroll percentile. + */ + get scrollYPercent() { + let elem = this.findScrollable(0, false); + if (elem.scrollHeight - elem.clientHeight === 0) + return 0; + return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight); + }, + + /** + * Adds a new section to the page information output. + * + * @param {string} option The section's value in 'pageinfo'. + * @param {string} title The heading for this section's + * output. + * @param {function} func The function to generate this + * section's output. + */ + addPageInfoSection: function addPageInfoSection(option, title, func) { + this.pageInfo[option] = Buffer.PageInfo(option, title, func); + }, + + /** + * Returns a list of all frames in the given window or current buffer. + */ + allFrames: function allFrames(win, focusedFirst) { + let frames = []; + (function rec(frame) { + if (frame.document.body instanceof HTMLBodyElement) + frames.push(frame); + Array.forEach(frame.frames, rec); + })(win || content); + if (focusedFirst) + return frames.filter(function (f) f === buffer.focusedFrame).concat( + frames.filter(function (f) f !== buffer.focusedFrame)); + return frames; + }, + + /** + * @property {Window} Returns the currently focused frame. + */ + get focusedFrame() { + let frame = this.localStore.focusedFrame; + return frame && frame.get() || content; + }, + set focusedFrame(frame) { + this.localStore.focusedFrame = Cu.getWeakReference(frame); + }, + + /** + * Returns the currently selected word. If the selection is + * null, it tries to guess the word that the caret is + * positioned in. + * + * @returns {string} + */ + get currentWord() Buffer.currentWord(this.focusedFrame), + getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() this.currentWord), + + /** + * Returns true if a scripts are allowed to focus the given input + * element or input elements in the given window. + * + * @param {Node|Window} + * @returns {boolean} + */ + focusAllowed: function focusAllowed(elem) { + if (elem instanceof Window && !Editor.getEditor(elem)) + return true; + let doc = elem.ownerDocument || elem.document || elem; + return !options["strictfocus"] || doc.dactylFocusAllowed; + }, + + /** + * Focuses the given element. In contrast to a simple + * elem.focus() call, this function works for iframes and + * image maps. + * + * @param {Node} elem The element to focus. + */ + focusElement: function focusElement(elem) { + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + win.document.dactylFocusAllowed = true; + + if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement])) + elem = elem.contentWindow; + if (elem.document) + elem.document.dactylFocusAllowed = true; + + if (elem instanceof HTMLInputElement && elem.type == "file") { + Buffer.openUploadPrompt(elem); + this.lastInputField = elem; + } + else { + if (isinstance(elem, [HTMLInputElement, XULTextBoxElement])) + var flags = services.focus.FLAG_BYMOUSE; + else + flags = services.focus.FLAG_SHOWRING; + dactyl.focus(elem, flags); + + if (elem instanceof Window) { + let sel = elem.getSelection(); + if (sel && !sel.rangeCount) + sel.addRange(RangeFind.endpoint( + RangeFind.nodeRange(elem.document.body || elem.document.documentElement), + true)); + } + else { + let range = RangeFind.nodeRange(elem); + let sel = (elem.ownerDocument || elem).defaultView.getSelection(); + if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) { + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + } + + // for imagemap + if (elem instanceof HTMLAreaElement) { + try { + let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat); + + events.dispatch(elem, events.create(elem.ownerDocument, "mouseover", { screenX: x, screenY: y })); + } + catch (e) {} + } + } + }, + + /** + * Find the counth last link on a page matching one of the given + * regular expressions, or with a @rel or @rev attribute matching + * the given relation. Each frame is searched beginning with the + * last link and progressing to the first, once checking for + * matching @rel or @rev properties, and then once for each given + * regular expression. The first match is returned. All frames of + * the page are searched, beginning with the currently focused. + * + * If follow is true, the link is followed. + * + * @param {string} rel The relationship to look for. + * @param {[RegExp]} regexps The regular expressions to search for. + * @param {number} count The nth matching link to follow. + * @param {bool} follow Whether to follow the matching link. + * @param {string} path The CSS to use for the search. @optional + */ + followDocumentRelationship: deprecated("buffer.findLink", + function followDocumentRelationship(rel) { + this.findLink(rel, options[rel + "pattern"], 0, true); + }), + findLink: function findLink(rel, regexps, count, follow, path) { + let selector = path || options.get("hinttags").stringDefaultValue; + + function followFrame(frame) { + function iter(elems) { + for (let i = 0; i < elems.length; i++) + if (elems[i].rel.toLowerCase() === rel || elems[i].rev.toLowerCase() === rel) + yield elems[i]; + } + + let elems = frame.document.getElementsByTagName("link"); + for (let elem in iter(elems)) + yield elem; + + elems = frame.document.getElementsByTagName("a"); + for (let elem in iter(elems)) + yield elem; + + let res = frame.document.querySelectorAll(selector); + for (let regexp in values(regexps)) { + for (let i in util.range(res.length, 0, -1)) { + let elem = res[i]; + if (regexp.test(elem.textContent) === regexp.result || regexp.test(elem.title) === regexp.result || + Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result)) + yield elem; + } + } + } + + for (let frame in values(this.allFrames(null, true))) + for (let elem in followFrame(frame)) + if (count-- === 0) { + if (follow) + this.followLink(elem, dactyl.CURRENT_TAB); + return elem; + } + + if (follow) + dactyl.beep(); + }, + + /** + * Fakes a click on a link. + * + * @param {Node} elem The element to click. + * @param {number} where Where to open the link. See + * {@link dactyl.open}. + */ + followLink: function followLink(elem, where) { + let doc = elem.ownerDocument; + let view = doc.defaultView; + let { left: offsetX, top: offsetY } = elem.getBoundingClientRect(); + + if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement])) + return this.focusElement(elem); + if (isinstance(elem, HTMLLinkElement)) + return dactyl.open(elem.href, where); + + if (elem instanceof HTMLAreaElement) { // for imagemap + let coords = elem.getAttribute("coords").split(","); + offsetX = Number(coords[0]) + 1; + offsetY = Number(coords[1]) + 1; + } + else if (elem instanceof HTMLInputElement && elem.type == "file") { + Buffer.openUploadPrompt(elem); + return; + } + + let ctrlKey = false, shiftKey = false; + switch (where) { + case dactyl.NEW_TAB: + case dactyl.NEW_BACKGROUND_TAB: + ctrlKey = true; + shiftKey = (where != dactyl.NEW_BACKGROUND_TAB); + break; + case dactyl.NEW_WINDOW: + shiftKey = true; + break; + case dactyl.CURRENT_TAB: + break; + } + + this.focusElement(elem); + + prefs.withContext(function () { + prefs.set("browser.tabs.loadInBackground", true); + ["mousedown", "mouseup", "click"].slice(0, util.haveGecko("2b") ? 2 : 3) + .forEach(function (event) { + events.dispatch(elem, events.create(doc, event, { + screenX: offsetX, screenY: offsetY, + ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey + })); + }); + }); + }, + + /** + * @property {nsISelectionController} The current document's selection + * controller. + */ + get selectionController() config.browser.docShell + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController), + + /** + * Opens the appropriate context menu for *elem*. + * + * @param {Node} elem The context element. + */ + openContextMenu: function openContextMenu(elem) { + document.popupNode = elem; + let menu = document.getElementById("contentAreaContextMenu"); + menu.showPopup(elem, -1, -1, "context", "bottomleft", "topleft"); + }, + + /** + * Saves a page link to disk. + * + * @param {HTMLAnchorElement} elem The page link to save. + */ + saveLink: function saveLink(elem) { + let doc = elem.ownerDocument; + let uri = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI)); + let referrer = util.newURI(doc.documentURI, doc.characterSet); + + try { + window.urlSecurityCheck(uri.spec, doc.nodePrincipal); + + io.CommandFileMode("Save link: ", { + onSubmit: function (path) { + let file = io.File(path); + if (file.exists() && file.isDirectory()) + file.append(Buffer.getDefaultNames(elem)[0][0]); + + try { + if (!file.exists()) + file.create(File.NORMAL_FILE_TYPE, octal(644)); + } + catch (e) { + util.assert(false, _("save.invalidDestination", e.name)); + } + + buffer.saveURI(uri, file); + }, + + completer: function (context) completion.savePage(context, elem) + }).open(); + } + catch (e) { + dactyl.echoerr(e); + } + }, + + /** + * Saves the contents of a URI to disk. + * + * @param {nsIURI} uri The URI to save + * @param {nsIFile} file The file into which to write the result. + */ + saveURI: function saveURI(uri, file, callback, self) { + var persist = services.Persist(); + persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE + | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; + + let downloadListener = new window.DownloadListener(window, + services.Transfer(uri, services.io.newFileURI(file), "", + null, null, null, persist)); + + persist.progressListener = update(Object.create(downloadListener), { + onStateChange: function onStateChange(progress, request, flag, status) { + if (callback && (flag & Ci.nsIWebProgressListener.STATE_STOP) && status == 0) + dactyl.trapErrors(callback, self, uri, file, progress, request, flag, status); + + return onStateChange.superapply(this, arguments); + } + }); + + persist.saveURI(uri, null, null, null, null, file); + }, + + /** + * Scrolls the currently active element horizontally. See + * {@link Buffer.scrollHorizontal} for parameters. + */ + scrollHorizontal: function scrollHorizontal(increment, number) + Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number), + + /** + * Scrolls the currently active element vertically. See + * {@link Buffer.scrollVertical} for parameters. + */ + scrollVertical: function scrollVertical(increment, number) + Buffer.scrollVertical(this.findScrollable(number, false), increment, number), + + /** + * Scrolls the currently active element to the given horizontal and + * vertical percentages. See {@link Buffer.scrollToPercent} for + * parameters. + */ + scrollToPercent: function scrollToPercent(horizontal, vertical) + Buffer.scrollToPercent(this.findScrollable(0, vertical == null), horizontal, vertical), + + _scrollByScrollSize: function _scrollByScrollSize(count, direction) { + if (count > 0) + options["scroll"] = count; + this.scrollByScrollSize(direction); + }, + + /** + * Scrolls the buffer vertically 'scroll' lines. + * + * @param {boolean} direction The direction to scroll. If true then + * scroll up and if false scroll down. + * @param {number} count The multiple of 'scroll' lines to scroll. + * @optional + */ + scrollByScrollSize: function scrollByScrollSize(direction, count) { + direction = direction ? 1 : -1; + count = count || 1; + + if (options["scroll"] > 0) + this.scrollVertical("lines", options["scroll"] * direction); + else + this.scrollVertical("pages", direction / 2); + }, + + /** + * Find the best candidate scrollable element for the given + * direction and orientation. + * + * @param {number} dir The direction in which the element must be + * able to scroll. Negative numbers represent up or left, while + * positive numbers represent down or right. + * @param {boolean} horizontal If true, look for horizontally + * scrollable elements, otherwise look for vertically scrollable + * elements. + */ + findScrollable: function findScrollable(dir, horizontal) { + function find(elem) { + while (!(elem instanceof Element) && elem.parentNode) + elem = elem.parentNode; + for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode) + if (Buffer.isScrollable(elem, dir, horizontal)) + break; + return elem; + } + + try { + var elem = this.focusedFrame.document.activeElement; + if (elem == elem.ownerDocument.body) + elem = null; + } + catch (e) {} + + try { + var sel = this.focusedFrame.getSelection(); + } + catch (e) {} + if (!elem && sel && sel.rangeCount) + elem = sel.getRangeAt(0).startContainer; + if (elem) + elem = find(elem); + + if (!(elem instanceof Element)) { + let doc = this.findScrollableWindow().document; + elem = find(doc.body || doc.getElementsByTagName("body")[0] || + doc.documentElement); + } + let doc = this.focusedFrame.document; + return elem || doc.body || doc.documentElement; + }, + + /** + * Find the best candidate scrollable frame in the current buffer. + */ + findScrollableWindow: function findScrollableWindow() { + win = window.document.commandDispatcher.focusedWindow; + if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0)) + return win; + + let win = this.focusedFrame; + if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0)) + return win; + + win = content; + if (win.scrollMaxX > 0 || win.scrollMaxY > 0) + return win; + + for (let frame in array.iterValues(win.frames)) + if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0) + return frame; + + return win; + }, + + // TODO: allow callback for filtering out unwanted frames? User defined? + /** + * Shifts the focus to another frame within the buffer. Each buffer + * contains at least one frame. + * + * @param {number} count The number of frames to skip through. A negative + * count skips backwards. + */ + shiftFrameFocus: function shiftFrameFocus(count) { + if (!(content.document instanceof HTMLDocument)) + return; + + let frames = this.allFrames(); + + if (frames.length == 0) // currently top is always included + return; + + // remove all hidden frames + frames = frames.filter(function (frame) !(frame.document.body instanceof HTMLFrameSetElement)) + .filter(function (frame) !frame.frameElement || + let (rect = frame.frameElement.getBoundingClientRect()) + rect.width && rect.height); + + // find the currently focused frame index + let current = Math.max(0, frames.indexOf(this.focusedFrame)); + + // calculate the next frame to focus + let next = current + count; + if (next < 0 || next >= frames.length) + dactyl.beep(); + next = Math.constrain(next, 0, frames.length - 1); + + // focus next frame and scroll into view + dactyl.focus(frames[next]); + if (frames[next] != content) + frames[next].frameElement.scrollIntoView(false); + + // add the frame indicator + let doc = frames[next].document; + let indicator = util.xmlToDom(
, doc); + (doc.body || doc.documentElement || doc).appendChild(indicator); + + util.timeout(function () { doc.body.removeChild(indicator); }, 500); + + // Doesn't unattach + //doc.body.setAttributeNS(NS.uri, "activeframe", "true"); + //util.timeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500); + }, + + // similar to pageInfo + // TODO: print more useful information, just like the DOM inspector + /** + * Displays information about the specified element. + * + * @param {Node} elem The element to query. + */ + showElementInfo: function showElementInfo(elem) { + dactyl.echo(<>Element:
{util.objectToString(elem, true)}, commandline.FORCE_MULTILINE); + }, + + /** + * Displays information about the current buffer. + * + * @param {boolean} verbose Display more verbose information. + * @param {string} sections A string limiting the displayed sections. + * @default The value of 'pageinfo'. + */ + showPageInfo: function showPageInfo(verbose, sections) { + // Ctrl-g single line output + if (!verbose) { + let file = content.location.pathname.split("/").pop() || "[No Name]"; + let title = content.document.title || "[No Title]"; + + let info = template.map("gf", + function (opt) template.map(buffer.pageInfo[opt].action(), util.identity, ", "), + ", "); + + if (bookmarkcache.isBookmarked(this.URL)) + info += ", bookmarked"; + + let pageInfoText = <>{file.quote()} [{info}] {title}; + dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE); + return; + } + + let list = template.map(sections || options["pageinfo"], function (option) { + let { action, title } = buffer.pageInfo[option]; + return template.table(title, action(true)); + },
); + dactyl.echo(list, commandline.FORCE_MULTILINE); + }, + + /** + * Stops loading and animations in the current content. + */ + stop: function stop() { + if (config.stop) + config.stop(); + else + config.browser.mCurrentBrowser.stop(); + }, + + /** + * Opens a viewer to inspect the source of the currently selected + * range. + */ + viewSelectionSource: function viewSelectionSource() { + // copied (and tuned somewhat) from browser.jar -> nsContextMenu.js + let win = document.commandDispatcher.focusedWindow; + if (win == window) + win = this.focusedFrame; + + let charset = win ? "charset=" + win.document.characterSet : null; + + window.openDialog("chrome://global/content/viewPartialSource.xul", + "_blank", "scrollbars,resizable,chrome,dialog=no", + null, charset, win.getSelection(), "selection"); + }, + + /** + * Opens a viewer to inspect the source of the current buffer or the + * specified *url*. Either the default viewer or the configured external + * editor is used. + * + * @param {string} url The URL of the source. + * @default The current buffer. + * @param {boolean} useExternalEditor View the source in the external editor. + */ + viewSource: function viewSource(url, useExternalEditor) { + let doc = this.focusedFrame.document; + + if (isArray(url)) { + if (options.get("editor").has("line")) + this.viewSourceExternally(url[0] || doc, url[1]); + else + window.openDialog("chrome://global/content/viewSource.xul", + "_blank", "all,dialog=no", + url[0], null, null, url[1]); + } + else { + if (useExternalEditor) + this.viewSourceExternally(url || doc); + else { + url = url || doc.location.href; + const PREFIX = "view-source:"; + if (url.indexOf(PREFIX) == 0) + url = url.substr(PREFIX.length); + else + url = PREFIX + url; + + let sh = history.session; + if (sh[sh.index].URI.spec == url) + window.getWebNavigation().gotoIndex(sh.index); + else + dactyl.open(url, { hide: true }); + } + } + }, + + /** + * Launches an editor to view the source of the given document. The + * contents of the document are saved to a temporary local file and + * removed when the editor returns. This function returns + * immediately. + * + * @param {Document} doc The document to view. + */ + viewSourceExternally: Class("viewSourceExternally", + XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), { + init: function init(doc, callback) { + this.callback = callable(callback) ? callback : + function (file, temp) { + editor.editFileExternally({ file: file.path, line: callback }, + function () { temp && file.remove(false); }); + return true; + }; + + let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href); + + if (!isString(doc)) + return io.withTempFiles(function (temp) { + let encoder = services.HtmlEncoder(); + encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted); + temp.write(encoder.encodeToString(), ">"); + return this.callback(temp, true); + }, this, true); + + let file = util.getFile(uri); + if (file) + this.callback(file, false); + else { + this.file = io.createTempFile(); + var persist = services.Persist(); + persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; + persist.progressListener = this; + persist.saveURI(uri, null, null, null, null, this.file); + } + return null; + }, + + onStateChange: function onStateChange(progress, request, flag, status) { + if ((flag & this.STATE_STOP) && status == 0) { + try { + var ok = this.callback(this.file, true); + } + finally { + if (ok !== true) + this.file.remove(false); + } + } + return 0; + } + }), + + /** + * Increases the zoom level of the current buffer. + * + * @param {number} steps The number of zoom levels to jump. + * @param {boolean} fullZoom Whether to use full zoom or text zoom. + */ + zoomIn: function zoomIn(steps, fullZoom) { + this.bumpZoomLevel(steps, fullZoom); + }, + + /** + * Decreases the zoom level of the current buffer. + * + * @param {number} steps The number of zoom levels to jump. + * @param {boolean} fullZoom Whether to use full zoom or text zoom. + */ + zoomOut: function zoomOut(steps, fullZoom) { + this.bumpZoomLevel(-steps, fullZoom); + }, + + /** + * Adjusts the page zoom of the current buffer to the given absolute + * value. + * + * @param {number} value The new zoom value as a possibly fractional + * percentage of the page's natural size. + * @param {boolean} fullZoom If true, zoom all content of the page, + * including raster images. If false, zoom only text. If omitted, + * use the current zoom function. @optional + * @throws {FailedAssertion} if the given *value* is not within the + * closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX]. + */ + setZoom: function setZoom(value, fullZoom) { + dactyl.assert(value >= Buffer.ZOOM_MIN || value <= Buffer.ZOOM_MAX, + _("zoom.outOfRange", Buffer.ZOOM_MIN, Buffer.ZOOM_MAX)); + + if (fullZoom !== undefined) + ZoomManager.useFullZoom = fullZoom; + try { + ZoomManager.zoom = value / 100; + } + catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) { + return dactyl.echoerr(_("zoom.illegal")); + } + + if ("FullZoom" in window) + FullZoom._applySettingToPref(); + + statusline.updateZoomLevel(value, ZoomManager.useFullZoom); + }, + + /** + * Adjusts the page zoom of the current buffer relative to the + * current zoom level. + * + * @param {number} steps The integral number of natural fractions by + * which to adjust the current page zoom. If positive, the zoom + * level is increased, if negative it is decreased. + * @param {boolean} fullZoom If true, zoom all content of the page, + * including raster images. If false, zoom only text. If omitted, + * use the current zoom function. @optional + * @throws {FailedAssertion} if the buffer's zoom level is already + * at its extreme in the given direction. + */ + bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) { + if (fullZoom === undefined) + fullZoom = ZoomManager.useFullZoom; + + let values = ZoomManager.zoomValues; + let cur = values.indexOf(ZoomManager.snap(ZoomManager.zoom)); + let i = Math.constrain(cur + steps, 0, values.length - 1); + + dactyl.assert(i != cur || fullZoom != ZoomManager.useFullZoom); + + this.setZoom(Math.round(values[i] * 100), fullZoom); + }, + + getAllFrames: deprecated("buffer.allFrames", function getAllFrames() buffer.getAllFrames.apply(buffer, arguments)), + scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() buffer.scrollToPercent(null, 0)), + scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() buffer.scrollToPercent(null, 100)), + scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() buffer.scrollToPercent(0, null)), + scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() buffer.scrollToPercent(100, null)), + scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) buffer.scrollHorizontal("columns", cols)), + scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) buffer.scrollVertical("pages", pages)), + scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) content.scrollTo(x, y)), + textZoom: deprecated("buffer.zoomValue and buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100) +}, { + PageInfo: Struct("PageInfo", "name", "title", "action") + .localize("title"), + + ZOOM_MIN: Class.memoize(function () prefs.get("zoom.minPercent")), + ZOOM_MAX: Class.memoize(function () prefs.get("zoom.maxPercent")), + + setZoom: deprecated("buffer.setZoom", function setZoom() buffer.setZoom.apply(buffer, arguments)), + bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel() buffer.bumpZoomLevel.apply(buffer, arguments)), + + /** + * Returns the currently selected word in *win*. If the selection is + * null, it tries to guess the word that the caret is positioned in. + * + * @returns {string} + */ + currentWord: function currentWord(win) { + let selection = win.getSelection(); + if (selection.rangeCount == 0) + return ""; + + let range = selection.getRangeAt(0).cloneRange(); + if (range.collapsed) { + let re = options.get("iskeyword").regexp; + Editor.extendRange(range, true, re, true); + Editor.extendRange(range, false, re, true); + } + return util.domToString(range); + }, + + getDefaultNames: function getDefaultNames(node) { + let url = node.href || node.src || node.documentURI; + let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase(); + + if (isinstance(node, [Document, HTMLImageElement])) { + let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent) + .getRequest(0).mimeType; + + if (type === "text/plain") + var ext = "." + (currExt || "txt"); + else + ext = "." + services.mime.getPrimaryExtension(type, currExt); + } + else if (currExt) + ext = "." + currExt; + else + ext = ""; + let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/; + + var names = []; + if (node.title) + names.push([node.title, "Page Name"]); + + if (node.alt) + names.push([node.alt, "Alternate Text"]); + + if (!isinstance(node, Document) && node.textContent) + names.push([node.textContent, "Link Text"]); + + names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]); + + return names.filter(function ([leaf, title]) leaf) + .map(function ([leaf, title]) [leaf.replace(util.OS.illegalCharacters, encodeURIComponent) + .replace(re, ext), title]); + }, + + findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow() buffer.findScrollableWindow.apply(buffer, arguments)), + findScrollable: deprecated("buffer.findScrollable", function findScrollable() buffer.findScrollable.apply(buffer, arguments)), + + isScrollable: function isScrollable(elem, dir, horizontal) { + let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight", + overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth"; + if (horizontal) + pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth", + overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth"; + + let style = util.computedStyle(elem); + let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2])); + let realSize = elem[size]; + // Stupid Gecko eccentricities. May fail for quirks mode documents. + if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic. + return false; + if (style[overflow] == "hidden") + realSize += borderSize; + return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max]; + }, + + /** + * Scroll the contents of the given element to the absolute *left* + * and *top* pixel offsets. + * + * @param {Element} elem The element to scroll. + * @param {number|null} left The left absolute pixel offset. If + * null, to not alter the horizontal scroll offset. + * @param {number|null} top The top absolute pixel offset. If + * null, to not alter the vertical scroll offset. + */ + scrollTo: function scrollTo(elem, left, top) { + // Temporary hack. Should be done better. + if (elem.ownerDocument == buffer.focusedFrame.document) + marks.add("'"); + if (left != null) + elem.scrollLeft = left; + if (top != null) + elem.scrollTop = top; + }, + + /** + * Scrolls the currently given element horizontally. + * + * @param {Element} elem The element to scroll. + * @param {string} increment The increment by which to scroll. + * Possible values are: "columns", "pages" + * @param {number} number The possibly fractional number of + * increments to scroll. Positive values scroll to the right while + * negative values scroll to the left. + * @throws {FailedAssertion} if scrolling is not possible in the + * given direction. + */ + scrollHorizontal: function scrollHorizontal(elem, increment, number) { + let fontSize = parseInt(util.computedStyle(elem).fontSize); + if (increment == "columns") + increment = fontSize; // Good enough, I suppose. + else if (increment == "pages") + increment = elem.clientWidth - fontSize; + else + throw Error(); + + let left = elem.dactylScrollDestX !== undefined ? elem.dactylScrollDestX : elem.scrollLeft; + elem.dactylScrollDestX = undefined; + + dactyl.assert(number < 0 ? left > 0 : left < elem.scrollWidth - elem.clientWidth); + Buffer.scrollTo(elem, left + number * increment, null); + }, + + /** + * Scrolls the currently given element vertically. + * + * @param {Element} elem The element to scroll. + * @param {string} increment The increment by which to scroll. + * Possible values are: "lines", "pages" + * @param {number} number The possibly fractional number of + * increments to scroll. Positive values scroll upward while + * negative values scroll downward. + * @throws {FailedAssertion} if scrolling is not possible in the + * given direction. + */ + scrollVertical: function scrollVertical(elem, increment, number) { + let fontSize = parseInt(util.computedStyle(elem).fontSize); + if (increment == "lines") + increment = fontSize; + else if (increment == "pages") + increment = elem.clientHeight - fontSize; + else + throw Error(); + + let top = elem.dactylScrollDestY !== undefined ? elem.dactylScrollDestY : elem.scrollTop; + elem.dactylScrollDestY = undefined; + + dactyl.assert(number < 0 ? top > 0 : top < elem.scrollHeight - elem.clientHeight); + Buffer.scrollTo(elem, null, top + number * increment); + }, + + /** + * Scrolls the currently active element to the given horizontal and + * vertical percentages. + * + * @param {Element} elem The element to scroll. + * @param {number|null} horizontal The possibly fractional + * percentage of the current viewport width to scroll to. If null, + * do not scroll horizontally. + * @param {number|null} vertical The possibly fractional percentage + * of the current viewport height to scroll to. If null, do not + * scroll vertically. + */ + scrollToPercent: function scrollToPercent(elem, horizontal, vertical) { + Buffer.scrollTo(elem, + horizontal == null ? null + : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100), + vertical == null ? null + : (elem.scrollHeight - elem.clientHeight) * (vertical / 100)); + }, + + openUploadPrompt: function openUploadPrompt(elem) { + io.CommandFileMode("Upload file: ", { + onSubmit: function onSubmit(path) { + let file = io.File(path); + dactyl.assert(file.exists()); + + elem.value = file.path; + events.dispatch(elem, events.create(elem.ownerDocument, "change", {})); + } + }).open(elem.value); + } +}, { + commands: function initCommands(dactyl, modules, window) { + commands.add(["frameo[nly]"], + "Show only the current frame's page", + function (args) { + dactyl.open(buffer.focusedFrame.location.href); + }, + { argCount: "0" }); + + commands.add(["ha[rdcopy]"], + "Print current document", + function (args) { + let arg = args[0]; + + // FIXME: arg handling is a bit of a mess, check for filename + dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows, + _("error.trailing")); + + prefs.withContext(function () { + if (arg) { + prefs.set("print.print_to_file", "true"); + prefs.set("print.print_to_filename", io.File(arg.substr(1)).path); + dactyl.echomsg(_("print.toFile", arg.substr(1))); + } + else + dactyl.echomsg(_("print.sending")); + + prefs.set("print.always_print_silent", args.bang); + prefs.set("print.show_print_progress", !args.bang); + + config.browser.contentWindow.print(); + }); + + if (arg) + dactyl.echomsg(_("print.printed", arg.substr(1))); + else + dactyl.echomsg(_("print.sent")); + }, + { + argCount: "?", + bang: true, + literal: 0 + }); + + commands.add(["pa[geinfo]"], + "Show various page information", + function (args) { + let arg = args[0]; + let opt = options.get("pageinfo"); + + dactyl.assert(!arg || opt.validator(opt.parse(arg)), + _("error.invalidArgument", arg)); + buffer.showPageInfo(true, arg); + }, + { + argCount: "?", + completer: function (context) { + completion.optionValue(context, "pageinfo", "+", ""); + context.title = ["Page Info"]; + } + }); + + commands.add(["pagest[yle]", "pas"], + "Select the author style sheet to apply", + function (args) { + let arg = args[0] || ""; + + let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title); + + dactyl.assert(!arg || titles.indexOf(arg) >= 0, + _("error.invalidArgument", arg)); + + if (options["usermode"]) + options["usermode"] = false; + + window.stylesheetSwitchAll(buffer.focusedFrame, arg); + }, + { + argCount: "?", + completer: function (context) completion.alternateStyleSheet(context), + literal: 0 + }); + + commands.add(["re[load]"], + "Reload the current web page", + function (args) { tabs.reload(config.browser.mCurrentTab, args.bang); }, + { + argCount: "0", + bang: true + }); + + // 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 = content.document; + let chosenData = null; + let filename = args[0]; + + let command = commandline.command; + if (filename) { + if (filename[0] == "!") + return buffer.viewSourceExternally(buffer.focusedFrame.document, + function (file) { + let output = io.system(filename.substr(1), file); + commandline.command = command; + commandline.commandOutput({output}); + }); + + if (/^>>/.test(filename)) { + let file = io.File(filename.replace(/^>>\s*/, "")); + dactyl.assert(args.bang || file.exists() && file.isWritable(), + _("io.notWriteable", file.path.quote())); + return buffer.viewSourceExternally(buffer.focusedFrame.document, + function (tmpFile) { + try { + file.write(tmpFile, ">>"); + } + catch (e) { + dactyl.echoerr(_("io.notWriteable", file.path.quote())); + } + }); + } + + let file = io.File(filename.replace(RegExp(File.PATH_SEP + "*$"), "")); + + if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory()) + file.append(Buffer.getDefaultNames(doc)[0][0]); + + dactyl.assert(args.bang || !file.exists(), _("io.exists")); + + chosenData = { file: file, uri: util.newURI(doc.location.href) }; + } + + // 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? + prefs.set("browser.download.lastDir", io.cwd.path); + + try { + var contentDisposition = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.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) { + if (context.filter[0] == "!") + return; + if (/^>>/.test(context.filter)) + context.advance(/^>>\s*/.exec(context.filter)[0].length); + + completion.savePage(context, content.document); + context.fork("file", 0, completion, "file"); + }, + literal: 0 + }); + + commands.add(["st[op]"], + "Stop loading the current web page", + function () { buffer.stop(); }, + { 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 arg = args[0]; + let level; + + if (!arg) + level = 100; + else if (/^\d+$/.test(arg)) + level = parseInt(arg, 10); + else if (/^[+-]\d+$/.test(arg)) { + level = Math.round(buffer.zoomLevel + parseInt(arg, 10)); + level = Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX); + } + else + dactyl.assert(false, _("error.trailing")); + + buffer.setZoom(level, args.bang); + }, + { + argCount: "?", + bang: true + }); + }, + completion: function initCompletion(dactyl, modules, window) { + completion.alternateStyleSheet = function alternateStylesheet(context) { + context.title = ["Stylesheet", "Location"]; + + // unify split style sheets + let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject(); + + buffer.alternateStyleSheets.forEach(function (style) { + styles[style.title].push(style.href || "inline"); + }); + + context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))]; + }; + + completion.buffer = function buffer(context) { + let filter = context.filter.toLowerCase(); + let defItem = { parent: { getTitle: function () "" } }; + let tabGroups = {}; + tabs.getGroups(); + tabs.allTabs.forEach(function (tab, i) { + let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent; + if (!set.has(tabGroups, group.id)) + tabGroups[group.id] = [group.getTitle(), []]; + group = tabGroups[group.id]; + group[1].push([i, tab.linkedBrowser]); + }); + + context.pushProcessor(0, function (item, text, next) <> + {item.indicator} + { next.call(this, item, text) } + ); + context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter)); + + context.anchored = false; + context.keys = { + text: "text", + description: "url", + indicator: function (item) item.tab === tabs.getTab() ? "%" : + item.tab === tabs.alternate ? "#" : " ", + icon: "icon", + id: "id", + command: function () "tabs.select" + }; + context.compare = CompletionContext.Sort.number; + context.filters = [CompletionContext.Filter.textDescription]; + + for (let [id, vals] in Iterator(tabGroups)) + context.fork(id, 0, this, function (context, [name, browsers]) { + context.title = [name || "Buffers"]; + context.generate = function () + Array.map(browsers, function ([i, browser]) { + let indicator = " "; + if (i == tabs.index()) + indicator = "%"; + else if (i == tabs.index(tabs.alternate)) + indicator = "#"; + + let tab = tabs.getTab(i); + let url = browser.contentDocument.location.href; + i = i + 1; + + return { + text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url], + tab: tab, + id: i - 1, + url: url, + icon: tab.image || DEFAULT_FAVICON + }; + }); + }, vals); + }; + + completion.savePage = function savePage(context, node) { + context.fork("generated", context.filter.replace(/[^/]*$/, "").length, + this, function (context) { + context.completions = Buffer.getDefaultNames(node); + }); + }; + }, + events: function initEvents(dactyl, modules, window) { + events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false); + }, + mappings: function initMappings(dactyl, modules, window) { + mappings.add([modes.NORMAL], + ["y", ""], "Yank current location to the clipboard", + function () { dactyl.clipboardWrite(buffer.uri.spec, true); }); + + mappings.add([modes.NORMAL], + [""], "Increment last number in URL", + function (args) { buffer.incrementURL(Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.NORMAL], + [""], "Decrement last number in URL", + function (args) { buffer.incrementURL(-Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.NORMAL], ["gu"], + "Go to parent directory", + function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.NORMAL], ["gU"], + "Go to the root of the website", + function () { buffer.climbUrlPath(-1); }); + + mappings.add([modes.COMMAND], [".", ""], + "Repeat the last key event", + function (args) { + if (mappings.repeat) { + for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100)) + mappings.repeat(); + } + }, + { count: true }); + + mappings.add([modes.COMMAND], ["i", ""], + "Start caret mode", + function () { modes.push(modes.CARET); }); + + mappings.add([modes.COMMAND], [""], + "Stop loading the current web page", + function () { ex.stop(); }); + + // scrolling + mappings.add([modes.COMMAND], ["j", "", "", ""], + "Scroll document down", + function (args) { buffer.scrollVertical("lines", Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["k", "", "", ""], + "Scroll document up", + function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", ""] : ["h", "", ""], + "Scroll document to the left", + function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], dactyl.has("mail") ? ["l", ""] : ["l", "", ""], + "Scroll document to the right", + function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["0", "^", ""], + "Scroll to the absolute left of the document", + function () { buffer.scrollToPercent(0, null); }); + + mappings.add([modes.COMMAND], ["$", ""], + "Scroll to the absolute right of the document", + function () { buffer.scrollToPercent(100, null); }); + + mappings.add([modes.COMMAND], ["gg", ""], + "Go to the top of the document", + function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); }, + { count: true }); + + mappings.add([modes.COMMAND], ["G", ""], + "Go to the end of the document", + function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 100); }, + { count: true }); + + mappings.add([modes.COMMAND], ["%", ""], + "Scroll to {count} percent of the document", + function (args) { + dactyl.assert(args.count > 0 && args.count <= 100); + buffer.scrollToPercent(null, args.count); + }, + { count: true }); + + mappings.add([modes.COMMAND], ["", ""], + "Scroll window downwards in the buffer", + function (args) { buffer._scrollByScrollSize(args.count, true); }, + { count: true }); + + mappings.add([modes.COMMAND], ["", ""], + "Scroll window upwards in the buffer", + function (args) { buffer._scrollByScrollSize(args.count, false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["", "", "", ""], + "Scroll up a full page", + function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["", "", "", ""], + "Scroll down a full page", + function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["]f", ""], + "Focus next frame", + function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["[f", ""], + "Focus previous frame", + function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.COMMAND], ["]]", ""], + "Follow the link labeled 'next' or '>' if it exists", + function (args) { + buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true); + }, + { count: true }); + + mappings.add([modes.COMMAND], ["[[", ""], + "Follow the link labeled 'prev', 'previous' or '<' if it exists", + function (args) { + buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true); + }, + { count: true }); + + mappings.add([modes.COMMAND], ["gf", ""], + "Toggle between rendered and source view", + function () { buffer.viewSource(null, false); }); + + mappings.add([modes.COMMAND], ["gF", ""], + "View source with an external editor", + function () { buffer.viewSource(null, true); }); + + mappings.add([modes.COMMAND], ["gi", ""], + "Focus last used input field", + function (args) { + let elem = buffer.lastInputField; + + if (args.count >= 1 || !elem || !events.isContentNode(elem)) { + let xpath = ["frame", "iframe", "input", "textarea[not(@disabled) and not(@readonly)]"]; + + let frames = buffer.allFrames(null, true); + + let elements = array.flatten(frames.map(function (win) [m for (m in util.evaluateXPath(xpath, win.document))])) + .filter(function (elem) { + if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement])) + return Editor.getEditor(elem.contentWindow); + + if (elem.readOnly || elem instanceof HTMLInputElement && !set.has(util.editableInputs, elem.type)) + return false; + + let computedStyle = util.computedStyle(elem); + let rect = elem.getBoundingClientRect(); + return computedStyle.visibility != "hidden" && computedStyle.display != "none" && + computedStyle.MozUserFocus != "ignore" && rect.width && rect.height; + }); + + dactyl.assert(elements.length > 0); + elem = elements[Math.constrain(args.count, 1, elements.length) - 1]; + } + buffer.focusElement(elem); + util.scrollIntoView(elem); + }, + { count: true }); + + mappings.add([modes.COMMAND], ["gP"], + "Open (]put) a URL based on the current clipboard contents in a new buffer", + function () { + let url = dactyl.clipboardRead(); + dactyl.assert(url, _("error.clipboardEmpty")); + dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB, background: true }); + }); + + mappings.add([modes.COMMAND], ["p", "", ""], + "Open (put) a URL based on the current clipboard contents in the current buffer", + function () { + let url = dactyl.clipboardRead(); + dactyl.assert(url, _("error.clipboardEmpty")); + dactyl.open(url); + }); + + mappings.add([modes.COMMAND], ["P", ""], + "Open (put) a URL based on the current clipboard contents in a new buffer", + function () { + let url = dactyl.clipboardRead(); + dactyl.assert(url, _("error.clipboardEmpty")); + dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB }); + }); + + // reloading + mappings.add([modes.COMMAND], ["r", ""], + "Reload the current web page", + function () { tabs.reload(tabs.getTab(), false); }); + + mappings.add([modes.COMMAND], ["R", ""], + "Reload while skipping the cache", + function () { tabs.reload(tabs.getTab(), true); }); + + // yanking + mappings.add([modes.COMMAND], ["Y", ""], + "Copy selected text or current word", + function () { + let sel = buffer.currentWord; + dactyl.assert(sel); + dactyl.clipboardWrite(sel, true); + }); + + // zooming + mappings.add([modes.COMMAND], ["zi", "+", ""], + "Enlarge text zoom of current web page", + function (args) { buffer.zoomIn(Math.max(args.count, 1), false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["zm", ""], + "Enlarge text zoom of current web page by a larger amount", + function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["zo", "-", ""], + "Reduce text zoom of current web page", + function (args) { buffer.zoomOut(Math.max(args.count, 1), false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["zr", ""], + "Reduce text zoom of current web page by a larger amount", + function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["zz", ""], + "Set text zoom value of current web page", + function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); }, + { count: true }); + + mappings.add([modes.COMMAND], ["ZI", "zI", ""], + "Enlarge full zoom of current web page", + function (args) { buffer.zoomIn(Math.max(args.count, 1), true); }, + { count: true }); + + mappings.add([modes.COMMAND], ["ZM", "zM", ""], + "Enlarge full zoom of current web page by a larger amount", + function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); }, + { count: true }); + + mappings.add([modes.COMMAND], ["ZO", "zO", ""], + "Reduce full zoom of current web page", + function (args) { buffer.zoomOut(Math.max(args.count, 1), true); }, + { count: true }); + + mappings.add([modes.COMMAND], ["ZR", "zR", ""], + "Reduce full zoom of current web page by a larger amount", + function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); }, + { count: true }); + + mappings.add([modes.COMMAND], ["zZ", ""], + "Set full zoom value of current web page", + function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); }, + { count: true }); + + // page info + mappings.add([modes.COMMAND], ["", ""], + "Print the current file name", + function () { buffer.showPageInfo(false); }); + + mappings.add([modes.COMMAND], ["g", ""], + "Print file information", + function () { buffer.showPageInfo(true); }); + }, + options: function initOptions(dactyl, modules, window) { + options.add(["encoding", "enc"], + "The current buffer's character encoding", + "string", "UTF-8", + { + scope: Option.SCOPE_LOCAL, + getter: function () config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset, + setter: function (val) { + if (options["encoding"] == val) + return val; + + // Stolen from browser.jar/content/browser/browser.js, more or less. + try { + config.browser.docShell.QueryInterface(Ci.nsIDocCharset).charset = val; + PlacesUtils.history.setCharsetForURI(getWebNavigation().currentURI, val); + getWebNavigation().reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); + } + catch (e) { dactyl.reportError(e); } + return null; + }, + completer: function (context) completion.charset(context) + }); + + options.add(["iskeyword", "isk"], + "Regular expression defining which characters constitute word characters", + "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]', + { + setter: function (value) { + this.regexp = util.regexp(value); + return value; + }, + validator: function (value) RegExp(value) + }); + + options.add(["nextpattern"], + "Patterns to use when guessing the next page in a document sequence", + "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"), + { regexpFlags: "i" }); + + options.add(["previouspattern"], + "Patterns to use when guessing the previous page in a document sequence", + "regexplist", UTF8("'\\bprev|previous\\b',^<$,^(<<|«)$,^(<|«),(<|«)$"), + { regexpFlags: "i" }); + + options.add(["pageinfo", "pa"], + "Define which sections are shown by the :pageinfo command", + "charlist", "gfm", + { get values() values(buffer.pageInfo).toObject() }); + + options.add(["scroll", "scr"], + "Number of lines to scroll with and commands", + "number", 0, + { validator: function (value) value >= 0 }); + + options.add(["showstatuslinks", "ssli"], + "Where to show the destination of the link under the cursor", + "string", "status", + { + values: { + "": "Don't show link destinations", + "status": "Show link destinations in the status line", + "command": "Show link destinations in the command line" + } + }); + + options.add(["usermode", "um"], + "Show current website without styling defined by the author", + "boolean", false, + { + setter: function (value) config.browser.markupDocumentViewer.authorStyleDisabled = value, + getter: function () config.browser.markupDocumentViewer.authorStyleDisabled + }); + } +}); + +// 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 0000000..6e05b7e --- /dev/null +++ b/common/content/buffer.xhtml @@ -0,0 +1,9 @@ + + + + + + </head> + <body/> +</html> diff --git a/common/content/commandline.js b/common/content/commandline.js new file mode 100644 index 0000000..69e6a3c --- /dev/null +++ b/common/content/commandline.js @@ -0,0 +1,1818 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org> +// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com> +// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +var CommandWidgets = Class("CommandWidgets", { + depends: ["statusline"], + + init: function init() { + let s = "dactyl-statusline-field-"; + + XML.ignoreWhitespace = true; + util.overlayWindow(window, { + objects: { + eventTarget: commandline + }, + append: <e4x xmlns={XUL} xmlns:dactyl={NS}> + <vbox id={config.commandContainer}> + <vbox class="dactyl-container" hidden="false" collapsed="true"> + <iframe class="dactyl-completions" id="dactyl-completions-dactyl-commandline" src="dactyl://content/buffer.xhtml" + contextmenu="dactyl-contextmenu" + flex="1" hidden="false" collapsed="false" + highlight="Events" events="mowEvents" /> + </vbox> + + <stack orient="horizontal" align="stretch" class="dactyl-container" id="dactyl-container" highlight="CmdLine CmdCmdLine"> + <textbox class="plain" id="dactyl-strut" flex="1" crop="end" collapsed="true"/> + <textbox class="plain" id="dactyl-mode" flex="1" crop="end"/> + <textbox class="plain" id="dactyl-message" flex="1" readonly="true"/> + + <hbox id="dactyl-commandline" hidden="false" class="dactyl-container" highlight="Normal CmdNormal" collapsed="true"> + <label id="dactyl-commandline-prompt" class="dactyl-commandline-prompt plain" flex="0" crop="end" value="" collapsed="true"/> + <textbox id="dactyl-commandline-command" class="dactyl-commandline-command plain" flex="1" type="input" timeout="100" + highlight="Events" /> + </hbox> + </stack> + + <vbox class="dactyl-container" hidden="false" collapsed="false" highlight="CmdLine"> + <textbox id="dactyl-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true" + highlight="Normal Events" events="multilineInputEvents" /> + </vbox> + </vbox> + + <stack id="dactyl-statusline-stack"> + <hbox id={s + "commandline"} hidden="false" class="dactyl-container" highlight="Normal StatusNormal" collapsed="true"> + <label id={s + "commandline-prompt"} class="dactyl-commandline-prompt plain" flex="0" crop="end" value="" collapsed="true"/> + <textbox id={s + "commandline-command"} class="dactyl-commandline-command plain" flex="1" type="text" timeout="100" + highlight="Events" /> + </hbox> + </stack> + </e4x>.elements(), + + before: <e4x xmlns={XUL} xmlns:dactyl={NS}> + <toolbar id={statusline.statusBar.id}> + <vbox id={"dactyl-completions-" + s + "commandline-container"} class="dactyl-container" hidden="false" collapsed="true"> + <iframe class="dactyl-completions" id={"dactyl-completions-" + s + "commandline"} src="dactyl://content/buffer.xhtml" + contextmenu="dactyl-contextmenu" flex="1" hidden="false" collapsed="false" + highlight="Events" events="mowEvents" /> + </vbox> + </toolbar> + </e4x>.elements() + }); + + this.elements = {}; + + this.addElement({ + name: "container", + noValue: true + }); + + this.addElement({ + name: "commandline", + getGroup: function () options.get("guioptions").has("C") ? this.commandbar : this.statusbar, + getValue: function () this.command + }); + + this.addElement({ + name: "strut", + defaultGroup: "Normal", + getGroup: function () this.commandbar, + getValue: function () options.get("guioptions").has("c") + }); + + this.addElement({ + name: "command", + test: function test(stack, prev) stack.pop && !isinstance(prev.main, modes.COMMAND_LINE), + id: "commandline-command", + get: function command_get(elem) { + // The long path is because of complications with the + // completion preview. + try { + return elem.inputField.editor.rootElement.firstChild.textContent; + } + catch (e) { + return elem.value; + } + }, + getElement: CommandWidgets.getEditor, + getGroup: function (value) this.activeGroup.commandline, + onChange: function command_onChange(elem, value) { + if (elem.inputField != dactyl.focusedElement) + try { + elem.selectionStart = elem.value.length; + elem.selectionEnd = elem.value.length; + } + catch (e) {} + + if (!elem.collapsed) + dactyl.focus(elem); + }, + onVisibility: function command_onVisibility(elem, visible) { + if (visible) + dactyl.focus(elem); + } + }); + + this.addElement({ + name: "prompt", + id: "commandline-prompt", + defaultGroup: "CmdPrompt", + getGroup: function () this.activeGroup.commandline + }); + + this.addElement({ + name: "message", + defaultGroup: "Normal", + getElement: CommandWidgets.getEditor, + getGroup: function (value) { + if (this.command && !options.get("guioptions").has("M")) + return this.statusbar; + + let statusElem = this.statusbar.message; + if (value && !value[2] && statusElem.editor && statusElem.editor.rootElement.scrollWidth > statusElem.scrollWidth) + return this.commandbar; + return this.activeGroup.mode; + } + }); + + this.addElement({ + name: "mode", + defaultGroup: "ModeMsg", + getGroup: function (value) { + if (!options.get("guioptions").has("M")) + if (this.commandbar.container.clientHeight == 0 || + value && !this.commandbar.commandline.collapsed) + return this.statusbar; + return this.commandbar; + } + }); + }, + addElement: function addElement(obj) { + const self = this; + this.elements[obj.name] = obj; + + function get(prefix, map, id) (obj.getElement || util.identity)(map[id] || document.getElementById(prefix + id)); + + this.active.__defineGetter__(obj.name, function () self.activeGroup[obj.name][obj.name]); + this.activeGroup.__defineGetter__(obj.name, function () self.getGroup(obj.name)); + + memoize(this.statusbar, obj.name, function () get("dactyl-statusline-field-", statusline.widgets, (obj.id || obj.name))); + memoize(this.commandbar, obj.name, function () get("dactyl-", {}, (obj.id || obj.name))); + + if (!(obj.noValue || obj.getValue)) { + Object.defineProperty(this, obj.name, Modes.boundProperty({ + test: obj.test, + + get: function get_widgetValue() { + let elem = self.getGroup(obj.name, obj.value)[obj.name]; + if (obj.value != null) + return [obj.value[0], + obj.get ? obj.get.call(this, elem) : elem.value] + .concat(obj.value.slice(2)) + return null; + }, + + set: function set_widgetValue(val) { + if (val != null && !isArray(val)) + val = [obj.defaultGroup || "", val]; + obj.value = val; + + [this.commandbar, this.statusbar].forEach(function (nodeSet) { + let elem = nodeSet[obj.name]; + if (val == null) + elem.value = ""; + else { + highlight.highlightNode(elem, + (val[0] != null ? val[0] : obj.defaultGroup) + .split(/\s/).filter(util.identity) + .map(function (g) g + " " + nodeSet.group + g) + .join(" ")); + elem.value = val[1]; + if (obj.onChange) + obj.onChange.call(this, elem, val); + } + }, this); + + this.updateVisibility(); + return val; + } + }).init(obj.name)); + } + else if (obj.defaultGroup) { + [this.commandbar, this.statusbar].forEach(function (nodeSet) { + let elem = nodeSet[obj.name]; + if (elem) + highlight.highlightNode(elem, obj.defaultGroup.split(/\s/) + .map(function (g) g + " " + nodeSet.group + g).join(" ")); + }); + } + }, + + getGroup: function getgroup(name, value) { + if (!statusline.visible) + return this.commandbar; + return this.elements[name].getGroup.call(this, arguments.length > 1 ? value : this[name]); + }, + + updateVisibility: function updateVisibility() { + for (let elem in values(this.elements)) + if (elem.getGroup) { + let value = elem.getValue ? elem.getValue.call(this) + : elem.noValue || this[elem.name]; + + let activeGroup = this.getGroup(elem.name, value); + for (let group in values([this.commandbar, this.statusbar])) { + let meth, node = group[elem.name]; + let visible = (value && group === activeGroup); + if (node && !node.collapsed == !visible) { + node.collapsed = !visible; + if (elem.onVisibility) + elem.onVisibility.call(this, node, visible); + } + } + } + + // Hack. Collapse hidden elements in the stack. + // Might possibly be better to use a deck and programmatically + // choose which element to select. + function check(node) { + if (util.computedStyle(node).display === "-moz-stack") { + let nodes = Array.filter(node.children, function (n) !n.collapsed && n.boxObject.height); + nodes.forEach(function (node, i) node.style.opacity = (i == nodes.length - 1) ? "" : "0"); + } + Array.forEach(node.children, check); + } + [this.commandbar.container, this.statusbar.container].forEach(check); + }, + + active: Class.memoize(Object), + activeGroup: Class.memoize(Object), + commandbar: Class.memoize(function () ({ group: "Cmd" })), + statusbar: Class.memoize(function () ({ group: "Status" })), + + _ready: function _ready(elem) { + return elem.contentDocument.documentURI === elem.getAttribute("src") && + ["viewable", "complete"].indexOf(elem.contentDocument.readyState) >= 0; + }, + + _whenReady: function _whenReady(id, init) { + let elem = document.getElementById(id); + while (!this._ready(elem)) + yield 10; + + if (init) + init.call(this, elem); + yield elem; + }, + + completionContainer: Class.memoize(function () this.completionList.parentNode), + + contextMenu: Class.memoize(function () { + ["copy", "copylink", "selectall"].forEach(function (tail) { + // some host apps use "hostPrefixContext-copy" ids + let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]"; + document.getElementById("dactyl-context-" + tail).style.listStyleImage = + util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage; + }); + return document.getElementById("dactyl-contextmenu"); + }), + + multilineOutput: Class.memoize(function () this._whenReady("dactyl-multiline-output", function (elem) { + elem.contentWindow.addEventListener("unload", function (event) { event.preventDefault(); }, true); + elem.contentDocument.documentElement.id = "dactyl-multiline-output-top"; + elem.contentDocument.body.id = "dactyl-multiline-output-content"; + }), true), + + multilineInput: Class.memoize(function () document.getElementById("dactyl-multiline-input")), + + mowContainer: Class.memoize(function () document.getElementById("dactyl-multiline-output-container")) +}, { + getEditor: function getEditor(elem) { + elem.inputField.QueryInterface(Ci.nsIDOMNSEditableElement); + return elem; + } +}); + +var CommandMode = Class("CommandMode", { + init: function init() { + this.keepCommand = userContext.hidden_option_command_afterimage; + }, + + get command() this.widgets.command[1], + set command(val) this.widgets.command = val, + + get prompt() this.widgets.prompt, + set prompt(val) this.widgets.prompt = val, + + open: function (command) { + dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE), + "Not opening command line in non-command-line mode."); + + this.messageCount = commandline.messageCount; + modes.push(this.mode, this.extendedMode, this.closure); + + this.widgets.active.commandline.collapsed = false; + this.widgets.prompt = this.prompt; + this.widgets.command = command || ""; + + this.input = this.widgets.active.command.inputField; + if (this.historyKey) + this.history = CommandLine.History(this.input, this.historyKey, this); + + if (this.complete) + this.completions = CommandLine.Completions(this.input, this); + + if (this.completions && command && commandline.commandSession === this) + this.completions.autocompleteTimer.flush(true); + }, + + get active() this === commandline.commandSession, + + get holdFocus() this.widgets.active.command.inputField, + + get mappingSelf() this, + + get widgets() commandline.widgets, + + enter: function (stack) { + commandline.commandSession = this; + if (stack.pop && commandline.command) { + this.onChange(commandline.command); + if (this.completions && stack.pop) + this.completions.complete(true, false); + } + }, + + leave: function (stack) { + if (!stack.push) { + commandline.commandSession = null; + this.input.dactylKeyPress = undefined; + + if (this.completions) + this.completions.cleanup(); + + if (this.history) + this.history.save(); + + this.resetCompletions(); + commandline.hideCompletions(); + + modes.delay(function () { + if (!this.keepCommand || commandline.silent || commandline.quiet) + commandline.hide(); + this[this.accepted ? "onSubmit" : "onCancel"](commandline.command); + if (commandline.messageCount === this.messageCount) + commandline.clearMessage(); + }, this); + } + }, + + events: { + input: function onInput(event) { + if (this.completions) { + this.resetCompletions(); + + this.completions.autocompleteTimer.tell(false); + } + this.onChange(commandline.command); + }, + keyup: function onKeyUp(event) { + let key = events.toString(event); + if (/-?Tab>$/.test(key) && this.completions) + this.completions.tabTimer.flush(); + } + }, + + keepCommand: false, + + onKeyPress: function onKeyPress(events) { + if (this.completions) + this.completions.previewClear(); + + return true; /* Pass event */ + }, + + onCancel: function (value) { + }, + + onChange: function (value) { + }, + + onSubmit: function (value) { + }, + + resetCompletions: function resetCompletions() { + if (this.completions) { + this.completions.context.cancelAll(); + this.completions.wildIndex = -1; + this.completions.previewClear(); + } + if (this.history) + this.history.reset(); + }, +}); + +var CommandExMode = Class("CommandExMode", CommandMode, { + + get mode() modes.EX, + + historyKey: "command", + + prompt: ["Normal", ":"], + + complete: function complete(context) { + context.fork("ex", 0, completion, "ex"); + }, + + onSubmit: function onSubmit(command) { + contexts.withContext({ file: "[Command Line]", line: 1 }, + function _onSubmit() { + io.withSavedValues(["readHeredoc"], function _onSubmit() { + this.readHeredoc = commandline.readHeredoc; + commands.repeat = command; + dactyl.execute(command); + }); + }); + } +}); + +var CommandPromptMode = Class("CommandPromptMode", CommandMode, { + init: function init(prompt, params) { + this.prompt = isArray(prompt) ? prompt : ["Question", prompt]; + update(this, params); + init.supercall(this); + }, + + complete: function (context) { + if (this.completer) + context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1)); + }, + + get mode() modes.PROMPT +}); + +/** + * 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. + */ +var CommandLine = Module("commandline", { + init: function init() { + const self = this; + + this._callbacks = {}; + + memoize(this, "_store", function () storage.newMap("command-history", { store: true, privateData: true })); + + for (let name in values(["command", "search"])) + if (storage.exists("history-" + name)) { + let ary = storage.newArray("history-" + name, { store: true, privateData: true }); + + this._store.set(name, [v for ([k, v] in ary)]); + ary.delete(); + this._store.changed(); + } + + this._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, + + clear: function clear() { + this._messages = []; + }, + + filter: function filter(fn, self) { + this._messages = this._messages.filter(fn, self); + }, + + add: function add(message) { + if (!message) + return; + + if (this._messages.length >= options["messages"]) + this._messages.shift(); + + this._messages.push(update({ + timestamp: Date.now() + }, message)); + } + }; //}}} + }, + + signals: { + "browser.locationChange": function (webProgress, request, uri) { + this.clear(); + } + }, + + /** + * Determines whether the command line should be visible. + * + * @returns {boolean} + */ + get commandVisible() !!this.commandSession, + + /** + * Ensure that the multiline input widget is the correct size. + */ + _autosizeMultilineInputWidget: function _autosizeMultilineInputWidget() { + let lines = this.widgets.multilineInput.value.split("\n").length - 1; + + this.widgets.multilineInput.setAttribute("rows", Math.max(lines, 1)); + }, + + 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. + ACTIVE_WINDOW : 1 << 4, // Only echo in active window. + + get completionContext() this._completions.context, + + _silent: false, + get silent() this._silent, + set silent(val) { + this._silent = val; + this.quiet = this.quiet; + }, + + _quite: false, + get quiet() this._quiet, + set quiet(val) { + this._quiet = val; + ["commandbar", "statusbar"].forEach(function (nodeSet) { + Array.forEach(this.widgets[nodeSet].commandline.children, function (node) { + node.style.opacity = this._quiet || this._silent ? "0" : ""; + }, this); + }, this); + }, + + widgets: Class.memoize(function () CommandWidgets()), + + runSilently: function runSilently(func, self) { + this.withSavedValues(["silent"], function () { + this.silent = true; + func.call(self); + }); + }, + + get completionList() { + let node = this.widgets.active.commandline; + if (!node.completionList) { + let elem = document.getElementById("dactyl-completions-" + node.id); + util.waitFor(bind(this.widgets._ready, null, elem)); + + node.completionList = ItemList(elem.id); + } + return node.completionList; + }, + + hideCompletions: function hideCompletions() { + for (let nodeSet in values([this.widgets.statusbar, this.widgets.commandbar])) + if (nodeSet.commandline.completionList) + nodeSet.commandline.completionList.visible = false; + }, + + _lastClearable: Modes.boundProperty(), + messages: Modes.boundProperty(), + + multilineInputVisible: Modes.boundProperty({ + set: function set_miwVisible(value) { this.widgets.multilineInput.collapsed = !value; } + }), + + get command() { + if (this.commandVisible && this.widgets.command) + return commands.lastCommand = this.widgets.command[1]; + return commands.lastCommand; + }, + set command(val) { + if (this.commandVisible && (modes.extended & modes.EX)) + return this.widgets.command = val; + return commands.lastCommand = val; + }, + + clear: function clear(scroll) { + if (!scroll || Date.now() - this._lastEchoTime > 5000) + this.clearMessage(); + this._lastEchoTime = 0; + + if (!this.commandSession) { + this.widgets.command = null; + this.hideCompletions(); + } + + if (modes.main == modes.OUTPUT_MULTILINE && !mow.isScrollable(1)) + modes.pop(); + + if (!modes.have(modes.OUTPUT_MULTILINE)) + mow.visible = false; + }, + + clearMessage: function clearMessage() { + if (this.widgets.message && this.widgets.message[1] === this._lastClearable) + this.widgets.message = null; + }, + + /** + * Displays the multi-line output of a command, preceded by the last + * executed ex command string. + * + * @param {XML} xml The output as an E4X XML object. + */ + commandOutput: function commandOutput(xml) { + XML.ignoreWhitespace = false; + XML.prettyPrinting = false; + if (this.command) + this.echo(<>:{this.command}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE); + this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE); + this.command = null; + }, + + /** + * Hides the command line, and shows any status messages that + * are under it. + */ + hide: function hide() { + this.widgets.command = null; + }, + + /** + * Display a message in the command-line area. + * + * @param {string} str + * @param {string} highlightGroup + * @param {boolean} forceSingle If provided, don't let over-long + * messages move to the MOW. + */ + _echoLine: function echoLine(str, highlightGroup, forceSingle, silent) { + this.widgets.message = str ? [highlightGroup, str, forceSingle] : null; + + dactyl.triggerObserver("echoLine", str, highlightGroup, null, forceSingle); + + if (!this.commandVisible) + this.hide(); + + let field = this.widgets.active.message.inputField; + if (field.value && !forceSingle && field.editor.rootElement.scrollWidth > field.scrollWidth) { + this.widgets.message = null; + mow.echo(<span highlight="Message">{str}</span>, highlightGroup, true); + } + }, + + _lastEcho: null, + + /** + * Output the given string onto the command line. With no flags, the + * message will be shown in the status line if it's short enough to + * fit, and contains no new lines, and isn't XML. Otherwise, it will be + * shown in the MOW. + * + * @param {string} str + * @param {string} highlightGroup The Highlight group for the + * message. + * @default "Normal" + * @param {number} flags Changes the behavior as follows: + * commandline.APPEND_TO_MESSAGES - Causes message to be added to the + * messages history, and shown by :messages. + * commandline.FORCE_SINGLELINE - Forbids the command from being + * pushed to the MOW if it's too long or of there are already + * status messages being shown. + * commandline.DISALLOW_MULTILINE - Cancels the operation if the MOW + * is already visible. + * commandline.FORCE_MULTILINE - Forces the message to appear in + * the MOW. + */ + messageCount: 0, + echo: function echo(data, highlightGroup, flags) { + // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst + if (this._silent || !this.widgets) + return; + + this.messageCount++; + + highlightGroup = highlightGroup || this.HL_NORMAL; + + if (flags & this.APPEND_TO_MESSAGES) { + let message = isObject(data) ? data : { message: data }; + this._messageHistory.add(update({ highlight: highlightGroup }, message)); + data = message.message; + } + + if ((flags & this.ACTIVE_WINDOW) && + window != services.windowWatcher.activeWindow && + services.windowWatcher.activeWindow.dactyl) + return; + + if ((flags & this.DISALLOW_MULTILINE) && !this.widgets.mowContainer.collapsed) + return; + + let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); + let action = this._echoLine; + + if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isString(data)) && !(flags & this.FORCE_SINGLELINE)) + action = mow.closure.echo; + + if (single) + this._lastEcho = null; + else { + if (this.widgets.message && this.widgets.message[1] == this._lastEcho) + mow.echo(<span highlight="Message">{this._lastEcho}</span>, + this.widgets.message[0], true); + + if (action === this._echoLine && !(flags & this.FORCE_MULTILINE) + && !(dactyl.fullyInitialized && this.widgets.mowContainer.collapsed)) { + highlightGroup += " Message"; + action = mow.closure.echo; + } + this._lastEcho = (action == this._echoLine) && data; + } + + this._lastClearable = action === this._echoLine && String(data); + this._lastEchoTime = (flags & this.FORCE_SINGLELINE) && Date.now(); + + if (action) + action.call(this, data, highlightGroup, single); + }, + _lastEchoTime: 0, + + /** + * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may + * pop at any time to close the prompt. + * + * @param {string} prompt The input prompt to use. + * @param {function(string)} callback + * @param {Object} extra + * @... {function} onChange - A function to be called with the current + * input every time it changes. + * @... {function(CompletionContext)} completer - A completion function + * for the user's input. + * @... {string} promptHighlight - The HighlightGroup used for the + * prompt. @default "Question" + * @... {string} default - The initial value that will be returned + * if the user presses <CR> straightaway. @default "" + */ + input: function _input(prompt, callback, extra) { + extra = extra || {}; + + CommandPromptMode(prompt, update({ onSubmit: callback }, extra)).open(); + }, + + readHeredoc: function readHeredoc(end) { + let args; + commandline.inputMultiline(end, function (res) { args = res; }); + util.waitFor(function () args !== undefined); + return args; + }, + + /** + * Get a multi-line input from a user, up to but not including the line + * which matches the given regular expression. Then execute the + * callback with that string as a parameter. + * + * @param {string} end + * @param {function(string)} callback + */ + // FIXME: Buggy, especially when pasting. + inputMultiline: function inputMultiline(end, callback) { + let cmd = this.command; + modes.push(modes.INPUT_MULTILINE, null, { + mappingSelf: { + end: "\n" + end + "\n", + callback: callback + } + }); + if (cmd != false) + this._echoLine(cmd, this.HL_NORMAL); + + // save the arguments, they are needed in the event handler onKeyPress + + this.multilineInputVisible = true; + this.widgets.multilineInput.value = ""; + this._autosizeMultilineInputWidget(); + + this.timeout(function () { dactyl.focus(this.widgets.multilineInput); }, 10); + }, + + get commandMode() this.commandSession && isinstance(modes.main, modes.COMMAND_LINE), + + events: update( + iter(CommandMode.prototype.events).map( + function ([event, handler]) [ + event, function (event) { + if (this.commandMode) + handler.call(this.commandSession, event); + } + ]).toObject(), + { + focus: function onFocus(event) { + if (!this.commandSession + && event.originalTarget === this.widgets.active.command.inputField) { + event.target.blur(); + dactyl.beep(); + } + }, + } + ), + + get mowEvents() mow.events, + + /** + * Multiline input events, they will come straight from + * #dactyl-multiline-input in the XUL. + * + * @param {Event} event + */ + multilineInputEvents: { + blur: function onBlur(event) { + if (modes.main == modes.INPUT_MULTILINE) + this.timeout(function () { + dactyl.focus(this.widgets.multilineInput.inputField); + }); + }, + input: function onInput(event) { + this._autosizeMultilineInputWidget(); + } + }, + + updateOutputHeight: deprecated("mow.resize", function updateOutputHeight(open, extra) mow.resize(open, extra)), + + withOutputToString: function withOutputToString(fn, self) { + dactyl.registerObserver("echoLine", observe, true); + dactyl.registerObserver("echoMultiline", observe, true); + + let output = []; + function observe(str, highlight, dom) { + output.push(dom && !isString(str) ? dom : str); + } + + this.savingOutput = true; + dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2))); + this.savingOutput = false; + return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem) + .join("\n"); + } +}, { + /** + * A class for managing the history of an input field. + * + * @param {HTMLInputElement} inputField + * @param {string} mode The mode for which we need history. + */ + History: Class("History", { + init: function init(inputField, mode, session) { + this.mode = mode; + this.input = inputField; + this.reset(); + this.session = session; + }, + get store() commandline._store.get(this.mode, []), + set store(ary) { commandline._store.set(this.mode, ary); }, + /** + * Reset the history index to the first entry. + */ + reset: function reset() { + this.index = null; + }, + /** + * Save the last entry to the permanent store. All duplicate entries + * are removed and the list is truncated, if necessary. + */ + save: function save() { + if (events.feedingKeys) + return; + let str = this.input.value; + if (/^\s*$/.test(str)) + return; + this.store = this.store.filter(function (line) (line.value || line) != str); + try { + this.store.push({ value: str, timestamp: Date.now()*1000, privateData: this.checkPrivate(str) }); + } + catch (e) { + dactyl.reportError(e); + } + this.store = this.store.slice(-options["history"]); + }, + /** + * @property {function} Returns whether a data item should be + * considered private. + */ + checkPrivate: function checkPrivate(str) { + // Not really the ideal place for this check. + if (this.mode == "command") + return commands.hasPrivateData(str); + return false; + }, + /** + * Replace the current input field value. + * + * @param {string} val The new value. + */ + replace: function replace(val) { + this.input.dactylKeyPress = undefined; + if (this.completions) + this.completions.previewClear(); + this.input.value = val; + }, + + /** + * Move forward or backward in history. + * + * @param {boolean} backward Direction to move. + * @param {boolean} matchCurrent Search for matches starting + * with the current input value. + */ + select: function select(backward, matchCurrent) { + // always reset the tab completion if we use up/down keys + if (this.session.completions) + this.session.completions.reset(); + + let diff = backward ? -1 : 1; + + if (this.index == null) { + this.original = this.input.value; + this.index = this.store.length; + } + + // search the history for the first item matching the current + // command-line string + while (true) { + this.index += diff; + if (this.index < 0 || this.index > this.store.length) { + this.index = Math.constrain(this.index, 0, this.store.length); + dactyl.beep(); + // I don't know why this kludge is needed. It + // prevents the caret from moving to the end of + // the input field. + if (this.input.value == "") { + this.input.value = " "; + this.input.value = ""; + } + break; + } + + let hist = this.store[this.index]; + // user pressed DOWN when there is no newer history item + if (!hist) + hist = this.original; + else + hist = (hist.value || hist); + + if (!matchCurrent || hist.substr(0, this.original.length) == this.original) { + this.replace(hist); + break; + } + } + } + }), + + /** + * A class for tab completions on an input field. + * + * @param {Object} input + */ + Completions: Class("Completions", { + init: function init(input, session) { + this.context = CompletionContext(input.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + this.context.onUpdate = this.closure._reset; + this.editor = input.editor; + this.input = input; + this.session = session; + this.selected = null; + this.wildmode = options.get("wildmode"); + this.wildtypes = this.wildmode.value; + this.itemList = commandline.completionList; + this.itemList.setItems(this.context); + + dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true); + + this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) { + if (events.feedingKeys) + this.ignoredCount++; + if (options["autocomplete"].length) { + this.itemList.visible = true; + this.complete(true, false); + } + }, this); + this.tabTimer = Timer(0, 0, function tabTell(event) { + this.tab(event.shiftKey, event.altKey && options["altwildmode"]); + }, this); + }, + + cleanup: function () { + dactyl.unregisterObserver("events.doneFeeding", this.closure.onDoneFeeding); + this.previewClear(); + this.tabTimer.reset(); + this.autocompleteTimer.reset(); + this.itemList.visible = false; + this.input.dactylKeyPress = undefined; + }, + + ignoredCount: 0, + onDoneFeeding: function onDoneFeeding() { + if (this.ignoredCount) + this.autocompleteTimer.flush(true); + this.ignoredCount = 0; + }, + + UP: {}, + DOWN: {}, + PAGE_UP: {}, + PAGE_DOWN: {}, + RESET: null, + + lastSubstring: "", + + get completion() { + let str = commandline.command; + return str.substring(this.prefix.length, str.length - this.suffix.length); + }, + set completion(completion) { + this.previewClear(); + + // Change the completion text. + // The second line is a hack to deal with some substring + // preview corner cases. + let value = this.prefix + completion + this.suffix; + commandline.widgets.active.command.value = value; + this.editor.selection.focusNode.textContent = value; + + // Reset the caret to one position after the completion. + this.caret = this.prefix.length + completion.length; + this._caret = this.caret; + + this.input.dactylKeyPress = undefined; + }, + + get caret() this.editor.selection.getRangeAt(0).startOffset, + set caret(offset) { + this.editor.selection.getRangeAt(0).setStart(this.editor.rootElement.firstChild, offset); + this.editor.selection.getRangeAt(0).setEnd(this.editor.rootElement.firstChild, offset); + }, + + get start() this.context.allItems.start, + + get items() this.context.allItems.items, + + get substring() this.context.longestAllSubstring, + + get wildtype() this.wildtypes[this.wildIndex] || "", + + complete: function complete(show, tabPressed) { + this.context.reset(); + this.context.tabPressed = tabPressed; + this.session.complete(this.context); + if (!this.session.active) + return; + this.context.updateAsync = true; + this.reset(show, tabPressed); + this.wildIndex = 0; + this._caret = this.caret; + }, + + haveType: function haveType(type) + this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type), + + preview: function preview() { + this.previewClear(); + if (this.wildIndex < 0 || this.suffix || !this.items.length) + return; + + let substring = ""; + switch (this.wildtype.replace(/.*:/, "")) { + case "": + substring = this.items[0].result; + 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.result; + break; + } + + // Don't show 1-character substrings unless we've just hit backspace + if (substring.length < 2 && this.lastSubstring.indexOf(substring) !== 0) + return; + + this.lastSubstring = substring; + + let value = this.completion; + if (util.compareIgnoreCase(value, substring.substr(0, value.length))) + return; + substring = substring.substr(value.length); + this.removeSubstring = substring; + + let node = util.xmlToDom(<span highlight="Preview">{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) { + try { + this.editor.deleteNode(node.nextSibling); + } + catch (e) { + node.nextSibling.textContent = ""; + } + } + else if (this.removeSubstring) { + let str = this.removeSubstring; + let cmd = commandline.widgets.active.command.value; + if (cmd.substr(cmd.length - str.length) == str) + commandline.widgets.active.command.value = cmd.substr(0, cmd.length - str.length); + } + delete this.removeSubstring; + }, + + reset: function reset(show) { + this.wildIndex = -1; + + this.prefix = this.context.value.substring(0, this.start); + this.value = this.context.value.substring(this.start, this.caret); + this.suffix = this.context.value.substring(this.caret); + + if (show) { + this.itemList.reset(); + if (this.haveType("list")) + this.itemList.visible = true; + this.selected = null; + this.wildIndex = 0; + } + + this.preview(); + }, + + _reset: function _reset() { + let value = this.editor.selection.focusNode.textContent; + this.prefix = value.substring(0, this.start); + this.value = value.substring(this.start, this.caret); + this.suffix = value.substring(this.caret); + + this.itemList.reset(); + this.itemList.selectItem(this.selected); + + this.preview(); + }, + + select: function select(idx) { + switch (idx) { + case this.UP: + if (this.selected == null) + idx = -2; + 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.constrain(idx, 0, this.items.length - 1); + break; + } + + if (idx == -1 || this.items.length && idx >= this.items.length || idx == null) { + // Wrapped. Start again. + this.selected = null; + this.completion = this.value; + } + else { + // Wait for contexts to complete if necessary. + // FIXME: Need to make idx relative to individual contexts. + let list = this.context.contextList; + if (idx == -2) + list = list.slice().reverse(); + let n = 0; + try { + this.waiting = true; + for (let [, context] in Iterator(list)) { + let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length); + + util.waitFor(function () !context.incomplete || done()) + if (done()) + break; + + n += context.items.length; + } + } + finally { + this.waiting = false; + } + + // See previous FIXME. This will break if new items in + // a previous context come in. + if (idx < 0) + idx = this.items.length - 1; + if (this.items.length == 0) + return; + + this.selected = idx; + this.completion = this.items[idx].result; + } + + this.itemList.selectItem(idx); + }, + + tabs: [], + + tab: function tab(reverse, wildmode) { + this.autocompleteTimer.flush(); + + if (this._caret != this.caret) + this.reset(); + this._caret = this.caret; + + // Check if we need to run the completer. + if (this.context.waitingForTab || this.wildIndex == -1) + this.complete(true, true); + + this.tabs.push([reverse, wildmode || options["wildmode"]]); + if (this.waiting) + return; + + while (this.tabs.length) { + [reverse, this.wildtypes] = this.tabs.shift(); + + this.wildIndex = Math.min(this.wildIndex, this.wildtypes.length - 1); + switch (this.wildtype.replace(/.*:/, "")) { + case "": + this.select(0); + break; + case "longest": + if (this.items.length > 1) { + if (this.substring && this.substring.length > this.completion.length) + this.completion = this.substring; + break; + } + // Fallthrough + case "full": + this.select(reverse ? this.UP : this.DOWN); + break; + } + + if (this.haveType("list")) + this.itemList.visible = true; + + this.wildIndex++; + this.preview(); + + if (this.selected == null) + statusline.progress = ""; + else + statusline.progress = "match " + (this.selected + 1) + " of " + this.items.length; + } + + if (this.items.length == 0) + dactyl.beep(); + } + }), + + /** + * Evaluate a JavaScript expression and return a string suitable + * to be echoed. + * + * @param {string} arg + * @param {boolean} useColor When true, the result is a + * highlighted XML object. + */ + echoArgumentToString: function (arg, useColor) { + if (!arg) + return ""; + + arg = dactyl.userEval(arg); + if (isObject(arg)) + arg = util.objectToString(arg, useColor); + else + arg = String(arg); + return arg; + } +}, { + commands: function init_commands() { + [ + { + name: "ec[ho]", + description: "Echo the expression", + action: dactyl.echo + }, + { + name: "echoe[rr]", + description: "Echo the expression as an error message", + action: dactyl.echoerr + }, + { + name: "echom[sg]", + description: "Echo the expression as an informational message", + action: dactyl.echomsg + } + ].forEach(function (command) { + commands.add([command.name], + command.description, + function (args) { + command.action(CommandLine.echoArgumentToString(args[0] || "", true)); + }, { + completer: function (context) completion.javascript(context), + literal: 0 + }); + }); + + commands.add(["mes[sages]"], + "Display previously shown messages", + function () { + // TODO: are all messages single line? Some display an aggregation + // of single line messages at least. E.g. :source + if (commandline._messageHistory.length == 1) { + let message = commandline._messageHistory.messages[0]; + commandline.echo(message.message, message.highlight, commandline.FORCE_SINGLELINE); + } + else if (commandline._messageHistory.length > 1) { + XML.ignoreWhitespace = false; + commandline.commandOutput( + template.map(commandline._messageHistory.messages, function (message) + <div highlight={message.highlight + " Message"}>{message.message}</div>)); + } + }, + { argCount: "0" }); + + commands.add(["messc[lear]"], + "Clear the message history", + function () { commandline._messageHistory.clear(); }, + { argCount: "0" }); + + commands.add(["sil[ent]"], + "Run a command silently", + function (args) { + commandline.runSilently(function () commands.execute(args[0] || "", null, true)); + }, { + completer: function (context) completion.ex(context), + literal: 0, + subCommand: 0 + }); + }, + modes: function initModes() { + modes.addMode("COMMAND_LINE", { + char: "c", + description: "Active when the command line is focused", + insert: true, + ownsFocus: true, + get mappingSelf() commandline.commandSession + }); + // this._extended modes, can include multiple modes, and even main modes + modes.addMode("EX", { + description: "Ex command mode, active when the command line is open for Ex commands", + bases: [modes.COMMAND_LINE] + }); + modes.addMode("PROMPT", { + description: "Active when a prompt is open in the command line", + bases: [modes.COMMAND_LINE] + }); + + modes.addMode("INPUT_MULTILINE", { + bases: [modes.INSERT] + }); + }, + mappings: function init_mappings() { + + mappings.add([modes.COMMAND], + [":"], "Enter command-line mode", + function () { CommandExMode().open(""); }); + + mappings.add([modes.INPUT_MULTILINE], + ["<Return>", "<C-j>", "<C-m>"], "Begin a new line", + function ({ self }) { + let text = "\n" + commandline.widgets.multilineInput + .value.substr(0, commandline.widgets.multilineInput.selectionStart) + + "\n"; + + let index = text.indexOf(self.end); + if (index >= 0) { + text = text.substring(1, index); + modes.pop(); + + return function () self.callback.call(commandline, text); + } + return Events.PASS; + }); + + let bind = function bind() + mappings.add.apply(mappings, [[modes.COMMAND_LINE]].concat(Array.slice(arguments))) + + // Any "non-keyword" character triggers abbreviation expansion + // TODO: Add "<CR>" and "<Tab>" to this list + // At the moment, adding "<Tab>" breaks tab completion. Adding + // "<CR>" has no effect. + // TODO: Make non-keyword recognition smarter so that there need not + // be two lists of the same characters (one here and a regexp in + // mappings.js) + bind(["<Space>", '"', "'"], "Expand command line abbreviation", + function ({ self }) { + self.resetCompletions(); + editor.expandAbbreviation(modes.COMMAND_LINE); + return Events.PASS; + }); + + bind(["<Return>", "<C-j>", "<C-m>"], "Accept the current input", + function ({ self }) { + let command = commandline.command; + + self.accepted = true; + return function () { modes.pop(); }; + }); + + [ + [["<Up>", "<A-p>"], "previous matching", true, true], + [["<S-Up>", "<C-p>", "<PageUp>"], "previous", true, false], + [["<Down>", "<A-n>"], "next matching", false, true], + [["<S-Down>", "<C-n>", "<PageDown>"], "next", false, false] + ].forEach(function ([keys, desc, up, search]) { + bind(keys, "Recall the " + desc + " command line from the history list", + function ({ self }) { + dactyl.assert(self.history); + self.history.select(up, search); + }); + }); + + bind(["<A-Tab>", "<Tab>"], "Select the next matching completion item", + function ({ keypressEvents, self }) { + dactyl.assert(self.completions); + self.completions.tabTimer.tell(keypressEvents[0]); + }); + + bind(["<A-S-Tab>", "<S-Tab>"], "Select the previous matching completion item", + function ({ keypressEvents, self }) { + dactyl.assert(self.completions); + self.completions.tabTimer.tell(keypressEvents[0]); + }); + + bind(["<BS>", "<C-h>"], "Delete the previous character", + function () { + if (!commandline.command) + modes.pop(); + else + return Events.PASS; + }); + + bind(["<C-]>", "<C-5>"], "Expand command line abbreviation", + function () { editor.expandAbbreviation(modes.COMMAND_LINE); }); + }, + options: function init_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(["maxitems"], + "Maximum number of completion items to display at once", + "number", 20, + { validator: function (value) value >= 1 }); + + options.add(["messages", "msgs"], + "Number of messages to store in the :messages history", + "number", 100, + { validator: function (value) value >= 0 }); + }, + sanitizer: function init_sanitizer() { + sanitizer.addItem("commandline", { + description: "Command-line and search history", + persistent: true, + action: function (timespan, host) { + let store = commandline._store; + for (let [k, v] in store) { + if (k == "command") + store.set(k, v.filter(function (item) + !(timespan.contains(item.timestamp) && (!host || commands.hasDomain(item.value, host))))); + else if (!host) + store.set(k, v.filter(function (item) !timespan.contains(item.timestamp))); + } + } + }); + // Delete history-like items from the commandline and messages on history purge + sanitizer.addItem("history", { + action: function (timespan, host) { + commandline._store.set("command", + commandline._store.get("command", []).filter(function (item) + !(timespan.contains(item.timestamp) && (host ? commands.hasDomain(item.value, host) + : item.privateData)))); + + commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) || + !item.domains && !item.privateData || + host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host)))); + } + }); + sanitizer.addItem("messages", { + description: "Saved :messages", + action: function (timespan, host) { + commandline._messageHistory.filter(function (item) !timespan.contains(item.timestamp * 1000) || + host && (!item.domains || !item.domains.some(function (d) util.isSubdomain(d, host)))); + } + }); + } +}); + +/** + * The list which is used for the completion box (and QuickFix window in + * future). + * + * @param {string} id The id of the iframe which will display the list. It + * must be in its own container element, whose height it will update as + * necessary. + */ +var ItemList = Class("ItemList", { + init: function init(id) { + this._completionElements = []; + + var iframe = document.getElementById(id); + + this._doc = iframe.contentDocument; + this._win = iframe.contentWindow; + this._container = iframe.parentNode; + + this._doc.documentElement.id = id + "-top"; + this._doc.body.id = id + "-content"; + this._doc.body.className = iframe.className + "-content"; + this._doc.body.appendChild(this._doc.createTextNode("")); + this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight. + + this._items = null; + this._startIndex = -1; // The index of the first displayed item + this._endIndex = -1; // The index one *after* the last displayed item + this._selIndex = -1; // The index of the currently selected element + this._div = null; + this._divNodes = {}; + this._minHeight = 0; + }, + + _dom: function _dom(xml, map) util.xmlToDom(xml instanceof XML ? xml : <>{xml}</>, this._doc, map), + + _autoSize: function _autoSize() { + if (!this._div) + return; + + if (this._container.collapsed) + this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px"; + + this._minHeight = Math.max(this._minHeight, + this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom); + + if (this._container.collapsed) + this._div.style.minWidth = ""; + + // FIXME: Belongs elsewhere. + mow.resize(false, Math.max(0, this._minHeight - this._container.height)); + + this._container.height = this._minHeight; + this._container.height -= mow.spaceNeeded; + mow.resize(false); + this.timeout(function () { + this._container.height -= mow.spaceNeeded; + }); + }, + + _getCompletion: function _getCompletion(index) this._completionElements.snapshotItem(index - this._startIndex), + + _init: function _init() { + this._div = this._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, options["maxitems"] * 2), function (i) + <div highlight="CompItem NonText"> + <li>~</li> + </div>) + } + </div> + </div>, this._divNodes); + this._doc.body.replaceChild(this._div, this._doc.body.firstChild); + util.scrollIntoView(this._div, true); + + this._items.contextList.forEach(function init_eachContext(context) { + delete context.cache.nodes; + if (!context.items.length && !context.message && !context.incomplete) + return; + context.cache.nodes = []; + this._dom(<div key="root" highlight="CompGroup"> + <div highlight="Completions"> + { context.createRow(context.title || [], "CompTitle") } + </div> + <div highlight="CompTitleSep"/> + <div key="message" highlight="CompMsg"/> + <div key="up" highlight="CompLess"/> + <div key="items" highlight="Completions"/> + <div key="waiting" highlight="CompMsg">{ItemList.WAITING_MESSAGE}</div> + <div key="down" highlight="CompMore"/> + </div>, context.cache.nodes); + this._divNodes.completions.appendChild(context.cache.nodes.root); + }, this); + + this.timeout(this._autoSize); + }, + + /** + * Uses the entries in "items" to fill the listbox and does incremental + * filling to speed up things. + * + * @param {number} offset Start at this index and show options["maxitems"]. + */ + _fill: function _fill(offset) { + XML.ignoreWhiteSpace = false; + let diff = offset - this._startIndex; + if (this._items == null || offset == null || diff == 0 || offset < 0) + return false; + + this._startIndex = offset; + this._endIndex = Math.min(this._startIndex + options["maxitems"], this._items.allItems.items.length); + + let haveCompletions = false; + let off = 0; + let end = this._startIndex + options["maxitems"]; + function getRows(context) { + function fix(n) Math.constrain(n, 0, len); + let len = context.items.length; + let start = off; + end -= !!context.message + context.incomplete; + off += len; + + let s = fix(offset - start), e = fix(end - start); + return [s, e, context.incomplete && e >= offset && off - 1 < end]; + } + + this._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 = ""; + nodes.message.appendChild(this._dom(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, this._doc))) + nodes[i] = row; + for (let [i, row] in array.iterItems(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"; + if (start == end) { + nodes.up.style.display = "none"; + nodes.down.style.display = "none"; + } + }, this); + + this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block"; + + this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc); + + return true; + }, + + clear: function clear() { this.setItems(); this._doc.body.innerHTML = ""; }, + get visible() !this._container.collapsed, + set visible(val) this._container.collapsed = !val, + + reset: function reset(brief) { + this._startIndex = this._endIndex = this._selIndex = -1; + this._div = null; + if (!brief) + this.selectItem(-1); + }, + + // if @param selectedItem is given, show the list and select that item + setItems: function setItems(newItems, selectedItem) { + if (this._selItem > -1) + this._getCompletion(this._selItem).removeAttribute("selected"); + if (this._container.collapsed) { + this._minHeight = 0; + this._container.height = 0; + } + this._startIndex = this._endIndex = this._selIndex = -1; + this._items = newItems; + this.reset(true); + if (typeof selectedItem == "number") { + this.selectItem(selectedItem); + this.visible = true; + } + }, + + // select index, refill list if necessary + selectItem: function selectItem(index) { + //let now = Date.now(); + + if (this._div == null) + this._init(); + + let sel = this._selIndex; + let len = this._items.allItems.items.length; + let newOffset = this._startIndex; + let maxItems = options["maxitems"]; + let contextLines = Math.min(3, parseInt((maxItems - 1) / 2)); + + if (index == -1 || index == null || index == len) { // wrapped around + if (this._selIndex < 0) + newOffset = 0; + this._selIndex = -1; + index = -1; + } + else { + if (index <= this._startIndex + contextLines) + newOffset = index - contextLines; + if (index >= this._endIndex - contextLines) + newOffset = index + contextLines - maxItems + 1; + + newOffset = Math.min(newOffset, len - maxItems); + newOffset = Math.max(newOffset, 0); + + this._selIndex = index; + } + + if (sel > -1) + this._getCompletion(sel).removeAttribute("selected"); + this._fill(newOffset); + if (index >= 0) { + this._getCompletion(index).setAttribute("selected", "true"); + if (this._container.height != 0) + util.scrollIntoView(this._getCompletion(index)); + } + + //if (index == 0) + // this.start = now; + //if (index == Math.min(len - 1, 100)) + // util.dump({ time: Date.now() - this.start }); + }, + + onKeyPress: function onKeyPress(event) false +}, { + WAITING_MESSAGE: "Generating results..." +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/dactyl.js b/common/content/dactyl.js new file mode 100644 index 0000000..6296b9a --- /dev/null +++ b/common/content/dactyl.js @@ -0,0 +1,2185 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org> +// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com> +// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +default xml namespace = XHTML; +XML.ignoreWhitespace = false; +XML.prettyPrinting = false; + +var userContext = { __proto__: modules }; +var _userContext = newContext(userContext); + +var EVAL_ERROR = "__dactyl_eval_error"; +var EVAL_RESULT = "__dactyl_eval_result"; +var EVAL_STRING = "__dactyl_eval_string"; + +var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { + init: function () { + window.dactyl = this; + // cheap attempt at compatibility + let prop = { get: deprecated("dactyl", function liberator() dactyl) }; + Object.defineProperty(window, "liberator", prop); + Object.defineProperty(modules, "liberator", prop); + this.commands = {}; + this.indices = {}; + this.modules = modules; + this._observers = {}; + util.addObserver(this); + + this.commands["dactyl.help"] = function (event) { + let elem = event.originalTarget; + dactyl.help(elem.getAttribute("tag") || elem.textContent); + }; + this.commands["dactyl.restart"] = function (event) { + dactyl.restart(); + }; + + styles.registerSheet("resource://dactyl-skin/dactyl.css"); + }, + + cleanup: function () { + delete window.dactyl; + delete window.liberator; + + styles.unregisterSheet("resource://dactyl-skin/dactyl.css"); + }, + + destroy: function () { + autocommands.trigger("LeavePre", {}); + dactyl.triggerObserver("shutdown", null); + util.dump("All dactyl modules destroyed\n"); + autocommands.trigger("Leave", {}); + }, + + observers: { + "dactyl-cleanup": function dactyl_cleanup() { + let modules = dactyl.modules; + + for (let mod in values(modules.moduleList.reverse())) { + mod.stale = true; + if ("cleanup" in mod) + this.trapErrors("cleanup", mod); + if ("destroy" in mod) + this.trapErrors("destroy", mod); + } + + for (let mod in values(modules.ownPropertyValues.reverse())) + if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT) + this.trapErrors(mod.cleanup, mod, dactyl, modules, window); + + for (let name in values(Object.getOwnPropertyNames(modules).reverse())) + try { + delete modules[name]; + } + catch (e) {} + modules.__proto__ = {}; + } + }, + + /** @property {string} The name of the current user profile. */ + profileName: Class.memoize(function () { + // NOTE: services.profile.selectedProfile.name doesn't return + // what you might expect. It returns the last _actively_ selected + // profile (i.e. via the Profile Manager or -P option) rather than the + // current profile. These will differ if the current process was run + // without explicitly selecting a profile. + + let dir = services.directory.get("ProfD", Ci.nsIFile); + for (let prof in iter(services.profile.profiles)) + if (prof.QueryInterface(Ci.nsIToolkitProfile).rootDir.path === dir.path) + return prof.name; + return "unknown"; + }), + + /** + * @property {number} The current main mode. + * @see modes#mainModes + */ + mode: deprecated("modes.main", { + get: function mode() modes.main, + set: function mode(val) modes.main = val + }), + + get menuItems() Dactyl.getMenuItems(), + + // Global constants + CURRENT_TAB: "here", + NEW_TAB: "tab", + NEW_BACKGROUND_TAB: "background-tab", + NEW_WINDOW: "window", + + forceNewTab: false, + forceNewWindow: false, + + version: deprecated("config.version", { get: function version() config.version }), + + /** + * @property {Object} The map of command-line options. These are + * specified in the argument to the host application's -{config.name} + * option. E.g. $ firefox -pentadactyl '+u=/tmp/rcfile ++noplugin' + * Supported options: + * +u RCFILE Use RCFILE instead of .pentadactylrc. + * ++noplugin Don't load plugins. + * These two can be specified multiple times: + * ++cmd CMD Execute an Ex command before initialization. + * +c CMD Execute an Ex command after initialization. + */ + commandLineOptions: { + /** @property Whether plugin loading should be prevented. */ + noPlugins: false, + /** @property An RC file to use rather than the default. */ + rcFile: null, + /** @property An Ex command to run before any initialization is performed. */ + preCommands: null, + /** @property An Ex command to run after all initialization has been performed. */ + postCommands: null + }, + + registerObserver: function registerObserver(type, callback, weak) { + if (!(type in this._observers)) + this._observers[type] = []; + this._observers[type].push(weak ? Cu.getWeakReference(callback) : { get: function () callback }); + }, + + registerObservers: function registerObservers(obj, prop) { + for (let [signal, func] in Iterator(obj[prop || "signals"])) + this.registerObserver(signal, obj.closure(func), false); + }, + + unregisterObserver: function unregisterObserver(type, callback) { + if (type in this._observers) + this._observers[type] = this._observers[type].filter(function (c) c.get() != callback); + }, + + // TODO: "zoom": if the zoom value of the current buffer changed + applyTriggerObserver: function triggerObserver(type, args) { + if (type in this._observers) + this._observers[type] = this._observers[type].filter(function (callback) { + if (callback.get()) { + try { + try { + callback.get().apply(null, args); + } + catch (e if e.message == "can't wrap XML objects") { + // Horrible kludge. + callback.get().apply(null, [String(args[0])].concat(args.slice(1))); + } + } + catch (e) { + dactyl.reportError(e); + } + return true; + } + }); + }, + + triggerObserver: function triggerObserver(type) { + return this.applyTriggerObserver(type, Array.slice(arguments, 1)); + }, + + addUsageCommand: function (params) { + let name = commands.add(params.name, params.description, + function (args) { + let results = array(params.iterate(args)) + .sort(function (a, b) String.localeCompare(a.name, b.name)); + + let filters = args.map(function (arg) RegExp("\\b" + util.regexp.escape(arg) + "\\b", "i")); + if (filters.length) + results = results.filter(function (item) filters.every(function (re) re.test(item.name + " " + item.description))); + + commandline.commandOutput( + template.usage(results, params.format)); + }, + { + argCount: "*", + completer: function (context, args) { + context.keys.text = util.identity; + context.keys.description = function () seen[this.text] + " matching items"; + let seen = {}; + context.completions = array(item.description.toLowerCase().split(/[()\s]+/) + for (item in params.iterate(args))) + .flatten().filter(function (w) /^\w[\w-_']+$/.test(w)) + .map(function (k) { + seen[k] = (seen[k] || 0) + 1; + return k; + }).uniq(); + }, + options: params.options || [] + }); + + if (params.index) + this.indices[params.index] = function () { + let results = array((params.iterateIndex || params.iterate).call(params, commands.get(name).newArgs())) + .array.sort(function (a, b) String.localeCompare(a.name, b.name)); + + let tags = services["dactyl:"].HELP_TAGS; + for (let obj in values(results)) { + let res = dactyl.generateHelp(obj, null, null, true); + if (!set.has(tags, obj.helpTag)) + res[1].@tag = obj.helpTag; + + yield res; + } + }; + }, + + /** + * Triggers the application bell to notify the user of an error. The + * bell may be either audible or visual depending on the value of the + * 'visualbell' option. + */ + beep: function () { + this.triggerObserver("beep"); + if (options["visualbell"]) { + let elems = { + bell: document.getElementById("dactyl-bell"), + strut: document.getElementById("dactyl-bell-strut") + }; + XML.ignoreWhitespace = true; + if (!elems.bell) + util.overlayWindow(window, { + objects: elems, + prepend: <> + <window id={document.documentElement.id} xmlns={XUL}> + <hbox style="display: none" highlight="Bell" id="dactyl-bell" key="bell"/> + </window> + </>, + append: <> + <window id={document.documentElement.id} xmlns={XUL}> + <hbox style="display: none" highlight="Bell" id="dactyl-bell-strut" key="strut"/> + </window> + </> + }, elems); + + elems.bell.style.height = window.innerHeight + "px"; + elems.strut.style.marginBottom = -window.innerHeight + "px"; + elems.strut.style.display = elems.bell.style.display = ""; + + util.timeout(function () { elems.strut.style.display = elems.bell.style.display = "none"; }, 20); + } + else { + let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound); + soundService.beep(); + } + }, + + /** + * Reads a string from the system clipboard. + * + * This is same as Firefox's readFromClipboard function, but is needed for + * apps like Thunderbird which do not provide it. + * + * @returns {string} + */ + clipboardRead: function clipboardRead(getClipboard) { + try { + const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); + const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + + transferable.addDataFlavor("text/unicode"); + + let source = clipboard[getClipboard || !clipboard.supportsSelectionClipboard() ? + "kGlobalClipboard" : "kSelectionClipboard"]; + clipboard.getData(transferable, source); + + let str = {}, len = {}; + transferable.getTransferData("text/unicode", str, len); + + if (str) + return str.value.QueryInterface(Ci.nsISupportsString) + .data.substr(0, len.value / 2); + } + catch (e) {} + return null; + }, + + /** + * Copies a string to the system clipboard. If *verbose* is specified the + * copied string is also echoed to the command line. + * + * @param {string} str + * @param {boolean} verbose + */ + clipboardWrite: function clipboardWrite(str, verbose) { + const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); + clipboardHelper.copyString(str); + + if (verbose) { + let message = { message: "Yanked " + str }; + try { + message.domains = [util.newURI(str).host]; + } + catch (e) {}; + dactyl.echomsg(message); + } + }, + + dump: deprecated("util.dump", + { get: function dump() util.closure.dump }), + dumpStack: deprecated("util.dumpStack", + { get: function dumpStack() util.closure.dumpStack }), + + /** + * Outputs a plain message to the command line. + * + * @param {string} str The message to output. + * @param {number} flags These control the multi-line message behavior. + * See {@link CommandLine#echo}. + */ + echo: function echo(str, flags) { + commandline.echo(str, commandline.HL_NORMAL, flags); + }, + + /** + * Outputs an error message to the command line. + * + * @param {string} str The message to output. + * @param {number} flags These control the multi-line message behavior. + * See {@link CommandLine#echo}. + */ + echoerr: function echoerr(str, flags) { + flags |= commandline.APPEND_TO_MESSAGES; + + if (isinstance(str, ["Error", "Exception"])) + dactyl.reportError(str); + if (isObject(str) && "echoerr" in str) + str = str.echoerr; + else if (isinstance(str, ["Error", FailedAssertion]) && str.fileName) + str = <>{str.fileName.replace(/^.* -> /, "")}: {str.lineNumber}: {str}</>; + + if (options["errorbells"]) + dactyl.beep(); + + commandline.echo(str, commandline.HL_ERRORMSG, flags); + }, + + /** + * Outputs a warning message to the command line. + * + * @param {string} str The message to output. + * @param {number} flags These control the multi-line message behavior. + * See {@link CommandLine#echo}. + */ + warn: function warn(str, flags) { + commandline.echo(str, "WarningMsg", flags | commandline.APPEND_TO_MESSAGES); + }, + + // TODO: add proper level constants + /** + * Outputs an information message to the command line. + * + * @param {string} str The message to output. + * @param {number} verbosity The messages log level (0 - 15). Only + * messages with verbosity less than or equal to the value of the + * *verbosity* option will be output. + * @param {number} flags These control the multi-line message behavior. + * See {@link CommandLine#echo}. + */ + echomsg: function echomsg(str, verbosity, flags) { + if (verbosity == null) + verbosity = 0; // verbosity level is exclusionary + + if (options["verbose"] >= verbosity) + commandline.echo(str, commandline.HL_INFOMSG, + flags | commandline.APPEND_TO_MESSAGES); + }, + + /** + * Loads and executes the script referenced by *uri* in the scope of the + * *context* object. + * + * @param {string} uri The URI of the script to load. Should be a local + * chrome:, file:, or resource: URL. + * @param {Object} context The context object into which the script + * should be loaded. + */ + loadScript: function (uri, context) { + JSMLoader.loadSubScript(uri, context, File.defaultEncoding); + }, + + userEval: function (str, context, fileName, lineNumber) { + let ctxt; + if (jsmodules.__proto__ != window) + str = "with (window) { with (modules) { (this.eval || eval)(" + str.quote() + ") } }"; + + let info = contexts.context; + if (fileName == null) + if (info && info.file[0] !== "[") + ({ file: fileName, line: lineNumber, context: ctxt }) = info; + + if (!context && fileName && fileName[0] !== "[") + context = _userContext || ctxt; + + if (isinstance(context, ["Sandbox"])) + return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber); + else + try { + if (!context) + context = userContext || ctxt; + + context[EVAL_ERROR] = null; + context[EVAL_STRING] = str; + context[EVAL_RESULT] = null; + this.loadScript("resource://dactyl-content/eval.js", context); + if (context[EVAL_ERROR]) { + try { + context[EVAL_ERROR].fileName = info.file; + context[EVAL_ERROR].lineNumber += info.line; + } + catch (e) {} + throw context[EVAL_ERROR]; + } + return context[EVAL_RESULT]; + } + finally { + delete context[EVAL_ERROR]; + delete context[EVAL_RESULT]; + delete context[EVAL_STRING]; + } + }, + + /** + * Acts like the Function builtin, but the code executes in the + * userContext global. + */ + userFunc: function () { + return this.userEval( + "(function userFunction(" + Array.slice(arguments, 0, -1).join(", ") + ")" + + " { " + arguments[arguments.length - 1] + " })"); + }, + + /** + * Execute an Ex command string. E.g. ":zoom 300". + * + * @param {string} str The command to execute. + * @param {Object} modifiers Any modifiers to be passed to + * {@link Command#action}. + * @param {boolean} silent Whether the command should be echoed on the + * command line. + */ + execute: function (str, modifiers, silent) { + // skip comments and blank lines + if (/^\s*("|$)/.test(str)) + return; + + modifiers = modifiers || {}; + + if (!silent) + commands.lastCommand = str.replace(/^\s*:\s*/, ""); + let res = true; + for (let [command, args] in commands.parseCommands(str.replace(/^'(.*)'$/, "$1"))) { + if (command === null) + throw FailedAssertion(_("dactyl.notCommand", config.appName, args.commandString)); + + res = res && command.execute(args, modifiers); + } + return res; + }, + + focus: function focus(elem, flags) { + flags = flags || services.focus.FLAG_BYMOUSE; + try { + if (elem instanceof Document) + elem = elem.defaultView; + if (elem instanceof Element) + services.focus.setFocus(elem, flags); + else if (elem instanceof Window) + services.focus.focusedWindow = elem; + } + catch (e) { + util.dump(elem); + util.reportError(e); + } + }, + + /** + * Focuses the content window. + * + * @param {boolean} clearFocusedElement Remove focus from any focused + * element. + */ + focusContent: function focusContent(clearFocusedElement) { + if (window != services.focus.activeWindow) + return; + + let win = document.commandDispatcher.focusedWindow; + let elem = config.mainWidget || content; + + // TODO: make more generic + try { + if (this.has("mail") && !config.isComposeWindow) { + let i = gDBView.selection.currentIndex; + if (i == -1 && gDBView.rowCount >= 0) + i = 0; + gDBView.selection.select(i); + } + else { + let frame = buffer.focusedFrame; + if (frame && frame.top == content && !Editor.getEditor(frame)) + elem = frame; + } + } + catch (e) {} + + if (clearFocusedElement) { + if (dactyl.focusedElement) + dactyl.focusedElement.blur(); + if (win && Editor.getEditor(win)) { + this.withSavedValues(["ignoreFocus"], function _focusContent() { + this.ignoreFocus = true; + if (win.frameElement) + win.frameElement.blur(); + // Grr. + if (content.document.activeElement instanceof HTMLIFrameElement) + content.document.activeElement.blur(); + }); + } + } + + if (elem instanceof Window && Editor.getEditor(elem)) + elem = window; + + if (elem && elem != dactyl.focusedElement) + dactyl.focus(elem); + }, + + /** @property {Element} The currently focused element. */ + get focusedElement() services.focus.getFocusedElementForWindow(window, true, {}), + set focusedElement(elem) dactyl.focus(elem), + + /** + * Returns whether this Dactyl extension supports *feature*. + * + * @param {string} feature The feature name. + * @returns {boolean} + */ + has: function (feature) set.has(config.features, feature), + + /** + * Returns the URL of the specified help *topic* if it exists. + * + * @param {string} topic The help topic to look up. + * @param {boolean} consolidated Whether to search the consolidated help page. + * @returns {string} + */ + findHelp: function (topic, consolidated) { + if (!consolidated && topic in services["dactyl:"].FILE_MAP) + return topic; + let items = completion._runCompleter("help", topic, null, !!consolidated).items; + let partialMatch = null; + + function format(item) item.description + "#" + encodeURIComponent(item.text); + + for (let [i, item] in Iterator(items)) { + if (item.text == topic) + return format(item); + else if (!partialMatch && topic) + partialMatch = item; + } + + if (partialMatch) + return format(partialMatch); + return null; + }, + + /** + * @private + */ + initDocument: function initDocument(doc) { + try { + if (doc.location.protocol === "dactyl:") { + dactyl.initHelp(); + config.styleHelp(); + } + } + catch (e) { + util.reportError(e); + } + }, + + /** + * @private + * Initialize the help system. + */ + initHelp: function (force) { + if (force || !this.helpInitialized) { + if ("noscriptOverlay" in window) { + noscriptOverlay.safeAllow("chrome-data:", true, false); + noscriptOverlay.safeAllow("dactyl:", true, false); + } + + // Find help and overlay files with the given name. + let findHelpFile = function findHelpFile(file) { + let result = []; + for (let [, namespace] in Iterator(namespaces)) { + let url = ["dactyl://", namespace, "/", file, ".xml"].join(""); + let res = util.httpGet(url); + if (res) { + if (res.responseXML.documentElement.localName == "document") + fileMap[file] = url; + if (res.responseXML.documentElement.localName == "overlay") + overlayMap[file] = url; + result.push(res.responseXML); + } + } + return result; + }; + // Find the tags in the document. + let addTags = function addTags(file, doc) { + for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc)) + for (let tag in values((elem.value || elem.textContent).split(/\s+/))) + tagMap[tag] = file; + }; + + let namespaces = ["locale-local", "locale"]; + services["dactyl:"].init({}); + + let tagMap = services["dactyl:"].HELP_TAGS; + let fileMap = services["dactyl:"].FILE_MAP; + let overlayMap = services["dactyl:"].OVERLAY_MAP; + + // Scrape the list of help files from all.xml + // Manually process main and overlay files, since XSLTProcessor and + // XMLHttpRequest don't allow access to chrome documents. + tagMap["all"] = tagMap["all.xml"] = "all"; + tagMap["versions"] = tagMap["versions.xml"] = "versions"; + let files = findHelpFile("all").map(function (doc) + [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]); + + // Scrape the tags from the rest of the help files. + array.flatten(files).forEach(function (file) { + tagMap[file + ".xml"] = file; + findHelpFile(file).forEach(function (doc) { + addTags(file, doc); + }); + }); + + // Process plugin help entries. + XML.ignoreWhiteSpace = XML.prettyPrinting = false; + + let body = XML(); + for (let [, context] in Iterator(plugins.contexts)) + if (context && context.INFO instanceof XML) { + let info = context.INFO; + if (info.*.@lang.length()) { + let lang = config.bestLocale(String(a) for each (a in info.*.@lang)); + + info.* = info.*.(function::attribute("lang").length() == 0 || @lang == lang); + + for each (let elem in info.NS::info) + for each (let attr in ["@name", "@summary", "@href"]) + if (elem[attr].length()) + info[attr] = elem[attr]; + } + body += <h2 xmlns={NS.uri} tag={context.INFO.@name + '-plugin'}>{context.INFO.@summary}</h2> + + context.INFO; + } + + let help = + '<?xml version="1.0"?>\n' + + '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' + + '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' + + unescape(encodeURI( // UTF-8 handling hack. + <document xmlns={NS} + name="plugins" title={config.appName + " Plugins"}> + <h1 tag="using-plugins">Using Plugins</h1> + <toc start="2"/> + + {body} + </document>.toXMLString())); + fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help]; + + fileMap["versions"] = function () { + let NEWS = util.httpGet(config.addon.getResourceURI("NEWS").spec, + { mimeType: "text/plain;charset=UTF-8" }) + .responseText; + + let re = util.regexp(<![CDATA[ + ^ (?P<comment> \s* # .*\n) + + | ^ (?P<space> \s*) + (?P<char> [-•*+]) \ // + (?P<content> .*\n + (?: \2\ \ .*\n | \s*\n)* ) + + | (?P<par> + (?: ^ [^\S\n]* + (?:[^-•*+\s] | [-•*+]\S) + .*\n + )+ + ) + + | (?: ^ [^\S\n]* \n) + + ]]>, "gmxy"); + + let betas = util.regexp(/\[(b\d)\]/, "gx"); + + let beta = array(betas.iterate(NEWS)) + .map(function (m) m[1]).uniq().slice(-1)[0]; + + default xml namespace = NS; + function rec(text, level, li) { + let res = <></>; + let list, space, i = 0; + + for (let match in re.iterate(text)) { + if (match.comment) + continue; + else if (match.char) { + if (!list) + res += list = <ul/>; + let li = <li/>; + li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li) + list.* += li; + } + else if (match.par) { + let [, par, tags] = /([^]*?)\s*((?:\[[^\]]+\])*)\n*$/.exec(match.par); + let t = tags; + tags = array(betas.iterate(tags)).map(function (m) m[1]); + + let group = !tags.length ? "" : + !tags.some(function (t) t == beta) ? "HelpNewsOld" : "HelpNewsNew"; + if (i === 0 && li) { + li.@highlight = group; + group = ""; + } + + list = null; + if (level == 0 && /^.*:\n$/.test(match.par)) + res += <h2>{template.linkifyHelp(par.slice(0, -1), true)}</h2>; + else { + let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par); + res += <p highlight={group + " HelpNews"}>{ + !tags.length ? "" : + <hl key="HelpNewsTag">{tags.join(" ")}</hl> + }{ + a ? <hl key="HelpWarning">{a}</hl> : "" + }{ + template.linkifyHelp(b, true) + }</p>; + } + } + i++; + } + for each (let attr in res..@highlight) { + attr.parent().@NS::highlight = attr; + delete attr.parent().@highlight; + } + return res; + } + + let body = rec(NEWS, 0); + for each (let li in body..li) { + let list = li..li.(@NS::highlight == "HelpNewsOld"); + if (list.length() && list.length() == li..li.(@NS::highlight != "").length()) { + for each (let li in list) + li.@NS::highlight = ""; + li.@NS::highlight = "HelpNewsOld"; + } + } + + XML.prettyPrinting = XML.ignoreWhitespace = false; + return ["application/xml", + '<?xml version="1.0"?>\n' + + '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' + + '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' + + unescape(encodeURI( // UTF-8 handling hack. + <document xmlns={NS} xmlns:dactyl={NS} + name="versions" title={config.appName + " Versions"}> + <h1 tag="versions news NEWS">{config.appName} Versions</h1> + <toc start="2"/> + + {body} + </document>.toXMLString())) + ]; + } + addTags("versions", util.httpGet("dactyl://help/versions").responseXML); + addTags("plugins", util.httpGet("dactyl://help/plugins").responseXML); + + default xml namespace = NS; + + overlayMap["index"] = ['text/xml;charset=UTF-8', + '<?xml version="1.0"?>\n' + + '<overlay xmlns="' + NS + '">\n' + + unescape(encodeURI( // UTF-8 handling hack. + template.map(dactyl.indices, function ([name, iter]) + <dl insertafter={name + "-index"}>{ + template.map(iter(), util.identity) + }</dl>, <>{"\n\n"}</>))) + + '\n</overlay>']; + + addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML); + + this.helpInitialized = true; + } + }, + + stringifyXML: function (xml) { + XML.prettyPrinting = false; + XML.ignoreWhitespace = false; + return UTF8(xml.toXMLString()); + }, + + exportHelp: JavaScript.setCompleter(function (path) { + const FILE = io.File(path); + const PATH = FILE.leafName.replace(/\..*/, "") + "/"; + const TIME = Date.now(); + + if (!FILE.exists() && (/\/$/.test(path) && !/\./.test(FILE.leafName))) + FILE.create(FILE.DIRECTORY_TYPE, octal(755)); + + dactyl.initHelp(); + if (FILE.isDirectory()) { + var addDataEntry = function addDataEntry(file, data) FILE.child(file).write(data); + var addURIEntry = function addURIEntry(file, uri) addDataEntry(file, util.httpGet(uri).responseText); + } + else { + var zip = services.ZipWriter(); + zip.open(FILE, File.MODE_CREATE | File.MODE_WRONLY | File.MODE_TRUNCATE); + + addURIEntry = function addURIEntry(file, uri) + zip.addEntryChannel(PATH + file, TIME, 9, + services.io.newChannel(uri, null, null), false); + addDataEntry = function addDataEntry(file, data) // Unideal to an extreme. + addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data)); + } + + let empty = set("area base basefont br col frame hr img input isindex link meta param" + .split(" ")); + function fix(node) { + switch(node.nodeType) { + case Node.ELEMENT_NODE: + if (isinstance(node, [HTMLBaseElement])) + return; + + data.push("<"); data.push(node.localName); + if (node instanceof HTMLHtmlElement) + data.push(" xmlns=" + XHTML.uri.quote()); + + for (let { name, value } in array.iterValues(node.attributes)) { + if (name == "dactyl:highlight") { + set.add(styles, value); + name = "class"; + value = "hl-" + value; + } + if (name == "href") { + value = node.href; + if (value.indexOf("dactyl://help-tag/") == 0) { + let uri = services.io.newChannel(value, null, null).originalURI; + value = uri.spec == value ? "javascript:;" : uri.path.substr(1); + } + if (!/^#|[\/](#|$)|^[a-z]+:/.test(value)) + value = value.replace(/(#|$)/, ".xhtml$1"); + } + if (name == "src" && value.indexOf(":") > 0) { + chromeFiles[value] = value.replace(/.*\//, ""); + value = value.replace(/.*\//, ""); + } + data.push(" "); + data.push(name); + data.push('="'); + data.push(<>{value}</>.toXMLString()); + data.push('"'); + } + if (node.localName in empty) + data.push(" />"); + else { + data.push(">"); + if (node instanceof HTMLHeadElement) + data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString()); + Array.map(node.childNodes, fix); + data.push("</"); data.push(node.localName); data.push(">"); + } + break; + case Node.TEXT_NODE: + data.push(<>{node.textContent}</>.toXMLString()); + } + } + + let chromeFiles = {}; + let styles = {}; + for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) { + dactyl.open("dactyl://help/" + file); + dactyl.modules.events.waitForPageLoad(); + let data = [ + '<?xml version="1.0" encoding="UTF-8"?>\n', + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n', + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' + ]; + fix(content.document.documentElement); + addDataEntry(file + ".xhtml", data.join("")); + } + + let data = [h for (h in highlight) if (set.has(styles, h.class) || /^Help/.test(h.class))] + .map(function (h) h.selector + .replace(/^\[.*?=(.*?)\]/, ".hl-$1") + .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}") + .join("\n"); + addDataEntry("help.css", data.replace(/chrome:[^ ")]+\//g, "")); + + addDataEntry("tag-map.json", JSON.stringify(services["dactyl:"].HELP_TAGS)); + + let m, re = /(chrome:[^ ");]+\/)([^ ");]+)/g; + while ((m = re.exec(data))) + chromeFiles[m[0]] = m[2]; + + for (let [uri, leaf] in Iterator(chromeFiles)) + addURIEntry(leaf, uri); + + if (zip) + zip.close(); + }, [function (context, args) completion.file(context)]), + + /** + * Generates a help entry and returns it as a string. + * + * @param {Command|Map|Option} obj A dactyl *Command*, *Map* or *Option* + * object + * @param {XMLList} extraHelp Extra help text beyond the description. + * @returns {string} + */ + generateHelp: function generateHelp(obj, extraHelp, str, specOnly) { + default xml namespace = ""; + + let link, tag, spec; + link = tag = spec = util.identity; + let args = null; + + if (obj instanceof Command) { + link = function (cmd) <ex>{cmd}</ex>; + args = obj.parseArgs("", CompletionContext(str || "")); + spec = function (cmd) cmd + (obj.bang ? <oa>!</oa> : <></>); + } + else if (obj instanceof Map) { + spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>; + link = function (map) { + let [, mode, name, extra] = /^(?:(.)_)?(?:<([^>]+)>)?(.*)$/.exec(map); + let k = <k>{extra}</k>; + if (name) + k.@name = name; + if (mode) + k.@mode = mode; + return k; + }; + } + else if (obj instanceof Option) { + link = function (opt, name) <o>{name}</o>; + } + + XML.prettyPrinting = false; + XML.ignoreWhitespace = false; + default xml namespace = NS; + + // E4X has its warts. + let br = <> + </>; + + let res = <res> + <dt>{link(obj.helpTag || obj.name, obj.name)}</dt> <dd>{ + template.linkifyHelp(obj.description ? obj.description.replace(/\.$/, "") : "", true) + }</dd></res>; + if (specOnly) + return res.elements(); + + res.* += <> + <item> + <tags>{template.map(obj.names.slice().reverse(), tag, " ")}</tags> + <spec>{ + spec(template.highlightRegexp((obj.specs || obj.names)[0], + /\[(.*?)\]/g, + function (m, n0) <oa>{n0}</oa>)) + }</spec>{ + !obj.type ? "" : <> + <type>{obj.type}</type> + <default>{obj.stringDefaultValue}</default></>} + <description>{ + obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{ + extraHelp ? br + extraHelp : "" }{ + !(extraHelp || obj.description) ? br + <p>Sorry, no help available.</p> : "" } + </description> + </item></>; + + function add(ary) { + res.item.description.* += br + + let (br = br + <> </>) + <><dl>{ br + template.map(ary, function ([a, b]) <><dt>{a}</dt> <dd>{b}</dd></>, br) } + </dl> + </>; + } + + if (obj.completer) + add(completion._runCompleter(obj.completer, "", null, args).items + .map(function (i) [i.text, i.description])); + + if (obj.options && obj.options.some(function (o) o.description)) + add(obj.options.filter(function (o) o.description) + .map(function (o) [ + o.names[0], + <>{o.description}{ + o.names.length == 1 ? "" : + <> (short name: { + template.map(o.names.slice(1), function (n) <em>{n}</em>, <>, </>) + })</> + }</> + ])); + return res.*.toXMLString() + .replace(' xmlns="' + NS + '"', "", "g") + .replace(/^ {12}|[ \t]+$/gm, "") + .replace(/^\s*\n|\n\s*$/g, "") + "\n"; + }, + + /** + * Opens the help page containing the specified *topic* if it exists. + * + * @param {string} topic The help topic to open. + * @param {boolean} consolidated Whether to use the consolidated help page. + */ + help: function (topic, consolidated) { + dactyl.initHelp(); + if (!topic) { + let helpFile = consolidated ? "all" : options["helpfile"]; + + if (helpFile in services["dactyl:"].FILE_MAP) + dactyl.open("dactyl://help/" + helpFile, { from: "help" }); + else + dactyl.echomsg(_("help.noFile", helpFile.quote())); + return; + } + + let page = this.findHelp(topic, consolidated); + dactyl.assert(page != null, _("help.noTopic", topic)); + + dactyl.open("dactyl://help/" + page, { from: "help" }); + }, + + /** + * The map of global variables. + * + * These are set and accessed with the "g:" prefix. + */ + _globalVariables: {}, + globalVariables: deprecated("the options system", { + get: function globalVariables() this._globalVariables + }), + + loadPlugins: function (args, force) { + function sourceDirectory(dir) { + dactyl.assert(dir.isReadable(), _("io.notReadable", dir.path)); + + dactyl.log(_("dactyl.sourcingPlugins", dir.path), 3); + + let loadplugins = options.get("loadplugins"); + if (args) + loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) } + + dir.readDirectory(true).forEach(function (file) { + if (file.isFile() && loadplugins.getKey(file.path) && !(!force && file.path in dactyl.pluginFiles)) { + try { + io.source(file.path); + dactyl.pluginFiles[file.path] = true; + } + catch (e) { + dactyl.reportError(e); + } + } + else if (file.isDirectory()) + sourceDirectory(file); + }); + } + + let dirs = io.getRuntimeDirectories("plugins"); + + if (dirs.length == 0) { + dactyl.log(_("dactyl.noPluginDir"), 3); + return; + } + + dactyl.echomsg( + _("plugin.searchingForIn", + ("plugins/**/*.{js," + config.fileExtension + "}").quote(), + [dir.path.replace(/.plugins$/, "") for ([, dir] in Iterator(dirs))] + .join(",").quote()), + 2); + + dirs.forEach(function (dir) { + dactyl.echomsg(_("plugin.searchingFor", (dir.path + "/**/*.{js," + config.fileExtension + "}").quote()), 3); + sourceDirectory(dir); + }); + }, + + // TODO: add proper level constants + /** + * Logs a message to the JavaScript error console. Each message has an + * associated log level. Only messages with a log level less than or equal + * to *level* will be printed. If *msg* is an object, it is pretty printed. + * + * @param {string|Object} msg The message to print. + * @param {number} level The logging level 0 - 15. + */ + log: function (msg, level) { + let verbose = localPrefs.get("loglevel", 0); + + if (!level || level <= verbose) { + if (isObject(msg) && !isinstance(msg, _)) + msg = util.objectToString(msg, false); + + services.console.logStringMessage(config.name + ": " + msg); + } + }, + + onClick: function onClick(event) { + if (event.originalTarget instanceof Element) { + let command = event.originalTarget.getAttributeNS(NS, "command"); + if (command && event.button == 0) { + event.preventDefault(); + + if (dactyl.commands[command]) + dactyl.withSavedValues(["forceNewTab"], function () { + dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1; + dactyl.commands[command](event); + }); + } + } + }, + + onExecute: function onExecute(event) { + let cmd = event.originalTarget.getAttribute("dactyl-execute"); + commands.execute(cmd, null, false, null, + { file: "[Command Line]", line: 1 }); + }, + + /** + * Opens one or more URLs. Returns true when load was initiated, or + * false on error. + * + * @param {string|Array} urls A representation of the URLs to open. May be + * either a string, which will be passed to + * {@see Dactyl#parseURLs}, or an array in the same format as + * would be returned by the same. + * @param {object} params A set of parameters specifying how to open the + * URLs. The following properties are recognized: + * + * • background If true, new tabs are opened in the background. + * + * • from The designation of the opener, as appears in + * 'activate' and 'newtab' options. If present, + * the newtab option provides the default 'where' + * parameter, and the value of the 'activate' + * parameter is inverted if 'background' is true. + * + * • where One of CURRENT_TAB, NEW_TAB, or NEW_WINDOW + * + * As a deprecated special case, the where parameter may be provided + * by itself, in which case it is transformed into { where: params }. + * + * @param {boolean} force Don't prompt whether to open more than 20 + * tabs. + * @returns {boolean} + */ + open: function (urls, params, force) { + if (typeof urls == "string") + urls = dactyl.parseURLs(urls); + + if (urls.length > prefs.get("browser.tabs.maxOpenBeforeWarn", 20) && !force) + return 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)) + dactyl.open(urls, params, true); + }); + + params = params || {}; + if (isString(params)) + params = { where: params }; + + let flags = 0; + for (let [opt, flag] in Iterator({ replace: "REPLACE_HISTORY", hide: "BYPASS_HISTORY" })) + flags |= params[opt] && Ci.nsIWebNavigation["LOAD_FLAGS_" + flag]; + + let where = params.where || dactyl.CURRENT_TAB; + let background = ("background" in params) ? params.background + : params.where == dactyl.NEW_BACKGROUND_TAB; + + if (params.from && dactyl.has("tabs")) { + if (!params.where && options.get("newtab").has(params.from)) + where = dactyl.NEW_TAB; + background ^= !options.get("activate").has(params.from); + } + + if (urls.length == 0) + return; + + let browser = config.tabbrowser; + function open(urls, where) { + try { + let url = Array.concat(urls)[0]; + let postdata = Array.concat(urls)[1]; + + // decide where to load the first url + switch (where) { + + case dactyl.NEW_TAB: + if (!dactyl.has("tabs")) + return open(urls, dactyl.NEW_WINDOW); + + return prefs.withContext(function () { + prefs.set("browser.tabs.loadInBackground", true); + return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument; + }); + + case dactyl.NEW_WINDOW: + let win = window.openDialog(document.documentURI, "_blank", "chrome,all,dialog=no"); + util.waitFor(function () win.document.readyState === "complete"); + browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser(); + // FALLTHROUGH + case dactyl.CURRENT_TAB: + browser.loadURIWithFlags(url, flags, null, null, postdata); + return browser.contentWindow; + } + } + catch (e) {} + // Unfortunately, failed page loads throw exceptions and + // cause a lot of unwanted noise. This solution means that + // any genuine errors go unreported. + } + + if (dactyl.forceNewTab) + where = dactyl.NEW_TAB; + else if (dactyl.forceNewWindow) + where = dactyl.NEW_WINDOW; + else if (!where) + where = dactyl.CURRENT_TAB; + + return urls.map(function (url) { + let res = open(url, where); + where = dactyl.NEW_TAB; + background = true; + return res; + }); + }, + + /** + * Returns an array of URLs parsed from *str*. + * + * Given a string like 'google bla, www.osnews.com' return an array + * ['www.google.com/search?q=bla', 'www.osnews.com'] + * + * @param {string} str + * @returns {string[]} + */ + parseURLs: function parseURLs(str) { + let urls; + + if (options["urlseparator"]) + urls = util.splitLiteral(str, util.regexp("\\s*" + options["urlseparator"] + "\\s*")); + else + urls = [str]; + + return urls.map(function (url) { + url = url.trim(); + + if (/^(\.{0,2}|~)(\/|$)/.test(url)) { + try { + // Try to find a matching file. + let file = io.File(url); + if (file.exists() && file.isReadable()) + return services.io.newFileURI(file).spec; + } + catch (e) {} + } + + // If it starts with a valid protocol, pass it through. + let proto = /^([-\w]+):/.exec(url); + if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc) + return url.replace(/\s+/g, ""); + + // Check for a matching search keyword. + let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false); + if (searchURL) + return searchURL; + + // If it looks like URL-ish (foo.com/bar), let Gecko figure it out. + if (this.urlish.test(url) || !this.has("bookmarks")) + return util.createURI(url).spec; + + // Pass it off to the default search engine or, failing + // that, let Gecko deal with it as is. + return bookmarks.getSearchURL(url, true) || util.createURI(url).spec; + }, this); + }, + stringToURLArray: deprecated("dactyl.parseURLs", "parseURLs"), + urlish: Class.memoize(function () util.regexp(<![CDATA[ + ^ ( + <domain>+ (:\d+)? (/ .*) | + <domain>+ (:\d+) | + <domain>+ \. [a-z0-9]+ | + localhost + ) $ + ]]>, "ix", { + domain: util.regexp(String.replace(<![CDATA[ + [^ + U0000-U002c // U002d-U002e --. + U002f // / + // U0030-U0039 0-9 + U003a-U0040 // U0041-U005a a-z + U005b-U0060 // U0061-U007a A-Z + U007b-U007f + ] + ]]>, /U/g, "\\u"), "x") + })), + + pluginFiles: {}, + + get plugins() plugins, + + setNodeVisible: function setNodeVisible(node, visible) { + if (window.setToolbarVisibility && node.localName == "toolbar") + window.setToolbarVisibility(node, visible); + else + node.collapsed = !visible; + }, + + confirmQuit: function confirmQuit() + prefs.withContext(function () { + prefs.set("browser.warnOnQuit", false); + return window.canQuitApplication(); + }), + + /** + * Quit the host application, no matter how many tabs/windows are open. + * + * @param {boolean} saveSession If true the current session will be + * saved and restored when the host application is restarted. + * @param {boolean} force Forcibly quit irrespective of whether all + * windows could be closed individually. + */ + quit: function (saveSession, force) { + if (!force && !this.confirmQuit()) + return; + + let pref = "browser.startup.page"; + prefs.save(pref); + if (saveSession) + prefs.safeSet(pref, 3); + if (!saveSession && prefs.get(pref) >= 2) + prefs.safeSet(pref, 1); + + services.appStartup.quit(Ci.nsIAppStartup[force ? "eForceQuit" : "eAttemptQuit"]); + }, + + /** + * Restart the host application. + */ + restart: function () { + if (!this.confirmQuit()) + return; + + services.appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + }, + + get assert() util.assert, + + /** + * Traps errors in the called function, possibly reporting them. + * + * @param {function} func The function to call + * @param {object} self The 'this' object for the function. + */ + trapErrors: function trapErrors(func, self) { + try { + if (isString(func)) + func = self[func]; + return func.apply(self || this, Array.slice(arguments, 2)); + } + catch (e) { + dactyl.reportError(e, true); + return e; + } + }, + + /** + * Reports an error to both the console and the host application's + * Error Console. + * + * @param {Object} error The error object. + */ + reportError: function reportError(error, echo) { + if (error instanceof FailedAssertion && error.noTrace || error.message === "Interrupted") { + let context = contexts.context; + let prefix = context ? context.file + ":" + context.line + ": " : ""; + if (error.message && error.message.indexOf(prefix) !== 0) + error.message = prefix + error.message; + + if (error.message) + dactyl.echoerr(template.linkifyHelp(error.message)); + else + dactyl.beep(); + + if (!error.noTrace) + util.reportError(error); + return; + } + if (error.result == Cr.NS_BINDING_ABORTED) + return; + if (echo) + dactyl.echoerr(error, commandline.FORCE_SINGLELINE); + else + util.reportError(error); + }, + + /** + * Parses a Dactyl command-line string i.e. the value of the + * -dactyl command-line option. + * + * @param {string} cmdline The string to parse for command-line + * options. + * @returns {Object} + * @see Commands#parseArgs + */ + parseCommandLine: function (cmdline) { + try { + return commands.get("rehash").parseArgs(cmdline); + } + catch (e) { + dactyl.reportError(e, true); + return []; + } + }, + + wrapCallback: function (callback, self) { + self = self || this; + let save = ["forceNewTab", "forceNewWindow"]; + let saved = save.map(function (p) dactyl[p]); + return function wrappedCallback() { + let args = arguments; + return dactyl.withSavedValues(save, function () { + saved.forEach(function (p, i) dactyl[save[i]] = p); + try { + return callback.apply(self, args); + } + catch (e) { + dactyl.reportError(e, true); + } + }); + } + }, + + /** + * @property {Window[]} Returns an array of all the host application's + * open windows. + */ + get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser")))], + +}, { + // initially hide all GUI elements, they are later restored unless the user + // has :set go= or something similar in his config + hideGUI: function () { + let guioptions = config.guioptions; + for (let option in guioptions) { + guioptions[option].forEach(function (elem) { + try { + document.getElementById(elem).collapsed = true; + } + catch (e) {} + }); + } + }, + + // TODO: move this + getMenuItems: function () { + function addChildren(node, parent) { + for (let [, item] in Iterator(node.childNodes)) { + if (item.childNodes.length == 0 && item.localName == "menuitem" + && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME + item.fullMenuPath = parent + item.getAttribute("label"); + items.push(item); + } + else { + let path = parent; + if (item.localName == "menu") + path += item.getAttribute("label") + "."; + addChildren(item, path); + } + } + } + + let items = []; + addChildren(document.getElementById(config.guioptions["m"][1]), ""); + return items; + } +}, { + events: function () { + events.listen(window, "click", dactyl.closure.onClick, true); + events.listen(window, "dactyl.execute", dactyl.closure.onExecute, true); + }, + // Only general options are added here, which are valid for all Dactyl extensions + options: function () { + options.add(["errorbells", "eb"], + "Ring the bell when an error message is displayed", + "boolean", false); + + options.add(["exrc", "ex"], + "Enable automatic sourcing of an RC file in the current directory at startup", + "boolean", false); + + options.add(["fullscreen", "fs"], + "Show the current window fullscreen", + "boolean", false, { + setter: function (value) window.fullScreen = value, + getter: function () window.fullScreen + }); + + const groups = [ + { + opts: { + c: ["Always show the command line, even when empty"], + C: ["Always show the command line outside of the status line"], + M: ["Always show messages outside of the status line"] + }, + setter: function (opts) { + if (loaded.commandline) + commandline.widgets.updateVisibility(); + } + }, + { + opts: update({ + s: ["Status bar", [statusline.statusBar.id]] + }, config.guioptions), + setter: function (opts) { + for (let [opt, [, ids]] in Iterator(this.opts)) { + ids.map(function (id) document.getElementById(id)) + .forEach(function (elem) { + if (elem) + dactyl.setNodeVisible(elem, opts.indexOf(opt) >= 0); + }); + } + } + }, + { + opts: { + r: ["Right Scrollbar", "vertical"], + l: ["Left Scrollbar", "vertical"], + b: ["Bottom Scrollbar", "horizontal"] + }, + setter: function (opts) { + let dir = ["horizontal", "vertical"].filter( + function (dir) !Array.some(opts, + function (o) this.opts[o] && this.opts[o][1] == dir, this), + this); + let class_ = dir.map(function (dir) "html|html > xul|scrollbar[orient=" + dir + "]"); + + styles.system.add("scrollbar", "*", + class_.length ? class_.join(", ") + " { visibility: collapse !important; }" : "", + true); + + prefs.safeSet("layout.scrollbar.side", opts.indexOf("l") >= 0 ? 3 : 2, + "See 'guioptions' scrollbar flags."); + }, + validator: function (opts) Option.validIf(!(opts.indexOf("l") >= 0 && opts.indexOf("r") >= 0), + UTF8("Only one of ‘l’ or ‘r’ allowed")) + }, + { + feature: "tabs", + opts: { + n: ["Tab number", highlight.selector("TabNumber")], + N: ["Tab number over icon", highlight.selector("TabIconNumber")] + }, + setter: function (opts) { + let classes = [v[1] for ([k, v] in Iterator(this.opts)) if (opts.indexOf(k) < 0)]; + + styles.system.add("taboptions", "chrome://*", + classes.length ? classes.join(",") + "{ display: none; }" : ""); + + if (!dactyl.has("Gecko2")) { + tabs.tabBinding.enabled = Array.some(opts, function (k) k in this.opts, this); + tabs.updateTabCount(); + } + if (config.tabbrowser.tabContainer._positionPinnedTabs) + config.tabbrowser.tabContainer._positionPinnedTabs(); + }, + /* + validator: function (opts) dactyl.has("Gecko2") || + Option.validIf(!/[nN]/.test(opts), "Tab numbering not available in this " + config.host + " version") + */ + } + ].filter(function (group) !group.feature || dactyl.has(group.feature)); + + options.add(["guioptions", "go"], + "Show or hide certain GUI elements like the menu or toolbar", + "charlist", config.defaults.guioptions || "", { + + // FIXME: cleanup + cleanupValue: config.cleanups.guioptions || + "r" + [k for ([k, v] in iter(groups[1].opts)) + if (!document.getElementById(v[1][0]).collapsed)].join(""), + + values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(), + + setter: function (value) { + for (let group in values(groups)) + group.setter(value); + events.checkFocus(); + return value; + }, + validator: function (val) Option.validateCompleter.call(this, val) && + groups.every(function (g) !g.validator || g.validator(val)) + }); + + options.add(["helpfile", "hf"], + "Name of the main help file", + "string", "intro"); + + options.add(["loadplugins", "lpl"], + "A regexp list that defines which plugins are loaded at startup and via :loadplugins", + "regexplist", "'\\.(js|" + config.fileExtension + ")$'"); + + options.add(["titlestring"], + "The string shown at the end of the window title", + "string", config.defaults.titlestring || config.host, + { + setter: function (value) { + let win = document.documentElement; + function updateTitle(old, current) { + document.title = document.title.replace(RegExp("(.*)" + util.regexp.escape(old)), "$1" + current); + } + + if (services.has("privateBrowsing")) { + let oldValue = win.getAttribute("titlemodifier_normal"); + let suffix = win.getAttribute("titlemodifier_privatebrowsing").substr(oldValue.length); + + win.setAttribute("titlemodifier_normal", value); + win.setAttribute("titlemodifier_privatebrowsing", value + suffix); + + if (services.privateBrowsing.privateBrowsingEnabled) { + updateTitle(oldValue + suffix, value + suffix); + return value; + } + } + + updateTitle(win.getAttribute("titlemodifier"), value); + win.setAttribute("titlemodifier", value); + + return value; + } + }); + + options.add(["urlseparator", "urlsep", "us"], + "The regular expression used to separate multiple URLs in :open and friends", + "string", "\\|", + { validator: function (value) RegExp(value) }); + + options.add(["verbose", "vbs"], + "Define which info messages are displayed", + "number", 1, + { validator: function (value) Option.validIf(value >= 0 && value <= 15, "Value must be between 0 and 15") }); + + options.add(["visualbell", "vb"], + "Use visual bell instead of beeping on errors", + "boolean", false, + { + setter: function (value) { + prefs.safeSet("accessibility.typeaheadfind.enablesound", !value, + "See 'visualbell' option"); + return value; + } + }); + }, + + mappings: function () { + mappings.add([modes.MAIN], ["<F1>"], + "Open the introductory help page", + function () { dactyl.help(); }); + + mappings.add([modes.MAIN], ["<A-F1>"], + "Open the single, consolidated help page", + function () { ex.helpall(); }); + + if (dactyl.has("session")) + mappings.add([modes.NORMAL], ["ZQ"], + "Quit and don't save the session", + function () { dactyl.quit(false); }); + + mappings.add([modes.NORMAL], ["ZZ"], + "Quit and save the session", + function () { dactyl.quit(true); }); + }, + + commands: function () { + commands.add(["dia[log]"], + "Open a " + config.appName + " dialog", + function (args) { + let dialog = args[0]; + + dactyl.assert(dialog in config.dialogs, + _("error.invalidArgument", dialog)); + dactyl.assert(!config.dialogs[dialog][2] || config.dialogs[dialog][2](), + _("dialog.notAvailable", dialog)); + try { + config.dialogs[dialog][1](); + } + catch (e) { + dactyl.echoerr(_("error.cantOpen", dialog.quote(), e.message || e)); + } + }, { + argCount: "1", + bang: true, + completer: function (context) { + context.ignoreCase = true; + completion.dialog(context); + } + }); + + commands.add(["em[enu]"], + "Execute the specified menu item from the command line", + function (args) { + let arg = args[0] || ""; + let items = Dactyl.getMenuItems(); + + dactyl.assert(items.some(function (i) i.fullMenuPath == arg), + _("emenu.notFound", arg)); + + for (let [, item] in Iterator(items)) { + if (item.fullMenuPath == arg) + item.doCommand(); + } + }, { + argCount: "1", + completer: function (context) completion.menuItem(context), + literal: 0 + }); + + commands.add(["exe[cute]"], + "Execute the argument as an Ex command", + function (args) { + try { + let cmd = dactyl.userEval(args[0] || ""); + dactyl.execute(cmd || "", null, true); + } + catch (e) { + dactyl.echoerr(e); + } + }, { + completer: function (context) completion.javascript(context), + literal: 0 + }); + + [ + { + name: "h[elp]", + description: "Open the introductory help page" + }, { + name: "helpa[ll]", + description: "Open the single consolidated help page" + } + ].forEach(function (command) { + let consolidated = command.name == "helpa[ll]"; + + commands.add([command.name], + command.description, + function (args) { + dactyl.assert(!args.bang, _("help.dontPanic")); + dactyl.help(args.literalArg, consolidated); + }, { + argCount: "?", + bang: true, + completer: function (context) completion.help(context, consolidated), + literal: 0 + }); + }); + + commands.add(["loadplugins", "lpl"], + "Load all plugins immediately", + function (args) { + dactyl.loadPlugins(args.length ? args : null, args.bang); + }, + { + argCount: "*", + bang: true, + keepQuotes: true, + serialGroup: 10, + serialize: function () [ + { + command: this.name, + literalArg: options["loadplugins"].join(" ") + } + ] + }); + + commands.add(["norm[al]"], + "Execute Normal mode commands", + function (args) { events.feedkeys(args[0], args.bang, false, modes.NORMAL); }, + { + argCount: "1", + bang: true, + literal: 0 + }); + + commands.add(["q[uit]"], + dactyl.has("tabs") ? "Quit current tab" : "Quit application", + function (args) { + if (dactyl.has("tabs") && tabs.remove(tabs.getTab(), 1, false)) + return; + else if (dactyl.windows.length > 1) + window.close(); + else + dactyl.quit(false, args.bang); + }, { + argCount: "0", + bang: true + }); + + commands.add(["reh[ash]"], + "Reload the " + config.appName + " add-on", + function (args) { + if (args.trailing) + JSMLoader.rehashCmd = args.trailing; // Hack. + args.break = true; + util.rehash(args); + }, + { + argCount: "0", + options: [ + { + names: ["+u"], + description: "The initialization file to execute at startup", + type: CommandOption.STRING + }, + { + names: ["++noplugin"], + description: "Do not automatically load plugins" + }, + { + names: ["++cmd"], + description: "Ex commands to execute prior to initialization", + type: CommandOption.STRING, + multiple: true + }, + { + names: ["+c"], + description: "Ex commands to execute after initialization", + type: CommandOption.STRING, + multiple: true + } + ] + }); + + commands.add(["res[tart]"], + "Force " + config.appName + " to restart", + function () { dactyl.restart(); }); + + function findToolbar(name) util.evaluateXPath( + "//*[@toolbarname=" + util.escapeString(name, "'") + "]", + document).snapshotItem(0); + + var toolbox = document.getElementById("navigator-toolbox"); + if (toolbox) { + let hidden = function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"; + + let toolbarCommand = function (names, desc, action, filter) { + commands.add(names, desc, + function (args) { + let toolbar = findToolbar(args[0] || ""); + dactyl.assert(toolbar, _("error.invalidArgument")); + action(toolbar); + events.checkFocus(); + }, { + argcount: "1", + completer: function (context) { + completion.toolbar(context); + if (filter) + context.filters.push(filter); + }, + literal: 0 + }); + }; + + toolbarCommand(["toolbars[how]", "tbs[how]"], "Show the named toolbar", + function (toolbar) dactyl.setNodeVisible(toolbar, true), + function ({ item }) hidden(item)); + toolbarCommand(["toolbarh[ide]", "tbh[ide]"], "Hide the named toolbar", + function (toolbar) dactyl.setNodeVisible(toolbar, false), + function ({ item }) !hidden(item)); + toolbarCommand(["toolbart[oggle]", "tbt[oggle]"], "Toggle the named toolbar", + function (toolbar) dactyl.setNodeVisible(toolbar, hidden(toolbar))); + } + + 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[0] || ""; + + if (args[0] == ":") + var method = function () commands.execute(args, null, true); + else + method = dactyl.userFunc(args); + + try { + if (count > 1) { + let each, eachUnits, totalUnits; + let total = 0; + + for (let i in util.interruptibleRange(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"; + + commandline.commandOutput( + <table> + <tr highlight="Title" align="left"> + <th colspan="3">Code execution summary</th> + </tr> + <tr><td>  Executed:</td><td align="right"><span class="times-executed">{count}</span></td><td>times</td></tr> + <tr><td>  Average time:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr> + <tr><td>  Total time:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr> + </table>); + } + else { + let beforeTime = Date.now(); + method(); + + if (special) + return; + + let afterTime = Date.now(); + + if (afterTime - beforeTime >= 100) + dactyl.echo(_("time.total", ((afterTime - beforeTime) / 1000.0).toFixed(2) + " sec")); + else + dactyl.echo(_("time.total", (afterTime - beforeTime) + " msec")); + } + } + catch (e) { + dactyl.echoerr(e); + } + }, { + argCount: "+", + bang: true, + completer: function (context) { + if (/^:/.test(context.filter)) + return completion.ex(context); + else + return completion.javascript(context); + }, + count: true, + hereDoc: true, + literal: 0, + subCommand: 0 + }); + + commands.add(["verb[ose]"], + "Execute a command with 'verbose' set", + function (args) { + let vbs = options.get("verbose"); + let value = vbs.value; + let setFrom = vbs.setFrom; + + try { + vbs.set(args.count || 1); + vbs.setFrom = null; + dactyl.execute(args[0] || "", null, true); + } + finally { + vbs.set(value); + vbs.setFrom = setFrom; + } + }, { + argCount: "+", + completer: function (context) completion.ex(context), + count: true, + literal: 0, + subCommand: 0 + }); + + commands.add(["ve[rsion]"], + "Show version information", + function (args) { + if (args.bang) + dactyl.open("about:"); + else + commandline.commandOutput(<> + {config.appName} {config.version} running on:<br/>{navigator.userAgent} + </>); + }, { + argCount: "0", + bang: true + }); + + }, + + completion: function () { + completion.dialog = function dialog(context) { + context.title = ["Dialog"]; + context.filters.push(function ({ item }) !item[2] || item[2]()); + context.completions = [[k, v[0], v[2]] for ([k, v] in Iterator(config.dialogs))]; + }; + + completion.help = function help(context, consolidated) { + dactyl.initHelp(); + context.title = ["Help"]; + context.anchored = false; + context.completions = services["dactyl:"].HELP_TAGS; + if (consolidated) + context.keys = { text: 0, description: function () "all" }; + }; + + completion.menuItem = function menuItem(context) { + context.title = ["Menu Path", "Label"]; + context.anchored = false; + context.keys = { text: "fullMenuPath", description: function (item) item.getAttribute("label") }; + context.completions = dactyl.menuItems; + }; + + var toolbox = document.getElementById("navigator-toolbox"); + completion.toolbar = function toolbar(context) { + context.title = ["Toolbar"]; + context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" }; + context.completions = util.evaluateXPath("//*[@toolbarname]", document); + }; + + completion.window = function window(context) { + context.title = ["Window", "Title"]; + context.keys = { text: function (win) dactyl.windows.indexOf(win) + 1, description: function (win) win.document.title }; + context.completions = dactyl.windows; + }; + }, + load: function () { + dactyl.triggerObserver("load"); + + dactyl.log(_("dactyl.modulesLoaded"), 3); + + dactyl.timeout(function () { + try { + var args = JSMLoader.commandlineArgs || services.commandLineHandler.optionValue; + if (isString(args)) + args = dactyl.parseCommandLine(args); + + if (args) { + dactyl.commandLineOptions.rcFile = args["+u"]; + dactyl.commandLineOptions.noPlugins = "++noplugin" in args; + dactyl.commandLineOptions.postCommands = args["+c"]; + dactyl.commandLineOptions.preCommands = args["++cmd"]; + util.dump("Processing command-line option: " + args.string); + } + } + catch (e) { + dactyl.echoerr(_("dactyl.parsingCommandLine", e)); + } + + dactyl.log(_("dactyl.commandlineOpts", util.objectToString(dactyl.commandLineOptions)), 3); + + // first time intro message + const firstTime = "extensions." + config.name + ".firsttime"; + if (prefs.get(firstTime, true)) { + dactyl.timeout(function () { + this.withSavedValues(["forceNewTab"], function () { + this.forceNewTab = true; + this.help(); + prefs.set(firstTime, false); + }); + }, 1000); + } + + // TODO: we should have some class where all this guioptions stuff fits well + // Dactyl.hideGUI(); + + if (dactyl.userEval("typeof document", null, "test.js") === "undefined") + jsmodules.__proto__ = XPCSafeJSObjectWrapper(window); + + if (dactyl.commandLineOptions.preCommands) + dactyl.commandLineOptions.preCommands.forEach(function (cmd) { + dactyl.execute(cmd); + }); + + // finally, read the RC file and source plugins + let init = services.environment.get(config.idName + "_INIT"); + let rcFile = io.getRCFile("~"); + + try { + if (dactyl.commandLineOptions.rcFile) { + let filename = dactyl.commandLineOptions.rcFile; + if (!/^(NONE|NORC)$/.test(filename)) + io.source(io.File(filename).path, { group: contexts.user }); + } + else { + if (init) + dactyl.execute(init); + else { + if (rcFile) { + io.source(rcFile.path, { group: contexts.user }); + services.environment.set("MY_" + config.idName + "RC", rcFile.path); + } + else + dactyl.log(_("dactyl.noRCFile"), 3); + } + + if (options["exrc"] && !dactyl.commandLineOptions.rcFile) { + let localRCFile = io.getRCFile(io.cwd); + if (localRCFile && !localRCFile.equals(rcFile)) + io.source(localRCFile.path, { group: contexts.user }); + } + } + + if (dactyl.commandLineOptions.rcFile == "NONE" || dactyl.commandLineOptions.noPlugins) + options["loadplugins"] = []; + + if (options["loadplugins"]) + dactyl.loadPlugins(); + } + catch (e) { + dactyl.reportError(e, true); + } + + // 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 values(options.needInit)) + option.initValue(); + + if (dactyl.commandLineOptions.postCommands) + dactyl.commandLineOptions.postCommands.forEach(function (cmd) { + dactyl.execute(cmd); + }); + + if (JSMLoader.rehashCmd) + dactyl.execute(JSMLoader.rehashCmd); + JSMLoader.rehashCmd = null; + + dactyl.fullyInitialized = true; + dactyl.triggerObserver("enter", null); + autocommands.trigger("Enter", {}); + }, 100); + + statusline.update(); + dactyl.log(_("dactyl.initialized", config.appName), 0); + dactyl.initialized = true; + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/disable-acr.jsm b/common/content/disable-acr.jsm new file mode 100644 index 0000000..80b9032 --- /dev/null +++ b/common/content/disable-acr.jsm @@ -0,0 +1,72 @@ +// By Kris Maglione. Public Domain. +// Please feel free to copy and use at will. + +var ADDON_ID; + +const OVERLAY_URLS = [ + "about:addons", + "chrome://mozapps/content/extensions/extensions.xul" +]; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function observe(window, topic, url) { + if (topic === "chrome-document-global-created") + checkDocument(window.document); +} +function init(id) { + if (id) + ADDON_ID = id; + + Services.obs[id ? "addObserver" : "removeObserver"](observe, "chrome-document-global-created", false); + for (let doc in chromeDocuments) + checkDocument(doc, !id); +} +function cleanup() { init(null); } + +function checkPopup(event) { + let doc = event.originalTarget.ownerDocument; + let binding = doc.getBindingParent(event.originalTarget); + if (binding && binding.addon && binding.addon.guid == ADDON_ID && !binding.addon.compatible) { + let elem = doc.getAnonymousElementByAttribute(binding, "anonid", "stillworks"); + if (elem && elem.nextSibling) { + elem.nextSibling.disabled = true; + elem.nextSibling.setAttribute("tooltiptext", "Developer has opted out of incompatibility reports\n"+ + "Development versions are available with updated support"); + } + } +} + +function checkDocument(doc, disable, force) { + if (["interactive", "complete"].indexOf(doc.readyState) >= 0 || force && doc.readyState === "uninitialized") { + if (OVERLAY_URLS.indexOf(doc.documentURI) >= 0) + doc[disable ? "removeEventListener" : "addEventListener"]("popupshowing", checkPopup, false); + } + else { + doc.addEventListener("DOMContentLoaded", function listener() { + doc.removeEventListener("DOMContentLoaded", listener, false); + checkDocument(doc, disable, true); + }, false); + } +} + +function chromeDocuments() { + let windows = services.windowMediator.getXULWindowEnumerator(null); + while (windows.hasMoreElements()) { + let window = windows.getNext().QueryInterface(Ci.nsIXULWindow); + for each (let type in ["typeChrome", "typeContent"]) { + let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type], + Ci.nsIDocShell.ENUMERATE_FORWARDS); + while (docShells.hasMoreElements()) + yield docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer.DOMDocument; + } + } +} + +var EXPORTED_SYMBOLS = ["cleanup", "init"]; + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/content/editor.js b/common/content/editor.js new file mode 100644 index 0000000..e1df653 --- /dev/null +++ b/common/content/editor.js @@ -0,0 +1,884 @@ +// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +// command names taken from: +// http://developer.mozilla.org/en/docs/Editor_Embedding_Guide + +/** @instance editor */ +var Editor = Module("editor", { + get isCaret() modes.getStack(1).main == modes.CARET, + get isTextEdit() modes.getStack(1).main == modes.TEXT_EDIT, + + unselectText: function (toEnd) { + try { + Editor.getEditor(null).selection[toEnd ? "collapseToEnd" : "collapseToStart"](); + } + catch (e) {} + }, + + selectedText: function () String(Editor.getEditor(null).selection), + + pasteClipboard: function (clipboard, toStart) { + // TODO: I don't think this is needed anymore? --djk + if (util.OS.isWindows) { + this.executeCommand("cmd_paste"); + return; + } + + let elem = dactyl.focusedElement; + if (elem.inputField) + elem = elem.inputField; + + if (elem.setSelectionRange) { + let text = dactyl.clipboardRead(clipboard); + if (!text) + return; + if (isinstance(elem, [HTMLInputElement, XULTextBoxElement])) + text = text.replace(/\n+/g, ""); + + // This is a hacky fix - but it works. + // <s-insert> in the bottom of a long textarea bounces up + let top = elem.scrollTop; + let left = elem.scrollLeft; + + let start = elem.selectionStart; // caret position + let end = elem.selectionEnd; + let value = elem.value.substring(0, start) + text + elem.value.substring(end); + elem.value = value; + + if (/^(search|text)$/.test(elem.type)) + Editor.getEditor(elem).rootElement.firstChild.textContent = value; + + elem.selectionStart = Math.min(start + (toStart ? 0 : text.length), elem.value.length); + elem.selectionEnd = elem.selectionStart; + + elem.scrollTop = top; + elem.scrollLeft = left; + + events.dispatch(elem, events.create(elem.ownerDocument, "input")); + } + }, + + // count is optional, defaults to 1 + executeCommand: function (cmd, count) { + let editor = Editor.getEditor(null); + let controller = Editor.getController(); + dactyl.assert(callable(cmd) || + controller && + controller.supportsCommand(cmd) && + controller.isCommandEnabled(cmd)); + + // XXX: better as a precondition + if (count == null) + count = 1; + + let 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 { + if (callable(cmd)) + cmd(editor, controller); + else + controller.doCommand(cmd); + didCommand = true; + } + catch (e) { + util.reportError(e); + dactyl.assert(didCommand); + break; + } + } + }, + + // cmd = y, d, c + // motion = b, 0, gg, G, etc. + selectMotion: function selectMotion(cmd, motion, count) { + // XXX: better as a precondition + if (count == null) + count = 1; + + if (cmd == motion) { + motion = "j"; + count--; + } + + if (modes.main != modes.VISUAL) + modes.push(modes.VISUAL); + + 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: + dactyl.beep(); + return; + } + }, + + // 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 Gecko bug always makes selectionStart <= selectionEnd) + // Use only for small movements! + moveToPosition: function (pos, forward, select) { + if (!select) { + Editor.getEditor().setSelectionRange(pos, pos); + return; + } + + if (forward) { + if (pos <= Editor.getEditor().selectionEnd || pos > Editor.getEditor().value.length) + return; + + do { // TODO: test code for endless loops + this.executeCommand("cmd_selectCharNext", 1); + } + while (Editor.getEditor().selectionEnd != pos); + } + else { + if (pos >= Editor.getEditor().selectionStart || pos < 0) + return; + + do { // TODO: test code for endless loops + this.executeCommand("cmd_selectCharPrevious", 1); + } + while (Editor.getEditor().selectionStart != pos); + } + }, + + // returns the position of char + findCharForward: function (ch, count) { + if (!Editor.getEditor()) + return -1; + + let text = Editor.getEditor().value; + // XXX + if (count == null) + count = 1; + + for (let i = Editor.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 + } + + dactyl.beep(); + return -1; + }, + + // returns the position of char + findCharBackward: function (ch, count) { + if (!Editor.getEditor()) + return -1; + + let text = Editor.getEditor().value; + // XXX + if (count == null) + count = 1; + + for (let i = Editor.getEditor().selectionStart - 1; i >= 0; i--) { + if (text[i] == "\n") + break; + if (text[i] == ch) + count--; + if (count == 0) + return i; + } + + dactyl.beep(); + return -1; + }, + + /** + * Edits the given file in the external editor as specified by the + * 'editor' option. + * + * @param {object|File|string} args An object specifying the file, + * line, and column to edit. If a non-object is specified, it is + * treated as the file parameter of the object. + * @param {boolean} blocking If true, this function does not return + * until the editor exits. + */ + editFileExternally: function (args, blocking) { + if (!isObject(args) || args instanceof File) + args = { file: args }; + args.file = args.file.path || args.file; + + let args = options.get("editor").format(args); + + dactyl.assert(args.length >= 1, _("editor.noEditor")); + + io.run(args.shift(), args, blocking); + }, + + // TODO: clean up with 2 functions for textboxes and currentEditor? + editFieldExternally: function editFieldExternally(forceEditing) { + if (!options["editor"]) + return; + + let textBox = config.isComposeWindow ? null : dactyl.focusedElement; + let line, column; + + if (!forceEditing && textBox && textBox.type == "password") { + commandline.input("Editing a password field externally will reveal the password. Would you like to continue? (yes/[no]): ", + function (resp) { + if (resp && resp.match(/^y(es)?$/i)) + editor.editFieldExternally(true); + }); + return; + } + + if (textBox) { + var text = textBox.value; + let pre = text.substr(0, textBox.selectionStart); + line = 1 + pre.replace(/[^\n]/g, "").length; + column = 1 + pre.replace(/[^]*\n/, "").length; + } + else { + var editor = window.GetCurrentEditor ? GetCurrentEditor() + : Editor.getEditor(document.commandDispatcher.focusedWindow); + dactyl.assert(editor); + text = Array.map(editor.rootElement.childNodes, function (e) util.domToString(e, true)).join(""); + } + + let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || ""; + let cleanup = util.yieldable(function cleanup(error) { + if (timer) + timer.cancel(); + + let blink = ["EditorBlink1", "EditorBlink2"]; + if (error) { + dactyl.reportError(error, true); + blink[1] = "EditorError"; + } + else + dactyl.trapErrors(update, null, true); + + if (tmpfile && tmpfile.exists()) + tmpfile.remove(false); + + if (textBox) { + dactyl.focus(textBox); + for (let group in values(blink.concat(blink, ""))) { + highlight.highlightNode(textBox, origGroup + " " + group); + yield 100; + } + } + }); + + function update(force) { + if (force !== true && tmpfile.lastModifiedTime <= lastUpdate) + return; + lastUpdate = Date.now(); + + let val = tmpfile.read(); + if (textBox) + textBox.value = val; + else { + while (editor.rootElement.firstChild) + editor.rootElement.removeChild(editor.rootElement.firstChild); + editor.rootElement.innerHTML = val; + } + } + + try { + var tmpfile = io.createTempFile(); + if (!tmpfile) + throw Error("Couldn't create temporary file"); + + if (textBox) { + highlight.highlightNode(textBox, origGroup + " EditorEditing"); + textBox.blur(); + } + + if (!tmpfile.write(text)) + throw Error("Input contains characters not valid in the current " + + "file encoding"); + + var lastUpdate = Date.now(); + + var timer = services.Timer(update, 100, services.Timer.TYPE_REPEATING_SLACK); + this.editFileExternally({ file: tmpfile.path, line: line, column: column }, cleanup); + } + catch (e) { + cleanup(e); + } + }, + + /** + * Expands an abbreviation in the currently active textbox. + * + * @param {string} mode The mode filter. + * @see Abbreviation#expand + */ + expandAbbreviation: function (mode) { + let elem = dactyl.focusedElement; + if (!(elem && elem.value)) + return; + + let text = elem.value; + let start = elem.selectionStart; + let end = elem.selectionEnd; + let abbrev = abbreviations.match(mode, text.substring(0, start).replace(/.*\s/g, "")); + if (abbrev) { + let len = abbrev.lhs.length; + let rhs = abbrev.expand(elem); + elem.value = text.substring(0, start - len) + rhs + text.substring(start); + elem.selectionStart = start - len + rhs.length; + elem.selectionEnd = end - len + rhs.length; + } + }, +}, { + extendRange: function extendRange(range, forward, re, sameWord) { + function advance(positive) { + let idx = range.endOffset; + while (idx < text.length && re.test(text[idx++]) == positive) + range.setEnd(range.endContainer, idx); + } + function retreat(positive) { + let idx = range.startOffset; + while (idx > 0 && re.test(text[--idx]) == positive) + range.setStart(range.startContainer, idx); + } + + let nodeRange = range.cloneRange(); + nodeRange.selectNodeContents(range.startContainer); + let text = String(nodeRange); + + if (forward) { + advance(true); + if (!sameWord) + advance(false); + } + else { + if (!sameWord) + retreat(false); + retreat(true); + } + return range; + }, + + getEditor: function (elem) { + if (arguments.length === 0) { + dactyl.assert(dactyl.focusedElement); + return dactyl.focusedElement; + } + + if (!elem) + elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow; + dactyl.assert(elem); + + if (elem instanceof Element) + return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor; + try { + return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession) + .getEditorForWindow(elem); + } + catch (e) { + return null; + } + }, + + getController: function () { + let ed = dactyl.focusedElement; + if (!ed || !ed.controllers) + return null; + + return ed.controllers.getControllerForCommand("cmd_beginLine"); + } +}, { + mappings: function () { + + // add mappings for commands like h,j,k,l,etc. in CARET, VISUAL and TEXT_EDIT mode + function addMovementMap(keys, description, hasCount, caretModeMethod, caretModeArg, textEditCommand, visualTextEditCommand) { + let extraInfo = {}; + if (hasCount) + extraInfo.count = true; + + function caretExecute(arg, again) { + function fixSelection() { + sel.removeAllRanges(); + sel.addRange(RangeFind.endpoint( + RangeFind.nodeRange(buffer.focusedFrame.document.documentElement), + true)); + } + + let controller = buffer.selectionController; + let sel = controller.getSelection(controller.SELECTION_NORMAL); + if (!sel.rangeCount) // Hack. + fixSelection(); + + try { + controller[caretModeMethod](caretModeArg, arg); + } + catch (e) { + dactyl.assert(again && e.result === Cr.NS_ERROR_FAILURE); + fixSelection(); + caretExecute(arg, false); + } + } + + mappings.add([modes.CARET], keys, description, + function ({ count }) { + if (!count) + count = 1; + + while (count--) + caretExecute(false, true); + }, + extraInfo); + + mappings.add([modes.VISUAL], keys, description, + function ({ count }) { + if (!count) + count = 1; + + let editor_ = Editor.getEditor(null); + let controller = buffer.selectionController; + while (count-- && modes.main == modes.VISUAL) { + if (editor.isTextEdit) { + if (callable(visualTextEditCommand)) + visualTextEditCommand(editor_); + else + editor.executeCommand(visualTextEditCommand); + } + else + caretExecute(true, true); + } + }, + extraInfo); + + mappings.add([modes.TEXT_EDIT], keys, description, + function ({ count }) { + if (!count) + count = 1; + + editor.executeCommand(textEditCommand, count); + }, + extraInfo); + } + + // add mappings for commands like i,a,s,c,etc. in TEXT_EDIT mode + function addBeginInsertModeMap(keys, commands, description) { + mappings.add([modes.TEXT_EDIT], keys, description || "", + function () { + commands.forEach(function (cmd) + editor.executeCommand(cmd, 1)); + modes.push(modes.INSERT); + }); + } + + 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"); + } + + function updateRange(editor, forward, re, modify) { + let range = Editor.extendRange(editor.selection.getRangeAt(0), + forward, re, false); + modify(range); + editor.selection.removeAllRanges(); + editor.selection.addRange(range); + } + function move(forward, re) + function _move(editor) { + updateRange(editor, forward, re, function (range) { range.collapse(!forward); }); + } + function select(forward, re) + function _select(editor) { + updateRange(editor, forward, re, function (range) {}); + } + function beginLine(editor_) { + editor.executeCommand("cmd_beginLine"); + move(true, /\S/)(editor_); + } + + // COUNT CARET TEXT_EDIT VISUAL_TEXT_EDIT + addMovementMap(["k", "<Up>"], "Move up one line", + true, "lineMove", false, "cmd_linePrevious", selectPreviousLine); + addMovementMap(["j", "<Down>", "<Return>"], "Move down one line", + true, "lineMove", true, "cmd_lineNext", selectNextLine); + addMovementMap(["h", "<Left>", "<BS>"], "Move left one character", + true, "characterMove", false, "cmd_charPrevious", "cmd_selectCharPrevious"); + addMovementMap(["l", "<Right>", "<Space>"], "Move right one character", + true, "characterMove", true, "cmd_charNext", "cmd_selectCharNext"); + addMovementMap(["b", "<C-Left>"], "Move left one word", + true, "wordMove", false, "cmd_wordPrevious", "cmd_selectWordPrevious"); + addMovementMap(["w", "<C-Right>"], "Move right one word", + true, "wordMove", true, "cmd_wordNext", "cmd_selectWordNext"); + addMovementMap(["B"], "Move left to the previous white space", + true, "wordMove", false, move(false, /\S/), select(false, /\S/)); + addMovementMap(["W"], "Move right to just beyond the next white space", + true, "wordMove", true, move(true, /\S/), select(true, /\S/)); + addMovementMap(["e"], "Move to the end of the current word", + true, "wordMove", true, move(true, /\W/), select(true, /\W/)); + addMovementMap(["E"], "Move right to the next white space", + true, "wordMove", true, move(true, /\s/), select(true, /\s/)); + addMovementMap(["<C-f>", "<PageDown>"], "Move down one page", + true, "pageMove", true, "cmd_movePageDown", "cmd_selectNextPage"); + addMovementMap(["<C-b>", "<PageUp>"], "Move up one page", + true, "pageMove", false, "cmd_movePageUp", "cmd_selectPreviousPage"); + addMovementMap(["gg", "<C-Home>"], "Move to the start of text", + false, "completeMove", false, "cmd_moveTop", "cmd_selectTop"); + addMovementMap(["G", "<C-End>"], "Move to the end of text", + false, "completeMove", true, "cmd_moveBottom", "cmd_selectBottom"); + addMovementMap(["0", "<Home>"], "Move to the beginning of the line", + false, "intraLineMove", false, "cmd_beginLine", "cmd_selectBeginLine"); + addMovementMap(["^"], "Move to the first non-whitespace character of the line", + false, "intraLineMove", false, beginLine, "cmd_selectBeginLine"); + addMovementMap(["$", "<End>"], "Move to the end of the current line", + false, "intraLineMove", true, "cmd_endLine" , "cmd_selectEndLine"); + + addBeginInsertModeMap(["i", "<Insert>"], [], "Insert text before the cursor"); + addBeginInsertModeMap(["a"], ["cmd_charNext"], "Append text after the cursor"); + addBeginInsertModeMap(["I"], ["cmd_beginLine"], "Insert text at the beginning of the line"); + addBeginInsertModeMap(["A"], ["cmd_endLine"], "Append text at the end of the line"); + addBeginInsertModeMap(["s"], ["cmd_deleteCharForward"], "Delete the character in front of the cursor and start insert"); + addBeginInsertModeMap(["S"], ["cmd_deleteToEndOfLine", "cmd_deleteToBeginningOfLine"], "Delete the current line and start insert"); + addBeginInsertModeMap(["C"], ["cmd_deleteToEndOfLine"], "Delete from the cursor to the end of the line and start insert"); + + function addMotionMap(key, desc, cmd, mode) { + mappings.add([modes.TEXT_EDIT], [key], + desc, + function ({ count, motion }) { + editor.selectMotion(key, motion, Math.max(count, 1)); + if (callable(cmd)) + cmd.call(events, Editor.getEditor(null)); + else { + editor.executeCommand(cmd, 1); + modes.pop(modes.TEXT_EDIT); + } + if (mode) + modes.push(mode); + }, + { count: true, motion: true }); + } + + addMotionMap("d", "Delete motion", "cmd_delete"); + addMotionMap("c", "Change motion", "cmd_delete", modes.INSERT); + addMotionMap("y", "Yank motion", "cmd_copy"); + + mappings.add([modes.INPUT], + ["<C-w>"], "Delete previous word", + function () { editor.executeCommand("cmd_deleteWordBackward", 1); }); + + mappings.add([modes.INPUT], + ["<C-u>"], "Delete until beginning of current line", + function () { + // Deletes the whole line. What the hell. + // editor.executeCommand("cmd_deleteToBeginningOfLine", 1); + + editor.executeCommand("cmd_selectBeginLine", 1); + if (Editor.getController().isCommandEnabled("cmd_delete")) + editor.executeCommand("cmd_delete", 1); + }); + + mappings.add([modes.INPUT], + ["<C-k>"], "Delete until end of current line", + function () { editor.executeCommand("cmd_deleteToEndOfLine", 1); }); + + mappings.add([modes.INPUT], + ["<C-a>"], "Move cursor to beginning of current line", + function () { editor.executeCommand("cmd_beginLine", 1); }); + + mappings.add([modes.INPUT], + ["<C-e>"], "Move cursor to end of current line", + function () { editor.executeCommand("cmd_endLine", 1); }); + + mappings.add([modes.INPUT], + ["<C-h>"], "Delete character to the left", + function () { events.feedkeys("<BS>", true); }); + + mappings.add([modes.INPUT], + ["<C-d>"], "Delete character to the right", + function () { editor.executeCommand("cmd_deleteCharForward", 1); }); + + mappings.add([modes.INPUT], + ["<S-Insert>"], "Insert clipboard/selection", + function () { editor.pasteClipboard(); }); + + mappings.add([modes.INPUT, modes.TEXT_EDIT], + ["<C-i>"], "Edit text field with an external editor", + function () { editor.editFieldExternally(); }); + + mappings.add([modes.INPUT], + ["<C-t>"], "Edit text field in Vi mode", + function () { + dactyl.assert(!editor.isTextEdit); + modes.push(modes.TEXT_EDIT); + }); + + mappings.add([modes.INSERT], + ["<Space>", "<Return>"], "Expand insert mode abbreviation", + function () { + editor.expandAbbreviation(modes.INSERT); + return Events.PASS; + }); + + mappings.add([modes.INSERT], + ["<C-]>", "<C-5>"], "Expand insert mode abbreviation", + function () { editor.expandAbbreviation(modes.INSERT); }); + + // text edit mode + mappings.add([modes.TEXT_EDIT], + ["u"], "Undo changes", + function (args) { + editor.executeCommand("cmd_undo", Math.max(args.count, 1)); + editor.unselectText(); + }, + { count: true }); + + mappings.add([modes.TEXT_EDIT], + ["<C-r>"], "Redo undone changes", + function (args) { + editor.executeCommand("cmd_redo", Math.max(args.count, 1)); + editor.unselectText(); + }, + { count: true }); + + mappings.add([modes.TEXT_EDIT], + ["D"], "Delete the characters under the cursor until the end of the line", + function () { editor.executeCommand("cmd_deleteToEndOfLine"); }); + + mappings.add([modes.TEXT_EDIT], + ["o"], "Open line below current", + function () { + editor.executeCommand("cmd_endLine", 1); + modes.push(modes.INSERT); + events.feedkeys("<Return>"); + }); + + mappings.add([modes.TEXT_EDIT], + ["O"], "Open line above current", + function () { + editor.executeCommand("cmd_beginLine", 1); + modes.push(modes.INSERT); + events.feedkeys("<Return>"); + editor.executeCommand("cmd_linePrevious", 1); + }); + + mappings.add([modes.TEXT_EDIT], + ["X"], "Delete character to the left", + function (args) { editor.executeCommand("cmd_deleteCharBackward", Math.max(args.count, 1)); }, + { count: true }); + + mappings.add([modes.TEXT_EDIT], + ["x"], "Delete character to the right", + function (args) { editor.executeCommand("cmd_deleteCharForward", Math.max(args.count, 1)); }, + { count: true }); + + // visual mode + mappings.add([modes.CARET, modes.TEXT_EDIT], + ["v"], "Start visual mode", + function () { modes.push(modes.VISUAL); }); + + mappings.add([modes.VISUAL], + ["v", "V"], "End visual mode", + function () { modes.pop(); }); + + mappings.add([modes.TEXT_EDIT], + ["V"], "Start visual line mode", + function () { + modes.push(modes.VISUAL, modes.LINE); + editor.executeCommand("cmd_beginLine", 1); + editor.executeCommand("cmd_selectLineNext", 1); + }); + + mappings.add([modes.VISUAL], + ["c", "s"], "Change selected text", + function () { + dactyl.assert(editor.isTextEdit); + editor.executeCommand("cmd_cut"); + modes.push(modes.INSERT); + }); + + mappings.add([modes.VISUAL], + ["d", "x"], "Delete selected text", + function () { + dactyl.assert(editor.isTextEdit); + editor.executeCommand("cmd_cut"); + }); + + mappings.add([modes.VISUAL], + ["y"], "Yank selected text", + function () { + if (editor.isTextEdit) { + editor.executeCommand("cmd_copy"); + modes.pop(); + } + else + dactyl.clipboardWrite(buffer.currentWord, true); + }); + + mappings.add([modes.VISUAL, modes.TEXT_EDIT], + ["p"], "Paste clipboard contents", + function ({ count }) { + dactyl.assert(!editor.isCaret); + editor.executeCommand("cmd_paste", count || 1); + modes.pop(modes.TEXT_EDIT); + }, + { count: true }); + + // finding characters + mappings.add([modes.TEXT_EDIT, modes.VISUAL], + ["f"], "Move to a character on the current line after the cursor", + function ({ arg, count }) { + let pos = editor.findCharForward(arg, Math.max(count, 1)); + if (pos >= 0) + editor.moveToPosition(pos, true, modes.main == modes.VISUAL); + }, + { arg: true, count: true }); + + mappings.add([modes.TEXT_EDIT, modes.VISUAL], + ["F"], "Move to a character on the current line before the cursor", + function ({ arg, count }) { + let pos = editor.findCharBackward(arg, Math.max(count, 1)); + if (pos >= 0) + editor.moveToPosition(pos, false, modes.main == modes.VISUAL); + }, + { arg: true, count: true }); + + mappings.add([modes.TEXT_EDIT, modes.VISUAL], + ["t"], "Move before a character on the current line", + function ({ arg, count }) { + let pos = editor.findCharForward(arg, Math.max(count, 1)); + if (pos >= 0) + editor.moveToPosition(pos - 1, true, modes.main == modes.VISUAL); + }, + { arg: true, count: true }); + + mappings.add([modes.TEXT_EDIT, modes.VISUAL], + ["T"], "Move before a character on the current line, backwards", + function ({ arg, count }) { + let pos = editor.findCharBackward(arg, Math.max(count, 1)); + if (pos >= 0) + editor.moveToPosition(pos + 1, false, modes.main == modes.VISUAL); + }, + { arg: true, count: true }); + + // text edit and visual mode + mappings.add([modes.TEXT_EDIT, 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 = Editor.getEditor().selectionEnd - Editor.getEditor().selectionStart; + count = Math.max(count, 1); + + // FIXME: do this in one pass? + while (count-- > 0) { + let text = Editor.getEditor().value; + let pos = Editor.getEditor().selectionStart; + dactyl.assert(pos < text.length); + + let chr = text[pos]; + Editor.getEditor().value = text.substring(0, pos) + + (chr == chr.toLocaleLowerCase() ? chr.toLocaleUpperCase() : chr.toLocaleLowerCase()) + + text.substring(pos + 1); + editor.moveToPosition(pos + 1, true, false); + } + modes.pop(modes.TEXT_EDIT); + }, + { count: true }); + + function bind() mappings.add.apply(mappings, + [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments))) + + bind(["<Esc>"], "Return to INSERT mode", + function () Events.PASS_THROUGH); + + bind(["<C-[>"], "Return to INSERT mode", + function () { events.feedkeys("<Esc>", { skipmap: true }); }); + + bind(["<Up>"], "Select the previous autocomplete result", + function () Events.PASS_THROUGH); + + bind(["<C-p>"], "Select the previous autocomplete result", + function () { events.feedkeys("<Up>", { skipmap: true }); }); + + bind(["<Down>"], "Select the next autocomplete result", + function () Events.PASS_THROUGH); + + bind(["<C-n>"], "Select the next autocomplete result", + function () { events.feedkeys("<Down>", { skipmap: true }); }); + }, + + options: function () { + options.add(["editor"], + "The external text editor", + "string", "gvim -f +<line> <file>", { + format: function (obj, value) { + let args = commands.parseArgs(value || this.value, { argCount: "*", allowUnknownOptions: true }) + .map(util.compileMacro).filter(function (fmt) fmt.valid(obj)) + .map(function (fmt) fmt(obj)); + if (obj["file"] && !this.has("file")) + args.push(obj["file"]); + return args; + }, + has: function (key) set.has(util.compileMacro(this.value).seen, key), + validator: function (value) { + this.format({}, value); + return Object.keys(util.compileMacro(value).seen).every(function (k) ["column", "file", "line"].indexOf(k) >= 0); + } + }); + + options.add(["insertmode", "im"], + "Enter Insert mode rather than Text Edit mode when focusing text areas", + "boolean", 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 0000000..49298d9 --- /dev/null +++ b/common/content/eval.js @@ -0,0 +1,12 @@ +try { __dactyl_eval_result = eval(__dactyl_eval_string); } +catch (e) { __dactyl_eval_error = e; } + +// IMPORTANT: The eval statement *must* remain on the first line +// in order for line numbering in any errors to remain correct. + +// Copyright (c) 2008-2010 by Kris Maglione <maglione.k at Gmail> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. + +// 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 0000000..0b279fd --- /dev/null +++ b/common/content/events.js @@ -0,0 +1,1647 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org> +// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com> +// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +var ProcessorStack = Class("ProcessorStack", { + init: function (mode, hives, builtin) { + this.main = mode.main; + this._actions = []; + this.actions = []; + this.buffer = ""; + this.events = []; + + let main = { __proto__: mode.main, params: mode.params }; + let keyModes = array([mode.params.keyModes, main, mode.main.allBases]).flatten().compact(); + + if (builtin) + hives = hives.filter(function (h) h.name === "builtin"); + + this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h))) + .flatten().array; + this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer); + + for (let [i, input] in Iterator(this.processors)) { + let params = input.main.params; + if (params.preExecute) + input.preExecute = params.preExecute; + if (params.postExecute) + input.postExecute = params.postExecute; + if (params.onKeyPress && input.hive === mappings.builtin) + input.fallthrough = function fallthrough(events) { + return params.onKeyPress(events) === false ? Events.KILL : Events.PASS; + }; + } + + let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"]; + if (!builtin && hive.active + && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement))) + this.processors.unshift(KeyProcessor(modes.BASE, hive)); + }, + + notify: function () { + events.keyEvents = []; + events.processor = null; + if (!this.execute(Events.KILL, true)) { + events.processor = this; + events.keyEvents = this.keyEvents; + } + }, + + _result: function (result) (result === Events.KILL ? "KILL" : + result === Events.PASS ? "PASS" : + result === Events.PASS_THROUGH ? "PASS_THROUGH" : + result === Events.ABORT ? "ABORT" : + callable(result) ? result.toSource().substr(0, 50) : result), + + execute: function execute(result, force) { + + if (force && this.actions.length) + this.processors.length = 0; + + if (this.ownsBuffer) + statusline.inputBuffer = this.processors.length ? this.buffer : ""; + + if (!this.processors.some(function (p) !p.extended) && this.actions.length) { + if (this._actions.length == 0) { + dactyl.beep(); + events.feedingKeys = false; + } + + for (var action in values(this.actions)) { + while (callable(action)) { + action = dactyl.trapErrors(action); + events.dbg("ACTION RES: " + this._result(action)); + } + if (action !== Events.PASS) + break; + } + + result = action !== undefined ? action : Events.KILL; + if (action !== Events.PASS) + this.processors.length = 0; + } + else if (this.processors.length) { + result = Events.KILL; + if (this.actions.length && options["timeout"]) + this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT); + } + else if (result !== Events.KILL && !this.actions.length && + (this.events.length > 1 || + this.processors.some(function (p) !p.main.passUnknown))) { + result = Events.KILL; + if (!Events.isEscape(this.events.slice(-1)[0])) + dactyl.beep(); + events.feedingKeys = false; + } + else if (result === undefined) + result = Events.PASS; + + events.dbg("RESULT: " + this._result(result)); + + if (result === Events.PASS || result === Events.PASS_THROUGH) + if (this.events[0].originalTarget) + this.events[0].originalTarget.dactylKeyPress = undefined; + + if (result !== Events.PASS || this.events.length > 1) + Events.kill(this.events[this.events.length - 1]); + + if (result === Events.PASS_THROUGH) + events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true }); + else if (result === Events.PASS || result === Events.ABORT) { + let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented); + if (list.length) + events.dbg("REFEED: " + list.map(events.closure.toString).join("")); + events.feedevents(null, list, { skipmap: true, isMacro: true, isReplay: true }); + } + + return this.processors.length === 0; + }, + + process: function process(event) { + if (this.timer) + this.timer.cancel(); + + let key = events.toString(event); + this.events.push(event); + if (this.keyEvents) + this.keyEvents.push(event); + + this.buffer += key; + + let actions = []; + let processors = []; + + events.dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay); + + for (let [i, input] in Iterator(this.processors)) { + let res = input.process(event); + if (res !== Events.ABORT) + var result = res; + + events.dbg("RES: " + input + " " + this._result(res)); + + if (res === Events.KILL) + break; + + buffer = buffer || input.inputBuffer; + + if (callable(res)) + actions.push(res); + + if (res === Events.WAIT || input.waiting) + processors.push(input); + if (isinstance(res, KeyProcessor)) + processors.push(res); + } + + events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result)); + events.dbg("ACTIONS: " + actions.length + " " + this.actions.length); + events.dbg("PROCESSORS:", processors); + + this._actions = actions; + this.actions = actions.concat(this.actions); + + if (result === Events.KILL) + this.actions = []; + else if (!this.actions.length && !processors.length) + for (let input in values(this.processors)) + if (input.fallthrough) { + if (result === Events.KILL) + break; + result = dactyl.trapErrors(input.fallthrough, input, this.events); + } + + this.processors = processors; + + return this.execute(result, options["timeout"] && options["timeoutlen"] === 0); + } +}); + +var KeyProcessor = Class("KeyProcessor", { + init: function init(main, hive) { + this.main = main; + this.events = []; + this.hive = hive; + this.wantCount = this.main.count; + }, + + get toStringParams() [this.main.name, this.hive.name], + + countStr: "", + command: "", + get count() this.countStr ? Number(this.countStr) : null, + + append: function append(event) { + this.events.push(event); + let key = events.toString(event); + + if (this.wantCount && !this.command && + (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key)) + this.countStr += key; + else + this.command += key; + return this.events; + }, + + process: function process(event) { + this.append(event); + this.waiting = false; + return this.onKeyPress(event); + }, + + execute: function execute(map, args) + let (self = this) + function execute() { + if (self.preExecute) + self.preExecute.apply(self, args); + let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || map }, + args)); + if (self.postExecute) + self.postExecute.apply(self, args); + return res; + }, + + onKeyPress: function onKeyPress(event) { + if (event.skipmap) + return Events.ABORT; + + if (!this.command) + return Events.WAIT; + + var map = this.hive.get(this.main, this.command); + this.waiting = this.hive.getCandidates(this.main, this.command); + if (map) { + if (map.arg) + return KeyArgProcessor(this, map, false, "arg"); + else if (map.motion) + return KeyArgProcessor(this, map, true, "motion"); + + return this.execute(map, { + keyEvents: this.keyEvents, + command: this.command, + count: this.count, + keypressEvents: this.events + }); + } + + if (!this.waiting) + return this.main.insert ? Events.PASS : Events.ABORT; + + return Events.WAIT; + } +}); + +var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, { + init: function init(input, map, wantCount, argName) { + init.supercall(this, input.main, input.hive); + this.map = map; + this.parent = input; + this.argName = argName; + this.wantCount = wantCount; + }, + + extended: true, + + onKeyPress: function onKeyPress(event) { + if (Events.isEscape(event)) + return Events.KILL; + if (!this.command) + return Events.WAIT; + + let args = { + command: this.parent.command, + count: this.count || this.parent.count, + events: this.parent.events.concat(this.events) + }; + args[this.argName] = this.command; + + return this.execute(this.map, args); + } +}); + +var EventHive = Class("EventHive", Contexts.Hive, { + init: function init(group) { + init.supercall(this, group); + this.sessionListeners = []; + }, + + cleanup: function cleanup() { + this.unlisten(null); + }, + + /** + * Adds an event listener for this session and removes it on + * dactyl shutdown. + * + * @param {Element} target The element on which to listen. + * @param {string} event The event to listen for. + * @param {function} callback The function to call when the event is received. + * @param {boolean} capture When true, listen during the capture + * phase, otherwise during the bubbling phase. + * @param {boolean} allowUntrusted When true, allow capturing of + * untrusted events. + */ + listen: function (target, event, callback, capture, allowUntrusted) { + if (!isObject(event)) + var [self, events] = [null, array.toObject([[event, callback]])]; + else { + [self, events] = [event, event[callback || "events"]]; + [,, capture, allowUntrusted] = arguments; + } + + for (let [event, callback] in Iterator(events)) { + let args = [Cu.getWeakReference(target), + event, + this.wrapListener(callback, self), + capture, + allowUntrusted]; + + target.addEventListener.apply(target, args.slice(1)); + this.sessionListeners.push(args); + } + }, + + /** + * Remove an event listener. + * + * @param {Element} target The element on which to listen. + * @param {string} event The event to listen for. + * @param {function} callback The function to call when the event is received. + * @param {boolean} capture When true, listen during the capture + * phase, otherwise during the bubbling phase. + */ + unlisten: function (target, event, callback, capture) { + this.sessionListeners = this.sessionListeners.filter(function (args) { + if (target == null || args[0].get() == target && args[1] == event && args[2] == callback && args[3] == capture) { + args[0].get().removeEventListener.apply(args[0].get(), args.slice(1)); + return false; + } + return !args[0].get(); + }); + } +}); + +/** + * @instance events + */ +var Events = Module("events", { + dbg: function () {}, + + init: function () { + const self = this; + this.keyEvents = []; + + update(this, { + hives: contexts.Hives("events", EventHive), + user: contexts.hives.events.user, + builtin: contexts.hives.events.builtin + }); + + EventHive.prototype.wrapListener = this.closure.wrapListener; + + XML.ignoreWhitespace = true; + util.overlayWindow(window, { + append: <e4x xmlns={XUL}> + <window id={document.documentElement.id}> + <!--this notifies us also of focus events in the XUL + from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !--> + <!-- I don't think we really need this. ––Kris --> + <commandset id="dactyl-onfocus" commandupdater="true" events="focus" + oncommandupdate="dactyl.modules.events.onFocusChange(event);"/> + <commandset id="dactyl-onselect" commandupdater="true" events="select" + oncommandupdate="dactyl.modules.events.onSelectionChange(event);"/> + </window> + </e4x>.elements() + }); + + this._fullscreen = window.fullScreen; + this._lastFocus = null; + this._macroKeys = []; + this._lastMacro = ""; + + this._macros = storage.newMap("macros", { privateData: true, store: true }); + for (let [k, m] in this._macros) + if (isString(m)) + m = { keys: m, timeRecorded: Date.now() }; + + // 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 dactyl's source code for + // comparisons like if (key == "<Esc>") { ... } + this._keyTable = { + add: ["Plus", "Add"], + back_space: ["BS"], + count: ["count"], + delete: ["Del"], + escape: ["Esc", "Escape"], + insert: ["Insert", "Ins"], + leader: ["Leader"], + left_shift: ["LT", "<"], + nop: ["Nop"], + pass: ["Pass"], + return: ["Return", "CR", "Enter"], + right_shift: [">"], + space: ["Space", " "], + subtract: ["Minus", "Subtract"] + }; + + this._pseudoKeys = set(["count", "leader", "nop", "pass"]); + + this._key_key = {}; + this._code_key = {}; + this._key_code = {}; + + for (let list in values(this._keyTable)) + for (let v in values(list)) { + if (v.length == 1) + v = v.toLowerCase(); + this._key_key[v.toLowerCase()] = v; + } + + for (let [k, v] in Iterator(KeyEvent)) { + k = k.substr(7).toLowerCase(); + let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase()) + .replace(/^NUMPAD/, "k")]; + + if (names[0].length == 1) + names[0] = names[0].toLowerCase(); + + if (k in this._keyTable) + names = this._keyTable[k]; + this._code_key[v] = names[0]; + for (let [, name] in Iterator(names)) { + this._key_key[name.toLowerCase()] = name; + this._key_code[name.toLowerCase()] = v; + } + } + + // HACK: as Gecko does not include an event for <, we must add this in manually. + if (!("<" in this._key_code)) { + this._key_code["<"] = 60; + this._key_code["lt"] = 60; + this._code_key[60] = "lt"; + } + + this._activeMenubar = false; + this.listen(window, this, "events", true); + + dactyl.registerObserver("modeChange", function () { + delete self.processor; + }); + }, + + signals: { + "browser.locationChange": function (webProgress, request, uri) { + options.get("passkeys").flush(); + } + }, + + /** + * Adds an event listener for this session and removes it on + * dactyl shutdown. + * + * @param {Element} target The element on which to listen. + * @param {string} event The event to listen for. + * @param {function} callback The function to call when the event is received. + * @param {boolean} capture When true, listen during the capture + * phase, otherwise during the bubbling phase. + */ + get listen() this.builtin.closure.listen, + addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }), + + /** + * Wraps an event listener to ensure that errors are reported. + */ + wrapListener: function wrapListener(method, self) { + self = self || this; + method.wrapped = wrappedListener; + function wrappedListener(event) { + try { + method.apply(self, arguments); + } + catch (e) { + dactyl.reportError(e); + if (e.message == "Interrupted") + dactyl.echoerr(_("error.interrupted"), commandline.FORCE_SINGLELINE); + else + dactyl.echoerr(_("event.error", event.type, e.echoerr || e), + commandline.FORCE_SINGLELINE); + } + }; + return wrappedListener; + }, + + /** + * @property {boolean} Whether synthetic key events are currently being + * processed. + */ + feedingKeys: false, + + /** + * Initiates the recording of a key event macro. + * + * @param {string} macro The name for the macro. + */ + _recording: null, + get recording() this._recording, + + set recording(macro) { + dactyl.assert(macro == null || /[a-zA-Z0-9]/.test(macro), + _("macro.invalid", macro)); + + modes.recording = !!macro; + + if (/[A-Z]/.test(macro)) { // uppercase (append) + macro = macro.toLowerCase(); + this._macroKeys = events.fromString((this._macros.get(macro) || { keys: "" }).keys, true) + .map(events.closure.toString); + } + else if (macro) { + this._macroKeys = []; + } + else { + this._macros.set(this.recording, { + keys: this._macroKeys.join(""), + timeRecorded: Date.now() + }); + + dactyl.log("Recorded " + this.recording + ": " + this._macroKeys.join(""), 9); + dactyl.echomsg(_("macro.recorded", this.recording)); + } + this._recording = macro || null; + }, + + /** + * Replays a macro. + * + * @param {string} The name of the macro to replay. + * @returns {boolean} + */ + playMacro: function (macro) { + let res = false; + dactyl.assert(/^[a-zA-Z0-9@]$/.test(macro), _("macro.invalid", macro)); + + if (macro == "@") + dactyl.assert(this._lastMacro, _("macro.noPrevious")); + else + this._lastMacro = macro.toLowerCase(); // XXX: sets last played macro, even if it does not yet exist + + if (this._macros.get(this._lastMacro)) { + try { + modes.replaying = true; + res = events.feedkeys(this._macros.get(this._lastMacro).keys, { noremap: true }); + } + finally { + modes.replaying = false; + } + } + else + // TODO: ignore this like Vim? + dactyl.echoerr(_("macro.noSuch", this._lastMacro)); + return res; + }, + + /** + * Returns all macros matching *filter*. + * + * @param {string} filter A regular expression filter string. A null + * filter selects all macros. + */ + getMacros: function (filter) { + let re = RegExp(filter || ""); + return ([k, m.keys] for ([k, m] in events._macros) if (re.test(k))); + }, + + /** + * Deletes all macros matching *filter*. + * + * @param {string} filter A regular expression filter string. A null + * filter deletes all macros. + */ + deleteMacros: function (filter) { + let re = RegExp(filter || ""); + for (let [item, ] in this._macros) { + if (!filter || re.test(item)) + this._macros.remove(item); + } + }, + + /** + * Feeds a list of events to *target* or the originalTarget member + * of each event if *target* is null. + * + * @param {EventTarget} target The destination node for the events. + * @optional + * @param {[Event]} list The events to dispatch. + * @param {object} extra Extra properties for processing by dactyl. + * @optional + */ + feedevents: function feedevents(target, list, extra) { + list.forEach(function _feedevent(event, i) { + let elem = target || event.originalTarget; + if (elem) { + let doc = elem.ownerDocument || elem.document || elem; + let evt = events.create(doc, event.type, event); + events.dispatch(elem, evt, extra); + } + else if (i > 0 && event.type === "keypress") + events.events.keypress.call(events, event); + }); + }, + + /** + * Pushes keys onto the event queue from dactyl. It is similar to + * Vim's feedkeys() method, but cannot cope with 2 partially-fed + * strings, you have to feed one parseable string. + * + * @param {string} keys A string like "2<C-f>" to push onto the event + * queue. If you want "<" to be taken literally, prepend it with a + * "\\". + * @param {boolean} noremap Whether recursive mappings should be + * disallowed. + * @param {boolean} silent Whether the command should be echoed to the + * command line. + * @returns {boolean} + */ + feedkeys: function (keys, noremap, quiet, mode) { + try { + var savedEvents = this._processor && this._processor.keyEvents; + + var wasFeeding = this.feedingKeys; + this.feedingKeys = true; + + var wasQuiet = commandline.quiet; + if (quiet) + commandline.quiet = quiet; + + for (let [, evt_obj] in Iterator(events.fromString(keys))) { + let now = Date.now(); + for (let type in values(["keydown", "keyup", "keypress"])) { + let evt = update({}, evt_obj, { type: type }); + + if (isObject(noremap)) + update(evt, noremap); + else + evt.noremap = !!noremap; + evt.isMacro = true; + evt.dactylMode = mode; + evt.dactylSavedEvents = savedEvents; + this.feedingEvent = evt; + + let event = events.create(document.commandDispatcher.focusedWindow.document, type, evt); + if (!evt_obj.dactylString && !evt_obj.dactylShift && !mode) + events.dispatch(dactyl.focusedElement || buffer.focusedFrame, event, evt); + else if (type === "keypress") + events.events.keypress.call(events, event); + } + + if (!this.feedingKeys) + return false; + } + } + catch (e) { + util.reportError(e); + } + finally { + this.feedingEvent = null; + this.feedingKeys = wasFeeding; + if (quiet) + commandline.quiet = wasQuiet; + dactyl.triggerObserver("events.doneFeeding"); + } + return true; + }, + + /** + * Creates an actual event from a pseudo-event object. + * + * The pseudo-event object (such as may be retrieved from events.fromString) + * should have any properties you want the event to have. + * + * @param {Document} doc The DOM document to associate this event with + * @param {Type} type The type of event (keypress, click, etc.) + * @param {Object} opts The pseudo-event. @optional + */ + create: function (doc, type, opts) { + opts = opts || {}; + var DEFAULTS = { + HTML: { + type: type, bubbles: true, cancelable: false + }, + Key: { + type: type, + bubbles: true, cancelable: true, + view: doc.defaultView, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0, charCode: 0 + }, + Mouse: { + type: type, + bubbles: true, cancelable: true, + view: doc.defaultView, + detail: 1, + screenX: 0, screenY: 0, + clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 0, + relatedTarget: null + } + }; + const TYPES = { + change: "", input: "", submit: "", + click: "Mouse", mousedown: "Mouse", mouseup: "Mouse", + mouseover: "Mouse", mouseout: "Mouse", + keypress: "Key", keyup: "Key", keydown: "Key" + }; + var t = TYPES[type]; + var evt = doc.createEvent((t || "HTML") + "Events"); + + let defaults = DEFAULTS[t || "HTML"]; + evt["init" + t + "Event"].apply(evt, Object.keys(defaults) + .map(function (k) k in opts ? opts[k] + : defaults[k])); + return evt; + }, + + /** + * Converts a user-input string of keys into a canonical + * representation. + * + * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A> + * <C- > maps to <C-Space>, <S-a> maps to A + * << maps to <lt><lt> + * + * <S-@> is preserved, as in Vim, to allow untypeable key-combinations + * in macros. + * + * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values + * of x. + * + * @param {string} keys Messy form. + * @param {boolean} unknownOk Whether unknown keys are passed + * through rather than being converted to <lt>keyname>. + * @default false + * @returns {string} Canonical form. + */ + canonicalKeys: function (keys, unknownOk) { + if (arguments.length === 1) + unknownOk = true; + return events.fromString(keys, unknownOk).map(events.closure.toString).join(""); + }, + + iterKeys: function (keys) { + let match, re = /<.*?>?>|[^<]/g; + while (match = re.exec(keys)) + yield match[0]; + }, + + /** + * Dispatches an event to an element as if it were a native event. + * + * @param {Node} target The DOM node to which to dispatch the event. + * @param {Event} event The event to dispatch. + */ + dispatch: Class.memoize(function () + util.haveGecko("2b") + ? function dispatch(target, event, extra) { + try { + this.feedingEvent = extra; + if (target instanceof Element) + // This causes a crash on Gecko<2.0, it seems. + return (target.ownerDocument || target.document || target).defaultView + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils) + .dispatchDOMEventViaPresShell(target, event, true); + else { + target.dispatchEvent(event); + return !event.getPreventDefault(); + } + } + catch (e) { + util.reportError(e); + } + finally { + this.feedingEvent = null; + } + } + : function dispatch(target, event, extra) { + try { + this.feedingEvent = extra; + target.dispatchEvent(update(event, extra)); + } + finally { + this.feedingEvent = null; + } + }), + + get defaultTarget() dactyl.focusedElement || content.document.body || document.documentElement, + + /** + * Converts an event string into an array of pseudo-event objects. + * + * These objects can be used as arguments to events.toString or + * events.create, though they are unlikely to be much use for other + * purposes. They have many of the properties you'd expect to find on a + * real event, but none of the methods. + * + * Also may contain two "special" parameters, .dactylString and + * .dactylShift these are set for characters that can never by + * typed, but may appear in mappings, for example <Nop> is passed as + * dactylString, and dactylShift is set when a user specifies + * <S-@> where @ is a non-case-changeable, non-space character. + * + * @param {string} keys The string to parse. + * @param {boolean} unknownOk Whether unknown keys are passed + * through rather than being converted to <lt>keyname>. + * @default false + * @returns {Array[Object]} + */ + fromString: function (input, unknownOk) { + + if (arguments.length === 1) + unknownOk = true; + + let out = []; + for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) { + let evt_str = match[0]; + let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, + keyCode: 0, charCode: 0, type: "keypress" }; + + if (evt_str.length > 1) { // <.*?> + let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', '']; + modifier = modifier.toUpperCase(); + keyname = keyname.toLowerCase(); + evt_obj.dactylKeyname = keyname; + if (/^u[0-9a-f]+$/.test(keyname)) + keyname = String.fromCharCode(parseInt(keyname.substr(1), 16)); + + if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) || + this._key_code[keyname] || set.has(this._pseudoKeys, keyname))) { + evt_obj.ctrlKey = /C-/.test(modifier); + evt_obj.altKey = /A-/.test(modifier); + evt_obj.shiftKey = /S-/.test(modifier); + evt_obj.metaKey = /M-/.test(modifier); + + if (keyname.length == 1) { // normal characters + if (evt_obj.shiftKey) { + keyname = keyname.toUpperCase(); + if (keyname == keyname.toLowerCase()) + evt_obj.dactylShift = true; + } + + evt_obj.charCode = keyname.charCodeAt(0); + } + else if (set.has(this._pseudoKeys, keyname)) { + evt_obj.dactylString = "<" + this._key_key[keyname] + ">"; + } + else if (/mouse$/.test(keyname)) { // mouse events + evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click"); + evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname); + delete evt_obj.keyCode; + delete evt_obj.charCode; + } + else { // spaces, control characters, and < + evt_obj.keyCode = this._key_code[keyname]; + evt_obj.charCode = 0; + } + } + else { // an invalid sequence starting with <, treat as a literal + out = out.concat(events.fromString("<lt>" + evt_str.substr(1))); + continue; + } + } + else // a simple key (no <...>) + evt_obj.charCode = evt_str.charCodeAt(0); + + // TODO: make a list of characters that need keyCode and charCode somewhere + if (evt_obj.keyCode == 32 || evt_obj.charCode == 32) + evt_obj.charCode = evt_obj.keyCode = 32; // <Space> + if (evt_obj.keyCode == 60 || evt_obj.charCode == 60) + evt_obj.charCode = evt_obj.keyCode = 60; // <lt> + + evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK) + | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK) + | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK) + | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK); + + out.push(evt_obj); + } + return out; + }, + + /** + * Converts the specified event to a string in dactyl key-code + * notation. Returns null for an unknown event. + * + * @param {Event} event + * @returns {string} + */ + toString: function toString(event) { + if (!event) + return toString.supercall(this); + + if (event.dactylString) + return event.dactylString; + + let key = null; + let modifier = ""; + + if (event.ctrlKey) + modifier += "C-"; + if (event.altKey) + modifier += "A-"; + if (event.metaKey) + modifier += "M-"; + + if (/^key/.test(event.type)) { + let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris + if (charCode == 0) { + if (event.keyCode in this._code_key) { + key = this._code_key[event.keyCode]; + + if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift) + modifier += "S-"; + else if (!modifier && key.length === 1) + if (event.shiftKey) + key = key.toUpperCase(); + else + key = key.toLowerCase(); + if (!modifier && /^[a-z0-9]$/i.test(key)) + return key; + } + } + // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X) + // (i.e., cntrl codes 27--31) + // --- + // For more information, see: + // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html + // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \" + // https://bugzilla.mozilla.org/show_bug.cgi?id=416227 + // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa + // https://bugzilla.mozilla.org/show_bug.cgi?id=432951 + // --- + // + // The following fixes are only activated if util.OS.isMacOSX. + // Technically, they prevent mappings from <C-Esc> (and + // <C-C-]> if your fancy keyboard permits such things<?>), but + // these <C-control> mappings are probably pathological (<C-Esc> + // certainly is on Windows), and so it is probably + // harmless to remove the util.OS.isMacOSX if desired. + // + else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) { + if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug + key = "Esc"; + modifier = modifier.replace("C-", ""); + } + else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs + key = String.fromCharCode(charCode + 64); + } + // a normal key like a, b, c, 0, etc. + else if (charCode > 0) { + key = String.fromCharCode(charCode); + + if (!/^[a-z0-9]$/i.test(key) && key in this._key_code) { + // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced + if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift) + modifier += "S-"; + + key = this._code_key[this._key_code[key]]; + } + else { + // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase, + // or if the shift has been forced for a non-alphabetical character by the user while :map-ping + if (key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift) + modifier += "S-"; + if (/^\s$/.test(key)) + key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s; + else if (modifier.length == 0) + return key; + } + } + if (key == null) + key = this._key_key[event.dactylKeyname] || event.dactylKeyname; + if (key == null) + return null; + } + 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; + + return "<" + modifier + key + ">"; + }, + + /** + * Whether *key* is a key code defined to accept/execute input on the + * command line. + * + * @param {string} key The key code to test. + * @returns {boolean} + */ + isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>", + + /** + * Whether *key* is a key code defined to reject/cancel input on the + * command line. + * + * @param {string} key The key code to test. + * @returns {boolean} + */ + isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>", + + isContentNode: function isContentNode(node) { + let win = (node.ownerDocument || node).defaultView || node; + return XPCNativeWrapper(win).top == content; + }, + + /** + * Waits for the current buffer to successfully finish loading. Returns + * true for a successful page load otherwise false. + * + * @returns {boolean} + */ + waitForPageLoad: function (time) { + if (buffer.loaded) + return true; + + dactyl.echo(_("macro.loadWaiting"), commandline.DISALLOW_MULTILINE); + + const maxWaitTime = (time || 25); + util.waitFor(function () !events.feedingKeys || buffer.loaded, this, maxWaitTime * 1000, true); + + if (!buffer.loaded) + dactyl.echoerr(_("macro.loadFailed", maxWaitTime)); + + return buffer.loaded; + }, + + /** + * Ensures that the currently focused element is visible and blurs + * it if it's not. + */ + checkFocus: function () { + if (dactyl.focusedElement) { + let rect = dactyl.focusedElement.getBoundingClientRect(); + if (!rect.width || !rect.height) { + services.focus.clearFocus(window); + document.commandDispatcher.focusedWindow = content; + // onFocusChange needs to die. + this.onFocusChange(); + } + } + }, + + events: { + DOMMenuBarActive: function () { + this._activeMenubar = true; + if (modes.main != modes.MENU) + modes.push(modes.MENU); + }, + + DOMMenuBarInactive: function () { + this._activeMenubar = false; + modes.remove(modes.MENU, true); + }, + + blur: function onBlur(event) { + let elem = event.originalTarget; + if (elem instanceof Window && services.focus.activeWindow == null + && document.commandDispatcher.focusedWindow !== window) { + // Deals with circumstances where, after the main window + // blurs while a collapsed frame has focus, re-activating + // the main window does not restore focus and we lose key + // input. + services.focus.clearFocus(window); + document.commandDispatcher.focusedWindow = Editor.getEditor(content) ? window : content; + } + + let hold = modes.topOfStack.params.holdFocus; + if (elem == hold) { + dactyl.focus(hold); + this.timeout(function () { dactyl.focus(hold); }); + } + }, + + // TODO: Merge with onFocusChange + focus: function onFocus(event) { + let elem = event.originalTarget; + + if (event.target instanceof Ci.nsIDOMXULTextBoxElement) + if (Events.isHidden(elem, true)) + elem.blur(); + + let win = (elem.ownerDocument || elem).defaultView || elem; + + if (events.isContentNode(elem) && !buffer.focusAllowed(elem) + && !(services.focus.getLastFocusMethod(win) & 0x7000) + && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) { + if (elem.frameElement) + dactyl.focusContent(true); + else if (!(elem instanceof Window) || Editor.getEditor(elem)) + dactyl.focus(window); + } + }, + + /* + onFocus: function onFocus(event) { + let elem = event.originalTarget; + if (!(elem instanceof Element)) + return; + let win = elem.ownerDocument.defaultView; + + try { + util.dump(elem, services.focus.getLastFocusMethod(win) & (0x7000)); + if (buffer.focusAllowed(win)) + win.dactylLastFocus = elem; + else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) { + if (win.dactylLastFocus) + dactyl.focus(win.dactylLastFocus); + else + elem.blur(); + } + } + catch (e) { + util.dump(win, String(elem.ownerDocument), String(elem.ownerDocument && elem.ownerDocument.defaultView)); + util.reportError(e); + } + }, + */ + + input: function onInput(event) { + event.originalTarget.dactylKeyPress = undefined; + }, + + // this keypress handler gets always called first, even if e.g. + // the command-line has focus + // TODO: ...help me...please... + keypress: function onKeyPress(event) { + event.dactylDefaultPrevented = event.getPreventDefault(); + + let duringFeed = this.duringFeed || []; + this.duringFeed = []; + try { + if (this.feedingEvent) + for (let [k, v] in Iterator(this.feedingEvent)) + if (!(k in event)) + event[k] = v; + this.feedingEvent = null; + + let key = events.toString(event); + + // Hack to deal with <BS> and so forth not dispatching input + // events + if (key && event.originalTarget instanceof HTMLInputElement && !modes.main.passthrough) { + let elem = event.originalTarget; + elem.dactylKeyPress = elem.value; + util.timeout(function () { + if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress) + events.dispatch(elem, events.create(elem.ownerDocument, "input")); + elem.dactylKeyPress = undefined; + }); + } + + if (!key) + return null; + + if (modes.recording && !event.isReplay) + events._macroKeys.push(key); + + // 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 && !event.isMacro) { + if (key == "<C-c>") { + events.feedingKeys = false; + if (modes.replaying) { + modes.replaying = false; + this.timeout(function () { dactyl.echomsg(_("macro.canceled", this._lastMacro)); }, 100); + } + } + else + duringFeed.push(event); + + return Events.kill(event); + } + + if (!this.processor) { + let mode = modes.getStack(0); + if (event.dactylMode) + mode = Modes.StackElement(event.dactylMode); + + let ignore = false; + + if (modes.main == modes.PASS_THROUGH) + ignore = !Events.isEscape(key) && key != "<C-v>"; + else if (modes.main == modes.QUOTE) { + if (modes.getStack(1).main == modes.PASS_THROUGH) { + mode.params.mainMode = modes.getStack(2).main; + ignore = Events.isEscape(key); + } + else if (events.shouldPass(event)) + mode.params.mainMode = modes.getStack(1).main; + else + ignore = true; + + if (ignore && !Events.isEscape(key)) + modes.pop(); + } + else if (!event.isMacro && !event.noremap && events.shouldPass(event)) + ignore = true; + + events.dbg("\n\n"); + events.dbg("ON KEYPRESS " + key + " ignore: " + ignore, + event.originalTarget instanceof Element ? event.originalTarget : String(event.originalTarget)); + + if (ignore) + return null; + + // FIXME: Why is this hard coded? --Kris + if (key == "<C-c>") + util.interrupted = true; + + this.processor = ProcessorStack(mode, mappings.hives.array, event.noremap); + this.processor.keyEvents = this.keyEvents; + } + + let { keyEvents, processor } = this; + this._processor = processor; + this.processor = null; + this.keyEvents = []; + + if (!processor.process(event)) { + this.keyEvents = keyEvents; + this.processor = processor; + } + + } + catch (e) { + dactyl.reportError(e); + } + finally { + [duringFeed, this.duringFeed] = [this.duringFeed, duringFeed]; + if (this.feedingKeys) + this.duringFeed = this.duringFeed.concat(duringFeed); + else + for (let event in values(duringFeed)) + try { + this.dispatch(event.originalTarget, event, event); + } + catch (e) { + util.reportError(e); + } + } + }, + + keyup: function onKeyUp(event) { + this.keyEvents.push(event); + + let pass = this.feedingEvent && this.feedingEvent.isReplay || + event.isReplay || + modes.main == modes.PASS_THROUGH || + modes.main == modes.QUOTE + && modes.getStack(1).main !== modes.PASS_THROUGH + && !this.shouldPass(event) || + !modes.passThrough && this.shouldPass(event); + + events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass); + + // Prevents certain sites from transferring focus to an input box + // before we get a chance to process our key bindings on the + // "keypress" event. + if (!pass && !Events.isInputElement(dactyl.focusedElement)) + event.stopPropagation(); + }, + keydown: function onKeyDown(event) { + this.events.keyup.call(this, event); + }, + + mousedown: function onMouseDown(event) { + let elem = event.target; + let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; + + for (; win; win = win != win.parent && win.parent) + win.document.dactylFocusAllowed = true; + }, + + popupshown: function onPopupShown(event) { + let elem = event.originalTarget; + if (elem instanceof Ci.nsIAutoCompletePopup) { + if (modes.main != modes.AUTOCOMPLETE) + modes.push(modes.AUTOCOMPLETE); + } + else if (elem.localName !== "tooltip") + if (Events.isHidden(elem)) { + if (elem.hidePopup && Events.isHidden(elem.parentNode)) + elem.hidePopup(); + } + else if (modes.main != modes.MENU) + modes.push(modes.MENU); + }, + + popuphidden: function onPopupHidden() { + // gContextMenu is set to NULL, when a context menu is closed + if (window.gContextMenu == null && !this._activeMenubar) + modes.remove(modes.MENU, true); + modes.remove(modes.AUTOCOMPLETE); + }, + + resize: function onResize(event) { + if (window.fullScreen != this._fullscreen) { + statusline.statusBar.removeAttribute("moz-collapsed"); + this._fullscreen = window.fullScreen; + dactyl.triggerObserver("fullscreen", this._fullscreen); + autocommands.trigger("Fullscreen", { url: this._fullscreen ? "on" : "off", state: this._fullscreen }); + } + } + }, + + // argument "event" is deliberately not used, as i don't seem to have + // access to the real focus target + // Huh? --djk + onFocusChange: function onFocusChange(event) { + function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument + if (dactyl.ignoreFocus) + return; + + let win = window.document.commandDispatcher.focusedWindow; + let elem = window.document.commandDispatcher.focusedElement; + + if (elem == null && Editor.getEditor(win)) + elem = win; + + if (win && win.top == content && dactyl.has("tabs")) + buffer.focusedFrame = win; + + try { + if (elem && elem.readOnly) + return; + + if (isinstance(elem, [HTMLEmbedElement, HTMLEmbedElement])) { + modes.push(modes.EMBED); + return; + } + + let haveInput = modes.stack.some(function (m) m.main.input); + + if (elem instanceof HTMLTextAreaElement + || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write" + || elem == null && win && Editor.getEditor(win)) { + + if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart) + modes.pop(); + + if (!haveInput) + if (options["insertmode"]) + modes.push(modes.INSERT); + else { + modes.push(modes.TEXT_EDIT); + if (elem.selectionEnd - elem.selectionStart > 0) + modes.push(modes.VISUAL); + } + + if (hasHTMLDocument(win)) + buffer.lastInputField = elem; + return; + } + + if (Events.isInputElement(elem)) { + if (!haveInput) + modes.push(modes.INSERT); + + if (hasHTMLDocument(win)) + buffer.lastInputField = elem; + return; + } + + if (config.focusChange) { + config.focusChange(win); + return; + } + + let urlbar = document.getElementById("urlbar"); + if (elem == null && urlbar && urlbar.inputField == this._lastFocus) + util.threadYield(true); // Why? --Kris + + while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus) + modes.pop(null, { fromFocus: true }); + } + finally { + this._lastFocus = elem; + } + }, + + onSelectionChange: function onSelectionChange(event) { + let controller = document.commandDispatcher.getControllerForCommand("cmd_copy"); + let couldCopy = controller && controller.isCommandEnabled("cmd_copy"); + + if (modes.main == modes.VISUAL) { + if (!couldCopy) + modes.pop(); // Really not ideal. + } + else if (couldCopy) { + if (modes.main == modes.TEXT_EDIT && !options["insertmode"]) + modes.push(modes.VISUAL); + else if (modes.main == modes.CARET) + modes.push(modes.VISUAL); + } + }, + + shouldPass: function shouldPass(event) + !event.noremap && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)) && + options.get("passkeys").has(events.toString(event)) +}, { + ABORT: {}, + KILL: true, + PASS: false, + PASS_THROUGH: {}, + WAIT: null, + + isEscape: function isEscape(event) + let (key = isString(event) ? event : events.toString(event)) + key === "<Esc>" || key === "<C-[>", + + isHidden: function isHidden(elem, aggressive) { + for (let e = elem; e instanceof Element; e = e.parentNode) { + if (util.computedStyle(e).visibility !== "visible" || + aggressive && e.boxObject && e.boxObject.height === 0) + return true; + } + return false; + }, + + isInputElement: function isInputElement(elem) { + return elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type) || + isinstance(elem, [HTMLIsIndexElement, HTMLEmbedElement, + HTMLObjectElement, HTMLSelectElement, + HTMLTextAreaElement, + Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) || + elem instanceof Window && Editor.getEditor(elem); + }, + + kill: function kill(event) { + event.stopPropagation(); + event.preventDefault(); + } +}, { + commands: function () { + commands.add(["delmac[ros]"], + "Delete macros", + function (args) { + dactyl.assert(!args.bang || !args[0], _("error.invalidArgument")); + + if (args.bang) + events.deleteMacros(); + else if (args[0]) + events.deleteMacros(args[0]); + else + dactyl.echoerr(_("error.argumentRequired")); + }, { + 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) + }); + }, + completion: function () { + completion.macro = function macro(context) { + context.title = ["Macro", "Keys"]; + context.completions = [item for (item in events.getMacros())]; + }; + }, + mappings: function () { + + mappings.add([modes.MAIN], + ["<A-b>"], "Process the next key as a builtin mapping", + function () { + events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true); + events.processor.keyEvents = events.keyEvents; + }); + + mappings.add([modes.MAIN], + ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings", + function () { modes.push(modes.PASS_THROUGH); }); + + mappings.add([modes.MAIN], + ["<C-v>", "<pass-next-key>"], "Pass through next key", + function () { + if (modes.main == modes.QUOTE) + return Events.PASS; + modes.push(modes.QUOTE); + }); + + mappings.add([modes.BASE], + ["<Nop>"], "Do nothing", + function () {}); + + mappings.add([modes.BASE], + ["<Pass>"], "Pass the events consumed by the last executed mapping", + function ({ keypressEvents: [event] }) { + dactyl.assert(event.dactylSavedEvents, + _("event.nothingToPass")); + return function () { + events.feedevents(null, event.dactylSavedEvents, + { skipmap: true, isMacro: true, isReplay: true }); + }; + }); + + // macros + mappings.add([modes.COMMAND], + ["q", "<record-macro>"], "Record a key sequence into a macro", + function ({ arg }) { + events._macroKeys.pop(); + events.recording = arg; + }, + { get arg() !modes.recording }); + + mappings.add([modes.COMMAND], + ["@", "<play-macro>"], "Play a macro", + function ({ arg, count }) { + count = Math.max(count, 1); + while (count--) + events.playMacro(arg); + }, + { arg: true, count: true }); + + mappings.add([modes.COMMAND], + ["<A-m>s", "<sleep>"], "Sleep for {count} milliseconds before continuing macro playback", + function ({ command, count }) { + let now = Date.now(); + dactyl.assert(count, _("error.countRequired", command)); + if (events.feedingKeys) + util.sleep(count); + }, + { count: true }); + + mappings.add([modes.COMMAND], + ["<A-m>l", "<wait-for-page-load>"], "Wait for the current page to finish loading before continuing macro playback", + function ({ count }) { + if (events.feedingKeys && !events.waitForPageLoad(count)) { + util.interrupted = true; + throw Error("Interrupted"); + } + }, + { count: true }); + }, + options: function () { + const Hive = Class("Hive", { + init: function init(values, map) { + this.name = "passkeys:" + map; + this.stack = MapHive.Stack(values.map(function (v) Map(v[map + "Keys"]))); + function Map(keys) ({ + execute: function () Events.PASS_THROUGH, + keys: keys + }); + }, + + get active() this.stack.length, + + get: function get(mode, key) this.stack.mappings[key], + + getCandidates: function getCandidates(mode, key) this.stack.candidates[key] + }); + options.add(["passkeys", "pk"], + "Pass certain keys through directly for the given URLs", + "sitemap", "", { + flush: function flush() { + memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI))); + memoize(this, "pass", function () set(array.flatten(this.filters.map(function (f) f.keys)))); + memoize(this, "commandHive", function hive() Hive(this.filters, "command")); + memoize(this, "inputHive", function hive() Hive(this.filters, "input")); + }, + + has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key), + + get pass() (this.flush(), this.pass), + + keepQuotes: true, + + setter: function (values) { + values.forEach(function (filter) { + let vals = Option.splitList(filter.result); + filter.keys = events.fromString(vals[0]).map(events.closure.toString); + + filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys); + filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/); + }); + this.flush(); + return values; + } + }); + + options.add(["strictfocus", "sf"], + "Prevent scripts from focusing input elements without user intervention", + "boolean", true); + + options.add(["timeout", "tmo"], + "Whether to execute a shorter key command after a timeout when a longer command exists", + "boolean", true); + + options.add(["timeoutlen", "tmol"], + "Maximum time (milliseconds) to wait for a longer key command when a shorter one exists", + "number", 1000); + }, + sanitizer: function () { + sanitizer.addItem("macros", { + description: "Saved macros", + persistent: true, + action: function (timespan, host) { + if (!host) + for (let [k, m] in events._macros) + if (timespan.contains(m.timeRecorded * 1000)) + events._macros.remove(k); + } + }); + } +}); + +// 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 0000000..cdcabaa --- /dev/null +++ b/common/content/help.css @@ -0,0 +1,120 @@ +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.pentadactyl { + border-width: 1px; + border-style: dotted; + border-color: gray; +} +table.pentadactyl 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/help.js b/common/content/help.js new file mode 100644 index 0000000..9faf7a2 --- /dev/null +++ b/common/content/help.js @@ -0,0 +1,27 @@ +// Copyright (c) 2009-2011 by Kris Maglione <kris@vimperator.org> +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +function checkFragment() { + document.title = document.getElementsByTagNameNS("http://www.w3.org/1999/xhtml", "title")[0].textContent; + + function action() { + content.scrollTo(0, content.scrollY + elem.getBoundingClientRect().top - 10); // 10px context + } + + var frag = document.location.hash.substr(1); + if (frag) { + var elem = document.getElementById(frag); + if (elem) { + action(); + setTimeout(action, 10); + } + } +} + +document.addEventListener("load", checkFragment, true); +document.addEventListener("hashChange", checkFragment, true); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/help.xsl b/common/content/help.xsl new file mode 100644 index 0000000..e70875b --- /dev/null +++ b/common/content/help.xsl @@ -0,0 +1,682 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE stylesheet SYSTEM "dactyl://content/dtd"> + +<!-- Header {{{1 --> +<xsl:stylesheet version="1.0" + xmlns="http://www.w3.org/1999/xhtml" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:dactyl="http://vimperator.org/namespaces/liberator" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:exsl="http://exslt.org/common" + xmlns:regexp="http://exslt.org/regular-expressions" + xmlns:str="http://exslt.org/strings" + extension-element-prefixes="exsl regexp str"> + + <xsl:output method="xml" indent="no"/> + + <!-- Variable Definitions {{{1 --> + + + <!-- Process Overlays {{{1 --> + + <xsl:template name="splice-overlays"> + <xsl:param name="elem"/> + <xsl:param name="tag"/> + <xsl:for-each select="ancestor::*/dactyl:overlay/*[@insertbefore=$tag]"> + <xsl:apply-templates select="." mode="overlay"/> + </xsl:for-each> + <xsl:choose> + <xsl:when test="ancestor::*/dactyl:overlay/*[@replace=$tag] and not($elem[@replace])"> + <xsl:for-each select="ancestor::*/dactyl:overlay/*[@replace=$tag]"> + <xsl:apply-templates select="." mode="overlay-2"/> + </xsl:for-each> + </xsl:when> + <xsl:otherwise> + <xsl:for-each select="$elem"> + <xsl:apply-templates select="." mode="overlay-2"/> + </xsl:for-each> + </xsl:otherwise> + </xsl:choose> + <xsl:for-each select="ancestor::*/dactyl:overlay/*[@insertafter=$tag]"> + <xsl:apply-templates select="." mode="overlay"/> + </xsl:for-each> + </xsl:template> + + <xsl:template match="dactyl:tags[parent::dactyl:document]|dactyl:tag" mode="overlay"> + <xsl:call-template name="splice-overlays"> + <xsl:with-param name="tag" select="substring-before(concat(., ' '), ' ')"/> + <xsl:with-param name="elem" select="self::node()"/> + </xsl:call-template> + </xsl:template> + <xsl:template match="*[dactyl:tags]" mode="overlay"> + <xsl:call-template name="splice-overlays"> + <xsl:with-param name="tag" select="substring-before(concat(dactyl:tags, ' '), ' ')"/> + <xsl:with-param name="elem" select="self::node()"/> + </xsl:call-template> + </xsl:template> + <xsl:template match="dactyl:*[@tag and not(@replace)]" mode="overlay"> + <xsl:call-template name="splice-overlays"> + <xsl:with-param name="tag" select="substring-before(concat(@tag, ' '), ' ')"/> + <xsl:with-param name="elem" select="self::node()"/> + </xsl:call-template> + </xsl:template> + + <!-- Process Inclusions {{{1 --> + + <xsl:template name="include"> + <xsl:param name="root-node" select="."/> + <xsl:param name="overlay" select="concat('dactyl://help-overlay/', $root-node/@name)"/> + + <!-- Ridiculous three-pass processing is needed to deal with + - lack of dynamic variable scope in XSL 1.0. --> + + <!-- Store a copy of the overlay for the current document. --> + <xsl:variable name="doc"> + <dactyl:document> + <xsl:copy-of select="document($overlay)/dactyl:overlay"/> + <xsl:copy-of select="$root-node/node()"/> + </dactyl:document> + </xsl:variable> + + <xsl:call-template name="parse-tags"> + <xsl:with-param name="text" select="concat($root-node/@name, '.xml')"/> + </xsl:call-template> + <xsl:apply-templates select="exsl:node-set($doc)/dactyl:document/node()[position() != 1]" mode="overlay"/> + </xsl:template> + + <xsl:template match="dactyl:include" mode="overlay-2"> + <div dactyl:highlight="HelpInclude"> + <xsl:call-template name="include"> + <xsl:with-param name="root-node" select="document(@href)/dactyl:document"/> + </xsl:call-template> + </div> + </xsl:template> + + <xsl:template match="@*|node()" mode="overlay"> + <xsl:apply-templates select="." mode="overlay-2"/> + </xsl:template> + <xsl:template match="@*|node()" mode="overlay-2"> + <xsl:copy> + <xsl:apply-templates select="@*|node()" mode="overlay"/> + </xsl:copy> + </xsl:template> + + <!-- Root {{{1 --> + + <xsl:template match="/"> + + <!-- Ridiculous three-pass processing is needed to deal with + - lack of dynamic variable scope in XSL 1.0. --> + + <xsl:variable name="doc1"> + <xsl:call-template name="include"> + <xsl:with-param name="root-node" select="dactyl:document"/> + </xsl:call-template> + </xsl:variable> + <xsl:variable name="root" select="exsl:node-set($doc1)"/> + + <!-- Store a cache of all tags defined --> + <xsl:variable name="doc2"> + <dactyl:document> + <xsl:attribute name="document-tags"> + <xsl:text> </xsl:text> + <xsl:for-each select="$root//@tag|$root//dactyl:tags/text()|$root//dactyl:tag/text()"> + <xsl:value-of select="concat(., ' ')"/> + </xsl:for-each> + </xsl:attribute> + <xsl:copy-of select="$root/node()"/> + </dactyl:document> + </xsl:variable> + <xsl:variable name="root2" select="exsl:node-set($doc2)/dactyl:document"/> + + <html dactyl:highlight="Help"> + <head> + <title><xsl:value-of select="/dactyl:document/@title"/> + + + + diff --git a/common/content/quickmarks.js b/common/content/quickmarks.js new file mode 100644 index 0000000..c12e768 --- /dev/null +++ b/common/content/quickmarks.js @@ -0,0 +1,207 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +/** +* @instance quickmarks +*/ +var QuickMarks = Module("quickmarks", { + init: function () { + this._qmarks = storage.newMap("quickmarks", { store: true }); + storage.addObserver("quickmarks", function () { + statusline.updateStatus(); + }, window); + }, + + /** + * Adds a new quickmark with name *qmark* referencing the URL *location*. + * Any existing quickmark with the same name will be replaced. + * + * @param {string} qmark The name of the quickmark {A-Z}. + * @param {string} location The URL accessed by this quickmark. + */ + add: function add(qmark, location) { + this._qmarks.set(qmark, location); + dactyl.echomsg({ domains: [util.getHost(location)], message: _("quickmark.added", qmark, location) }, 1); + }, + + /** + * Returns a list of QuickMarks associates with the given URL. + * + * @param {string} url The url to find QuickMarks for. + * @return {[string]} + */ + find: function find(url) { + let res = []; + for (let [k, v] in this._qmarks) + if (dactyl.parseURLs(v).some(function (u) String.replace(u, /#.*/, "") == url)) + res.push(k); + return res; + }, + + /** + * Returns the URL of the given QuickMark, or null if none exists. + * + * @param {string} mark The mark to find. + * @returns {string} The mark's URL. + */ + get: function (mark) this._qmarks.get(mark) || null, + + /** + * Deletes the specified quickmarks. The *filter* is a list of quickmarks + * and ranges are supported. Eg. "ab c d e-k". + * + * @param {string} filter The list of quickmarks to delete. + * + */ + remove: function remove(filter) { + let pattern = util.charListToRegexp(filter, "a-zA-Z0-9"); + + for (let [qmark, ] in this._qmarks) { + if (pattern.test(qmark)) + this._qmarks.remove(qmark); + } + }, + + /** + * Removes all quickmarks. + */ + removeAll: function removeAll() { + this._qmarks.clear(); + }, + + /** + * Opens the URL referenced by the specified *qmark*. + * + * @param {string} qmark The quickmark to open. + * @param {object} where A set of parameters specifying how to open the + * URL. See {@link Dactyl#open}. + */ + jumpTo: function jumpTo(qmark, where) { + let url = this.get(qmark); + + if (url) + dactyl.open(url, where); + else + dactyl.echoerr(_("quickmark.notSet")); + }, + + /** + * Lists all quickmarks matching *filter* in the message window. + * + * @param {string} filter The list of quickmarks to display, e.g. "a-c i O-X". + */ + list: function list(filter) { + let marks = [k for ([k, v] in this._qmarks)]; + let lowercaseMarks = marks.filter(function (x) /[a-z]/.test(x)).sort(); + let uppercaseMarks = marks.filter(function (x) /[A-Z]/.test(x)).sort(); + let numberMarks = marks.filter(function (x) /[0-9]/.test(x)).sort(); + + marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks); + + dactyl.assert(marks.length > 0, _("quickmark.none")); + + if (filter.length > 0) { + let pattern = util.charListToRegexp(filter, "a-zA-Z0-9"); + marks = marks.filter(function (qmark) pattern.test(qmark)); + dactyl.assert(marks.length >= 0, _("quickmark.noMatching", filter.quote())); + } + + commandline.commandOutput(template.tabular(["QuickMark", "URL"], [], + ([mark, quickmarks._qmarks.get(mark)] for ([k, mark] in Iterator(marks))))); + } +}, { +}, { + commands: function () { + commands.add(["delqm[arks]"], + "Delete the specified QuickMarks", + function (args) { + // TODO: finish arg parsing - we really need a proper way to do this. :) + // assert(args.bang ^ args[0]) + dactyl.assert( args.bang || args[0], _("error.argumentRequired")); + dactyl.assert(!args.bang || !args[0], _("error.invalidArgument")); + + if (args.bang) + quickmarks.removeAll(); + else + quickmarks.remove(args[0]); + }, + { + argCount: "?", + bang: true, + completer: function (context) completion.quickmark(context) + }); + + commands.add(["qma[rk]"], + "Mark a URL with a letter for quick access", + function (args) { + dactyl.assert(/^[a-zA-Z0-9]$/.test(args[0]), + _("quickmark.invalid")); + if (!args[1]) + quickmarks.add(args[0], buffer.uri.spec); + else + quickmarks.add(args[0], args[1]); + }, + { + argCount: "+", + completer: function (context, args) { + if (args.length == 1) + return completion.quickmark(context); + if (args.length == 2) { + context.fork("current", 0, this, function (context) { + context.title = ["Extra Completions"]; + context.completions = [ + [quickmarks.get(args[0]), "Current Value"] + ].filter(function ([k, v]) k); + }); + context.fork("url", 0, completion, "url"); + } + }, + literal: 1 + }); + + commands.add(["qmarks"], + "Show the specified QuickMarks", + function (args) { + quickmarks.list(args[0] || ""); + }, { + argCount: "?", + completer: function (context) completion.quickmark(context), + }); + }, + completion: function () { + completion.quickmark = function (context) { + context.title = ["QuickMark", "URL"]; + context.generate = function () Iterator(quickmarks._qmarks); + }; + }, + mappings: function () { + var myModes = config.browserModes; + + mappings.add(myModes, + ["go"], "Jump to a QuickMark", + function ({ arg }) { quickmarks.jumpTo(arg, dactyl.CURRENT_TAB); }, + { arg: true }); + + mappings.add(myModes, + ["gn"], "Jump to a QuickMark in a new tab", + function ({ arg }) { quickmarks.jumpTo(arg, { from: "quickmark", where: dactyl.NEW_TAB }); }, + { arg: true }); + + mappings.add(myModes, + ["M"], "Add new QuickMark for current URL", + function ({ arg }) { + dactyl.assert(/^[a-zA-Z0-9]$/.test(arg), _("quickmark.invalid")); + quickmarks.add(arg, buffer.uri.spec); + }, + { arg: true }); + } +}); + +// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/statusline.js b/common/content/statusline.js new file mode 100644 index 0000000..5494342 --- /dev/null +++ b/common/content/statusline.js @@ -0,0 +1,392 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2011 by Doug Kearns +// Copyright (c) 2008-2011 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +/** @scope modules */ + +var StatusLine = Module("statusline", { + init: function init() { + this._statusLine = document.getElementById("status-bar"); + this.statusBar = document.getElementById("addon-bar") || this._statusLine; + this.statusBar.collapsed = true; + this.baseGroup = this.statusBar == this._statusLine ? "StatusLine " : ""; + + if (this.statusBar.localName == "toolbar") { + styles.system.add("addon-bar", config.styleableChrome, statusbar { -moz-box-flex: 1 } + #addon-bar > #addonbar-closebutton { visibility: collapse; } + #addon-bar > xul|toolbarspring { visibility: collapse; } + ]]>); + + util.overlayWindow(window, { append: <> }); + + highlight.loadCSS(util.compileMacro( + } + !AddonButton;#addon-bar xul|toolbarbutton { + -moz-appearance: none !important; + padding: 0 !important; + border-width: 0px !important; + min-width: 0 !important; + color: inherit !important; + } + AddonButton:not(:hover) background: transparent !important; + ]]>)({ padding: util.OS.isMacOSX ? "padding-right: 10px !important;" : "" })); + + if (document.getElementById("appmenu-button")) + highlight.loadCSS(); + } + + XML.ignoreWhitespace = true; + let _commandline = "if (window.dactyl) return dactyl.modules.commandline"; + let prepend = +