2017-11-15 13:41:04 -08:00
|
|
|
/*
|
|
|
|
|
|
|
|
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 {toNumber} from './convert'
|
2017-11-22 18:05:54 +00:00
|
|
|
import * as Messaging from './messaging'
|
2017-11-15 13:41:04 -08:00
|
|
|
|
|
|
|
const DEFAULT_FAVICON = browser.extension.getURL("static/defaultFavicon.svg")
|
|
|
|
|
|
|
|
// {{{ INTERFACES
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
type OptionState = 'focused' | 'hidden' | 'normal'
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
abstract class CompletionOption {
|
|
|
|
/** What to fill into cmdline */
|
|
|
|
value: string
|
|
|
|
/** Control presentation of the option */
|
|
|
|
state: OptionState
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class CompletionSource {
|
2017-11-22 18:05:54 +00:00
|
|
|
readonly options: CompletionOption[]
|
|
|
|
node: HTMLElement
|
|
|
|
public completion: string
|
|
|
|
|
|
|
|
/** Update [[node]] to display completions relevant to exstr */
|
|
|
|
public abstract filter(exstr: string): Promise<void>
|
|
|
|
|
|
|
|
private _state: OptionState
|
|
|
|
|
|
|
|
/** Control presentation of Source */
|
|
|
|
set state(newstate: OptionState) {
|
|
|
|
switch (newstate) {
|
|
|
|
case 'normal':
|
|
|
|
this.node.classList.remove('hidden')
|
|
|
|
this.completion = undefined
|
|
|
|
break
|
|
|
|
case 'hidden':
|
|
|
|
this.node.classList.add('hidden')
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
this._state = newstate
|
|
|
|
}
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
get state() {
|
|
|
|
return this._state
|
|
|
|
}
|
2017-11-23 15:44:07 +00:00
|
|
|
|
2017-11-24 19:00:26 +00:00
|
|
|
next(inc = 1): boolean {
|
2017-11-23 15:44:07 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-11-24 19:00:26 +00:00
|
|
|
prev(inc = 1): boolean {
|
|
|
|
return this.next(-1*inc)
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
// Default classes
|
|
|
|
|
|
|
|
abstract class CompletionOptionHTML extends CompletionOption {
|
|
|
|
public html: HTMLElement
|
|
|
|
public value
|
|
|
|
|
|
|
|
private _state: OptionState = 'hidden'
|
|
|
|
|
|
|
|
/** Control presentation of element */
|
|
|
|
set state(newstate: OptionState) {
|
2017-11-24 18:46:49 +00:00
|
|
|
// console.log("state from to", this._state, newstate)
|
2017-11-22 18:05:54 +00:00
|
|
|
switch (newstate) {
|
|
|
|
case 'focused':
|
|
|
|
this.html.classList.add('focused')
|
|
|
|
this.html.classList.remove('hidden')
|
|
|
|
break
|
|
|
|
case 'normal':
|
|
|
|
this.html.classList.remove('focused')
|
|
|
|
this.html.classList.remove('hidden')
|
|
|
|
break
|
|
|
|
case 'hidden':
|
|
|
|
this.html.classList.remove('focused')
|
|
|
|
this.html.classList.add('hidden')
|
|
|
|
break;
|
|
|
|
}
|
2017-11-24 18:46:49 +00:00
|
|
|
this._state = newstate
|
|
|
|
}
|
|
|
|
|
|
|
|
get state() {
|
|
|
|
return this._state
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
}
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
interface CompletionOptionFuse extends CompletionOptionHTML {
|
2017-11-22 18:05:54 +00:00
|
|
|
// For fuzzy matching
|
|
|
|
fuseKeys: any[]
|
|
|
|
}
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
type ScoredOption = {
|
|
|
|
index: number,
|
|
|
|
option: CompletionOptionFuse,
|
|
|
|
score: number
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class CompletionSourceFuse extends CompletionSource {
|
|
|
|
public node
|
|
|
|
public options: CompletionOptionFuse[]
|
2017-11-22 18:13:31 +00:00
|
|
|
protected lastExstr: string
|
2017-11-24 18:46:49 +00:00
|
|
|
protected lastFocused: CompletionOption
|
2017-11-22 18:05:54 +00:00
|
|
|
|
2017-11-23 01:09:10 +00:00
|
|
|
protected optionContainer = html`<table class="optionContainer">`
|
2017-11-22 18:05:54 +00:00
|
|
|
|
|
|
|
constructor(private prefixes, className: string, title?: string) {
|
|
|
|
super()
|
|
|
|
this.node = html
|
|
|
|
`<div class="${className} hidden">
|
|
|
|
<div class="sectionHeader">${title || className}</div>
|
|
|
|
</div>`
|
|
|
|
this.node.appendChild(this.optionContainer)
|
2017-11-22 18:13:31 +00:00
|
|
|
this.state = 'hidden'
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
/* abstract onUpdate(query: string, prefix: string, options: CompletionOptionFuse[]) */
|
|
|
|
abstract onInput(exstr: string)
|
2017-11-22 18:05:54 +00:00
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
// Helpful default implementations
|
|
|
|
|
|
|
|
public async filter(exstr: string) {
|
|
|
|
this.lastExstr = exstr
|
|
|
|
this.onInput(exstr)
|
|
|
|
this.updateChain()
|
|
|
|
}
|
2017-11-23 15:44:07 +00:00
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
updateChain(exstr = this.lastExstr, options = this.options) {
|
|
|
|
if (options === undefined) {
|
|
|
|
this.state = 'hidden'
|
|
|
|
return
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
const [prefix, query] = this.splitOnPrefix(exstr)
|
|
|
|
|
2017-11-24 18:46:49 +00:00
|
|
|
// console.log(prefix, query, options)
|
2017-11-22 18:13:31 +00:00
|
|
|
|
|
|
|
// Hide self and stop if prefixes don't match
|
2017-11-22 18:05:54 +00:00
|
|
|
if (prefix) {
|
2017-11-22 18:13:31 +00:00
|
|
|
// Show self if prefix and currently hidden
|
|
|
|
if (this.state === 'hidden') {
|
|
|
|
this.state = 'normal'
|
|
|
|
}
|
2017-11-22 18:05:54 +00:00
|
|
|
} else {
|
|
|
|
this.state = 'hidden'
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
// Filter by query if query is not empty
|
|
|
|
if (query) {
|
|
|
|
this.setStateFromScore(this.scoredOptions(query))
|
|
|
|
// Else show all options
|
|
|
|
} else {
|
|
|
|
options.forEach(option => option.state = 'normal')
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
// Call concrete class
|
2017-11-22 18:13:31 +00:00
|
|
|
this.updateDisplay()
|
|
|
|
}
|
|
|
|
|
|
|
|
select(option: CompletionOption) {
|
|
|
|
if (this.lastExstr !== undefined && option !== undefined) {
|
|
|
|
const [prefix, _] = this.splitOnPrefix(this.lastExstr)
|
|
|
|
this.completion = prefix + option.value
|
|
|
|
option.state = 'focused'
|
2017-11-24 18:46:49 +00:00
|
|
|
this.lastFocused = option
|
2017-11-22 18:13:31 +00:00
|
|
|
} else {
|
|
|
|
throw new Error("lastExstr and option must be defined!")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deselect() {
|
|
|
|
this.completion = undefined
|
2017-11-24 18:46:49 +00:00
|
|
|
if (this.lastFocused != undefined) this.lastFocused.state = "normal"
|
2017-11-22 18:13:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
splitOnPrefix(exstr: string) {
|
|
|
|
for (const prefix of this.prefixes) {
|
|
|
|
if (exstr.startsWith(prefix)) {
|
|
|
|
const query = exstr.replace(prefix, '')
|
|
|
|
return [prefix, query]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [undefined, undefined]
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
2017-11-24 18:46:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
fuseOptions = {
|
|
|
|
keys: ["fuseKeys"],
|
|
|
|
shouldSort: true,
|
|
|
|
id: "index",
|
|
|
|
includeScore: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
// PERF: Could be expensive not to cache Fuse()
|
|
|
|
// yeah, it was.
|
|
|
|
fuse = undefined
|
2017-11-22 18:05:54 +00:00
|
|
|
|
|
|
|
/** Rtn sorted array of {option, score} */
|
2017-11-22 18:13:31 +00:00
|
|
|
scoredOptions(query: string, options = this.options): ScoredOption[] {
|
2017-11-23 15:44:07 +00:00
|
|
|
// This is about as slow.
|
|
|
|
let USE_FUSE = true
|
|
|
|
if (!USE_FUSE){
|
|
|
|
const searchThis = this.options.map(
|
|
|
|
(elem, index) => {
|
|
|
|
return {index, fuseKeys: elem.fuseKeys[0]}
|
|
|
|
})
|
|
|
|
|
|
|
|
return searchThis.map(r => {
|
2017-11-22 18:05:54 +00:00
|
|
|
return {
|
2017-11-23 15:44:07 +00:00
|
|
|
index: r.index,
|
|
|
|
option: this.options[r.index],
|
|
|
|
score: r.fuseKeys.length
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
})
|
2017-11-23 15:44:07 +00:00
|
|
|
} else {
|
|
|
|
|
|
|
|
// Can't sort the real options array because Fuse loses class information.
|
2017-11-24 18:46:49 +00:00
|
|
|
|
|
|
|
if (!this.fuse){
|
|
|
|
let searchThis = this.options.map(
|
|
|
|
(elem, index) => {
|
|
|
|
return {index, fuseKeys: elem.fuseKeys}
|
|
|
|
}
|
|
|
|
)
|
2017-11-23 15:44:07 +00:00
|
|
|
|
2017-11-24 18:46:49 +00:00
|
|
|
this.fuse = new Fuse(searchThis, this.fuseOptions)
|
|
|
|
}
|
|
|
|
return this.fuse.search(query).map(
|
2017-11-23 15:44:07 +00:00
|
|
|
res => {
|
|
|
|
let result = res as any
|
2017-11-24 18:46:49 +00:00
|
|
|
// console.log(result, result.item, query)
|
2017-11-23 15:44:07 +00:00
|
|
|
let index = toNumber(result.item)
|
|
|
|
return {
|
|
|
|
index,
|
|
|
|
option: this.options[index],
|
|
|
|
score: result.score as number
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Set option state by score
|
|
|
|
|
|
|
|
For now just displays all scored elements (see threshold in fuse) and
|
|
|
|
focus the best match.
|
|
|
|
*/
|
2017-11-24 18:46:49 +00:00
|
|
|
setStateFromScore(scoredOpts: ScoredOption[], autoselect = false) {
|
2017-11-22 18:05:54 +00:00
|
|
|
let matches = scoredOpts.map(res => res.index)
|
|
|
|
|
|
|
|
for (const [index, option] of enumerate(this.options)) {
|
|
|
|
if (matches.includes(index)) option.state = 'normal'
|
|
|
|
else option.state = 'hidden'
|
|
|
|
}
|
|
|
|
|
2017-11-24 18:46:49 +00:00
|
|
|
// ideally, this would not deselect anything unless it fell off the list of matches
|
|
|
|
if (matches.length && autoselect) {
|
2017-11-22 18:13:31 +00:00
|
|
|
this.select(this.options[matches[0]])
|
|
|
|
} else {
|
|
|
|
this.deselect()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Call to replace the current display */
|
|
|
|
// TODO: optionContainer.replaceWith and optionContainer.remove don't work.
|
|
|
|
// I don't know why, but it means we can't replace the div in one go. Maybe
|
|
|
|
// an iframe thing.
|
|
|
|
updateDisplay() {
|
|
|
|
/* const newContainer = html`<div>` */
|
|
|
|
|
|
|
|
while (this.optionContainer.hasChildNodes()) {
|
|
|
|
this.optionContainer.removeChild(this.optionContainer.lastChild)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const option of this.options) {
|
|
|
|
/* newContainer.appendChild(option.html) */
|
2017-11-25 13:00:15 +00:00
|
|
|
if (option.state != "hidden") this.optionContainer.appendChild(option.html)
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
2017-11-22 18:13:31 +00:00
|
|
|
|
|
|
|
/* console.log('updateDisplay', this.optionContainer, newContainer) */
|
|
|
|
|
|
|
|
/* let result1 = this.optionContainer.remove() */
|
|
|
|
/* let res2 = this.node.appendChild(newContainer) */
|
|
|
|
/* console.log('results', result1, res2) */
|
2017-11-22 18:05:54 +00:00
|
|
|
}
|
2017-11-23 15:44:07 +00:00
|
|
|
|
2017-11-24 19:00:26 +00:00
|
|
|
next(inc=1){
|
2017-11-24 18:46:49 +00:00
|
|
|
if (this.state != "hidden"){
|
|
|
|
let visopts = this.options.filter((o) => o.state != "hidden")
|
|
|
|
let currind = visopts.findIndex((o) => o.state == "focused")
|
|
|
|
this.deselect()
|
2017-11-24 19:00:26 +00:00
|
|
|
this.select(visopts[currind + inc])
|
2017-11-24 18:46:49 +00:00
|
|
|
return true
|
|
|
|
} else return false
|
2017-11-23 15:44:07 +00:00
|
|
|
}
|
|
|
|
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
// {{{ IMPLEMENTATIONS
|
|
|
|
|
2017-11-23 14:20:44 +00:00
|
|
|
class HistoryCompletionOption extends CompletionOptionHTML implements CompletionOptionFuse {
|
|
|
|
public fuseKeys = []
|
|
|
|
|
|
|
|
constructor(public value: string, page: browser.history.HistoryItem) {
|
|
|
|
super()
|
|
|
|
// Two character buffer properties prefix
|
|
|
|
// Push prefix before padding so we don't match on whitespace
|
|
|
|
|
|
|
|
// Push properties we want to fuzmatch on
|
|
|
|
this.fuseKeys.push(page.title, page.url) // weight by page.visitCount
|
|
|
|
|
|
|
|
// Create HTMLElement
|
|
|
|
// need to download favicon
|
|
|
|
const favIconUrl = DEFAULT_FAVICON
|
|
|
|
// const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
|
|
|
|
this.html = html`<tr class="HistoryCompletionOption option">
|
2017-11-23 15:44:07 +00:00
|
|
|
<td class="prefix">${"".padEnd(2)}</td>
|
|
|
|
<td></td>
|
|
|
|
<td>${page.title}</td>
|
2017-11-23 14:20:44 +00:00
|
|
|
<td><a class="url" target="_blank" href=${page.url}>${page.url}</a></td>
|
|
|
|
</tr>`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-25 13:00:15 +00:00
|
|
|
function sleep(ms: number) {
|
|
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-23 14:20:44 +00:00
|
|
|
export class HistoryCompletionSource extends CompletionSourceFuse {
|
|
|
|
public options: HistoryCompletionOption[]
|
|
|
|
|
|
|
|
// TODO:
|
|
|
|
// - store the exstr and trigger redraws on user or data input without
|
|
|
|
// callback faffery
|
|
|
|
// - sort out the element redrawing.
|
|
|
|
|
|
|
|
constructor(private _parent) {
|
|
|
|
super(
|
|
|
|
[
|
|
|
|
"open ",
|
|
|
|
"tabopen ",
|
|
|
|
"winopen ",
|
|
|
|
],
|
|
|
|
"HistoryCompletionSource", "History"
|
|
|
|
)
|
|
|
|
|
|
|
|
this.updateOptions()
|
|
|
|
this._parent.appendChild(this.node)
|
|
|
|
}
|
|
|
|
|
|
|
|
private async updateOptions(exstr?: string) {
|
|
|
|
/* console.log('updateOptions', this.optionContainer) */
|
2017-11-25 13:00:15 +00:00
|
|
|
// this sleep stops input from being blocked, but also breaks :open until something is typed
|
2017-11-25 23:26:11 +00:00
|
|
|
// await sleep(0)
|
2017-11-23 14:20:44 +00:00
|
|
|
const history: browser.history.HistoryItem[] =
|
|
|
|
await Messaging.message("commandline_background", "history")
|
|
|
|
|
|
|
|
const options = []
|
|
|
|
|
|
|
|
// Get alternative tab, defined as last accessed tab.
|
|
|
|
history.sort((a, b) => { return a.lastVisitTime < b.lastVisitTime ? 1 : -1 })
|
|
|
|
|
|
|
|
for (const page of history) {
|
|
|
|
options.push(new HistoryCompletionOption(
|
|
|
|
page.url,
|
|
|
|
page,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
/* console.log('updateOptions end', this.waiting, this.optionContainer) */
|
|
|
|
this.options = options
|
|
|
|
this.updateChain()
|
|
|
|
}
|
|
|
|
|
|
|
|
async onInput(exstr) {
|
|
|
|
// Schedule an update, if you like. Not very useful for buffers, but
|
|
|
|
// will be for other things.
|
|
|
|
this.updateOptions()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
class BufferCompletionOption extends CompletionOptionHTML implements CompletionOptionFuse {
|
|
|
|
public fuseKeys = []
|
2017-11-15 13:41:04 -08:00
|
|
|
|
|
|
|
constructor(public value: string, tab: browser.tabs.Tab, isAlternative = false) {
|
2017-11-22 18:05:54 +00:00
|
|
|
super()
|
2017-11-15 13:41:04 -08:00
|
|
|
// Two character buffer properties prefix
|
|
|
|
let pre = ""
|
|
|
|
if (tab.active) pre += "%"
|
|
|
|
else if (isAlternative) pre += "#"
|
2017-11-22 18:05:54 +00:00
|
|
|
if (tab.pinned) pre += "@"
|
|
|
|
|
|
|
|
// Push prefix before padding so we don't match on whitespace
|
|
|
|
this.fuseKeys.push(pre)
|
|
|
|
|
|
|
|
// Push properties we want to fuzmatch on
|
|
|
|
this.fuseKeys.push(String(tab.index + 1), tab.title, tab.url)
|
|
|
|
|
|
|
|
// Create HTMLElement
|
2017-11-15 13:41:04 -08:00
|
|
|
const favIconUrl = tab.favIconUrl ? tab.favIconUrl : DEFAULT_FAVICON
|
2017-11-23 01:09:10 +00:00
|
|
|
this.html = html`<tr class="BufferCompletionOption option">
|
|
|
|
<td class="prefix">${pre.padEnd(2)}</td>
|
|
|
|
<td><img src=${favIconUrl} /></td>
|
|
|
|
<td>${tab.index + 1}: ${tab.title}</td>
|
|
|
|
<td><a class="url" target="_blank" href=${tab.url}>${tab.url}</a></td>
|
|
|
|
</tr>`
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
export class BufferCompletionSource extends CompletionSourceFuse {
|
|
|
|
public options: BufferCompletionOption[]
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
// TODO:
|
|
|
|
// - store the exstr and trigger redraws on user or data input without
|
|
|
|
// callback faffery
|
|
|
|
// - sort out the element redrawing.
|
2017-11-15 13:41:04 -08:00
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
constructor(private _parent) {
|
2017-11-22 18:13:31 +00:00
|
|
|
super(
|
|
|
|
[
|
|
|
|
"buffer ",
|
|
|
|
"tabclose ",
|
|
|
|
"tabdetach ",
|
|
|
|
"tabduplicate ",
|
|
|
|
"tabmove ",
|
|
|
|
],
|
|
|
|
"BufferCompletionSource", "Buffers"
|
|
|
|
)
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
this.updateOptions()
|
|
|
|
this._parent.appendChild(this.node)
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
private async updateOptions(exstr?: string) {
|
|
|
|
/* console.log('updateOptions', this.optionContainer) */
|
|
|
|
const tabs: browser.tabs.Tab[] =
|
|
|
|
await Messaging.message("commandline_background", "currentWindowTabs")
|
|
|
|
|
|
|
|
const options = []
|
2017-11-15 13:41:04 -08:00
|
|
|
|
|
|
|
// 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 })
|
|
|
|
|
|
|
|
for (const tab of tabs) {
|
|
|
|
options.push(new BufferCompletionOption(
|
|
|
|
(tab.index + 1).toString(),
|
|
|
|
tab,
|
|
|
|
tab === alt)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
/* console.log('updateOptions end', this.waiting, this.optionContainer) */
|
|
|
|
this.options = options
|
2017-11-22 18:13:31 +00:00
|
|
|
this.updateChain()
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
|
2017-11-22 18:13:31 +00:00
|
|
|
async onInput(exstr) {
|
2017-11-22 18:05:54 +00:00
|
|
|
// Schedule an update, if you like. Not very useful for buffers, but
|
|
|
|
// will be for other things.
|
2017-11-22 18:13:31 +00:00
|
|
|
this.updateOptions()
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
2017-11-24 18:46:49 +00:00
|
|
|
setStateFromScore(scoredOpts: ScoredOption[]){super.setStateFromScore(scoredOpts, true)}
|
2017-11-15 13:41:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// {{{ UNUSED: MANAGING ASYNC CHANGES
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
/** If first to modify epoch, commit change. May want to change epoch after commiting. */
|
2017-11-15 13:41:04 -08:00
|
|
|
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
|
|
|
|
// being called, up to the first await, despite this being an async
|
|
|
|
// function. But I don't know. Should check.
|
|
|
|
const epoch = epochref
|
|
|
|
const res = await asyncFunc(...args)
|
|
|
|
if (epoch === epochref) return commitFunc(res)
|
|
|
|
else console.error(new Error("Update failed: epoch out of date!"))
|
|
|
|
}
|
|
|
|
|
2017-11-22 18:05:54 +00:00
|
|
|
/** Indicate changes to completions we would like.
|
|
|
|
|
|
|
|
This will probably never be used for original designed purpose.
|
|
|
|
*/
|
2017-11-15 13:41:04 -08:00
|
|
|
function updateCompletions(filter: string, sources: CompletionSource[]) {
|
|
|
|
for (let [index, source] of enumerate(sources)) {
|
|
|
|
// Tell each compOpt to filter, and if they finish fast enough they:
|
|
|
|
// 0. Leave a note for any siblings that they got here first
|
|
|
|
// 1. Take over their parent's slot in compOpts
|
|
|
|
// 2. Update their display
|
|
|
|
commitIfCurrent(
|
|
|
|
source.obsolete, // Flag/epoch
|
|
|
|
source.filter, // asyncFunc
|
|
|
|
(childSource) => { // commitFunc
|
|
|
|
source.obsolete = true
|
|
|
|
sources[index] = childSource
|
|
|
|
childSource.activate()
|
|
|
|
},
|
|
|
|
filter // argument to asyncFunc
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// }}}
|