Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/css-language-features/client/src/cssClient.ts
5222 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
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, FormattingOptions, workspace, l10n } from 'vscode';
7
import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, BaseLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient';
8
import { getCustomDataSource } from './customData';
9
import { RequestService, serveFileSystemRequests } from './requests';
10
11
namespace CustomDataChangedNotification {
12
export const type: NotificationType<string[]> = new NotificationType('css/customDataChanged');
13
}
14
15
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
16
17
export interface Runtime {
18
TextDecoder: typeof TextDecoder;
19
fs?: RequestService;
20
}
21
22
interface FormatterRegistration {
23
readonly languageId: string;
24
readonly settingId: string;
25
provider: Disposable | undefined;
26
}
27
28
interface CSSFormatSettings {
29
newlineBetweenSelectors?: boolean;
30
newlineBetweenRules?: boolean;
31
spaceAroundSelectorSeparator?: boolean;
32
braceStyle?: 'collapse' | 'expand';
33
preserveNewLines?: boolean;
34
maxPreserveNewLines?: number | null;
35
}
36
37
const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['newlineBetweenSelectors', 'newlineBetweenRules', 'spaceAroundSelectorSeparator', 'braceStyle', 'preserveNewLines', 'maxPreserveNewLines'];
38
39
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {
40
41
const customDataSource = getCustomDataSource(context.subscriptions);
42
43
const documentSelector = ['css', 'scss', 'less'];
44
45
const formatterRegistrations: FormatterRegistration[] = documentSelector.map(languageId => ({
46
languageId, settingId: `${languageId}.format.enable`, provider: undefined
47
}));
48
49
// Options to control the language client
50
const clientOptions: LanguageClientOptions = {
51
documentSelector,
52
synchronize: {
53
configurationSection: ['css', 'scss', 'less']
54
},
55
initializationOptions: {
56
handledSchemas: ['file'],
57
provideFormatter: false, // tell the server to not provide formatting capability
58
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
59
},
60
middleware: {
61
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
62
// testing the replace / insert mode
63
function updateRanges(item: CompletionItem) {
64
const range = item.range;
65
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
66
item.range = { inserting: new Range(range.start, position), replacing: range };
67
68
}
69
}
70
function updateLabel(item: CompletionItem) {
71
if (item.kind === CompletionItemKind.Color) {
72
item.label = {
73
label: item.label as string,
74
description: (item.documentation as string)
75
};
76
}
77
}
78
// testing the new completion
79
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
80
if (r) {
81
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
82
(Array.isArray(r) ? r : r.items).forEach(updateLabel);
83
}
84
return r;
85
}
86
function isThenable<T>(obj: unknown): obj is Thenable<T> {
87
return !!obj && typeof (obj as unknown as Thenable<T>).then === 'function';
88
}
89
90
const r = next(document, position, context, token);
91
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
92
return r.then(updateProposals);
93
}
94
return updateProposals(r);
95
}
96
}
97
};
98
99
// Create the language client and start the client.
100
const client = newLanguageClient('css', l10n.t('CSS Language Server'), clientOptions);
101
client.registerProposedFeatures();
102
103
await client.start();
104
105
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
106
customDataSource.onDidChange(() => {
107
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
108
});
109
110
// manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652.
111
for (const registration of formatterRegistrations) {
112
updateFormatterRegistration(registration);
113
context.subscriptions.push({ dispose: () => registration.provider?.dispose() });
114
context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration)));
115
}
116
117
serveFileSystemRequests(client, runtime);
118
119
120
context.subscriptions.push(initCompletionProvider());
121
122
function initCompletionProvider(): Disposable {
123
const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/;
124
125
return languages.registerCompletionItemProvider(documentSelector, {
126
provideCompletionItems(doc: TextDocument, pos: Position) {
127
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
128
const match = lineUntilPos.match(regionCompletionRegExpr);
129
if (match) {
130
const range = new Range(new Position(pos.line, match[1].length), pos);
131
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
132
beginProposal.range = range; TextEdit.replace(range, '/* #region */');
133
beginProposal.insertText = new SnippetString('/* #region $1*/');
134
beginProposal.documentation = l10n.t('Folding Region Start');
135
beginProposal.filterText = match[2];
136
beginProposal.sortText = 'za';
137
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
138
endProposal.range = range;
139
endProposal.insertText = '/* #endregion */';
140
endProposal.documentation = l10n.t('Folding Region End');
141
endProposal.sortText = 'zb';
142
endProposal.filterText = match[2];
143
return [beginProposal, endProposal];
144
}
145
return null;
146
}
147
});
148
}
149
150
commands.registerCommand('_css.applyCodeAction', applyCodeAction);
151
152
function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) {
153
const textEditor = window.activeTextEditor;
154
if (textEditor && textEditor.document.uri.toString() === uri) {
155
if (textEditor.document.version !== documentVersion) {
156
window.showInformationMessage(l10n.t('CSS fix is outdated and can\'t be applied to the document.'));
157
}
158
textEditor.edit(mutator => {
159
for (const edit of edits) {
160
mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);
161
}
162
}).then(success => {
163
if (!success) {
164
window.showErrorMessage(l10n.t('Failed to apply CSS fix to the document. Please consider opening an issue with steps to reproduce.'));
165
}
166
});
167
}
168
}
169
170
function updateFormatterRegistration(registration: FormatterRegistration) {
171
const formatEnabled = workspace.getConfiguration().get(registration.settingId);
172
if (!formatEnabled && registration.provider) {
173
registration.provider.dispose();
174
registration.provider = undefined;
175
} else if (formatEnabled && !registration.provider) {
176
registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, {
177
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
178
const filesConfig = workspace.getConfiguration('files', document);
179
180
const fileFormattingOptions = {
181
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
182
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
183
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
184
};
185
const params: DocumentRangeFormattingParams = {
186
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
187
range: client.code2ProtocolConverter.asRange(range),
188
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
189
};
190
// add the css formatter options from the settings
191
const formatterSettings = workspace.getConfiguration(registration.languageId, document).get<CSSFormatSettings>('format');
192
if (formatterSettings) {
193
for (const key of cssFormatSettingKeys) {
194
const val = formatterSettings[key];
195
if (val !== undefined && val !== null) {
196
params.options[key] = val;
197
}
198
}
199
}
200
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
201
client.protocol2CodeConverter.asTextEdits,
202
(error) => {
203
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
204
return Promise.resolve([]);
205
}
206
);
207
}
208
});
209
}
210
}
211
212
return client;
213
}
214
215