// Copyright (c) 2006-2009 by Martin Stubenschrott // Some code based on Venkman // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. /** @scope modules */ plugins.contexts = {}; const Script = Class("Script", { init: function (file) { let self = plugins.contexts[file.path]; if (self) { if (self.onUnload) self.onUnload(); return self; } plugins.contexts[file.path] = this; this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase()); this.PATH = file.path; this.toString = this.toString; this.__context__ = this; this.__proto__ = plugins; // This belongs elsewhere for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) { if (dir.contains(file, false)) plugins[this.NAME] = this; } return this; } }); /** * @class File A class to wrap nsIFile objects and simplify operations * thereon. * * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} * @param {boolean} checkPWD Whether to allow expansion relative to the * current directory. @default true */ const File = Class("File", { init: function (path, checkPWD) { if (arguments.length < 2) checkPWD = true; let file = services.create("file"); if (path instanceof Ci.nsIFile) file = path; else if (/file:\/\//.test(path)) file = services.create("file:").getFileFromURLSpec(path); else { let expandedPath = File.expandPath(path); if (!File.isAbsolutePath(expandedPath) && checkPWD) file = File.joinPaths(io.getCurrentDirectory().path, expandedPath); else file.initWithPath(expandedPath); } let self = XPCSafeJSObjectWrapper(file); self.__proto__ = File.prototype; return self; }, /** * Iterates over the objects in this directory. */ iterDirectory: function () { if (!this.isDirectory()) throw Error("Not a directory"); let entries = this.directoryEntries; while (entries.hasMoreElements()) yield File(entries.getNext().QueryInterface(Ci.nsIFile)); }, /** * Returns the list of files in this directory. * * @param {boolean} sort Whether to sort the returned directory * entries. * @returns {nsIFile[]} */ readDirectory: function (sort) { if (!this.isDirectory()) throw Error("Not a directory"); let array = [e for (e in this.iterDirectory())]; if (sort) array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); return array; }, /** * Reads this file's entire contents in "text" mode and returns the * content as a string. * * @param {string} encoding The encoding from which to decode the file. * @default options["fileencoding"] * @returns {string} */ read: function (encoding) { let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); if (!encoding) encoding = options["fileencoding"]; ifstream.init(this, -1, 0, 0); icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering let buffer = []; let str = {}; while (icstream.readString(4096, str) != 0) buffer.push(str.value); icstream.close(); ifstream.close(); return buffer.join(""); }, /** * Writes the string buf to this file. * * @param {string} buf The file content. * @param {string|number} mode The file access mode, a bitwise OR of * the following flags: * {@link #MODE_RDONLY}: 0x01 * {@link #MODE_WRONLY}: 0x02 * {@link #MODE_RDWR}: 0x04 * {@link #MODE_CREATE}: 0x08 * {@link #MODE_APPEND}: 0x10 * {@link #MODE_TRUNCATE}: 0x20 * {@link #MODE_SYNC}: 0x40 * Alternatively, the following abbreviations may be used: * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} * @default ">" * @param {number} perms The file mode bits of the created file. This * is only used when creating a new file and does not change * permissions if the file exists. * @default 0644 * @param {string} encoding The encoding to used to write the file. * @default options["fileencoding"] */ write: function (buf, mode, perms, encoding) { let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); function getStream(defaultChar) { let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); stream.init(ofstream, encoding, 0, defaultChar); return stream; } if (!encoding) encoding = options["fileencoding"]; if (mode == ">>") mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND; else if (!mode || mode == ">") mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; if (!perms) perms = 0644; ofstream.init(this, mode, perms, 0); let ocstream = getStream(0); try { ocstream.writeString(buf); } catch (e) { liberator.dump(e); if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { ocstream = getStream("?".charCodeAt(0)); ocstream.writeString(buf); return false; } else throw e; } finally { try { ocstream.close(); } catch (e) {} ofstream.close(); } return true; } }, { /** * @property {number} Open for reading only. * @final */ MODE_RDONLY: 0x01, /** * @property {number} Open for writing only. * @final */ MODE_WRONLY: 0x02, /** * @property {number} Open for reading and writing. * @final */ MODE_RDWR: 0x04, /** * @property {number} If the file does not exist, the file is created. * If the file exists, this flag has no effect. * @final */ MODE_CREATE: 0x08, /** * @property {number} The file pointer is set to the end of the file * prior to each write. * @final */ MODE_APPEND: 0x10, /** * @property {number} If the file exists, its length is truncated to 0. * @final */ MODE_TRUNCATE: 0x20, /** * @property {number} If set, each write will wait for both the file * data and file status to be physically updated. * @final */ MODE_SYNC: 0x40, /** * @property {number} With MODE_CREATE, if the file does not exist, the * file is created. If the file already exists, no action and NULL * is returned. * @final */ MODE_EXCL: 0x80, expandPathList: function (list) list.map(this.expandPath), expandPath: function (path, relative) { // expand any $ENV vars - this is naive but so is Vim and we like to be compatible // TODO: Vim does not expand variables set to an empty string (and documents it). // Kris reckons we shouldn't replicate this 'bug'. --djk // TODO: should we be doing this for all paths? function expand(path) path.replace( !liberator.has("Win32") ? /\$(\w+)\b|\${(\w+)}/g : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g, function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m ); path = expand(path); // expand ~ // Yuck. if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(IO.PATH_SEP) + "])").test(path)) { // Try $HOME first, on all systems let home = services.get("environment").get("HOME"); // Windows has its own idiosyncratic $HOME variables. if (!home && liberator.has("Win32")) home = services.get("environment").get("USERPROFILE") || services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH"); path = home + path.substr(1); } // TODO: Vim expands paths twice, once before checking for ~, once // after, but doesn't document it. Is this just a bug? --Kris path = expand(path); return path.replace("/", IO.PATH_SEP, "g"); }, getPathsFromPathList: function (list) { if (!list) return []; // empty list item means the current directory return list.replace(/,$/, "").split(",") .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); }, replacePathSep: function (path) path.replace("/", IO.PATH_SEP, "g"), joinPaths: function (head, tail) { let path = this(head); try { path.appendRelativePath(this.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators // TODO: This code breaks the external editor at least in ubuntu // because /usr/bin/gvim becomes /usr/bin/vim.gnome normalized and for // some strange reason it will start without a gui then (which is not // optimal if you don't start firefox from a terminal ;) // Why do we need this code? // if (path.exists() && path.normalize) // path.normalize(); } catch (e) { return { exists: function () false, __noSuchMethod__: function () { throw e; } }; } return path; }, isAbsolutePath: function (path) { try { services.create("file").initWithPath(path); return true; } catch (e) { return false; } } }); // TODO: why are we passing around strings rather than file objects? /** * Provides a basic interface to common system I/O operations. * @instance io */ const IO = Module("io", { requires: ["config", "services"], init: function () { this._processDir = services.get("directory").get("CurWorkD", Ci.nsIFile); this._cwd = this._processDir; this._oldcwd = null; this._lastRunCommand = ""; // updated whenever the users runs a command with :! this._scriptNames = []; this.downloadListener = { onDownloadStateChange: function (state, download) { if (download.state == services.get("downloadManager").DOWNLOAD_FINISHED) { let url = download.source.spec; let title = download.displayName; let file = download.targetFile.path; let size = download.size; liberator.echomsg("Download of " + title + " to " + file + " finished", 1); autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size }); } }, onStateChange: function () {}, onProgressChange: function () {}, onSecurityChange: function () {} }; services.get("downloadManager").addListener(this.downloadListener); }, destroy: function () { services.get("downloadManager").removeListener(this.downloadListener); for (let [, plugin] in Iterator(plugins.contexts)) if (plugin.onUnload) plugin.onUnload(); }, /** * @property {function} File class. * @final */ File: File, /** * @property {Object} The current file sourcing context. As a file is * being sourced the 'file' and 'line' properties of this context * object are updated appropriately. */ sourcing: null, /** * Expands "~" and environment variables in path. * * "~" is expanded to to the value of $HOME. On Windows if this is not * set then the following are tried in order: * $USERPROFILE * ${HOMDRIVE}$HOMEPATH * * The variable notation is $VAR (terminated by a non-word character) * or ${VAR}. %VAR% is also supported on Windows. * * @param {string} path The unexpanded path string. * @param {boolean} relative Whether the path is relative or absolute. * @returns {string} */ expandPath: File.expandPath, // TODO: there seems to be no way, short of a new component, to change // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953 /** * Returns the current working directory. * * It's not possible to change the real CWD of the process so this * state is maintained internally. External commands run via * {@link #system} are executed in this directory. * * @returns {nsIFile} */ getCurrentDirectory: function () { let dir = File(this._cwd.path); // NOTE: the directory could have been deleted underneath us so // fallback to the process's CWD if (dir.exists() && dir.isDirectory()) return dir; else return this._processDir; }, /** * Sets the current working directory. * * @param {string} newDir The new CWD. This may be a relative or * absolute path and is expanded by {@link #expandPath}. */ setCurrentDirectory: function (newDir) { newDir = newDir || "~"; if (newDir == "-") [this._cwd, this._oldcwd] = [this._oldcwd, this.getCurrentDirectory()]; else { let dir = File(newDir); if (!dir.exists() || !dir.isDirectory()) { liberator.echoerr("E344: Can't find directory \"" + dir.path + "\" in path"); return null; } [this._cwd, this._oldcwd] = [dir, this.getCurrentDirectory()]; } return self.getCurrentDirectory(); }, /** * Returns all directories named name in 'runtimepath'. * * @param {string} name * @returns {nsIFile[]) */ getRuntimeDirectories: function (name) { let dirs = File.getPathsFromPathList(options["runtimepath"]); dirs = dirs.map(function (dir) File.joinPaths(dir, name)) .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable()); return dirs; }, /** * Returns the first user RC file found in dir. * * @param {string} dir The directory to search. * @param {boolean} always When true, return a path whether * the file exists or not. * @default $HOME. * @returns {nsIFile} The RC file or null if none is found. */ getRCFile: function (dir, always) { dir = dir || "~"; let rcFile1 = File.joinPaths(dir, "." + config.name.toLowerCase() + "rc"); let rcFile2 = File.joinPaths(dir, "_" + config.name.toLowerCase() + "rc"); if (liberator.has("Win32")) [rcFile1, rcFile2] = [rcFile2, rcFile1]; if (rcFile1.exists() && rcFile1.isFile()) return rcFile1; else if (rcFile2.exists() && rcFile2.isFile()) return rcFile2; else if (always) return rcFile1; return null; }, // TODO: make secure /** * Creates a temporary file. * * @returns {File} */ createTempFile: function () { let file = services.get("directory").get("TmpD", Ci.nsIFile); file.append(config.tempFile); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600); return File(file); }, /** * Runs an external program. * * @param {string} program The program to run. * @param {string[]} args An array of arguments to pass to program. * @param {boolean} blocking Whether to wait until the process terminates. */ blockingProcesses: [], run: function (program, args, blocking) { args = args || []; blocking = !!blocking; let file; if (File.isAbsolutePath(program)) file = File(program, true); else { let dirs = services.get("environment").get("PATH").split(liberator.has("Win32") ? ";" : ":"); // Windows tries the CWD first TODO: desirable? if (liberator.has("Win32")) dirs = [io.getCurrentDirectory().path].concat(dirs); lookup: for (let [, dir] in Iterator(dirs)) { file = File.joinPaths(dir, program); try { if (file.exists()) break; // TODO: couldn't we just palm this off to the start command? // automatically try to add the executable path extensions on windows if (liberator.has("Win32")) { let extensions = services.get("environment").get("PATHEXT").split(";"); for (let [, extension] in Iterator(extensions)) { file = File.joinPaths(dir, program + extension); if (file.exists()) break lookup; } } } catch (e) {} } } if (!file || !file.exists()) { liberator.echoerr("Command not found: " + program); return -1; } let process = services.create("process"); process.init(file); process.run(blocking, args.map(String), args.length); return process.exitValue; }, // FIXME: multiple paths? /** * Sources files found in 'runtimepath'. For each relative path in * paths each directory in 'runtimepath' is searched and if a * matching file is found it is sourced. Only the first file found (per * specified path) is sourced unless all is specified, then * all found files are sourced. * * @param {string[]} paths An array of relative paths to source. * @param {boolean} all Whether all found files should be sourced. */ sourceFromRuntimePath: function (paths, all) { let dirs = File.getPathsFromPathList(options["runtimepath"]); let found = false; liberator.echomsg("Searching for \"" + paths.join(" ") + "\" in \"" + options["runtimepath"] + "\"", 2); outer: for (let [, dir] in Iterator(dirs)) { for (let [, path] in Iterator(paths)) { let file = File.joinPaths(dir, path); liberator.echomsg("Searching for \"" + file.path + "\"", 3); if (file.exists() && file.isFile() && file.isReadable()) { io.source(file.path, false); found = true; if (!all) break outer; } } } if (!found) liberator.echomsg("not found in 'runtimepath': \"" + paths.join(" ") + "\"", 1); return found; }, /** * Reads Ex commands, JavaScript or CSS from filename. * * @param {string} filename The name of the file to source. * @param {boolean} silent Whether errors should be reported. */ source: function (filename, silent) { let wasSourcing = this.sourcing; try { var file = File(filename); this.sourcing = { file: file.path, line: 0 }; if (!file.exists() || !file.isReadable() || file.isDirectory()) { if (!silent) { if (file.exists() && file.isDirectory()) liberator.echomsg("Cannot source a directory: \"" + filename + "\"", 0); else liberator.echomsg("could not source: \"" + filename + "\"", 1); liberator.echoerr("E484: Can't open file " + filename); } return; } liberator.echomsg("sourcing \"" + filename + "\"", 2); let str = file.read(); let uri = services.get("io").newFileURI(file); // handle pure JavaScript files specially if (/\.js$/.test(filename)) { try { liberator.loadScript(uri.spec, Script(file)); if (liberator.initialized) liberator.initHelp(); } catch (e) { let err = new Error(); for (let [k, v] in Iterator(e)) err[k] = v; err.echoerr = <>{file.path}:{e.lineNumber}: {e}; throw err; } } else if (/\.css$/.test(filename)) storage.styles.registerSheet(uri.spec, false, true); else { let heredoc = ""; let heredocEnd = null; // the string which ends the heredoc let lines = str.split(/\r\n|[\r\n]/); function execute(args) { command.execute(args, special, count, { setFrom: file }); } for (let [i, line] in Iterator(lines)) { if (heredocEnd) { // we already are in a heredoc if (heredocEnd.test(line)) { execute(heredoc); heredoc = ""; heredocEnd = null; } else heredoc += line + "\n"; } else { this.sourcing.line = i + 1; // skip line comments and blank lines line = line.replace(/\r$/, ""); if (/^\s*(".*)?$/.test(line)) continue; var [count, cmd, special, args] = commands.parseCommand(line); var command = commands.get(cmd); if (!command) { let lineNumber = i + 1; liberator.echoerr("Error detected while processing " + file.path, commandline.FORCE_MULTILINE); commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR, commandline.APPEND_TO_MESSAGES); liberator.echoerr("E492: Not an editor command: " + line); } else { if (command.name == "finish") break; else if (command.hereDoc) { // check for a heredoc let matches = args.match(/(.*)<<\s*(\S+)$/); if (matches) { args = matches[1]; heredocEnd = RegExp("^" + matches[2] + "$", "m"); if (matches[1]) heredoc = matches[1] + "\n"; continue; } } execute(args); } } } // if no heredoc-end delimiter is found before EOF then // process the heredoc anyway - Vim compatible ;-) if (heredocEnd) execute(heredoc); } if (this._scriptNames.indexOf(file.path) == -1) this._scriptNames.push(file.path); liberator.echomsg("finished sourcing \"" + filename + "\"", 2); liberator.log("Sourced: " + filename, 3); } catch (e) { liberator.reportError(e); let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e); if (!silent) liberator.echoerr(message); } finally { this.sourcing = wasSourcing; } }, // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is // fixed use that instead of a tmpfile /** * Runs command in a subshell and returns the output in a * string. The shell used is that specified by the 'shell' option. * * @param {string} command The command to run. * @param {string} input Any input to be provided to the command on stdin. * @returns {string} */ system: function (command, input) { liberator.echomsg("Calling shell to execute: " + command, 4); function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"'; return this.withTempFiles(function (stdin, stdout, cmd) { if (input) stdin.write(input); // TODO: implement 'shellredir' if (liberator.has("Win32")) { command = "cd /D " + this._cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path; var res = this.run(options["shell"], options["shellcmdflag"].split(/\s+/).concat(command), true); } else { cmd.write("cd " + escape(this._cwd.path) + "\n" + ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path), escape(options["shell"]), options["shellcmdflag"], escape(command)].join(" ")); res = this.run("/bin/sh", ["-e", cmd.path], true); } let output = stdout.read(); if (res > 0) output += "\nshell returned " + res; // if there is only one \n at the end, chop it off else if (output && output.indexOf("\n") == output.length - 1) output = output.substr(0, output.length - 1); return output; }) || ""; }, /** * Creates a temporary file context for executing external commands. * func is called with a temp file, created with * {@link #createTempFile}, for each explicit argument. Ensures that * all files are removed when func returns. * * @param {function} func The function to execute. * @param {Object} self The 'this' object used when executing func. * @returns {boolean} false if temp files couldn't be created, * otherwise, the return value of func. */ withTempFiles: function (func, self) { let args = util.map(util.range(0, func.length), this.createTempFile); if (!args.every(util.identity)) return false; try { return func.apply(self || this, args); } finally { args.forEach(function (f) f.remove(false)); } } }, { /** * @property {string} The value of the $VIMPERATOR_RUNTIME environment * variable. */ get runtimePath() { const rtpvar = config.name.toUpperCase() + "_RUNTIME"; let rtp = services.get("environment").get(rtpvar); if (!rtp) { rtp = "~/" + (liberator.has("Win32") ? "" : ".") + config.name.toLowerCase(); services.get("environment").set(rtpvar, rtp); } return rtp; }, /** * @property {string} The current platform's path seperator. */ get PATH_SEP() { delete this.PATH_SEP; let f = services.get("directory").get("CurProcD", Ci.nsIFile); f.append("foo"); return this.PATH_SEP = f.path.substr(f.parent.path.length, 1); } }, { commands: function () { commands.add(["cd", "chd[ir]"], "Change the current directory", function (args) { let arg = args.literalArg; if (!arg) arg = "~"; else if (arg == "-") { liberator.assert(io._oldcwd, "E186: No previous directory"); arg = io._oldcwd.path; } arg = File.expandPath(arg); // go directly to an absolute path or look for a relative path // match in 'cdpath' // TODO: handle ../ and ./ paths if (File.isAbsolutePath(arg)) { if (io.setCurrentDirectory(arg)) liberator.echomsg(io.getCurrentDirectory().path); } else { let dirs = File.getPathsFromPathList(options["cdpath"]); let found = false; for (let [, dir] in Iterator(dirs)) { dir = File.joinPaths(dir, arg); if (dir.exists() && dir.isDirectory() && dir.isReadable()) { io.setCurrentDirectory(dir.path); liberator.echomsg(io.getCurrentDirectory().path); found = true; break; } } if (!found) { liberator.echoerr("E344: Can't find directory " + arg.quote() + " in cdpath\n" + "E472: Command failed"); } } }, { argCount: "?", completer: function (context) completion.directory(context, true), literal: 0 }); // NOTE: this command is only used in :source commands.add(["fini[sh]"], "Stop sourcing a script file", function () { liberator.echoerr("E168: :finish used outside of a sourced file"); }, { argCount: "0" }); commands.add(["pw[d]"], "Print the current directory name", function () { liberator.echomsg(io.getCurrentDirectory().path); }, { argCount: "0" }); // "mkv[imperatorrc]" or "mkm[uttatorrc]" commands.add([config.name.toLowerCase().replace(/(.)(.*)/, "mk$1[$2rc]")], "Write current key mappings and changed options to the config file", function (args) { liberator.assert(args.length <= 1, "E172: Only one file name allowed"); let filename = args[0] || io.getRCFile(null, true).path; let file = File(filename); liberator.assert(!file.exists() || args.bang, "E189: \"" + filename + "\" exists (add ! to override)"); // TODO: Use a set/specifiable list here: let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)]; lines = util.Array.flatten(lines); // source a user .vimperatorrc file lines.unshift('"' + liberator.version + "\n"); // For the record, I think that adding this line is absurd. --Kris // I can't disagree. --djk lines.push(commands.commandToString({ command: "source", bang: true, arguments: [filename + ".local"] })); lines.push("\n\" vim: set ft=" + config.name.toLowerCase() + ":"); try { file.write(lines.join("\n")); } catch (e) { liberator.echoerr("E190: Cannot open \"" + filename + "\" for writing"); liberator.log("Could not write to " + file.path + ": " + e.message); // XXX } }, { argCount: "*", // FIXME: should be "?" but kludged for proper error message bang: true, completer: function (context) completion.file(context, true) }); commands.add(["runt[ime]"], "Source the specified file from each directory in 'runtimepath'", function (args) { io.sourceFromRuntimePath(args, args.bang); }, { argCount: "+", bang: true } ); commands.add(["scrip[tnames]"], "List all sourced script names", function () { let list = template.tabular(["", "Filename"], ["text-align: right; padding-right: 1em;"], ([i + 1, file] for ([i, file] in Iterator(this._scriptNames)))); // TODO: add colon and remove column titles for pedantic Vim compatibility? commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, { argCount: "0" }); commands.add(["so[urce]"], "Read Ex commands from a file", function (args) { if (args.length > 1) liberator.echoerr("E172: Only one file name allowed"); else io.source(args[0], args.bang); }, { argCount: "+", // FIXME: should be "1" but kludged for proper error message bang: true, completer: function (context) completion.file(context, true) }); commands.add(["!", "run"], "Run a command", function (args) { let arg = args.literalArg; // :!! needs to be treated specially as the command parser sets the // bang flag but removes the ! from arg if (args.bang) arg = "!" + arg; // replaceable bang and no previous command? liberator.assert(!/((^|[^\\])(\\\\)*)!/.test(arg) || this._lastRunCommand, "E34: No previous command"); // NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable? // pass through a raw bang when escaped or substitute the last command arg = arg.replace(/(\\)*!/g, function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", this._lastRunCommand) ); this._lastRunCommand = arg; let output = io.system(arg); commandline.command = "!" + arg; commandline.echo(template.commandOutput({output})); autocommands.trigger("ShellCmdPost", {}); }, { argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk bang: true, completer: function (context) completion.shellCommand(context), literal: 0 }); }, completion: function () { JavaScript.setCompleter([this.File, File.expandPath], [function (context, obj, args) { context.quote[2] = ""; completion.file(context, true); }]); completion.charset = function (context) { context.anchored = false; context.generate = function () { let names = util.Array( "more1 more2 more3 more4 more5 unicode".split(" ").map(function (key) options.getPref("intl.charsetmenu.browser." + key).split(', ')) ).flatten().uniq(); let bundle = document.getElementById("liberator-charset-bundle"); return names.map(function (name) [name, bundle.getString(name.toLowerCase() + ".title")]); }; }; completion.directory = function directory(context, full) { this.file(context, full); context.filters.push(function ({ item: f }) f.isDirectory()); }; completion.environment = function environment(context) { let command = liberator.has("Win32") ? "set" : "env"; let lines = io.system(command).split("\n"); lines.pop(); context.title = ["Environment Variable", "Value"]; context.generate = function () lines.map(function (line) (line.match(/([^=]+)=(.+)/) || []).slice(1)); }; // TODO: support file:// and \ or / path separators on both platforms // if "tail" is true, only return names without any directory components completion.file = function file(context, full) { // dir == "" is expanded inside readDirectory to the current dir let [dir] = context.filter.match(/^(?:.*[\/\\])?/); if (!full) context.advance(dir.length); context.title = [full ? "Path" : "Filename", "Type"]; context.keys = { text: !full ? "leafName" : function (f) dir + f.leafName, description: function (f) f.isDirectory() ? "Directory" : "File", isdir: function (f) f.isDirectory(), icon: function (f) f.isDirectory() ? "resource://gre/res/html/folder.png" : "moz-icon://" + f.leafName }; context.compare = function (a, b) b.isdir - a.isdir || String.localeCompare(a.text, b.text); if (options["wildignore"]) { let wig = options.get("wildignore"); context.filters.push(function ({item: f}) f.isDirectory() || !wig.getKey(this.name)); } // context.background = true; context.key = dir; context.generate = function generate_file() { try { return File(dir).readDirectory(); } catch (e) {} return []; }; }; completion.shellCommand = function shellCommand(context) { context.title = ["Shell Command", "Path"]; context.generate = function () { let dirNames = services.get("environment").get("PATH").split(RegExp(liberator.has("Win32") ? ";" : ":")); let commands = []; for (let [, dirName] in Iterator(dirNames)) { let dir = io.File(dirName); if (dir.exists() && dir.isDirectory()) { commands.push([[file.leafName, dir.path] for (file in dir.iterDirectory()) if (file.isFile() && file.isExecutable())]); } } return util.Array.flatten(commands); }; }; completion.addUrlCompleter("f", "Local files", completion.file); }, options: function () { var shell, shellcmdflag; if (liberator.has("Win32")) { shell = "cmd.exe"; // TODO: setting 'shell' to "something containing sh" updates // 'shellcmdflag' appropriately at startup on Windows in Vim shellcmdflag = "/c"; } else { shell = services.get("environment").get("SHELL") || "sh"; shellcmdflag = "-c"; } options.add(["fileencoding", "fenc"], "Sets the character encoding of read and written files", "string", "UTF-8", { completer: function (context) completion.charset(context) }); options.add(["cdpath", "cd"], "List of directories searched when executing :cd", "stringlist", "," + (services.get("environment").get("CDPATH").replace(/[:;]/g, ",") || ","), { setter: function (value) File.expandPathList(value) }); options.add(["runtimepath", "rtp"], "List of directories searched for runtime files", "stringlist", IO.runtimePath, { setter: function (value) File.expandPathList(value) }); options.add(["shell", "sh"], "Shell to use for executing :! and :run commands", "string", shell, { setter: function (value) File.expandPath(value) }); options.add(["shellcmdflag", "shcf"], "Flag passed to shell when executing :! and :run commands", "string", shellcmdflag); options.add(["wildignore", "wig"], "List of file patterns to ignore when completing files", "regexlist", ""); } }); // vim: set fdm=marker sw=4 ts=4 et: