'use strict' import { Neovim } from '@chemzqm/neovim' import { CancellationTokenSource, Disposable, DocumentHighlight, DocumentHighlightKind, Position, Range } from 'vscode-languageserver-protocol' import events from '../events' import languages from '../languages' import Document from '../model/document' import { ConfigurationChangeEvent, HandlerDelegate } from '../types' import { disposeAll } from '../util' import workspace from '../workspace' const logger = require('../util/logger')('documentHighlight') interface HighlightConfig { priority: number timeout: number } /** * Highlight same symbols on current window. * Highlights are added to window by matchaddpos. */ export default class Highlights { private config: HighlightConfig private disposables: Disposable[] = [] private tokenSource: CancellationTokenSource private highlights: Map = new Map() private timer: NodeJS.Timer constructor(private nvim: Neovim, private handler: HandlerDelegate) { events.on(['CursorMoved', 'CursorMovedI'], () => { this.cancel() this.clearHighlights() }, null, this.disposables) this.getConfiguration() workspace.onDidChangeConfiguration(this.getConfiguration, this, this.disposables) } private getConfiguration(e?: ConfigurationChangeEvent): void { let config = workspace.getConfiguration('documentHighlight') if (!e || e.affectsConfiguration('documentHighlight')) { this.config = Object.assign(this.config || {}, { priority: config.get('priority', -1), timeout: config.get('timeout', 300) }) } } public isEnabled(bufnr: number, cursors: number): boolean { let doc = workspace.getDocument(bufnr) if (!doc || !doc.attached || cursors) return false if (!languages.hasProvider('documentHighlight', doc.textDocument)) return false return true } public clearHighlights(): void { if (this.highlights.size == 0) return for (let winid of this.highlights.keys()) { let win = this.nvim.createWindow(winid) win.clearMatchGroup('^CocHighlight') } this.highlights.clear() } public async highlight(): Promise { let { nvim } = this this.cancel() let [bufnr, winid, pos, cursors] = await nvim.eval(`[bufnr("%"),win_getid(),coc#cursor#position(),get(b:,'coc_cursors_activated',0)]`) as [number, number, [number, number], number] if (!this.isEnabled(bufnr, cursors)) return let doc = workspace.getDocument(bufnr) let highlights = await this.getHighlights(doc, Position.create(pos[0], pos[1])) if (!highlights) return let groups: { [index: string]: Range[] } = {} for (let hl of highlights) { if (!hl.range) continue let hlGroup = hl.kind == DocumentHighlightKind.Text ? 'CocHighlightText' : hl.kind == DocumentHighlightKind.Read ? 'CocHighlightRead' : 'CocHighlightWrite' groups[hlGroup] = groups[hlGroup] || [] groups[hlGroup].push(hl.range) } let win = nvim.createWindow(winid) nvim.pauseNotification() win.clearMatchGroup('^CocHighlight') for (let hlGroup of Object.keys(groups)) { win.highlightRanges(hlGroup, groups[hlGroup], this.config.priority, true) } nvim.resumeNotification(true, true) this.highlights.set(winid, highlights) } public async getSymbolsRanges(): Promise { let { doc, position } = await this.handler.getCurrentState() this.handler.checkProvier('documentHighlight', doc.textDocument) let highlights = await this.getHighlights(doc, position) if (!highlights) return null return highlights.map(o => o.range) } public hasHighlights(winid: number): boolean { return this.highlights.get(winid) != null } public async getHighlights(doc: Document, position: Position): Promise { let line = doc.getline(position.line) let ch = line[position.character] if (!ch || !doc.isWord(ch)) return null await doc.synchronize() this.cancel() let source = this.tokenSource = new CancellationTokenSource() let timer = this.timer = setTimeout(() => { if (source.token.isCancellationRequested) return source.cancel() }, this.config.timeout) let highlights = await languages.getDocumentHighLight(doc.textDocument, position, source.token) clearTimeout(timer) if (source.token.isCancellationRequested) return null return highlights } private cancel(): void { if (this.tokenSource) { this.tokenSource.cancel() this.tokenSource.dispose() this.tokenSource = null } } public dispose(): void { if (this.timer) clearTimeout(this.timer) this.cancel() this.highlights.clear() disposeAll(this.disposables) } }