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
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
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
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
87
88
const r = next(document, position, context, token);
89
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
90
return r.then(updateProposals);
91
}
92
return updateProposals(r);
93
}
94
}
95
};
96
97
// Create the language client and start the client.
98
const client = newLanguageClient('css', l10n.t('CSS Language Server'), clientOptions);
99
client.registerProposedFeatures();
100
101
await client.start();
102
103
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
104
customDataSource.onDidChange(() => {
105
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
106
});
107
108
// manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652.
109
for (const registration of formatterRegistrations) {
110
updateFormatterRegistration(registration);
111
context.subscriptions.push({ dispose: () => registration.provider?.dispose() });
112
context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration)));
113
}
114
115
serveFileSystemRequests(client, runtime);
116
117
118
context.subscriptions.push(initCompletionProvider());
119
120
function initCompletionProvider(): Disposable {
121
const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/;
122
123
return languages.registerCompletionItemProvider(documentSelector, {
124
provideCompletionItems(doc: TextDocument, pos: Position) {
125
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
126
const match = lineUntilPos.match(regionCompletionRegExpr);
127
if (match) {
128
const range = new Range(new Position(pos.line, match[1].length), pos);
129
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
130
beginProposal.range = range; TextEdit.replace(range, '/* #region */');
131
beginProposal.insertText = new SnippetString('/* #region $1*/');
132
beginProposal.documentation = l10n.t('Folding Region Start');
133
beginProposal.filterText = match[2];
134
beginProposal.sortText = 'za';
135
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
136
endProposal.range = range;
137
endProposal.insertText = '/* #endregion */';
138
endProposal.documentation = l10n.t('Folding Region End');
139
endProposal.sortText = 'zb';
140
endProposal.filterText = match[2];
141
return [beginProposal, endProposal];
142
}
143
return null;
144
}
145
});
146
}
147
148
commands.registerCommand('_css.applyCodeAction', applyCodeAction);
149
150
function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) {
151
const textEditor = window.activeTextEditor;
152
if (textEditor && textEditor.document.uri.toString() === uri) {
153
if (textEditor.document.version !== documentVersion) {
154
window.showInformationMessage(l10n.t('CSS fix is outdated and can\'t be applied to the document.'));
155
}
156
textEditor.edit(mutator => {
157
for (const edit of edits) {
158
mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);
159
}
160
}).then(success => {
161
if (!success) {
162
window.showErrorMessage(l10n.t('Failed to apply CSS fix to the document. Please consider opening an issue with steps to reproduce.'));
163
}
164
});
165
}
166
}
167
168
function updateFormatterRegistration(registration: FormatterRegistration) {
169
const formatEnabled = workspace.getConfiguration().get(registration.settingId);
170
if (!formatEnabled && registration.provider) {
171
registration.provider.dispose();
172
registration.provider = undefined;
173
} else if (formatEnabled && !registration.provider) {
174
registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, {
175
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
176
const filesConfig = workspace.getConfiguration('files', document);
177
178
const fileFormattingOptions = {
179
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
180
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
181
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
182
};
183
const params: DocumentRangeFormattingParams = {
184
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
185
range: client.code2ProtocolConverter.asRange(range),
186
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
187
};
188
// add the css formatter options from the settings
189
const formatterSettings = workspace.getConfiguration(registration.languageId, document).get<CSSFormatSettings>('format');
190
if (formatterSettings) {
191
for (const key of cssFormatSettingKeys) {
192
const val = formatterSettings[key];
193
if (val !== undefined && val !== null) {
194
params.options[key] = val;
195
}
196
}
197
}
198
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
199
client.protocol2CodeConverter.asTextEdits,
200
(error) => {
201
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
202
return Promise.resolve([]);
203
}
204
);
205
}
206
});
207
}
208
}
209
210
return client;
211
}
212
213