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;