]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/io.jsm
a6487a99ea99d18ecd88928349a378180cafc0db
[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
206                     dactyl.log("Sourced: " + filename, 3);
207                     return context;
208                 }
209                 catch (e) {
210                     dactyl.reportError(e);
211                     let message = "Sourcing file: " + (e.echoerr || file.path + ": " + 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) {
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                 jar.close();
370             }
371         }
372     },
373
374     readHeredoc: function (end) {
375         return "";
376     },
377
378     /**
379      * Searches for the given executable file in the system executable
380      * file paths as specified by the PATH environment variable.
381      *
382      * On Windows, if the unadorned filename cannot be found, the
383      * extensions in the semicolon-separated list in the PATHSEP
384      * environment variable are successively appended to the original
385      * name and searched for in turn.
386      *
387      * @param {string} bin The name of the executable to find.
388      */
389     pathSearch: function (bin) {
390         if (bin instanceof File || File.isAbsolutePath(bin))
391             return this.File(bin);
392
393         let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
394         // Windows tries the CWD first TODO: desirable?
395         if (util.OS.isWindows)
396             dirs = [io.cwd].concat(dirs);
397
398         for (let [, dir] in Iterator(dirs))
399             try {
400                 dir = this.File(dir, true);
401
402                 let file = dir.child(bin);
403                 if (file.exists() && file.isFile() && file.isExecutable())
404                     return file;
405
406                 // TODO: couldn't we just palm this off to the start command?
407                 // automatically try to add the executable path extensions on windows
408                 if (util.OS.isWindows) {
409                     let extensions = services.environment.get("PATHEXT").split(";");
410                     for (let [, extension] in Iterator(extensions)) {
411                         file = dir.child(bin + extension);
412                         if (file.exists())
413                             return file;
414                     }
415                 }
416             }
417             catch (e) {}
418         return null;
419     },
420
421     /**
422      * Runs an external program.
423      *
424      * @param {File|string} program The program to run.
425      * @param {string[]} args An array of arguments to pass to *program*.
426      */
427     run: function (program, args, blocking) {
428         args = args || [];
429
430         let file = this.pathSearch(program);
431
432         if (!file || !file.exists()) {
433             util.dactyl.echoerr(_("io.noCommand", program));
434             if (callable(blocking))
435                 util.trapErrors(blocking);
436             return -1;
437         }
438
439         let process = services.Process(file);
440         process.run(false, args.map(String), args.length);
441         try {
442             if (callable(blocking))
443                 var timer = services.Timer(
444                     function () {
445                         if (!process.isRunning) {
446                             timer.cancel();
447                             util.trapErrors(blocking);
448                         }
449                     },
450                     100, services.Timer.TYPE_REPEATING_SLACK);
451             else if (blocking)
452                 while (process.isRunning)
453                     util.threadYield(false, true);
454         }
455         catch (e) {
456             process.kill();
457             throw e;
458         }
459
460         return process.exitValue;
461     },
462
463     // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
464     // fixed use that instead of a tmpfile
465     /**
466      * Runs *command* in a subshell and returns the output in a string. The
467      * shell used is that specified by the 'shell' option.
468      *
469      * @param {string} command The command to run.
470      * @param {string} input Any input to be provided to the command on stdin.
471      * @returns {object}
472      */
473     system: function (command, input) {
474         util.dactyl.echomsg(_("io.callingShell", command), 4);
475
476         function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
477
478         return this.withTempFiles(function (stdin, stdout, cmd) {
479             if (input instanceof File)
480                 stdin = input;
481             else if (input)
482                 stdin.write(input);
483
484             let shell = io.pathSearch(storage["options"].get("shell").value);
485             let shcf = storage["options"].get("shellcmdflag").value;
486             util.assert(shell, _("error.invalid", "'shell'"));
487
488             if (isArray(command))
489                 command = command.map(escape).join(" ");
490
491             // TODO: implement 'shellredir'
492             if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
493                 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
494                 var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
495             }
496             else {
497                 cmd.write("cd " + escape(this.cwd.path) + "\n" +
498                         ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
499                          escape(shell.path), shcf, escape(command)].join(" "));
500                 res = this.run("/bin/sh", ["-e", cmd.path], true);
501             }
502
503             return {
504                 __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
505                 valueOf: function () this.output,
506                 output: stdout.read().replace(/^(.*)\n$/, "$1"),
507                 returnValue: res,
508                 toString: function () this.output
509             };
510         }) || "";
511     },
512
513     /**
514      * Creates a temporary file context for executing external commands.
515      * *func* is called with a temp file, created with {@link #createTempFile},
516      * for each explicit argument. Ensures that all files are removed when
517      * *func* returns.
518      *
519      * @param {function} func The function to execute.
520      * @param {Object} self The 'this' object used when executing func.
521      * @returns {boolean} false if temp files couldn't be created,
522      *     otherwise, the return value of *func*.
523      */
524     withTempFiles: function (func, self, checked) {
525         let args = array(util.range(0, func.length)).map(this.closure.createTempFile).array;
526         try {
527             if (!args.every(util.identity))
528                 return false;
529             var res = func.apply(self || this, args);
530         }
531         finally {
532             if (!checked || res !== true)
533                 args.forEach(function (f) f && f.remove(false));
534         }
535         return res;
536     }
537 }, {
538     /**
539      * @property {string} The value of the $PENTADACTYL_RUNTIME environment
540      *     variable.
541      */
542     get runtimePath() {
543         const rtpvar = config.idName + "_RUNTIME";
544         let rtp = services.environment.get(rtpvar);
545         if (!rtp) {
546             rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name;
547             services.environment.set(rtpvar, rtp);
548         }
549         return rtp;
550     },
551
552     /**
553      * @property {string} The current platform's path separator.
554      */
555     PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
556 }, {
557     commands: function (dactyl, modules, window) {
558         const { commands, completion, io } = modules;
559
560         commands.add(["cd", "chd[ir]"],
561             "Change the current directory",
562             function (args) {
563                 let arg = args[0];
564
565                 if (!arg)
566                     arg = "~";
567
568                 arg = File.expandPath(arg);
569
570                 // go directly to an absolute path or look for a relative path
571                 // match in 'cdpath'
572                 // TODO: handle ../ and ./ paths
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(true)) 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("Could not write to " + 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, "File 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 match   <name>Comment +".*$+ contains=<name>Todo,@Spell
703 syn keyword <name>Todo FIXME NOTE TODO XXX contained
704
705 syn region <name>String start="\z(["']\)" end="\z1" skip="\\\\\|\\\z1" oneline
706
707 syn match <name>LineComment +^\s*".*$+ contains=<name>Todo,@Spell
708
709 " NOTE: match vim.vim highlighting group names
710 hi def link <name>AutoCmd               <name>Command
711 hi def link <name>AutoEvent             Type
712 hi def link <name>Command               Statement
713 hi def link <name>Comment               Comment
714 hi def link <name>JavaScriptDelimiter   Delimiter
715 hi def link <name>CssDelimiter          Delimiter
716 hi def link <name>Notation              Special
717 hi def link <name>LineComment           Comment
718 hi def link <name>Option                PreProc
719 hi def link <name>SetMod                <name>Option
720 hi def link <name>String                String
721 hi def link <name>Todo                  Todo
722
723 let b:current_syntax = "<name>"
724
725 let &cpo = s:cpo_save
726 unlet s:cpo_save
727
728 " vim: tw=130 et ts=4 sw=4:
729 ]]>, true);
730
731                 const WIDTH = 80;
732                 function wrap(prefix, items, sep) {
733                     sep = sep || " ";
734                     let width = 0;
735                     let lines = [];
736                     lines.__defineGetter__("last", function () this[this.length - 1]);
737
738                     for (let item in values(items.array || items)) {
739                         if (item.length > width && (!lines.length || lines.last.length > 1)) {
740                             lines.push([prefix]);
741                             width = WIDTH - prefix.length;
742                             prefix = "    \\ ";
743                         }
744                         width -= item.length + sep.length;
745                         lines.last.push(item, sep);
746                     }
747                     lines.last.pop();
748                     return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
749                 }
750
751                 const { commands, options } = modules;
752                 file.write(template({
753                     name: config.name,
754                     autocommands: wrap("syn keyword " + config.name + "AutoEvent ",
755                                        keys(config.autocommands)),
756                     commands: wrap("syn keyword " + config.name + "Command ",
757                                   array(c.specs for (c in commands.iterator())).flatten()),
758                     options: wrap("syn keyword " + config.name + "Option ",
759                                   array(o.names for (o in options) if (o.type != "boolean")).flatten()),
760                     toggleoptions: wrap("let s:toggleOptions = [",
761                                         array(o.realNames for (o in options) if (o.type == "boolean"))
762                                             .flatten().map(String.quote),
763                                         ", ") + "]"
764                 }));
765             }, {
766                 argCount: "?",
767                 bang: true,
768                 completer: function (context) completion.file(context, true),
769                 literal: 1
770             });
771
772         commands.add(["runt[ime]"],
773             "Source the specified file from each directory in 'runtimepath'",
774             function (args) { io.sourceFromRuntimePath(args, args.bang); },
775             {
776                 argCount: "+",
777                 bang: true,
778                 completer: function (context) completion.runtime(context)
779             }
780         );
781
782         commands.add(["scrip[tnames]"],
783             "List all sourced script names",
784             function () {
785                 modules.commandline.commandOutput(
786                     template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
787                         ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))));  // TODO: add colon and remove column titles for pedantic Vim compatibility?
788             },
789             { argCount: "0" });
790
791         commands.add(["so[urce]"],
792             "Read Ex commands from a file",
793             function (args) {
794                 if (args.length > 1)
795                     dactyl.echoerr(_("io.oneFileAllowed"));
796                 else
797                     io.source(args[0], { silent: args.bang });
798             }, {
799                 argCount: "+", // FIXME: should be "1" but kludged for proper error message
800                 bang: true,
801                 completer: function (context) completion.file(context, true)
802             });
803
804         commands.add(["!", "run"],
805             "Run a command",
806             function (args) {
807                 let arg = args[0] || "";
808
809                 // :!! needs to be treated specially as the command parser sets the
810                 // bang flag but removes the ! from arg
811                 if (args.bang)
812                     arg = "!" + arg;
813
814                 // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
815                 // pass through a raw bang when escaped or substitute the last command
816
817                 // This is an asinine and irritating feature when we have searchable
818                 // command-line history. --Kris
819                 if (modules.options["banghist"]) {
820                     // replaceable bang and no previous command?
821                     dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
822                         "E34: No previous command");
823
824                     arg = arg.replace(/(\\)*!/g,
825                         function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", io._lastRunCommand)
826                     );
827                 }
828
829                 io._lastRunCommand = arg;
830
831                 let result = io.system(arg);
832                 if (result.returnValue != 0)
833                     result.output += "\nshell returned " + result.returnValue;
834
835                 modules.commandline.command = "!" + arg;
836                 modules.commandline.commandOutput(<span highlight="CmdOutput">{result.output}</span>);
837
838                 modules.autocommands.trigger("ShellCmdPost", {});
839             }, {
840                 argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk
841                 bang: true,
842                 // This is abominably slow.
843                 // completer: function (context) completion.shellCommand(context),
844                 literal: 0
845             });
846     },
847     completion: function (dactyl, modules, window) {
848         const { completion, io } = modules;
849
850         completion.charset = function (context) {
851             context.anchored = false;
852             context.keys = {
853                 text: util.identity,
854                 description: function (charset) {
855                     try {
856                         return services.charset.getCharsetTitle(charset);
857                     }
858                     catch (e) {
859                         return charset;
860                     }
861                 }
862             };
863             context.generate = function () iter(services.charset.getDecoderList());
864         };
865
866         completion.directory = function directory(context, full) {
867             this.file(context, full);
868             context.filters.push(function (item) item.isdir);
869         };
870
871         completion.environment = function environment(context) {
872             context.title = ["Environment Variable", "Value"];
873             context.generate = function ()
874                 io.system(util.OS.isWindows ? "set" : "env")
875                   .output.split("\n")
876                   .filter(function (line) line.indexOf("=") > 0)
877                   .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
878         };
879
880         completion.file = function file(context, full, dir) {
881             // dir == "" is expanded inside readDirectory to the current dir
882             function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
883             dir = getDir(dir || context.filter);
884
885             let file = util.getFile(dir);
886             if (file && (!file.exists() || !file.isDirectory()))
887                 file = file.parent;
888
889             if (!full)
890                 context.advance(dir.length);
891
892             context.title = [full ? "Path" : "Filename", "Type"];
893             context.keys = {
894                 text: !full ? "leafName" : function (f) this.path,
895                 path: function (f) dir + f.leafName,
896                 description: function (f) this.isdir ? "Directory" : "File",
897                 isdir: function (f) f.isDirectory(),
898                 icon: function (f) this.isdir ? "resource://gre/res/html/folder.png"
899                                               : "moz-icon://" + f.leafName
900             };
901             context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text);
902
903             if (modules.options["wildignore"]) {
904                 let wig = modules.options.get("wildignore");
905                 context.filters.push(function (item) item.isdir || !wig.getKey(this.name));
906             }
907
908             // context.background = true;
909             context.key = dir;
910             let uri = io.isJarURL(dir);
911             if (uri)
912                 context.generate = function generate_jar() {
913                     return [
914                         {
915                               isDirectory: function () s.substr(-1) == "/",
916                               leafName: /([^\/]*)\/?$/.exec(s)[1]
917                         }
918                         for (s in io.listJar(uri.JARFile, getDir(uri.JAREntry)))]
919                 };
920             else
921                 context.generate = function generate_file() {
922                     try {
923                         return io.File(file || dir).readDirectory();
924                     }
925                     catch (e) {}
926                     return [];
927                 };
928         };
929
930         completion.runtime = function (context) {
931             for (let [, dir] in Iterator(modules.options["runtimepath"]))
932                 context.fork(dir, 0, this, function (context) {
933                     dir = dir.replace("/+$", "") + "/";
934                     completion.file(context, true, dir + context.filter);
935                     context.title[0] = dir;
936                     context.keys.text = function (f) this.path.substr(dir.length);
937                 });
938         };
939
940         completion.shellCommand = function shellCommand(context) {
941             context.title = ["Shell Command", "Path"];
942             context.generate = function () {
943                 let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
944                 let commands = [];
945
946                 for (let [, dirName] in Iterator(dirNames)) {
947                     let dir = io.File(dirName);
948                     if (dir.exists() && dir.isDirectory())
949                         commands.push([[file.leafName, dir.path] for (file in iter(dir.directoryEntries))
950                                        if (file.isFile() && file.isExecutable())]);
951                 }
952
953                 return array.flatten(commands);
954             };
955         };
956
957         completion.addUrlCompleter("f", "Local files", function (context, full) {
958             let match = util.regexp(<![CDATA[
959                 ^
960                 (?P<prefix>
961                     (?P<proto>
962                         (?P<scheme> chrome|resource)
963                         :\/\/
964                     )
965                     [^\/]*
966                 )
967                 (?P<path> \/[^\/]* )?
968                 $
969             ]]>, "x").exec(context.filter);
970             if (match) {
971                 if (!match.path) {
972                     context.key = match.proto;
973                     context.advance(match.proto.length);
974                     context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]);
975                 }
976                 else if (match.scheme === "chrome") {
977                     context.key = match.prefix;
978                     context.advance(match.prefix.length + 1);
979                     context.generate = function () iter({
980                         content: "Chrome content",
981                         locale: "Locale-specific content",
982                         skin: "Theme-specific content"
983                     });
984                 }
985             }
986             if (!match || match.scheme === "resource" && match.path)
987                 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter) || util.getFile(context.filter) || io.isJarURL(context.filter))
988                     completion.file(context, full);
989         });
990     },
991     javascript: function (dactyl, modules, window) {
992         modules.JavaScript.setCompleter([File, File.expandPath],
993             [function (context, obj, args) {
994                 context.quote[2] = "";
995                 modules.completion.file(context, true);
996             }]);
997
998     },
999     modes: function initModes(dactyl, modules, window) {
1000         initModes.require("commandline");
1001         const { modes } = modules;
1002
1003         modes.addMode("FILE_INPUT", {
1004             extended: true,
1005             description: "Active when selecting a file",
1006             bases: [modes.COMMAND_LINE],
1007             input: true
1008         });
1009     },
1010     options: function (dactyl, modules, window) {
1011         const { completion, options } = modules;
1012
1013         var shell, shellcmdflag;
1014         if (util.OS.isWindows) {
1015             shell = "cmd.exe";
1016             shellcmdflag = "/c";
1017         }
1018         else {
1019             shell = services.environment.get("SHELL") || "sh";
1020             shellcmdflag = "-c";
1021         }
1022
1023         options.add(["banghist", "bh"],
1024             "Replace occurrences of ! with the previous command when executing external commands",
1025             "boolean", true);
1026
1027         options.add(["fileencoding", "fenc"],
1028             "The character encoding used when reading and writing files",
1029             "string", "UTF-8", {
1030                 completer: function (context) completion.charset(context),
1031                 getter: function () File.defaultEncoding,
1032                 setter: function (value) (File.defaultEncoding = value)
1033             });
1034         options.add(["cdpath", "cd"],
1035             "List of directories searched when executing :cd",
1036             "stringlist", ["."].concat(services.environment.get("CDPATH").split(/[:;]/).filter(util.identity)).join(","),
1037             {
1038                 get files() this.value.map(function (path) File(path, modules.io.cwd))
1039                                 .filter(function (dir) dir.exists()),
1040                 setter: function (value) File.expandPathList(value)
1041             });
1042
1043         options.add(["runtimepath", "rtp"],
1044             "List of directories searched for runtime files",
1045             "stringlist", IO.runtimePath,
1046             {
1047                 get files() this.value.map(function (path) File(path, modules.io.cwd))
1048                                 .filter(function (dir) dir.exists())
1049             });
1050
1051         options.add(["shell", "sh"],
1052             "Shell to use for executing external commands with :! and :run",
1053             "string", shell,
1054             { validator: function (val) io.pathSearch(val) });
1055
1056         options.add(["shellcmdflag", "shcf"],
1057             "Flag passed to shell when executing external commands with :! and :run",
1058             "string", shellcmdflag,
1059             {
1060                 getter: function (value) {
1061                     if (this.hasChanged || !util.OS.isWindows)
1062                         return value;
1063                     return /sh/.test(options["shell"]) ? "-c" : "/c";
1064                 }
1065             });
1066         options["shell"]; // Make sure it's loaded into global storage.
1067         options["shellcmdflag"];
1068
1069         options.add(["wildignore", "wig"],
1070             "List of file patterns to ignore when completing file names",
1071             "regexplist", "");
1072     }
1073 });
1074
1075 endModule();
1076
1077 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1078
1079 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: