refactor tagClosing feature

Avoid workspace.registerAutocmd
This commit is contained in:
Qiming Zhao 2021-08-16 23:03:10 +08:00
parent 66ae279b1a
commit 12c3a08776
2 changed files with 68 additions and 146 deletions

View file

@ -2,8 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource, Disposable, disposeAll, Position, Range, snippetManager, window, workspace } from 'coc.nvim'
import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'
import { CancellationTokenSource, Disposable, disposeAll, Position, Range, snippetManager, events, workspace, InsertChange } from 'coc.nvim'
import * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import API from '../utils/api'
@ -19,49 +18,82 @@ export default class TagClosing implements Disposable {
}
private _disposables: Disposable[] = []
private _enabled: boolean = false
private _disposed = false
private _timeout: NodeJS.Timer | undefined = undefined
private _cancel: CancellationTokenSource | undefined = undefined
private lastInsert: string
constructor(
private readonly client: ITypeScriptServiceClient,
private readonly descriptionLanguageId: string
) {
workspace.onDidChangeTextDocument(
(event) =>
this.onDidChangeTextDocument(
event.textDocument,
event.contentChanges
),
null,
this._disposables
)
this.updateEnabledState()
workspace.registerAutocmd({
event: ['BufEnter'],
request: false,
callback: () => this.updateEnabledState(),
})
events.on('InsertCharPre', character => {
this.lastInsert = character
}, null, this._disposables)
events.on('TextChangedI', this.onChange, this, this._disposables)
events.on('TextChangedP', this.onChange, this, this._disposables)
}
async updateEnabledState(): Promise<void> {
this._enabled = false
const doc = await workspace.document
if (!doc) {
return
private async onChange(bufnr: number, change: InsertChange): Promise<void> {
let doc = workspace.getDocument((bufnr))
if (!doc || !doc.attached) return
let enabled = this.isEnabled(doc.filetype, doc.uri)
if (!enabled) return
let { pre, changedtick, lnum } = change
if (!pre.endsWith('/') && !pre.endsWith('>')) return
if (!pre.endsWith(this.lastInsert)) return
if (pre.length > 1 && pre[pre.length - 2] == '>') return
const filepath = this.client.toOpenedFilePath(doc.uri)
if (!filepath) return
if (this._timeout) {
clearTimeout(this._timeout)
}
const document = doc.textDocument
const configLang = TagClosing._configurationLanguages[document.languageId]
if (this._cancel) {
this._cancel.cancel()
this._cancel.dispose()
this._cancel = undefined
}
await (doc as any).patchChange()
this._timeout = setTimeout(async () => {
this._timeout = undefined
if (this._disposed) return
if (doc.changedtick > changedtick) return
const position = Position.create(lnum - 1, pre.length)
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(
filepath,
position
)
this._cancel = new CancellationTokenSource()
const response = await this.client.execute(
'jsxClosingTag',
args,
this._cancel.token
)
if (response.type !== 'response' || !response.body) {
return
}
if (this._disposed) return
const insertion = response.body
if (doc.changedtick === changedtick) {
snippetManager.insertSnippet(
this.getTagSnippet(insertion).value,
false,
Range.create(position, position)
)
}
}, 50)
}
private isEnabled(languageId: string, uri: string): boolean {
const configLang = TagClosing._configurationLanguages[languageId]
if (!configLang || configLang !== this.descriptionLanguageId) {
return
return false
}
if (!workspace.getConfiguration(undefined, document.uri).get<boolean>(`${configLang}.autoClosingTags`)) {
return
if (!workspace.getConfiguration(undefined, uri).get<boolean>(`${configLang}.autoClosingTags`)) {
return false
}
this._enabled = true
return true
}
public dispose() {
@ -82,120 +114,10 @@ export default class TagClosing implements Disposable {
this._disposables = []
}
private async onDidChangeTextDocument(
documentEvent: {
uri: string,
version: number,
},
changes: readonly TextDocumentContentChangeEvent[]
) {
if (!this._enabled) {
return
}
const document = await workspace.document
if (!document) {
return
}
const activeDocument = document.textDocument
if (activeDocument.uri !== documentEvent.uri || changes.length === 0) {
return
}
const filepath = this.client.toOpenedFilePath(documentEvent.uri)
if (!filepath) {
return
}
if (typeof this._timeout !== 'undefined') {
clearTimeout(this._timeout)
}
if (this._cancel) {
this._cancel.cancel()
this._cancel.dispose()
this._cancel = undefined
}
const lastChange = changes[changes.length - 1]
if (!Range.is(lastChange['range']) || !lastChange.text) {
return
}
const lastCharacter = lastChange.text[lastChange.text.length - 1]
if (lastCharacter !== '>' && lastCharacter !== '/') {
return
}
const version = documentEvent.version
const rangeStart = lastChange['range'].start
const priorCharacter =
lastChange['range'].start.character > 0
? activeDocument.getText(
Range.create(
Position.create(rangeStart.line, rangeStart.character - 1),
rangeStart
)
)
: ''
if (priorCharacter === '>') {
return
}
this._timeout = setTimeout(async () => {
this._timeout = undefined
if (this._disposed) {
return
}
const addedLines = lastChange.text.split(/\r\n|\n/g)
const position =
addedLines.length <= 1
? Position.create(
rangeStart.line,
rangeStart.character + lastChange.text.length
)
: Position.create(
rangeStart.line + addedLines.length - 1,
addedLines[addedLines.length - 1].length
)
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(
filepath,
position
)
this._cancel = new CancellationTokenSource()
const response = await this.client.execute(
'jsxClosingTag',
args,
this._cancel.token
)
if (response.type !== 'response' || !response.body) {
return
}
if (this._disposed) {
return
}
const insertion = response.body;
if (
documentEvent.uri === activeDocument.uri &&
activeDocument.version === version
) {
snippetManager.insertSnippet(
this.getTagSnippet(insertion).value,
false,
Range.create(position, position)
)
}
}, 100);
}
private getTagSnippet(closingTag: Proto.TextInsertion): SnippetString {
const snippet = new SnippetString();
snippet.appendPlaceholder('', 0);
snippet.appendText(closingTag.newText);
return snippet;
const snippet = new SnippetString()
snippet.appendPlaceholder('', 0)
snippet.appendText(closingTag.newText)
return snippet
}
}

View file

@ -44,7 +44,7 @@ export default class LanguageProvider {
public client: TypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager,
private description: LanguageDescription,
private typingsStatus: TypingsStatus
typingsStatus: TypingsStatus
) {
workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables)
this.configurationChanged()