]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/io.jsm
Import 1.0 supporting Firefox up to 14.*
[dactyl.git] / common / modules / io.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 // Some code based on Venkman
5 //
6 // This work is licensed for reuse under an MIT license. Details are
7 // given in the LICENSE.txt file included with this file.
8 /* use strict */
9
10 try {
11
12 Components.utils.import("resource://dactyl/bootstrap.jsm");
13 defineModule("io", {
14     exports: ["IO", "io"],
15     require: ["services"]
16 }, this);
17
18 this.lazyRequire("config", ["config"]);
19 this.lazyRequire("contexts", ["Contexts", "contexts"]);
20
21 // TODO: why are we passing around strings rather than file objects?
22 /**
23  * Provides a basic interface to common system I/O operations.
24  * @instance io
25  */
26 var IO = Module("io", {
27     init: function init() {
28         this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
29         this._cwd = this._processDir.path;
30         this._oldcwd = null;
31         lazyRequire("config", ["config"], this);
32     },
33
34     Local: function Local(dactyl, modules, window) let ({ io, plugins } = modules) ({
35
36         init: function init() {
37             this.config = modules.config;
38             this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
39             this._cwd = this._processDir.path;
40             this._oldcwd = null;
41
42             this._lastRunCommand = ""; // updated whenever the users runs a command with :!
43             this._scriptNames = [];
44         },
45
46         CommandFileMode: Class("CommandFileMode", modules.CommandMode, {
47             init: function init(prompt, params) {
48                 init.supercall(this);
49                 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
50                 update(this, params);
51             },
52
53             historyKey: "file",
54
55             get mode() modules.modes.FILE_INPUT,
56
57             complete: function (context) {
58                 if (this.completer)
59                     this.completer(context);
60
61                 context = context.fork("files", 0);
62                 modules.completion.file(context);
63                 context.filters = context.filters.concat(this.filters || []);
64             }
65         }),
66
67         /**
68          * Returns all directories named *name* in 'runtimepath'.
69          *
70          * @param {string} name
71          * @returns {nsIFile[])
72          */
73         getRuntimeDirectories: function getRuntimeDirectories(name) {
74             return modules.options.get("runtimepath").files
75                 .map(function (dir) dir.child(name))
76                 .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
77         },
78
79         // FIXME: multiple paths?
80         /**
81          * Sources files found in 'runtimepath'. For each relative path in *paths*
82          * each directory in 'runtimepath' is searched and if a matching file is
83          * found it is sourced. Only the first file found (per specified path) is
84          * sourced unless *all* is specified, then all found files are sourced.
85          *
86          * @param {[string]} paths An array of relative paths to source.
87          * @param {boolean} all Whether all found files should be sourced.
88          */
89         sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
90             let dirs = modules.options.get("runtimepath").files;
91             let found = null;
92
93             dactyl.echomsg(_("io.searchingFor", paths.join(" ").quote(), modules.options.get("runtimepath").stringValue), 2);
94
95         outer:
96             for (let dir in values(dirs)) {
97                 for (let [, path] in Iterator(paths)) {
98                     let file = dir.child(path);
99
100                     dactyl.echomsg(_("io.searchingFor", file.path.quote()), 3);
101
102                     if (file.exists() && file.isFile() && file.isReadable()) {
103                         found = io.source(file.path, false) || true;
104
105                         if (!all)
106                             break outer;
107                     }
108                 }
109             }
110
111             if (!found)
112                 dactyl.echomsg(_("io.notInRTP", paths.join(" ").quote()), 1);
113
114             return found;
115         },
116
117         /**
118          * Reads Ex commands, JavaScript or CSS from *filename*.
119          *
120          * @param {string} filename The name of the file to source.
121          * @param {object} params Extra parameters:
122          *      group:  The group in which to execute commands.
123          *      silent: Whether errors should not be reported.
124          */
125         source: function source(filename, params) {
126             const { contexts } = modules;
127             defineModule.loadLog.push("sourcing " + filename);
128
129             if (!isObject(params))
130                 params = { silent: params };
131
132             let time = Date.now();
133             return contexts.withContext(null, function () {
134                 try {
135                     var file = util.getFile(filename) || io.File(filename);
136
137                     if (!file.exists() || !file.isReadable() || file.isDirectory()) {
138                         if (!params.silent)
139                             dactyl.echoerr(_("io.notReadable", filename.quote()));
140                         return;
141                     }
142
143                     dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
144
145                     let uri = file.URI;
146
147                     let sourceJSM = function sourceJSM() {
148                         context = contexts.Module(uri);
149                         dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
150                     }
151
152                     if (/\.js,$/.test(filename))
153                         sourceJSM();
154                     else if (/\.js$/.test(filename)) {
155                         try {
156                             var context = contexts.Script(file, params.group);
157                             if (Set.has(this._scriptNames, file.path))
158                                 util.flushCache();
159
160                             dactyl.loadScript(uri.spec, context);
161                             dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
162                         }
163                         catch (e) {
164                             if (e == Contexts) { // Hack;
165                                 context.unload();
166                                 sourceJSM();
167                             }
168                             else {
169                                 if (e.fileName && !(e instanceof FailedAssertion))
170                                     try {
171                                         e.fileName = util.fixURI(e.fileName);
172                                         if (e.fileName == uri.spec)
173                                             e.fileName = filename;
174                                         e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
175                                     }
176                                     catch (e) {}
177                                 throw e;
178                             }
179                         }
180                     }
181                     else if (/\.css$/.test(filename))
182                         styles.registerSheet(uri.spec, false, true);
183                     else {
184                         context = contexts.Context(file, params.group);
185                         modules.commands.execute(file.read(), null, params.silent,
186                                                  null, {
187                             context: context,
188                             file: file.path,
189                             group: context.GROUP,
190                             line: 1
191                         });
192                         dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
193                     }
194
195                     Set.add(this._scriptNames, file.path);
196
197                     dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
198                     dactyl.log(_("dactyl.sourced", filename), 3);
199
200                     return context;
201                 }
202                 catch (e) {
203                     util.reportError(e);
204                     let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
205                     if (!params.silent)
206                         dactyl.echoerr(message);
207                 }
208                 finally {
209                     defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
210                 }
211             }, this);
212         }
213     }),
214
215     // TODO: there seems to be no way, short of a new component, to change
216     // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
217     /**
218      * Returns the current working directory.
219      *
220      * It's not possible to change the real CWD of the process so this
221      * state is maintained internally. External commands run via
222      * {@link #system} are executed in this directory.
223      *
224      * @returns {nsIFile}
225      */
226     get cwd() {
227         let dir = File(this._cwd);
228
229         // NOTE: the directory could have been deleted underneath us so
230         // fallback to the process's CWD
231         if (dir.exists() && dir.isDirectory())
232             return dir;
233         else
234             return this._processDir.clone();
235     },
236
237     /**
238      * Sets the current working directory.
239      *
240      * @param {string} newDir The new CWD. This may be a relative or
241      *     absolute path and is expanded by {@link #expandPath}.
242      */
243     set cwd(newDir) {
244         newDir = newDir && newDir.path || newDir || "~";
245
246         if (newDir == "-") {
247             util.assert(this._oldcwd != null, _("io.noPrevDir"));
248             [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
249         }
250         else {
251             let dir = io.File(newDir);
252             util.assert(dir.exists() && dir.isDirectory(), _("io.noSuchDir", dir.path.quote()));
253             dir.normalize();
254             [this._cwd, this._oldcwd] = [dir.path, this.cwd];
255         }
256         return this.cwd;
257     },
258
259     /**
260      * @property {function} File class.
261      * @final
262      */
263     File: Class.Memoize(function () let (io = this)
264         Class("File", File, {
265             init: function init(path, checkCWD)
266                 init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
267         })),
268
269     /**
270      * @property {Object} The current file sourcing context. As a file is
271      *     being sourced the 'file' and 'line' properties of this context
272      *     object are updated appropriately.
273      */
274     sourcing: null,
275
276     expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),
277
278     /**
279      * Returns the first user RC file found in *dir*.
280      *
281      * @param {File|string} dir The directory to search.
282      * @param {boolean} always When true, return a path whether
283      *     the file exists or not.
284      * @default $HOME.
285      * @returns {nsIFile} The RC file or null if none is found.
286      */
287     getRCFile: function getRCFile(dir, always) {
288         dir = this.File(dir || "~");
289
290         let rcFile1 = dir.child("." + config.name + "rc");
291         let rcFile2 = dir.child("_" + config.name + "rc");
292
293         if (config.OS.isWindows)
294             [rcFile1, rcFile2] = [rcFile2, rcFile1];
295
296         if (rcFile1.exists() && rcFile1.isFile())
297             return rcFile1;
298         else if (rcFile2.exists() && rcFile2.isFile())
299             return rcFile2;
300         else if (always)
301             return rcFile1;
302         return null;
303     },
304
305     /**
306      * Creates a temporary file.
307      *
308      * @returns {File}
309      */
310     createTempFile: function createTempFile(name, type) {
311         if (name instanceof Ci.nsIFile) {
312             var file = name.clone();
313             if (!type || type == "file")
314                 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
315             else
316                 file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, octal(777));
317         }
318         else {
319             file = services.directory.get("TmpD", Ci.nsIFile);
320             file.append(this.config.tempFile + (name ? "." + name : ""));
321             file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
322         }
323
324         services.externalApp.deleteTemporaryFileOnExit(file);
325
326         return File(file);
327     },
328
329     /**
330      * Determines whether the given URL string resolves to a JAR URL and
331      * returns the matching nsIJARURI object if it does.
332      *
333      * @param {string} url The URL to check.
334      * @returns {nsIJARURI|null}
335      */
336     isJarURL: function isJarURL(url) {
337         try {
338             let uri = util.newURI(url);
339             if (uri instanceof Ci.nsIJARURI)
340                 return uri;
341
342             let channel = services.io.newChannelFromURI(uri);
343             try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
344             if (channel instanceof Ci.nsIJARChannel)
345                 return channel.URI.QueryInterface(Ci.nsIJARURI);
346         }
347         catch (e) {}
348         return null;
349     },
350
351     /**
352      * Returns a list of the contents of the given JAR file which are
353      * children of the given path.
354      *
355      * @param {nsIURI|string} file The URI of the JAR file to list.
356      * @param {string} path The prefix path to search.
357      */
358     listJar: function listJar(file, path) {
359         file = util.getFile(file);
360         if (file && file.exists() && file.isFile() && file.isReadable()) {
361             // let jar = services.zipReader.getZip(file); Crashes.
362             let jar = services.ZipReader(file);
363             try {
364                 let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
365                                     + "[^/]*/?$");
366
367                 for (let entry in iter(jar.findEntries("*")))
368                     if (filter.test(entry))
369                         yield entry;
370             }
371             finally {
372                 if (jar)
373                     jar.close();
374             }
375         }
376     },
377
378     readHeredoc: function readHeredoc(end) {
379         return "";
380     },
381
382     /**
383      * Searches for the given executable file in the system executable
384      * file paths as specified by the PATH environment variable.
385      *
386      * On Windows, if the unadorned filename cannot be found, the
387      * extensions in the semicolon-separated list in the PATHSEP
388      * environment variable are successively appended to the original
389      * name and searched for in turn.
390      *
391      * @param {string} bin The name of the executable to find.
392      * @returns {File|null}
393      */
394     pathSearch: function pathSearch(bin) {
395         if (bin instanceof File || File.isAbsolutePath(bin))
396             return this.File(bin);
397
398         let dirs = services.environment.get("PATH")
399                            .split(config.OS.pathListSep);
400         // Windows tries the CWD first TODO: desirable?
401         if (config.OS.isWindows)
402             dirs = [io.cwd].concat(dirs);
403
404         for (let [, dir] in Iterator(dirs))
405             try {
406                 dir = this.File(dir, true);
407
408                 let file = dir.child(bin);
409                 if (file.exists() && file.isFile() && file.isExecutable())
410                     return file;
411
412                 // TODO: couldn't we just palm this off to the start command?
413                 // automatically try to add the executable path extensions on windows
414                 if (config.OS.isWindows) {
415                     let extensions = services.environment.get("PATHEXT").split(";");
416                     for (let [, extension] in Iterator(extensions)) {
417                         file = dir.child(bin + extension);
418                         if (file.exists())
419                             return file;
420                     }
421                 }
422             }
423             catch (e) {}
424         return null;
425     },
426
427     /**
428      * Runs an external program.
429      *
430      * @param {File|string} program The program to run.
431      * @param {[string]} args An array of arguments to pass to *program*.
432      */
433     run: function run(program, args, blocking, self) {
434         args = args || [];
435
436         let file = this.pathSearch(program);
437
438         if (!file || !file.exists()) {
439             util.dactyl.echoerr(_("io.noCommand", program));
440             if (callable(blocking))
441                 util.trapErrors(blocking);
442             return -1;
443         }
444
445         let process = services.Process(file);
446         process.run(false, args.map(String), args.length);
447         try {
448             if (callable(blocking))
449                 var timer = services.Timer(
450                     function () {
451                         if (!process.isRunning) {
452                             timer.cancel();
453                             util.trapErrors(blocking, self, process.exitValue);
454                         }
455                     },
456                     100, services.Timer.TYPE_REPEATING_SLACK);
457             else if (blocking)
458                 while (process.isRunning)
459                     util.threadYield(false, true);
460         }
461         catch (e) {
462             process.kill();
463             throw e;
464         }
465
466         return process.exitValue;
467     },
468
469     // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
470     // fixed use that instead of a tmpfile
471     /**
472      * Runs *command* in a subshell and returns the output. The shell used is
473      * that specified by the 'shell' option.
474      *
475      * @param {string|[string]} command The command to run. This can be a shell
476      *      command string or an array of strings (a command and arguments)
477      *      which will be escaped and concatenated.
478      * @param {string} input Any input to be provided to the command on stdin.
479      * @param {function(object)} callback A callback to be called when
480      *      the command completes. @optional
481      * @returns {object|null}
482      */
483     system: function system(command, input, callback) {
484         util.dactyl.echomsg(_("io.callingShell", command), 4);
485
486         let { shellEscape } = util.closure;
487
488         return this.withTempFiles(function (stdin, stdout, cmd) {
489             if (input instanceof File)
490                 stdin = input;
491             else if (input)
492                 stdin.write(input);
493
494             function result(status, output) ({
495                 __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
496                 valueOf: function () this.output,
497                 output: output.replace(/^(.*)\n$/, "$1"),
498                 returnValue: status,
499                 toString: function () this.output
500             });
501
502             function async(status) {
503                 let output = stdout.read();
504                 [stdin, stdout, cmd].forEach(function (f) f.exists() && f.remove(false));
505                 callback(result(status, output));
506             }
507
508             let shell = io.pathSearch(storage["options"].get("shell").value);
509             let shcf = storage["options"].get("shellcmdflag").value;
510             util.assert(shell, _("error.invalid", "'shell'"));
511
512             if (isArray(command))
513                 command = command.map(shellEscape).join(" ");
514
515             // TODO: implement 'shellredir'
516             if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
517                 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
518                 var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
519             }
520             else {
521                 cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
522                         ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
523                          shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
524                 res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
525             }
526
527             return callback ? true : result(res, stdout.read());
528         }, this, true);
529     },
530
531     /**
532      * Creates a temporary file context for executing external commands.
533      * *func* is called with a temp file, created with {@link #createTempFile},
534      * for each explicit argument. Ensures that all files are removed when
535      * *func* returns.
536      *
537      * @param {function} func The function to execute.
538      * @param {Object} self The 'this' object used when executing func.
539      * @returns {boolean} false if temp files couldn't be created,
540      *     otherwise, the return value of *func*.
541      */
542     withTempFiles: function withTempFiles(func, self, checked, ext) {
543         let args = array(util.range(0, func.length))
544                     .map(bind("createTempFile", this, ext)).array;
545         try {
546             if (!args.every(util.identity))
547                 return false;
548             var res = func.apply(self || this, args);
549         }
550         finally {
551             if (!checked || res !== true)
552                 args.forEach(function (f) f.remove(false));
553         }
554         return res;
555     }
556 }, {
557     /**
558      * @property {string} The value of the $PENTADACTYL_RUNTIME environment
559      *     variable.
560      */
561     get runtimePath() {
562         const rtpvar = config.idName + "_RUNTIME";
563         let rtp = services.environment.get(rtpvar);
564         if (!rtp) {
565             rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
566             services.environment.set(rtpvar, rtp);
567         }
568         return rtp;
569     },
570
571     /**
572      * @property {string} The current platform's path separator.
573      */
574     PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
575 }, {
576     commands: function init_commands(dactyl, modules, window) {
577         const { commands, completion, io } = modules;
578
579         commands.add(["cd", "chd[ir]"],
580             "Change the current directory",
581             function (args) {
582                 let arg = args[0];
583
584                 if (!arg)
585                     arg = "~";
586
587                 arg = File.expandPath(arg);
588
589                 // go directly to an absolute path or look for a relative path
590                 // match in 'cdpath'
591                 if (File.isAbsolutePath(arg)) {
592                     io.cwd = arg;
593                     dactyl.echomsg(io.cwd.path);
594                 }
595                 else {
596                     let dirs = modules.options.get("cdpath").files;
597                     for (let dir in values(dirs)) {
598                         dir = dir.child(arg);
599
600                         if (dir.exists() && dir.isDirectory() && dir.isReadable()) {
601                             io.cwd = dir;
602                             dactyl.echomsg(io.cwd.path);
603                             return;
604                         }
605                     }
606
607                     dactyl.echoerr(_("io.noSuchDir", arg.quote()));
608                     dactyl.echoerr(_("io.commandFailed"));
609                 }
610             }, {
611                 argCount: "?",
612                 completer: function (context) completion.directory(context, true),
613                 literal: 0
614             });
615
616         commands.add(["pw[d]"],
617             "Print the current directory name",
618             function () { dactyl.echomsg(io.cwd.path); },
619             { argCount: "0" });
620
621         commands.add([config.name.replace(/(.)(.*)/, "mk$1[$2rc]")],
622             "Write current key mappings and changed options to the config file",
623             function (args) {
624                 dactyl.assert(args.length <= 1, _("io.oneFileAllowed"));
625
626                 let file = io.File(args[0] || io.getRCFile(null, true));
627
628                 dactyl.assert(!file.exists() || args.bang, _("io.exists", file.path.quote()));
629
630                 // TODO: Use a set/specifiable list here:
631                 let lines = [cmd.serialize().map(commands.commandToString, cmd) for (cmd in commands.iterator()) if (cmd.serialize)];
632                 lines = array.flatten(lines);
633
634                 lines.unshift('"' + config.version + "\n");
635                 lines.push("\n\" vim: set ft=" + config.name + ":");
636
637                 try {
638                     file.write(lines.join("\n"));
639                 }
640                 catch (e) {
641                     dactyl.echoerr(_("io.notWriteable", file.path.quote()));
642                     dactyl.log(_("error.notWriteable", file.path, e.message)); // XXX
643                 }
644             }, {
645                 argCount: "*", // FIXME: should be "?" but kludged for proper error message
646                 bang: true,
647                 completer: function (context) completion.file(context, true)
648             });
649
650         commands.add(["mks[yntax]"],
651             "Generate a Vim syntax file",
652             function (args) {
653                 let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
654                 let file = io.File(runtime + "syntax/" + config.name + ".vim");
655                 if (args.length)
656                     file = io.File(args[0]);
657
658                 if (file.exists() && file.isDirectory() || args[0] && /\/$/.test(args[0]))
659                     file.append(config.name + ".vim");
660                 dactyl.assert(!file.exists() || args.bang, _("io.exists"));
661
662                 let template = util.compileMacro(<![CDATA[
663 " Vim syntax file
664 " Language:         Pentadactyl configuration file
665 " Maintainer:       Doug Kearns <dougkearns@gmail.com>
666
667 " TODO: make this <name> specific - shared dactyl config?
668
669 if exists("b:current_syntax")
670   finish
671 endif
672
673 let s:cpo_save = &cpo
674 set cpo&vim
675
676 syn include @javascriptTop syntax/javascript.vim
677 unlet b:current_syntax
678
679 syn include @cssTop syntax/css.vim
680 unlet b:current_syntax
681
682 syn match <name>CommandStart "\%(^\s*:\=\)\@<=" nextgroup=<name>Command,<name>AutoCmd
683
684 <commands>
685     \ contained
686
687 syn match <name>Command "!" contained
688
689 syn keyword <name>AutoCmd au[tocmd] contained nextgroup=<name>AutoEventList skipwhite
690
691 <autocommands>
692     \ contained
693
694 syn match <name>AutoEventList "\(\a\+,\)*\a\+" contained contains=<name>AutoEvent
695
696 syn region <name>Set matchgroup=<name>Command start="\%(^\s*:\=\)\@<=\<\%(setl\%[ocal]\|setg\%[lobal]\|set\=\)\=\>"
697     \ end="$" keepend oneline contains=<name>Option,<name>String
698
699 <options>
700     \ contained nextgroup=pentadactylSetMod
701
702 <toggleoptions>
703 execute 'syn match <name>Option "\<\%(no\|inv\)\=\%(' .
704     \ join(s:toggleOptions, '\|') .
705     \ '\)\>!\=" contained nextgroup=<name>SetMod'
706
707 syn match <name>SetMod "\%(\<[a-z_]\+\)\@<=&" contained
708
709 syn region <name>JavaScript start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=" end="$" contains=@javascriptTop keepend oneline
710 syn region <name>JavaScript matchgroup=<name>JavaScriptDelimiter
711     \ start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@javascriptTop fold
712
713 let s:cssRegionStart = '\%(^\s*sty\%[le]!\=\s\+\%(-\%(n\|name\)\%(\s\+\|=\)\S\+\s\+\)\=[^-]\S\+\s\+\)\@<='
714 execute 'syn region <name>Css start="' . s:cssRegionStart . '" end="$" contains=@cssTop keepend oneline'
715 execute 'syn region <name>Css matchgroup=<name>CssDelimiter'
716     \ 'start="' . s:cssRegionStart . '<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@cssTop fold'
717
718 syn match <name>Notation "<[0-9A-Za-z-]\+>"
719
720 syn keyword <name>Todo FIXME NOTE TODO XXX contained
721
722 syn region <name>String start="\z(["']\)" end="\z1" skip="\\\\\|\\\z1" oneline
723
724 syn match <name>Comment +^\s*".*$+ contains=<name>Todo,@Spell
725
726 " NOTE: match vim.vim highlighting group names
727 hi def link <name>AutoCmd               <name>Command
728 hi def link <name>AutoEvent             Type
729 hi def link <name>Command               Statement
730 hi def link <name>JavaScriptDelimiter   Delimiter
731 hi def link <name>CssDelimiter          Delimiter
732 hi def link <name>Notation              Special
733 hi def link <name>Comment               Comment
734 hi def link <name>Option                PreProc
735 hi def link <name>SetMod                <name>Option
736 hi def link <name>String                String
737 hi def link <name>Todo                  Todo
738
739 let b:current_syntax = "<name>"
740
741 let &cpo = s:cpo_save
742 unlet s:cpo_save
743
744 " vim: tw=130 et ts=4 sw=4:
745 ]]>, true);
746
747                 const WIDTH = 80;
748                 function wrap(prefix, items, sep) {
749                     sep = sep || " ";
750                     let width = 0;
751                     let lines = [];
752                     lines.__defineGetter__("last", function () this[this.length - 1]);
753
754                     for (let item in values(items.array || items)) {
755                         if (item.length > width && (!lines.length || lines.last.length > 1)) {
756                             lines.push([prefix]);
757                             width = WIDTH - prefix.length;
758                             prefix = "    \\ ";
759                         }
760                         width -= item.length + sep.length;
761                         lines.last.push(item, sep);
762                     }
763                     lines.last.pop();
764                     return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
765                 }
766
767                 const { commands, options } = modules;
768                 file.write(template({
769                     name: config.name,
770                     autocommands: wrap("syn keyword " + config.name + "AutoEvent ",
771                                        keys(config.autocommands)),
772                     commands: wrap("syn keyword " + config.name + "Command ",
773                                   array(c.specs for (c in commands.iterator())).flatten()),
774                     options: wrap("syn keyword " + config.name + "Option ",
775                                   array(o.names for (o in options) if (o.type != "boolean")).flatten()),
776                     toggleoptions: wrap("let s:toggleOptions = [",
777                                         array(o.realNames for (o in options) if (o.type == "boolean"))
778                                             .flatten().map(String.quote),
779                                         ", ") + "]"
780                 }));
781             }, {
782                 argCount: "?",
783                 bang: true,
784                 completer: function (context) completion.file(context, true),
785                 literal: 1
786             });
787
788         commands.add(["runt[ime]"],
789             "Source the specified file from each directory in 'runtimepath'",
790             function (args) { io.sourceFromRuntimePath(args, args.bang); },
791             {
792                 argCount: "+",
793                 bang: true,
794                 completer: function (context) completion.runtime(context)
795             }
796         );
797
798         commands.add(["scrip[tnames]"],
799             "List all sourced script names",
800             function () {
801                 let names = Object.keys(io._scriptNames);
802                 if (!names.length)
803                     dactyl.echomsg(_("command.scriptnames.none"));
804                 else
805                     modules.commandline.commandOutput(
806                         template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
807                             ([i + 1, file] for ([i, file] in Iterator(names)))));
808
809             },
810             { argCount: "0" });
811
812         commands.add(["so[urce]"],
813             "Read Ex commands, JavaScript or CSS from a file",
814             function (args) {
815                 if (args.length > 1)
816                     dactyl.echoerr(_("io.oneFileAllowed"));
817                 else
818                     io.source(args[0], { silent: args.bang });
819             }, {
820                 argCount: "+", // FIXME: should be "1" but kludged for proper error message
821                 bang: true,
822                 completer: function (context) completion.file(context, true)
823             });
824
825         commands.add(["!", "run"],
826             "Run a command",
827             function (args) {
828                 let arg = args[0] || "";
829
830                 // :!! needs to be treated specially as the command parser sets the
831                 // bang flag but removes the ! from arg
832                 if (args.bang)
833                     arg = "!" + arg;
834
835                 // This is an asinine and irritating "feature" when we have searchable
836                 // command-line history. --Kris
837                 if (modules.options["banghist"]) {
838                     // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
839                     // pass through a raw bang when escaped or substitute the last command
840
841                     // replaceable bang and no previous command?
842                     dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
843                         _("command.run.noPrevious"));
844
845                     arg = arg.replace(/(\\)*!/g,
846                         function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", io._lastRunCommand)
847                     );
848                 }
849
850                 io._lastRunCommand = arg;
851
852                 let result = io.system(arg);
853                 if (result.returnValue != 0)
854                     result.output += "\n" + _("io.shellReturn", result.returnValue);
855
856                 modules.commandline.command = args.commandName.replace("run", "$& ") + arg;
857                 modules.commandline.commandOutput(<span highlight="CmdOutput">{result.output}</span>);
858
859                 modules.autocommands.trigger("ShellCmdPost", {});
860             }, {
861                 argCount: "?",
862                 bang: true,
863                 // This is abominably slow.
864                 // completer: function (context) completion.shellCommand(context),
865                 literal: 0
866             });
867     },
868     completion: function init_completion(dactyl, modules, window) {
869         const { completion, io } = modules;
870
871         completion.charset = function (context) {
872             context.anchored = false;
873             context.keys = {
874                 text: util.identity,
875                 description: function (charset) {
876                     try {
877                         return services.charset.getCharsetTitle(charset);
878                     }
879                     catch (e) {
880                         return charset;
881                     }
882                 }
883             };
884             context.generate = function () iter(services.charset.getDecoderList());
885         };
886
887         completion.directory = function directory(context, full) {
888             this.file(context, full);
889             context.filters.push(function (item) item.isdir);
890         };
891
892         completion.environment = function environment(context) {
893             context.title = ["Environment Variable", "Value"];
894             context.generate = function ()
895                 io.system(config.OS.isWindows ? "set" : "env")
896                   .output.split("\n")
897                   .filter(function (line) line.indexOf("=") > 0)
898                   .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
899         };
900
901         completion.file = function file(context, full, dir) {
902             if (/^jar:[^!]*$/.test(context.filter))
903                 context.advance(4);
904
905             // dir == "" is expanded inside readDirectory to the current dir
906             function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
907             dir = getDir(dir || context.filter);
908
909             let file = util.getFile(dir);
910             if (file && (!file.exists() || !file.isDirectory()))
911                 file = file.parent;
912
913             if (!full)
914                 context.advance(dir.length);
915
916             context.title = [full ? "Path" : "Filename", "Type"];
917             context.keys = {
918                 text: !full ? "leafName" : function (f) this.path,
919                 path: function (f) dir + f.leafName,
920                 description: function (f) this.isdir ? "Directory" : "File",
921                 isdir: function (f) f.isDirectory(),
922                 icon: function (f) this.isdir ? "resource://gre/res/html/folder.png"
923                                               : "moz-icon://" + f.leafName
924             };
925             context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text);
926
927             if (modules.options["wildignore"])
928                 context.filters.push(function (item) !modules.options.get("wildignore").getKey(item.path));
929
930             // context.background = true;
931             context.key = dir;
932             let uri = io.isJarURL(dir);
933             if (uri)
934                 context.generate = function generate_jar() {
935                     return [
936                         {
937                               isDirectory: function () s.substr(-1) == "/",
938                               leafName: /([^\/]*)\/?$/.exec(s)[1]
939                         }
940                         for (s in io.listJar(uri.JARFile, getDir(uri.JAREntry)))]
941                 };
942             else
943                 context.generate = function generate_file() {
944                     try {
945                         return io.File(file || dir).readDirectory();
946                     }
947                     catch (e) {}
948                     return [];
949                 };
950         };
951
952         completion.runtime = function (context) {
953             for (let [, dir] in Iterator(modules.options["runtimepath"]))
954                 context.fork(dir, 0, this, function (context) {
955                     dir = dir.replace("/+$", "") + "/";
956                     completion.file(context, true, dir + context.filter);
957                     context.title[0] = dir;
958                     context.keys.text = function (f) this.path.substr(dir.length);
959                 });
960         };
961
962         completion.shellCommand = function shellCommand(context) {
963             context.title = ["Shell Command", "Path"];
964             context.generate = function () {
965                 let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
966                 let commands = [];
967
968                 for (let [, dirName] in Iterator(dirNames)) {
969                     let dir = io.File(dirName);
970                     if (dir.exists() && dir.isDirectory())
971                         commands.push([[file.leafName, dir.path] for (file in iter(dir.directoryEntries))
972                                        if (file.isFile() && file.isExecutable())]);
973                 }
974
975                 return array.flatten(commands);
976             };
977         };
978
979         completion.addUrlCompleter("file", "Local files", function (context, full) {
980             let match = util.regexp(<![CDATA[
981                 ^
982                 (?P<prefix>
983                     (?P<proto>
984                         (?P<scheme> chrome|resource)
985                         :\/\/
986                     )
987                     [^\/]*
988                 )
989                 (?P<path> \/[^\/]* )?
990                 $
991             ]]>, "x").exec(context.filter);
992             if (match) {
993                 if (!match.path) {
994                     context.key = match.proto;
995                     context.advance(match.proto.length);
996                     context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
997                 }
998                 else if (match.scheme === "chrome") {
999                     context.key = match.prefix;
1000                     context.advance(match.prefix.length + 1);
1001                     context.generate = function () iter({
1002                         content: /*L*/"Chrome content",
1003                         locale: /*L*/"Locale-specific content",
1004                         skin: /*L*/"Theme-specific content"
1005                     });
1006                 }
1007             }
1008             if (!match || match.scheme === "resource" && match.path)
1009                 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
1010                     || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
1011                     || util.getFile(context.filter)
1012                     || io.isJarURL(context.filter))
1013                     completion.file(context, full);
1014         });
1015     },
1016     javascript: function init_javascript(dactyl, modules, window) {
1017         modules.JavaScript.setCompleter([File, File.expandPath],
1018             [function (context, obj, args) {
1019                 context.quote[2] = "";
1020                 modules.completion.file(context, true);
1021             }]);
1022
1023     },
1024     modes: function initModes(dactyl, modules, window) {
1025         initModes.require("commandline");
1026         const { modes } = modules;
1027
1028         modes.addMode("FILE_INPUT", {
1029             extended: true,
1030             description: "Active when selecting a file",
1031             bases: [modes.COMMAND_LINE],
1032             input: true
1033         });
1034     },
1035     options: function init_options(dactyl, modules, window) {
1036         const { completion, options } = modules;
1037
1038         var shell, shellcmdflag;
1039         if (config.OS.isWindows) {
1040             shell = "cmd.exe";
1041             shellcmdflag = "/c";
1042         }
1043         else {
1044             shell = services.environment.get("SHELL") || "sh";
1045             shellcmdflag = "-c";
1046         }
1047
1048         options.add(["banghist", "bh"],
1049             "Replace occurrences of ! with the previous command when executing external commands",
1050             "boolean", false);
1051
1052         options.add(["fileencoding", "fenc"],
1053             "The character encoding used when reading and writing files",
1054             "string", "UTF-8", {
1055                 completer: function (context) completion.charset(context),
1056                 getter: function () File.defaultEncoding,
1057                 setter: function (value) (File.defaultEncoding = value)
1058             });
1059         options.add(["cdpath", "cd"],
1060             "List of directories searched when executing :cd",
1061             "stringlist", ["."].concat(services.environment.get("CDPATH").split(/[:;]/).filter(util.identity)).join(","),
1062             {
1063                 get files() this.value.map(function (path) File(path, modules.io.cwd))
1064                                 .filter(function (dir) dir.exists()),
1065                 setter: function (value) File.expandPathList(value)
1066             });
1067
1068         options.add(["runtimepath", "rtp"],
1069             "List of directories searched for runtime files",
1070             "stringlist", IO.runtimePath,
1071             {
1072                 get files() this.value.map(function (path) File(path, modules.io.cwd))
1073                                 .filter(function (dir) dir.exists())
1074             });
1075
1076         options.add(["shell", "sh"],
1077             "Shell to use for executing external commands with :! and :run",
1078             "string", shell,
1079             { validator: function (val) io.pathSearch(val) });
1080
1081         options.add(["shellcmdflag", "shcf"],
1082             "Flag passed to shell when executing external commands with :! and :run",
1083             "string", shellcmdflag,
1084             {
1085                 getter: function (value) {
1086                     if (this.hasChanged || !config.OS.isWindows)
1087                         return value;
1088                     return /sh/.test(options["shell"]) ? "-c" : "/c";
1089                 }
1090             });
1091         options["shell"]; // Make sure it's loaded into global storage.
1092         options["shellcmdflag"];
1093
1094         options.add(["wildignore", "wig"],
1095             "List of path name patterns to ignore when completing files and directories",
1096             "regexplist", "");
1097     }
1098 });
1099
1100 endModule();
1101
1102 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1103
1104 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: