From ec8d7686fca3f70ee85cfe99cd55dab5a1ba024f Mon Sep 17 00:00:00 2001 From: Doug Kearns Date: Thu, 18 Jun 2009 20:46:09 +1000 Subject: [PATCH] Move the standard type completers to appropriate modules. --- common/content/bookmarks.js | 104 +++++++ common/content/buffer.js | 68 +++++ common/content/commands.js | 75 ++++- common/content/completion.js | 548 ----------------------------------- common/content/editor.js | 17 ++ common/content/events.js | 15 +- common/content/io.js | 94 +++++- common/content/liberator.js | 161 ++++++---- common/content/mappings.js | 17 ++ common/content/options.js | 71 ++++- common/content/style.js | 48 ++- common/content/template.js | 4 +- vimperator/content/config.js | 44 +++ xulmus/content/player.js | 39 +++ 14 files changed, 680 insertions(+), 625 deletions(-) diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index effa9663..657e9c06 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -399,6 +399,93 @@ function Bookmarks() //{{{ literal: 0 }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + completion.bookmark = function bookmark(context, tags, extra) { + context.title = ["Bookmark", "Title"]; + context.format = bookmarks.format; + for (let val in Iterator(extra || [])) + { + let [k, v] = val; // Need block scope here for the closure + if (v) + context.filters.push(function (item) this._match(v, item[k])); + } + // Need to make a copy because set completions() checks instanceof Array, + // and this may be an Array from another window. + context.completions = Array.slice(storage["bookmark-cache"].bookmarks); + completion.urls(context, tags); + }; + + completion.search = function search(context, noSuggest) { + let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); + let keywords = bookmarks.getKeywords(); + let engines = bookmarks.getSearchEngines(); + + context.title = ["Search Keywords"]; + context.completions = keywords.concat(engines); + context.keys = { text: 0, description: 1, icon: 2 }; + + if (!space || noSuggest) + return; + + context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", + keyword, true); + + let item = keywords.filter(function (k) k.keyword == keyword)[0]; + if (item && item.url.indexOf("%s") > -1) + context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) { + context.format = history.format; + context.title = [keyword + " Quick Search"]; + // context.background = true; + context.compare = CompletionContext.Sort.unsorted; + context.generate = function () { + let [begin, end] = item.url.split("%s"); + + return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) { + let rest = item.url.length - end.length; + let query = item.url.substring(begin.length, rest); + if (item.url.substr(rest) == end && query.indexOf("&") == -1) + { + item.url = decodeURIComponent(query); + return item; + } + }).filter(util.identity); + }; + }); + }; + + completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) { + if (!context.filter) + return; + + let engineList = (engineAliases || options["suggestengines"] || "google").split(","); + + let completions = []; + engineList.forEach(function (name) { + let engine = services.get("browserSearch").getEngineByAlias(name); + if (!engine) + return; + let [,word] = /^\s*(\S+)/.exec(context.filter) || []; + if (!kludge && word == name) // FIXME: Check for matching keywords + return; + let ctxt = context.fork(name, 0); + + ctxt.title = [engine.description + " Suggestions"]; + ctxt.compare = CompletionContext.Sort.unsorted; + ctxt.incomplete = true; + bookmarks.getSuggestions(name, ctxt.filter, function (compl) { + ctxt.incomplete = false; + ctxt.completions = compl; + }); + }); + }; + + completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest); + completion.addUrlCompleter("b", "Bookmarks", completion.bookmark); + completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search); + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ @@ -840,6 +927,23 @@ function History() //{{{ options: [[["-max", "-m"], options.OPTION_INT]] }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + completion.history = function _history(context, maxItems) { + context.format = history.format; + context.title = ["History"] + context.compare = CompletionContext.Sort.unsorted; + //context.background = true; + if (context.maxItems == null) + context.maxItems = 100; + context.regenerate = true; + context.generate = function () history.get(context.filter, this.maxItems); + }; + + completion.addUrlCompleter("h", "History", completion.history); + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ diff --git a/common/content/buffer.js b/common/content/buffer.js index 25f1df71..63321fbb 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -673,6 +673,61 @@ function Buffer() //{{{ bang: true }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + liberator.registerObserver("load_completion", function () { + completion.alternateStyleSheet = function alternateStylesheet(context) { + context.title = ["Stylesheet", "Location"]; + + // unify split style sheets + let styles = {}; + + buffer.alternateStyleSheets.forEach(function (style) { + if (!(style.title in styles)) + styles[style.title] = []; + + styles[style.title].push(style.href || "inline"); + }); + + context.completions = [[s, styles[s].join(", ")] for (s in styles)]; + }; + + completion.buffer = function buffer(context) { + filter = context.filter.toLowerCase(); + context.anchored = false; + context.title = ["Buffer", "URL"]; + context.keys = { text: "text", description: "url", icon: "icon" }; + context.compare = CompletionContext.Sort.number; + let process = context.process[0]; + context.process = [function (item, text) + <> + {item.item.indicator} + { process.call(this, item, text) } + ]; + + context.completions = util.map(tabs.browsers, function ([i, browser]) { + let indicator = " "; + if (i == tabs.index()) + indicator = "%" + else if (i == tabs.index(tabs.alternate)) + indicator = "#"; + + let tab = tabs.getTab(i); + let url = browser.contentDocument.location.href; + i = i + 1; + + return { + text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url], + url: template.highlightURL(url), + indicator: indicator, + icon: tab.image || DEFAULT_FAVICON + }; + }); + }; + }); + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PAGE INFO /////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ @@ -1769,6 +1824,19 @@ function Marks() //{{{ marks.list(filter); }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + completion.mark = function mark(context) { + function percent(i) Math.round(i * 100); + + // FIXME: Line/Column doesn't make sense with % + context.title = ["Mark", "Line Column File"]; + context.keys.description = function ([,m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location; + context.completions = marks.all; + }; + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ diff --git a/common/content/commands.js b/common/content/commands.js index 5d7707b2..10554283 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -392,10 +392,6 @@ function Commands() //{{{ ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - liberator.registerObserver("load_completion", function () { - completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]); - }); - const self = { // FIXME: remove later, when our option handler is better @@ -1156,6 +1152,77 @@ function Commands() //{{{ completer: function (context) completion.userCommand(context) }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + liberator.registerObserver("load_completion", function () { + completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]); + + completion.command = function command(context) { + context.title = ["Command"]; + context.keys = { text: "longNames", description: "description" }; + context.completions = [k for (k in commands)]; + }; + + // provides completions for ex commands, including their arguments + completion.ex = function ex(context) { + // if there is no space between the command name and the cursor + // then get completions of the command name + let [count, cmd, bang, args] = commands.parseCommand(context.filter); + let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || []; + context.advance(prefix.length) + if (!junk) + return context.fork("", 0, this, "command"); + + // dynamically get completions as specified with the command's completer function + let command = commands.get(cmd); + if (!command) + { + context.highlight(0, cmd.length, "SPELLCHECK"); + return; + } + + [prefix] = context.filter.match(/^(?:\w*[\s!]|!)\s*/); + let cmdContext = context.fork(cmd, prefix.length); + let argContext = context.fork("args", prefix.length); + args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang }); + if (args) + { + // FIXME: Move to parseCommand + args.count = count; + args.bang = bang; + if (!args.completeOpt && command.completer) + { + cmdContext.advance(args.completeStart); + cmdContext.quote = args.quote; + cmdContext.filter = args.completeFilter; + try + { + let compObject = command.completer.call(command, cmdContext, args); + if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects + compObject = { start: compObject[0], items: compObject[1] }; + if (compObject != null) + { + cmdContext.advance(compObject.start); + cmdContext.filterFunc = null; + cmdContext.completions = compObject.items; + } + } + catch (e) + { + liberator.reportError(e); + } + } + } + }; + + completion.userCommand = function userCommand(context) { + context.title = ["User Command", "Definition"]; + context.keys = { text: "name", description: "replacementText" }; + context.completions = commands.getUserCommands(); + }; + }); //}}} return self; diff --git a/common/content/completion.js b/common/content/completion.js index e8308e44..0a3f796b 100644 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -1340,533 +1340,10 @@ function Completion() //{{{ ////////////////////// COMPLETION TYPES //////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk - abbreviation: function abbreviation(context, args, mode) - { - mode = mode || "!"; - - if (args.completeArg == 0) - { - let abbreviations = editor.getAbbreviations(mode); - context.completions = [[lhs, ""] for ([, [, lhs,]] in Iterator(abbreviations))]; - } - }, - - alternateStyleSheet: function alternateStylesheet(context) - { - context.title = ["Stylesheet", "Location"]; - - // unify split style sheets - let styles = {}; - - buffer.alternateStyleSheets.forEach(function (style) { - if (!(style.title in styles)) - styles[style.title] = []; - - styles[style.title].push(style.href || "inline"); - }); - - context.completions = [[s, styles[s].join(", ")] for (s in styles)]; - }, - - autocmdEvent: function autocmdEvent(context) - { - context.completions = config.autocommands; - }, - - // TODO: shouldn't these app-specific completers be moved to config.js? --djk - bookmark: function bookmark(context, tags, extra) - { - context.title = ["Bookmark", "Title"]; - context.format = bookmarks.format; - for (let val in Iterator(extra || [])) - { - let [k, v] = val; // Need block scope here for the closure - if (v) - context.filters.push(function (item) this._match(v, item[k])); - } - // Need to make a copy because set completions() checks instanceof Array, - // and this may be an Array from another window. - context.completions = Array.slice(storage["bookmark-cache"].bookmarks); - completion.urls(context, tags); - }, - - song: function song(context, args) - { - // TODO: useful descriptions? - function map(list) list.map(function (i) [i, ""]); - let [artist, album] = [args[0], args[1]]; - - if (args.completeArg == 0) - { - context.title = ["Artists"]; - context.completions = map(library.getArtists()); - } - else if (args.completeArg == 1) - { - context.title = ["Albums by " + artist]; - context.completions = map(library.getAlbums(artist)); - } - else if (args.completeArg == 2) - { - context.title = ["Tracks from " + album + " by " + artist]; - context.completions = map(library.getTracks(artist, album)); - } - }, - - playlist: function playlist(context, args) - { - context.title = ["Playlist", "Type"]; - context.keys = { text: "name", description: "type" }; - context.completions = player.getPlaylists(); - }, - - buffer: function buffer(context) - { - filter = context.filter.toLowerCase(); - context.anchored = false; - context.title = ["Buffer", "URL"]; - context.keys = { text: "text", description: "url", icon: "icon" }; - context.compare = CompletionContext.Sort.number; - let process = context.process[0]; - context.process = [function (item, text) - <> - {item.item.indicator} - { process.call(this, item, text) } - ]; - - context.completions = util.map(tabs.browsers, function ([i, browser]) { - let indicator = " "; - if (i == tabs.index()) - indicator = "%" - else if (i == tabs.index(tabs.alternate)) - indicator = "#"; - - let tab = tabs.getTab(i); - let url = browser.contentDocument.location.href; - i = i + 1; - - return { - text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url], - url: template.highlightURL(url), - indicator: indicator, - icon: tab.image || DEFAULT_FAVICON - }; - }); - }, - - 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')]); - }; - }, - - colorScheme: function colorScheme(context) - { - let colors = []; - - io.getRuntimeDirectories("colors").forEach(function (dir) { - io.readDirectory(dir).forEach(function (file) { - if (/\.vimp$/.test(file.leafName) && !colors.some(function (c) c.leafName == file.leafName)) - colors.push(file); - }); - }); - - context.title = ["Color Scheme", "Runtime Path"]; - context.completions = [[c.leafName.replace(/\.vimp$/, ""), c.parent.path] for ([,c] in Iterator(colors))] - }, - - command: function command(context) - { - context.title = ["Command"]; - context.keys = { text: "longNames", description: "description" }; - context.completions = [k for (k in commands)]; - }, - - dialog: function dialog(context) - { - context.title = ["Dialog"]; - context.completions = config.dialogs; - }, - - directory: function directory(context, full) - { - this.file(context, full); - context.filters.push(function ({ item: f }) f.isDirectory()); - }, - - 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)); - }, - - // provides completions for ex commands, including their arguments - ex: function ex(context) - { - // if there is no space between the command name and the cursor - // then get completions of the command name - let [count, cmd, bang, args] = commands.parseCommand(context.filter); - let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || []; - context.advance(prefix.length) - if (!junk) - return context.fork("", 0, this, "command"); - - // dynamically get completions as specified with the command's completer function - let command = commands.get(cmd); - if (!command) - { - context.highlight(0, cmd.length, "SPELLCHECK"); - return; - } - - [prefix] = context.filter.match(/^(?:\w*[\s!]|!)\s*/); - let cmdContext = context.fork(cmd, prefix.length); - let argContext = context.fork("args", prefix.length); - args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang }); - if (args) - { - // FIXME: Move to parseCommand - args.count = count; - args.bang = bang; - if (!args.completeOpt && command.completer) - { - cmdContext.advance(args.completeStart); - cmdContext.quote = args.quote; - cmdContext.filter = args.completeFilter; - try - { - let compObject = command.completer.call(command, cmdContext, args); - if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects - compObject = { start: compObject[0], items: compObject[1] }; - if (compObject != null) - { - cmdContext.advance(compObject.start); - cmdContext.filterFunc = null; - cmdContext.completions = compObject.items; - } - } - catch (e) - { - liberator.reportError(e); - } - } - } - }, - - // TODO: support file:// and \ or / path separators on both platforms - // if "tail" is true, only return names without any directory components - 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 wigRegexp = RegExp("(^" + options.get("wildignore").values.join("|") + ")$"); - context.filters.push(function ({item: f}) f.isDirectory() || !wigRegexp.test(f.leafName)); - } - - // context.background = true; - context.key = dir; - context.generate = function generate_file() - { - try - { - return io.readDirectory(dir); - } - catch (e) {} - }; - }, - - help: function help(context) - { - context.title = ["Help"]; - context.anchored = false; - context.generate = function () - { - let res = config.helpFiles.map(function (file) { - let resp = util.httpGet("chrome://liberator/locale/" + file); - if (!resp) - return []; - let doc = resp.responseXML; - return Array.map(doc.getElementsByClassName("tag"), - function (elem) [elem.textContent, file]); - }); - return util.Array.flatten(res); - } - }, - - // XXX - highlightGroup: function highlightGroup(context, args) commands.get("highlight").completer(context, args), - - history: function _history(context, maxItems) - { - context.format = history.format; - context.title = ["History"] - context.compare = CompletionContext.Sort.unsorted; - //context.background = true; - if (context.maxItems == null) - context.maxItems = 100; - context.regenerate = true; - context.generate = function () history.get(context.filter, this.maxItems); - }, - get javascriptCompleter() javascript, javascript: function _javascript(context) javascript.complete(context), - location: function location(context) - { - if (!services.get("autoCompleteSearch")) - return; - - context.anchored = false; - context.title = ["Smart Completions"]; - context.keys.icon = 2; - context.incomplete = true; - context.hasItems = context.completions.length > 0; // XXX - context.filterFunc = null; - context.cancel = function () services.get("autoCompleteSearch").stopSearch(); - context.compare = CompletionContext.Sort.unsorted; - let timer = new Timer(50, 100, function (result) { - context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING; - context.completions = [ - [result.getValueAt(i), result.getCommentAt(i), result.getImageAt(i)] - for (i in util.range(0, result.matchCount)) - ]; - }); - services.get("autoCompleteSearch").stopSearch(); - services.get("autoCompleteSearch").startSearch(context.filter, "", context.result, { - onSearchResult: function onSearchResult(search, result) - { - context.result = result; - timer.tell(result); - if (result.searchResult <= result.RESULT_SUCCESS) - timer.flush(); - } - }); - }, - - macro: function macro(context) - { - context.title = ["Macro", "Keys"]; - context.completions = [item for (item in events.getMacros())]; - }, - - mark: function mark(context) - { - function percent(i) Math.round(i * 100); - - // FIXME: Line/Column doesn't make sense with % - context.title = ["Mark", "Line Column File"]; - context.keys.description = function ([,m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location; - context.completions = marks.all; - }, - - mediaView: function mediaView(context) - { - context.title = ["Media View", "URL"]; - context.anchored = false; - context.keys = { text: "contentTitle", description: "contentUrl" }; - context.completions = player.getMediaPages(); - }, - - menuItem: function menuItem(context) - { - context.title = ["Menu Path", "Label"]; - context.anchored = false; - context.keys = { text: "fullMenuPath", description: function (item) item.getAttribute("label") }; - context.completions = liberator.menuItems; - }, - - option: function option(context, scope) - { - context.title = ["Option"]; - context.keys = { text: "names", description: "description" }; - context.completions = options; - if (scope) - context.filters.push(function ({ item: opt }) opt.scope & scope); - }, - - optionValue: function (context, name, op, curValue) - { - let opt = options.get(name); - let completer = opt.completer; - if (!completer) - return; - - let curValues = curValue != null ? opt.parseValues(curValue) : opt.values; - let newValues = opt.parseValues(context.filter); - - let len = context.filter.length; - switch (opt.type) - { - case "boolean": - if (!completer) - completer = function () [["true", ""], ["false", ""]]; - break; - case "stringlist": - let target = newValues.pop(); - len = target ? target.length : 0; - break; - case "charlist": - len = 0; - break; - } - // TODO: Highlight when invalid - context.advance(context.filter.length - len); - - context.title = ["Option Value"]; - let completions = completer(context); - if (!completions) - return; - // Not Vim compatible, but is a significant enough improvement - // that it's worth breaking compatibility. - if (newValues instanceof Array) - { - completions = completions.filter(function (val) newValues.indexOf(val[0]) == -1); - switch (op) - { - case "+": - completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1); - break; - case "-": - completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1); - break; - } - } - context.completions = completions; - }, - - preference: function preference(context) - { - context.anchored = false; - context.title = ["Firefox Preference", "Value"]; - context.keys = { text: function (item) item, description: function (item) options.getPref(item) }; - context.completions = services.get("pref").getChildList("", { value: 0 }); - }, - - search: function search(context, noSuggest) - { - let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); - let keywords = bookmarks.getKeywords(); - let engines = bookmarks.getSearchEngines(); - - context.title = ["Search Keywords"]; - context.completions = keywords.concat(engines); - context.keys = { text: 0, description: 1, icon: 2 }; - - if (!space || noSuggest) - return; - - context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", - keyword, true); - - let item = keywords.filter(function (k) k.keyword == keyword)[0]; - if (item && item.url.indexOf("%s") > -1) - context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) { - context.format = history.format; - context.title = [keyword + " Quick Search"]; - // context.background = true; - context.compare = CompletionContext.Sort.unsorted; - context.generate = function () { - let [begin, end] = item.url.split("%s"); - - return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) { - let rest = item.url.length - end.length; - let query = item.url.substring(begin.length, rest); - if (item.url.substr(rest) == end && query.indexOf("&") == -1) - { - item.url = decodeURIComponent(query); - return item; - } - }).filter(util.identity); - }; - }); - }, - - searchEngineSuggest: function searchEngineSuggest(context, engineAliases, kludge) - { - if (!context.filter) - return; - - let engineList = (engineAliases || options["suggestengines"] || "google").split(","); - - let completions = []; - engineList.forEach(function (name) { - let engine = services.get("browserSearch").getEngineByAlias(name); - if (!engine) - return; - let [,word] = /^\s*(\S+)/.exec(context.filter) || []; - if (!kludge && word == name) // FIXME: Check for matching keywords - return; - let ctxt = context.fork(name, 0); - - ctxt.title = [engine.description + " Suggestions"]; - ctxt.compare = CompletionContext.Sort.unsorted; - ctxt.incomplete = true; - bookmarks.getSuggestions(name, ctxt.filter, function (compl) { - ctxt.incomplete = false; - ctxt.completions = compl; - }); - }); - }, - - 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.getFile(dirName); - if (dir.exists() && dir.isDirectory()) - { - commands.push([[file.leafName, dir.path] for ([i, file] in Iterator(io.readDirectory(dir))) - if (file.isFile() && file.isExecutable())]); - } - } - - return util.Array.flatten(commands); - } - }, - - sidebar: function sidebar(context) - { - let menu = document.getElementById("viewSidebarMenu"); - context.title = ["Sidebar Panel"]; - context.completions = Array.map(menu.childNodes, function (n) [n.label, ""]); - }, - // filter a list of urls // // may consist of search engines, filenames, bookmarks and history, @@ -1938,36 +1415,11 @@ function Completion() //{{{ function (item, text) highlight.call(this, item, text, 1) ]; }); - }, - - userCommand: function userCommand(context) - { - context.title = ["User Command", "Definition"]; - context.keys = { text: "name", description: "replacementText" }; - context.completions = commands.getUserCommands(); - }, - - userMapping: function userMapping(context, args, modes) - { - // FIXME: have we decided on a 'standard' way to handle this clash? --djk - modes = modes || [modules.modes.NORMAL]; - - if (args.completeArg == 0) - { - let maps = [[m.names[0], ""] for (m in mappings.getUserIterator(modes))]; - context.completions = maps; - } } //}}} }; const UrlCompleter = new Struct("name", "description", "completer"); - self.addUrlCompleter("S", "Suggest engines", self.searchEngineSuggest); - self.addUrlCompleter("b", "Bookmarks", self.bookmark); - self.addUrlCompleter("h", "History", self.history); - self.addUrlCompleter("f", "Local files", self.file); - self.addUrlCompleter("l", "Firefox location bar entries (bookmarks and history sorted in an intelligent way)", self.location); - self.addUrlCompleter("s", "Search engines and keyword URLs", self.search); return self; //}}} diff --git a/common/content/editor.js b/common/content/editor.js index 1f722d81..bccf70eb 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -565,6 +565,23 @@ function Editor() //{{{ addAbbreviationCommands("i", "insert"); addAbbreviationCommands("c", "command line"); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + liberator.registerObserver("load_completion", function () { + // TODO: shouldn't all of these have a standard signature (context, args, ...)? --djk + completion.abbreviation = function abbreviation(context, args, mode) { + mode = mode || "!"; + + if (args.completeArg == 0) + { + let abbreviations = editor.getAbbreviations(mode); + context.completions = [[lhs, ""] for ([, [, lhs,]] in Iterator(abbreviations))]; + } + }; + }); + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ diff --git a/common/content/events.js b/common/content/events.js index 7a0e7a4e..f170d3ca 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -161,13 +161,26 @@ function AutoCommands() //{{{ ); /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + ////////////////////// COMPLETIONS ///////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ liberator.registerObserver("load_completion", function () { completion.setFunctionCompleter(autocommands.get, [function () config.autocommands]); + + completion.autocmdEvent = function autocmdEvent(context) { + context.completions = config.autocommands; + }; + + completion.macro = function macro(context) { + context.title = ["Macro", "Keys"]; + context.completions = [item for (item in events.getMacros())]; + }; }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + return { __iterator__: function () util.Array.itervalues(store), diff --git a/common/content/io.js b/common/content/io.js index 7101ed20..9feee498 100644 --- a/common/content/io.js +++ b/common/content/io.js @@ -386,7 +386,7 @@ function IO() //{{{ }); /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + ////////////////////// COMPLETIONS ///////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ liberator.registerObserver("load_completion", function () { @@ -395,8 +395,100 @@ function IO() //{{{ 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 wigRegexp = RegExp("(^" + options.get("wildignore").values.join("|") + ")$"); + context.filters.push(function ({item: f}) f.isDirectory() || !wigRegexp.test(f.leafName)); + } + + // context.background = true; + context.key = dir; + context.generate = function generate_file() + { + try + { + return io.readDirectory(dir); + } + catch (e) {} + }; + }; + + 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.getFile(dirName); + if (dir.exists() && dir.isDirectory()) + { + commands.push([[file.leafName, dir.path] for ([i, file] in Iterator(io.readDirectory(dir))) + if (file.isFile() && file.isExecutable())]); + } + } + + return util.Array.flatten(commands); + } + }; + + completion.addUrlCompleter("f", "Local files", completion.file); }); + + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + const self = { /** diff --git a/common/content/liberator.js b/common/content/liberator.js index 855004d4..74c982ce 100644 --- a/common/content/liberator.js +++ b/common/content/liberator.js @@ -87,6 +87,72 @@ const liberator = (function () //{{{ } } + // initially hide all GUI, it is later restored unless the user has :set go= or something + // similar in his config + function hideGUI() + { + let guioptions = config.guioptions; + for (let option in guioptions) + { + guioptions[option].forEach(function (elem) { + try + { + document.getElementById(elem).collapsed = true; + } + catch (e) {} + }); + } + } + + // return the platform normalized to Vim values + function getPlatformFeature() + { + let platform = navigator.platform; + + return /^Mac/.test(platform) ? "MacUnix" : platform == "Win32" ? "Win32" : "Unix"; + } + + // TODO: move this + function getMenuItems() + { + function addChildren(node, parent) + { + for (let [,item] in Iterator(node.childNodes)) + { + if (item.childNodes.length == 0 && item.localName == "menuitem" + && !/rdf:http:/.test(item.getAttribute("label"))) // FIXME + { + item.fullMenuPath = parent + item.getAttribute("label"); + items.push(item); + } + else + { + let path = parent; + if (item.localName == "menu") + path += item.getAttribute("label") + "."; + addChildren(item, path); + } + } + } + + let items = []; + addChildren(document.getElementById(config.guioptions["m"][1]), ""); + return items; + } + + // show a usage index either in the MOW or as a full help page + function showHelpIndex(tag, items, inMow) + { + if (inMow) + liberator.echo(template.usage(items), commandline.FORCE_MULTILINE); + else + liberator.help(tag); + } + + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// OPTIONS ///////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + // Only general options are added here, which are valid for all Vimperator like extensions registerObserver("load_options", function () { @@ -191,6 +257,10 @@ const liberator = (function () //{{{ }); }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// MAPPINGS //////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + registerObserver("load_mappings", function () { mappings.add(modes.all, [""], @@ -209,33 +279,9 @@ const liberator = (function () //{{{ function () { liberator.quit(true); }); }); - // TODO: move this - function getMenuItems() - { - function addChildren(node, parent) - { - for (let [,item] in Iterator(node.childNodes)) - { - if (item.childNodes.length == 0 && item.localName == "menuitem" - && !/rdf:http:/.test(item.getAttribute("label"))) // FIXME - { - item.fullMenuPath = parent + item.getAttribute("label"); - items.push(item); - } - else - { - let path = parent; - if (item.localName == "menu") - path += item.getAttribute("label") + "."; - addChildren(item, path); - } - } - } - - let items = []; - addChildren(document.getElementById(config.guioptions["m"][1]), ""); - return items; - } + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMMANDS //////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ registerObserver("load_commands", function () { @@ -544,39 +590,40 @@ const liberator = (function () //{{{ }); }); - // initially hide all GUI, it is later restored unless the user has :set go= or something - // similar in his config - function hideGUI() - { - let guioptions = config.guioptions; - for (let option in guioptions) - { - guioptions[option].forEach(function (elem) { - try - { - document.getElementById(elem).collapsed = true; - } - catch (e) {} - }); - } - } + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ - // return the platform normalized to Vim values - function getPlatformFeature() - { - let platform = navigator.platform; + registerObserver("load_completion", function () { + completion.dialog = function dialog(context) { + context.title = ["Dialog"]; + context.completions = config.dialogs; + }; - return /^Mac/.test(platform) ? "MacUnix" : platform == "Win32" ? "Win32" : "Unix"; - } + completion.help = function help(context) { + context.title = ["Help"]; + context.anchored = false; + context.generate = function () + { + let res = config.helpFiles.map(function (file) { + let resp = util.httpGet("chrome://liberator/locale/" + file); + if (!resp) + return []; + let doc = resp.responseXML; + return Array.map(doc.getElementsByClassName("tag"), + function (elem) [elem.textContent, file]); + }); + return util.Array.flatten(res); + } + }; - // show a usage index either in the MOW or as a full help page - function showHelpIndex(tag, items, inMow) - { - if (inMow) - liberator.echo(template.usage(items), commandline.FORCE_MULTILINE); - else - liberator.help(tag); - } + completion.menuItem = function menuItem(context) { + context.title = ["Menu Path", "Label"]; + context.anchored = false; + context.keys = { text: "fullMenuPath", description: function (item) item.getAttribute("label") }; + context.completions = liberator.menuItems; + }; + }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// diff --git a/common/content/mappings.js b/common/content/mappings.js index 59cf3df0..1db6f5f3 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -297,6 +297,23 @@ function Mappings() //{{{ [m.mask for (m in modes.mainModes) if (m.char == mode.char)], [mode.disp.toLowerCase()]); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + liberator.registerObserver("load_completion", function () { + completion.userMapping = function userMapping(context, args, modes) { + // FIXME: have we decided on a 'standard' way to handle this clash? --djk + modes = modes || [modules.modes.NORMAL]; + + if (args.completeArg == 0) + { + let maps = [[m.names[0], ""] for (m in mappings.getUserIterator(modes))]; + context.completions = maps; + } + }; + }); + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ diff --git a/common/content/options.js b/common/content/options.js index 35b9ad36..719c3107 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -913,18 +913,85 @@ function Options() //{{{ }); /////////////////////////////////////////////////////////////////////////////}}} - ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + ////////////////////// COMPLETIONS ///////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - // TODO: Does this belong elsewhere? liberator.registerObserver("load_completion", function () { completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]); completion.setFunctionCompleter([options.getPref, options.safeSetPref, options.setPref, options.resetPref, options.invertPref], [function () services.get("pref") .getChildList("", { value: 0 }) .map(function (pref) [pref, ""])]); + + completion.option = function option(context, scope) { + context.title = ["Option"]; + context.keys = { text: "names", description: "description" }; + context.completions = options; + if (scope) + context.filters.push(function ({ item: opt }) opt.scope & scope); + }; + + completion.optionValue = function (context, name, op, curValue) { + let opt = options.get(name); + let completer = opt.completer; + if (!completer) + return; + + let curValues = curValue != null ? opt.parseValues(curValue) : opt.values; + let newValues = opt.parseValues(context.filter); + + let len = context.filter.length; + switch (opt.type) + { + case "boolean": + if (!completer) + completer = function () [["true", ""], ["false", ""]]; + break; + case "stringlist": + let target = newValues.pop(); + len = target ? target.length : 0; + break; + case "charlist": + len = 0; + break; + } + // TODO: Highlight when invalid + context.advance(context.filter.length - len); + + context.title = ["Option Value"]; + let completions = completer(context); + if (!completions) + return; + // Not Vim compatible, but is a significant enough improvement + // that it's worth breaking compatibility. + if (newValues instanceof Array) + { + completions = completions.filter(function (val) newValues.indexOf(val[0]) == -1); + switch (op) + { + case "+": + completions = completions.filter(function (val) curValues.indexOf(val[0]) == -1); + break; + case "-": + completions = completions.filter(function (val) curValues.indexOf(val[0]) > -1); + break; + } + } + context.completions = completions; + }; + + completion.preference = function preference(context) { + context.anchored = false; + context.title = ["Firefox Preference", "Value"]; + context.keys = { text: function (item) item, description: function (item) options.getPref(item) }; + context.completions = services.get("pref").getChildList("", { value: 0 }); + }; }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// PUBLIC SECTION ////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + const self = { /** diff --git a/common/content/style.js b/common/content/style.js index 0e260395..7ffad6ab 100644 --- a/common/content/style.js +++ b/common/content/style.js @@ -534,16 +534,9 @@ if (highlight.CSS != Highlights.prototype.CSS) liberator.triggerObserver("load_styles", "styles"); liberator.triggerObserver("load_highlight", "highlight"); -liberator.registerObserver("load_completion", function () { - completion.setFunctionCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), - [ // Prototype: (system, name, filter, css, index) - null, - function (context, obj, args) args[0] ? styles.systemNames : styles.userNames, - function (context, obj, args) styles.completeSite(context, content), - null, - function (context, obj, args) args[0] ? styles.systemSheets : styles.userSheets - ]); -}); +/////////////////////////////////////////////////////////////////////////////}}} +////////////////////// COMMANDS //////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////{{{ liberator.registerObserver("load_commands", function () { @@ -711,5 +704,40 @@ liberator.registerObserver("load_commands", function () { ] }); }); +/////////////////////////////////////////////////////////////////////////////}}} +////////////////////// COMPLETIONS ///////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////{{{ + +liberator.registerObserver("load_completion", function () { + completion.setFunctionCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), + [ // Prototype: (system, name, filter, css, index) + null, + function (context, obj, args) args[0] ? styles.systemNames : styles.userNames, + function (context, obj, args) styles.completeSite(context, content), + null, + function (context, obj, args) args[0] ? styles.systemSheets : styles.userSheets + ]); + + completion.colorScheme = function colorScheme(context) { + let colors = []; + + io.getRuntimeDirectories("colors").forEach(function (dir) { + io.readDirectory(dir).forEach(function (file) { + if (/\.vimp$/.test(file.leafName) && !colors.some(function (c) c.leafName == file.leafName)) + colors.push(file); + }); + }); + + context.title = ["Color Scheme", "Runtime Path"]; + context.completions = [[c.leafName.replace(/\.vimp$/, ""), c.parent.path] for ([,c] in Iterator(colors))] + }; + + // FIXME: extract from :highlight + completion.highlightGroup = function highlightGroup(context, args) { + return commands.get("highlight").completer(context, args); + }; + +}); +//}}} // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/template.js b/common/content/template.js index ee149ab2..eb3090fa 100644 --- a/common/content/template.js +++ b/common/content/template.js @@ -8,7 +8,7 @@ /** @scope modules */ -const template = { +const template = { //{{{ add: function add(a, b) a + b, join: function join(c) function (a, b) a + c + b, @@ -332,6 +332,6 @@ const template = { ); // } -}; +}; //}}} // vim: set fdm=marker sw=4 ts=4 et: diff --git a/vimperator/content/config.js b/vimperator/content/config.js index 65ea883f..e319fd43 100644 --- a/vimperator/content/config.js +++ b/vimperator/content/config.js @@ -505,6 +505,50 @@ const config = { //{{{ "Set the separator regexp used to separate multiple URL args", "string", ",\\s"); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + completion.location = function location(context) { + if (!services.get("autoCompleteSearch")) + return; + + context.anchored = false; + context.title = ["Smart Completions"]; + context.keys.icon = 2; + context.incomplete = true; + context.hasItems = context.completions.length > 0; // XXX + context.filterFunc = null; + context.cancel = function () services.get("autoCompleteSearch").stopSearch(); + context.compare = CompletionContext.Sort.unsorted; + let timer = new Timer(50, 100, function (result) { + context.incomplete = result.searchResult >= result.RESULT_NOMATCH_ONGOING; + context.completions = [ + [result.getValueAt(i), result.getCommentAt(i), result.getImageAt(i)] + for (i in util.range(0, result.matchCount)) + ]; + }); + services.get("autoCompleteSearch").stopSearch(); + services.get("autoCompleteSearch").startSearch(context.filter, "", context.result, { + onSearchResult: function onSearchResult(search, result) { + context.result = result; + timer.tell(result); + if (result.searchResult <= result.RESULT_SUCCESS) + timer.flush(); + } + }); + }; + + completion.sidebar = function sidebar(context) { + let menu = document.getElementById("viewSidebarMenu"); + context.title = ["Sidebar Panel"]; + context.completions = Array.map(menu.childNodes, function (n) [n.label, ""]); + }; + + completion.addUrlCompleter("l", + "Firefox location bar entries (bookmarks and history sorted in an intelligent way)", + completion.location); + //}}} } }; //}}} diff --git a/xulmus/content/player.js b/xulmus/content/player.js index 5ced3f57..5a986693 100644 --- a/xulmus/content/player.js +++ b/xulmus/content/player.js @@ -406,6 +406,45 @@ function Player() // {{{ }, { argCount: "1" }); + /////////////////////////////////////////////////////////////////////////////}}} + ////////////////////// COMPLETIONS ///////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////{{{ + + completion.song = function song(context, args) { + // TODO: useful descriptions? + function map(list) list.map(function (i) [i, ""]); + let [artist, album] = [args[0], args[1]]; + + if (args.completeArg == 0) + { + context.title = ["Artists"]; + context.completions = map(library.getArtists()); + } + else if (args.completeArg == 1) + { + context.title = ["Albums by " + artist]; + context.completions = map(library.getAlbums(artist)); + } + else if (args.completeArg == 2) + { + context.title = ["Tracks from " + album + " by " + artist]; + context.completions = map(library.getTracks(artist, album)); + } + }; + + completion.playlist = function playlist(context, args) { + context.title = ["Playlist", "Type"]; + context.keys = { text: "name", description: "type" }; + context.completions = player.getPlaylists(); + }; + + completion.mediaView = function mediaView(context) { + context.title = ["Media View", "URL"]; + context.anchored = false; + context.keys = { text: "contentTitle", description: "contentUrl" }; + context.completions = player.getMediaPages(); + }; + /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{