Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/html-language-features/client/src/htmlClient.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
7
import {
8
languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions,
9
Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend,
10
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, l10n,
11
LogOutputChannel
12
} from 'vscode';
13
import {
14
LanguageClientOptions, RequestType, DocumentRangeFormattingParams,
15
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, BaseLanguageClient
16
} from 'vscode-languageclient';
17
import { FileSystemProvider, serveFileSystemRequests } from './requests';
18
import { getCustomDataSource } from './customData';
19
import { activateAutoInsertion } from './autoInsertion';
20
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
21
22
namespace CustomDataChangedNotification {
23
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
24
}
25
26
namespace CustomDataContent {
27
export const type: RequestType<string, string, any> = new RequestType('html/customDataContent');
28
}
29
30
interface AutoInsertParams {
31
/**
32
* The auto insert kind
33
*/
34
kind: 'autoQuote' | 'autoClose';
35
/**
36
* The text document.
37
*/
38
textDocument: TextDocumentIdentifier;
39
/**
40
* The position inside the text document.
41
*/
42
position: LspPosition;
43
}
44
45
namespace AutoInsertRequest {
46
export const type: RequestType<AutoInsertParams, string, any> = new RequestType('html/autoInsert');
47
}
48
49
// experimental: semantic tokens
50
interface SemanticTokenParams {
51
textDocument: TextDocumentIdentifier;
52
ranges?: LspRange[];
53
}
54
namespace SemanticTokenRequest {
55
export const type: RequestType<SemanticTokenParams, number[] | null, any> = new RequestType('html/semanticTokens');
56
}
57
namespace SemanticTokenLegendRequest {
58
export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any> = new RequestType0('html/semanticTokenLegend');
59
}
60
61
namespace SettingIds {
62
export const linkedEditing = 'editor.linkedEditing';
63
export const formatEnable = 'html.format.enable';
64
65
}
66
67
export interface TelemetryReporter {
68
sendTelemetryEvent(eventName: string, properties?: {
69
[key: string]: string;
70
}, measurements?: {
71
[key: string]: number;
72
}): void;
73
}
74
75
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
76
77
export const languageServerDescription = l10n.t('HTML Language Server');
78
79
export interface Runtime {
80
TextDecoder: typeof TextDecoder;
81
fileFs?: FileSystemProvider;
82
telemetry?: TelemetryReporter;
83
readonly timer: {
84
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;
85
};
86
}
87
88
export interface AsyncDisposable {
89
dispose(): Promise<void>;
90
}
91
92
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
93
94
const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true });
95
96
const languageParticipants = getLanguageParticipants();
97
context.subscriptions.push(languageParticipants);
98
99
let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, logOutputChannel, runtime);
100
101
const promptForLinkedEditingKey = 'html.promptForLinkedEditing';
102
if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) {
103
const config = workspace.getConfiguration('editor', { languageId: 'html' });
104
if (!config.get('linkedEditing') && !config.get('renameOnType')) {
105
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
106
if (e && languageParticipants.hasLanguage(e.document.languageId)) {
107
context.globalState.update(promptForLinkedEditingKey, false);
108
activeEditorListener.dispose();
109
const configure = l10n.t('Configure');
110
const res = await window.showInformationMessage(l10n.t('VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
111
if (res === configure) {
112
commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing);
113
}
114
}
115
});
116
context.subscriptions.push(activeEditorListener);
117
}
118
}
119
120
let restartTrigger: Disposable | undefined;
121
languageParticipants.onDidChange(() => {
122
if (restartTrigger) {
123
restartTrigger.dispose();
124
}
125
restartTrigger = runtime.timer.setTimeout(async () => {
126
if (client) {
127
logOutputChannel.info('Extensions have changed, restarting HTML server...');
128
logOutputChannel.info('');
129
const oldClient = client;
130
client = undefined;
131
await oldClient.dispose();
132
client = await startClientWithParticipants(languageParticipants, newLanguageClient, logOutputChannel, runtime);
133
}
134
}, 2000);
135
});
136
137
return {
138
dispose: async () => {
139
restartTrigger?.dispose();
140
await client?.dispose();
141
logOutputChannel.dispose();
142
}
143
};
144
}
145
146
async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, logOutputChannel: LogOutputChannel, runtime: Runtime): Promise<AsyncDisposable> {
147
148
const toDispose: Disposable[] = [];
149
150
const documentSelector = languageParticipants.documentSelector;
151
const embeddedLanguages = { css: true, javascript: true };
152
153
let rangeFormatting: Disposable | undefined = undefined;
154
155
// Options to control the language client
156
const clientOptions: LanguageClientOptions = {
157
documentSelector,
158
synchronize: {
159
configurationSection: ['html', 'css', 'javascript', 'js/ts'], // the settings to synchronize
160
},
161
initializationOptions: {
162
embeddedLanguages,
163
handledSchemas: ['file'],
164
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting.
165
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
166
},
167
middleware: {
168
// testing the replace / insert mode
169
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
170
function updateRanges(item: CompletionItem) {
171
const range = item.range;
172
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
173
item.range = { inserting: new Range(range.start, position), replacing: range };
174
}
175
}
176
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
177
if (r) {
178
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
179
}
180
return r;
181
}
182
function isThenable<T>(obj: unknown): obj is Thenable<T> {
183
return !!obj && typeof (obj as unknown as Thenable<T>).then === 'function';
184
}
185
const r = next(document, position, context, token);
186
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
187
return r.then(updateProposals);
188
}
189
return updateProposals(r);
190
}
191
}
192
};
193
clientOptions.outputChannel = logOutputChannel;
194
195
// Create the language client and start the client.
196
const client = newLanguageClient('html', languageServerDescription, clientOptions);
197
client.registerProposedFeatures();
198
199
await client.start();
200
201
toDispose.push(serveFileSystemRequests(client, runtime));
202
203
const customDataSource = getCustomDataSource(runtime, toDispose);
204
205
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
206
customDataSource.onDidChange(() => {
207
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
208
}, undefined, toDispose);
209
toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent));
210
211
212
const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise<string> => {
213
const param: AutoInsertParams = {
214
kind,
215
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
216
position: client.code2ProtocolConverter.asPosition(position)
217
};
218
return client.sendRequest(AutoInsertRequest.type, param);
219
};
220
221
const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime);
222
toDispose.push(disposable);
223
224
const disposable2 = client.onTelemetry(e => {
225
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
226
});
227
toDispose.push(disposable2);
228
229
// manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652.
230
updateFormatterRegistration();
231
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
232
toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration()));
233
234
client.sendRequest(SemanticTokenLegendRequest.type).then(legend => {
235
if (legend) {
236
const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = {
237
provideDocumentSemanticTokens(doc) {
238
const params: SemanticTokenParams = {
239
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
240
};
241
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
242
return data && new SemanticTokens(new Uint32Array(data));
243
});
244
},
245
provideDocumentRangeSemanticTokens(doc, range) {
246
const params: SemanticTokenParams = {
247
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
248
ranges: [client.code2ProtocolConverter.asRange(range)]
249
};
250
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
251
return data && new SemanticTokens(new Uint32Array(data));
252
});
253
}
254
};
255
toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers)));
256
}
257
});
258
259
function updateFormatterRegistration() {
260
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
261
if (!formatEnabled && rangeFormatting) {
262
rangeFormatting.dispose();
263
rangeFormatting = undefined;
264
} else if (formatEnabled && !rangeFormatting) {
265
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
266
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
267
const filesConfig = workspace.getConfiguration('files', document);
268
const fileFormattingOptions = {
269
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
270
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
271
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
272
};
273
const params: DocumentRangeFormattingParams = {
274
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
275
range: client.code2ProtocolConverter.asRange(range),
276
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
277
};
278
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
279
client.protocol2CodeConverter.asTextEdits,
280
(error) => {
281
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
282
return Promise.resolve([]);
283
}
284
);
285
}
286
});
287
}
288
}
289
290
const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
291
const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/;
292
toDispose.push(languages.registerCompletionItemProvider(documentSelector, {
293
provideCompletionItems(doc, pos) {
294
const results: CompletionItem[] = [];
295
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
296
const match = lineUntilPos.match(regionCompletionRegExpr);
297
if (match) {
298
const range = new Range(new Position(pos.line, match[1].length), pos);
299
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
300
beginProposal.range = range;
301
beginProposal.insertText = new SnippetString('<!-- #region $1-->');
302
beginProposal.documentation = l10n.t('Folding Region Start');
303
beginProposal.filterText = match[2];
304
beginProposal.sortText = 'za';
305
results.push(beginProposal);
306
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
307
endProposal.range = range;
308
endProposal.insertText = new SnippetString('<!-- #endregion -->');
309
endProposal.documentation = l10n.t('Folding Region End');
310
endProposal.filterText = match[2];
311
endProposal.sortText = 'zb';
312
results.push(endProposal);
313
}
314
const match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr);
315
if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) {
316
const range = new Range(new Position(pos.line, match2[1].length), pos);
317
const snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet);
318
snippetProposal.range = range;
319
const content = ['<!DOCTYPE html>',
320
'<html>',
321
'<head>',
322
'\t<meta charset=\'utf-8\'>',
323
'\t<meta http-equiv=\'X-UA-Compatible\' content=\'IE=edge\'>',
324
'\t<title>${1:Page Title}</title>',
325
'\t<meta name=\'viewport\' content=\'width=device-width, initial-scale=1\'>',
326
'\t<link rel=\'stylesheet\' type=\'text/css\' media=\'screen\' href=\'${2:main.css}\'>',
327
'\t<script src=\'${3:main.js}\'></script>',
328
'</head>',
329
'<body>',
330
'\t$0',
331
'</body>',
332
'</html>'].join('\n');
333
snippetProposal.insertText = new SnippetString(content);
334
snippetProposal.documentation = l10n.t('Simple HTML5 starting point');
335
snippetProposal.filterText = match2[2];
336
snippetProposal.sortText = 'za';
337
results.push(snippetProposal);
338
}
339
return results;
340
}
341
}));
342
343
return {
344
dispose: async () => {
345
await client.stop();
346
toDispose.forEach(d => d.dispose());
347
rangeFormatting?.dispose();
348
}
349
};
350
351
}
352
353