diff --git a/src/commandline_frame.ts b/src/commandline_frame.ts
index 6190fb4e..fed7224c 100644
--- a/src/commandline_frame.ts
+++ b/src/commandline_frame.ts
@@ -21,6 +21,7 @@ import * as perf from "@src/perf"
import "@src/lib/number.clamp"
import "@src/lib/html-tagged-template"
import { TabAllCompletionSource } from "@src/completions/TabAll"
+import { BindingsCompletionSource } from "@src/completions/Bindings"
import { BufferCompletionSource } from "@src/completions/Tab"
import { BmarkCompletionSource } from "@src/completions/Bmark"
import { ExcmdCompletionSource } from "@src/completions/Excmd"
@@ -102,6 +103,7 @@ export function enableCompletions() {
if (!commandline_state.activeCompletions) {
commandline_state.activeCompletions = [
// FindCompletionSource,
+ BindingsCompletionSource,
BmarkCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,
diff --git a/src/completions/Bindings.ts b/src/completions/Bindings.ts
new file mode 100644
index 00000000..fb895b34
--- /dev/null
+++ b/src/completions/Bindings.ts
@@ -0,0 +1,152 @@
+import * as Completions from "@src/completions"
+import * as config from "@src/lib/config"
+
+class BindingsCompletionOption extends Completions.CompletionOptionHTML
+ implements Completions.CompletionOptionFuse {
+ public fuseKeys = []
+
+ constructor(
+ public value: string,
+ binding: { name: string; value: string; mode: string },
+ ) {
+ super()
+ this.html = html`
+ ${binding.name} |
+ ${binding.value} |
+ ${binding.mode} |
+
`
+ }
+}
+
+export class BindingsCompletionSource extends Completions.CompletionSourceFuse {
+ public options: BindingsCompletionOption[]
+
+ constructor(private _parent) {
+ super(
+ ["bind", "unbind", "bindurl", "unbindurl", "reset", "reseturl"],
+ "BindingsCompletionSource",
+ "Bindings",
+ )
+
+ this._parent.appendChild(this.node)
+ }
+
+ public async filter(exstr: string) {
+ this.lastExstr = exstr
+ let options = ""
+ let [prefix, query] = this.splitOnPrefix(exstr)
+ const args = query ? query.split(/\s+/) : []
+ let configName: string = "nmaps"
+ let modeName = "normal"
+ let urlPattern: string = null
+
+ // Hide self and stop if prefixes don't match
+ if (prefix) {
+ // Show self if prefix and currently hidden
+ if (this.state === "hidden") {
+ this.state = "normal"
+ }
+ } else {
+ this.state = "hidden"
+ return
+ }
+
+ // url pattern is mandatory: bindurl, unbindurl, reseturl
+ if (prefix.trim().endsWith("url")) {
+ urlPattern = args.length > 0 ? args.shift() : ""
+ options += urlPattern ? urlPattern + " " : ""
+
+ if (args.length === 0) {
+ const patterns = config.get("subconfigs")
+ this.options = Object.keys(patterns)
+ .filter(pattern => pattern.startsWith(urlPattern))
+ .sort()
+ .map(pattern => {
+ return new BindingsCompletionOption(
+ pattern, {
+ name: pattern,
+ value: "",
+ mode: "URL Pattern",
+ })
+ })
+
+ return this.updateChain()
+ }
+ }
+
+ const mode2maps = new Map([
+ ["normal", "nmaps"], ["ignore", "ignoremaps"],
+ ["insert", "imaps"], ["input", "inputmaps"], ["ex", "exmaps"],
+ ["hint", "hintmaps"], ["visual", "vmaps"]])
+ const maps2mode = new Map(
+ Array.from(mode2maps.keys()).map(k => [mode2maps.get(k), k]))
+
+ // completion maps mode
+ if (args.length === 1 && args[0].startsWith("--m")) {
+ const margs = args[0].split("=")
+ if ("--mode".includes(margs[0])) {
+ const modeStr = margs.length > 1 ? margs[1] : ""
+ this.options = Array.from(mode2maps.keys())
+ .filter(k => k.startsWith(modeStr))
+ .map(name => {
+ return new BindingsCompletionOption(
+ options + "--mode=" + name, {
+ name,
+ value: "",
+ mode: "Mode Name",
+ })
+ })
+ return this.updateChain()
+ }
+ }
+
+ if (args.length > 0 && args[0].startsWith("--mode=")) {
+ const modeStr = args.shift()
+ const mode = modeStr.replace("--mode=", "")
+
+ modeName = mode
+ if (maps2mode.has(mode + "maps")) {
+ modeName = maps2mode.get(mode + "maps")
+ }
+ configName = mode2maps.get(modeName)
+ options += `--mode=${modeName} `
+ }
+
+ if (!configName) {
+ this.options = []
+ return this.updateChain()
+ }
+
+ const bindings = urlPattern ? config.getURL(urlPattern, [configName]) : config.get(configName as any)
+
+ if (bindings === undefined) {
+ this.options = []
+ return this.updateChain()
+ }
+
+ query = args.join(" ").toLowerCase()
+ this.options = Object.keys(bindings)
+ .filter(x => x.toLowerCase().startsWith(query) )
+ .sort()
+ .map(keystr => {
+ return new BindingsCompletionOption(
+ options + keystr + " " + bindings[keystr], {
+ name: keystr,
+ value: JSON.stringify(bindings[keystr]),
+ mode: `${configName} (${modeName})`,
+ })
+ })
+
+ return this.updateChain()
+ }
+
+ updateChain() {
+ // Options are pre-trimmed to the right length.
+ this.options.forEach(option => (option.state = "normal"))
+
+ // Call concrete class
+ return this.updateDisplay()
+ }
+
+ onInput() {}
+}