Add initial support for auto close jsx tags

Fixes #34307
This commit is contained in:
Matt Bierner 2018-07-09 18:59:59 -07:00
parent dc137ce28f
commit adfa9ce977
5 changed files with 137 additions and 36 deletions

View file

@ -505,6 +505,16 @@
"default": "prompt",
"description": "%typescript.updateImportsOnFileMove.enabled%",
"scope": "resource"
},
"typescript.autoClosingTags": {
"type": "boolean",
"default": true,
"description": "%typescript.autoClosingTags%"
},
"javascript.autoClosingTags": {
"type": "boolean",
"default": true,
"description": "%typescript.autoClosingTags%"
}
}
},

View file

@ -55,5 +55,6 @@
"typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: 'single' quotes, 'double' quotes, or 'auto' infer quote type from existing imports. Requires using TypeScript 2.9 or newer in the workspace.",
"typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports:\n- \"relative\" to the file location.\n- \"non-relative\" based on the 'baseUrl' configured in your 'jsconfig.json' / 'tsconfig.json'.\n- \"auto\" infer the shortest path type.\nRequires using TypeScript 2.9 or newer in the workspace.",
"typescript.showUnused": "Enable/disable highlighting of unused variables in code. Requires using TypeScript 2.9 or newer in the workspace.",
"typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires using TypeScript 2.9 or newer in the workspace."
"typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires using TypeScript 2.9 or newer in the workspace.",
"typescript.autoClosingTags": "Enable/disable automatic closing of JSX tags. Requires using TypeScript 3.0 or newer in the workspace."
}

View file

@ -7,44 +7,104 @@ import * as vscode from 'vscode';
import * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
import API from '../utils/api';
import { VersionDependentRegistration } from '../utils/dependentRegistration';
import { VersionDependentRegistration, ConfigurationDependentRegistration, ConditionalRegistration } from '../utils/dependentRegistration';
import { disposeAll } from '../utils/dispose';
import * as typeConverters from '../utils/typeConverters';
class TypeScriptTagCompletion implements vscode.CompletionItemProvider {
class TagClosing {
private _disposed = false;
private timeout: NodeJS.Timer | undefined = undefined;
private readonly disposables: vscode.Disposable[] = [];
constructor(
private readonly client: ITypeScriptServiceClient
) { }
) {
vscode.workspace.onDidChangeTextDocument(
event => this.onDidChangeTextDocument(event.document, event.contentChanges),
null, this.disposables);
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
_context: vscode.CompletionContext
): Promise<vscode.CompletionItem[] | undefined> {
const filepath = this.client.toPath(document.uri);
if (!filepath) {
return undefined;
}
vscode.window.onDidChangeActiveTextEditor(
() => this.updateEnabledState(),
null, this.disposables);
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
let body: Proto.TextInsertion | undefined = undefined;
try {
const response = await this.client.execute('jsxClosingTag', args, token);
body = response && response.body;
if (!body) {
return undefined;
}
} catch {
return undefined;
}
return [this.getCompletion(body)];
this.updateEnabledState();
}
private getCompletion(body: Proto.TextInsertion) {
const completion = new vscode.CompletionItem(body.newText);
completion.insertText = this.getTagSnippet(body);
return completion;
public dispose() {
disposeAll(this.disposables);
this._disposed = true;
this.timeout = undefined;
}
private updateEnabledState() {
}
private onDidChangeTextDocument(
document: vscode.TextDocument,
changes: vscode.TextDocumentContentChangeEvent[]
) {
const activeDocument = vscode.window.activeTextEditor && vscode.window.activeTextEditor.document;
if (document !== activeDocument || changes.length === 0) {
return;
}
const filepath = this.client.toPath(document.uri);
if (!filepath) {
return;
}
if (typeof this.timeout !== 'undefined') {
clearTimeout(this.timeout);
}
const lastChange = changes[changes.length - 1];
const lastCharacter = lastChange.text[lastChange.text.length - 1];
if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') {
return;
}
const rangeStart = lastChange.range.start;
const version = document.version;
this.timeout = setTimeout(async () => {
if (this._disposed) {
return;
}
let position = new vscode.Position(rangeStart.line, rangeStart.character + lastChange.text.length);
let body: Proto.TextInsertion | undefined = undefined;
const args: Proto.JsxClosingTagRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
try {
const response = await this.client.execute('jsxClosingTag', args, null as any);
body = response && response.body;
if (!body) {
return;
}
} catch {
return;
}
if (!this._disposed) {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor) {
const activeDocument = activeEditor.document;
if (document === activeDocument && activeDocument.version === version) {
const selections = activeEditor.selections;
const snippet = this.getTagSnippet(body);
if (selections.length && selections.some(s => s.active.isEqual(position))) {
activeEditor.insertSnippet(snippet, selections.map(s => s.active));
} else {
activeEditor.insertSnippet(snippet, position);
}
}
}
}
this.timeout = void 0;
}, 100);
}
private getTagSnippet(closingTag: Proto.TextInsertion): vscode.SnippetString {
@ -55,12 +115,42 @@ class TypeScriptTagCompletion implements vscode.CompletionItemProvider {
}
}
export class ActiveDocumentDependentRegistration {
private readonly _registration: ConditionalRegistration;
private readonly _disposables: vscode.Disposable[] = [];
constructor(
private readonly selector: vscode.DocumentSelector,
register: () => vscode.Disposable,
) {
this._registration = new ConditionalRegistration(register);
this.update();
vscode.window.onDidChangeActiveTextEditor(() => {
this.update();
}, null, this._disposables);
}
public dispose() {
disposeAll(this._disposables);
this._registration.dispose();
}
private update() {
const editor = vscode.window.activeTextEditor;
const enabled = !!(editor && vscode.languages.match(this.selector, editor.document));
this._registration.update(enabled);
}
}
export function register(
selector: vscode.DocumentSelector,
modeId: string,
client: ITypeScriptServiceClient,
) {
return new VersionDependentRegistration(client, API.v300, () =>
vscode.languages.registerCompletionItemProvider(selector,
new TypeScriptTagCompletion(client),
'>'));
new ConfigurationDependentRegistration(modeId, 'autoClosingTags', () =>
new ActiveDocumentDependentRegistration(selector, () =>
new TagClosing(client))));
}

View file

@ -91,7 +91,7 @@ export default class LanguageProvider {
this.disposables.push((await import('./features/referencesCodeLens')).register(selector, this.description.id, this.client, cachedResponse));
this.disposables.push((await import('./features/rename')).register(selector, this.client));
this.disposables.push((await import('./features/signatureHelp')).register(selector, this.client));
this.disposables.push((await import('./features/tagCompletion')).register(selector, this.client));
this.disposables.push((await import('./features/tagCompletion')).register(selector, this.description.id, this.client));
this.disposables.push((await import('./features/typeDefinitions')).register(selector, this.client));
this.disposables.push((await import('./features/workspaceSymbols')).register(this.client, this.description.modeIds));
}

View file

@ -8,7 +8,7 @@ import { ITypeScriptServiceClient } from '../typescriptService';
import API from './api';
import { disposeAll } from './dispose';
class ConditionalRegistration {
export class ConditionalRegistration {
private registration: vscode.Disposable | undefined = undefined;
public constructor(