eae5d8c807
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
309 lines
13 KiB
TypeScript
309 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as nls from 'vscode-nls';
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
import {
|
|
languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions,
|
|
Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend,
|
|
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands
|
|
} from 'vscode';
|
|
import {
|
|
LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams,
|
|
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient
|
|
} from 'vscode-languageclient';
|
|
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
|
|
import { activateTagClosing } from './tagClosing';
|
|
import { RequestService } from './requests';
|
|
import { getCustomDataSource } from './customData';
|
|
|
|
namespace CustomDataChangedNotification {
|
|
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
|
|
}
|
|
|
|
namespace TagCloseRequest {
|
|
export const type: RequestType<TextDocumentPositionParams, string, any> = new RequestType('html/tag');
|
|
}
|
|
// experimental: semantic tokens
|
|
interface SemanticTokenParams {
|
|
textDocument: TextDocumentIdentifier;
|
|
ranges?: LspRange[];
|
|
}
|
|
namespace SemanticTokenRequest {
|
|
export const type: RequestType<SemanticTokenParams, number[] | null, any> = new RequestType('html/semanticTokens');
|
|
}
|
|
namespace SemanticTokenLegendRequest {
|
|
export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any> = new RequestType0('html/semanticTokenLegend');
|
|
}
|
|
|
|
namespace SettingIds {
|
|
export const linkedEditing = 'editor.linkedEditing';
|
|
export const formatEnable = 'html.format.enable';
|
|
|
|
}
|
|
|
|
export interface TelemetryReporter {
|
|
sendTelemetryEvent(eventName: string, properties?: {
|
|
[key: string]: string;
|
|
}, measurements?: {
|
|
[key: string]: number;
|
|
}): void;
|
|
}
|
|
|
|
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient;
|
|
|
|
export interface Runtime {
|
|
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } };
|
|
fs?: RequestService;
|
|
telemetry?: TelemetryReporter;
|
|
}
|
|
|
|
export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) {
|
|
|
|
let toDispose = context.subscriptions;
|
|
|
|
|
|
let documentSelector = ['html', 'handlebars'];
|
|
let embeddedLanguages = { css: true, javascript: true };
|
|
|
|
let rangeFormatting: Disposable | undefined = undefined;
|
|
|
|
const customDataSource = getCustomDataSource(context.subscriptions);
|
|
|
|
// Options to control the language client
|
|
let clientOptions: LanguageClientOptions = {
|
|
documentSelector,
|
|
synchronize: {
|
|
configurationSection: ['html', 'css', 'javascript'], // the settings to synchronize
|
|
},
|
|
initializationOptions: {
|
|
embeddedLanguages,
|
|
handledSchemas: ['file'],
|
|
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting.
|
|
},
|
|
middleware: {
|
|
// testing the replace / insert mode
|
|
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
|
|
function updateRanges(item: CompletionItem) {
|
|
const range = item.range;
|
|
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
|
|
item.range = { inserting: new Range(range.start, position), replacing: range };
|
|
}
|
|
}
|
|
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
|
|
if (r) {
|
|
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
|
|
}
|
|
return r;
|
|
}
|
|
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
|
|
|
|
const r = next(document, position, context, token);
|
|
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
|
|
return r.then(updateProposals);
|
|
}
|
|
return updateProposals(r);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Create the language client and start the client.
|
|
let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions);
|
|
client.registerProposedFeatures();
|
|
|
|
let disposable = client.start();
|
|
toDispose.push(disposable);
|
|
client.onReady().then(() => {
|
|
|
|
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
|
customDataSource.onDidChange(() => {
|
|
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
|
});
|
|
|
|
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 }, 'html.autoClosingTags');
|
|
toDispose.push(disposable);
|
|
|
|
disposable = client.onTelemetry(e => {
|
|
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
|
|
});
|
|
toDispose.push(disposable);
|
|
|
|
// manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652.
|
|
updateFormatterRegistration();
|
|
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
|
|
toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration()));
|
|
|
|
client.sendRequest(SemanticTokenLegendRequest.type).then(legend => {
|
|
if (legend) {
|
|
const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = {
|
|
provideDocumentSemanticTokens(doc) {
|
|
const params: SemanticTokenParams = {
|
|
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
|
|
};
|
|
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
|
|
return data && new SemanticTokens(new Uint32Array(data));
|
|
});
|
|
},
|
|
provideDocumentRangeSemanticTokens(doc, range) {
|
|
const params: SemanticTokenParams = {
|
|
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
|
|
ranges: [client.code2ProtocolConverter.asRange(range)]
|
|
};
|
|
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
|
|
return data && new SemanticTokens(new Uint32Array(data));
|
|
});
|
|
}
|
|
};
|
|
toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers)));
|
|
}
|
|
});
|
|
});
|
|
|
|
function updateFormatterRegistration() {
|
|
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
|
|
if (!formatEnabled && rangeFormatting) {
|
|
rangeFormatting.dispose();
|
|
rangeFormatting = undefined;
|
|
} else if (formatEnabled && !rangeFormatting) {
|
|
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
|
|
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
|
|
const filesConfig = workspace.getConfiguration('files', document);
|
|
const fileFormattingOptions = {
|
|
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
|
|
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
|
|
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
|
|
};
|
|
let params: DocumentRangeFormattingParams = {
|
|
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
|
range: client.code2ProtocolConverter.asRange(range),
|
|
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
|
|
};
|
|
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
|
|
client.protocol2CodeConverter.asTextEdits,
|
|
(error) => {
|
|
client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []);
|
|
return Promise.resolve([]);
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
languages.setLanguageConfiguration('html', {
|
|
indentationRules: {
|
|
increaseIndentPattern: /<(?!\?|(?:area|base|br|col|frame|hr|html|img|input|link|meta|param)\b|[^>]*\/>)([-_\.A-Za-z0-9]+)(?=\s|>)\b[^>]*>(?!.*<\/\1>)|<!--(?!.*-->)|\{[^}"']*$/,
|
|
decreaseIndentPattern: /^\s*(<\/(?!html)[-_\.A-Za-z0-9]+\b[^>]*>|-->|\})/
|
|
},
|
|
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
|
|
onEnterRules: [
|
|
{
|
|
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
|
|
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
|
|
action: { indentAction: IndentAction.IndentOutdent }
|
|
},
|
|
{
|
|
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
|
|
action: { indentAction: IndentAction.Indent }
|
|
}
|
|
],
|
|
});
|
|
|
|
languages.setLanguageConfiguration('handlebars', {
|
|
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
|
|
onEnterRules: [
|
|
{
|
|
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
|
|
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i,
|
|
action: { indentAction: IndentAction.IndentOutdent }
|
|
},
|
|
{
|
|
beforeText: new RegExp(`<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`, 'i'),
|
|
action: { indentAction: IndentAction.Indent }
|
|
}
|
|
],
|
|
});
|
|
|
|
const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
|
|
const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/;
|
|
languages.registerCompletionItemProvider(documentSelector, {
|
|
provideCompletionItems(doc, pos) {
|
|
const results: CompletionItem[] = [];
|
|
let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
|
|
let match = lineUntilPos.match(regionCompletionRegExpr);
|
|
if (match) {
|
|
let range = new Range(new Position(pos.line, match[1].length), pos);
|
|
let beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
|
|
beginProposal.range = range;
|
|
beginProposal.insertText = new SnippetString('<!-- #region $1-->');
|
|
beginProposal.documentation = localize('folding.start', 'Folding Region Start');
|
|
beginProposal.filterText = match[2];
|
|
beginProposal.sortText = 'za';
|
|
results.push(beginProposal);
|
|
let endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
|
|
endProposal.range = range;
|
|
endProposal.insertText = new SnippetString('<!-- #endregion -->');
|
|
endProposal.documentation = localize('folding.end', 'Folding Region End');
|
|
endProposal.filterText = match[2];
|
|
endProposal.sortText = 'zb';
|
|
results.push(endProposal);
|
|
}
|
|
let match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr);
|
|
if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) {
|
|
let range = new Range(new Position(pos.line, match2[1].length), pos);
|
|
let snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet);
|
|
snippetProposal.range = range;
|
|
const content = ['<!DOCTYPE html>',
|
|
'<html>',
|
|
'<head>',
|
|
'\t<meta charset=\'utf-8\'>',
|
|
'\t<meta http-equiv=\'X-UA-Compatible\' content=\'IE=edge\'>',
|
|
'\t<title>${1:Page Title}</title>',
|
|
'\t<meta name=\'viewport\' content=\'width=device-width, initial-scale=1\'>',
|
|
'\t<link rel=\'stylesheet\' type=\'text/css\' media=\'screen\' href=\'${2:main.css}\'>',
|
|
'\t<script src=\'${3:main.js}\'></script>',
|
|
'</head>',
|
|
'<body>',
|
|
'\t$0',
|
|
'</body>',
|
|
'</html>'].join('\n');
|
|
snippetProposal.insertText = new SnippetString(content);
|
|
snippetProposal.documentation = localize('folding.html', 'Simple HTML5 starting point');
|
|
snippetProposal.filterText = match2[2];
|
|
snippetProposal.sortText = 'za';
|
|
results.push(snippetProposal);
|
|
}
|
|
return results;
|
|
}
|
|
});
|
|
|
|
const promptForLinkedEditingKey = 'html.promptForLinkedEditing';
|
|
if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) {
|
|
const config = workspace.getConfiguration('editor', { languageId: 'html' });
|
|
if (!config.get('linkedEditing') && !config.get('renameOnType')) {
|
|
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
|
|
if (e && documentSelector.indexOf(e.document.languageId) !== -1) {
|
|
context.globalState.update(promptForLinkedEditingKey, false);
|
|
activeEditorListener.dispose();
|
|
const configure = localize('configureButton', 'Configure');
|
|
const res = await window.showInformationMessage(localize('linkedEditingQuestion', 'VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
|
|
if (res === configure) {
|
|
commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing);
|
|
}
|
|
}
|
|
});
|
|
toDispose.push(activeEditorListener);
|
|
}
|
|
}
|
|
|
|
|
|
toDispose.push();
|
|
}
|