Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
- [XInclude features](Features/XIncludeFeatures.md#xinclude-features)
- [XML Catalog features](Features/XMLCatalogFeatures.md#xml-catalog-features)
- [XML References features](Features/XMLReferencesFeatures.md#xml-references-features)
- [XML Colors features](Features/XMLColorsFeatures.md#xml-colors-features)
- [XML Colors features](Features/XMLColorsFeatures.md#xml-colors-features)
- [XML Embedded Languages features](Features/XMLEmbeddedLanguagesFeatures.md#xml-embedded-languages-features)
204 changes: 204 additions & 0 deletions docs/Features/XMLEmbeddedLanguagesFeatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# XML Embedded Languages Features

XML embedded languages support provides syntax highlighting for embedded language content (e.g., Groovy, SQL, JavaScript) inside XML text and CDATA nodes using TextMate grammars.

This feature uses the `xml.embeddedLanguages` setting with XPath expressions to identify which XML nodes contain embedded code, and the `languageId` to specify which language grammar to use for tokenization.

## How it works

1. You define XPath expressions that match text/CDATA nodes containing embedded code
2. You specify the `languageId` (e.g., `groovy`, `sql`, `javascript`)
3. The TextMate grammar is automatically resolved from installed VS Code extensions
4. LSP semantic tokens are used to provide syntax highlighting

## Groovy in XML workflow files

Given this XML workflow file with Groovy code inside CDATA sections:

```xml
<workflow workflowId="demo.validationCheck">
<scriptsInclude><![CDATA[
def renderUiUrl() {
return programContext.renderDefaultTaskUrl()
}
]]></scriptsInclude>
<start id="start">
<handlerScript><![CDATA[
programContext.setProjectState("ELIGIBILITY_CHECK", "VALIDATION")
return true
]]></handlerScript>
</start>
</workflow>
```

You can enable Groovy syntax highlighting in these CDATA sections with the following setting:

```json
"xml.embeddedLanguages": [
{
"pattern": "**/workflow.xml",
"expressions": [
{
"xpath": "//scriptsInclude/text()",
"languageId": "groovy"
},
{
"xpath": "//handlerScript/text()",
"languageId": "groovy"
}
]
}
]
```

## SQL in MyBatis mapper files

Given a MyBatis mapper XML file with SQL inside text nodes:

```xml
<mapper namespace="com.example.UserMapper">
<select id="findUser" resultType="User">
SELECT id, name, email
FROM users
WHERE id = #{id}
</select>
<insert id="insertUser">
INSERT INTO users (name, email)
VALUES (#{name}, #{email})
</insert>
</mapper>
```

You can enable SQL syntax highlighting with:

```json
"xml.embeddedLanguages": [
{
"pattern": "**/*Mapper.xml",
"expressions": [
{
"xpath": "mapper/select/text()",
"languageId": "sql"
},
{
"xpath": "mapper/insert/text()",
"languageId": "sql"
},
{
"xpath": "mapper/update/text()",
"languageId": "sql"
},
{
"xpath": "mapper/delete/text()",
"languageId": "sql"
}
]
}
]
```

## JavaScript and CSS in XHTML

Given an XHTML file with embedded JavaScript and CSS:

```xml
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style type="text/css">
body { color: red; }
</style>
</head>
<body>
<script type="text/javascript">
var x = document.getElementById("demo");
</script>
</body>
</html>
```

You can enable syntax highlighting for both languages:

```json
"xml.embeddedLanguages": [
{
"pattern": "**/*.xhtml",
"expressions": [
{
"xpath": "//script/text()",
"languageId": "javascript"
},
{
"xpath": "//style/text()",
"languageId": "css"
}
]
}
]
```

## Using Document Matcher

Like `xml.colors` and `xml.references`, the `xml.embeddedLanguages` setting supports [Document Matcher](../DocumentMatcher.md) criteria for more precise document matching beyond file patterns:

```json
"xml.embeddedLanguages": [
{
"pattern": "**/*.xml",
"rootElement": ["workflow"],
"expressions": [
{
"xpath": "//handlerScript/text()",
"languageId": "groovy"
}
]
}
]
```

Available Document Matcher properties:

| Property | Description |
|---|---|
| `pattern` | File name pattern (glob syntax) |
| `namespaceURI` | Root element namespace URI patterns |
| `rootElement` | Root element name patterns |
| `publicId` | DOCTYPE public ID patterns |
| `systemId` | DOCTYPE system ID patterns |
| `grammarURI` | Resolved grammar URI patterns |

## Custom TextMate grammar

By default, the TextMate grammar is automatically resolved from installed VS Code extensions based on the `languageId`. If a language is not available as a VS Code extension, you can specify the grammar path manually:

```json
"xml.embeddedLanguages": [
{
"pattern": "**/*.xml",
"expressions": [
{
"xpath": "//code/text()",
"languageId": "mylang",
"grammarPath": "/path/to/mylang.tmLanguage.json"
}
]
}
]
```

## Supported token types

The following LSP semantic token types are used for embedded language highlighting:

| Token Type | TextMate Scope |
|---|---|
| Comment | `comment.*` |
| Keyword | `keyword.*`, `storage.type`, `storage.modifier` |
| String | `string.*` |
| Number | `constant.numeric` |
| Function | `entity.name.function`, `support.function` |
| Class | `entity.name.type`, `entity.name.class`, `support.class` |
| Variable | `variable.*`, `constant.character`, `constant.language` |
| Operator | `keyword.operator`, `punctuation.*` |
| Namespace | `entity.name.namespace`, `entity.name.module` |
| Parameter | `variable.parameter` |
| Regexp | `string.regexp` |
16 changes: 16 additions & 0 deletions docs/Features/XMLFeatures.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ When placing the cursor on an XML tag, the corresponding opening/closing tag wil

![Highlight Corresponding XML](../images/Features/HighlightCorrespondingXML.gif)

## Semantic Tokens

LemMinX provides LSP semantic tokens for XML documents to enhance syntax highlighting beyond what TextMate grammars offer. Semantic tokens provide precise, context-aware highlighting for:

| Token Type | Description |
|---|---|
| Property | Attribute names (e.g., `id`, `name`) |
| Variable | Entity references (e.g., `&amp;`) |
| Type | Element and notation names |

Semantic tokens work alongside the TextMate grammar-based highlighting and provide additional semantic information that the editor theme can use for more accurate coloring.

### Embedded Language Highlighting

LemMinX also supports syntax highlighting for embedded language content inside XML text and CDATA nodes (e.g., Groovy, SQL, JavaScript) using TextMate grammars. See [XML Embedded Languages features](XMLEmbeddedLanguagesFeatures.md#xml-embedded-languages-features) for more information.

## Symbols from Outline and Breadcrumbs

The `OUTLINE` dropdown under `EXPLORER` details the XML tags in the document and their children. The `Breadcrumbs` bar also shows the hierarchy of the tag that the cursor is on. Both features permit jumping to the tag(s) displayed.
Expand Down
4 changes: 2 additions & 2 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ const server_dir = '../lemminx';

gulp.task('build_server', function (done) {
cp.execSync(mvnw() + " clean verify -DskipTests", { cwd: server_dir, stdio: [0, 1, 2] });
gulp.src(server_dir + '/org.eclipse.lemminx/target/org.eclipse.lemminx-uber.jar', { encoding: false })
gulp.src(server_dir + '/org.eclipse.lemminx.uber/target/org.eclipse.lemminx-uber.jar', { encoding: false })
.pipe(gulp.dest('./server'));
done();
});

gulp.task('build_server_with_binary', function (done) {
cp.execSync(mvnw() + " clean verify -DskipTests -Dnative", { cwd: server_dir, stdio: [0, 1, 2] });
gulp.src([server_dir + '/org.eclipse.lemminx/target/org.eclipse.lemminx-uber.jar', server_dir + '/org.eclipse.lemminx/target/lemminx-*'], { encoding: false })
gulp.src([server_dir + '/org.eclipse.lemminx.uber/target/org.eclipse.lemminx-uber.jar', server_dir + '/org.eclipse.lemminx/target/lemminx-*'], { encoding: false })
.pipe(gulp.dest('./server'))
.on('end', function () {
glob.Glob('./server/lemminx-*.txt', (_er, txtfiles) => {
Expand Down
64 changes: 64 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,70 @@
"markdownDescription": "Allows colors for the given file name patterns or [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22documentmatcher%22%7D%5D) criteria. See [here](command:xml.open.docs?%5B%7B%22page%22%3A%22Features/XMLColorsFeatures%22%2C%22section%22%3A%22xmlcolorsfeatures%22%7D%5D) for more information.",
"scope": "window"
},
"xml.embeddedLanguages": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"markdownDescription": "Matches the files that embedded language expressions declared with `expressions` applies to.\n\nMore information on the glob syntax: https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22documentmatcher%22%7D%5D) for more information."
},
"namespaceURI": {
"type": "array",
"items": { "type": "string" },
"markdownDescription": "Root element namespace URI patterns (glob supported).\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22namespaceuri%22%7D%5D) for more information."
},
"rootElement": {
"type": "array",
"items": { "type": "string" },
"markdownDescription": "Root element name patterns (glob supported). Example: `mapper` matches `<mapper>`. Uses local name (without prefix).\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22rootelement%22%7D%5D) for more information."
},
"publicId": {
"type": "array",
"items": { "type": "string" },
"markdownDescription": "DOCTYPE public ID patterns (glob supported).\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22publicid%22%7D%5D) for more information."
},
"systemId": {
"type": "array",
"items": { "type": "string" },
"markdownDescription": "DOCTYPE system ID patterns (glob supported).\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22systemid%22%7D%5D) for more information."
},
"grammarURI": {
"type": "array",
"items": { "type": "string" },
"markdownDescription": "Resolved grammar URI patterns (glob supported).\n\nSee [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22grammaruri%22%7D%5D) for more information."
},
"expressions": {
"type": "array",
"default": [],
"items": {
"type": "object",
"properties": {
"xpath": {
"type": "string",
"description": "The XPath expression to match text/CDATA nodes containing embedded language content (e.g., select/text(), //![CDATA[]])."
},
"languageId": {
"type": "string",
"description": "The language identifier of the embedded content (e.g., java, sql, javascript). Used to resolve the TextMate grammar from installed VS Code extensions."
}
},
"required": [
"xpath",
"languageId"
]
}
}
},
"required": [
"expressions"
]
},
"markdownDescription": "Allows embedded language syntax highlighting for the given file name patterns or [Document Matcher](command:xml.open.docs?%5B%7B%22page%22%3A%22DocumentMatcher%22%2C%22section%22%3A%22documentmatcher%22%7D%5D) criteria. Uses TextMate grammars from installed VS Code extensions to tokenize embedded content in XML text/CDATA nodes.\n\nExample:\n```json\n[\n {\n \"pattern\": \"**/*.mybatis.xml\",\n \"expressions\": [\n {\n \"xpath\": \"select/text()\",\n \"languageId\": \"sql\"\n }\n ]\n }\n]\n```",
"scope": "window"
},
"xml.filePathSupport.mappings": {
"type": "array",
"default": [],
Expand Down
8 changes: 7 additions & 1 deletion src/commands/clientCommandConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ export const EXECUTE_WORKSPACE_COMMAND = 'xml.workspace.executeCommand';
/**
* VSCode client command to generate XML from a grammar file.
*/
export const GENERATE_XML_FROM_GRAMMAR = 'xml.generate.fromGrammar';
export const GENERATE_XML_FROM_GRAMMAR = 'xml.generate.fromGrammar';

/**
* Resolve a TextMate grammar path for a given language ID.
* Called by the XML language server to find grammars from installed VS Code extensions.
*/
export const TEXT_MATE_GRAMMAR = 'xml.textMateGrammar';
23 changes: 23 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@
registerGenerateXMLCommands(context, languageClient);
registerRestartLanguageServerCommand(context, languageClient);
registerConfigurationUpdateCommand();
registerTextMateGrammarCommand(context);

// Register client command to execute custom XML Language Server command
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND, (command, ...rest) => {
let token: CancellationToken;
let commandArgs: any[] = rest;

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / matrix-smoke-test (linux-aarch_64) / smoke-test

Unexpected any. Specify a different type

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / matrix-smoke-test (linux-x86_64) / smoke-test

Unexpected any. Specify a different type

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / matrix-smoke-test-windows (win32) / smoke-test

Unexpected any. Specify a different type

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / matrix-smoke-test (osx-aarch_64) / smoke-test

Unexpected any. Specify a different type

Check warning on line 46 in src/commands/registerCommands.ts

View workflow job for this annotation

GitHub Actions / matrix-smoke-test (osx-x86_64) / smoke-test

Unexpected any. Specify a different type
if (rest && rest.length && CancellationToken.is(rest[rest.length - 1])) {
token = rest[rest.length - 1];
commandArgs = rest.slice(0, rest.length - 1);
Expand Down Expand Up @@ -702,3 +703,25 @@
}
}

/**
* Register the command to resolve TextMate grammar paths for embedded languages.
* Called by the XML language server via xml/executeClientCommand.
*/
function registerTextMateGrammarCommand(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.TEXT_MATE_GRAMMAR, (languageId: string) => {
for (const ext of vscode.extensions.all) {
const grammars = ext.packageJSON?.contributes?.grammars;
if (!Array.isArray(grammars)) {
continue;
}
for (const grammar of grammars) {
if (grammar.language === languageId && grammar.path) {
const grammarPath = path.join(ext.extensionPath, grammar.path);
return { grammarPath: grammarPath, scopeName: grammar.scopeName || '' };
}
}
}
return null;
}));
}

Loading