]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/io.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / io.jsm
index 63ac6061921a35d2d88c12a13d1f65c7712c458b..5eefcc17b6bb6aeaceecae247a49df9035a36846 100644 (file)
@@ -5,31 +5,33 @@
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
 
 try {
 
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("io", {
     exports: ["IO", "io"],
-    require: ["services"],
-    use: ["config", "messages", "storage", "styles", "template", "util"]
+    require: ["services"]
 }, this);
 
+this.lazyRequire("config", ["config"]);
+this.lazyRequire("contexts", ["Contexts", "contexts"]);
+
 // TODO: why are we passing around strings rather than file objects?
 /**
  * Provides a basic interface to common system I/O operations.
  * @instance io
  */
 var IO = Module("io", {
-    init: function () {
+    init: function init() {
         this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
         this._cwd = this._processDir.path;
         this._oldcwd = null;
-        this.config = config;
+        lazyRequire("config", ["config"], this);
     },
 
-    Local: function (dactyl, modules, window) let ({ io, plugins } = modules) ({
+    Local: function Local(dactyl, modules, window) let ({ io, plugins } = modules) ({
 
         init: function init() {
             this.config = modules.config;
@@ -164,25 +166,40 @@ var IO = Module("io", {
 
                     dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
 
-                    let uri = services.io.newFileURI(file);
+                    let uri = file.URI;
+
+                    let sourceJSM = function sourceJSM() {
+                        context = contexts.Module(uri);
+                        dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
+                    }
 
-                    // handle pure JavaScript files specially
-                    if (/\.js$/.test(filename)) {
+                    if (/\.js,$/.test(filename))
+                        sourceJSM();
+                    else if (/\.js$/.test(filename)) {
                         try {
                             var context = contexts.Script(file, params.group);
+                            if (Set.has(this._scriptNames, file.path))
+                                util.flushCache();
+
                             dactyl.loadScript(uri.spec, context);
-                            dactyl.helpInitialized = false;
+                            dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
                         }
                         catch (e) {
-                            if (e.fileName)
-                                try {
-                                    e.fileName = util.fixURI(e.fileName);
-                                    if (e.fileName == uri.spec)
-                                        e.fileName = filename;
-                                    e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
-                                }
-                                catch (e) {}
-                            throw e;
+                            if (e == Contexts) { // Hack;
+                                context.unload();
+                                sourceJSM();
+                            }
+                            else {
+                                if (e.fileName && !(e instanceof FailedAssertion))
+                                    try {
+                                        e.fileName = util.fixURI(e.fileName);
+                                        if (e.fileName == uri.spec)
+                                            e.fileName = filename;
+                                        e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
+                                    }
+                                    catch (e) {}
+                                throw e;
+                            }
                         }
                     }
                     else if (/\.css$/.test(filename))
@@ -196,10 +213,10 @@ var IO = Module("io", {
                             group: context.GROUP,
                             line: 1
                         });
+                        dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
                     }
 
-                    if (this._scriptNames.indexOf(file.path) == -1)
-                        this._scriptNames.push(file.path);
+                    Set.add(this._scriptNames, file.path);
 
                     dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
                     dactyl.log(_("dactyl.sourced", filename), 3);
@@ -207,7 +224,7 @@ var IO = Module("io", {
                     return context;
                 }
                 catch (e) {
-                    dactyl.reportError(e);
+                    util.reportError(e);
                     let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
                     if (!params.silent)
                         dactyl.echoerr(message);
@@ -267,7 +284,7 @@ var IO = Module("io", {
      * @property {function} File class.
      * @final
      */
-    File: Class.memoize(function () let (io = this)
+    File: Class.Memoize(function () let (io = this)
         Class("File", File, {
             init: function init(path, checkCWD)
                 init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
@@ -291,13 +308,13 @@ var IO = Module("io", {
      * @default $HOME.
      * @returns {nsIFile} The RC file or null if none is found.
      */
-    getRCFile: function (dir, always) {
+    getRCFile: function getRCFile(dir, always) {
         dir = this.File(dir || "~");
 
         let rcFile1 = dir.child("." + config.name + "rc");
         let rcFile2 = dir.child("_" + config.name + "rc");
 
-        if (util.OS.isWindows)
+        if (config.OS.isWindows)
             [rcFile1, rcFile2] = [rcFile2, rcFile1];
 
         if (rcFile1.exists() && rcFile1.isFile())
@@ -309,19 +326,21 @@ var IO = Module("io", {
         return null;
     },
 
-    // TODO: make secure
     /**
      * Creates a temporary file.
      *
      * @returns {File}
      */
-    createTempFile: function () {
-        let file = services.directory.get("TmpD", Ci.nsIFile);
-        file.append(this.config.tempFile);
+    createTempFile: function createTempFile(name) {
+        if (name instanceof Ci.nsIFile)
+            var file = name.clone();
+        else {
+            file = services.directory.get("TmpD", Ci.nsIFile);
+            file.append(this.config.tempFile + (name ? "." + name : ""));
+        }
         file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(600));
 
-        Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
-            .getService(Ci.nsPIExternalAppLauncher).deleteTemporaryFileOnExit(file);
+        services.externalApp.deleteTemporaryFileOnExit(file);
 
         return File(file);
     },
@@ -335,9 +354,12 @@ var IO = Module("io", {
      */
     isJarURL: function isJarURL(url) {
         try {
-            let uri = util.newURI(util.fixURI(url));
+            let uri = util.newURI(url);
+            if (uri instanceof Ci.nsIJARURI)
+                return uri;
+
             let channel = services.io.newChannelFromURI(uri);
-            channel.cancel(Cr.NS_BINDING_ABORTED);
+            try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
             if (channel instanceof Ci.nsIJARChannel)
                 return channel.URI.QueryInterface(Ci.nsIJARURI);
         }
@@ -372,7 +394,7 @@ var IO = Module("io", {
         }
     },
 
-    readHeredoc: function (end) {
+    readHeredoc: function readHeredoc(end) {
         return "";
     },
 
@@ -386,14 +408,15 @@ var IO = Module("io", {
      * name and searched for in turn.
      *
      * @param {string} bin The name of the executable to find.
+     * @returns {File|null}
      */
-    pathSearch: function (bin) {
+    pathSearch: function pathSearch(bin) {
         if (bin instanceof File || File.isAbsolutePath(bin))
             return this.File(bin);
 
-        let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+        let dirs = services.environment.get("PATH").split(config.OS.isWindows ? ";" : ":");
         // Windows tries the CWD first TODO: desirable?
-        if (util.OS.isWindows)
+        if (config.OS.isWindows)
             dirs = [io.cwd].concat(dirs);
 
         for (let [, dir] in Iterator(dirs))
@@ -406,7 +429,7 @@ var IO = Module("io", {
 
                 // TODO: couldn't we just palm this off to the start command?
                 // automatically try to add the executable path extensions on windows
-                if (util.OS.isWindows) {
+                if (config.OS.isWindows) {
                     let extensions = services.environment.get("PATHEXT").split(";");
                     for (let [, extension] in Iterator(extensions)) {
                         file = dir.child(bin + extension);
@@ -425,7 +448,7 @@ var IO = Module("io", {
      * @param {File|string} program The program to run.
      * @param {[string]} args An array of arguments to pass to *program*.
      */
-    run: function (program, args, blocking) {
+    run: function run(program, args, blocking, self) {
         args = args || [];
 
         let file = this.pathSearch(program);
@@ -445,7 +468,7 @@ var IO = Module("io", {
                     function () {
                         if (!process.isRunning) {
                             timer.cancel();
-                            util.trapErrors(blocking);
+                            util.trapErrors(blocking, self, process.exitValue);
                         }
                     },
                     100, services.Timer.TYPE_REPEATING_SLACK);
@@ -469,12 +492,14 @@ var IO = Module("io", {
      *
      * @param {string} command The command to run.
      * @param {string} input Any input to be provided to the command on stdin.
-     * @returns {object}
+     * @param {function(object)} callback A callback to be called when
+     *      the command completes. @optional
+     * @returns {object|null}
      */
-    system: function (command, input) {
+    system: function system(command, input, callback) {
         util.dactyl.echomsg(_("io.callingShell", command), 4);
 
-        function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
+        let { shellEscape } = util.closure;
 
         return this.withTempFiles(function (stdin, stdout, cmd) {
             if (input instanceof File)
@@ -482,33 +507,41 @@ var IO = Module("io", {
             else if (input)
                 stdin.write(input);
 
+            function result(status, output) ({
+                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
+                valueOf: function () this.output,
+                output: output.replace(/^(.*)\n$/, "$1"),
+                returnValue: status,
+                toString: function () this.output
+            });
+
+            function async(status) {
+                let output = stdout.read();
+                [stdin, stdout, cmd].forEach(function (f) f.exists() && f.remove(false));
+                callback(result(status, output));
+            }
+
             let shell = io.pathSearch(storage["options"].get("shell").value);
             let shcf = storage["options"].get("shellcmdflag").value;
             util.assert(shell, _("error.invalid", "'shell'"));
 
             if (isArray(command))
-                command = command.map(escape).join(" ");
+                command = command.map(shellEscape).join(" ");
 
             // TODO: implement 'shellredir'
-            if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
+            if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
                 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
-                var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
+                var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
             }
             else {
-                cmd.write("cd " + escape(this.cwd.path) + "\n" +
-                        ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
-                         escape(shell.path), shcf, escape(command)].join(" "));
-                res = this.run("/bin/sh", ["-e", cmd.path], true);
+                cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
+                        ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
+                         shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
+                res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
             }
 
-            return {
-                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
-                valueOf: function () this.output,
-                output: stdout.read().replace(/^(.*)\n$/, "$1"),
-                returnValue: res,
-                toString: function () this.output
-            };
-        }) || "";
+            return callback ? true : result(res, stdout.read());
+        }, this, true);
     },
 
     /**
@@ -522,8 +555,8 @@ var IO = Module("io", {
      * @returns {boolean} false if temp files couldn't be created,
      *     otherwise, the return value of *func*.
      */
-    withTempFiles: function (func, self, checked) {
-        let args = array(util.range(0, func.length)).map(this.closure.createTempFile).array;
+    withTempFiles: function withTempFiles(func, self, checked, ext) {
+        let args = array(util.range(0, func.length)).map(bind("createTempFile", this, ext)).array;
         try {
             if (!args.every(util.identity))
                 return false;
@@ -531,7 +564,7 @@ var IO = Module("io", {
         }
         finally {
             if (!checked || res !== true)
-                args.forEach(function (f) f && f.remove(false));
+                args.forEach(function (f) f.remove(false));
         }
         return res;
     }
@@ -544,7 +577,7 @@ var IO = Module("io", {
         const rtpvar = config.idName + "_RUNTIME";
         let rtp = services.environment.get(rtpvar);
         if (!rtp) {
-            rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name;
+            rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
             services.environment.set(rtpvar, rtp);
         }
         return rtp;
@@ -555,7 +588,7 @@ var IO = Module("io", {
      */
     PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
 }, {
-    commands: function (dactyl, modules, window) {
+    commands: function init_commands(dactyl, modules, window) {
         const { commands, completion, io } = modules;
 
         commands.add(["cd", "chd[ir]"],
@@ -632,7 +665,7 @@ var IO = Module("io", {
         commands.add(["mks[yntax]"],
             "Generate a Vim syntax file",
             function (args) {
-                let runtime = util.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
+                let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
                 let file = io.File(runtime + "syntax/" + config.name + ".vim");
                 if (args.length)
                     file = io.File(args[0]);
@@ -780,12 +813,13 @@ unlet s:cpo_save
         commands.add(["scrip[tnames]"],
             "List all sourced script names",
             function () {
-                if (!io._scriptNames.length)
+                let names = Object.keys(io._scriptNames);
+                if (!names.length)
                     dactyl.echomsg(_("command.scriptnames.none"));
                 else
                     modules.commandline.commandOutput(
                         template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
-                            ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))));
+                            ([i + 1, file] for ([i, file] in Iterator(names)))));
 
             },
             { argCount: "0" });
@@ -813,12 +847,12 @@ unlet s:cpo_save
                 if (args.bang)
                     arg = "!" + arg;
 
-                // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
-                // pass through a raw bang when escaped or substitute the last command
-
-                // This is an asinine and irritating feature when we have searchable
+                // This is an asinine and irritating "feature" when we have searchable
                 // command-line history. --Kris
                 if (modules.options["banghist"]) {
+                    // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
+                    // pass through a raw bang when escaped or substitute the last command
+
                     // replaceable bang and no previous command?
                     dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
                         _("command.run.noPrevious"));
@@ -846,7 +880,7 @@ unlet s:cpo_save
                 literal: 0
             });
     },
-    completion: function (dactyl, modules, window) {
+    completion: function init_completion(dactyl, modules, window) {
         const { completion, io } = modules;
 
         completion.charset = function (context) {
@@ -873,7 +907,7 @@ unlet s:cpo_save
         completion.environment = function environment(context) {
             context.title = ["Environment Variable", "Value"];
             context.generate = function ()
-                io.system(util.OS.isWindows ? "set" : "env")
+                io.system(config.OS.isWindows ? "set" : "env")
                   .output.split("\n")
                   .filter(function (line) line.indexOf("=") > 0)
                   .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
@@ -943,7 +977,7 @@ unlet s:cpo_save
         completion.shellCommand = function shellCommand(context) {
             context.title = ["Shell Command", "Path"];
             context.generate = function () {
-                let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
+                let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
                 let commands = [];
 
                 for (let [, dirName] in Iterator(dirNames)) {
@@ -957,7 +991,7 @@ unlet s:cpo_save
             };
         };
 
-        completion.addUrlCompleter("f", "Local files", function (context, full) {
+        completion.addUrlCompleter("file", "Local files", function (context, full) {
             let match = util.regexp(<![CDATA[
                 ^
                 (?P<prefix>
@@ -974,7 +1008,7 @@ unlet s:cpo_save
                 if (!match.path) {
                     context.key = match.proto;
                     context.advance(match.proto.length);
-                    context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]);
+                    context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
                 }
                 else if (match.scheme === "chrome") {
                     context.key = match.prefix;
@@ -988,13 +1022,13 @@ unlet s:cpo_save
             }
             if (!match || match.scheme === "resource" && match.path)
                 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
-                    || util.OS.isWindows && /^[a-z]:/i.test(context.filter)
+                    || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
                     || util.getFile(context.filter)
                     || io.isJarURL(context.filter))
                     completion.file(context, full);
         });
     },
-    javascript: function (dactyl, modules, window) {
+    javascript: function init_javascript(dactyl, modules, window) {
         modules.JavaScript.setCompleter([File, File.expandPath],
             [function (context, obj, args) {
                 context.quote[2] = "";
@@ -1013,11 +1047,11 @@ unlet s:cpo_save
             input: true
         });
     },
-    options: function (dactyl, modules, window) {
+    options: function init_options(dactyl, modules, window) {
         const { completion, options } = modules;
 
         var shell, shellcmdflag;
-        if (util.OS.isWindows) {
+        if (config.OS.isWindows) {
             shell = "cmd.exe";
             shellcmdflag = "/c";
         }
@@ -1028,7 +1062,7 @@ unlet s:cpo_save
 
         options.add(["banghist", "bh"],
             "Replace occurrences of ! with the previous command when executing external commands",
-            "boolean", true);
+            "boolean", false);
 
         options.add(["fileencoding", "fenc"],
             "The character encoding used when reading and writing files",
@@ -1064,7 +1098,7 @@ unlet s:cpo_save
             "string", shellcmdflag,
             {
                 getter: function (value) {
-                    if (this.hasChanged || !util.OS.isWindows)
+                    if (this.hasChanged || !config.OS.isWindows)
                         return value;
                     return /sh/.test(options["shell"]) ? "-c" : "/c";
                 }