tridactyl/src/parsers/normalmode.ts

78 lines
2.4 KiB
TypeScript
Raw Normal View History

/** Tridactyl normal mode:
differs from Vim in that no map may be a prefix of another map (e.g. 'g' and 'gg' cannot both be maps). This simplifies the parser.
*/
import * as config from "../config"
let nmaps = config.get("nmaps")
// Split a string into a number prefix and some following keys.
function keys_split_count(keys: string[]) {
// Extracts the first number with capturing parentheses
const FIRST_NUM_REGEX = /^([0-9]+)/
let keystr = keys.join("")
let regexCapture = FIRST_NUM_REGEX.exec(keystr)
let count = regexCapture ? regexCapture[0] : null
keystr = keystr.replace(FIRST_NUM_REGEX, "")
return [count, keystr]
}
// Given a valid keymap, resolve it to an ex_str
function resolve_map(map) {
// TODO: This needs to become recursive to allow maps to be defined in terms of other maps.
return config.get("nmaps")[map]
}
// Valid keystr to ex_str by splitting count, resolving keystr and appending count as final argument.
// TODO: This is a naive way to deal with counts and won't work for ExCmds that don't expect a numeric answer.
// TODO: Refactor to return a ExCmdPartial object?
function get_ex_str(keys): string {
let [count, keystr] = keys_split_count(keys)
let ex_str = resolve_map(keystr)
if (ex_str) {
ex_str = count ? ex_str + " " + count : ex_str
}
return ex_str
}
// A list of maps that keys could potentially map to.
function possible_maps(keys): string[] {
let [count, keystr] = keys_split_count(keys)
// Short circuit or search maps.
if (Object.getOwnPropertyNames(config.get("nmaps")).includes(keystr)) {
return [keystr]
} else {
// Efficiency: this can be short-circuited.
return completions(keystr)
}
}
// A list of maps that start with the fragment.
export function completions(fragment): string[] {
let posskeystrs = Array.from(Object.keys(config.get("nmaps")))
return posskeystrs.filter(key => key.startsWith(fragment))
}
export interface NormalResponse {
keys?: string[]
ex_str?: string
}
export function parser(keys): NormalResponse {
// If there aren't any possible matches, throw away keys until there are
while (possible_maps(keys).length == 0 && keys.length) {
keys = keys.slice(1)
}
// If keys map to an ex_str, send it
let ex_str = get_ex_str(keys)
if (ex_str) {
return { ex_str }
}
// Otherwise, return the keys that might be used in a future command
return { keys }
}