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() : ""; }
