This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-25208-code-assist
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e61c89a7c8fb47460e27475abf7be9020b53a667
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 23:15:08 2026 +0200

    CAMEL-25208: camel-jbang - TUI Source tab completion in the uri attributes 
of XML routes
    
    The completion of the endpoint uris of Java routes works in the uri
    attribute of from, to, toD, wireTap, enrich, pollEnrich and poll in XML
    route files as well (the XML DSL had no completion): the component name,
    the endpoint options after ? or &amp;, their values. The context reader is
    EndpointUriContext now, with one reader per DSL.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 ...aStringContext.java => EndpointUriContext.java} | 56 +++++++++++++---
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  9 +--
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 54 ++++++++-------
 .../src/main/resources/tui/help/source.md          |  7 +-
 ...ionTest.java => EndpointUriCompletionTest.java} | 76 ++++++++++++++++++----
 5 files changed, 149 insertions(+), 53 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriContext.java
similarity index 53%
rename from 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java
rename to 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriContext.java
index 68613c4c8c92..fda7068659db 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriContext.java
@@ -19,22 +19,23 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.Set;
 
 /**
- * The string literal of a Java line the cursor is in, and the method it is an 
argument of: to("kafka:orders?bro| is in
- * to, with kafka:orders?bro before the cursor. Read from the line alone, so 
it works on a line being typed, which does
- * not parse (CAMEL-25208).
+ * The endpoint uri the cursor is in, read from the line alone so it works on 
a line being typed, which does not parse
+ * (CAMEL-25208): in Java the string literal given to a DSL method, 
to("kafka:orders?bro| is in to with kafka:orders?bro
+ * before the cursor; in XML the uri attribute of an element, &lt;to 
uri="kafka:orders?bro| is in to (its &amp;amp; read
+ * as &amp;).
  *
- * @param call   the method the literal is the first text of the arguments of, 
such as to or from
- * @param before the text of the literal before the cursor
+ * @param call   the DSL method (Java) or element (XML) of the uri, such as to 
or from
+ * @param before the text of the uri before the cursor
  */
