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

davsclaus pushed a commit to branch feature/CAMEL-24259-tui-tabs-and-inline-docs
in repository https://gitbox.apache.org/repos/asf/camel.git

commit aec301767e7b857aa9abdd5f588117f1cbd35a3f
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Jul 26 10:46:23 2026 +0200

    camel-jbang - TUI: Fix inline doc scroll, multi-route support, and YAML 
parameters
    
    Fix source viewer scroll calculation to account for inline doc lines
    when toggling quick docs (i key). Load processor detail for all routes
    (routeId=*) so inline docs appear across all routes in multi-route files.
    Descend into YAML parameters: blocks to document individual endpoint
    options. Resolve component name from uri option for endpoint-bearing EIPs
    (e.g. "To (SQL)"). Place doc after multi-line YAML values instead of
    splitting them. Skip redundant uri in clustered options when parameters
    are expanded.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/DiagramTab.java    |  37 ++---
 .../dsl/jbang/core/commands/tui/RoutesTab.java     | 178 +++++++++++++++++----
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  40 ++++-
 3 files changed, 204 insertions(+), 51 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
index 3e33db3fbddc..7c7ac2116bac 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTab.java
@@ -1201,17 +1201,16 @@ class DiagramTab extends AbstractTab {
             detailScroll = 0;
         }
 
