Path: blob/main/extensions/css-language-features/client/src/cssClient.ts
5222 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, FormattingOptions, workspace, l10n } from 'vscode';6import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, BaseLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient';7import { getCustomDataSource } from './customData';8import { RequestService, serveFileSystemRequests } from './requests';910namespace CustomDataChangedNotification {11export const type: NotificationType<string[]> = new NotificationType('css/customDataChanged');12}1314export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;1516export interface Runtime {17TextDecoder: typeof TextDecoder;18fs?: RequestService;19}2021interface FormatterRegistration {22readonly languageId: string;23readonly settingId: string;24provider: Disposable | undefined;25}2627interface CSSFormatSettings {28newlineBetweenSelectors?: boolean;29newlineBetweenRules?: boolean;30spaceAroundSelectorSeparator?: boolean;31braceStyle?: 'collapse' | 'expand';32preserveNewLines?: boolean;33maxPreserveNewLines?: number | null;34}3536const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['newlineBetweenSelectors', 'newlineBetweenRules', 'spaceAroundSelectorSeparator', 'braceStyle', 'preserveNewLines', 'maxPreserveNewLines'];3738export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<BaseLanguageClient> {3940const customDataSource = getCustomDataSource(context.subscriptions);4142const documentSelector = ['css', 'scss', 'less'];4344const formatterRegistrations: FormatterRegistration[] = documentSelector.map(languageId => ({45languageId, settingId: `${languageId}.format.enable`, provider: undefined46}));4748// Options to control the language client49const clientOptions: LanguageClientOptions = {50documentSelector,51synchronize: {52configurationSection: ['css', 'scss', 'less']53},54initializationOptions: {55handledSchemas: ['file'],56provideFormatter: false, // tell the server to not provide formatting capability57customCapabilities: { rangeFormatting: { editLimit: 10000 } }58},59middleware: {60provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {61// testing the replace / insert mode62function updateRanges(item: CompletionItem) {63const range = item.range;64if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {65item.range = { inserting: new Range(range.start, position), replacing: range };6667}68}69function updateLabel(item: CompletionItem) {70if (item.kind === CompletionItemKind.Color) {71item.label = {72label: item.label as string,73description: (item.documentation as string)74};75}76}77// testing the new completion78function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {79if (r) {80(Array.isArray(r) ? r : r.items).forEach(updateRanges);81(Array.isArray(r) ? r : r.items).forEach(updateLabel);82}83return r;84}85function isThenable<T>(obj: unknown): obj is Thenable<T> {86return !!obj && typeof (obj as unknown as Thenable<T>).then === 'function';87}8889const r = next(document, position, context, token);90if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {91return r.then(updateProposals);92}93return updateProposals(r);94}95}96};9798// Create the language client and start the client.99const client = newLanguageClient('css', l10n.t('CSS Language Server'), clientOptions);100client.registerProposedFeatures();101102await client.start();103104client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);105customDataSource.onDidChange(() => {106client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);107});108109// manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652.110for (const registration of formatterRegistrations) {111updateFormatterRegistration(registration);112context.subscriptions.push({ dispose: () => registration.provider?.dispose() });113context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration)));114}115116serveFileSystemRequests(client, runtime);117118119context.subscriptions.push(initCompletionProvider());120121function initCompletionProvider(): Disposable {122const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/;123124return languages.registerCompletionItemProvider(documentSelector, {125provideCompletionItems(doc: TextDocument, pos: Position) {126const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));127const match = lineUntilPos.match(regionCompletionRegExpr);128if (match) {129const range = new Range(new Position(pos.line, match[1].length), pos);130const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);131beginProposal.range = range; TextEdit.replace(range, '/* #region */');132beginProposal.insertText = new SnippetString('/* #region $1*/');133beginProposal.documentation = l10n.t('Folding Region Start');134beginProposal.filterText = match[2];135beginProposal.sortText = 'za';136const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);137endProposal.range = range;138endProposal.insertText = '/* #endregion */';139endProposal.documentation = l10n.t('Folding Region End');140endProposal.sortText = 'zb';141endProposal.filterText = match[2];142return [beginProposal, endProposal];143}144return null;145}146});147}148149commands.registerCommand('_css.applyCodeAction', applyCodeAction);150151function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) {152const textEditor = window.activeTextEditor;153if (textEditor && textEditor.document.uri.toString() === uri) {154if (textEditor.document.version !== documentVersion) {155window.showInformationMessage(l10n.t('CSS fix is outdated and can\'t be applied to the document.'));156}157textEditor.edit(mutator => {158for (const edit of edits) {159mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);160}161}).then(success => {162if (!success) {163window.showErrorMessage(l10n.t('Failed to apply CSS fix to the document. Please consider opening an issue with steps to reproduce.'));164}165});166}167}168169function updateFormatterRegistration(registration: FormatterRegistration) {170const formatEnabled = workspace.getConfiguration().get(registration.settingId);171if (!formatEnabled && registration.provider) {172registration.provider.dispose();173registration.provider = undefined;174} else if (formatEnabled && !registration.provider) {175registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, {176provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {177const filesConfig = workspace.getConfiguration('files', document);178179const fileFormattingOptions = {180trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),181trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),182insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),183};184const params: DocumentRangeFormattingParams = {185textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),186range: client.code2ProtocolConverter.asRange(range),187options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)188};189// add the css formatter options from the settings190const formatterSettings = workspace.getConfiguration(registration.languageId, document).get<CSSFormatSettings>('format');191if (formatterSettings) {192for (const key of cssFormatSettingKeys) {193const val = formatterSettings[key];194if (val !== undefined && val !== null) {195params.options[key] = val;196}197}198}199return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(200client.protocol2CodeConverter.asTextEdits,201(error) => {202client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);203return Promise.resolve([]);204}205);206}207});208}209}210211return client;212}213214215