-record JavaStringContext(String call, String before) {
+record EndpointUriContext(String call, String before) {
 
-    /** The DSL methods whose argument is an endpoint uri. */
+    /** The DSL methods and elements whose argument or attribute is an 
endpoint uri. */
     static final Set<String> ENDPOINT_CALLS = Set.of("from", "to", "toD", 
"wireTap", "enrich", "pollEnrich", "poll");
     /** The ones of them that consume from their endpoint. */
     static final Set<String> CONSUMER_CALLS = Set.of("from", "pollEnrich", 
"poll");
 
-    /** The context at the cursor, or null when the cursor is not in a string 
literal given to a method. */
-    static JavaStringContext at(String line, int col) {
+    /** The context at the cursor in a Java line, or null when it is not in a 
string literal given to a method. */
+    static EndpointUriContext inJava(String line, int col) {
         if (line == null || col < 0 || col > line.length()) {
             return null;
         }
@@ -70,7 +71,42 @@ record JavaStringContext(String call, String before) {
         if (call.isEmpty()) {
             return null;
         }
-        return new JavaStringContext(call, line.substring(open + 1, col));
+        return new EndpointUriContext(call, line.substring(open + 1, col));
+    }
+
+    /** The context at the cursor in an XML line, or null when it is not in 
the uri attribute of an element. */
+    static EndpointUriContext inXml(String line, int col) {
+        if (line == null || col < 0 || col > line.length()) {
+            return null;
+        }
+        String head = line.substring(0, col);
+        int attr = Math.max(head.lastIndexOf("uri=\""), 
head.lastIndexOf("uri='"));
+        if (attr < 0 || attr > 0 && !Character.isWhitespace(head.charAt(attr - 
1))) {
+            return null;
+        }
+        char quote = head.charAt(attr + 4);
+        String value = head.substring(attr + 5);
+        if (value.indexOf(quote) >= 0) {
+            // the attribute is closed before the cursor
+            return null;
+        }
+        int lt = head.lastIndexOf('<', attr);
+        if (lt < 0 || head.indexOf('>', lt) >= 0 && head.indexOf('>', lt) < 
attr) {
+            return null;
+        }
+        int start = lt + 1;
+        int end = start;
+        while (end < head.length() && 
(Character.isLetterOrDigit(head.charAt(end)) || head.charAt(end) == ':'
+                || head.charAt(end) == '-')) {
+            end++;
+        }
+        String element = head.substring(start, end);
+        // a namespace prefix: <camel:to uri="...
+        element = element.substring(element.indexOf(':') + 1);
+        if (element.isEmpty()) {
+            return null;
+        }
+        return new EndpointUriContext(element, value.replace("&amp;", "&"));
     }
 
     /** Whether the literal is the endpoint uri of a DSL method. */
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 477cd05b6957..4c6cde703bac 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -826,7 +826,8 @@ class SourceTab extends AbstractTab {
         boolean routeFile = !SourceEditAssist.isYamlFile(filePath)
                 && (isJavaRouteFile(filePath) || isCamelSourceFile(filePath));
         sourceViewer.setRouteValidator(routeFile ? content -> 
assist.validateRoutes(filePath, content) : null);
-        sourceViewer.setJavaStringCompletion(routeFile && 
filePath.getFileName().toString().endsWith(".java"));
+        String name = filePath.getFileName().toString();
+        sourceViewer.setUriCompletion(!routeFile ? null : 
name.endsWith(".java") ? "java" : "xml");
         if (isCamelSourceFile(filePath)) {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
             sourceViewer.setDeprecatedLineScanner(null);
@@ -838,10 +839,10 @@ class SourceTab extends AbstractTab {
                 
sourceViewer.setListItemNodeChecker(assist::isListChildrenNode);
                 
sourceViewer.setEditQuickDocProvider(assist::provideEditQuickDoc);
             } else {
-                // XML: the quick doc of the routes read into the model 
(CAMEL-25208)
+                // XML: the quick doc of the routes read into the model, Tab 
in the uri attributes (CAMEL-25208)
                 sourceViewer.setQuickDocProvider(cd -> 
assist.provideRouteQuickDocs(filePath, cd));
-                sourceViewer.setAutocompleteProvider(null);
-                sourceViewer.setAutocompleteValueProvider(null);
+                
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
+                
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
                 sourceViewer.setEditQuickDocProvider(
                         (lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row));
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index c061b4f7181e..67a84656d18e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -188,9 +188,10 @@ class SourceViewer {
     private EndpointValidator endpointValidator;
     private EndpointValidator simpleValidator;
     private EndpointValidator routeValidator;
-    private boolean javaStringCompletion;
-    /** The completion being chosen in a Java string: its row, the column its 
prefix ends at, the prefix and suffix. */
-    private JavaCompletion javaCompletion;
+    /** java or xml: Tab completes the endpoint uris of a Java or XML route 
file; null for neither. */
+    private String uriCompletion;
+    /** The completion being chosen in a uri: its row, the column its prefix 
ends at, the prefix and suffix. */
+    private UriCompletion pendingUriCompletion;
     private List<String> routeProblems = List.of();
     private List<String> validationErrors;
     private int validationErrorScroll;
@@ -272,14 +273,17 @@ class SourceViewer {
     }
 
     /**
-     * Tab completion inside the strings of a Java route file (CAMEL-25208): 
in the endpoint uri of from, to, toD... the
-     * component names, the endpoint options and their values, from the 
completion providers the YAML uris use.
+     * Tab completion in the endpoint uris of a Java or XML route file 
(CAMEL-25208): in the string given to from, to,
+     * toD... of Java, or the uri attribute of those elements in XML, the 
component names, the endpoint options and
+     * their values, from the completion providers the YAML uris use.
+     *
+     * @param dsl java or xml; null for a file that is neither
      */
-    void setJavaStringCompletion(boolean javaStringCompletion) {
-        this.javaStringCompletion = javaStringCompletion;
+    void setUriCompletion(String dsl) {
+        this.uriCompletion = dsl;
     }
 
-    private record JavaCompletion(int row, int endCol, String prefix, String 
suffix) {
+    private record UriCompletion(int row, int endCol, String prefix, String 
suffix) {
     }
 
     void hide() {
@@ -294,7 +298,7 @@ class SourceViewer {
         endpointValidator = null;
         simpleValidator = null;
         routeValidator = null;
-        javaStringCompletion = false;
+        uriCompletion = null;
     }
 
     void reset() {
@@ -335,7 +339,7 @@ class SourceViewer {
         endpointValidator = null;
         simpleValidator = null;
         routeValidator = null;
-        javaStringCompletion = false;
+        uriCompletion = null;
     }
 
     boolean isMarkdownMode() {
@@ -1011,24 +1015,26 @@ class SourceViewer {
     }
 
     private void openAutocomplete() {
-        javaCompletion = null;
+        pendingUriCompletion = null;
         if (isCamelYamlFile()) {
             openYamlAutocomplete();
-        } else if (javaStringCompletion) {
-            openJavaStringAutocomplete();
+        } else if (uriCompletion != null) {
+            openUriAutocomplete();
         } else {
             openPropertiesAutocomplete();
         }
     }
 
     /**
-     * The completion of the endpoint uri the cursor is in, in a Java route: 
the component before the colon, an option
-     * after ? or &, its value after =. Read from the line alone, so it works 
while the file does not parse.
+     * The completion of the endpoint uri the cursor is in, in a Java or XML 
route: the component before the colon, an
+     * option after ? or &, its value after =. Read from the line alone, so it 
works while the file does not parse.
      */
-    private void openJavaStringAutocomplete() {
+    private void openUriAutocomplete() {
         int row = editState.cursorRow();
         int col = editState.cursorCol();
-        JavaStringContext c = JavaStringContext.at(editState.getLine(row), 
col);
+        String line = editState.getLine(row);
+        EndpointUriContext c = "xml".equals(uriCompletion)
+                ? EndpointUriContext.inXml(line, col) : 
EndpointUriContext.inJava(line, col);
         if (c == null || !c.isEndpoint() || autocompleteProvider == null) {
             return;
         }
@@ -1076,14 +1082,14 @@ class SourceViewer {
             if (colon < 0) {
                 autocompletePopup.setTitlePrefix("Components");
             }
-            javaCompletion = new JavaCompletion(row, col, prefix, suffix);
+            pendingUriCompletion = new UriCompletion(row, col, prefix, suffix);
         }
     }
 
     /** Replaces the prefix the completion was opened on with the chosen item 
and what follows it (: or =). */
-    private void insertJavaCompletion(AutocompletePopup.CompletionItem item) {
-        JavaCompletion jc = javaCompletion;
-        javaCompletion = null;
+    private void insertUriCompletion(AutocompletePopup.CompletionItem item) {
+        UriCompletion jc = pendingUriCompletion;
+        pendingUriCompletion = null;
         if (editState.cursorRow() != jc.row()) {
             return;
         }
@@ -1116,7 +1122,7 @@ class SourceViewer {
     /** A character of the name or value being completed: up to the : of a 
component, the = of an option, the & or ". */
     private static boolean isUriWordChar(char c, String suffix) {
         if (suffix.isEmpty()) {
-            return c != '&' && c != '"' && c != '\\';
+            return c != '&' && c != '"' && c != '\'' && c != '\\';
         }
         return Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == 
'.';
     }
@@ -1286,8 +1292,8 @@ class SourceViewer {
 
     private void insertCompletion(AutocompletePopup.CompletionItem item, 
boolean valueMode, boolean listItem) {
         recordEditChange();
-        if (javaCompletion != null) {
-            insertJavaCompletion(item);
+        if (pendingUriCompletion != null) {
+            insertUriCompletion(item);
             return;
         }
         String currentLine = editState.getLine(editState.cursorRow());
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 2539d637f9ad..933c86b03efb 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -72,10 +72,11 @@ Press **F4** to enter edit mode, then **Tab** for 
context-aware completion:
 - Value completion shows enum choices, boolean values, and `{{placeholder}}`
   suggestions from your `.properties` files
 
-**Java DSL routes:**
+**Java and XML DSL routes:**
 - In the endpoint uri of `from`, `to`, `toD`, `wireTap`, `enrich`, `pollEnrich`
-  and `poll`, Tab completes the component name before the `:`, the endpoint
-  options after `?` or `&` (filtered by consumer/producer role, already given
+  and `poll` (the string given to them in Java, their `uri` attribute in XML),
+  Tab completes the component name before the `:`, the endpoint options after
+  `?` or `&` (`&amp;` in XML; filtered by consumer/producer role, already given
   ones left out), and the value of an option after `=`
 
 Use **Up/Down** to navigate, **Enter** to accept, **Esc** to dismiss, and
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriCompletionTest.java
similarity index 69%
rename from 
dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
rename to 
dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriCompletionTest.java
index 5f461b288d54..cf28f1a2cb63 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointUriCompletionTest.java
@@ -32,9 +32,9 @@ import org.junit.jupiter.api.io.TempDir;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
- * Tab completion in the endpoint uris of a Java route file (CAMEL-25208).
+ * Tab completion in the endpoint uris of Java and XML route files 
(CAMEL-25208).
  */
-class JavaStringCompletionTest {
+class EndpointUriCompletionTest {
 
     @TempDir
     Path tempDir;
@@ -46,30 +46,82 @@ class JavaStringCompletionTest {
 
     @Test
     void theStringTheCursorIsIn() {
-        JavaStringContext c = JavaStringContext.at("        
from(\"timer:tick?per\")", 28);
+        EndpointUriContext c = EndpointUriContext.inJava("        
from(\"timer:tick?per\")", 28);
         assertThat(c.call()).isEqualTo("from");
         assertThat(c.before()).isEqualTo("timer:tick?per");
         assertThat(c.isEndpoint()).isTrue();
         assertThat(c.role()).isEqualTo("consumer");
 
-        c = JavaStringContext.at("            .to( \"kaf", 21);
+        c = EndpointUriContext.inJava("            .to( \"kaf", 21);
         assertThat(c.call()).isEqualTo("to");
         assertThat(c.before()).isEqualTo("kaf");
         assertThat(c.role()).isEqualTo("producer");
 
         // after the string, in another call, in a comment, not in a string
-        assertThat(JavaStringContext.at("        from(\"timer:tick\").to(", 
30)).isNull();
+        assertThat(EndpointUriContext.inJava("        
from(\"timer:tick\").to(", 30)).isNull();
         // the second string of a concatenation is not the argument's start
-        assertThat(JavaStringContext.at("        log.info(\"x\" + \"y", 
25)).isNull();
-        assertThat(JavaStringContext.at("        log.info(\"x", 
19).call()).isEqualTo("info");
-        assertThat(JavaStringContext.at("        // to(\"kaf", 18)).isNull();
-        assertThat(JavaStringContext.at("        x = \"kaf", 17)).isNull();
+        assertThat(EndpointUriContext.inJava("        log.info(\"x\" + \"y", 
25)).isNull();
+        assertThat(EndpointUriContext.inJava("        log.info(\"x", 
19).call()).isEqualTo("info");
+        assertThat(EndpointUriContext.inJava("        // to(\"kaf", 
18)).isNull();
+        assertThat(EndpointUriContext.inJava("        x = \"kaf", 
17)).isNull();
         // an escaped quote stays in the string
-        assertThat(JavaStringContext.at("        to(\"a\\\"b", 
16).before()).isEqualTo("a\\\"b");
+        assertThat(EndpointUriContext.inJava("        to(\"a\\\"b", 
16).before()).isEqualTo("a\\\"b");
+    }
+
+    @Test
+    void theUriAttributeTheCursorIsIn() {
+        String line = "        <from uri=\"timer:tick?per\"/>";
+        EndpointUriContext c = EndpointUriContext.inXml(line, line.length() - 
3);
+        assertThat(c.call()).isEqualTo("from");
+        assertThat(c.before()).isEqualTo("timer:tick?per");
+        assertThat(c.role()).isEqualTo("consumer");
+
+        line = "<camel:to id=\"x\" uri='kafka:t?a=1&amp;b";
+        c = EndpointUriContext.inXml(line, line.length());
+        assertThat(c.call()).isEqualTo("to");
+        assertThat(c.before()).isEqualTo("kafka:t?a=1&b");
+
+        // the attribute closed before the cursor, another attribute, no 
element
+        line = "        <to uri=\"seda:a\" id=\"x";
+        assertThat(EndpointUriContext.inXml(line, line.length())).isNull();
+        line = "        <to id=\"seda";
+        assertThat(EndpointUriContext.inXml(line, line.length())).isNull();
+        assertThat(EndpointUriContext.inXml("uri=\"seda", 9)).isNull();
+    }
+
+    @Test
+    void anXmlUri() throws Exception {
+        SourceViewer viewer = viewer("routes.xml", """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="timer:tick?period=1000&amp;fixedR"/>
+                        <to uri="sed"/>
+                    </route>
+                </routes>
+                """);
+        viewer.setUriCompletion("xml");
+        cursorAt(viewer, 3, "\"/>");
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE));
+        type(viewer, "a");
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
+        assertThat(line(viewer, 3)).isEqualTo("        <to uri=\"seda:\"/>");
+
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.UP, KeyModifiers.NONE));
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+        for (int i = 0; i < 3; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE));
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
+        assertThat(line(viewer, 2)).isEqualTo("        <from 
uri=\"timer:tick?period=1000&amp;fixedRate=\"/>");
     }
 
     private SourceViewer viewer(String src) throws Exception {
-        Path file = tempDir.resolve("MyRoute.java");
+        return viewer("MyRoute.java", src);
+    }
+
+    private SourceViewer viewer(String fileName, String src) throws Exception {
+        Path file = tempDir.resolve(fileName);
         Files.writeString(file, src, StandardCharsets.UTF_8);
         AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of());
         AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
@@ -77,7 +129,7 @@ class JavaStringCompletionTest {
         SourceViewer viewer = new SourceViewer();
         viewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
         
viewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
-        viewer.setJavaStringCompletion(true);
+        viewer.setUriCompletion("java");
         viewer.loadFile(file);
         viewer.enterEditMode();
         return viewer;

Reply via email to