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 e2f90f2eec6add2d5e241199442e00728a58d655
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 22:57:59 2026 +0200

    CAMEL-25208: camel-jbang - TUI Source tab quick doc for Java and XML routes
    
    The quick doc of a Java or XML DSL route file is read from its model
    (RouteNodes) instead of YAML line patterns: the component of an endpoint
    with the options it is given, the EIP of a step, the language of its
    expression, in the panel for the line of the cursor and inline. Java route
    files had no quick doc; XML files had the YAML patterns, which seldom
    matched. The catalog falls back to the CLI's own when no integration is
    selected, so it works on a project that is not running.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/SourceEditAssist.java  | 118 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  12 ++-
 .../src/main/resources/tui/help/source.md          |   2 +
 .../commands/tui/SourceRouteValidationTest.java    |  55 ++++++++++
 4 files changed, 186 insertions(+), 1 deletion(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index 238e0c378479..e3e093424fca 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -39,6 +39,7 @@ import java.util.regex.Pattern;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteNodes;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.ComponentModel;
@@ -85,6 +86,11 @@ final class SourceEditAssist {
     private long javaSourcesCacheTime;
     private Path javaSourcesCacheDir;
 
+    // the nodes of the route file last read for its quick doc
+    private List<RouteNodes.Node> routeNodesCache = List.of();
+    private String routeNodesContent;
+    private Path routeNodesFile;
+
     // Component name completion cache (keyed by catalog version)
     private String componentsCatalogVersion;
     private List<AutocompletePopup.CompletionItem> consumerComponents;
@@ -1577,6 +1583,118 @@ final class SourceEditAssist {
         return answer;
     }
 
+    /**
+     * The quick doc of each line of a Java or XML DSL route file 
(CAMEL-25208), read from its model: the component of
+     * an endpoint and the options it is given, the EIP of a step.
+     */
+    Map<Integer, List<SourceViewer.DocEntry>> provideRouteQuickDocs(Path file, 
List<JsonObject> codeData) {
+        CamelCatalog catalog = validationCatalog();
+        if (catalog == null || codeData.isEmpty()) {
+            return Map.of();
+        }
+        List<String> lines = new ArrayList<>(codeData.size());
+        for (JsonObject jo : codeData) {
+            lines.add(jo.getString("code") != null ? jo.getString("code") : 
"");
+        }
+        Map<Integer, List<SourceViewer.DocEntry>> result = new 
LinkedHashMap<>();
+        for (RouteNodes.Node n : routeNodes(file, lines)) {
+            int idx = n.line() - 1;
+            if (idx < 0 || idx >= codeData.size() || result.containsKey(idx)) {
+                continue;
+            }
+            if (n.kind() == RouteNodes.Kind.ENDPOINT) {
+                EipDocSupport.buildEndpointInlineDoc(result, codeData, 
catalog, n.uri(), idx);
+            } else if (n.kind() == RouteNodes.Kind.STEP && !hasEndpoint(file, 
lines, n.line())
+                    && catalog.eipModel(n.eip()) != null) {
+                EipDocSupport.buildEipInlineDoc(result, codeData, catalog, 
n.eip(), null, idx);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * The quick doc of the line the cursor is on in a Java or XML DSL route 
file (CAMEL-25208): the component of an
+     * endpoint with the options it is given, else the EIP of the step and the 
language of its expression.
+     */
+    List<SourceViewer.DocEntry> provideRouteEditQuickDoc(Path file, 
List<String> lines, int cursorRow) {
+        CamelCatalog catalog = validationCatalog();
+        if (catalog == null || lines == null || cursorRow < 0 || cursorRow >= 
lines.size()) {
+            return List.of();
+        }
+        List<RouteNodes.Node> here = RouteNodes.at(routeNodes(file, lines), 
cursorRow + 1);
+        List<SourceViewer.DocEntry> answer = new ArrayList<>();
+        for (RouteNodes.Node n : here) {
+            if (n.kind() == RouteNodes.Kind.ENDPOINT && n.uri() != null) {
+                endpointDoc(catalog, n.uri(), answer);
+                return answer;
+            }
+        }
+        for (RouteNodes.Node n : here) {
+            if (n.kind() == RouteNodes.Kind.STEP && answer.isEmpty()) {
+                EipModel eip = catalog.eipModel(n.eip());
+                if (eip != null) {
+                    answer.add(SourceViewer.DocEntry.of((eip.getTitle() != 
null ? eip.getTitle() : n.eip()) + " — "
+                                                        + 
(eip.getDescription() != null ? eip.getDescription() : "")));
+                }
+            } else if (n.kind() == RouteNodes.Kind.EXPRESSION && n.language() 
!= null) {
+                LanguageModel language = catalog.languageModel(n.language());
+                if (language != null) {
+                    answer.add(SourceViewer.DocEntry.of((language.getTitle() 
!= null ? language.getTitle() : n.language())
+                                                        + (n.predicate() ? " 
predicate" : " expression") + ": "
+                                                        + (n.text() != null ? 
n.text() : "")));
+                }
+            }
+        }
+        return answer;
+    }
+
+    /** The component of an endpoint, then each option it is given with its 
doc. */
+    private static void endpointDoc(CamelCatalog catalog, String uri, 
List<SourceViewer.DocEntry> answer) {
+        String component = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+        ComponentModel model = catalog.componentModel(component);
+        if (model == null) {
+            return;
+        }
+        String title = model.getTitle() != null ? model.getTitle() : component;
+        answer.add(SourceViewer.DocEntry.of(title + " — " + 
(model.getDescription() != null ? model.getDescription() : "")));
+        Map<String, String> props;
+        try {
+            props = catalog.endpointProperties(uri);
+        } catch (Exception e) {
+            return;
+        }
+        int q = uri.indexOf('?');
+        String query = q >= 0 ? uri.substring(q + 1) : "";
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
+            String value = props != null ? props.get(opt.getName()) : null;
+            // the options written in the uri's query, not the path parameters 
already in the title's line
+            if (value != null && (query.startsWith(opt.getName() + "=") || 
query.contains("&" + opt.getName() + "="))) {
+                String doc = EipDocSupport.formatOptionDoc(opt);
+                answer.add(SourceViewer.DocEntry.of(opt.getName() + "=" + 
value + (doc != null ? " — " + doc : "")));
+            }
+        }
+    }
+
+    private boolean hasEndpoint(Path file, List<String> lines, int line) {
+        for (RouteNodes.Node n : RouteNodes.at(routeNodes(file, lines), line)) 
{
+            if (n.kind() == RouteNodes.Kind.ENDPOINT) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** The nodes of a route file's content, read again only when the content 
changed. */
+    private List<RouteNodes.Node> routeNodes(Path file, List<String> lines) {
+        String content = String.join("\n", lines);
+        if (!content.equals(routeNodesContent) || !Objects.equals(file, 
routeNodesFile)) {
+            routeNodesCache = RouteAssist.nodes(file.getFileName().toString(), 
content, validationCatalog(), javaSources());
+            routeNodesContent = content;
+            routeNodesFile = file;
+        }
+        return routeNodesCache;
+    }
+
     /** The Java sources of the project, for the constants a route refers to 
in another class; kept for a while. */
     private Map<String, Supplier<String>> javaSources() {
         long now = System.currentTimeMillis();
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 f138f7262cee..69c3a283a4c1 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
@@ -837,10 +837,20 @@ 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)
+                sourceViewer.setQuickDocProvider(cd -> 
assist.provideRouteQuickDocs(filePath, cd));
                 sourceViewer.setAutocompleteProvider(null);
                 sourceViewer.setAutocompleteValueProvider(null);
-                sourceViewer.setEditQuickDocProvider(null);
+                sourceViewer.setEditQuickDocProvider(
+                        (lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row));
             }
+        } else if (routeFile) {
+            // Java: the quick doc of the routes read into the model by the 
Java DSL parser (CAMEL-25208)
+            sourceViewer.setQuickDocProvider(cd -> 
assist.provideRouteQuickDocs(filePath, cd));
+            sourceViewer.setDeprecatedLineScanner(null);
+            sourceViewer.setAutocompleteProvider(null);
+            sourceViewer.setAutocompleteValueProvider(null);
+            sourceViewer.setEditQuickDocProvider((lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row));
         } else if (SourceEditAssist.isPropertiesFile(filePath)) {
             
sourceViewer.setQuickDocProvider(assist::providePropertiesQuickDocs);
             
sourceViewer.setDeprecatedLineScanner(assist::scanDeprecatedProperties);
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 75e8d5191dc5..b6eb604a176f 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
@@ -20,6 +20,8 @@ explorer on the left and a source viewer on the right.
 - **Ctrl+R** — open refactoring menu in edit mode (YAML files only; choose an 
action for the current line)
 - **Space** — cycle format (YAML/Java/XML) for Camel routes
 - Quick documentation panel is shown at the bottom for Camel source files
+  (YAML, XML and Java DSL routes: the component and options of an endpoint,
+  the EIP of a step, the language of an expression)
 - **/** — search in source
 - **h** — highlight text
 - **n/N** — next/previous match
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
index 99326794c7fb..bbde4724d29b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
@@ -19,12 +19,15 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -90,6 +93,58 @@ class SourceRouteValidationTest {
         assertThat(errors.get(0)).startsWith("Line 4: Simple syntax error");
     }
 
+    @Test
+    void theQuickDocOfAJavaLine() {
+        String src = """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class MyRoute extends RouteBuilder {
+                    @Override
+                    public void configure() throws Exception {
+                        from("timer:tick?period=1000")
+                            .filter(simple("${header.foo} == 'bar'"))
+                                .to("seda:out");
+                    }
+                }
+                """;
+        List<String> lines = List.of(src.split("\n"));
+        SourceEditAssist assist = assist();
+        List<SourceViewer.DocEntry> from = 
assist.provideRouteEditQuickDoc(Path.of("MyRoute.java"), lines, 5);
+        assertThat(from).extracting(SourceViewer.DocEntry::text)
+                .satisfies(t -> assertThat(t.get(0)).startsWith("Timer — "))
+                .anySatisfy(t -> assertThat(t).startsWith("period=1000 — "));
+        List<SourceViewer.DocEntry> filter = 
assist.provideRouteEditQuickDoc(Path.of("MyRoute.java"), lines, 6);
+        assertThat(filter).extracting(SourceViewer.DocEntry::text)
+                .containsExactly(filter.get(0).text(), "Simple predicate: 
${header.foo} == 'bar'");
+        assertThat(filter.get(0).text()).startsWith("Filter — ");
+        // a line of plain Java has none
+        assertThat(assist.provideRouteEditQuickDoc(Path.of("MyRoute.java"), 
lines, 2)).isEmpty();
+
+        List<JsonObject> codeData = new ArrayList<>();
+        for (String l : lines) {
+            JsonObject jo = new JsonObject();
+            jo.put("code", l);
+            codeData.add(jo);
+        }
+        Map<Integer, List<SourceViewer.DocEntry>> all = 
assist.provideRouteQuickDocs(Path.of("MyRoute.java"), codeData);
+        assertThat(all).containsKeys(5, 6, 7);
+        assertThat(all.get(7).get(0).text()).startsWith("SEDA — ");
+    }
+
+    @Test
+    void theQuickDocOfAnXmlLine() {
+        List<String> lines = List.of("""
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="timer:tick?period=1000"/>
+                        <to uri="seda:out"/>
+                    </route>
+                </routes>
+                """.split("\n"));
+        List<SourceViewer.DocEntry> from = 
assist().provideRouteEditQuickDoc(Path.of("routes.xml"), lines, 2);
+        assertThat(from.get(0).text()).startsWith("Timer — ");
+    }
+
     @Test
     void aProblemIsMarkedAndSaidButTheFileIsSaved() throws Exception {
         Path file = tempDir.resolve("MyRoute.java");

Reply via email to