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
6 // This work is licensed for reuse under an MIT license. Details are
7 // given in the LICENSE.txt file included with this file.
12 Components.utils.import("resource://dactyl/bootstrap.jsm");
14 exports: ["IO", "io"],
18 this.lazyRequire("config", ["config"]);
19 this.lazyRequire("contexts", ["Contexts", "contexts"]);
21 // TODO: why are we passing around strings rather than file objects?
23 * Provides a basic interface to common system I/O operations.
26 var IO = Module("io", {
27 init: function init() {
28 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
29 this._cwd = this._processDir.path;
31 lazyRequire("config", ["config"], this);
34 Local: function Local(dactyl, modules, window) let ({ io, plugins } = modules) ({
36 init: function init() {
37 this.config = modules.config;
38 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
39 this._cwd = this._processDir.path;
42 this._lastRunCommand = ""; // updated whenever the users runs a command with :!
43 this._scriptNames = [];
46 CommandFileMode: Class("CommandFileMode", modules.CommandMode, {
47 init: function init(prompt, params) {
49 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
55 get mode() modules.modes.FILE_INPUT,
57 complete: function (context) {
59 this.completer(context);
61 context = context.fork("files", 0);
62 modules.completion.file(context);
63 context.filters = context.filters.concat(this.filters || []);
68 * Returns all directories named *name* in 'runtimepath'.
70 * @param {string} name
71 * @returns {nsIFile[])
73 getRuntimeDirectories: function getRuntimeDirectories(name) {
74 return modules.options.get("runtimepath").files
75 .map(function (dir) dir.child(name))
76 .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
79 // FIXME: multiple paths?
81 * Sources files found in 'runtimepath'. For each relative path in *paths*
82 * each directory in 'runtimepath' is searched and if a matching file is
83 * found it is sourced. Only the first file found (per specified path) is
84 * sourced unless *all* is specified, then all found files are sourced.
86 * @param {[string]} paths An array of relative paths to source.
87 * @param {boolean} all Whether all found files should be sourced.
89 sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
90 let dirs = modules.options.get("runtimepath").files;
93 dactyl.echomsg(_("io.searchingFor", paths.join(" ").quote(), modules.options.get("runtimepath").stringValue), 2);
96 for (let dir in values(dirs)) {
97 for (let [, path] in Iterator(paths)) {
98 let file = dir.child(path);
100 dactyl.echomsg(_("io.searchingFor", file.path.quote()), 3);
102 if (file.exists() && file.isFile() && file.isReadable()) {
103 found = io.source(file.path, false) || true;
112 dactyl.echomsg(_("io.notInRTP", paths.join(" ").quote()), 1);
118 * Reads Ex commands, JavaScript or CSS from *filename*.
120 * @param {string} filename The name of the file to source.
121 * @param {object} params Extra parameters:
122 * group: The group in which to execute commands.
123 * silent: Whether errors should not be reported.
125 source: function source(filename, params) {
126 const { contexts } = modules;
127 defineModule.loadLog.push("sourcing " + filename);
129 if (!isObject(params))
130 params = { silent: params };
132 let time = Date.now();
133 return contexts.withContext(null, function () {
135 var file = util.getFile(filename) || io.File(filename);
137 if (!file.exists() || !file.isReadable() || file.isDirectory()) {
139 dactyl.echoerr(_("io.notReadable", filename.quote()));
143 dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
147 let sourceJSM = function sourceJSM() {
148 context = contexts.Module(uri);
149 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
152 if (/\.js,$/.test(filename))
154 else if (/\.js$/.test(filename)) {
156 var context = contexts.Script(file, params.group);
157 if (Set.has(this._scriptNames, file.path))
160 dactyl.loadScript(uri.spec, context);
161 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
164 if (e == Contexts) { // Hack;
169 if (e.fileName && !(e instanceof FailedAssertion))
171 e.fileName = util.fixURI(e.fileName);
172 if (e.fileName == uri.spec)
173 e.fileName = filename;
174 e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
181 else if (/\.css$/.test(filename))
182 styles.registerSheet(uri.spec, false, true);
184 context = contexts.Context(file, params.group);
185 modules.commands.execute(file.read(), null, params.silent,
189 group: context.GROUP,
192 dactyl.triggerObserver("io.source", context, file, file.lastModifiedTime);
195 Set.add(this._scriptNames, file.path);
197 dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
198 dactyl.log(_("dactyl.sourced", filename), 3);
204 let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
206 dactyl.echoerr(message);
209 defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
215 // TODO: there seems to be no way, short of a new component, to change
216 // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
218 * Returns the current working directory.
220 * It's not possible to change the real CWD of the process so this
221 * state is maintained internally. External commands run via
222 * {@link #system} are executed in this directory.
227 let dir = File(this._cwd);
229 // NOTE: the directory could have been deleted underneath us so
230 // fallback to the process's CWD
231 if (dir.exists() && dir.isDirectory())
234 return this._processDir.clone();
238 * Sets the current working directory.
240 * @param {string} newDir The new CWD. This may be a relative or
241 * absolute path and is expanded by {@link #expandPath}.
244 newDir = newDir && newDir.path || newDir || "~";
247 util.assert(this._oldcwd != null, _("io.noPrevDir"));
248 [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
251 let dir = io.File(newDir);
252 util.assert(dir.exists() && dir.isDirectory(), _("io.noSuchDir", dir.path.quote()));
254 [this._cwd, this._oldcwd] = [dir.path, this.cwd];
260 * @property {function} File class.
263 File: Class.Memoize(function () let (io = this)
264 Class("File", File, {
265 init: function init(path, checkCWD)
266 init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
270 * @property {Object} The current file sourcing context. As a file is
271 * being sourced the 'file' and 'line' properties of this context
272 * object are updated appropriately.
276 expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),
279 * Returns the first user RC file found in *dir*.
281 * @param {File|string} dir The directory to search.
282 * @param {boolean} always When true, return a path whether
283 * the file exists or not.
285 * @returns {nsIFile} The RC file or null if none is found.
287 getRCFile: function getRCFile(dir, always) {
288 dir = this.File(dir || "~");
290 let rcFile1 = dir.child("." + config.name + "rc");
291 let rcFile2 = dir.child("_" + config.name + "rc");
293 if (config.OS.isWindows)
294 [rcFile1, rcFile2] = [rcFile2, rcFile1];
296 if (rcFile1.exists() && rcFile1.isFile())
298 else if (rcFile2.exists() && rcFile2.isFile())
306 * Creates a temporary file.
310 createTempFile: function createTempFile(name, type) {
311 if (name instanceof Ci.nsIFile) {
312 var file = name.clone();
313 if (!type || type == "file")
314 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
316 file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, octal(777));
319 file = services.directory.get("TmpD", Ci.nsIFile);
320 file.append(this.config.tempFile + (name ? "." + name : ""));
321 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
324 services.externalApp.deleteTemporaryFileOnExit(file);
330 * Determines whether the given URL string resolves to a JAR URL and
331 * returns the matching nsIJARURI object if it does.
333 * @param {string} url The URL to check.
334 * @returns {nsIJARURI|null}
336 isJarURL: function isJarURL(url) {
338 let uri = util.newURI(url);
339 if (uri instanceof Ci.nsIJARURI)
342 let channel = services.io.newChannelFromURI(uri);
343 try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
344 if (channel instanceof Ci.nsIJARChannel)
345 return channel.URI.QueryInterface(Ci.nsIJARURI);
352 * Returns a list of the contents of the given JAR file which are
353 * children of the given path.
355 * @param {nsIURI|string} file The URI of the JAR file to list.
356 * @param {string} path The prefix path to search.
358 listJar: function listJar(file, path) {
359 file = util.getFile(file);
360 if (file && file.exists() && file.isFile() && file.isReadable()) {
361 // let jar = services.zipReader.getZip(file); Crashes.
362 let jar = services.ZipReader(file);
364 let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
367 for (let entry in iter(jar.findEntries("*")))
368 if (filter.test(entry))
378 readHeredoc: function readHeredoc(end) {
383 * Searches for the given executable file in the system executable
384 * file paths as specified by the PATH environment variable.
386 * On Windows, if the unadorned filename cannot be found, the
387 * extensions in the semicolon-separated list in the PATHSEP
388 * environment variable are successively appended to the original
389 * name and searched for in turn.
391 * @param {string} bin The name of the executable to find.
392 * @returns {File|null}
394 pathSearch: function pathSearch(bin) {
395 if (bin instanceof File || File.isAbsolutePath(bin))
396 return this.File(bin);
398 let dirs = services.environment.get("PATH")
399 .split(config.OS.pathListSep);
400 // Windows tries the CWD first TODO: desirable?
401 if (config.OS.isWindows)
402 dirs = [io.cwd].concat(dirs);
404 for (let [, dir] in Iterator(dirs))
406 dir = this.File(dir, true);
408 let file = dir.child(bin);
409 if (file.exists() && file.isFile() && file.isExecutable())
412 // TODO: couldn't we just palm this off to the start command?
413 // automatically try to add the executable path extensions on windows
414 if (config.OS.isWindows) {
415 let extensions = services.environment.get("PATHEXT").split(";");
416 for (let [, extension] in Iterator(extensions)) {
417 file = dir.child(bin + extension);
428 * Runs an external program.
430 * @param {File|string} program The program to run.
431 * @param {[string]} args An array of arguments to pass to *program*.
433 run: function run(program, args, blocking, self) {
436 let file = this.pathSearch(program);
438 if (!file || !file.exists()) {
439 util.dactyl.echoerr(_("io.noCommand", program));
440 if (callable(blocking))
441 util.trapErrors(blocking);
445 let process = services.Process(file);
446 process.run(false, args.map(String), args.length);
448 if (callable(blocking))
449 var timer = services.Timer(
451 if (!process.isRunning) {
453 util.trapErrors(blocking, self, process.exitValue);
456 100, services.Timer.TYPE_REPEATING_SLACK);
458 while (process.isRunning)
459 util.threadYield(false, true);
466 return process.exitValue;
469 // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
470 // fixed use that instead of a tmpfile
472 * Runs *command* in a subshell and returns the output. The shell used is
473 * that specified by the 'shell' option.
475 * @param {string|[string]} command The command to run. This can be a shell
476 * command string or an array of strings (a command and arguments)
477 * which will be escaped and concatenated.
478 * @param {string} input Any input to be provided to the command on stdin.
479 * @param {function(object)} callback A callback to be called when
480 * the command completes. @optional
481 * @returns {object|null}
483 system: function system(command, input, callback) {
484 util.dactyl.echomsg(_("io.callingShell", command), 4);
486 let { shellEscape } = util.closure;
488 return this.withTempFiles(function (stdin, stdout, cmd) {
489 if (input instanceof File)
494 function result(status, output) ({
495 __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
496 valueOf: function () this.output,
497 output: output.replace(/^(.*)\n$/, "$1"),
499 toString: function () this.output
502 function async(status) {
503 let output = stdout.read();
504 [stdin, stdout, cmd].forEach(function (f) f.exists() && f.remove(false));
505 callback(result(status, output));
508 let shell = io.pathSearch(storage["options"].get("shell").value);
509 let shcf = storage["options"].get("shellcmdflag").value;
510 util.assert(shell, _("error.invalid", "'shell'"));
512 if (isArray(command))
513 command = command.map(shellEscape).join(" ");
515 // TODO: implement 'shellredir'
516 if (config.OS.isWindows && !/sh/.test(shell.leafName)) {
517 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
518 var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true);
521 cmd.write("cd " + shellEscape(this.cwd.path) + "\n" +
522 ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path),
523 shellEscape(shell.path), shcf, shellEscape(command)].join(" "));
524 res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true);
527 return callback ? true : result(res, stdout.read());
532 * Creates a temporary file context for executing external commands.
533 * *func* is called with a temp file, created with {@link #createTempFile},
534 * for each explicit argument. Ensures that all files are removed when
537 * @param {function} func The function to execute.
538 * @param {Object} self The 'this' object used when executing func.
539 * @returns {boolean} false if temp files couldn't be created,
540 * otherwise, the return value of *func*.
542 withTempFiles: function withTempFiles(func, self, checked, ext) {
543 let args = array(util.range(0, func.length))
544 .map(bind("createTempFile", this, ext)).array;
546 if (!args.every(util.identity))
548 var res = func.apply(self || this, args);
551 if (!checked || res !== true)
552 args.forEach(function (f) f.remove(false));
558 * @property {string} The value of the $PENTADACTYL_RUNTIME environment
562 const rtpvar = config.idName + "_RUNTIME";
563 let rtp = services.environment.get(rtpvar);
565 rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name;
566 services.environment.set(rtpvar, rtp);
572 * @property {string} The current platform's path separator.
574 PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
576 commands: function init_commands(dactyl, modules, window) {
577 const { commands, completion, io } = modules;
579 commands.add(["cd", "chd[ir]"],
580 "Change the current directory",
587 arg = File.expandPath(arg);
589 // go directly to an absolute path or look for a relative path
591 if (File.isAbsolutePath(arg)) {
593 dactyl.echomsg(io.cwd.path);
596 let dirs = modules.options.get("cdpath").files;
597 for (let dir in values(dirs)) {
598 dir = dir.child(arg);
600 if (dir.exists() && dir.isDirectory() && dir.isReadable()) {
602 dactyl.echomsg(io.cwd.path);
607 dactyl.echoerr(_("io.noSuchDir", arg.quote()));
608 dactyl.echoerr(_("io.commandFailed"));
612 completer: function (context) completion.directory(context, true),
616 commands.add(["pw[d]"],
617 "Print the current directory name",
618 function () { dactyl.echomsg(io.cwd.path); },
621 commands.add([config.name.replace(/(.)(.*)/, "mk$1[$2rc]")],
622 "Write current key mappings and changed options to the config file",
624 dactyl.assert(args.length <= 1, _("io.oneFileAllowed"));
626 let file = io.File(args[0] || io.getRCFile(null, true));
628 dactyl.assert(!file.exists() || args.bang, _("io.exists", file.path.quote()));
630 // TODO: Use a set/specifiable list here:
631 let lines = [cmd.serialize().map(commands.commandToString, cmd) for (cmd in commands.iterator()) if (cmd.serialize)];
632 lines = array.flatten(lines);
634 lines.unshift('"' + config.version + "\n");
635 lines.push("\n\" vim: set ft=" + config.name + ":");
638 file.write(lines.join("\n"));
641 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
642 dactyl.log(_("error.notWriteable", file.path, e.message)); // XXX
645 argCount: "*", // FIXME: should be "?" but kludged for proper error message
647 completer: function (context) completion.file(context, true)
650 commands.add(["mks[yntax]"],
651 "Generate a Vim syntax file",
653 let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
654 let file = io.File(runtime + "syntax/" + config.name + ".vim");
656 file = io.File(args[0]);
658 if (file.exists() && file.isDirectory() || args[0] && /\/$/.test(args[0]))
659 file.append(config.name + ".vim");
660 dactyl.assert(!file.exists() || args.bang, _("io.exists"));
662 let template = util.compileMacro(<![CDATA[
664 " Language: Pentadactyl configuration file
665 " Maintainer: Doug Kearns <dougkearns@gmail.com>
667 " TODO: make this <name> specific - shared dactyl config?
669 if exists("b:current_syntax")
673 let s:cpo_save = &cpo
676 syn include @javascriptTop syntax/javascript.vim
677 unlet b:current_syntax
679 syn include @cssTop syntax/css.vim
680 unlet b:current_syntax
682 syn match <name>CommandStart "\%(^\s*:\=\)\@<=" nextgroup=<name>Command,<name>AutoCmd
687 syn match <name>Command "!" contained
689 syn keyword <name>AutoCmd au[tocmd] contained nextgroup=<name>AutoEventList skipwhite
694 syn match <name>AutoEventList "\(\a\+,\)*\a\+" contained contains=<name>AutoEvent
696 syn region <name>Set matchgroup=<name>Command start="\%(^\s*:\=\)\@<=\<\%(setl\%[ocal]\|setg\%[lobal]\|set\=\)\=\>"
697 \ end="$" keepend oneline contains=<name>Option,<name>String
700 \ contained nextgroup=pentadactylSetMod
703 execute 'syn match <name>Option "\<\%(no\|inv\)\=\%(' .
704 \ join(s:toggleOptions, '\|') .
705 \ '\)\>!\=" contained nextgroup=<name>SetMod'
707 syn match <name>SetMod "\%(\<[a-z_]\+\)\@<=&" contained
709 syn region <name>JavaScript start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=" end="$" contains=@javascriptTop keepend oneline
710 syn region <name>JavaScript matchgroup=<name>JavaScriptDelimiter
711 \ start="\%(^\s*\%(javascript\|js\)\s\+\)\@<=<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@javascriptTop fold
713 let s:cssRegionStart = '\%(^\s*sty\%[le]!\=\s\+\%(-\%(n\|name\)\%(\s\+\|=\)\S\+\s\+\)\=[^-]\S\+\s\+\)\@<='
714 execute 'syn region <name>Css start="' . s:cssRegionStart . '" end="$" contains=@cssTop keepend oneline'
715 execute 'syn region <name>Css matchgroup=<name>CssDelimiter'
716 \ 'start="' . s:cssRegionStart . '<<\s*\z(\h\w*\)"hs=s+2 end="^\z1$" contains=@cssTop fold'
718 syn match <name>Notation "<[0-9A-Za-z-]\+>"
720 syn keyword <name>Todo FIXME NOTE TODO XXX contained
722 syn region <name>String start="\z(["']\)" end="\z1" skip="\\\\\|\\\z1" oneline
724 syn match <name>Comment +^\s*".*$+ contains=<name>Todo,@Spell
726 " NOTE: match vim.vim highlighting group names
727 hi def link <name>AutoCmd <name>Command
728 hi def link <name>AutoEvent Type
729 hi def link <name>Command Statement
730 hi def link <name>JavaScriptDelimiter Delimiter
731 hi def link <name>CssDelimiter Delimiter
732 hi def link <name>Notation Special
733 hi def link <name>Comment Comment
734 hi def link <name>Option PreProc
735 hi def link <name>SetMod <name>Option
736 hi def link <name>String String
737 hi def link <name>Todo Todo
739 let b:current_syntax = "<name>"
741 let &cpo = s:cpo_save
744 " vim: tw=130 et ts=4 sw=4:
748 function wrap(prefix, items, sep) {
752 lines.__defineGetter__("last", function () this[this.length - 1]);
754 for (let item in values(items.array || items)) {
755 if (item.length > width && (!lines.length || lines.last.length > 1)) {
756 lines.push([prefix]);
757 width = WIDTH - prefix.length;
760 width -= item.length + sep.length;
761 lines.last.push(item, sep);
764 return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
767 const { commands, options } = modules;
768 file.write(template({
770 autocommands: wrap("syn keyword " + config.name + "AutoEvent ",
771 keys(config.autocommands)),
772 commands: wrap("syn keyword " + config.name + "Command ",
773 array(c.specs for (c in commands.iterator())).flatten()),
774 options: wrap("syn keyword " + config.name + "Option ",
775 array(o.names for (o in options) if (o.type != "boolean")).flatten()),
776 toggleoptions: wrap("let s:toggleOptions = [",
777 array(o.realNames for (o in options) if (o.type == "boolean"))
778 .flatten().map(String.quote),
784 completer: function (context) completion.file(context, true),
788 commands.add(["runt[ime]"],
789 "Source the specified file from each directory in 'runtimepath'",
790 function (args) { io.sourceFromRuntimePath(args, args.bang); },
794 completer: function (context) completion.runtime(context)
798 commands.add(["scrip[tnames]"],
799 "List all sourced script names",
801 let names = Object.keys(io._scriptNames);
803 dactyl.echomsg(_("command.scriptnames.none"));
805 modules.commandline.commandOutput(
806 template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
807 ([i + 1, file] for ([i, file] in Iterator(names)))));
812 commands.add(["so[urce]"],
813 "Read Ex commands, JavaScript or CSS from a file",
816 dactyl.echoerr(_("io.oneFileAllowed"));
818 io.source(args[0], { silent: args.bang });
820 argCount: "+", // FIXME: should be "1" but kludged for proper error message
822 completer: function (context) completion.file(context, true)
825 commands.add(["!", "run"],
828 let arg = args[0] || "";
830 // :!! needs to be treated specially as the command parser sets the
831 // bang flag but removes the ! from arg
835 // This is an asinine and irritating "feature" when we have searchable
836 // command-line history. --Kris
837 if (modules.options["banghist"]) {
838 // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
839 // pass through a raw bang when escaped or substitute the last command
841 // replaceable bang and no previous command?
842 dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
843 _("command.run.noPrevious"));
845 arg = arg.replace(/(\\)*!/g,
846 function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", io._lastRunCommand)
850 io._lastRunCommand = arg;
852 let result = io.system(arg);
853 if (result.returnValue != 0)
854 result.output += "\n" + _("io.shellReturn", result.returnValue);
856 modules.commandline.command = args.commandName.replace("run", "$& ") + arg;
857 modules.commandline.commandOutput(<span highlight="CmdOutput">{result.output}</span>);
859 modules.autocommands.trigger("ShellCmdPost", {});
863 // This is abominably slow.
864 // completer: function (context) completion.shellCommand(context),
868 completion: function init_completion(dactyl, modules, window) {
869 const { completion, io } = modules;
871 completion.charset = function (context) {
872 context.anchored = false;
875 description: function (charset) {
877 return services.charset.getCharsetTitle(charset);
884 context.generate = function () iter(services.charset.getDecoderList());
887 completion.directory = function directory(context, full) {
888 this.file(context, full);
889 context.filters.push(function (item) item.isdir);
892 completion.environment = function environment(context) {
893 context.title = ["Environment Variable", "Value"];
894 context.generate = function ()
895 io.system(config.OS.isWindows ? "set" : "env")
897 .filter(function (line) line.indexOf("=") > 0)
898 .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
901 completion.file = function file(context, full, dir) {
902 if (/^jar:[^!]*$/.test(context.filter))
905 // dir == "" is expanded inside readDirectory to the current dir
906 function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
907 dir = getDir(dir || context.filter);
909 let file = util.getFile(dir);
910 if (file && (!file.exists() || !file.isDirectory()))
914 context.advance(dir.length);
916 context.title = [full ? "Path" : "Filename", "Type"];
918 text: !full ? "leafName" : function (f) this.path,
919 path: function (f) dir + f.leafName,
920 description: function (f) this.isdir ? "Directory" : "File",
921 isdir: function (f) f.isDirectory(),
922 icon: function (f) this.isdir ? "resource://gre/res/html/folder.png"
923 : "moz-icon://" + f.leafName
925 context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text);
927 if (modules.options["wildignore"])
928 context.filters.push(function (item) !modules.options.get("wildignore").getKey(item.path));
930 // context.background = true;
932 let uri = io.isJarURL(dir);
934 context.generate = function generate_jar() {
937 isDirectory: function () s.substr(-1) == "/",
938 leafName: /([^\/]*)\/?$/.exec(s)[1]
940 for (s in io.listJar(uri.JARFile, getDir(uri.JAREntry)))]
943 context.generate = function generate_file() {
945 return io.File(file || dir).readDirectory();
952 completion.runtime = function (context) {
953 for (let [, dir] in Iterator(modules.options["runtimepath"]))
954 context.fork(dir, 0, this, function (context) {
955 dir = dir.replace("/+$", "") + "/";
956 completion.file(context, true, dir + context.filter);
957 context.title[0] = dir;
958 context.keys.text = function (f) this.path.substr(dir.length);
962 completion.shellCommand = function shellCommand(context) {
963 context.title = ["Shell Command", "Path"];
964 context.generate = function () {
965 let dirNames = services.environment.get("PATH").split(config.OS.pathListSep);
968 for (let [, dirName] in Iterator(dirNames)) {
969 let dir = io.File(dirName);
970 if (dir.exists() && dir.isDirectory())
971 commands.push([[file.leafName, dir.path] for (file in iter(dir.directoryEntries))
972 if (file.isFile() && file.isExecutable())]);
975 return array.flatten(commands);
979 completion.addUrlCompleter("file", "Local files", function (context, full) {
980 let match = util.regexp(<![CDATA[
984 (?P<scheme> chrome|resource)
989 (?P<path> \/[^\/]* )?
991 ]]>, "x").exec(context.filter);
994 context.key = match.proto;
995 context.advance(match.proto.length);
996 context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]);
998 else if (match.scheme === "chrome") {
999 context.key = match.prefix;
1000 context.advance(match.prefix.length + 1);
1001 context.generate = function () iter({
1002 content: /*L*/"Chrome content",
1003 locale: /*L*/"Locale-specific content",
1004 skin: /*L*/"Theme-specific content"
1008 if (!match || match.scheme === "resource" && match.path)
1009 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
1010 || config.OS.isWindows && /^[a-z]:/i.test(context.filter)
1011 || util.getFile(context.filter)
1012 || io.isJarURL(context.filter))
1013 completion.file(context, full);
1016 javascript: function init_javascript(dactyl, modules, window) {
1017 modules.JavaScript.setCompleter([File, File.expandPath],
1018 [function (context, obj, args) {
1019 context.quote[2] = "";
1020 modules.completion.file(context, true);
1024 modes: function initModes(dactyl, modules, window) {
1025 initModes.require("commandline");
1026 const { modes } = modules;
1028 modes.addMode("FILE_INPUT", {
1030 description: "Active when selecting a file",
1031 bases: [modes.COMMAND_LINE],
1035 options: function init_options(dactyl, modules, window) {
1036 const { completion, options } = modules;
1038 var shell, shellcmdflag;
1039 if (config.OS.isWindows) {
1041 shellcmdflag = "/c";
1044 shell = services.environment.get("SHELL") || "sh";
1045 shellcmdflag = "-c";
1048 options.add(["banghist", "bh"],
1049 "Replace occurrences of ! with the previous command when executing external commands",
1052 options.add(["fileencoding", "fenc"],
1053 "The character encoding used when reading and writing files",
1054 "string", "UTF-8", {
1055 completer: function (context) completion.charset(context),
1056 getter: function () File.defaultEncoding,
1057 setter: function (value) (File.defaultEncoding = value)
1059 options.add(["cdpath", "cd"],
1060 "List of directories searched when executing :cd",
1061 "stringlist", ["."].concat(services.environment.get("CDPATH").split(/[:;]/).filter(util.identity)).join(","),
1063 get files() this.value.map(function (path) File(path, modules.io.cwd))
1064 .filter(function (dir) dir.exists()),
1065 setter: function (value) File.expandPathList(value)
1068 options.add(["runtimepath", "rtp"],
1069 "List of directories searched for runtime files",
1070 "stringlist", IO.runtimePath,
1072 get files() this.value.map(function (path) File(path, modules.io.cwd))
1073 .filter(function (dir) dir.exists())
1076 options.add(["shell", "sh"],
1077 "Shell to use for executing external commands with :! and :run",
1079 { validator: function (val) io.pathSearch(val) });
1081 options.add(["shellcmdflag", "shcf"],
1082 "Flag passed to shell when executing external commands with :! and :run",
1083 "string", shellcmdflag,
1085 getter: function (value) {
1086 if (this.hasChanged || !config.OS.isWindows)
1088 return /sh/.test(options["shell"]) ? "-c" : "/c";
1091 options["shell"]; // Make sure it's loaded into global storage.
1092 options["shellcmdflag"];
1094 options.add(["wildignore", "wig"],
1095 "List of path name patterns to ignore when completing files and directories",
1102 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1104 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: