diff --git a/docs/BindingWithGrammar.md b/docs/BindingWithGrammar.md
index 7e2737a0..ee449d9a 100644
--- a/docs/BindingWithGrammar.md
+++ b/docs/BindingWithGrammar.md
@@ -1,6 +1,6 @@
# Binding with Grammar
-[vscode-xml](https://github.com/redhat-developer/vscode-xml) provides support for automatic XML schema/grammar binding through generation and/or using an existing `.xsd/.dtd` file.
+[vscode-xml](https://github.com/redhat-developer/vscode-xml) provides support for automatic XML schema/grammar binding through generation and/or using an existing `.xsd/.dtd/.rng/.rnc` file.
> **Tip:** You can also [generate a grammar from an XML document](GenerateGrammarFromXML.md) or [generate an XML document from a grammar](GenerateXMLFromGrammar.md).
@@ -8,88 +8,226 @@
[Validation with XSD grammar](Validation.md#validation-with-xsd-grammar) and [Validation with DTD grammar](Validation.md#validation-with-dtd-grammar) provide more information on manual binding of an XML document.
-In the following sections, we would like to bind the XML document `foo.xml`:
-```xml
-
-
-
-```
-with an associated grammar file.
+The binding wizard can be triggered by [Command](#command), [CodeLens](#codelens), or [Quick Fix](#quick-fix).
+
+In the following sections, we would like to bind the XML document `team.xml` with a grammar file.
-## Binding with Existing Grammar
+Consider an XSD document, `team.xsd` (in the same directory as `team.xml`), that declares **two root elements** (`person` and `team`):
-Consider an XSD document, `foo.xsd` (in the same directory as `foo.xml`), defined as follows :
-```xsd
+```xml
-
+
+
+
+
+
+
+
+
+
-
+
+
```
-and a DTD document, `foo.dtd` (in the same directory as `foo.xml`), defined as follows:
+
+a DTD document, `team.dtd` (in the same directory as `team.xml`):
+
```dtd
-
-
+
+
+
+
```
-Either file can be bound using the [XML Binding Wizard](#the-xml-binding-wizard), which can be triggered by [Command](#command), [CodeLens](#codelens) or [Quick Fix](#quick-fix).
+and a RelaxNG document, `team.rng` (in the same directory as `team.xml`):
-### The XML Binding Wizard
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
-After opening the binding wizard using one of the methods mentioned above, a dropdown with the following 2 options will appear at the Command Palette:
+or equivalently in RelaxNG Compact syntax, `team.rnc`:
+
+```rnc
+start = person | team
+
+person = element person {
+ element name { text },
+ element age { text }
+}
+
+team = element team {
+ element name { text },
+ person+
+}
+```
+
+After opening the binding wizard, a dropdown with 3 options appears at the Command Palette:

-1. Standard (xsi, DOCTYPE)
+For Standard and XML Model options, a file explorer lets you select the grammar file:
+
+
+
+## Standard (xsi, DOCTYPE)
+
+### XSD binding
+
+If the selected file is an XSD file, this option binds the grammar by adding the `xmlns:xsi` (with the value `http://www.w3.org/2001/XMLSchema-instance`) and `xsi:noNamespaceSchemaLocation` (with the path to the grammar file) attributes to the root tag.
+
+Since `team.xsd` declares **two root elements** (`person` and `team`), vscode-xml uses a **SnippetTextEdit** with a [choice placeholder](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax), letting you pick the root element directly in the editor:
+
+
+
+After selecting `person`:
+```xml
+
+```
+
+> **Note:** If the XSD declares a single root element, a regular TextEdit is used directly without the choice dropdown.
+
+### With targetNamespace
+
+If the XSD declares a `targetNamespace` with `elementFormDefault="qualified"`:
+```xml
+
+```
+then the binding uses `xmlns` and `xsi:schemaLocation` instead:
+```xml
+
+```
+
+### DTD binding
+
+If the selected file is a DTD file, this option adds a `` declaration at the beginning of the document:
+
+
+
+After selecting `team`:
+```xml
+
+
+```
+
+## XML Model association
+
+This option binds the grammar by adding an `` processing instruction at the beginning of the document:
-If the selected file is an XSD file, this option will bind the grammar file by adding the `xmlns:xsi` (with the value as "http://www.w3.org/2001/XMLSchema-instance") and `xsi:noNamespaceSchemaLocation` (with the value as the path to the grammar file) attributes to the root tag:
+
+
+After selecting `person` with an XSD file:
```xml
-
-
-
+
+
```
-If the XSD file contains a `targetNamespace` attribute, with an `elementFormDefault` attribute with value "qualified" in the `xs:schema` tag, as follows:
-```xsd
-
+The same applies when selecting a DTD file:
+```xml
+
+
```
-then the XML document will have the `targetNamespace` set as a `xmlns` attribute and an additional attribute for `xsi:schemaLocation`:
+
+### RelaxNG binding
+
+When selecting a RelaxNG file (`.rng` or `.rnc`), the `` processing instruction:
+
+For a `.rng` (XML syntax) file:
```xml
-
-
-
+
+
```
-If the selected file is a DTD file, this option will bind the grammar file by adding a ` SYSTEM "">` tag at the beginning of the document:
+For a `.rnc` (Compact syntax) file:
```xml
-
-
-
-
+
+
```
-2. XML Model association
+> **Note:** RelaxNG grammars can only be bound via XML Model or Catalog entry, not via Standard (xsi, DOCTYPE).
+
+## Catalog entry
+
+This option binds the XML document through an [XML Catalog](https://www.oasis-open.org/committees/entity/spec-2001-08-06.html). An XML Catalog is a mapping file that redirects XML identifiers (public IDs, system IDs, URIs) to local grammar resources, allowing you to manage grammar bindings centrally rather than embedding paths in each XML file.
+
+When you select this option, the wizard presents two choices:
+
+### Pick an existing catalog entry
-This option will bind the grammar file by adding the `xml-model` tag at the beginning of the document, with an `href` value as the path to the grammar file:
+If your workspace has registered catalogs (via the `xml.catalogs` setting) that already contain entries, they are listed grouped by catalog file. Selecting an entry inserts the appropriate binding in your XML document (DOCTYPE for public/system entries, xsi:schemaLocation for XSD entries).
+
+
+
+With multiple root elements, the choice placeholder lets you select the root element, just like with Standard and XML Model options.
+
+### Add a new catalog entry
+
+If no existing entry matches, select "Add new catalog entry..." at the bottom of the list.
+
+
+
+The wizard will:
+
+1. Ask which catalog should contain the new entry (an existing registered catalog, browse for another catalog file, or create a new catalog file)
+
+
+
+2. Ask for the entry type (`public`, `system`, or `uri`)
+3. Ask for the identifier (the public ID, system ID, or namespace URI)
+4. Let you select the grammar file
+
+The entry is added to the catalog file, the catalog is registered in `xml.catalogs` if needed, and the XML document is bound using the catalog entry.
+
+For example, with a `public` catalog entry, the resulting binding is a PUBLIC DOCTYPE:
+```xml
+
+
+```
+
+With a `uri` catalog entry pointing to an XSD with a target namespace, the result uses `xsi:schemaLocation`:
```xml
-
-
-
-
+
```
-The same applies when selecting `foo.dtd`.
-After selecting an option, you can select the desired grammar file using your file explorer:
+> **Note:** The generated catalog entry respects your formatting settings (indentation, tab/spaces, space before empty close tag).
-
+## Triggers
+
+The binding wizard can be triggered in three ways:
### Command
@@ -97,15 +235,13 @@ The command "XML: Bind to grammar/schema file" can be executed from the VSCode c

-If the current XML document is not bound using an existing grammar/schema, the command will have the user select a existing `.xsd/.dtd` grammar file to bind to the XML document.
-
-Otherwise, an error will be thrown on the client if the XML document already has a bound grammar/schema.
+If the current XML document already has a bound grammar/schema, an error will be shown.
### CodeLens
-When [CodeLenses](./Preferences.md#code-lens) are enabled, an unbound XML document will display a CodeLens above the root element as follows:
+When [CodeLenses](./Preferences.md#code-lens) are enabled, an unbound XML document will display a CodeLens above the root element:
-
+
### Quick Fix
@@ -113,7 +249,7 @@ For any unbound XML document, a Quick Fix suggestion will appear beside the root

-With the cursor on the first opening tag, use `Ctrl + .`, `Quick Fix...` or the lightbulb that appears and select the "Bind to existing grammar/schema"
+With the cursor on the first opening tag, use `Ctrl + .`, `Quick Fix...` or the lightbulb that appears and select "Bind to existing grammar/schema":

diff --git a/docs/Features/DTDFeatures.md b/docs/Features/DTDFeatures.md
index 0b4c8b60..f92fefc8 100644
--- a/docs/Features/DTDFeatures.md
+++ b/docs/Features/DTDFeatures.md
@@ -1,5 +1,9 @@
# DTD Features
+## Binding
+
+See [Binding with Grammar](../BindingWithGrammar.md#dtd-binding) to associate an XML file with a DTD using the binding wizard.
+
## CodeLens Support
CodeLens is supported for `.dtd` files, which can show where an element declaration is referenced.
diff --git a/docs/Features/RelaxNGFeatures.md b/docs/Features/RelaxNGFeatures.md
index 71ad79cf..71aa3882 100644
--- a/docs/Features/RelaxNGFeatures.md
+++ b/docs/Features/RelaxNGFeatures.md
@@ -13,6 +13,7 @@ It supports both:
You can associate your XML file with `RelaxNG grammar` (`file` or `URL`) using :
+ * the [Binding Wizard](../BindingWithGrammar.md#relaxng-binding) (XML Model or Catalog entry)
* an `xml-model` processing instruction
```xml
@@ -147,4 +148,4 @@ The following file, `addressBook.xml`, references `addressBook.rng` using the `x
When an unbound XML file is open, a RELAX NG schema can be generated from the opening tag in the XML file.
-See: [Generate RELAX NG from XML](../BindingWithGrammar.md#generate-relax-ng-from-xml).
+See: [Generate Grammar from XML](../GenerateGrammarFromXML.md).
diff --git a/docs/Features/XMLCatalogFeatures.md b/docs/Features/XMLCatalogFeatures.md
index 12d6b638..581afaae 100644
--- a/docs/Features/XMLCatalogFeatures.md
+++ b/docs/Features/XMLCatalogFeatures.md
@@ -1,5 +1,9 @@
# XML Catalog Features
+## Binding with Catalog
+
+The [Binding Wizard](../BindingWithGrammar.md#catalog-entry) supports binding an XML document to a grammar through an XML Catalog entry. You can pick an existing catalog entry or add a new one directly from the wizard.
+
## Validation
XML Catalog is supported with both DTD and XSD. Please see here for more information on the usage of [XML Catalog With DTD](../Validation.md#xml-catalog-with-dtd) and [XML Catalog With XSD](../Validation.md#xml-catalog-with-xsd).
diff --git a/docs/Features/XSDFeatures.md b/docs/Features/XSDFeatures.md
index 45117c6f..31253edd 100644
--- a/docs/Features/XSDFeatures.md
+++ b/docs/Features/XSDFeatures.md
@@ -1,5 +1,9 @@
# XSD Features
+## Binding
+
+See [Binding with Grammar](../BindingWithGrammar.md#xsd-binding) to associate an XML file with an XSD using the binding wizard.
+
## CodeLens Support
CodeLens is supported for `.xsd` files, which can show where a type is referenced.
diff --git a/docs/images/BindingWithGrammar/BindingWizardCatalogAddNewEntry.png b/docs/images/BindingWithGrammar/BindingWizardCatalogAddNewEntry.png
new file mode 100644
index 00000000..fe46c90e
Binary files /dev/null and b/docs/images/BindingWithGrammar/BindingWizardCatalogAddNewEntry.png differ
diff --git a/docs/images/BindingWithGrammar/BindingWizardCatalogEntries.png b/docs/images/BindingWithGrammar/BindingWizardCatalogEntries.png
new file mode 100644
index 00000000..20ba951f
Binary files /dev/null and b/docs/images/BindingWithGrammar/BindingWizardCatalogEntries.png differ
diff --git a/docs/images/BindingWithGrammar/BindingWizardCatalogSelect.png b/docs/images/BindingWithGrammar/BindingWizardCatalogSelect.png
new file mode 100644
index 00000000..14095d4d
Binary files /dev/null and b/docs/images/BindingWithGrammar/BindingWizardCatalogSelect.png differ
diff --git a/docs/images/BindingWithGrammar/BindingWizardDemo.gif b/docs/images/BindingWithGrammar/BindingWizardDemo.gif
index 3c2102bd..df8ceaf3 100644
Binary files a/docs/images/BindingWithGrammar/BindingWizardDemo.gif and b/docs/images/BindingWithGrammar/BindingWizardDemo.gif differ
diff --git a/docs/images/BindingWithGrammar/BindingWizardDropdown.png b/docs/images/BindingWithGrammar/BindingWizardDropdown.png
index f80c94b1..651cb84d 100644
Binary files a/docs/images/BindingWithGrammar/BindingWizardDropdown.png and b/docs/images/BindingWithGrammar/BindingWizardDropdown.png differ
diff --git a/docs/images/BindingWithGrammar/BindingWizardFileExplorer.png b/docs/images/BindingWithGrammar/BindingWizardFileExplorer.png
index 9dd3be42..dec1315a 100644
Binary files a/docs/images/BindingWithGrammar/BindingWizardFileExplorer.png and b/docs/images/BindingWithGrammar/BindingWizardFileExplorer.png differ
diff --git a/docs/images/BindingWithGrammar/SnippetChoiceDOCTYPE.png b/docs/images/BindingWithGrammar/SnippetChoiceDOCTYPE.png
new file mode 100644
index 00000000..24e1777a
Binary files /dev/null and b/docs/images/BindingWithGrammar/SnippetChoiceDOCTYPE.png differ
diff --git a/docs/images/BindingWithGrammar/SnippetChoiceXMLModel.png b/docs/images/BindingWithGrammar/SnippetChoiceXMLModel.png
new file mode 100644
index 00000000..9b034c39
Binary files /dev/null and b/docs/images/BindingWithGrammar/SnippetChoiceXMLModel.png differ
diff --git a/docs/images/BindingWithGrammar/SnippetChoiceXSI.png b/docs/images/BindingWithGrammar/SnippetChoiceXSI.png
new file mode 100644
index 00000000..b6568099
Binary files /dev/null and b/docs/images/BindingWithGrammar/SnippetChoiceXSI.png differ
diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts
index ad98ec16..aa986076 100644
--- a/src/commands/registerCommands.ts
+++ b/src/commands/registerCommands.ts
@@ -182,12 +182,20 @@ for (const entry of bindingTypes.entries()) {
async function grammarAssociationCommand(documentURI: Uri, languageClient: LanguageClient) {
// A click on Bind to grammar/schema... has been processed in the XML document which is not bound to a grammar
- // step 1: select local / remote Uri type
+ // step 1: select local / remote / catalog Uri type
let grammarURI;
- const uriType = await window.showQuickPick([{ "label": "local" }, { "label": "remote" }], { placeHolder: "Binding type" });
+ const uriType = await window.showQuickPick([
+ { label: "local", description: "Select a grammar file from the local file system" },
+ { label: "remote", description: "Enter a remote URL for the grammar" },
+ { label: "Catalog entry", description: "Bind using an XML catalog entry" }
+ ], { placeHolder: "How do you want to bind the grammar?" });
if (!uriType) {
return;
}
+ if (uriType.label === 'Catalog entry') {
+ await catalogBindingFlow(documentURI, languageClient);
+ return;
+ }
if (uriType.label === 'remote') {
let predefinedUrl = await env.clipboard.readText();
if (!predefinedUrl || !predefinedUrl.startsWith('http')) {
@@ -289,15 +297,30 @@ async function bindWithFileAssociation(documentURI: Uri, grammarURI: Uri, docume
async function bindWithStandard(documentURI: Uri, grammarURI: Uri, bindingType: string, languageClient: LanguageClient) {
const identifier = TextDocumentIdentifier.create(documentURI.toString());
const result = await commands.executeCommand(ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT, identifier, grammarURI.toString(), bindingType);
- // Insert the proper syntax for binding
const lspTextDocumentEdit = result;
- const workEdits = new WorkspaceEdit();
+ await applyTextDocumentEdit(documentURI, lspTextDocumentEdit, languageClient);
+}
+
+/**
+ * Apply a TextDocumentEdit from the server, handling both plain TextEdit
+ * (with newText) and SnippetTextEdit (with snippet) for cursor placeholders.
+ */
+async function applyTextDocumentEdit(documentURI: Uri, lspTextDocumentEdit: TextDocumentEdit, languageClient: LanguageClient) {
+ const edits: (vscode.TextEdit | vscode.SnippetTextEdit)[] = [];
for (const edit of lspTextDocumentEdit.edits) {
- if ('newText' in edit) {
- workEdits.replace(documentURI, languageClient.protocol2CodeConverter.asRange(edit.range), edit.newText);
+ const r = (edit as any).range;
+ const range = new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character);
+ if ('snippet' in edit) {
+ edits.push(new vscode.SnippetTextEdit(range, new SnippetString((edit as any).snippet.value)));
+ } else if ('newText' in edit) {
+ edits.push(new vscode.TextEdit(range, (edit as TextEdit).newText));
}
}
- workspace.applyEdit(workEdits); // apply the edits
+ if (edits.length > 0) {
+ const workEdits = new WorkspaceEdit();
+ workEdits.set(documentURI, edits);
+ await workspace.applyEdit(workEdits);
+ }
}
/**
@@ -306,13 +329,322 @@ async function bindWithStandard(documentURI: Uri, grammarURI: Uri, bindingType:
* @param key the filename/path of the xml document
* @param value the object to add to the config
*/
-function addToValueToSettingArray(key: string, value: T): void {
+async function addToValueToSettingArray(key: string, value: T): Promise {
const settingArray: T[] = workspace.getConfiguration().get(key, []);
if (settingArray.includes(value)) {
return;
}
settingArray.push(value);
- workspace.getConfiguration().update(key, settingArray, ConfigurationTarget.Workspace);
+ await workspace.getConfiguration().update(key, settingArray, ConfigurationTarget.Workspace);
+}
+
+/**
+ * Bindable catalog entry returned by the server
+ */
+interface BindableCatalogEntry {
+ catalogURI: string;
+ entryType: string;
+ identifier: string;
+ targetURI: string;
+ uriValue: string;
+ grammarType: string;
+}
+
+/**
+ * Flow for binding an XML document to a grammar via an XML catalog entry
+ */
+async function catalogBindingFlow(documentURI: Uri, languageClient: LanguageClient) {
+ let entries: BindableCatalogEntry[];
+ try {
+ entries = await commands.executeCommand(
+ ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND,
+ ServerCommandConstants.LIST_BINDABLE_CATALOG_ENTRIES
+ );
+ } catch (error) {
+ window.showErrorMessage('Error listing catalog entries: ' + error.message);
+ return;
+ }
+
+ if (!entries || entries.length === 0) {
+ const choice = await window.showQuickPick([
+ { label: "Create new catalog...", description: "Create a new XML catalog file and add an entry" },
+ { label: "Select existing catalog...", description: "Select an existing catalog file and add an entry" }
+ ], { placeHolder: "No catalog entries found" });
+ if (!choice) return;
+
+ let catalogURI: string;
+ if (choice.label === "Create new catalog...") {
+ catalogURI = await createCatalogFlow();
+ } else {
+ catalogURI = await selectExistingCatalogFlow();
+ }
+ if (!catalogURI) return;
+
+ await addCatalogEntryFlow(catalogURI, documentURI, languageClient);
+ return;
+ }
+
+ const items: (QuickPickItem & { entry?: BindableCatalogEntry })[] = [];
+
+ const entriesByCatalog = new Map();
+ for (const entry of entries) {
+ const existing = entriesByCatalog.get(entry.catalogURI) || [];
+ existing.push(entry);
+ entriesByCatalog.set(entry.catalogURI, existing);
+ }
+
+ for (const [catURI, catalogEntries] of entriesByCatalog) {
+ items.push({
+ label: getFileName(catURI),
+ kind: vscode.QuickPickItemKind.Separator
+ } as any);
+
+ for (const entry of catalogEntries) {
+ items.push({
+ label: `${entry.entryType.toUpperCase()} ${entry.identifier}`,
+ description: getFileName(entry.targetURI),
+ detail: entry.grammarType.toUpperCase(),
+ entry: entry
+ });
+ }
+ }
+
+ items.push({
+ label: '',
+ kind: vscode.QuickPickItemKind.Separator
+ } as any);
+ items.push({
+ label: "$(add) Add new catalog entry...",
+ description: "Create a new entry in a catalog file"
+ });
+
+ const picked = await window.showQuickPick(items, {
+ placeHolder: "Select a catalog entry to bind",
+ matchOnDescription: true,
+ matchOnDetail: true
+ });
+
+ if (!picked) return;
+
+ if (!picked.entry) {
+ const catalogOptions: QuickPickItem[] = [];
+ for (const cat of entriesByCatalog.keys()) {
+ catalogOptions.push({ label: getFileName(cat), description: cat });
+ }
+ catalogOptions.push({ label: "$(folder-opened) Select existing catalog...", description: "Browse for a catalog file" });
+ catalogOptions.push({ label: "$(add) Create new catalog...", description: "Create a new catalog file" });
+
+ const catalogPick = await window.showQuickPick(catalogOptions, {
+ placeHolder: "Which catalog should contain the new entry?"
+ });
+ if (!catalogPick) return;
+
+ let catalogURI: string;
+ if (catalogPick.label.includes("Create new catalog")) {
+ catalogURI = await createCatalogFlow();
+ } else if (catalogPick.label.includes("Select existing catalog")) {
+ catalogURI = await selectExistingCatalogFlow();
+ } else {
+ catalogURI = catalogPick.description;
+ }
+ if (!catalogURI) return;
+
+ await addCatalogEntryFlow(catalogURI, documentURI, languageClient);
+ return;
+ }
+
+ await bindWithCatalogEntry(documentURI, picked.entry, languageClient);
+}
+
+async function bindWithCatalogEntry(documentURI: Uri, entry: BindableCatalogEntry, languageClient: LanguageClient) {
+ const identifier = TextDocumentIdentifier.create(documentURI.toString());
+ try {
+ const result = await commands.executeCommand(
+ ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND,
+ ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT,
+ identifier,
+ entry.entryType,
+ "catalog",
+ entry.identifier,
+ entry.targetURI,
+ entry.uriValue
+ );
+ const lspTextDocumentEdit = result;
+ await applyTextDocumentEdit(documentURI, lspTextDocumentEdit, languageClient);
+ } catch (error) {
+ window.showErrorMessage('Error binding grammar via catalog: ' + error.message);
+ }
+}
+
+async function createCatalogFlow(): Promise {
+ const saveOptions: vscode.SaveDialogOptions = {
+ saveLabel: 'Create Catalog',
+ filters: { 'XML Catalog': ['xml'] },
+ defaultUri: workspace.workspaceFolders?.[0]?.uri
+ };
+ const fileUri = await window.showSaveDialog(saveOptions);
+ if (!fileUri) return undefined;
+
+ let catalogContent: string;
+ try {
+ catalogContent = await commands.executeCommand(
+ ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND,
+ ServerCommandConstants.CREATE_CATALOG,
+ fileUri.toString()
+ );
+ } catch (error) {
+ catalogContent = '\n\n\n';
+ }
+
+ const encoder = new TextEncoder();
+ await workspace.fs.writeFile(fileUri, encoder.encode(catalogContent));
+
+ await addToValueToSettingArray('xml.catalogs', fileUri.fsPath);
+
+ const doc = await workspace.openTextDocument(fileUri);
+ await window.showTextDocument(doc, { preview: false });
+
+ return fileUri.toString();
+}
+
+async function selectExistingCatalogFlow(): Promise {
+ const options: OpenDialogOptions = {
+ canSelectMany: false,
+ openLabel: 'Select XML Catalog',
+ filters: { 'XML files': ['xml'] }
+ };
+ const fileUri = await window.showOpenDialog(options);
+ if (!fileUri || !fileUri[0]) return undefined;
+
+ await addToValueToSettingArray('xml.catalogs', fileUri[0].fsPath);
+
+ return fileUri[0].toString();
+}
+
+async function addCatalogEntryFlow(catalogURI: string, documentURI: Uri, languageClient: LanguageClient) {
+ let grammarURI: string;
+ const uriType = await window.showQuickPick(
+ [{ label: "local" }, { label: "remote" }],
+ { placeHolder: "Select grammar source" }
+ );
+ if (!uriType) return;
+
+ if (uriType.label === 'remote') {
+ let predefinedUrl = await env.clipboard.readText();
+ if (!predefinedUrl || !predefinedUrl.startsWith('http')) {
+ predefinedUrl = '';
+ }
+ grammarURI = await window.showInputBox({
+ title: 'Enter grammar URL',
+ value: predefinedUrl
+ });
+ } else {
+ const options: OpenDialogOptions = {
+ canSelectMany: false,
+ openLabel: 'Select grammar file',
+ filters: { 'Grammar files': ['xsd', 'dtd', 'rng', 'rnc'] }
+ };
+ const fileUri = await window.showOpenDialog(options);
+ if (!fileUri || !fileUri[0]) return;
+ grammarURI = fileUri[0].toString();
+ }
+ if (!grammarURI) return;
+
+ const grammarPath = grammarURI.toLowerCase();
+ const defaultEntryType = grammarPath.endsWith('.dtd') ? 'public' : 'uri';
+
+ const entryTypeOptions: QuickPickItem[] = [
+ { label: 'public', description: 'Map a PUBLIC identifier to the grammar (recommended for DTDs)' },
+ { label: 'system', description: 'Map a SYSTEM identifier to the grammar' },
+ { label: 'uri', description: 'Map a URI/namespace to the grammar (recommended for XSD/RNG)' }
+ ];
+ const reorderedOptions = entryTypeOptions.sort((a, b) => {
+ if (a.label === defaultEntryType) return -1;
+ if (b.label === defaultEntryType) return 1;
+ return 0;
+ });
+
+ const pickedEntryType = await window.showQuickPick(reorderedOptions, {
+ placeHolder: 'Select catalog entry type'
+ });
+ if (!pickedEntryType) return;
+ const entryType = pickedEntryType.label;
+
+ let identifierPrompt: string;
+ let identifierPlaceholder: string;
+ switch (entryType) {
+ case 'public':
+ identifierPrompt = 'Enter the PUBLIC identifier';
+ identifierPlaceholder = '-//Example//DTD My Document//EN';
+ break;
+ case 'system':
+ identifierPrompt = 'Enter the SYSTEM identifier';
+ identifierPlaceholder = 'http://example.com/schema.dtd';
+ break;
+ default:
+ identifierPrompt = 'Enter the URI/namespace';
+ identifierPlaceholder = 'http://example.com/schema';
+ break;
+ }
+ const identifier = await window.showInputBox({
+ title: identifierPrompt,
+ placeHolder: identifierPlaceholder,
+ prompt: identifierPrompt,
+ validateInput: (value) => {
+ if (!value || value.trim().length === 0) {
+ return 'Identifier cannot be empty';
+ }
+ return null;
+ }
+ });
+ if (!identifier) return;
+
+ try {
+ const result = await commands.executeCommand(
+ ClientCommandConstants.EXECUTE_WORKSPACE_COMMAND,
+ ServerCommandConstants.ADD_CATALOG_ENTRY,
+ catalogURI,
+ entryType,
+ identifier,
+ grammarURI
+ );
+ if (result) {
+ const lspTextDocumentEdit = result;
+ const catalogFileUri = Uri.parse(lspTextDocumentEdit.textDocument.uri);
+ const workEdits = new WorkspaceEdit();
+ for (const edit of lspTextDocumentEdit.edits) {
+ if ('newText' in edit) {
+ workEdits.replace(catalogFileUri, languageClient.protocol2CodeConverter.asRange(edit.range), edit.newText);
+ }
+ }
+ const applied = await workspace.applyEdit(workEdits);
+ if (!applied) {
+ window.showWarningMessage('Failed to apply catalog entry edit.');
+ return;
+ }
+ const catalogDoc = workspace.textDocuments.find(d => d.uri.toString() === catalogFileUri.toString());
+ if (catalogDoc) {
+ await catalogDoc.save();
+ if (catalogDoc.isDirty) {
+ window.showWarningMessage('Failed to save the catalog file. The catalog entry was not persisted.');
+ return;
+ }
+ }
+ }
+ } catch (error) {
+ window.showErrorMessage('Error adding catalog entry: ' + error.message);
+ return;
+ }
+
+ let grammarType = 'dtd';
+ if (grammarURI.toLowerCase().endsWith('.xsd')) grammarType = 'xsd';
+ else if (grammarURI.toLowerCase().endsWith('.rng')) grammarType = 'rng';
+ else if (grammarURI.toLowerCase().endsWith('.rnc')) grammarType = 'rnc';
+
+ const entry: BindableCatalogEntry = {
+ catalogURI, entryType, identifier, targetURI: grammarURI, uriValue: grammarURI, grammarType
+ };
+ await bindWithCatalogEntry(documentURI, entry, languageClient);
}
/**
diff --git a/src/commands/serverCommandConstants.ts b/src/commands/serverCommandConstants.ts
index f392d9f8..ffb6c3dd 100644
--- a/src/commands/serverCommandConstants.ts
+++ b/src/commands/serverCommandConstants.ts
@@ -59,4 +59,19 @@ export const GENERATE_XML = "xml.grammar.generate";
/**
* Command to generate a grammar (XSD, DTD, RelaxNG) from an XML document
*/
-export const GENERATE_GRAMMAR_FROM_XML = "xml.grammar.generateFromXml";
\ No newline at end of file
+export const GENERATE_GRAMMAR_FROM_XML = "xml.grammar.generateFromXml";
+
+/**
+ * Command to list all bindable catalog entries
+ */
+export const LIST_BINDABLE_CATALOG_ENTRIES = "xml.catalog.entries.bindable";
+
+/**
+ * Command to add an entry to a catalog file
+ */
+export const ADD_CATALOG_ENTRY = "xml.catalog.entry.add";
+
+/**
+ * Command to create a new catalog file
+ */
+export const CREATE_CATALOG = "xml.catalog.create";
\ No newline at end of file