1
0
Fork 0
mirror of synced 2024-06-25 18:21:11 -04:00
ultimate-vim/sources_non_forked/coc.nvim/src/handler/format.ts
2022-07-20 13:20:15 +08:00

213 lines
8.4 KiB
TypeScript

'use strict'
import { Neovim } from '@chemzqm/neovim'
import { CancellationTokenSource, Position, Range, TextEdit } from 'vscode-languageserver-protocol'
import commandManager from '../commands'
import events from '../events'
import languages from '../languages'
import Document from '../model/document'
import snippetManager from '../snippets/manager'
import { ConfigurationChangeEvent, HandlerDelegate } from '../types'
import { isWord } from '../util/string'
import window from '../window'
import workspace from '../workspace'
const logger = require('../util/logger')('handler-format')
const pairs: Map<string, string> = new Map([
['<', '>'],
['>', '<'],
['{', '}'],
['[', ']'],
['(', ')'],
])
interface FormatPreferences {
formatOnType: boolean
formatOnTypeFiletypes: string[]
formatOnSaveFiletypes: string[]
bracketEnterImprove: boolean
}
export default class FormatHandler {
private preferences: FormatPreferences
constructor(
private nvim: Neovim,
private handler: HandlerDelegate
) {
this.loadPreferences()
handler.addDisposable(workspace.onDidChangeConfiguration(this.loadPreferences, this))
handler.addDisposable(workspace.onWillSaveTextDocument(event => {
let { languageId } = event.document
let filetypes = this.preferences.formatOnSaveFiletypes
if (filetypes.includes(languageId) || filetypes.includes('*')) {
let willSaveWaitUntil = async (): Promise<TextEdit[] | undefined> => {
if (!languages.hasFormatProvider(event.document)) {
logger.warn(`Format provider not found for ${event.document.uri}`)
return undefined
}
let options = await workspace.getFormatOptions(event.document.uri)
let tokenSource = new CancellationTokenSource()
let timer: NodeJS.Timer
const tp = new Promise<undefined>(c => {
timer = setTimeout(() => {
logger.warn(`Format on save ${event.document.uri} timeout after 0.5s`)
tokenSource.cancel()
c(undefined)
}, 500)
})
const provideEdits = languages.provideDocumentFormattingEdits(event.document, options, tokenSource.token)
let textEdits = await Promise.race([tp, provideEdits])
clearTimeout(timer)
return Array.isArray(textEdits) ? textEdits : undefined
}
event.waitUntil(willSaveWaitUntil())
}
}))
let enterTs: number
let enterBufnr: number
handler.addDisposable(events.on('Enter', async bufnr => {
enterTs = Date.now()
enterBufnr = bufnr
}))
handler.addDisposable(events.on('CursorMovedI', async bufnr => {
if (bufnr == enterBufnr && Date.now() - enterTs < 100) {
enterBufnr = undefined
await this.handleEnter(bufnr)
}
}))
handler.addDisposable(events.on('TextInsert', async (bufnr: number, info, character: string) => {
if (!events.pumvisible) await this.tryFormatOnType(character, bufnr)
}))
handler.addDisposable(commandManager.registerCommand('editor.action.formatDocument', async (uri?: string | number) => {
const doc = uri ? workspace.getDocument(uri) : (await this.handler.getCurrentState()).doc
await this.documentFormat(doc)
}))
commandManager.titles.set('editor.action.formatDocument', 'Format Document')
}
private loadPreferences(e?: ConfigurationChangeEvent): void {
if (!e || e.affectsConfiguration('coc.preferences')) {
let config = workspace.getConfiguration('coc.preferences')
this.preferences = {
formatOnType: config.get<boolean>('formatOnType', false),
formatOnSaveFiletypes: config.get<string[]>('formatOnSaveFiletypes', []),
formatOnTypeFiletypes: config.get('formatOnTypeFiletypes', []),
bracketEnterImprove: config.get<boolean>('bracketEnterImprove', true),
}
}
}
private async tryFormatOnType(ch: string, bufnr: number, newLine = false): Promise<void> {
if (!ch || isWord(ch) || !this.preferences.formatOnType) return
if (snippetManager.getSession(bufnr) != null) return
let doc = workspace.getDocument(bufnr)
if (!doc || !doc.attached || doc.isCommandLine) return
const filetypes = this.preferences.formatOnTypeFiletypes
if (filetypes.length && !filetypes.includes(doc.filetype) && !filetypes.includes('*')) {
// Only check formatOnTypeFiletypes when set, avoid breaking change
return
}
if (!languages.hasProvider('formatOnType', doc.textDocument)) {
logger.warn(`Format on type provider not found for buffer: ${doc.uri}`)
return
}
if (!languages.canFormatOnType(ch, doc.textDocument)) return
let position: Position
let edits = await this.handler.withRequestToken('Format on type', async token => {
position = await window.getCursorPosition()
let origLine = doc.getline(position.line - 1)
// not format for empty line.
if (newLine && /^\s*$/.test(origLine)) return
await doc.synchronize()
return await languages.provideDocumentOnTypeEdits(ch, doc.textDocument, position, token)
})
if (!edits || !edits.length) return
await doc.applyEdits(edits, false, true)
}
public async formatCurrentBuffer(): Promise<boolean> {
let { doc } = await this.handler.getCurrentState()
return await this.documentFormat(doc)
}
public async formatCurrentRange(mode: string): Promise<number> {
let { doc } = await this.handler.getCurrentState()
return await this.documentRangeFormat(doc, mode)
}
public async documentFormat(doc: Document): Promise<boolean> {
await doc.synchronize()
if (!languages.hasFormatProvider(doc.textDocument)) {
throw new Error(`Format provider not found for buffer: ${doc.bufnr}`)
}
let options = await workspace.getFormatOptions(doc.uri)
let textEdits = await this.handler.withRequestToken('format', token => {
return languages.provideDocumentFormattingEdits(doc.textDocument, options, token)
})
if (textEdits && textEdits.length > 0) {
await doc.applyEdits(textEdits, false, true)
return true
}
return false
}
private async handleEnter(bufnr: number): Promise<void> {
let { nvim } = this
let { bracketEnterImprove } = this.preferences
await this.tryFormatOnType('\n', bufnr)
if (bracketEnterImprove) {
let line = (await nvim.call('line', '.') as number) - 1
let doc = workspace.getDocument(bufnr)
if (!doc) return
await doc.patchChange()
let pre = doc.getline(line - 1)
let curr = doc.getline(line)
let prevChar = pre[pre.length - 1]
if (prevChar && pairs.has(prevChar)) {
let nextChar = curr.trim()[0]
if (nextChar && pairs.get(prevChar) == nextChar) {
let edits: TextEdit[] = []
let opts = await workspace.getFormatOptions(doc.uri)
let space = opts.insertSpaces ? ' '.repeat(opts.tabSize) : '\t'
let currIndent = curr.match(/^\s*/)[0]
let pos: Position = Position.create(line - 1, pre.length)
// make sure indent of current line
if (doc.filetype == 'vim') {
let newText = '\n' + currIndent + space
edits.push({ range: Range.create(line, currIndent.length, line, currIndent.length), newText: ' \\ ' })
newText = newText + '\\ '
edits.push({ range: Range.create(pos, pos), newText })
await doc.applyEdits(edits)
await window.moveTo(Position.create(line, newText.length - 1))
} else {
await nvim.eval(`feedkeys("\\<Esc>O", 'in')`)
}
}
}
}
}
public async documentRangeFormat(doc: Document, mode?: string): Promise<number> {
this.handler.checkProvier('formatRange', doc.textDocument)
await doc.synchronize()
let range: Range
if (mode) {
range = await window.getSelectedRange(mode)
if (!range) return -1
} else {
let [lnum, count, mode] = await this.nvim.eval("[v:lnum,v:count,mode()]") as [number, number, string]
// we can't handle
if (count == 0 || mode == 'i' || mode == 'R') return -1
range = Range.create(lnum - 1, 0, lnum - 1 + count, 0)
}
let options = await workspace.getFormatOptions(doc.uri)
let textEdits = await this.handler.withRequestToken('Format range', token => {
return languages.provideDocumentRangeFormattingEdits(doc.textDocument, range, options, token)
})
if (textEdits && textEdits.length > 0) {
await doc.applyEdits(textEdits, false, true)
return 0
}
return -1
}
}