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