diff --git a/extensions/html/client/src/htmlMain.ts b/extensions/html/client/src/htmlMain.ts
index 82a1d0ffc59..b100b35a052 100644
--- a/extensions/html/client/src/htmlMain.ts
+++ b/extensions/html/client/src/htmlMain.ts
@@ -6,10 +6,11 @@
import * as path from 'path';
-import { languages, workspace, ExtensionContext, IndentAction } from 'vscode';
-import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Range, RequestType } from 'vscode-languageclient';
+import { languages, workspace, ExtensionContext, IndentAction, Position, TextDocument } from 'vscode';
+import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Range as LSRange, RequestType, TextDocumentPositionParams } from 'vscode-languageclient';
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
import { activateColorDecorations } from './colorDecorators';
+import { activateTagClosing } from './tagClosing';
import TelemetryReporter from 'vscode-extension-telemetry';
import { ConfigurationFeature } from 'vscode-languageclient/lib/proposed';
@@ -18,7 +19,11 @@ import * as nls from 'vscode-nls';
let localize = nls.loadMessageBundle();
namespace ColorSymbolRequest {
- export const type: RequestType = new RequestType('css/colorSymbols');
+ export const type: RequestType = new RequestType('html/colorSymbols');
+}
+
+namespace TagCloseRequest {
+ export const type: RequestType = new RequestType('html/tag');
}
interface IPackageInfo {
@@ -28,10 +33,13 @@ interface IPackageInfo {
}
export function activate(context: ExtensionContext) {
+ let toDispose = context.subscriptions;
let packageInfo = getPackageInfo(context);
let telemetryReporter: TelemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
- context.subscriptions.push(telemetryReporter);
+ if (telemetryReporter) {
+ toDispose.push(telemetryReporter);
+ }
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'htmlServerMain.js'));
@@ -64,7 +72,7 @@ export function activate(context: ExtensionContext) {
client.registerFeature(new ConfigurationFeature(client));
let disposable = client.start();
- context.subscriptions.push(disposable);
+ toDispose.push(disposable);
client.onReady().then(() => {
let colorRequestor = (uri: string) => {
return client.sendRequest(ColorSymbolRequest.type, uri).then(ranges => ranges.map(client.protocol2CodeConverter.asRange));
@@ -73,12 +81,21 @@ export function activate(context: ExtensionContext) {
return workspace.getConfiguration().get('css.colorDecorators.enable');
};
let disposable = activateColorDecorations(colorRequestor, { html: true, handlebars: true, razor: true }, isDecoratorEnabled);
- context.subscriptions.push(disposable);
- client.onTelemetry(e => {
+ toDispose.push(disposable);
+
+ let tagRequestor = (document: TextDocument, position: Position) => {
+ let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
+ return client.sendRequest(TagCloseRequest.type, param);
+ };
+ disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true, razor: true }, 'html.autoClosingTags.enable');
+ toDispose.push(disposable);
+
+ disposable = client.onTelemetry(e => {
if (telemetryReporter) {
telemetryReporter.sendTelemetryEvent(e.key, e.data);
}
});
+ toDispose.push(disposable);
});
languages.setLanguageConfiguration('html', {
diff --git a/extensions/html/client/src/tagClosing.ts b/extensions/html/client/src/tagClosing.ts
new file mode 100644
index 00000000000..45434196834
--- /dev/null
+++ b/extensions/html/client/src/tagClosing.ts
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+'use strict';
+
+import { window, workspace, Disposable, TextDocumentContentChangeEvent, TextDocument, Position, SnippetString } from 'vscode';
+
+export function activateTagClosing(tagProvider: (document: TextDocument, position: Position) => Thenable, supportedLanguages: { [id: string]: boolean }, configName: string): Disposable {
+
+ let disposables: Disposable[] = [];
+ workspace.onDidChangeTextDocument(event => onDidChangeTextDocument(event.document, event.contentChanges), null, disposables);
+
+ let isEnabled = false;
+ updateEnabledState();
+ window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
+
+ let timeout: NodeJS.Timer = void 0;
+
+ function updateEnabledState() {
+ isEnabled = false;
+ let editor = window.activeTextEditor;
+ if (!editor) {
+ return;
+ }
+ let document = editor.document;
+ if (!supportedLanguages[document.languageId]) {
+ return;
+ }
+ if (!workspace.getConfiguration(void 0, document.uri).get(configName)) {
+ return;
+ }
+ isEnabled = true;
+ }
+
+ function onDidChangeTextDocument(document: TextDocument, changes: TextDocumentContentChangeEvent[]) {
+ if (!isEnabled) {
+ return;
+ }
+ let activeDocument = window.activeTextEditor && window.activeTextEditor.document;
+ if (document !== activeDocument || changes.length === 0) {
+ return;
+ }
+ if (typeof timeout !== 'undefined') {
+ clearTimeout(timeout);
+ }
+ let lastChange = changes[changes.length - 1];
+ let lastCharacter = lastChange.text[lastChange.text.length - 1];
+ if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') {
+ return;
+ }
+ let rangeStart = lastChange.range.start;
+ let version = document.version;
+ timeout = setTimeout(() => {
+ let position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
+ tagProvider(document, position).then(text => {
+ if (text && isEnabled) {
+ let activeDocument = window.activeTextEditor && window.activeTextEditor.document;
+ if (document === activeDocument && activeDocument.version === version) {
+ window.activeTextEditor.insertSnippet(new SnippetString(text), position);
+ }
+ }
+ });
+ }, 100);
+ }
+ return Disposable.from(...disposables);
+}
\ No newline at end of file
diff --git a/extensions/html/package.json b/extensions/html/package.json
index a06789f08a2..fdbdc533d84 100644
--- a/extensions/html/package.json
+++ b/extensions/html/package.json
@@ -193,6 +193,12 @@
"default": true,
"description": "%html.validate.styles%"
},
+ "html.autoClosingTags.enable": {
+ "type": "boolean",
+ "scope": "resource",
+ "default": true,
+ "description": "%html.autoClosingTags.enable%"
+ },
"html.trace.server": {
"type": "string",
"scope": "window",
diff --git a/extensions/html/package.nls.json b/extensions/html/package.nls.json
index b27892fe954..757a2f57b3b 100644
--- a/extensions/html/package.nls.json
+++ b/extensions/html/package.nls.json
@@ -1,5 +1,5 @@
{
- "html.format.enable.desc": "Enable/disable default HTML formatter (requires restart)",
+ "html.format.enable.desc": "Enable/disable default HTML formatter",
"html.format.wrapLineLength.desc": "Maximum amount of characters per line (0 = disable).",
"html.format.unformatted.desc": "List of tags, comma separated, that shouldn't be reformatted. 'null' defaults to all tags listed at https://www.w3.org/TR/html5/dom.html#phrasing-content.",
"html.format.contentUnformatted.desc": "List of tags, comma separated, where the content shouldn't be reformatted. 'null' defaults to the 'pre' tag.",
@@ -18,5 +18,6 @@
"html.suggest.ionic.desc": "Configures if the built-in HTML language support suggests Ionic tags, properties and values.",
"html.suggest.html5.desc":"Configures if the built-in HTML language support suggests HTML5 tags, properties and values.",
"html.validate.scripts": "Configures if the built-in HTML language support validates embedded scripts.",
- "html.validate.styles": "Configures if the built-in HTML language support validates embedded styles."
+ "html.validate.styles": "Configures if the built-in HTML language support validates embedded styles.",
+ "html.autoClosingTags.enable": "Enable/disable autoclosing of HTML tags."
}
\ No newline at end of file
diff --git a/extensions/html/server/npm-shrinkwrap.json b/extensions/html/server/npm-shrinkwrap.json
index 8853dfc48b4..18dcf812c0b 100644
--- a/extensions/html/server/npm-shrinkwrap.json
+++ b/extensions/html/server/npm-shrinkwrap.json
@@ -8,9 +8,9 @@
"resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.1.3.tgz"
},
"vscode-html-languageservice": {
- "version": "2.0.5",
+ "version": "2.0.7",
"from": "vscode-html-languageservice@next",
- "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.5.tgz"
+ "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.7.tgz"
},
"vscode-jsonrpc": {
"version": "3.3.1",
diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json
index 3fe6263139d..deba35c6b2a 100644
--- a/extensions/html/server/package.json
+++ b/extensions/html/server/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"vscode-css-languageservice": "^2.1.3",
- "vscode-html-languageservice": "^2.0.5",
+ "vscode-html-languageservice": "^2.0.7",
"vscode-languageserver": "3.4.0-next.4",
"vscode-languageserver-types": "^3.3.0",
"vscode-nls": "^2.0.2",
diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts
index 1ea94f267a3..425127c6c9f 100644
--- a/extensions/html/server/src/htmlServerMain.ts
+++ b/extensions/html/server/src/htmlServerMain.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
-import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType, DocumentRangeFormattingRequest, Disposable, DocumentSelector, GetConfigurationParams } from 'vscode-languageserver';
+import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType, DocumentRangeFormattingRequest, Disposable, DocumentSelector, GetConfigurationParams, TextDocumentPositionParams } from 'vscode-languageserver';
import { DocumentContext } from 'vscode-html-languageservice';
import { TextDocument, Diagnostic, DocumentLink, Range, SymbolInformation } from 'vscode-languageserver-types';
import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes';
@@ -22,9 +22,14 @@ import * as nls from 'vscode-nls';
nls.config(process.env['VSCODE_NLS_CONFIG']);
namespace ColorSymbolRequest {
- export const type: RequestType = new RequestType('css/colorSymbols');
+ export const type: RequestType = new RequestType('html/colorSymbols');
}
+namespace TagCloseRequest {
+ export const type: RequestType = new RequestType('html/tag');
+}
+
+
// Create a connection for the server
let connection: IConnection = createConnection();
@@ -96,7 +101,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
capabilities: {
// Tell the client that the server works in FULL text document sync mode
textDocumentSync: documents.syncKind,
- completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : null,
+ completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/', '>'] } : null,
hoverProvider: true,
documentHighlightProvider: true,
documentRangeFormattingProvider: false,
@@ -321,5 +326,17 @@ connection.onRequest(ColorSymbolRequest.type, uri => {
return ranges;
});
+connection.onRequest(TagCloseRequest.type, params => {
+ let document = documents.get(params.textDocument.uri);
+ if (document) {
+ let mode = languageModes.getModeAtPosition(document, params.position);
+ if (mode && mode.doAutoClose) {
+ return mode.doAutoClose(document, params.position);
+ }
+ }
+ return null;
+});
+
+
// Listen on the connection
connection.listen();
\ No newline at end of file
diff --git a/extensions/html/server/src/modes/htmlMode.ts b/extensions/html/server/src/modes/htmlMode.ts
index 186df1217a2..c4471fddc25 100644
--- a/extensions/html/server/src/modes/htmlMode.ts
+++ b/extensions/html/server/src/modes/htmlMode.ts
@@ -21,6 +21,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageM
},
doComplete(document: TextDocument, position: Position, settings: Settings = globalSettings) {
let options = settings && settings.html && settings.html.suggest;
+ let doAutoComplete = settings && settings.html && settings.html.autoClosingTags.enable;
+ if (doAutoComplete) {
+ options.hideAutoCompleteProposals = true;
+ }
return htmlLanguageService.doComplete(document, position, htmlDocuments.get(document), options);
},
doHover(document: TextDocument, position: Position) {
@@ -44,6 +48,14 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageM
}
return htmlLanguageService.format(document, range, formatSettings);
},
+ doAutoClose(document: TextDocument, position: Position) {
+ let offset = document.offsetAt(position);
+ let text = document.getText();
+ if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) {
+ return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document));
+ }
+ return null;
+ },
onDocumentRemoved(document: TextDocument) {
htmlDocuments.onDocumentRemoved(document);
},
diff --git a/extensions/html/server/src/modes/languageModes.ts b/extensions/html/server/src/modes/languageModes.ts
index fb6f90b3cc3..a12348d7eb1 100644
--- a/extensions/html/server/src/modes/languageModes.ts
+++ b/extensions/html/server/src/modes/languageModes.ts
@@ -41,6 +41,7 @@ export interface LanguageMode {
findReferences?: (document: TextDocument, position: Position) => Location[];
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings: Settings) => TextEdit[];
findColorSymbols?: (document: TextDocument) => Range[];
+ doAutoClose?: (document: TextDocument, position: Position) => string;
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}