feat: support tagClosing for JSX (#277)

This commit is contained in:
Raidou 2021-04-07 18:31:04 +08:00 committed by GitHub
parent f0a9f46dad
commit 579e8920a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 217 additions and 68 deletions

View file

@ -141,6 +141,7 @@ for guide of coc.nvim's configuration.
- `typescript.validate.enable`:Enable/disable TypeScript validation., default:
`true`
- `typescript.showUnused`: show unused variable hint, default: `true`.
- `typescript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `false`.
- `typescript.suggest.enabled` default: `true`
- `typescript.suggest.paths`:Enable/disable suggest paths in import statement
and require calls, default: `true`
@ -176,6 +177,7 @@ for guide of coc.nvim's configuration.
- `typescript.suggest.includeAutomaticOptionalChainCompletions`: default: `true`
- `javascript.format.enabled`: Enable/disable format for javascript files.
- `javascript.showUnused`: show unused variable hint.
- `javascript.autoClosingTags`: Enable/disable autoClosing of JSX tags, default: `false`.
- `javascript.updateImportsOnFileMove.enable` default: `true`
- `javascript.implementationsCodeLens.enable` default: `true`
- `javascript.referencesCodeLens.enable` default: `true`

View file

@ -431,6 +431,10 @@
"default": "allOpenProjects",
"scope": "window"
},
"typescript.autoClosingTags": {
"type": "boolean",
"default": false
},
"javascript.showUnused": {
"type": "boolean",
"default": true,
@ -593,6 +597,10 @@
"description": "%configuration.suggest.includeAutomaticOptionalChainCompletions%",
"scope": "resource"
},
"javascript.autoClosingTags": {
"type": "boolean",
"default": false
},
"javascript.format.semicolons": {
"type": "string",
"default": "ignore",

View file

@ -0,0 +1,201 @@
/*---------------------------------------------------------------------------------------------
* 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 * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import API from '../utils/api'
import SnippetString from '../utils/SnippetString'
import * as typeConverters from '../utils/typeConverters'
export default class TagClosing implements Disposable {
public static readonly minVersion = API.v300
private static _configurationLanguages: Record<string, string> = {
'javascriptreact': 'javascript',
'typescriptreact': 'typescript',
}
private _disposables: Disposable[] = []
private _enabled: boolean = false
private _disposed = false
private _timeout: NodeJS.Timer | undefined = undefined
private _cancel: CancellationTokenSource | undefined = undefined
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(),
})
}
async updateEnabledState(): Promise<void> {
this._enabled = false
const doc = await workspace.document
if (!doc) {
return
}
const document = doc.textDocument
const configLang = TagClosing._configurationLanguages[document.languageId]
if (!configLang || configLang !== this.descriptionLanguageId) {
return
}
if (!workspace.getConfiguration(undefined, document.uri).get<boolean>(`${configLang}.autoClosingTags`)) {
return
}
this._enabled = true
}
public dispose() {
this._disposed = true
if (this._timeout) {
clearTimeout(this._timeout)
this._timeout = undefined
}
if (this._cancel) {
this._cancel.cancel()
this._cancel.dispose()
this._cancel = undefined
}
disposeAll(this._disposables)
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;
}
}

View file

@ -1,54 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'coc.nvim'
import { CompletionItemProvider } from 'coc.nvim'
import { CancellationToken, CompletionContext, CompletionItem, Position } from 'vscode-languageserver-protocol'
import * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import * as typeConverters from '../utils/typeConverters'
export default class TypeScriptTagCompletion implements CompletionItemProvider {
constructor(
private readonly client: ITypeScriptServiceClient
) {}
public async provideCompletionItems(
document: TextDocument,
position: Position,
token: CancellationToken,
context: CompletionContext
): Promise<CompletionItem[] | undefined> {
const filepath = this.client.toPath(document.uri)
if (!filepath) return undefined
if (context.triggerCharacter != '>') {
return undefined
}
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position)
let body: Proto.TextInsertion | undefined
try {
const response = await this.client.execute('jsxClosingTag', args, token)
body = response && (response as any).body
if (!body) {
return undefined
}
} catch {
return undefined
}
return [this.getCompletion(body)]
}
private getCompletion(body: Proto.TextInsertion): CompletionItem {
const completion = CompletionItem.create(body.newText)
completion.insertText = this.getTagSnippet(body) // tslint:disable-line
return completion
}
private getTagSnippet(closingTag: Proto.TextInsertion): string {
let { newText, caretOffset } = closingTag
return newText.slice(0, caretOffset) + '$0' + newText.slice(caretOffset)
}
}

View file

@ -2,9 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { disposeAll, languages, Uri, workspace } from 'coc.nvim'
import { disposeAll, languages, TextDocument, Uri, workspace } from 'coc.nvim'
import path from 'path'
import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable, TextDocument } from 'vscode-languageserver-protocol'
import { CodeActionKind, Diagnostic, DiagnosticSeverity, Disposable } from 'vscode-languageserver-protocol'
import { CachedNavTreeResponse } from './features/baseCodeLensProvider'
import CompletionItemProvider from './features/completionItemProvider'
import DefinitionProvider from './features/definitionProvider'
@ -19,7 +19,6 @@ import HoverProvider from './features/hover'
import ImplementationsCodeLensProvider from './features/implementationsCodeLens'
import ImportfixProvider from './features/importFix'
import InstallModuleProvider from './features/moduleInstall'
// import TagCompletionProvider from './features/tagCompletion'
import QuickfixProvider from './features/quickfix'
import RefactorProvider from './features/refactor'
import ReferenceProvider from './features/references'
@ -27,6 +26,7 @@ import ReferencesCodeLensProvider from './features/referencesCodeLens'
import RenameProvider from './features/rename'
import SignatureHelpProvider from './features/signatureHelp'
import SmartSelection from './features/smartSelect'
import TagClosing from './features/tagClosing'
import UpdateImportsOnFileRenameHandler from './features/updatePathOnRename'
import { OrganizeImportsCodeActionProvider } from './organizeImports'
import TypeScriptServiceClient from './typescriptServiceClient'
@ -147,17 +147,9 @@ export default class LanguageProvider {
if (this.client.apiVersion.gte(API.v350)) {
this._register(languages.registerSelectionRangeProvider(languageIds, new SmartSelection(this.client)))
}
// if (this.client.apiVersion.gte(API.v300)) {
// this._register(
// languages.registerCompletionItemProvider(
// `tsserver-${this.description.id}-tag`,
// 'TSC',
// languageIds,
// new TagCompletionProvider(client),
// ['>']
// )
// )
// }
if (this.client.apiVersion.gte(API.v300)) {
this._register(new TagClosing(this.client, this.description.id))
}
}
public handles(resource: string, doc: TextDocument): boolean {