-        // load route detail once per route (covers all processors)
-        if (drillDownRouteId != null && 
!drillDownRouteId.equals(cachedRouteDetailId)
-                && !drillDownRouteId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = drillDownRouteId;
+        // load route detail once (covers all routes and processors)
+        if (drillDownRouteId != null && cachedRouteDetail == null
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
-            String rid = drillDownRouteId;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(rid);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = rid;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -1397,12 +1396,7 @@ class DiagramTab extends AbstractTab {
         if (cachedRouteDetail == null || nodeId == null) {
             return null;
         }
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null) {
-            return null;
-        }
-        for (Object obj : processors) {
-            JsonObject p = (JsonObject) obj;
+        for (JsonObject p : RoutesTab.getAllProcessors(cachedRouteDetail)) {
             if (nodeId.equals(p.getString("id"))) {
                 return p;
             }
@@ -1417,7 +1411,7 @@ class DiagramTab extends AbstractTab {
         try {
             JsonObject root = new JsonObject();
             root.put("action", "processor-detail");
-            root.put("routeId", routeId);
+            root.put("routeId", "*");
             return ctx.executeAction(ctx.selectedPid, root, 5000);
         } catch (Exception e) {
             return null;
@@ -1431,8 +1425,8 @@ class DiagramTab extends AbstractTab {
             return Map.of();
         }
 
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null || processors.isEmpty()) {
+        List<JsonObject> processors = 
RoutesTab.getAllProcessors(cachedRouteDetail);
+        if (processors.isEmpty()) {
             return Map.of();
         }
 
@@ -1440,8 +1434,7 @@ class DiagramTab extends AbstractTab {
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
         Map<Integer, List<String>> result = new LinkedHashMap<>();
-        for (Object obj : processors) {
-            JsonObject proc = (JsonObject) obj;
+        for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
                 continue;
@@ -1465,14 +1458,14 @@ class DiagramTab extends AbstractTab {
 
     private void ensureProcessorDetailLoaded(String routeId) {
         if (routeId != null && cachedRouteDetail == null
-                && !routeId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = routeId;
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(routeId);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = routeId;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index ad5e22996c01..6590530ad796 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -1812,16 +1812,15 @@ class RoutesTab extends AbstractTab {
             detailScroll = 0;
         }
 
-        if (drillDownRouteId != null && 
!drillDownRouteId.equals(cachedRouteDetailId)
-                && !drillDownRouteId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = drillDownRouteId;
+        if (drillDownRouteId != null && cachedRouteDetail == null
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
-            String rid = drillDownRouteId;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(rid);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = rid;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -2009,7 +2008,7 @@ class RoutesTab extends AbstractTab {
         try {
             JsonObject root = new JsonObject();
             root.put("action", "processor-detail");
-            root.put("routeId", routeId);
+            root.put("routeId", "*");
             return ctx.executeAction(ctx.selectedPid, root, 5000);
         } catch (Exception e) {
             return null;
@@ -2020,12 +2019,7 @@ class RoutesTab extends AbstractTab {
         if (cachedRouteDetail == null || nodeId == null) {
             return null;
         }
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null) {
-            return null;
-        }
-        for (Object obj : processors) {
-            JsonObject p = (JsonObject) obj;
+        for (JsonObject p : getAllProcessors(cachedRouteDetail)) {
             if (nodeId.equals(p.getString("id"))) {
                 return p;
             }
@@ -2033,6 +2027,35 @@ class RoutesTab extends AbstractTab {
         return null;
     }
 
+    static List<JsonObject> getAllProcessors(JsonObject routeDetail) {
+        if (routeDetail == null) {
+            return List.of();
+        }
+        JsonArray routes = (JsonArray) routeDetail.get("routes");
+        if (routes != null) {
+            List<JsonObject> all = new ArrayList<>();
+            for (Object obj : routes) {
+                JsonObject route = (JsonObject) obj;
+                JsonArray procs = (JsonArray) route.get("processors");
+                if (procs != null) {
+                    for (Object p : procs) {
+                        all.add((JsonObject) p);
+                    }
+                }
+            }
+            return all;
+        }
+        JsonArray procs = (JsonArray) routeDetail.get("processors");
+        if (procs != null) {
+            List<JsonObject> all = new ArrayList<>();
+            for (Object p : procs) {
+                all.add((JsonObject) p);
+            }
+            return all;
+        }
+        return List.of();
+    }
+
     // ---- Quick doc (q toggle in source viewer) ----
 
     private Map<Integer, List<String>> provideAllQuickDocs(List<JsonObject> 
cd) {
@@ -2040,8 +2063,8 @@ class RoutesTab extends AbstractTab {
             return Map.of();
         }
 
-        JsonArray processors = (JsonArray) cachedRouteDetail.get("processors");
-        if (processors == null || processors.isEmpty()) {
+        List<JsonObject> processors = getAllProcessors(cachedRouteDetail);
+        if (processors.isEmpty()) {
             return Map.of();
         }
 
@@ -2049,8 +2072,7 @@ class RoutesTab extends AbstractTab {
         CamelCatalog catalog = info != null ? getCatalog(info) : null;
 
         Map<Integer, List<String>> result = new LinkedHashMap<>();
-        for (Object obj : processors) {
-            JsonObject proc = (JsonObject) obj;
+        for (JsonObject proc : processors) {
             Integer line = proc.getInteger("line");
             if (line == null || line <= 0) {
                 continue;
@@ -2074,14 +2096,14 @@ class RoutesTab extends AbstractTab {
 
     private void ensureProcessorDetailLoaded(String routeId) {
         if (routeId != null && cachedRouteDetail == null
-                && !routeId.equals(detailLoadingRouteId)) {
-            detailLoadingRouteId = routeId;
+                && !"*".equals(detailLoadingRouteId)) {
+            detailLoadingRouteId = "*";
             detailLoading = true;
             if (ctx.runner != null) {
                 ctx.backgroundExecutor.execute(() -> {
-                    JsonObject result = requestRouteProcessorDetail(routeId);
+                    JsonObject result = requestRouteProcessorDetail("*");
                     cachedRouteDetail = result;
-                    cachedRouteDetailId = routeId;
+                    cachedRouteDetailId = "*";
                     detailLoading = false;
                 });
             }
@@ -2148,10 +2170,31 @@ class RoutesTab extends AbstractTab {
             CamelCatalog catalog, String type, JsonObject opts, int eipIdx) {
 
         EipModel model = catalog != null ? catalog.eipModel(type) : null;
+
+        // For endpoint-bearing EIPs, resolve the component from the uri option
+        ComponentModel compModel = null;
+        if (opts != null && catalog != null) {
+            Object uriObj = opts.get("uri");
+            if (uriObj != null) {
+                String uri = uriObj.toString();
+                String comp = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+                compModel = catalog.componentModel(comp);
+            }
+        }
+
         List<String> titleLines = new ArrayList<>();
         if (model != null && model.getTitle() != null) {
-            String desc = model.getDescription() != null ? 
truncateText(model.getDescription(), 80) : "";
-            titleLines.add(model.getTitle() + " — " + desc);
+            String eipTitle = model.getTitle();
+            if (compModel != null && compModel.getTitle() != null) {
+                eipTitle += " (" + compModel.getTitle() + ")";
+            }
+            String desc;
+            if (compModel != null && compModel.getDescription() != null) {
+                desc = truncateText(compModel.getDescription(), 80);
+            } else {
+                desc = model.getDescription() != null ? 
truncateText(model.getDescription(), 80) : "";
+            }
+            titleLines.add(eipTitle + " — " + desc);
         } else {
             titleLines.add(type);
         }
@@ -2166,20 +2209,31 @@ class RoutesTab extends AbstractTab {
                     optionDocs.put(opt.getName(), opt);
                 }
             }
+            if (compModel != null) {
+                for (ComponentModel.EndpointOptionModel opt : 
compModel.getEndpointOptions()) {
+                    if (opt.getName() != null && 
!optionDocs.containsKey(opt.getName())) {
+                        optionDocs.put(opt.getName(), opt);
+                    }
+                }
+            }
             inlineParameterDocs(result, cd, eipIdx, optionDocs);
 
             // For Java/XML where options are on the same line,
             // cluster the option docs under the title
             if (result.size() == beforeSize + 1 && opts != null) {
-                clusterEipOptions(titleLines, opts, optionDocs);
+                boolean hasParams = hasParametersChild(cd, eipIdx);
+                clusterEipOptions(titleLines, opts, optionDocs, hasParams);
             }
         }
     }
 
     private static void clusterEipOptions(
             List<String> docLines, JsonObject opts,
-            Map<String, BaseOptionModel> optionDocs) {
+            Map<String, BaseOptionModel> optionDocs, boolean skipUri) {
         for (Map.Entry<String, Object> entry : opts.entrySet()) {
+            if (skipUri && "uri".equals(entry.getKey())) {
+                continue;
+            }
             BaseOptionModel optModel = optionDocs.get(entry.getKey());
             if (optModel != null) {
                 String optDoc = formatOptionDoc(optModel);
@@ -2190,6 +2244,22 @@ class RoutesTab extends AbstractTab {
         }
     }
 
+    static boolean hasParametersChild(List<JsonObject> cd, int eipIdx) {
+        int eipIndent = lineIndent(cd, eipIdx);
+        for (int i = eipIdx + 1; i < cd.size(); i++) {
+            String code = cd.get(i).get("code") != null ? 
cd.get(i).get("code").toString() : "";
+            int indent = leadingSpaces(code);
+            if (indent < eipIndent && !code.isBlank()) {
+                break;
+            }
+            String trimmed = code.stripLeading();
+            if (trimmed.startsWith("parameters:")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     static void inlineParameterDocs(
             Map<Integer, List<String>> result, List<JsonObject> cd,
             int eipIdx, Map<String, BaseOptionModel> optionDocs) {
@@ -2200,6 +2270,9 @@ class RoutesTab extends AbstractTab {
 
         int eipIndent = lineIndent(cd, eipIdx);
         int childIndent = -1;
+        boolean inParameters = false;
+        int paramIndent = -1;
+        int parametersLineIndent = -1;
 
         for (int i = eipIdx + 1; i < cd.size(); i++) {
             String code = cd.get(i).get("code") != null ? 
cd.get(i).get("code").toString() : "";
@@ -2210,6 +2283,35 @@ class RoutesTab extends AbstractTab {
             if (childIndent < 0 && indent > eipIndent) {
                 childIndent = indent;
             }
+
+            if (inParameters) {
+                if (paramIndent < 0 && indent > parametersLineIndent && 
!code.isBlank()) {
+                    paramIndent = indent;
+                }
+                if (paramIndent > 0 && indent <= parametersLineIndent && 
!code.isBlank()) {
+                    inParameters = false;
+                    paramIndent = -1;
+                    parametersLineIndent = -1;
+                } else if (paramIndent > 0 && indent == paramIndent) {
+                    String trimmed = code.stripLeading();
+                    int colon = trimmed.indexOf(':');
+                    if (colon > 0) {
+                        String key = trimmed.substring(0, colon).strip();
+                        BaseOptionModel doc = optionDocs.get(key);
+                        if (doc != null) {
+                            String docLine = formatOptionDoc(doc);
+                            if (docLine != null) {
+                                int docIdx = lastContinuationLine(cd, i, 
indent);
+                                result.put(docIdx, List.of(docLine));
+                            }
+                        }
+                    }
+                    continue;
+                } else {
+                    continue;
+                }
+            }
+
             if (childIndent > 0 && indent != childIndent) {
                 continue;
             }
@@ -2219,7 +2321,12 @@ class RoutesTab extends AbstractTab {
                 continue;
             }
             String key = trimmed.substring(0, colon).strip();
-            if ("parameters".equals(key) || "uri".equals(key) || 
"steps".equals(key)
+            if ("parameters".equals(key)) {
+                inParameters = true;
+                parametersLineIndent = indent;
+                continue;
+            }
+            if ("uri".equals(key) || "steps".equals(key)
                     || "id".equals(key) || "description".equals(key)) {
                 continue;
             }
@@ -2228,12 +2335,29 @@ class RoutesTab extends AbstractTab {
             if (doc != null) {
                 String docLine = formatOptionDoc(doc);
                 if (docLine != null) {
-                    result.put(i, List.of(docLine));
+                    int docIdx = lastContinuationLine(cd, i, indent);
+                    result.put(docIdx, List.of(docLine));
                 }
             }
         }
     }
 
+    static int lastContinuationLine(List<JsonObject> cd, int keyIdx, int 
keyIndent) {
+        int last = keyIdx;
+        for (int j = keyIdx + 1; j < cd.size(); j++) {
+            String c = cd.get(j).get("code") != null ? 
cd.get(j).get("code").toString() : "";
+            if (c.isBlank()) {
+                continue;
+            }
+            if (leadingSpaces(c) > keyIndent) {
+                last = j;
+            } else {
+                break;
+            }
+        }
+        return last;
+    }
+
     static String formatOptionDoc(BaseOptionModel doc) {
         StringBuilder sb = new StringBuilder();
         if (doc.getDescription() != null && !doc.getDescription().isEmpty()) {
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 c685be6ed7fe..83bf0e67da04 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
@@ -363,7 +363,6 @@ class SourceViewer {
 
         int visibleLines = inner.height();
         lastVisibleLines = visibleLines;
-        int maxScroll = Math.max(0, lines.size() - visibleLines);
 
         // On initial load, position selected line at 2/3 of viewport
         if (pendingScroll && selectedLine >= 0) {
@@ -372,14 +371,37 @@ class SourceViewer {
             pendingScroll = false;
         }
 
-        // Auto-scroll to keep selected line visible
+        // Auto-scroll to keep selected line visible (accounting for inline 
doc lines)
         if (selectedLine >= 0) {
             if (selectedLine < scrollY) {
                 scrollY = selectedLine;
+            } else if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+                while (scrollY < selectedLine && countVisualRows(scrollY, 
selectedLine) + 1 > visibleLines) {
+                    scrollY++;
+                }
             } else if (selectedLine >= scrollY + visibleLines) {
                 scrollY = selectedLine - visibleLines + 1;
             }
         }
+
+        int maxScroll;
+        if (quickDocEnabled && !quickDocEntries.isEmpty()) {
+            maxScroll = 0;
+            int visualFromEnd = 0;
+            for (int i = lines.size() - 1; i >= 0; i--) {
+                visualFromEnd++;
+                List<String> docs = quickDocEntries.get(i);
+                if (docs != null) {
+                    visualFromEnd += docs.size();
+                }
+                if (visualFromEnd >= visibleLines) {
+                    maxScroll = i;
+                    break;
+                }
+            }
+        } else {
+            maxScroll = Math.max(0, lines.size() - visibleLines);
+        }
         scrollY = Math.min(scrollY, maxScroll);
 
         int hSkip = wordWrap ? 0 : scrollX;
@@ -1089,6 +1111,20 @@ class SourceViewer {
         return result;
     }
 
+    private int countVisualRows(int fromLine, int toLine) {
+        int count = 0;
+        for (int i = fromLine; i < toLine && i < lines.size(); i++) {
+            count++;
+            if (quickDocEnabled) {
+                List<String> docs = quickDocEntries.get(i);
+                if (docs != null) {
+                    count += docs.size();
+                }
+            }
+        }
+        return count;
+    }
+
     private static String objToString(Object o) {
         return o != null ? o.toString() : "";
     }

Reply via email to