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