mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
[html] VSCode doesn't automatically close HTML tags Fixes #2246.
This commit is contained in:
@@ -6,10 +6,11 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { languages, workspace, ExtensionContext, IndentAction } from 'vscode';
|
import { languages, workspace, ExtensionContext, IndentAction, Position, TextDocument } from 'vscode';
|
||||||
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Range, RequestType } from 'vscode-languageclient';
|
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, Range as LSRange, RequestType, TextDocumentPositionParams } from 'vscode-languageclient';
|
||||||
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
|
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
|
||||||
import { activateColorDecorations } from './colorDecorators';
|
import { activateColorDecorations } from './colorDecorators';
|
||||||
|
import { activateTagClosing } from './tagClosing';
|
||||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||||
|
|
||||||
import { ConfigurationFeature } from 'vscode-languageclient/lib/proposed';
|
import { ConfigurationFeature } from 'vscode-languageclient/lib/proposed';
|
||||||
@@ -18,7 +19,11 @@ import * as nls from 'vscode-nls';
|
|||||||
let localize = nls.loadMessageBundle();
|
let localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
namespace ColorSymbolRequest {
|
namespace ColorSymbolRequest {
|
||||||
export const type: RequestType<string, Range[], any, any> = new RequestType('css/colorSymbols');
|
export const type: RequestType<string, LSRange[], any, any> = new RequestType('html/colorSymbols');
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TagCloseRequest {
|
||||||
|
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPackageInfo {
|
interface IPackageInfo {
|
||||||
@@ -28,10 +33,13 @@ interface IPackageInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function activate(context: ExtensionContext) {
|
export function activate(context: ExtensionContext) {
|
||||||
|
let toDispose = context.subscriptions;
|
||||||
|
|
||||||
let packageInfo = getPackageInfo(context);
|
let packageInfo = getPackageInfo(context);
|
||||||
let telemetryReporter: TelemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
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
|
// The server is implemented in node
|
||||||
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'htmlServerMain.js'));
|
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'htmlServerMain.js'));
|
||||||
@@ -64,7 +72,7 @@ export function activate(context: ExtensionContext) {
|
|||||||
client.registerFeature(new ConfigurationFeature(client));
|
client.registerFeature(new ConfigurationFeature(client));
|
||||||
|
|
||||||
let disposable = client.start();
|
let disposable = client.start();
|
||||||
context.subscriptions.push(disposable);
|
toDispose.push(disposable);
|
||||||
client.onReady().then(() => {
|
client.onReady().then(() => {
|
||||||
let colorRequestor = (uri: string) => {
|
let colorRequestor = (uri: string) => {
|
||||||
return client.sendRequest(ColorSymbolRequest.type, uri).then(ranges => ranges.map(client.protocol2CodeConverter.asRange));
|
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<boolean>('css.colorDecorators.enable');
|
return workspace.getConfiguration().get<boolean>('css.colorDecorators.enable');
|
||||||
};
|
};
|
||||||
let disposable = activateColorDecorations(colorRequestor, { html: true, handlebars: true, razor: true }, isDecoratorEnabled);
|
let disposable = activateColorDecorations(colorRequestor, { html: true, handlebars: true, razor: true }, isDecoratorEnabled);
|
||||||
context.subscriptions.push(disposable);
|
toDispose.push(disposable);
|
||||||
client.onTelemetry(e => {
|
|
||||||
|
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) {
|
if (telemetryReporter) {
|
||||||
telemetryReporter.sendTelemetryEvent(e.key, e.data);
|
telemetryReporter.sendTelemetryEvent(e.key, e.data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
toDispose.push(disposable);
|
||||||
});
|
});
|
||||||
|
|
||||||
languages.setLanguageConfiguration('html', {
|
languages.setLanguageConfiguration('html', {
|
||||||
|
|||||||
@@ -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<string>, 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<boolean>(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);
|
||||||
|
}
|
||||||
@@ -193,6 +193,12 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"description": "%html.validate.styles%"
|
"description": "%html.validate.styles%"
|
||||||
},
|
},
|
||||||
|
"html.autoClosingTags.enable": {
|
||||||
|
"type": "boolean",
|
||||||
|
"scope": "resource",
|
||||||
|
"default": true,
|
||||||
|
"description": "%html.autoClosingTags.enable%"
|
||||||
|
},
|
||||||
"html.trace.server": {
|
"html.trace.server": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"scope": "window",
|
"scope": "window",
|
||||||
|
|||||||
@@ -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.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.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.",
|
"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.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.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.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."
|
||||||
}
|
}
|
||||||
+2
-2
@@ -8,9 +8,9 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.1.3.tgz"
|
"resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.1.3.tgz"
|
||||||
},
|
},
|
||||||
"vscode-html-languageservice": {
|
"vscode-html-languageservice": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.7",
|
||||||
"from": "vscode-html-languageservice@next",
|
"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": {
|
"vscode-jsonrpc": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-css-languageservice": "^2.1.3",
|
"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": "3.4.0-next.4",
|
||||||
"vscode-languageserver-types": "^3.3.0",
|
"vscode-languageserver-types": "^3.3.0",
|
||||||
"vscode-nls": "^2.0.2",
|
"vscode-nls": "^2.0.2",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
'use strict';
|
'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 { DocumentContext } from 'vscode-html-languageservice';
|
||||||
import { TextDocument, Diagnostic, DocumentLink, Range, SymbolInformation } from 'vscode-languageserver-types';
|
import { TextDocument, Diagnostic, DocumentLink, Range, SymbolInformation } from 'vscode-languageserver-types';
|
||||||
import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes';
|
import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes';
|
||||||
@@ -22,9 +22,14 @@ import * as nls from 'vscode-nls';
|
|||||||
nls.config(process.env['VSCODE_NLS_CONFIG']);
|
nls.config(process.env['VSCODE_NLS_CONFIG']);
|
||||||
|
|
||||||
namespace ColorSymbolRequest {
|
namespace ColorSymbolRequest {
|
||||||
export const type: RequestType<string, Range[], any, any> = new RequestType('css/colorSymbols');
|
export const type: RequestType<string, Range[], any, any> = new RequestType('html/colorSymbols');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace TagCloseRequest {
|
||||||
|
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create a connection for the server
|
// Create a connection for the server
|
||||||
let connection: IConnection = createConnection();
|
let connection: IConnection = createConnection();
|
||||||
|
|
||||||
@@ -96,7 +101,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
// Tell the client that the server works in FULL text document sync mode
|
// Tell the client that the server works in FULL text document sync mode
|
||||||
textDocumentSync: documents.syncKind,
|
textDocumentSync: documents.syncKind,
|
||||||
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : null,
|
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/', '>'] } : null,
|
||||||
hoverProvider: true,
|
hoverProvider: true,
|
||||||
documentHighlightProvider: true,
|
documentHighlightProvider: true,
|
||||||
documentRangeFormattingProvider: false,
|
documentRangeFormattingProvider: false,
|
||||||
@@ -321,5 +326,17 @@ connection.onRequest(ColorSymbolRequest.type, uri => {
|
|||||||
return ranges;
|
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
|
// Listen on the connection
|
||||||
connection.listen();
|
connection.listen();
|
||||||
@@ -21,6 +21,10 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageM
|
|||||||
},
|
},
|
||||||
doComplete(document: TextDocument, position: Position, settings: Settings = globalSettings) {
|
doComplete(document: TextDocument, position: Position, settings: Settings = globalSettings) {
|
||||||
let options = settings && settings.html && settings.html.suggest;
|
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);
|
return htmlLanguageService.doComplete(document, position, htmlDocuments.get(document), options);
|
||||||
},
|
},
|
||||||
doHover(document: TextDocument, position: Position) {
|
doHover(document: TextDocument, position: Position) {
|
||||||
@@ -44,6 +48,14 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageM
|
|||||||
}
|
}
|
||||||
return htmlLanguageService.format(document, range, formatSettings);
|
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) {
|
onDocumentRemoved(document: TextDocument) {
|
||||||
htmlDocuments.onDocumentRemoved(document);
|
htmlDocuments.onDocumentRemoved(document);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface LanguageMode {
|
|||||||
findReferences?: (document: TextDocument, position: Position) => Location[];
|
findReferences?: (document: TextDocument, position: Position) => Location[];
|
||||||
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings: Settings) => TextEdit[];
|
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings: Settings) => TextEdit[];
|
||||||
findColorSymbols?: (document: TextDocument) => Range[];
|
findColorSymbols?: (document: TextDocument) => Range[];
|
||||||
|
doAutoClose?: (document: TextDocument, position: Position) => string;
|
||||||
onDocumentRemoved(document: TextDocument): void;
|
onDocumentRemoved(document: TextDocument): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user