mirror of
https://github.com/vale981/tridactyl
synced 2025-03-05 17:41:40 -05:00
completions: WIP
This commit is contained in:
parent
e55d174747
commit
1045eadc0e
3 changed files with 145 additions and 64 deletions
|
@ -1,63 +1,166 @@
|
||||||
|
/*
|
||||||
|
|
||||||
|
Have an array of all completion sources. Completion sources display nothing if the filter doesn't match for them.
|
||||||
|
|
||||||
|
On each input event, call updateCompletions on the array. That will mutate the array and update the display as required.
|
||||||
|
|
||||||
|
How to handle cached e.g. buffer information going out of date?
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import * as Fuse from 'fuse.js'
|
||||||
import {enumerate} from './itertools'
|
import {enumerate} from './itertools'
|
||||||
|
|
||||||
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
|
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
|
||||||
|
|
||||||
// HIGH LEVEL OBJECTS
|
// {{{ INTERFACES
|
||||||
|
|
||||||
interface CompletionOption {
|
interface CompletionOption {
|
||||||
// Highlight this option
|
// Highlight this option
|
||||||
focus: () => void
|
focus: () => void
|
||||||
|
blur: () => void
|
||||||
|
|
||||||
// What to fill into cmdline
|
// What to fill into cmdline
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class CompletionSource {
|
export abstract class CompletionSource {
|
||||||
private obsolete = false
|
private obsolete = false
|
||||||
|
|
||||||
readonly options = new Array<CompletionOption>()
|
readonly options = new Array<CompletionOption>()
|
||||||
|
|
||||||
public node: HTMLElement
|
public node: HTMLElement
|
||||||
|
|
||||||
// Called by updateCompletions on the child that succeeds its parent
|
// Called by updateCompletions on the child that succeeds its parent
|
||||||
abstract activate()
|
abstract activate(): void
|
||||||
// this.node now belongs to you, update it or something :)
|
// this.node now belongs to you, update it or something :)
|
||||||
// Example: Mutate node or call replaceChild on its parent
|
// Example: Mutate node or call replaceChild on its parent
|
||||||
|
|
||||||
abstract async filter(exstr): Promise<CompletionSource>
|
abstract async filter(exstr): Promise<CompletionSource>
|
||||||
// <Do some async work that doesn't mutate any non-local vars>
|
// <Do some async work that doesn't mutate any non-local vars>
|
||||||
// Make a new CompletionOptions and return it
|
// Make a new CompletionOptions and return it
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTEXT SPECIFIC
|
// }}}
|
||||||
|
|
||||||
|
// {{{ IMPLEMENTATIONS
|
||||||
|
|
||||||
class BufferCompletionOption implements CompletionOption {
|
class BufferCompletionOption implements CompletionOption {
|
||||||
constructor(public favIconUrl: string, public index: number, public title: string, public pre: string, public url: string, public value: string) {}
|
// keep a reference to our markup so we can highlight it on focus().
|
||||||
html: HTMLElement // keep a reference to our markup so we can change it
|
html: HTMLElement
|
||||||
render(highlight: boolean = false): void {
|
|
||||||
const html = document.createElement("div")
|
// For fuzzy matching
|
||||||
if (highlight) html.setAttribute("class", "highlighted")
|
matchStrings: string[]
|
||||||
html.innerHTML = `<span></span><span></span><span></span><span></span><a href="${this.url}" class="url" target="_blank"><span></span></a>`
|
|
||||||
html.childNodes[0].textContent = ` ${this.pre.padEnd(2)} `
|
constructor(public value: string, tab: browser.tabs.Tab, isAlternative = false) {
|
||||||
html.childNodes[1].nodeValue = `<img src="${this.favIconUrl}"> `
|
// Two character buffer properties prefix
|
||||||
if (this.index) html.childNodes[2].textContent = `${this.index}: `
|
let pre = ""
|
||||||
html.childNodes[3].textContent = `${this.title} `
|
if (tab.active) pre += "%"
|
||||||
html.childNodes[4].textContent = `${this.url} `
|
else if (isAlternative) pre += "#"
|
||||||
this.html = html
|
|
||||||
}
|
if (tab.pinned) {
|
||||||
focus(): void { this.render(true) }
|
pre += "@"
|
||||||
}
|
this.matchStrings.push('@')
|
||||||
class BufferCompletionSource extends CompletionSource {
|
|
||||||
constructor(readonly options: BufferCompletionOption[], public node: HTMLElement) { super() }
|
|
||||||
activate(): HTMLElement { return this.node }
|
|
||||||
async filter(exstr: string): Promise<BufferCompletionSource> {
|
|
||||||
const match = function(queries: string[], option): boolean {
|
|
||||||
return queries.every((query) => {
|
|
||||||
return (/[A-Z]/.test(query)
|
|
||||||
? option.title.includes(query) || option.url.includes(query)
|
|
||||||
: option.title.toLowerCase().includes(query) || option.url.toLowerCase().includes(query)
|
|
||||||
)})}.bind(null, exstr.split(/\s+/))
|
|
||||||
const filtered: HTMLElement = window.document.createElement("div")
|
|
||||||
filtered.setAttribute("id", "completions")
|
|
||||||
for (const option of this.options) {
|
|
||||||
if (match(option)) { option.render(); filtered.appendChild(option.html) }
|
|
||||||
}
|
}
|
||||||
return new BufferCompletionSource(this.options, filtered)
|
|
||||||
|
pre = pre.padEnd(2)
|
||||||
|
|
||||||
|
this.matchStrings = [tab.title, tab.url]
|
||||||
|
|
||||||
|
const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
|
||||||
|
|
||||||
|
this.html = html`
|
||||||
|
<div>
|
||||||
|
<span>${pre}</span>
|
||||||
|
<img src=${favIconUrl}></img>
|
||||||
|
<span>${tab.index + 1}: ${tab.title}</span>
|
||||||
|
<a class="url" href=${tab.url}>${tab.url}</a>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.html.classList.add("focused")
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.html.classList.remove("focused")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MANAGING ASYNC CHANGES
|
export class BufferCompletionSource extends CompletionSource {
|
||||||
|
private fuse: Fuse
|
||||||
|
public prefixes = [ "buffer " ]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly options: BufferCompletionOption[],
|
||||||
|
public node: HTMLElement,
|
||||||
|
private selection?: BufferCompletionOption
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
const fuseOptions = {
|
||||||
|
shouldSort: true,
|
||||||
|
includeScore: true,
|
||||||
|
keys: ["matchStrings"],
|
||||||
|
}
|
||||||
|
this.fuse = new Fuse(options, fuseOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromTabs(tabs: browser.tabs.Tab[]) {
|
||||||
|
const node = html`<div class="BufferCompletionSource">`
|
||||||
|
|
||||||
|
// Get alternative tab, defined as last accessed tab.
|
||||||
|
const alt = tabs.sort((a, b) => { return a.lastAccessed < b.lastAccessed ? 1 : -1 })[1]
|
||||||
|
tabs.sort((a, b) => { return a.index < b.index ? -1 : 1 })
|
||||||
|
|
||||||
|
const options: BufferCompletionOption[] = []
|
||||||
|
|
||||||
|
for (const tab of tabs) {
|
||||||
|
options.push(new BufferCompletionOption(
|
||||||
|
(tab.index + 1).toString(),
|
||||||
|
tab,
|
||||||
|
tab === alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const option of options) {
|
||||||
|
node.appendChild(option.html)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BufferCompletionSource(options, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
// TODO... this bit of the interface isn't super clear to me yet.
|
||||||
|
}
|
||||||
|
|
||||||
|
async filter(exstr: string) {
|
||||||
|
if (! exstr.startsWith('buffer ')) {
|
||||||
|
// Disable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the 'buffer ' bit.
|
||||||
|
const query = exstr.slice(exstr.indexOf(' '))
|
||||||
|
|
||||||
|
/** If query is a number, focus corresponding index.
|
||||||
|
Else fuzzy search
|
||||||
|
*/
|
||||||
|
let match: BufferCompletionOption = undefined
|
||||||
|
if (! isNaN(Number(query)) && Number(query) <= this.options.length) {
|
||||||
|
match = this.options[Number(query)]
|
||||||
|
} else {
|
||||||
|
// Fuzzy search!
|
||||||
|
match = this.fuse.search('query')[0] as BufferCompletionOption
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BufferCompletionSource(this.options, this.node, match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
// {{{ MANAGING ASYNC CHANGES
|
||||||
|
|
||||||
/* If first to modify completions, update it. */
|
/* If first to modify completions, update it. */
|
||||||
async function commitIfCurrent(epochref: any, asyncFunc: Function, commitFunc: Function, ...args: any[]): Promise<any> {
|
async function commitIfCurrent(epochref: any, asyncFunc: Function, commitFunc: Function, ...args: any[]): Promise<any> {
|
||||||
// I *think* sync stuff in here is guaranteed to happen immediately after
|
// I *think* sync stuff in here is guaranteed to happen immediately after
|
||||||
|
@ -66,8 +169,9 @@ async function commitIfCurrent(epochref: any, asyncFunc: Function, commitFunc: F
|
||||||
const epoch = epochref
|
const epoch = epochref
|
||||||
const res = await asyncFunc(...args)
|
const res = await asyncFunc(...args)
|
||||||
if (epoch === epochref) return commitFunc(res)
|
if (epoch === epochref) return commitFunc(res)
|
||||||
else throw "Update failed: epoch out of date!"
|
else console.error(new Error("Update failed: epoch out of date!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Indicate changes to completions we would like. */
|
/* Indicate changes to completions we would like. */
|
||||||
function updateCompletions(filter: string, sources: CompletionSource[]) {
|
function updateCompletions(filter: string, sources: CompletionSource[]) {
|
||||||
for (let [index, source] of enumerate(sources)) {
|
for (let [index, source] of enumerate(sources)) {
|
||||||
|
@ -88,29 +192,4 @@ function updateCompletions(filter: string, sources: CompletionSource[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUFFER SPECIFIC HELPERS
|
// }}}
|
||||||
/** Create completion item from given tab. */
|
|
||||||
function optionFromTab(tab: browser.tabs.Tab, prev: boolean = false): BufferCompletionOption {
|
|
||||||
let pre: string = ""
|
|
||||||
if (tab.active) pre += "%"
|
|
||||||
else if (prev) pre += "#"
|
|
||||||
if (tab.pinned) pre += "@"
|
|
||||||
return new BufferCompletionOption(
|
|
||||||
tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON,
|
|
||||||
tab.index + 1,
|
|
||||||
tab.title,
|
|
||||||
pre,
|
|
||||||
tab.url,
|
|
||||||
`${tab.index + 1}: ${tab.title}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export function getBuffersFromTabs(tabs: browser.tabs.Tab[]): BufferCompletionSource {
|
|
||||||
const buffers: BufferCompletionOption[] = []
|
|
||||||
const markup: HTMLElement = window.document.createElement("div")
|
|
||||||
const prev = tabs.sort((a, b) => { return a.lastAccessed < b.lastAccessed ? 1 : -1 })[1]
|
|
||||||
tabs.sort((a, b) => { return a.index < b.index ? -1 : 1 })
|
|
||||||
for (const tab of tabs) { buffers.push(optionFromTab(tab, tab === prev)) }
|
|
||||||
markup.setAttribute("id", "completions")
|
|
||||||
for (const buffer of buffers) { buffer.render(); markup.appendChild(buffer.html) }
|
|
||||||
return new BufferCompletionSource(buffers, markup)
|
|
||||||
}
|
|
||||||
|
|
5
src/tridactyl.d.ts
vendored
5
src/tridactyl.d.ts
vendored
|
@ -14,8 +14,6 @@ interface Number {
|
||||||
interface Window {
|
interface Window {
|
||||||
scrollByLines(n: number): void
|
scrollByLines(n: number): void
|
||||||
scrollByPages(n: number): void
|
scrollByPages(n: number): void
|
||||||
|
|
||||||
html(strings: TemplateStringsArray, ...values: any[]): HTMLElement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix typescript bugs
|
// Fix typescript bugs
|
||||||
|
@ -32,3 +30,6 @@ declare namespace browser.find {
|
||||||
declare namespace browser.tabs {
|
declare namespace browser.tabs {
|
||||||
function setZoom(number): any
|
function setZoom(number): any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// html-tagged-template.js
|
||||||
|
declare function html(strings: TemplateStringsArray, ...values: any[]): HTMLElement
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["ES2017","dom"],
|
"lib": ["ES2017","dom"],
|
||||||
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types/"]
|
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types/"],
|
||||||
|
"alwaysStrict": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src/**/*"
|
"./src/**/*"
|
||||||
|
|
Loading…
Add table
Reference in a new issue