add css formatter. fixes #19166

This commit is contained in:
Martin Aeschlimann
2022-03-18 18:03:24 +01:00
parent 4d9393e9d3
commit aa65936ebf
6 changed files with 202 additions and 22 deletions

View File

@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode';
import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, CommonLanguageClient } from 'vscode-languageclient';
import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, FormattingOptions, workspace } from 'vscode';
import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, CommonLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { getCustomDataSource } from './customData';
import { RequestService, serveFileSystemRequests } from './requests';
@@ -22,12 +22,30 @@ export interface Runtime {
fs?: RequestService;
}
interface FormatterRegistration {
readonly languageId: string;
readonly settingId: string;
provider: Disposable | undefined;
}
interface CSSFormatSettings {
selectorSeparatorNewline?: boolean;
newlineBetweenRules?: boolean;
spaceAroundSelectorSeparator?: boolean;
}
const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['selectorSeparatorNewline', 'newlineBetweenRules', 'spaceAroundSelectorSeparator'];
export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) {
const customDataSource = getCustomDataSource(context.subscriptions);
let documentSelector = ['css', 'scss', 'less'];
const formatterRegistrations: FormatterRegistration[] = documentSelector.map(languageId => ({
languageId, settingId: `${languageId}.format.enable`, provider: undefined
}));
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector,
@@ -35,7 +53,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
configurationSection: ['css', 'scss', 'less']
},
initializationOptions: {
handledSchemas: ['file']
handledSchemas: ['file'],
provideFormatter: false, // tell the server to not provide formatting capability
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
},
middleware: {
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
@@ -84,6 +104,13 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
});
// manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652.
for (const registration of formatterRegistrations) {
updateFormatterRegistration(registration);
context.subscriptions.push({ dispose: () => registration.provider?.dispose() });
context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration)));
}
serveFileSystemRequests(client, runtime);
});
@@ -143,4 +170,47 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
});
}
}
function updateFormatterRegistration(registration: FormatterRegistration) {
const formatEnabled = workspace.getConfiguration().get(registration.settingId);
if (!formatEnabled && registration.provider) {
registration.provider.dispose();
registration.provider = undefined;
} else if (formatEnabled && !registration.provider) {
registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, {
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
const filesConfig = workspace.getConfiguration('files', document);
const fileFormattingOptions = {
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
};
const params: DocumentRangeFormattingParams = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
range: client.code2ProtocolConverter.asRange(range),
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
};
// add the css formatter options from the settings
const formatterSettings = workspace.getConfiguration(registration.languageId, document).get<CSSFormatSettings>('format');
if (formatterSettings) {
for (const key of cssFormatSettingKeys) {
const val = formatterSettings[key];
if (val !== undefined) {
params.options[key] = val;
}
}
}
console.log(JSON.stringify(params.options));
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
client.protocol2CodeConverter.asTextEdits,
(error) => {
client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []);
return Promise.resolve([]);
}
);
}
});
}
}
}

View File

@@ -307,6 +307,30 @@
],
"default": "off",
"description": "%css.trace.server.desc%"
},
"css.format.enable": {
"type": "boolean",
"scope": "window",
"default": true,
"description": "%css.format.enable.desc%"
},
"css.format.selectorSeparatorNewline": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%css.format.selectorSeparatorNewline.desc%"
},
"css.format.newlineBetweenRules": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%css.format.newlineBetweenRules.desc%"
},
"css.format.spaceAroundSelectorSeparator": {
"type": "boolean",
"scope": "resource",
"default": false,
"markdownDescription": "%css.format.spaceAroundSelectorSeparator.desc%"
}
}
},
@@ -563,6 +587,30 @@
],
"default": "warning",
"description": "%scss.lint.unknownAtRules.desc%"
},
"scss.format.enable": {
"type": "boolean",
"scope": "window",
"default": true,
"description": "%scss.format.enable.desc%"
},
"scss.format.selectorSeparatorNewline": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%scss.format.selectorSeparatorNewline.desc%"
},
"scss.format.newlineBetweenRules": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%scss.format.newlineBetweenRules.desc%"
},
"scss.format.spaceAroundSelectorSeparator": {
"type": "boolean",
"scope": "resource",
"default": false,
"markdownDescription": "%scss.format.spaceAroundSelectorSeparator.desc%"
}
}
},
@@ -820,6 +868,30 @@
],
"default": "warning",
"description": "%less.lint.unknownAtRules.desc%"
},
"less.format.enable": {
"type": "boolean",
"scope": "window",
"default": true,
"description": "%less.format.enable.desc%"
},
"less.format.selectorSeparatorNewline": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%less.format.selectorSeparatorNewline.desc%"
},
"less.format.newlineBetweenRules": {
"type": "boolean",
"scope": "resource",
"default": true,
"markdownDescription": "%less.format.newlineBetweenRules.desc%"
},
"less.format.spaceAroundSelectorSeparator": {
"type": "boolean",
"scope": "resource",
"default": false,
"markdownDescription": "%less.format.spaceAroundSelectorSeparator.desc%"
}
}
}

View File

@@ -30,6 +30,10 @@
"css.validate.desc": "Enables or disables all validations.",
"css.hover.documentation": "Show tag and attribute documentation in CSS hovers.",
"css.hover.references": "Show references to MDN in CSS hovers.",
"css.format.enable.desc": "Enable/disable default LESS formatter.",
"css.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not",
"css.format.newlineBetweenRules.desc": "Add a new line after every css rule",
"css.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators",
"less.title": "LESS",
"less.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.",
"less.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties.",
@@ -57,6 +61,10 @@
"less.validate.desc": "Enables or disables all validations.",
"less.hover.documentation": "Show tag and attribute documentation in LESS hovers.",
"less.hover.references": "Show references to MDN in LESS hovers.",
"less.format.enable.desc": "Enable/disable default LESS formatter.",
"less.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not",
"less.format.newlineBetweenRules.desc": "Add a new line after every css rule",
"less.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators",
"scss.title": "SCSS (Sass)",
"scss.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.",
"scss.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties.",
@@ -84,6 +92,10 @@
"scss.validate.desc": "Enables or disables all validations.",
"scss.hover.documentation": "Show tag and attribute documentation in SCSS hovers.",
"scss.hover.references": "Show references to MDN in SCSS hovers.",
"scss.format.enable.desc": "Enable/disable default LESS formatter.",
"scss.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not",
"scss.format.newlineBetweenRules.desc": "Add a new line after every css rule",
"scss.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators",
"css.colorDecorators.enable.deprecationMessage": "The setting `css.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.",
"scss.colorDecorators.enable.deprecationMessage": "The setting `scss.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.",
"less.colorDecorators.enable.deprecationMessage": "The setting `less.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`."

View File

@@ -10,7 +10,7 @@
"main": "./out/node/cssServerMain",
"browser": "./dist/browser/cssServerMain",
"dependencies": {
"vscode-css-languageservice": "^5.1.13",
"vscode-css-languageservice": "^5.2.0",
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.3"
},

View File

@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import {
Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable
Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable, TextDocumentIdentifier, Range, FormattingOptions, TextEdit
} from 'vscode-languageserver';
import { URI } from 'vscode-uri';
import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position } from 'vscode-css-languageservice';
import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position, CSSFormatConfiguration } from 'vscode-css-languageservice';
import { getLanguageModelCache } from './languageModelCache';
import { formatError, runSafeAsync } from './utils/runner';
import { getDocumentContext } from './utils/documentContext';
@@ -52,6 +52,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
let scopedSettingsSupport = false;
let foldingRangeLimit = Number.MAX_VALUE;
let workspaceFolders: WorkspaceFolder[];
let formatterMaxNumberOfEdits = Number.MAX_VALUE;
let dataProvidersReady: Promise<any> = Promise.resolve();
@@ -87,6 +88,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
const snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
scopedSettingsSupport = !!getClientCapability('workspace.configuration', false);
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
formatterMaxNumberOfEdits = params.initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE;
languageServices.css = getCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities });
languageServices.scss = getSCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities });
@@ -107,7 +109,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
renameProvider: true,
colorProvider: {},
foldingRangeProvider: true,
selectionRangeProvider: true
selectionRangeProvider: true,
documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true,
documentFormattingProvider: params.initializationOptions?.provideFormatter === true,
};
return { capabilities };
});
@@ -367,6 +371,28 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
});
async function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): Promise<TextEdit[]> {
const document = documents.get(textDocument.uri);
if (document) {
console.log(JSON.stringify(options));
const edits = getLanguageService(document).format(document, range ?? getFullRange(document), options as CSSFormatConfiguration);
if (edits.length > formatterMaxNumberOfEdits) {
const newText = TextDocument.applyEdits(document, edits);
return [TextEdit.replace(getFullRange(document), newText)];
}
return edits;
}
return [];
}
connection.onDocumentRangeFormatting((formatParams, token) => {
return runSafeAsync(runtime, () => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
});
connection.onDocumentFormatting((formatParams, token) => {
return runSafeAsync(runtime, () => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token);
});
connection.onNotification(CustomDataChangedNotification.type, updateDataProviders);
// Listen on the connection
@@ -374,4 +400,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
}
function getFullRange(document: TextDocument): Range {
return Range.create(Position.create(0, 0), document.positionAt(document.getText().length));
}

View File

@@ -12,15 +12,15 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae"
integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==
vscode-css-languageservice@^5.1.13:
version "5.1.13"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.13.tgz#debc7c8368223b211a734cb7eb7789c586d3e2d9"
integrity sha512-FA0foqMzMmEoO0WJP+MjoD4dRERhKS+Ag+yBrtmWQDmw2OuZ1R/5FkvI/XdTkCpHmTD9VMczugpHRejQyTXCNQ==
vscode-css-languageservice@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.2.0.tgz#84fa95f8314c742080c09f623bf9f0727621eebf"
integrity sha512-FR5yDEfzbXJtYmZYrA7JWFcRSLHsJw3nv55XAmx7qdwRpFj9yy0ulKfN/NUUdiZW2jZU2fD/+Y4VJYPdafHDag==
dependencies:
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-textdocument "^1.0.4"
vscode-languageserver-types "^3.16.0"
vscode-nls "^5.0.0"
vscode-uri "^3.0.2"
vscode-uri "^3.0.3"
vscode-jsonrpc@6.0.0:
version "6.0.0"
@@ -35,10 +35,10 @@ vscode-languageserver-protocol@3.16.0:
vscode-jsonrpc "6.0.0"
vscode-languageserver-types "3.16.0"
vscode-languageserver-textdocument@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f"
integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==
vscode-languageserver-textdocument@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157"
integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==
vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.16.0:
version "3.16.0"
@@ -57,11 +57,6 @@ vscode-nls@^5.0.0:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"
integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==
vscode-uri@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0"
integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==
vscode-uri@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"