Merge pull request #2194 from mozbugbox/bind-completion

Binding completion
This commit is contained in:
Oliver Blanthorn 2020-02-28 21:10:04 +00:00 committed by GitHub
commit 3b38f093b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 222 additions and 55 deletions

View file

@ -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"
@ -103,6 +104,7 @@ export function enableCompletions() {
if (!commandline_state.activeCompletions) {
commandline_state.activeCompletions = [
// FindCompletionSource,
BindingsCompletionSource,
BmarkCompletionSource,
TabAllCompletionSource,
BufferCompletionSource,

View file

@ -108,8 +108,16 @@ export abstract class CompletionOptionHTML extends CompletionOption {
switch (newstate) {
case "focused":
this.html.classList.add("focused")
this.html.scrollIntoView()
this.html.classList.remove("hidden")
const myRect = this.html.getClientRects()[0]
if (myRect) {
const container = document.getElementById("completions")
const boxRect = container.getClientRects()[0]
if (myRect.bottom > boxRect.bottom)
this.html.scrollIntoView()
else if (myRect.top < boxRect.top)
this.html.scrollIntoView(false)
}
break
case "normal":
this.html.classList.remove("focused")

148
src/completions/Bindings.ts Normal file
View file

@ -0,0 +1,148 @@
import * as Completions from "@src/completions"
import * as config from "@src/lib/config"
import * as Binding from "@src/lib/binding"
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`<tr class="BindingsCompletionOption option">
<td class="name">${binding.name}</td>
<td class="content">${binding.value}</td>
<td class="type">${binding.mode}</td>
</tr>`
}
}
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
}
this.deselect()
// 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()
}
}
// 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 = Binding.modes
.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 (Binding.maps2mode.has(mode + "maps")) {
modeName = Binding.maps2mode.get(mode + "maps")
}
configName = Binding.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() {}
}

View file

@ -90,6 +90,15 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
}
}
// Add partial matched funcs like: 'conf' ~= 'viewconfig'
const seen = new Set(this.options.map(o => o.value))
const partial_options = this.scoreOptions(
fns
.filter(([name, fn]) => !fn.hidden && name.includes(exstr) && !seen.has(name))
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
)
this.options = this.options.concat(partial_options)
this.options.forEach(o => (o.state = "normal"))
return this.updateChain()
}

View file

@ -156,6 +156,7 @@ import { EditorCmds as BgEditorCmds } from "@src/background/editor"
import { messageActiveTab } from "@src/lib/messaging"
import { EditorCmds } from "@src/background/editor"
import { firefoxVersionAtLeast } from "@src/lib/webext"
import { parse_bind_args, modeMaps } from "@src/lib/binding"
import * as rc from "@src/background/config_rc"
import * as css_util from "@src/lib/css_util"
import * as Updates from "@src/lib/updates"
@ -1398,7 +1399,7 @@ export async function help(...helpItems: string[]) {
},
// -b: look for a binding
"-b": (settings, helpItem) => {
for (const mode of ["nmaps", "imaps", "inputmaps", "ignoremaps"]) {
for (const mode of modeMaps) {
const bindings = settings[mode]
// If 'helpItem' matches a binding, replace 'helpItem' with
// the command that would be executed when pressing the key
@ -3120,48 +3121,6 @@ export function comclear(name: string) {
config.unset("exaliases", name)
}
/** @hidden */
//#background_helper
interface bind_args {
mode: string
configName: string
key: string
excmd: string
}
/** @hidden */
//#background_helper
function parse_bind_args(...args: string[]): bind_args {
if (args.length === 0) throw new Error("Invalid bind/unbind arguments.")
const result = {} as bind_args
result.mode = "normal"
// TODO: This mapping is copy-pasted in controller_content.ts,
// where it constructs the list of parsers. it should be
// centralized, possibly as part of rewrite for content-local maps
// and similar.
const mode2maps = new Map([["normal", "nmaps"], ["ignore", "ignoremaps"], ["insert", "imaps"], ["input", "inputmaps"], ["ex", "exmaps"], ["hint", "hintmaps"], ["visual", "vmaps"]])
if (args[0].startsWith("--mode=")) {
result.mode = args.shift().replace("--mode=", "")
}
if (!mode2maps.has(result.mode)) {
result.configName = result.mode + "maps"
} else {
result.configName = mode2maps.get(result.mode)
}
const key = args.shift()
// Convert key to internal representation
result.key = mapstrToKeyseq(key)
.map(k => k.toMapstr())
.join("")
result.excmd = args.join(" ")
return result
}
/** Bind a sequence of keys to an excmd or view bound sequence.
This is an easier-to-implement bodge while we work on vim-style maps.

View file

@ -1,6 +1,7 @@
// This file is only loaded in tridacyl's help pages
import * as config from "@src/lib/config"
import { modeMaps } from "@src/lib/binding"
/** Create the element that should contain keybinding information */
function initTridactylSettingElem(
@ -93,7 +94,7 @@ async function onExcmdPageLoad() {
browser.storage.onChanged.addListener((changes, areaname) => {
if ("userconfig" in changes) {
// JSON.stringify for comparisons like it's 2012
["nmaps", "imaps", "ignoremaps", "inputmaps", "exaliases"].forEach(
[...modeMaps, "exaliases"].forEach(
kind => {
if (
JSON.stringify(changes.userconfig.newValue[kind]) !==
@ -105,16 +106,7 @@ async function onExcmdPageLoad() {
}
})
await Promise.all(
[
"nmaps",
"imaps",
"ignoremaps",
"inputmaps",
"exaliases",
"exmaps",
].map(addSetting),
)
await Promise.all([...modeMaps, "exaliases"].map(addSetting))
// setCommandSetting() can change the height of nodes in the page so we need to scroll to the right place again
if (document.location.hash) {
/* tslint:disable:no-self-assignment */

49
src/lib/binding.ts Normal file
View file

@ -0,0 +1,49 @@
/** # Binding Functions
*
*/
import { mapstrToKeyseq } from "@src/lib/keyseq"
export const mode2maps = new Map([
["normal", "nmaps"], ["ignore", "ignoremaps"],
["insert", "imaps"], ["input", "inputmaps"], ["ex", "exmaps"],
["hint", "hintmaps"], ["visual", "vmaps"]])
export const maps2mode = new Map(
Array.from(mode2maps.keys()).map(k => [mode2maps.get(k), k]))
export const modes = Array.from(mode2maps.keys())
export const modeMaps = Array.from(maps2mode.keys())
interface bind_args {
mode: string
configName: string
key: string
excmd: string
}
export function parse_bind_args(...args: string[]): bind_args {
if (args.length === 0) throw new Error("Invalid bind/unbind arguments.")
const result = {} as bind_args
result.mode = "normal"
if (args[0].startsWith("--mode=")) {
result.mode = args.shift().replace("--mode=", "")
}
if (!mode2maps.has(result.mode)) {
result.configName = result.mode + "maps"
} else {
result.configName = mode2maps.get(result.mode)
}
const key = args.shift()
// Convert key to internal representation
result.key = mapstrToKeyseq(key)
.map(k => k.toMapstr())
.join("")
result.excmd = args.join(" ")
return result
}