From 12c3a087766305f51e8fc7b65d11c3b81a0d83bd Mon Sep 17 00:00:00 2001 From: Qiming Zhao Date: Mon, 16 Aug 2021 23:03:10 +0800 Subject: [PATCH] refactor tagClosing feature Avoid workspace.registerAutocmd --- src/server/features/tagClosing.ts | 212 ++++++++++-------------------- src/server/languageProvider.ts | 2 +- 2 files changed, 68 insertions(+), 146 deletions(-) diff --git a/src/server/features/tagClosing.ts b/src/server/features/tagClosing.ts index b55c127..bb53810 100644 --- a/src/server/features/tagClosing.ts +++ b/src/server/features/tagClosing.ts @@ -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 { - this._enabled = false - const doc = await workspace.document - if (!doc) { - return + private async onChange(bufnr: number, change: InsertChange): Promise { + 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(`${configLang}.autoClosingTags`)) { - return + if (!workspace.getConfiguration(undefined, uri).get(`${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 } } diff --git a/src/server/languageProvider.ts b/src/server/languageProvider.ts index bce2bd7..efdb855 100644 --- a/src/server/languageProvider.ts +++ b/src/server/languageProvider.ts @@ -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()