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"],
15 require: ["services"],
16 use: ["config", "messages", "storage", "styles", "template", "util"]
19 // TODO: why are we passing around strings rather than file objects?
21 * Provides a basic interface to common system I/O operations.
24 var IO = Module("io", {
26 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
27 this._cwd = this._processDir.path;
32 Local: function (dactyl, modules, window) let ({ io, plugins } = modules) ({
34 init: function init() {
35 this.config = modules.config;
36 this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
37 this._cwd = this._processDir.path;
40 this._lastRunCommand = ""; // updated whenever the users runs a command with :!
41 this._scriptNames = [];
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;
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 });
56 onStateChange: function () {},
57 onProgressChange: function () {},
58 onSecurityChange: function () {}
61 services.downloadManager.addListener(this.downloadListener);
64 CommandFileMode: Class("CommandFileMode", modules.CommandMode, {
65 init: function init(prompt, params) {
67 this.prompt = isArray(prompt) ? prompt : ["Question", prompt];
73 get mode() modules.modes.FILE_INPUT,
75 complete: function (context) {
77 this.completer(context);
79 context = context.fork("files", 0);
80 modules.completion.file(context);
81 context.filters = context.filters.concat(this.filters || []);
85 destroy: function destroy() {
86 services.downloadManager.removeListener(this.downloadListener);
90 * Returns all directories named *name* in 'runtimepath'.
92 * @param {string} name
93 * @returns {nsIFile[])
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());
101 // FIXME: multiple paths?
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.
108 * @param {[string]} paths An array of relative paths to source.
109 * @param {boolean} all Whether all found files should be sourced.
111 sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
112 let dirs = modules.options.get("runtimepath").files;
115 dactyl.echomsg(_("io.searchingFor", paths.join(" ").quote(), modules.options.get("runtimepath").stringValue), 2);
118 for (let dir in values(dirs)) {
119 for (let [, path] in Iterator(paths)) {
120 let file = dir.child(path);
122 dactyl.echomsg(_("io.searchingFor", file.path.quote()), 3);
124 if (file.exists() && file.isFile() && file.isReadable()) {
125 found = io.source(file.path, false) || true;
134 dactyl.echomsg(_("io.notInRTP", paths.join(" ").quote()), 1);
140 * Reads Ex commands, JavaScript or CSS from *filename*.
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.
147 source: function source(filename, params) {
148 const { contexts } = modules;
149 defineModule.loadLog.push("sourcing " + filename);
151 if (!isObject(params))
152 params = { silent: params };
154 let time = Date.now();
155 return contexts.withContext(null, function () {
157 var file = util.getFile(filename) || io.File(filename);
159 if (!file.exists() || !file.isReadable() || file.isDirectory()) {
161 dactyl.echoerr(_("io.notReadable", filename.quote()));
165 dactyl.echomsg(_("io.sourcing", filename.quote()), 2);
167 let uri = services.io.newFileURI(file);
169 // handle pure JavaScript files specially
170 if (/\.js$/.test(filename)) {
172 var context = contexts.Script(file, params.group);
173 dactyl.loadScript(uri.spec, context);
174 dactyl.helpInitialized = false;
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}</>;
188 else if (/\.css$/.test(filename))
189 styles.registerSheet(uri.spec, false, true);
191 context = contexts.Context(file, params.group);
192 modules.commands.execute(file.read(), null, params.silent,
196 group: context.GROUP,
201 if (this._scriptNames.indexOf(file.path) == -1)
202 this._scriptNames.push(file.path);
204 dactyl.echomsg(_("io.sourcingEnd", filename.quote()), 2);
205 dactyl.log(_("dactyl.sourced", filename), 3);
210 dactyl.reportError(e);
211 let message = _("io.sourcingError", e.echoerr || (file ? file.path : filename) + ": " + e);
213 dactyl.echoerr(message);
216 defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
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
225 * Returns the current working directory.
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.
234 let dir = File(this._cwd);
236 // NOTE: the directory could have been deleted underneath us so
237 // fallback to the process's CWD
238 if (dir.exists() && dir.isDirectory())
241 return this._processDir.clone();
245 * Sets the current working directory.
247 * @param {string} newDir The new CWD. This may be a relative or
248 * absolute path and is expanded by {@link #expandPath}.
251 newDir = newDir && newDir.path || newDir || "~";
254 util.assert(this._oldcwd != null, _("io.noPrevDir"));
255 [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
258 let dir = io.File(newDir);
259 util.assert(dir.exists() && dir.isDirectory(), _("io.noSuchDir", dir.path.quote()));
261 [this._cwd, this._oldcwd] = [dir.path, this.cwd];
267 * @property {function} File class.
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)
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.
283 expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),
286 * Returns the first user RC file found in *dir*.
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.
292 * @returns {nsIFile} The RC file or null if none is found.
294 getRCFile: function (dir, always) {
295 dir = this.File(dir || "~");
297 let rcFile1 = dir.child("." + config.name + "rc");
298 let rcFile2 = dir.child("_" + config.name + "rc");
300 if (util.OS.isWindows)
301 [rcFile1, rcFile2] = [rcFile2, rcFile1];
303 if (rcFile1.exists() && rcFile1.isFile())
305 else if (rcFile2.exists() && rcFile2.isFile())
314 * Creates a temporary file.
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));
323 Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
324 .getService(Ci.nsPIExternalAppLauncher).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(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);
349 * Returns a list of the contents of the given JAR file which are
350 * children of the given path.
352 * @param {nsIURI|string} file The URI of the JAR file to list.
353 * @param {string} path The prefix path to search.
355 listJar: function listJar(file, path) {
356 file = util.getFile(file);
357 if (file && file.exists() && file.isFile() && file.isReadable()) {
358 // let jar = services.zipReader.getZip(file); Crashes.
359 let jar = services.ZipReader(file);
361 let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
364 for (let entry in iter(jar.findEntries("*")))
365 if (filter.test(entry))
375 readHeredoc: function (end) {
380 * Searches for the given executable file in the system executable
381 * file paths as specified by the PATH environment variable.
383 * On Windows, if the unadorned filename cannot be found, the
384 * extensions in the semicolon-separated list in the PATHSEP
385 * environment variable are successively appended to the original
386 * name and searched for in turn.
388 * @param {string} bin The name of the executable to find.
390 pathSearch: function (bin) {
391 if (bin instanceof File || File.isAbsolutePath(bin))
392 return this.File(bin);
394 let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
395 // Windows tries the CWD first TODO: desirable?
396 if (util.OS.isWindows)
397 dirs = [io.cwd].concat(dirs);
399 for (let [, dir] in Iterator(dirs))
401 dir = this.File(dir, true);
403 let file = dir.child(bin);
404 if (file.exists() && file.isFile() && file.isExecutable())
407 // TODO: couldn't we just palm this off to the start command?
408 // automatically try to add the executable path extensions on windows
409 if (util.OS.isWindows) {
410 let extensions = services.environment.get("PATHEXT").split(";");
411 for (let [, extension] in Iterator(extensions)) {
412 file = dir.child(bin + extension);
423 * Runs an external program.
425 * @param {File|string} program The program to run.
426 * @param {[string]} args An array of arguments to pass to *program*.
428 run: function (program, args, blocking) {
431 let file = this.pathSearch(program);
433 if (!file || !file.exists()) {
434 util.dactyl.echoerr(_("io.noCommand", program));
435 if (callable(blocking))
436 util.trapErrors(blocking);
440 let process = services.Process(file);
441 process.run(false, args.map(String), args.length);
443 if (callable(blocking))
444 var timer = services.Timer(
446 if (!process.isRunning) {
448 util.trapErrors(blocking);
451 100, services.Timer.TYPE_REPEATING_SLACK);
453 while (process.isRunning)
454 util.threadYield(false, true);
461 return process.exitValue;
464 // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
465 // fixed use that instead of a tmpfile
467 * Runs *command* in a subshell and returns the output in a string. The
468 * shell used is that specified by the 'shell' option.
470 * @param {string} command The command to run.
471 * @param {string} input Any input to be provided to the command on stdin.
474 system: function (command, input) {
475 util.dactyl.echomsg(_("io.callingShell", command), 4);
477 function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';
479 return this.withTempFiles(function (stdin, stdout, cmd) {
480 if (input instanceof File)
485 let shell = io.pathSearch(storage["options"].get("shell").value);
486 let shcf = storage["options"].get("shellcmdflag").value;
487 util.assert(shell, _("error.invalid", "'shell'"));
489 if (isArray(command))
490 command = command.map(escape).join(" ");
492 // TODO: implement 'shellredir'
493 if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
494 command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
495 var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
498 cmd.write("cd " + escape(this.cwd.path) + "\n" +
499 ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
500 escape(shell.path), shcf, escape(command)].join(" "));
501 res = this.run("/bin/sh", ["-e", cmd.path], true);
505 __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
506 valueOf: function () this.output,
507 output: stdout.read().replace(/^(.*)\n$/, "$1"),
509 toString: function () this.output
515 * Creates a temporary file context for executing external commands.
516 * *func* is called with a temp file, created with {@link #createTempFile},
517 * for each explicit argument. Ensures that all files are removed when
520 * @param {function} func The function to execute.
521 * @param {Object} self The 'this' object used when executing func.
522 * @returns {boolean} false if temp files couldn't be created,
523 * otherwise, the return value of *func*.
525 withTempFiles: function (func, self, checked) {
526 let args = array(util.range(0, func.length)).map(this.closure.createTempFile).array;
528 if (!args.every(util.identity))
530 var res = func.apply(self || this, args);
533 if (!checked || res !== true)
534 args.forEach(function (f) f && f.remove(false));
540 * @property {string} The value of the $PENTADACTYL_RUNTIME environment
544 const rtpvar = config.idName + "_RUNTIME";
545 let rtp = services.environment.get(rtpvar);
547 rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name;
548 services.environment.set(rtpvar, rtp);
554 * @property {string} The current platform's path separator.
556 PATH_SEP: deprecated("File.PATH_SEP", { get: function PATH_SEP() File.PATH_SEP })
558 commands: function (dactyl, modules, window) {
559 const { commands, completion, io } = modules;
561 commands.add(["cd", "chd[ir]"],
562 "Change the current directory",
569 arg = File.expandPath(arg);
571 // go directly to an absolute path or look for a relative path
573 if (File.isAbsolutePath(arg)) {
575 dactyl.echomsg(io.cwd.path);
578 let dirs = modules.options.get("cdpath").files;
579 for (let dir in values(dirs)) {
580 dir = dir.child(arg);
582 if (dir.exists() && dir.isDirectory() && dir.isReadable()) {
584 dactyl.echomsg(io.cwd.path);
589 dactyl.echoerr(_("io.noSuchDir", arg.quote()));
590 dactyl.echoerr(_("io.commandFailed"));
594 completer: function (context) completion.directory(context, true),
598 commands.add(["pw[d]"],
599 "Print the current directory name",
600 function () { dactyl.echomsg(io.cwd.path); },
603 commands.add([config.name.replace(/(.)(.*)/, "mk$1[$2rc]")],
604 "Write current key mappings and changed options to the config file",
606 dactyl.assert(args.length <= 1, _("io.oneFileAllowed"));
608 let file = io.File(args[0] || io.getRCFile(null, true));
610 dactyl.assert(!file.exists() || args.bang, _("io.exists", file.path.quote()));
612 // TODO: Use a set/specifiable list here:
613 let lines = [cmd.serialize().map(commands.commandToString, cmd) for (cmd in commands.iterator()) if (cmd.serialize)];
614 lines = array.flatten(lines);
616 lines.unshift('"' + config.version + "\n");
617 lines.push("\n\" vim: set ft=" + config.name + ":");
620 file.write(lines.join("\n"));
623 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
624 dactyl.log(_("error.notWriteable", file.path, e.message)); // XXX
627 argCount: "*", // FIXME: should be "?" but kludged for proper error message
629 completer: function (context) completion.file(context, true)
632 commands.add(["mks[yntax]"],
633 "Generate a Vim syntax file",
635 let runtime = util.OS.isWindows ? "~/vimfiles/" : "~/.vim/";
636 let file = io.File(runtime + "syntax/" + config.name + ".vim");
638 file = io.File(args[0]);
640 if (file.exists() && file.isDirectory() || args[0] && /\/$/.test(args[0]))
641 file.append(config.name + ".vim");
642 dactyl.assert(!file.exists() || args.bang, _("io.exists"));
644 let template = util.compileMacro(<![CDATA[
646 " Language: Pentadactyl configuration file
647 " Maintainer: Doug Kearns <dougkearns@gmail.com>
649 " TODO: make this <name> specific - shared dactyl config?
651 if exists("b:current_syntax")
655 let s:cpo_save = &cpo
658 syn include @javascriptTop syntax/javascript.vim
659 unlet b:current_syntax
661 syn include @cssTop syntax/css.vim
662 unlet b:current_syntax
664 syn match <name>CommandStart "\%(^\s*:\=\)\@<=" nextgroup=<name>Command,<name>AutoCmd
669 syn match <name>Command "!" contained
671 syn keyword <name>AutoCmd au[tocmd] contained nextgroup=<name>AutoEventList skipwhite
676 syn match <name>AutoEventList "\(\a\+,\)*\a\+" contained contains=<name>AutoEvent
678 syn region <name>Set matchgroup=<name>Command start="\%(^\s*:\=\)\@<=\<\%(setl\%[ocal]\|setg\%[lobal]\|set\=\)\=\>"
679 \ end="$" keepend oneline contains=<name>Option,<name>String
682 \ contained nextgroup=pentadactylSetMod
685 execute 'syn match <name>Option "\<\%(no\|inv\)\=\%(' .
686 \ join(s:toggleOptions, '\|') .
687 \ '\)\>!\=" contained nextgroup=<name>SetMod'
689 syn match <name>SetMod "\%(\<[a-z_]\+\)\@<=&" contained
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
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'
700 syn match <name>Notation "<[0-9A-Za-z-]\+>"
702 syn keyword <name>Todo FIXME NOTE TODO XXX contained
704 syn region <name>String start="\z(["']\)" end="\z1" skip="\\\\\|\\\z1" oneline
706 syn match <name>Comment +^\s*".*$+ contains=<name>Todo,@Spell
708 " NOTE: match vim.vim highlighting group names
709 hi def link <name>AutoCmd <name>Command
710 hi def link <name>AutoEvent Type
711 hi def link <name>Command Statement
712 hi def link <name>JavaScriptDelimiter Delimiter
713 hi def link <name>CssDelimiter Delimiter
714 hi def link <name>Notation Special
715 hi def link <name>Comment Comment
716 hi def link <name>Option PreProc
717 hi def link <name>SetMod <name>Option
718 hi def link <name>String String
719 hi def link <name>Todo Todo
721 let b:current_syntax = "<name>"
723 let &cpo = s:cpo_save
726 " vim: tw=130 et ts=4 sw=4:
730 function wrap(prefix, items, sep) {
734 lines.__defineGetter__("last", function () this[this.length - 1]);
736 for (let item in values(items.array || items)) {
737 if (item.length > width && (!lines.length || lines.last.length > 1)) {
738 lines.push([prefix]);
739 width = WIDTH - prefix.length;
742 width -= item.length + sep.length;
743 lines.last.push(item, sep);
746 return lines.map(function (l) l.join("")).join("\n").replace(/\s+\n/gm, "\n");
749 const { commands, options } = modules;
750 file.write(template({
752 autocommands: wrap("syn keyword " + config.name + "AutoEvent ",
753 keys(config.autocommands)),
754 commands: wrap("syn keyword " + config.name + "Command ",
755 array(c.specs for (c in commands.iterator())).flatten()),
756 options: wrap("syn keyword " + config.name + "Option ",
757 array(o.names for (o in options) if (o.type != "boolean")).flatten()),
758 toggleoptions: wrap("let s:toggleOptions = [",
759 array(o.realNames for (o in options) if (o.type == "boolean"))
760 .flatten().map(String.quote),
766 completer: function (context) completion.file(context, true),
770 commands.add(["runt[ime]"],
771 "Source the specified file from each directory in 'runtimepath'",
772 function (args) { io.sourceFromRuntimePath(args, args.bang); },
776 completer: function (context) completion.runtime(context)
780 commands.add(["scrip[tnames]"],
781 "List all sourced script names",
783 if (!io._scriptNames.length)
784 dactyl.echomsg(_("command.scriptnames.none"));
786 modules.commandline.commandOutput(
787 template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
788 ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))));
793 commands.add(["so[urce]"],
794 "Read Ex commands, JavaScript or CSS from a file",
797 dactyl.echoerr(_("io.oneFileAllowed"));
799 io.source(args[0], { silent: args.bang });
801 argCount: "+", // FIXME: should be "1" but kludged for proper error message
803 completer: function (context) completion.file(context, true)
806 commands.add(["!", "run"],
809 let arg = args[0] || "";
811 // :!! needs to be treated specially as the command parser sets the
812 // bang flag but removes the ! from arg
816 // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
817 // pass through a raw bang when escaped or substitute the last command
819 // This is an asinine and irritating feature when we have searchable
820 // command-line history. --Kris
821 if (modules.options["banghist"]) {
822 // replaceable bang and no previous command?
823 dactyl.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || io._lastRunCommand,
824 _("command.run.noPrevious"));
826 arg = arg.replace(/(\\)*!/g,
827 function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", io._lastRunCommand)
831 io._lastRunCommand = arg;
833 let result = io.system(arg);
834 if (result.returnValue != 0)
835 result.output += "\n" + _("io.shellReturn", result.returnValue);
837 modules.commandline.command = args.commandName.replace("run", "$& ") + arg;
838 modules.commandline.commandOutput(<span highlight="CmdOutput">{result.output}</span>);
840 modules.autocommands.trigger("ShellCmdPost", {});
844 // This is abominably slow.
845 // completer: function (context) completion.shellCommand(context),
849 completion: function (dactyl, modules, window) {
850 const { completion, io } = modules;
852 completion.charset = function (context) {
853 context.anchored = false;
856 description: function (charset) {
858 return services.charset.getCharsetTitle(charset);
865 context.generate = function () iter(services.charset.getDecoderList());
868 completion.directory = function directory(context, full) {
869 this.file(context, full);
870 context.filters.push(function (item) item.isdir);
873 completion.environment = function environment(context) {
874 context.title = ["Environment Variable", "Value"];
875 context.generate = function ()
876 io.system(util.OS.isWindows ? "set" : "env")
878 .filter(function (line) line.indexOf("=") > 0)
879 .map(function (line) line.match(/([^=]+)=(.*)/).slice(1));
882 completion.file = function file(context, full, dir) {
883 if (/^jar:[^!]*$/.test(context.filter))
886 // dir == "" is expanded inside readDirectory to the current dir
887 function getDir(str) str.match(/^(?:.*[\/\\])?/)[0];
888 dir = getDir(dir || context.filter);
890 let file = util.getFile(dir);
891 if (file && (!file.exists() || !file.isDirectory()))
895 context.advance(dir.length);
897 context.title = [full ? "Path" : "Filename", "Type"];
899 text: !full ? "leafName" : function (f) this.path,
900 path: function (f) dir + f.leafName,
901 description: function (f) this.isdir ? "Directory" : "File",
902 isdir: function (f) f.isDirectory(),
903 icon: function (f) this.isdir ? "resource://gre/res/html/folder.png"
904 : "moz-icon://" + f.leafName
906 context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text);
908 if (modules.options["wildignore"])
909 context.filters.push(function (item) !modules.options.get("wildignore").getKey(item.path));
911 // context.background = true;
913 let uri = io.isJarURL(dir);
915 context.generate = function generate_jar() {
918 isDirectory: function () s.substr(-1) == "/",
919 leafName: /([^\/]*)\/?$/.exec(s)[1]
921 for (s in io.listJar(uri.JARFile, getDir(uri.JAREntry)))]
924 context.generate = function generate_file() {
926 return io.File(file || dir).readDirectory();
933 completion.runtime = function (context) {
934 for (let [, dir] in Iterator(modules.options["runtimepath"]))
935 context.fork(dir, 0, this, function (context) {
936 dir = dir.replace("/+$", "") + "/";
937 completion.file(context, true, dir + context.filter);
938 context.title[0] = dir;
939 context.keys.text = function (f) this.path.substr(dir.length);
943 completion.shellCommand = function shellCommand(context) {
944 context.title = ["Shell Command", "Path"];
945 context.generate = function () {
946 let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
949 for (let [, dirName] in Iterator(dirNames)) {
950 let dir = io.File(dirName);
951 if (dir.exists() && dir.isDirectory())
952 commands.push([[file.leafName, dir.path] for (file in iter(dir.directoryEntries))
953 if (file.isFile() && file.isExecutable())]);
956 return array.flatten(commands);
960 completion.addUrlCompleter("f", "Local files", function (context, full) {
961 let match = util.regexp(<![CDATA[
965 (?P<scheme> chrome|resource)
970 (?P<path> \/[^\/]* )?
972 ]]>, "x").exec(context.filter);
975 context.key = match.proto;
976 context.advance(match.proto.length);
977 context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]);
979 else if (match.scheme === "chrome") {
980 context.key = match.prefix;
981 context.advance(match.prefix.length + 1);
982 context.generate = function () iter({
983 content: /*L*/"Chrome content",
984 locale: /*L*/"Locale-specific content",
985 skin: /*L*/"Theme-specific content"
989 if (!match || match.scheme === "resource" && match.path)
990 if (/^(\.{0,2}|~)\/|^file:/.test(context.filter)
991 || util.OS.isWindows && /^[a-z]:/i.test(context.filter)
992 || util.getFile(context.filter)
993 || io.isJarURL(context.filter))
994 completion.file(context, full);
997 javascript: function (dactyl, modules, window) {
998 modules.JavaScript.setCompleter([File, File.expandPath],
999 [function (context, obj, args) {
1000 context.quote[2] = "";
1001 modules.completion.file(context, true);
1005 modes: function initModes(dactyl, modules, window) {
1006 initModes.require("commandline");
1007 const { modes } = modules;
1009 modes.addMode("FILE_INPUT", {
1011 description: "Active when selecting a file",
1012 bases: [modes.COMMAND_LINE],
1016 options: function (dactyl, modules, window) {
1017 const { completion, options } = modules;
1019 var shell, shellcmdflag;
1020 if (util.OS.isWindows) {
1022 shellcmdflag = "/c";
1025 shell = services.environment.get("SHELL") || "sh";
1026 shellcmdflag = "-c";
1029 options.add(["banghist", "bh"],
1030 "Replace occurrences of ! with the previous command when executing external commands",
1033 options.add(["fileencoding", "fenc"],
1034 "The character encoding used when reading and writing files",
1035 "string", "UTF-8", {
1036 completer: function (context) completion.charset(context),
1037 getter: function () File.defaultEncoding,
1038 setter: function (value) (File.defaultEncoding = value)
1040 options.add(["cdpath", "cd"],
1041 "List of directories searched when executing :cd",
1042 "stringlist", ["."].concat(services.environment.get("CDPATH").split(/[:;]/).filter(util.identity)).join(","),
1044 get files() this.value.map(function (path) File(path, modules.io.cwd))
1045 .filter(function (dir) dir.exists()),
1046 setter: function (value) File.expandPathList(value)
1049 options.add(["runtimepath", "rtp"],
1050 "List of directories searched for runtime files",
1051 "stringlist", IO.runtimePath,
1053 get files() this.value.map(function (path) File(path, modules.io.cwd))
1054 .filter(function (dir) dir.exists())
1057 options.add(["shell", "sh"],
1058 "Shell to use for executing external commands with :! and :run",
1060 { validator: function (val) io.pathSearch(val) });
1062 options.add(["shellcmdflag", "shcf"],
1063 "Flag passed to shell when executing external commands with :! and :run",
1064 "string", shellcmdflag,
1066 getter: function (value) {
1067 if (this.hasChanged || !util.OS.isWindows)
1069 return /sh/.test(options["shell"]) ? "-c" : "/c";
1072 options["shell"]; // Make sure it's loaded into global storage.
1073 options["shellcmdflag"];
1075 options.add(["wildignore", "wig"],
1076 "List of path name patterns to ignore when completing files and directories",
1083 } catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1085 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: