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

283 lines
8.3 KiB
TypeScript

'use strict'
import { Buffer, Neovim } from '@chemzqm/neovim'
import { CancellationToken, Disposable, Emitter, Event } from 'vscode-languageserver-protocol'
import events from '../events'
import { HighlightItem } from '../types'
import { disposeAll } from '../util'
import { byteLength, isAlphabet } from '../util/string'
import { DialogPreferences } from './dialog'
import Popup from './popup'
const logger = require('../util/logger')('model-menu')
export interface MenuItem {
text: string
disabled?: boolean | { reason: string }
}
export interface MenuConfig {
items: string[] | MenuItem[]
title?: string
content?: string
shortcuts?: boolean
position?: 'cursor' | 'center'
borderhighlight?: string
}
export function isMenuItem(item: any): item is MenuItem {
if (!item) return false
return typeof item.text === 'string'
}
/**
* Select single item from menu at cursor position.
*/
export default class Menu {
private bufnr: number
private win: Popup
private currIndex = 0
private contentHeight = 0
private total: number
private disposables: Disposable[] = []
private keyMappings: Map<string, (character: string) => void> = new Map()
private shortcutIndexes: Set<number> = new Set()
private _disposed = false
private readonly _onDidClose = new Emitter<number>()
public readonly onDidClose: Event<number> = this._onDidClose.event
constructor(private nvim: Neovim, private config: MenuConfig, token?: CancellationToken) {
this.total = config.items.length
if (token) {
token.onCancellationRequested(() => {
if (this.win) {
this.win?.close()
} else {
this._onDidClose.fire(-1)
this.dispose()
}
})
}
this.disposables.push(this._onDidClose)
this.addKeymappings()
}
private attachEvents(): void {
events.on('InputChar', this.onInputChar.bind(this), null, this.disposables)
events.on('BufWinLeave', bufnr => {
if (bufnr == this.bufnr) {
this._onDidClose.fire(-1)
this.dispose()
}
}, null, this.disposables)
}
private addKeymappings(): void {
let { nvim } = this
this.addKeys(['<esc>', '<C-c>'], () => {
this._onDidClose.fire(-1)
this.dispose()
})
this.addKeys(['\r', '<cr>'], () => {
this.selectCurrent()
})
let setCursorIndex = idx => {
if (!this.win) return
nvim.pauseNotification()
this.setCursor(idx + this.contentHeight)
this.win?.refreshScrollbar()
nvim.command('redraw', true)
nvim.resumeNotification(false, true)
}
this.addKeys('<C-f>', async () => {
await this.win?.scrollForward()
})
this.addKeys('<C-b>', async () => {
await this.win?.scrollBackward()
})
this.addKeys(['j', '<down>', '<tab>', '<C-n>'], () => {
// next
let idx = this.currIndex == this.total - 1 ? 0 : this.currIndex + 1
setCursorIndex(idx)
})
this.addKeys(['k', '<up>', '<s-tab>', '<C-p>'], () => {
// previous
let idx = this.currIndex == 0 ? this.total - 1 : this.currIndex - 1
setCursorIndex(idx)
})
this.addKeys(['g'], () => {
setCursorIndex(0)
})
this.addKeys(['G'], () => {
setCursorIndex(this.total - 1)
})
let timer: NodeJS.Timeout
let firstNumber: number
const choose = (n: number) => {
let disabled = this.isDisabled(n)
if (disabled) return
this._onDidClose.fire(n)
this.dispose()
}
this.addKeys(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], character => {
if (timer) clearTimeout(timer)
let n = parseInt(character, 10)
if (isNaN(n) || n > this.total) return
if (firstNumber == null && n == 0) return
if (firstNumber) {
let count = firstNumber * 10 + n
firstNumber = undefined
choose(count - 1)
return
}
if (this.total < 10 || n * 10 > this.total) {
choose(n - 1)
return
}
timer = setTimeout(async () => {
choose(n - 1)
}, 200)
firstNumber = n
})
if (this.config.shortcuts) {
this.addShortcuts(choose)
}
}
private addShortcuts(choose: (idx: number) => void): void {
let { items } = this.config
let texts: string[] = items.map(o => {
return isMenuItem(o) ? o.text : o
})
texts.forEach((text, idx) => {
if (text.length) {
let s = text[0]
if (isAlphabet(s.charCodeAt(0)) && !this.keyMappings.has(s)) {
this.shortcutIndexes.add(idx)
this.addKeys(s, () => {
choose(idx)
})
}
}
})
}
private isDisabled(idx: number): boolean {
let { items } = this.config
let item = items[idx]
if (isMenuItem(item) && item.disabled) {
return true
}
return false
}
public async show(preferences: DialogPreferences = {}): Promise<void> {
let { nvim, shortcutIndexes } = this
let { title, items, borderhighlight, position, content } = this.config
let opts: any = {}
if (title) opts.title = title
if (position === 'center') opts.relative = 'editor'
if (preferences.maxHeight) opts.maxHeight = preferences.maxHeight
if (preferences.maxWidth) opts.maxWidth = preferences.maxWidth
if (preferences.floatHighlight) opts.highlight = preferences.floatHighlight
if (borderhighlight) {
opts.borderhighlight = [borderhighlight]
} else if (preferences.floatBorderHighlight) {
opts.borderhighlight = [preferences.floatBorderHighlight]
}
if (preferences.rounded) opts.rounded = 1
if (typeof content === 'string') opts.content = content
let highlights: HighlightItem[] = []
let lines = items.map((v, i) => {
let text: string = isMenuItem(v) ? v.text : v
let pre = i < 99 ? `${i + 1}. ` : ''
// if (i < 99) return `${i + 1}. ${text.trim()}`
if (shortcutIndexes.has(i)) {
highlights.push({
lnum: i,
hlGroup: preferences.shortcutHighlight || 'MoreMsg',
colStart: byteLength(pre),
colEnd: byteLength(pre) + 1
})
}
return pre + text.trim()
})
lines.forEach((line, i) => {
let item = items[i]
if (isMenuItem(item) && item.disabled) {
highlights.push({
hlGroup: 'CocDisabled',
lnum: i,
colStart: 0,
colEnd: byteLength(line)
})
}
})
if (highlights.length) opts.highlights = highlights
if (preferences.confirmKey && preferences.confirmKey != '<cr>') {
this.addKeys(preferences.confirmKey, () => {
this.selectCurrent()
})
}
let res = await nvim.call('coc#dialog#create_menu', [lines, opts]) as [number, number, number]
if (!res) throw new Error('Unable to create menu window')
nvim.command('redraw', true)
if (this._disposed) return
this.win = new Popup(nvim, res[0], res[1], lines.length + res[2], res[2])
this.bufnr = res[1]
this.contentHeight = res[2]
this.attachEvents()
nvim.call('coc#prompt#start_prompt', ['menu'], true)
}
private selectCurrent(): void {
if (this.isDisabled(this.currIndex)) {
let item = this.config.items[this.currIndex] as MenuItem
if (item.disabled['reason']) {
this.nvim.outWriteLine(`Item disabled: ${item.disabled['reason']}`)
}
return
}
this._onDidClose.fire(this.currIndex)
this.dispose()
}
public get buffer(): Buffer {
return this.bufnr ? this.nvim.createBuffer(this.bufnr) : undefined
}
public dispose(): void {
this._disposed = true
disposeAll(this.disposables)
this.shortcutIndexes.clear()
this.keyMappings.clear()
this.nvim.call('coc#prompt#stop_prompt', ['menu'], true)
this.win?.close()
this.bufnr = undefined
this.win = undefined
}
private async onInputChar(session: string, character: string): Promise<void> {
if (session != 'menu' || !this.win) return
let fn = this.keyMappings.get(character)
if (fn) {
await Promise.resolve(fn(character))
} else {
logger.warn(`Ignored key press: ${character}`)
}
}
private setCursor(index: number): void {
if (!this.win) return
this.currIndex = index - this.contentHeight
this.win.setCursor(index)
}
private addKeys(keys: string | string[], fn: (character: string) => void): void {
if (Array.isArray(keys)) {
for (let key of keys) {
this.keyMappings.set(key, fn)
}
} else {
this.keyMappings.set(keys, fn)
}
}
}