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

324 lines
9.9 KiB
TypeScript

'use strict'
import { Neovim } from '@chemzqm/neovim'
import fs from 'fs'
import path from 'path'
import readline from 'readline'
import { CancellationToken, Disposable, Location, Position, Range } from 'vscode-languageserver-protocol'
import { URI } from 'vscode-uri'
import { ProviderResult } from '../provider'
import { IList, ListAction, ListArgument, ListContext, ListItem, ListTask, LocationWithLine, WorkspaceConfiguration } from '../types'
import { disposeAll } from '../util'
import { readFile } from '../util/fs'
import { comparePosition, emptyRange } from '../util/position'
import workspace from '../workspace'
import CommandTask, { CommandTaskOption } from './commandTask'
import ListConfiguration from './configuration'
const logger = require('../util/logger')('list-basic')
interface ActionOptions {
persist?: boolean
reload?: boolean
parallel?: boolean
tabPersist?: boolean
}
interface ArgumentItem {
hasValue: boolean
name: string
}
interface PreviewConfig {
winid: number
position: string
hlGroup: string
maxHeight: number
name?: string
splitRight: boolean
lnum: number
filetype?: string
range?: Range
scheme?: string
toplineStyle: string
toplineOffset: number
}
export interface PreviewOptions {
bufname?: string
filetype: string
lines: string[]
lnum?: number
range?: Range
sketch?: boolean
}
export default abstract class BasicList implements IList, Disposable {
public name: string
public defaultAction = 'open'
public readonly actions: ListAction[] = []
public options: ListArgument[] = []
protected disposables: Disposable[] = []
private optionMap: Map<string, ArgumentItem>
public config: ListConfiguration
constructor(protected nvim: Neovim) {
this.config = new ListConfiguration()
}
public get alignColumns(): boolean {
return this.config.get('alignColumns', false)
}
protected get hlGroup(): string {
return this.config.get('previewHighlightGroup', 'Search')
}
protected get previewHeight(): number {
return this.config.get('maxPreviewHeight', 12)
}
protected get splitRight(): boolean {
return this.config.get('previewSplitRight', false)
}
protected get toplineStyle(): string {
return this.config.get('previewToplineStyle', 'offset')
}
protected get toplineOffset(): number {
return this.config.get('previewToplineOffset', 3)
}
public parseArguments(args: string[]): { [key: string]: string | boolean } {
if (!this.optionMap) {
this.optionMap = new Map()
for (let opt of this.options) {
let parts = opt.name.split(/,\s*/g).map(s => s.replace(/\s+.*/g, ''))
let name = opt.key ? opt.key : parts[parts.length - 1].replace(/^-/, '')
for (let p of parts) {
this.optionMap.set(p, { name, hasValue: opt.hasValue })
}
}
}
let res: { [key: string]: string | boolean } = {}
for (let i = 0; i < args.length; i++) {
let arg = args[i]
let def = this.optionMap.get(arg)
if (!def) continue
let value: string | boolean = true
if (def.hasValue) {
value = args[i + 1] || ''
i = i + 1
}
res[def.name] = value
}
return res
}
/**
* Get configuration of current list
*/
protected getConfig(): WorkspaceConfiguration {
return workspace.getConfiguration(`list.source.${this.name}`)
}
protected addAction(name: string, fn: (item: ListItem, context: ListContext) => ProviderResult<void>, options?: ActionOptions): void {
this.createAction(Object.assign({
name,
execute: fn
}, options || {}))
}
protected addMultipleAction(name: string, fn: (item: ListItem[], context: ListContext) => ProviderResult<void>, options?: ActionOptions): void {
this.createAction(Object.assign({
name,
multiple: true,
execute: fn
}, options || {}))
}
protected createCommandTask(opt: CommandTaskOption): CommandTask {
return new CommandTask(opt)
}
public addLocationActions(): void {
this.createAction({
name: 'preview',
execute: async (item: ListItem, context: ListContext) => {
let loc = await this.convertLocation(item.location)
await this.previewLocation(loc, context)
}
})
let { nvim } = this
this.createAction({
name: 'quickfix',
multiple: true,
execute: async (items: ListItem[]) => {
let quickfixItems = await Promise.all(items.map(item => this.convertLocation(item.location).then(loc => workspace.getQuickfixItem(loc))))
await nvim.call('setqflist', [quickfixItems])
let openCommand = await nvim.getVar('coc_quickfix_open_command') as string
nvim.command(typeof openCommand === 'string' ? openCommand : 'copen', true)
}
})
for (let name of ['open', 'tabe', 'drop', 'vsplit', 'split']) {
this.createAction({
name,
execute: async (item: ListItem, context: ListContext) => {
await this.jumpTo(item.location, name == 'open' ? null : name, context)
},
tabPersist: name === 'open'
})
}
}
public async convertLocation(location: Location | LocationWithLine | string): Promise<Location> {
if (typeof location == 'string') return Location.create(location, Range.create(0, 0, 0, 0))
if (Location.is(location)) return location
let u = URI.parse(location.uri)
if (u.scheme != 'file') return Location.create(location.uri, Range.create(0, 0, 0, 0))
const rl = readline.createInterface({
input: fs.createReadStream(u.fsPath, { encoding: 'utf8' }),
})
let match = location.line
let n = 0
let resolved = false
let line = await new Promise<string>(resolve => {
rl.on('line', line => {
if (resolved) return
if (line.includes(match)) {
rl.removeAllListeners()
rl.close()
resolved = true
resolve(line)
return
}
n = n + 1
})
rl.on('error', e => {
this.nvim.errWriteLine(`Read ${u.fsPath} error: ${e.message}`)
resolve(null)
})
})
if (line != null) {
let character = location.text ? line.indexOf(location.text) : 0
if (character == 0) character = line.match(/^\s*/)[0].length
let end = Position.create(n, character + (location.text ? location.text.length : 0))
return Location.create(location.uri, Range.create(Position.create(n, character), end))
}
return Location.create(location.uri, Range.create(0, 0, 0, 0))
}
public async jumpTo(location: Location | LocationWithLine | string, command?: string, context?: ListContext): Promise<void> {
if (command == null && context && context.options.position === 'tab') {
command = 'tabe'
}
if (typeof location == 'string') {
await workspace.jumpTo(location, null, command)
return
}
let { range, uri } = await this.convertLocation(location)
let position = range.start
if (position.line == 0 && position.character == 0 && comparePosition(position, range.end) == 0) {
// allow plugin that remember position.
position = null
}
await workspace.jumpTo(uri, position, command)
}
public createAction(action: ListAction): void {
let { name } = action
let idx = this.actions.findIndex(o => o.name == name)
// allow override
if (idx !== -1) this.actions.splice(idx, 1)
this.actions.push(action)
}
protected async previewLocation(location: Location, context: ListContext): Promise<void> {
if (!context.listWindow) return
let { nvim } = this
let { uri, range } = location
let doc = workspace.getDocument(location.uri)
let u = URI.parse(uri)
let lines: string[] = []
if (doc) {
lines = doc.getLines()
} else if (u.scheme == 'file') {
try {
let content = await readFile(u.fsPath, 'utf8')
lines = content.split(/\r?\n/)
} catch (e) {
[`Error on read file ${u.fsPath}`, e.toString()]
}
}
let config: PreviewConfig = {
winid: context.window.id,
range: emptyRange(range) ? null : range,
lnum: range.start.line + 1,
name: u.scheme == 'file' ? u.fsPath : uri,
filetype: toVimFiletype(doc ? doc.languageId : this.getLanguageId(u.fsPath)),
position: context.options.position,
maxHeight: this.previewHeight,
splitRight: this.splitRight,
hlGroup: this.hlGroup,
scheme: u.scheme,
toplineStyle: this.toplineStyle,
toplineOffset: this.toplineOffset,
}
await nvim.call('coc#list#preview', [lines, config])
}
public async preview(options: PreviewOptions, context: ListContext): Promise<void> {
let { nvim } = this
let { bufname, filetype, range, lines, lnum } = options
let config: PreviewConfig = {
winid: context.window.id,
lnum: range ? range.start.line + 1 : lnum || 1,
filetype: filetype || 'txt',
position: context.options.position,
maxHeight: this.previewHeight,
splitRight: this.splitRight,
hlGroup: this.hlGroup,
toplineStyle: this.toplineStyle,
toplineOffset: this.toplineOffset,
}
if (bufname) config.name = bufname
if (range) config.range = range
await nvim.call('coc#list#preview', [lines, config])
nvim.command('redraw', true)
}
public abstract loadItems(context: ListContext, token?: CancellationToken): Promise<ListItem[] | ListTask | null | undefined>
public doHighlight(): void {
// noop
}
public dispose(): void {
disposeAll(this.disposables)
}
/**
* Get filetype by check same extension name buffer.
*/
private getLanguageId(filepath: string): string {
let extname = path.extname(filepath)
if (!extname) return ''
for (let doc of workspace.documents) {
let fsPath = URI.parse(doc.uri).fsPath
if (path.extname(fsPath) == extname) {
return doc.languageId
}
}
return ''
}
}
export function toVimFiletype(filetype: string): string {
switch (filetype) {
case 'latex':
// LaTeX (LSP language ID 'latex') has Vim filetype 'tex'
return 'tex'
default:
return filetype
}
}