1
0
Fork 0
mirror of synced 2024-06-26 02:31:09 -04:00
ultimate-vim/sources_non_forked/coc.nvim/src/list/session.ts
2022-07-20 13:20:15 +08:00

583 lines
18 KiB
TypeScript

'use strict'
import { Buffer, Neovim, Window } from '@chemzqm/neovim'
import debounce from 'debounce'
import { Disposable } from 'vscode-languageserver-protocol'
import extensions from '../extensions'
import Highlighter from '../model/highligher'
import { IList, ListAction, ListContext, ListItem, ListMode, ListOptions, Matcher } from '../types'
import { disposeAll, wait } from '../util'
import window from '../window'
import workspace from '../workspace'
import ListConfiguration from './configuration'
import InputHistory from './history'
import Prompt from './prompt'
import UI from './ui'
import Worker, { getItemHighlights } from './worker'
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
const logger = require('../util/logger')('list-session')
/**
* Activated list session with UI and worker
*/
export default class ListSession {
public readonly history: InputHistory
public readonly ui: UI
public readonly worker: Worker
private cwd: string
private loadingFrame = ''
private timer: NodeJS.Timer
private hidden = false
private disposables: Disposable[] = []
private savedHeight: number
private window: Window
private buffer: Buffer
private interactiveDebounceTime: number
/**
* Original list arguments.
*/
private args: string[] = []
constructor(
private nvim: Neovim,
private prompt: Prompt,
private list: IList,
public readonly listOptions: ListOptions,
private listArgs: string[] = [],
private config: ListConfiguration
) {
this.ui = new UI(nvim, list.name, listOptions, config)
this.history = new InputHistory(prompt, list.name)
this.worker = new Worker(nvim, list, prompt, listOptions, {
interactiveDebounceTime: config.get<number>('interactiveDebounceTime', 100),
extendedSearchMode: config.get<boolean>('extendedSearchMode', true)
})
this.interactiveDebounceTime = config.get<number>('interactiveDebounceTime', 100)
let debouncedChangeLine = debounce(async () => {
let [previewing, currwin, lnum] = await nvim.eval('[coc#list#has_preview(),win_getid(),line(".")]') as [number, number, number]
if (previewing && currwin == this.winid) {
let idx = this.ui.lnumToIndex(lnum)
await this.doPreview(idx)
}
}, 50)
this.disposables.push({
dispose: () => {
debouncedChangeLine.clear()
}
})
this.ui.onDidChangeLine(debouncedChangeLine, null, this.disposables)
this.ui.onDidChangeLine(this.resolveItem, this, this.disposables)
this.ui.onDidLineChange(this.resolveItem, this, this.disposables)
let debounced = debounce(async () => {
this.updateStatus()
let { autoPreview } = this.listOptions
if (!autoPreview) {
let [previewing, mode] = await nvim.eval('[coc#list#has_preview(),mode()]') as [number, string]
if (!previewing || mode != 'n') return
}
await this.doAction('preview')
}, 50)
this.disposables.push({
dispose: () => {
debounced.clear()
}
})
this.ui.onDidLineChange(debounced, null, this.disposables)
this.ui.onDidOpen(async () => {
if (typeof this.list.doHighlight == 'function') {
this.list.doHighlight()
}
if (workspace.isVim) this.prompt.drawPrompt()
if (this.listOptions.first) {
await this.doAction()
}
}, null, this.disposables)
this.ui.onDidClose(async () => {
await this.hide()
}, null, this.disposables)
this.ui.onDidDoubleClick(async () => {
await this.doAction()
}, null, this.disposables)
this.worker.onDidChangeItems(async ({ items, reload, append, finished }) => {
if (this.hidden) return
if (append) {
await this.ui.appendItems(items)
} else {
let height = this.config.get<number>('height', 10)
if (finished && !listOptions.interactive && listOptions.input.length == 0) {
height = Math.min(items.length, height)
}
await this.ui.drawItems(items, Math.max(1, height), reload)
}
}, null, this.disposables)
let start = 0
let timer: NodeJS.Timeout
let interval: NodeJS.Timeout
this.disposables.push(Disposable.create(() => {
if (timer) clearTimeout(timer)
if (interval) clearInterval(interval)
}))
this.worker.onDidChangeLoading(loading => {
if (this.hidden) return
if (timer) clearTimeout(timer)
if (loading) {
start = Date.now()
interval = setInterval(() => {
let idx = Math.floor((Date.now() - start) % 1000 / 100)
this.loadingFrame = frames[idx]
this.updateStatus()
}, 100)
} else {
timer = setTimeout(() => {
this.loadingFrame = ''
if (interval) clearInterval(interval)
interval = null
this.updateStatus()
}, Math.max(0, 200 - (Date.now() - start)))
}
}, null, this.disposables)
}
public async start(args: string[]): Promise<void> {
this.args = args
this.cwd = workspace.cwd
this.hidden = false
let { listOptions, listArgs } = this
let res = await this.nvim.eval('[win_getid(),bufnr("%"),winheight("%")]')
this.listArgs = listArgs
this.history.load(listOptions.input || '')
this.window = this.nvim.createWindow(res[0])
this.buffer = this.nvim.createBuffer(res[1])
this.savedHeight = res[2]
await this.worker.loadItems(this.context)
}
public async reloadItems(): Promise<void> {
if (!this.ui.winid) return
await this.worker.loadItems(this.context, true)
}
public async call(fname: string): Promise<any> {
await this.nvim.call('coc#prompt#stop_prompt', ['list'])
let targets = await this.ui.getItems()
let context = {
name: this.name,
args: this.listArgs,
input: this.prompt.input,
winid: this.window?.id,
bufnr: this.buffer?.id,
targets
}
let res = await this.nvim.call(fname, [context])
this.prompt.start()
return res
}
public async chooseAction(): Promise<void> {
let { nvim, defaultAction } = this
let { actions } = this.list
let names: string[] = actions.map(o => o.name)
let idx = names.indexOf(defaultAction.name)
if (idx != -1) {
names.splice(idx, 1)
names.unshift(defaultAction.name)
}
let shortcuts: Set<string> = new Set()
let choices: string[] = []
let invalids: string[] = []
let menuAction = workspace.env.dialog && this.config.get('menuAction', false)
for (let name of names) {
let i = 0
for (let ch of name) {
if (!shortcuts.has(ch)) {
shortcuts.add(ch)
choices.push(`${name.slice(0, i)}&${name.slice(i)}`)
break
}
i++
}
if (i == name.length) {
invalids.push(name)
}
}
if (invalids.length && !menuAction) {
names = names.filter(s => !invalids.includes(s))
}
let n: number
if (menuAction) {
nvim.call('coc#prompt#stop_prompt', ['list'], true)
n = await window.showMenuPicker(names, { title: 'Choose action', shortcuts: true })
n = n + 1
if (workspace.isVim) await wait(10)
this.prompt.start()
} else {
await nvim.call('coc#prompt#stop_prompt', ['list'])
n = await nvim.call('confirm', ['Choose action:', choices.join('\n')]) as number
await wait(10)
this.prompt.start()
}
if (n) await this.doAction(names[n - 1])
}
public async doAction(name?: string): Promise<void> {
let { list } = this
let action: ListAction
if (name != null) {
action = list.actions.find(o => o.name == name)
if (!action) {
void window.showErrorMessage(`Action ${name} not found`)
return
}
} else {
action = this.defaultAction
}
let items: ListItem[]
if (name == 'preview') {
let item = await this.ui.item
items = item ? [item] : []
} else {
items = await this.ui.getItems()
}
if (items.length) await this.doItemAction(items, action)
}
private async doPreview(index: number): Promise<void> {
let item = this.ui.getItem(index)
let action = this.list.actions.find(o => o.name == 'preview')
if (!item || !action) return
await this.doItemAction([item], action)
}
public async first(): Promise<void> {
await this.doDefaultAction(0)
}
public async last(): Promise<void> {
await this.doDefaultAction(this.ui.length - 1)
}
public async previous(): Promise<void> {
await this.doDefaultAction(this.ui.index - 1)
}
public async next(): Promise<void> {
await this.doDefaultAction(this.ui.index + 1)
}
private async doDefaultAction(index: number): Promise<void> {
let { ui } = this
let item = ui.getItem(index)
if (!item) return
ui.index = index
await this.doItemAction([item], this.defaultAction)
await ui.echoMessage(item)
}
/**
* list name
*/
public get name(): string {
return this.list.name
}
/**
* Window id used by list.
*
* @returns {number | undefined}
*/
public get winid(): number | undefined {
return this.ui.winid
}
public get length(): number {
return this.ui.length
}
public get defaultAction(): ListAction {
let { defaultAction, actions, name } = this.list
let config = workspace.getConfiguration(`list.source.${name}`)
let action: ListAction
if (config.defaultAction) action = actions.find(o => o.name == config.defaultAction)
if (!action) action = actions.find(o => o.name == defaultAction)
if (!action) action = actions[0]
if (!action) throw new Error(`default action "${defaultAction}" not found`)
return action
}
public async hide(notify = false): Promise<void> {
if (this.hidden) return
let { nvim, timer, window } = this
let { winid, tabnr } = this.ui
if (timer) clearTimeout(timer)
this.worker.stop()
this.history.add()
this.ui.reset()
this.hidden = true
let { isVim } = workspace
nvim.pauseNotification()
if (!isVim) nvim.call('coc#prompt#stop_prompt', ['list'], true)
if (tabnr) nvim.call('coc#list#close_preview', [tabnr], true)
if (window) nvim.call('win_gotoid', [window.id], true)
if (winid) nvim.call('coc#window#close', [winid], true)
if (window && this.savedHeight && this.listOptions.position !== 'tab') {
nvim.call('coc#window#set_height', [window.id, this.savedHeight], true)
}
if (notify) return nvim.resumeNotification(false, true)
await nvim.resumeNotification(false)
if (isVim) {
// otherwise we could receive <esc> for new list.
await wait(10)
nvim.call('feedkeys', ['\x1b', 'int'], true)
nvim.command('redraw', true)
}
}
public toggleMode(): void {
let mode: ListMode = this.prompt.mode == 'normal' ? 'insert' : 'normal'
this.prompt.mode = mode
this.listOptions.mode = mode
this.updateStatus()
}
public stop(): void {
this.worker.stop()
}
private async resolveItem(): Promise<void> {
let index = this.ui.index
let item = this.ui.getItem(index)
if (!item || item.resolved) return
let { list } = this
if (typeof list.resolveItem == 'function') {
let label = item.label
let resolved = await Promise.resolve(list.resolveItem(item))
if (resolved && index == this.ui.index) {
let highlights = getItemHighlights(this.prompt.input, resolved)
this.ui.updateItem(Object.assign({ highlights }, resolved), index, label != resolved.label)
}
}
}
public async showHelp(): Promise<void> {
await this.hide()
let { list, nvim } = this
if (!list) return
nvim.pauseNotification()
nvim.command(`tabe +setl\\ previewwindow [LIST HELP]`, true)
nvim.command('setl nobuflisted noswapfile buftype=nofile bufhidden=wipe', true)
await nvim.resumeNotification()
let hasOptions = list.options && list.options.length
let buf = await nvim.buffer
let highligher = new Highlighter()
highligher.addLine('NAME', 'Label')
highligher.addLine(` ${list.name} - ${list.description || ''}\n`)
highligher.addLine('SYNOPSIS', 'Label')
highligher.addLine(` :CocList [LIST OPTIONS] ${list.name}${hasOptions ? ' [ARGUMENTS]' : ''}\n`)
if (list.detail) {
highligher.addLine('DESCRIPTION', 'Label')
let lines = list.detail.split('\n').map(s => ' ' + s)
highligher.addLine(lines.join('\n') + '\n')
}
if (hasOptions) {
highligher.addLine('ARGUMENTS', 'Label')
highligher.addLine('')
for (let opt of list.options) {
highligher.addLine(opt.name, 'Special')
highligher.addLine(` ${opt.description}`)
highligher.addLine('')
}
highligher.addLine('')
}
let config = workspace.getConfiguration(`list.source.${list.name}`)
if (Object.keys(config).length) {
highligher.addLine('CONFIGURATIONS', 'Label')
highligher.addLine('')
let props = {}
extensions.all.forEach(extension => {
let { packageJSON } = extension
let { contributes } = packageJSON
if (!contributes) return
let { configuration } = contributes
if (configuration) {
let { properties } = configuration
if (properties) {
for (let key of Object.keys(properties)) {
props[key] = properties[key]
}
}
}
})
for (let key of Object.keys(config)) {
let val = config[key]
let name = `list.source.${list.name}.${key}`
let description = props[name] && props[name].description ? props[name].description : key
highligher.addLine(` "${name}"`, 'MoreMsg')
highligher.addText(` - ${description}, current value: `)
highligher.addText(JSON.stringify(val), 'Special')
}
highligher.addLine('')
}
highligher.addLine('ACTIONS', 'Label')
highligher.addLine(` ${list.actions.map(o => o.name).join(', ')}`)
highligher.addLine('')
highligher.addLine(`see ':h coc-list-options' for available list options.`, 'Comment')
nvim.pauseNotification()
highligher.render(buf, 0, -1)
nvim.command('setl nomod', true)
nvim.command('setl nomodifiable', true)
nvim.command('normal! gg', true)
nvim.command('nnoremap <buffer> q :bd!<CR>', true)
await nvim.resumeNotification()
}
public async switchMatcher(): Promise<void> {
let { matcher, interactive } = this.listOptions
if (interactive) return
const list: Matcher[] = ['fuzzy', 'strict', 'regex']
let idx = list.indexOf(matcher) + 1
if (idx >= list.length) idx = 0
this.listOptions.matcher = list[idx]
this.prompt.matcher = list[idx]
await this.worker.drawItems()
}
private updateStatus(): void {
let { ui, list, nvim } = this
if (!ui.bufnr) return
let buf = nvim.createBuffer(ui.bufnr)
let status = {
mode: this.prompt.mode.toUpperCase(),
args: this.args.join(' '),
name: list.name,
cwd: this.cwd,
loading: this.loadingFrame,
total: this.worker.length
}
buf.setVar('list_status', status, true)
nvim.command('redraws', true)
}
public get context(): ListContext {
let { winid } = this.ui
return {
options: this.listOptions,
args: this.listArgs,
input: this.prompt.input,
cwd: workspace.cwd,
window: this.window,
buffer: this.buffer,
listWindow: winid ? this.nvim.createWindow(winid) : undefined
}
}
public onMouseEvent(key): Promise<void> {
switch (key) {
case '<LeftMouse>':
return this.ui.onMouse('mouseDown')
case '<LeftDrag>':
return this.ui.onMouse('mouseDrag')
case '<LeftRelease>':
return this.ui.onMouse('mouseUp')
case '<2-LeftMouse>':
return this.ui.onMouse('doubleClick')
}
}
public async doNumberSelect(ch: string): Promise<boolean> {
if (!this.listOptions.numberSelect) return false
let code = ch.charCodeAt(0)
if (code >= 48 && code <= 57) {
let n = Number(ch)
if (n == 0) n = 10
if (this.ui.length >= n) {
this.nvim.pauseNotification()
this.ui.setCursor(n)
await this.nvim.resumeNotification()
await this.doAction()
return true
}
}
return false
}
public jumpBack(): void {
let { window, nvim } = this
if (window) {
nvim.pauseNotification()
nvim.call('coc#prompt#stop_prompt', ['list'], true)
this.nvim.call('win_gotoid', [window.id], true)
nvim.resumeNotification(false, true)
}
}
public async resume(): Promise<void> {
if (this.winid) await this.hide()
let res = await this.nvim.eval('[win_getid(),bufnr("%"),winheight("%")]')
this.hidden = false
this.window = this.nvim.createWindow(res[0])
this.buffer = this.nvim.createBuffer(res[1])
this.savedHeight = res[2]
this.prompt.start()
await this.ui.resume()
if (this.listOptions.autoPreview) {
await this.doAction('preview')
}
}
private async doItemAction(items: ListItem[], action: ListAction): Promise<void> {
let { noQuit, position } = this.listOptions
let { nvim } = this
let persistAction = action.persist === true || action.name == 'preview'
if (position === 'tab' && action.tabPersist) persistAction = true
let persist = this.winid && (persistAction || noQuit)
try {
if (persist) {
if (!persistAction) {
nvim.pauseNotification()
nvim.call('coc#prompt#stop_prompt', ['list'], true)
nvim.call('win_gotoid', [this.context.window.id], true)
await nvim.resumeNotification()
}
} else {
await this.hide()
}
if (action.multiple) {
await Promise.resolve(action.execute(items, this.context))
} else if (action.parallel) {
await Promise.all(items.map(item => Promise.resolve(action.execute(item, this.context))))
} else {
for (let item of items) {
await Promise.resolve(action.execute(item, this.context))
}
}
if (persist) this.ui.restoreWindow()
if (action.reload && persist) {
await this.reloadItems()
} else if (persist) {
this.nvim.command('redraw', true)
}
} catch (e) {
this.nvim.echoError(e)
}
}
public onInputChange(): void {
if (this.timer) clearTimeout(this.timer)
this.listOptions.input = this.prompt.input
// reload or filter items
if (this.listOptions.interactive) {
this.worker.stop()
this.timer = setTimeout(async () => {
await this.worker.loadItems(this.context)
}, this.interactiveDebounceTime)
} else {
void this.worker.drawItems()
}
}
public dispose(): void {
void this.hide(true)
disposeAll(this.disposables)
this.worker.dispose()
this.ui.dispose()
}
}