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");
