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
3320 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
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
183
184
const r = next(document, position, context, token);
185
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
186
return r.then(updateProposals);
187
}
188
return updateProposals(r);
189
}
190
}
191
};
192
clientOptions.outputChannel = logOutputChannel;
193
194
// Create the language client and start the client.
195
const client = newLanguageClient('html', languageServerDescription, clientOptions);
196
client.registerProposedFeatures();
197
198
await client.start();
199
200
toDispose.push(serveFileSystemRequests(client, runtime));
201
202
const customDataSource = getCustomDataSource(runtime, toDispose);
203
204
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
205
customDataSource.onDidChange(() => {
206
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
207
}, undefined, toDispose);
208
toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent));
209
210
211
const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise<string> => {
212
const param: AutoInsertParams = {
213
kind,
214
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
215
position: client.code2ProtocolConverter.asPosition(position)
216
};
217
return client.sendRequest(AutoInsertRequest.type, param);
218
};
219
220
const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime);
221
toDispose.push(disposable);
222
223
const disposable2 = client.onTelemetry(e => {
224
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
225
});
226
toDispose.push(disposable2);
227
228
// manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652.
229
updateFormatterRegistration();
230
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
231
toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration()));
232
233
client.sendRequest(SemanticTokenLegendRequest.type).then(legend => {
234
if (legend) {
235
const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = {
236
provideDocumentSemanticTokens(doc) {
237
const params: SemanticTokenParams = {
238
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
239
};
240
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
241
return data && new SemanticTokens(new Uint32Array(data));
242
});
243
},
244
provideDocumentRangeSemanticTokens(doc, range) {
245
const params: SemanticTokenParams = {
246
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
247
ranges: [client.code2ProtocolConverter.asRange(range)]
248
};
249
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
250
return data && new SemanticTokens(new Uint32Array(data));
251
});
252
}
253
};
254
toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers)));
255
}
256
});
257
258
function updateFormatterRegistration() {
259
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
260
if (!formatEnabled && rangeFormatting) {
261
rangeFormatting.dispose();
262
rangeFormatting = undefined;
263
} else if (formatEnabled && !rangeFormatting) {
264
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
265
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
266
const filesConfig = workspace.getConfiguration('files', document);
267
const fileFormattingOptions = {
268
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
269
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
270
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
271
};
272
const params: DocumentRangeFormattingParams = {
273
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
274
range: client.code2ProtocolConverter.asRange(range),
275
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
276
};
277
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
278
client.protocol2CodeConverter.asTextEdits,
279
(error) => {
280
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
281
return Promise.resolve([]);
282
}
283
);
284
}
285
});
286
}
287
}
288
289
const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
290
const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/;
291
toDispose.push(languages.registerCompletionItemProvider(documentSelector, {
292
provideCompletionItems(doc, pos) {
293
const results: CompletionItem[] = [];
294
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
295
const match = lineUntilPos.match(regionCompletionRegExpr);
296
if (match) {
297
const range = new Range(new Position(pos.line, match[1].length), pos);
298
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
299
beginProposal.range = range;
300
beginProposal.insertText = new SnippetString('<!-- #region $1-->');
301
beginProposal.documentation = l10n.t('Folding Region Start');
302
beginProposal.filterText = match[2];
303
beginProposal.sortText = 'za';
304
results.push(beginProposal);
305
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
306
endProposal.range = range;
307
endProposal.insertText = new SnippetString('<!-- #endregion -->');
308
endProposal.documentation = l10n.t('Folding Region End');
309
endProposal.filterText = match[2];
310
endProposal.sortText = 'zb';
311
results.push(endProposal);
312
}
313
const match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr);
314
if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) {
315
const range = new Range(new Position(pos.line, match2[1].length), pos);
316
const snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet);
317
snippetProposal.range = range;
318
const content = ['<!DOCTYPE html>',
319
'<html>',
320
'<head>',
321
'\t<meta charset=\'utf-8\'>',
322
'\t<meta http-equiv=\'X-UA-Compatible\' content=\'IE=edge\'>',
323
'\t<title>${1:Page Title}</title>',
324
'\t<meta name=\'viewport\' content=\'width=device-width, initial-scale=1\'>',
325
'\t<link rel=\'stylesheet\' type=\'text/css\' media=\'screen\' href=\'${2:main.css}\'>',
326
'\t<script src=\'${3:main.js}\'></script>',
327
'</head>',
328
'<body>',
329
'\t$0',
330
'</body>',
331
'</html>'].join('\n');
332
snippetProposal.insertText = new SnippetString(content);
333
snippetProposal.documentation = l10n.t('Simple HTML5 starting point');
334
snippetProposal.filterText = match2[2];
335
snippetProposal.sortText = 'za';
336
results.push(snippetProposal);
337
}
338
return results;
339
}
340
}));
341
342
return {
343
dispose: async () => {
344
await client.stop();
345
toDispose.forEach(d => d.dispose());
346
rangeFormatting?.dispose();
347
}
348
};
349
350
}
351
352