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