1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2012 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
4 // Some code based on Venkman
6 // This work is licensed for reuse under an MIT license. Details are
7 // given in the LICENSE.txt file included with this file.
13 exports: ["IO", "io"],
17 lazyRequire("config", ["config"]);
18 lazyRequire("contexts", ["Contexts", "contexts"]);
19 lazyRequire("storage", ["File", "storage"]);
20 lazyRequire("styles", ["styles"]);
21 lazyRequire("template", ["template"]);
23 // TODO: why are we passing around strings rather than file objects?
25 * Provides a basic interface to common system I/O operations.
28 var IO = Module("io", {
29 init: function init() {
30 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
31 this._cwd = this._processDir.path;
33 lazyRequire("config", ["config"], this);
36 Local: function Local(dactyl, modules, window) let ({ io, plugins } = modules) ({
38 init: function init() {
39 this.config = modules.config;
40 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
41 this._cwd = this._processDir.path;
44 this._lastRunCommand = ""; // updated whenever the users runs a command with :!
45 this._scriptNames = [];
48 CommandFileMode: Class("CommandFileMode", modules.CommandMode, {
49 init: function init(prompt, params) {
51 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
57 get mode() modules.modes.FILE_INPUT,
59 complete: function (context) {
61 this.completer(context);
63 context = context.fork("files", 0);
64 modules.completion.file(context);
65 context.filters = context.filters.concat(this.filters || []);
70 * Returns all directories named *name* in 'runtimepath'.
72 * @param {string} name
73 * @returns {nsIFile[])
75 getRuntimeDirectories: function getRuntimeDirectories(name) {
76 return modules.options.get("runtimepath").files
77 .map(function (dir) dir.child(name))
78 .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
81 // FIXME: multiple paths?
83 * Sources files found in 'runtimepath'. For each relative path in *paths*
84 * each directory in 'runtimepath' is searched and if a matching file is
85 * found it is sourced. Only the first file found (per specified path) is
86 * sourced unless *all* is specified, then all found files are sourced.
88 * @param {[string]} paths An array of relative paths to source.
89 * @param {boolean} all Whether all found files should be sourced.
91 sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
92 let dirs = modules.options.get("runtimepath").files;
95 dactyl.echomsg(_("io.searchingFor", paths.join(" ").quote(), modules.options.get("runtimepath").stringValue), 2);
98 for (let dir in values(dirs)) {
99 for (let [, path] in Iterator(paths)) {
100 let file = dir.child(path);
102 dactyl.echomsg(_("io.searchingFor", file.path.quote()), 3);
104 if (file.exists() && file.isFile() && file.isReadable()) {
105 found = io.source(file.path, false) || true;
114 dactyl.echomsg(_("io.notInRTP", paths.join(" ").quote()), 1);
120 * Reads Ex commands, JavaScript or CSS from *filename*.
122 * @param {string} filename The name of the file to source.
123 * @param {object} params Extra parameters:
124 * group: The group in which to execute commands.
125 * silent: Whether errors should not be reported.
127 source: function source(filename, params) {
128 const { contexts } = modules;
129 defineModule.loadLog.push("sourcing " + filename);
131 if (!isObject(params))
132 params = { silent: params };
134 let time = Date.now();
135 return contexts.withContext(null, function () {
137 var file = util.getFile(filename) || io.File(filename);
139 if (!file.exists() || !file.isReadable() || file.isDirectory()) {
141 dactyl.echoerr(_("io.notReadable", filename.quote()));
145 dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
149 let sourceJSM = function sourceJSM() {
150 context = contexts.Module(uri);
151 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
154 if (/\.jsm$/.test(filename))
156 else if (/\.js$/.test(filename)) {
158 var context = contexts.Script(file, params.group);
159 if (Set.has(this._scriptNames, file.path))
162 dactyl.loadScript(uri.spec, context);
163 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
166 if (e == Contexts) { // Hack;
171 if (e instanceof Finished)
173 if (e.fileName && !(e instanceof FailedAssertion))
175 e.fileName = util.fixURI(e.fileName);
176 if (e.fileName == uri.spec)
177 e.fileName = filename;
178 e.echoerr = [e.fileName, ":", e.lineNumber, ": ", e].join("");
185 else if (/\.css$/.test(filename))
186 styles.registerSheet(uri.spec, false, true);
188 context = contexts.Context(file, params.group);
189 modules.commands.execute(file.read(), null, params.silent,
193 group: context.GROUP,
196 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
199 Set.add(this._scriptNames, file.path);
201 dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
202 dactyl.log(_("dactyl.sourced", filename), 3);
208 let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
210 dactyl.echoerr(message);
213 defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
219 // TODO: there seems to be no way, short of a new component, to change
220 // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
222 * Returns the current working directory.
224 * It's not possible to change the real CWD of the process so this
225 * state is maintained internally. External commands run via
226 * {@link #system} are executed in this directory.
231 let dir = File(this._cwd);
233 // NOTE: the directory could have been deleted underneath us so
234 // fallback to the process's CWD
235 if (dir.exists() && dir.isDirectory())
238 return this._processDir.clone();
242 * Sets the current working directory.
244 * @param {string} newDir The new CWD. This may be a relative or
245 * absolute path and is expanded by {@link #expandPath}.
248 newDir = newDir && newDir.path || newDir || "~";
251 util.assert(this._oldcwd != null, _("io.noPrevDir"));
252 [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
255 let dir = io.File(newDir);
256 util.assert(dir.exists() && dir.isDirectory(), _("io.noSuchDir", dir.path.quote()));
258 [this._cwd, this._oldcwd] = [dir.path, this.cwd];
264 * @property {function} File class.
267 File: Class.Memoize(function () let (io = this)
268 Class("File", File, {
269 init: function init(path, checkCWD)
270 init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
274 * @property {Object} The current file sourcing context. As a file is
275 * being sourced the 'file' and 'line' properties of this context
276 * object are updated appropriately.
280 expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),
283 * Returns the first user RC file found in *dir*.
285 * @param {File|string} dir The directory to search.
286 * @param {boolean} always When true, return a path whether
287 * the file exists or not.
289 * @returns {nsIFile} The RC file or null if none is found.
291 getRCFile: function getRCFile(dir, always) {
292 dir = this.File(dir || "~");
294 let rcFile1 = dir.child("." + config.name + "rc");
295 let rcFile2 = dir.child("_" + config.name + "rc");
297 if (config.OS.isWindows)
298 [rcFile1, rcFile2] = [rcFile2, rcFile1];
300 if (rcFile1.exists() && rcFile1.isFile())
302 else if (rcFile2.exists() && rcFile2.isFile())
310 * Creates a temporary file.
314 createTempFile: function createTempFile(name, type) {
315 if (name instanceof Ci.nsIFile) {
316 var file = name.clone();
317 if (!type || type == "file")
318 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
320 file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, octal(777));
323 file = services.directory.get("TmpD", Ci.nsIFile);
324 file.append(this.config.tempFile + (name ? "." + name : ""));
325 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
328 services.externalApp.deleteTemporaryFileOnExit(file);
334 * Determines whether the given URL string resolves to a JAR URL and
335 * returns the matching nsIJARURI object if it does.
337 * @param {string} url The URL to check.
338 * @returns {nsIJARURI|null}
340 isJarURL: function isJarURL(url) {
342 let uri = util.newURI(url);
343 if (uri instanceof Ci.nsIJARURI)
346 let channel = services.io.newChannelFromURI(uri);
347 try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
348 if (channel instanceof Ci.nsIJARChannel)
349 return channel.URI.QueryInterface(Ci.nsIJARURI);
356 * Returns a list of the contents of the given JAR file which are
357 * children of the given path.
359 * @param {nsIURI|string} file The URI of the JAR file to list.
360 * @param {string} path The prefix path to search.
362 listJar: function listJar(file, path) {
363 file = util.getFile(file);
364 if (file && file.exists() && file.isFile() && file.isReadable()) {
365 // let jar = services.zipReader.getZip(file); Crashes.
366 let jar = services.ZipReader(file.file);
368 let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
371 for (let entry in iter(jar.findEntries("*")))
372 if (filter.test(entry))
382 readHeredoc: function readHeredoc(end) {
387 * Searches for the given executable file in the system executable
388 * file paths as specified by the PATH environment variable.
390 * On Windows, if the unadorned filename cannot be found, the
391 * extensions in the semicolon-separated list in the PATHSEP
392 * environment variable are successively appended to the original
393 * name and searched for in turn.
395 * @param {string} bin The name of the executable to find.
396 * @returns {File|null}
398 pathSearch: function pathSearch(bin) {
399 if (bin instanceof File || File.isAbsolutePath(bin))
400 return this.File(bin);
402 let dirs = services.environment.get("PATH")
403 .split(config.OS.pathListSep);
404 // Windows tries the CWD first TODO: desirable?
405 if (config.OS.isWindows)
406 dirs = [io.cwd].concat(dirs);
408 for (let [, dir] in Iterator(dirs))
410 dir = this.File(dir, true);
412 let file = dir.child(bin);
413 if (file.exists() && file.isFile() && file.isExecutable())
416 // TODO: couldn't we just palm this off to the start command?
417 // automatically try to add the executable path extensions on windows
418 if (config.OS.isWindows) {
419 let extensions = services.environment.get("PATHEXT").split(";");
420 for (let [, extension] in Iterator(extensions)) {
421 file = dir.child(bin + extension);
432 * Runs an external program.
434 * @param {File|string} program The program to run.
435 * @param {[string]} args An array of arguments to pass to *program*.
437 run: function run(program, args, blocking, self) {
440 let file = this.pathSearch(program);
442 if (!file || !file.exists()) {
443 util.dactyl.echoerr(_("io.noCommand", program));
444 if (callable(blocking))
445 util.trapErrors(blocking);
449 let process = services.Process(file.file);
450 process.run(false, args.map(String), args.length);
452 if (callable(blocking))
453 var timer = services.Timer(
455 if (!process.isRunning) {
457 util.trapErrors(blocking, self, process.exitValue);
460 100, services.Timer.TYPE_REPEATING_SLACK);
462 while (process.isRunning)
463 util.threadYield(false, true);
470 return process.exitValue;
473 // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
474 // fixed use that instead of a tmpfile
476 * Runs *command* in a subshell and returns the output. The shell used is
477 * that specified by the 'shell' option.
479 * @param {string|[string]} command The command to run. This can be a shell
480 * command string or an array of strings (a command and arguments)
481 * which will be escaped and concatenated.
482 * @param {string} input Any input to be provided to the command on stdin.
483 * @param {function(object)} callback A callback to be called when
484 * the command completes. @optional
485 * @returns {object|null}
487 system: function system(command, input, callback) {
488 util.dactyl.echomsg(_("io.callingShell", command), 4);
490 let { shellEscape } = util.closure;
492 return this.withTempFiles(function (stdin, stdout, cmd) {
493 if (input instanceof File)
498 function result(status, output) ({
499 __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
500 valueOf: function () this.output,
501 output: output.replace(/^(.*)\n$/, "$1"),
503 toString: function () this.output
506 function async(status) {
507 let output = stdout.read();
508 [stdin, stdout, cmd].forEach(function (f) f.exists() && f.remove(false));
509 callback(result(status, output));
512 let shell = io.pathSearch(storage["options"].get("shell").value);
513 let shcf = storage["options"].get("shellcmdflag").value;
514 util.assert(shell, _("error.invalid", "'shell'"));
516 if (isArray(command))
517 command = command.map(shellEscape).join(" ");
519 // TODO: implement 'shellredir'
520 if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
521 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
522 var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
525 cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
526 ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
527 shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
528 res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
531 return callback ? true : result(res, stdout.read());
536 * Creates a temporary file context for executing external commands.
537 * *func* is called with a temp file, created with {@link #createTempFile},
538 * for each explicit argument. Ensures that all files are removed when
541 * @param {function} func The function to execute.
542 * @param {Object} self The 'this' object used when executing func.
543 * @returns {boolean} false if temp files couldn't be created,
544 * otherwise, the return value of *func*.
546 withTempFiles: function withTempFiles(func, self, checked, ext) {
547 let args = array(util.range(0, func.length))
548 .map(bind("createTempFile", this, ext)).array;
550 if (!args.every(util.identity))
552 var res = func.apply(self || this, args);
555 if (!checked || res !== true)
556 args.forEach(function (f) f.remove(false));
562 * @property {string} The value of the $PENTADACTYL_RUNTIME environment
566 const rtpvar = config.idName + "_RUNTIME";
567 let rtp = services.environment.get(rtpvar);
569 rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
570 services.environment.set(rtpvar, rtp);
576 * @property {string} The current platform's path separator.
578 PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
580 commands: function initCommands(dactyl, modules, window) {
581 const { commands, completion, io } = modules;
583 commands.add(["cd", "chd[ir]"],
584 "Change the current directory",
591 arg = File.expandPath(arg);
593 // go directly to an absolute path or look for a relative path
595 if (File.isAbsolutePath(arg)) {
597 dactyl.echomsg(io.cwd.path);
600 let dirs = modules.options.get("cdpath").files;
601 for (let dir in values(dirs)) {
602 dir = dir.child(arg);
604 if (dir.exists() && dir.isDirectory() && dir.isReadable()) {
606 dactyl.echomsg(io.cwd.path);
611 dactyl.echoerr(_("io.noSuchDir", arg.quote()));
612 dactyl.echoerr(_("io.commandFailed"));
616 completer: function (context) completion.directory(context, true),
620 commands.add(["pw[d]"],
621 "Print the current directory name",
622 function () { dactyl.echomsg(io.cwd.path); },
625 commands.add([config.name.replace(/(.)(.*)/, "mk$1[$2rc]")],
626 "Write current key mappings and changed options to the config file",
628 dactyl.assert(args.length <= 1, _("io.oneFileAllowed"));
630 let file = io.File(args[0] || io.getRCFile(null, true));
632 dactyl.assert(!file.exists() || args.bang, _("io.exists", file.path.quote()));
634 // TODO: Use a set/specifiable list here:
635 let lines = [cmd.serialize().map(commands.commandToString, cmd) for (cmd in commands.iterator()) if (cmd.serialize)];
636 lines = array.flatten(lines);
638 lines.unshift('"' + config.version + "\n");
639 lines.push("\n\" vim: set ft=" + config.name + ":");
642 file.write(lines.join("\n"));
643 dactyl.echomsg(_("io.writing", file.path.quote()), 2);
646 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
647 dactyl.log(_("error.notWriteable", file.path, e.message)); // XXX
650 argCount: "*", // FIXME: should be "?" but kludged for proper error message
652 completer: function (context) completion.file(context, true)
655 commands.add(["mkv[imruntime]"],
656 "Create and install Vim runtime files for " + config.appName,
658 dactyl.assert(args.length <= 1, _("io.oneFileAllowed"));
661 var rtDir = io.File(args[0]);
662 dactyl.assert(rtDir.exists(), _("io.noSuchDir", rtDir.path.quote()));
665 rtDir = io.File(config.OS.isWindows ? "~/vimfiles/" : "~/.vim/");
667 dactyl.assert(!rtDir.exists() || rtDir.isDirectory(), _("io.eNotDir", rtDir.path.quote()));
669 let rtItems = { ftdetect: {}, ftplugin: {}, syntax: {} };
671 // require bang if any of the paths exist
672 for (let [type, item] in iter(rtItems)) {
673 let file = io.File(rtDir).child(type, config.name + ".vim");
674 dactyl.assert(!file.exists() || args.bang, _("io.exists", file.path.quote()));
678 rtItems.ftdetect.template = // {{{
679 literal(/*" Vim filetype detection file
682 au BufNewFile,BufRead *<name>rc*,*.<fileext> set filetype=<name>
684 rtItems.ftplugin.template = // {{{
685 literal(/*" Vim filetype plugin file
688 if exists("b:did_ftplugin")
691 let b:did_ftplugin = 1
693 let s:cpo_save = &cpo
696 let b:undo_ftplugin = "setl com< cms< fo< ofu< | unlet! b:browsefilter"
698 setlocal comments=:\"
699 setlocal commentstring=\"%s
700 setlocal formatoptions-=t formatoptions+=croql
701 setlocal omnifunc=syntaxcomplete#Complete
703 if has("gui_win32") && !exists("b:browsefilter")
704 let b:browsefilter = "<appname> Config Files (*.<fileext>)\t*.<fileext>\n" .
705 \ "All Files (*.*)\t*.*\n"
708 let &cpo = s:cpo_save
711 rtItems.syntax.template = // {{{
712 literal(/*" Vim syntax file
715 if exists("b:current_syntax")
719 let s:cpo_save = &cpo
722 syn include @javascriptTop syntax/javascript.vim
723 unlet b:current_syntax
725 syn include @cssTop syntax/css.vim
726 unlet b:current_syntax
728 syn match <name>CommandStart "\%(^\s*:\=\)\@<=" nextgroup=<name>Command,<name>AutoCmd
733 syn match <name>Command "!" contained
735 syn keyword <name>AutoCmd au[tocmd] contained nextgroup=<name>AutoEventList skipwhite
740 syn match <name>AutoEventList "\(\a\+,\)*\a\+" contained contains=<name>AutoEvent
742 syn region <name>Set matchgroup=<name>Command start="\%(^\s*:\=\)\@<=\<\%(setl\%[ocal]\|setg\%[lobal]\|set\=\)\=\>"
743 \ end="$" keepend oneline contains=<name>Option,<name>String
746 \ contained nextgroup=pentadactylSetMod
749 execute 'syn match <name>Option "\<\%(no\|inv\)\=\%(' .
750 \ join(s:toggleOptions, '\|') .
751 \ '\)\>!\=" contained nextgroup=<name>SetMod'
753 syn match <name>SetMod "\%(\<[a-z_]\+\)\@<=&" contained
755 syn region <name>JavaScript start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=" end="$" contains=@javascriptTop keepend oneline
756 syn region <name>JavaScript matchgroup=<name>JavaScriptDelimiter
757 \ start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@javascriptTop fold
759 let s:cssRegionStart = '\%(^\s*sty\%[le]!\=\s\+\%(-\%(n\|name\)\%(\s\+\|=\)\S\+\s\+\)\=[^-]\S\+\s\+\)\@<='
760 execute 'syn region <name>Css start="' . s:cssRegionStart . '" end="$" contains=@cssTop keepend oneline'
761 execute 'syn region <name>Css matchgroup=<name>CssDelimiter'
762 \ 'start="' . s:cssRegionStart . '<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@cssTop fold'
764 syn match <name>Notation "<[0-9A-Za-z-]\+>"
766 syn keyword <name>Todo FIXME NOTE TODO XXX contained
768 syn region <name>String start="\z(["']\)" end="\z1" skip="\\\\\|\\\z1" oneline
770 syn match <name>Comment +^\s*".*$+ contains=<name>Todo,@Spell
772 " NOTE: match vim.vim highlighting group names
773 hi def link <name>AutoCmd <name>Command
774 hi def link <name>AutoEvent Type
775 hi def link <name>Command Statement
776 hi def link <name>JavaScriptDelimiter Delimiter
777 hi def link <name>CssDelimiter Delimiter
778 hi def link <name>Notation Special
779 hi def link <name>Comment Comment
780 hi def link <name>Option PreProc
781 hi def link <name>SetMod <name>Option
782 hi def link <name>String String
783 hi def link <name>Todo Todo
785 let b:current_syntax = "<name>"
787 let &cpo = s:cpo_save
790 " vim: tw=130 et ts=8 sts=4 sw=4:
793 const { options } = modules;
796 function wrap(prefix, items, sep) {//{{{
800 lines.__defineGetter__("last", function () this[this.length - 1]);
802 for (let item in values(items.array || items)) {
803 if (item.length > width && (!lines.length || lines.last.length > 1)) {
804 lines.push([prefix]);
805 width = WIDTH - prefix.length;
808 width -= item.length + sep.length;
809 lines.last.push(item, sep);
812 return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
815 let params = { // {{{
816 header: ['" Language: ' + config.appName + ' configuration file',
817 '" Maintainer: Doug Kearns <dougkearns@gmail.com>',
818 '" Version: ' + config.version].join("\n"),
820 appname: config.appName,
821 fileext: config.fileExtension,
822 maintainer: "Doug Kearns <dougkearns@gmail.com>",
823 autocommands: wrap("syn keyword " + config.name + "AutoEvent ",
824 keys(config.autocommands)),
825 commands: wrap("syn keyword " + config.name + "Command ",
826 array(c.specs for (c in commands.iterator())).flatten()),
827 options: wrap("syn keyword " + config.name + "Option ",
828 array(o.names for (o in options) if (o.type != "boolean")).flatten()),
829 toggleoptions: wrap("let s:toggleOptions = [",
830 array(o.realNames for (o in options) if (o.type == "boolean"))
831 .flatten().map(String.quote),
835 for (let { file, template } in values(rtItems)) {
837 file.write(util.compileMacro(template, true)(params));
838 dactyl.echomsg(_("io.writing", file.path.quote()), 2);
841 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
842 dactyl.log(_("error.notWriteable", file.path, e.message));
848 completer: function (context) completion.directory(context, true),
852 commands.add(["runt[ime]"],
853 "Source the specified file from each directory in 'runtimepath'",
854 function (args) { io.sourceFromRuntimePath(args, args.bang); },
858 completer: function (context) completion.runtime(context)
862 commands.add(["scrip[tnames]"],
863 "List all sourced script names",
865 let names = Object.keys(io._scriptNames);
867 dactyl.echomsg(_("command.scriptnames.none"));
869 modules.commandline.commandOutput(
870 template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
871 ([i + 1, file] for ([i, file] in Iterator(names)))));
876 commands.add(["so[urce]"],
877 "Read Ex commands, JavaScript or CSS from a file",
880 dactyl.echoerr(_("io.oneFileAllowed"));
882 io.source(args[0], { silent: args.bang });
884 argCount: "+", // FIXME: should be "1" but kludged for proper error message
886 completer: function (context) completion.file(context, true)
889 commands.add(["!", "run"],
892 let arg = args[0] || "";
894 // :!! needs to be treated specially as the command parser sets the
895 // bang flag but removes the ! from arg
899 // This is an asinine and irritating "feature" when we have searchable
900 // command-line history. --Kris
901 if (modules.options["banghist"]) {
902 // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
903 // pass through a raw bang when escaped or substitute the last command
905 // replaceable bang and no previous command?
906 dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
907 _("command.run.noPrevious"));
909 arg = arg.replace(/(\\)*!/g,
910 function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", io._lastRunCommand)
914 io._lastRunCommand = arg;
916 let result = io.system(arg);
917 if (result.returnValue != 0)
918 result.output += "\n" + _("io.shellReturn", result.returnValue);
920 modules.commandline.command = args.commandName.replace("run", "$& ") + arg;
921 modules.commandline.commandOutput(["span", { highlight: "CmdOutput" }, result.output]);
923 modules.autocommands.trigger("ShellCmdPost", {});
927 // This is abominably slow.
928 // completer: function (context) completion.shellCommand(context),
932 completion: function initCompletion(dactyl, modules, window) {
933 const { completion, io } = modules;
935 completion.charset = function (context) {
936 context.anchored = false;
939 description: function (charset) {
941 return services.charset.getCharsetTitle(charset);
948 context.generate = function () iter(services.charset.getDecoderList());
951 completion.directory = function directory(context, full) {
952 this.file(context, full);
953 context.filters.push(function (item) item.isdir);
956 completion.environment = function environment(context) {
957 context.title = ["Environment Variable", "Value"];
958 context.generate = function ()
959 io.system(config.OS.isWindows ? "set" : "env")
961 .filter(function (line) line.indexOf("=") > 0)
962 .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
965 completion.file = function file(context, full, dir) {
966 if (/^jar:[^!]*$/.test(context.filter))
969 // dir == "" is expanded inside readDirectory to the current dir
970 function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
971 dir = getDir(dir || context.filter);
973 let file = util.getFile(dir);
974 if (file && (!file.exists() || !file.isDirectory()))
978 context.advance(dir.length);
980 context.title = [full ? "Path" : "Filename", "Type"];
982 text: !full ? "leafName" : function (f) this.path,
983 path: function (f) dir + f.leafName,
984 description: function (f) this.isdir ? "Directory" : "File",
985 isdir: function (f) f.isDirectory(),
986 icon: function (f) this.isdir ? "resource://gre/res/html/folder.png"
987 : "moz-icon://" + f.leafName
989 context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text);
991 if (modules.options["wildignore"])
992 context.filters.push(function (item) !modules.options.get("wildignore").getKey(item.path));
994 // context.background = true;
996 let uri = io.isJarURL(dir);
998 context.generate = function generate_jar() {
1001 isDirectory: function () s.substr(-1) == "/",
1002 leafName: /([^\/]*)\/?$/.exec(s)[1]
1004 for (s in io.listJar(uri.JARFile, getDir(uri.JAREntry)))]
1007 context.generate = function generate_file() {
1009 return io.File(file || dir).readDirectory();
1016 completion.runtime = function (context) {
1017 for (let [, dir] in Iterator(modules.options["runtimepath"]))
1018 context.fork(dir, 0, this, function (context) {
1019 dir = dir.replace("/+$", "") + "/";
1020 completion.file(context, true, dir + context.filter);
1021 context.title[0] = dir;
1022 context.keys.text = function (f) this.path.substr(dir.length);
1026 completion.shellCommand = function shellCommand(context) {
1027 context.title = ["Shell Command", "Path"];
1028 context.generate = function () {
1029 let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
1032 for (let [, dirName] in Iterator(dirNames)) {
1033 let dir = io.File(dirName);
1034 if (dir.exists() && dir.isDirectory())
1035 commands.push([[file.leafName, dir.path] for (file in iter(dir.directoryEntries))
1036 if (file.isFile() && file.isExecutable())]);
1039 return array.flatten(commands);
1043 completion.addUrlCompleter("file", "Local files", function (context, full) {
1044 let match = util.regexp(literal(/*
1048 (?P<scheme> chrome|resource)
1053 (?P<path> \/[^\/]* )?
1055 */), "x").exec(context.filter);
1058 context.key = match.proto;
1059 context.advance(match.proto.length);
1060 context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
1062 else if (match.scheme === "chrome") {
1063 context.key = match.prefix;
1064 context.advance(match.prefix.length + 1);
1065 context.generate = function () iter({
1066 content: /*L*/"Chrome content",
1067 locale: /*L*/"Locale-specific content",
1068 skin: /*L*/"Theme-specific content"
1072 if (!match || match.scheme === "resource" && match.path)
1073 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
1074 || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
1075 || util.getFile(context.filter)
1076 || io.isJarURL(context.filter))
1077 completion.file(context, full);
1080 javascript: function initJavascript(dactyl, modules, window) {
1081 modules.JavaScript.setCompleter([File, File.expandPath],
1082 [function (context, obj, args) {
1083 context.quote[2] = "";
1084 modules.completion.file(context, true);
1088 modes: function initModes(dactyl, modules, window) {
1089 initModes.require("commandline");
1090 const { modes } = modules;
1092 modes.addMode("FILE_INPUT", {
1094 description: "Active when selecting a file",
1095 bases: [modes.COMMAND_LINE],
1099 options: function initOptions(dactyl, modules, window) {
1100 const { completion, options } = modules;
1102 var shell, shellcmdflag;
1103 if (config.OS.isWindows) {
1105 shellcmdflag = "/c";
1108 shell = services.environment.get("SHELL") || "sh";
1109 shellcmdflag = "-c";
1112 options.add(["banghist", "bh"],
1113 "Replace occurrences of ! with the previous command when executing external commands",
1116 options.add(["fileencoding", "fenc"],
1117 "The character encoding used when reading and writing files",
1118 "string", "UTF-8", {
1119 completer: function (context) completion.charset(context),
1120 getter: function () File.defaultEncoding,
1121 setter: function (value) (File.defaultEncoding = value)
1123 options.add(["cdpath", "cd"],
1124 "List of directories searched when executing :cd",
1125 "stringlist", ["."].concat(services.environment.get("CDPATH").split(/[:;]/).filter(util.identity)).join(","),
1127 get files() this.value.map(function (path) File(path, modules.io.cwd))
1128 .filter(function (dir) dir.exists()),
1129 setter: function (value) File.expandPathList(value)
1132 options.add(["runtimepath", "rtp"],
1133 "List of directories searched for runtime files",
1134 "stringlist", IO.runtimePath,
1136 get files() this.value.map(function (path) File(path, modules.io.cwd))
1137 .filter(function (dir) dir.exists())
1140 options.add(["shell", "sh"],
1141 "Shell to use for executing external commands with :! and :run",
1143 { validator: function (val) io.pathSearch(val) });
1145 options.add(["shellcmdflag", "shcf"],
1146 "Flag passed to shell when executing external commands with :! and :run",
1147 "string", shellcmdflag,
1149 getter: function (value) {
1150 if (this.hasChanged || !config.OS.isWindows)
1152 return /sh/.test(options["shell"]) ? "-c" : "/c";
1155 options["shell"]; // Make sure it's loaded into global storage.
1156 options["shellcmdflag"];
1158 options.add(["wildignore", "wig"],
1159 "List of path name patterns to ignore when completing files and directories",
1166 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1168 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: