From e84f18660ec0d50be6c8a4e584b41e89e95e06be Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 13 Oct 2026 12:14:05 +0200 Subject: [PATCH] Support scoped workspace/configuration with scopeUri for per-document settings - Change all xml.format.* settings scope from window to language-overridable to enable per-folder and per-language overrides - Add scope to splitAttributesIndentSize and closingBracketNewLine which were missing it - Rewrite ConfigurationRequest handler to support scopeUri with languageId for language-specific overrides (fixes [xml] sections) - Fix configuration resolution to use section prefix so that language-overridable settings (e.g. [axaml]) are properly resolved - Fix old handler bug where non-insertSpaces/tabSize sections returned no result when active editor matched scopeUri, causing array misalignment - Add xml.languageParticipants setting to register custom language IDs without requiring a separate extension - Listen to xml.languageParticipants configuration changes to update document selector dynamically - Document per-folder and per-language formatting overrides - Document xml.languageParticipants setting in Extensions.md Fixes #722, #967, #1047, #1156 Signed-off-by: azerr --- docs/Extensions.md | 31 ++++++++++++- docs/Formatting.md | 71 ++++++++++++++++++++++++++++++ package.json | 51 ++++++++++++--------- src/client/languageParticipants.ts | 23 +++++++++- src/client/xmlClient.ts | 29 ++++++++---- 5 files changed, 174 insertions(+), 31 deletions(-) diff --git a/docs/Extensions.md b/docs/Extensions.md index 20fdb6aa..0a8f11f7 100644 --- a/docs/Extensions.md +++ b/docs/Extensions.md @@ -29,7 +29,36 @@ and you want that `myxml` language uses [vscode-xml](https://github.com/redhat-d ] } ``` - + +### Custom language via settings + +If you don't want to create an extension, you can register a custom language ID using the `xml.languageParticipants` setting. This enables XML language features (completion, validation, formatting) for custom language IDs without syntax coloring. + +1. Map the file extension to a language ID: + + ```json + "files.associations": { + "*.myxml": "myxml" + } + ``` + +2. Register the language ID with vscode-xml: + + ```json + "xml.languageParticipants": ["myxml"] + ``` + +3. Optionally, override XML settings per language: + + ```json + "[myxml]": { + "xml.format.splitAttributes": "force-expand-multiline", + "xml.format.maxLineWidth": 120 + } + ``` + +**Note:** This setting-based approach does not provide syntax coloring. For full support including syntax coloring, create an extension that registers the language with a TextMate grammar (see [Custom language](#custom-language) above). + ## Custom XML Extensions (written in Java) The [LemMinX - XML Language Server](https://github.com/eclipse/lemminx) can be extended to support custom completion, hover, validation, rename, etc by using the [Java Service Provider Interface (SPI)](https://www.baeldung.com/java-spi) mechanism. diff --git a/docs/Formatting.md b/docs/Formatting.md index 007a1897..07c51654 100644 --- a/docs/Formatting.md +++ b/docs/Formatting.md @@ -11,6 +11,77 @@ The standard `editor.insertSpaces` & `editor.tabSize` [format settings](https:// }, ``` +### Per-folder and per-language formatting settings + +All `xml.format.*` settings have `language-overridable` scope, which means they can be overridden per workspace folder (in multi-root workspaces) and per language ID. + +#### Per-folder overrides (multi-root workspace) + +In a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces), each folder can have its own `.vscode/settings.json` with different formatting settings: + +**Folder A** `.vscode/settings.json`: +```json +{ + "xml.format.splitAttributes": "preserve", + "xml.format.maxLineWidth": 120 +} +``` + +**Folder B** `.vscode/settings.json`: +```json +{ + "xml.format.splitAttributes": "force-expand-multiline", + "xml.format.maxLineWidth": 80 +} +``` + +When formatting an XML file, the XML language server pulls the settings scoped to that file's workspace folder. + +#### Per-language overrides + +If you have a custom language ID registered as an XML language participant (see [Extensions - Custom language](Extensions.md#custom-language)), you can override formatting settings per language: + +```json +{ + "xml.format.splitAttributes": "preserve", + "[myxml]": { + "xml.format.splitAttributes": "force-expand-multiline" + } +} +``` + +XML files use `splitAttributes: "preserve"`, while files with language ID `myxml` use `splitAttributes: "force-expand-multiline"`. + +#### Example: AXAML files with different formatting + +[AXAML](https://github.com/nickvdyck/axaml) files (`.axaml`) are XML-based but often require different formatting rules. To set this up: + +1. Create or install an extension that registers the `axaml` language with `xmlLanguageParticipants` (see [Extensions - Custom language](Extensions.md#custom-language)), or use the `xml.languageParticipants` setting (see [Extensions - Custom language via settings](Extensions.md#custom-language-via-settings)). + +2. Map `.axaml` files to the `axaml` language ID: + + ```json + "files.associations": { + "*.axaml": "axaml" + } + ``` + +3. Override formatting settings for `axaml`: + + ```json + { + "xml.format.splitAttributes": "preserve", + "[axaml]": { + "xml.format.splitAttributes": "force-expand-multiline", + "xml.format.maxLineWidth": 120 + } + } + ``` + +This way, `.xml` files are formatted with `splitAttributes: "preserve"`, while `.axaml` files use `force-expand-multiline` with a wider line width. + +*** + ### Formatting strategy As the frequency of issues regarding the previous XML formatter increased, we have decided to redo our formatter from scratch. To revert to the old formatter, the setting `xml.format.legacy` should be set to true. diff --git a/package.json b/package.json index 664e4865..be7f22a1 100644 --- a/package.json +++ b/package.json @@ -263,6 +263,15 @@ "markdownDescription": "Allows XML schemas/ DTD to be associated to file name patterns. Please refer to [XML file association with XSD](command:xml.open.docs?%5B%7B%22page%22%3A%22Validation%22%2C%22section%22%3A%22xml-file-association-with-xsd%22%7D%5D) or [XML file association with DTD](command:xml.open.docs?%5B%7B%22page%22%3A%22Validation%22%2C%22section%22%3A%22xml-file-association-with-dtd%22%7D%5D) for more information. \n\nExample:\n```json\n[{\n \"pattern\": \"file1.xml\",\n \"systemId\": \"path/to/file.xsd\"\n},\n{\n \"pattern\": \"**/*.xsd\",\n \"systemId\": \"http://www.w3.org/2001/XMLSchema.xsd\"\n}]\n```", "scope": "window" }, + "xml.languageParticipants": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "markdownDescription": "Additional VS Code language IDs to be handled by the XML language server. Combine with `files.associations` to map custom file extensions.\n\nExample: to format `.axaml` files as XML:\n1. Add `\"files.associations\": { \"*.axaml\": \"axaml\" }`\n2. Add `\"xml.languageParticipants\": [\"axaml\"]`\n3. Optionally override settings per language: `\"[axaml]\": { \"xml.format.maxLineWidth\": 120 }`", + "scope": "window" + }, "xml.foldings.includeClosingTagInFold": { "type": "boolean", "default": false, @@ -325,7 +334,7 @@ "type": "boolean", "default": true, "markdownDescription": "Enable/disable ability to format document. Default is `true`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatenabled%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.emptyElements": { "type": "string", @@ -336,7 +345,7 @@ ], "default": "ignore", "markdownDescription": "Expand/collapse empty elements. Default is `ignore`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatemptyelements%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.enforceQuoteStyle": { "type": "string", @@ -350,55 +359,55 @@ ], "default": "ignore", "markdownDescription": "Enforce `preferred` quote style (set by `#xml.preferences.quoteStyle#`) or `ignore` quote style when formatting. Default is `ignore`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatenforcequotestyle%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.legacy": { "type": "boolean", "default": false, "markdownDescription": "Enable/disable legacy formatter. Default is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatlegacy%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.grammarAwareFormatting": { "type": "boolean", "default": true, "markdownDescription": "Use Schema/DTD grammar information while formatting. Default is `true`. Not supported by legacy formatter. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatgrammarawareformatting%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.joinCDATALines": { "type": "boolean", "default": false, "markdownDescription": "Set to `true` to join lines in CDATA content during formatting. Default is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatjoincdatalines%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.joinCommentLines": { "type": "boolean", "default": false, "markdownDescription": "Join comment content on format. Default is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatjoincommentlines%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.joinContentLines": { "type": "boolean", "default": false, "markdownDescription": "Normalize the whitespace of content inside an element. Newlines and excess whitespace are removed. Default is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatjoincontentlines%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.maxLineWidth": { "type": "integer", "default": 100, "markdownDescription": "Max line width. Set to `0` to disable this setting. Default is `100`. Not supported by legacy formatter. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatmaxlinewidth%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.preserveAttributeLineBreaks": { "type": "boolean", "default": true, "markdownDescription": "Preserve line breaks that appear before and after attributes. This setting is overridden if `#xml.format.splitAttributes#` is set to any mode except `preserve` or `preserve-aligned`. Default is `true`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatpreserveattributelinebreaks%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.preserveEmptyContent": { "type": "boolean", "default": false, "markdownDescription": "Preserve empty content/whitespace in a tag. Default is `false`. Supported only with legacy formatter. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatpreserveemptycontent%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.preservedNewlines": { "type": "integer", @@ -410,7 +419,7 @@ ], "default": 2, "markdownDescription": "Preserve new lines that separate tags. The value represents the maximum number of new lines per section. A value of 0 removes all new lines. Default is `2`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatpreservednewlines%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.preserveSpace": { "type": "array", @@ -431,7 +440,7 @@ "type": "string" }, "markdownDescription": "Element names for which spaces will be preserved. Not supported by legacy formatter. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatpreservespace%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.mixedContent": { "type": "string", @@ -449,7 +458,7 @@ ], "default": "normalize", "markdownDescription": "Controls how mixed content (text + child elements) is formatted. Default is `normalize`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatmixedcontent%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.blockElements": { "type": "array", @@ -458,13 +467,13 @@ "type": "string" }, "markdownDescription": "Element names to treat as block in mixed content. Listed elements get their own line with indentation. Unlisted elements stay inline with surrounding text. Only effective in `reflow` mode. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatblockelements%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.spaceBeforeEmptyCloseTag": { "type": "boolean", "default": true, "markdownDescription": "Insert space before end of self closing tag. \nExample:\n ``` -> ```. Default is `true`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatspacebeforeemptyclosetag%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.xsiSchemaLocationSplit": { "type": "string", @@ -480,7 +489,7 @@ ], "default": "onPair", "markdownDescription": "Split `xsi:schemaLocation` content. Default is `onPair`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatxsischemalocationsplit%22%7D%5D) for more information", - "scope": "window" + "scope": "language-overridable" }, "xml.format.splitAttributes": { "type": "string", @@ -508,17 +517,19 @@ ], "default": "preserve", "markdownDescription": "Controls how attributes are wrapped across lines. Values follow the same naming convention as VS Code's `html.format.wrapAttributes` for consistency. Default is `preserve`. Indicate level of indentation with `#xml.format.splitAttributesIndentSize#`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatsplitattributes%22%7D%5D) for more information.", - "scope": "window" + "scope": "language-overridable" }, "xml.format.splitAttributesIndentSize": { "type": "number", "default": 2, - "markdownDescription": "How many levels to indent the attributes by when `#xml.format.splitAttributes#` is `true`. Default value is `2`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatsplitattributesindentsize%22%7D%5D) for more information" + "markdownDescription": "How many levels to indent the attributes by when `#xml.format.splitAttributes#` is `true`. Default value is `2`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatsplitattributesindentsize%22%7D%5D) for more information", + "scope": "language-overridable" }, "xml.format.closingBracketNewLine": { "type": "boolean", "default": false, - "markdownDescription": "The option to put a closing bracket on a newline when `#xml.format.splitAttributes#` is `true`. Default value is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatclosingbracketnewline%22%7D%5D) for more information." + "markdownDescription": "The option to put a closing bracket on a newline when `#xml.format.splitAttributes#` is `true`. Default value is `false`. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Formatting%22%2C%22section%22%3A%22xmlformatclosingbracketnewline%22%7D%5D) for more information.", + "scope": "language-overridable" }, "xml.completion.autoCloseTags": { "type": "boolean", diff --git a/src/client/languageParticipants.ts b/src/client/languageParticipants.ts index 310d6e8d..5fdbe719 100644 --- a/src/client/languageParticipants.ts +++ b/src/client/languageParticipants.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, EventEmitter, extensions } from 'vscode'; +import { Event, EventEmitter, extensions, workspace } from 'vscode'; import { DocumentFilter, DocumentSelector } from 'vscode-languageclient'; const SCHEMES: string[] = [ @@ -57,6 +57,17 @@ export function getLanguageParticipants(): LanguageParticipants { } } } + + const settingParticipants = workspace.getConfiguration('xml').get('languageParticipants', []); + if (!Array.isArray(settingParticipants)) { + return !isEqualSet(languages, oldLanguages); + } + for (const languageId of settingParticipants) { + if (typeof languageId === 'string') { + languages.add(languageId); + } + } + return !isEqualSet(languages, oldLanguages); } update(); @@ -67,6 +78,14 @@ export function getLanguageParticipants(): LanguageParticipants { } }); + const configListener = workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('xml.languageParticipants')) { + if (update()) { + onDidChangeEmmiter.fire(); + } + } + }); + return { onDidChange: onDidChangeEmmiter.event, get documentSelector() { @@ -80,7 +99,7 @@ export function getLanguageParticipants(): LanguageParticipants { }); }, hasLanguage(languageId: string) { return languages.has(languageId); }, - dispose: () => changeListener.dispose() + dispose: () => { changeListener.dispose(); configListener.dispose(); } }; } diff --git a/src/client/xmlClient.ts b/src/client/xmlClient.ts index 77623a5c..16b95bf4 100644 --- a/src/client/xmlClient.ts +++ b/src/client/xmlClient.ts @@ -103,20 +103,33 @@ export async function startLanguageClient(context: ExtensionContext, executable: })); } - // Copied from: - // https://github.com/redhat-developer/vscode-java/pull/1081/files languageClient.onRequest(ConfigurationRequest.type, (params: ConfigurationParams) => { const result: any[] = []; const activeEditor: TextEditor | undefined = window.activeTextEditor; for (const item of params.items) { - if (activeEditor && activeEditor.document.uri.toString() === Uri.parse(item.scopeUri).toString()) { - if (item.section === "xml.format.insertSpaces") { - result.push(activeEditor.options.insertSpaces); - } else if (item.section === "xml.format.tabSize") { - result.push(activeEditor.options.tabSize); + const scopeUri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined; + const isActiveDocument = activeEditor && scopeUri + && activeEditor.document.uri.toString() === scopeUri.toString(); + if (isActiveDocument && item.section === "xml.format.insertSpaces") { + result.push(activeEditor.options.insertSpaces); + } else if (isActiveDocument && item.section === "xml.format.tabSize") { + result.push(activeEditor.options.tabSize); + } else if (scopeUri) { + const doc = workspace.textDocuments.find( + d => d.uri.toString() === scopeUri.toString()); + const scope = doc + ? { uri: scopeUri, languageId: doc.languageId } + : scopeUri; + if (!item.section) { + result.push(workspace.getConfiguration(undefined, scope)); + } else { + const lastDot = item.section.lastIndexOf('.'); + const sectionPrefix = lastDot > 0 ? item.section.substring(0, lastDot) : undefined; + const key = lastDot > 0 ? item.section.substring(lastDot + 1) : item.section; + result.push(workspace.getConfiguration(sectionPrefix, scope).get(key)); } } else { - result.push(workspace.getConfiguration(null, Uri.parse(item.scopeUri)).get(item.section)); + result.push(workspace.getConfiguration().get(item.section)); } } return result;