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