Skip to content

Commit

Permalink
Add initial support for auto close jsx tags
Browse files Browse the repository at this point in the history
Fixes #34307
  • Loading branch information
mjbvz committed Jul 10, 2018
1 parent dc137ce commit adfa9ce
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 31 deletions.
10 changes: 10 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
146 changes: 118 additions & 28 deletions extensions/typescript-language-features/src/features/tagCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

vscode.window.onDidChangeActiveTextEditor(
() => this.updateEnabledState(),
null, this.disposables);

this.updateEnabledState();
}

public dispose() {
disposeAll(this.disposables);
this._disposed = true;
this.timeout = undefined;
}

private updateEnabledState() {

async provideCompletionItems(
}

private onDidChangeTextDocument(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
_context: vscode.CompletionContext
): Promise<vscode.CompletionItem[] | undefined> {
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 undefined;
return;
}

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;
if (typeof this.timeout !== 'undefined') {
clearTimeout(this.timeout);
}

return [this.getCompletion(body)];
}
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);

private getCompletion(body: Proto.TextInsertion) {
const completion = new vscode.CompletionItem(body.newText);
completion.insertText = this.getTagSnippet(body);
return completion;
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 {
Expand All @@ -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))));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit adfa9ce

Please sign in to comment.