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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 34a422b0bc1c CAMEL-25192, CAMEL-25193: camel-jbang - TUI jump links 
for Java DSL routes and Switch EIP destinations in the overview (#27133)
34a422b0bc1c is described below

commit 34a422b0bc1ccbfc975d102739c2b30d6f08053d
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 18:41:58 2026 +0200

    CAMEL-25192, CAMEL-25193: camel-jbang - TUI jump links for Java DSL routes 
and Switch EIP destinations in the overview (#27133)
    
    * CAMEL-25192: camel-jbang - TUI Source tab route jump links for Java DSL 
routes
    
    - LwJavaParser gives every step the line of the call that added it, not only
      the route (to(...) returns the route, so the new step had no line)
    - ProjectRoutes.parseJava reads a Java source with the endpoint DSL and the
      project's constants resolved, as the overview does
    - the Source tab indexes the routes and to/toD/wireTap/enrich endpoints of
      Java route builders with JavaRouteScanner, so Java routes get the jump
      links and go-to-route entries YAML routes have (also across files)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25192: camel-jbang - TUI names a route without an id by its full 
from endpoint
    
    A jump link to a route without an id read as src/main/data; it is now
    file:src/main/data, and the go-to-route popup does not repeat the endpoint.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25192: camel-jbang - TUI go to route and go to line popups take 
digits instead of switching tabs
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25192: camel-jbang - TUI Source viewer footer shows g go to route 
and Ctrl+G go to
    
    After a jump the focus is on the viewer, whose footer did not show them.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25192: camel-jbang - TUI Source tab jump links for the destinations 
of the Switch EIP
    
    - the Java DSL parser gives switch cases and the fallback the line of their 
call
    - Java and YAML switch cases and otherwise link to the route of their uri
    - a YAML case may start with - uri:
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25193: camel-jbang - project overview reads the destinations of the 
Switch EIP
    
    The uri of each switch case and of otherwise is an endpoint the route sends
    to, in the Java, YAML and XML readers, so the Architecture and Topology 
views
    connect routes through a switch.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../org/apache/camel/java/in/ChainReplayer.java    |  61 +++++++++
 .../org/apache/camel/java/in/LwJavaParserTest.java |  55 ++++++++
 .../jbang/core/commands/ai/JavaRouteReader.java    |  29 +++-
 .../dsl/jbang/core/commands/ai/ProjectRoutes.java  |  33 +++++
 .../core/commands/ai/ProjectOverviewTest.java      |  60 +++++++++
 .../jbang/core/commands/tui/ArchitectureView.java  |   2 +-
 .../jbang/core/commands/tui/GotoRoutePopup.java    |   5 +-
 .../jbang/core/commands/tui/JavaRouteScanner.java  | 140 +++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 149 +++++++++++++++++----
 .../core/commands/tui/JavaRouteScannerTest.java    | 130 ++++++++++++++++++
 .../core/commands/tui/SourceTabJumpLinksTest.java  | 134 ++++++++++++++++++
 .../core/commands/tui/SourceViewerEditTest.java    |  19 +++
 12 files changed, 782 insertions(+), 35 deletions(-)

diff --git 
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java 
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
index 6276d223d68b..576088a50ca3 100644
--- 
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
+++ 
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java
@@ -62,9 +62,15 @@ import org.apache.camel.java.in.JavaChainParser.Num;
 import org.apache.camel.java.in.JavaChainParser.Opaque;
 import org.apache.camel.java.in.JavaChainParser.Ref;
 import org.apache.camel.java.in.JavaChainParser.Str;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.ProcessorDefinition;
 import org.apache.camel.model.RouteConfigurationsDefinition;
+import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.RouteTemplatesDefinition;
 import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.SwitchCaseDefinition;
+import org.apache.camel.model.SwitchDefinition;
+import org.apache.camel.model.WhenDefinition;
 import org.apache.camel.model.language.XPathExpression;
 import org.apache.camel.model.rest.RestsDefinition;
 import org.apache.camel.support.builder.Namespaces;
@@ -901,6 +907,18 @@ final class ChainReplayer {
             if (result instanceof LineNumberAware la && la.getLineNumber() < 
0) {
                 la.setLineNumber(call.line());
             }
+            if (result instanceof RouteDefinition route && route.getInput() != 
null
+                    && route.getInput().getLineNumber() < 0) {
+                route.getInput().setLineNumber(call.line());
+            }
+            if (target instanceof ProcessorDefinition<?> step) {
+                // to(...) returns the route, not the step it adds: the step 
is the one without a line
+                lineOfNewSteps(step, call.line());
+            }
+            if (result instanceof SwitchDefinition sw) {
+                // doCase(value, uri), doCase(value).to(uri) and 
otherwise(uri) add to the switch, not its outputs
+                lineOfNewCases(sw, call.line());
+            }
             return result;
         } catch (InvocationTargetException e) {
             Throwable cause = e.getCause() != null ? e.getCause() : e;
@@ -915,6 +933,49 @@ final class ChainReplayer {
         }
     }
 
+    /**
+     * Gives the line of a call to the steps it added. A DSL call appends to 
the step it is called on, or to the block
+     * open in it (the when of a choice, the body of a split), and an open 
block is always the last output; so the steps
+     * without a line on the path of last outputs are the new ones.
+     */
+    private static void lineOfNewSteps(ProcessorDefinition<?> step, int line) {
+        ProcessorDefinition<?> current = step;
+        for (int depth = 0; depth < JavaChainParser.MAX_DEPTH && current != 
null; depth++) {
+            if (current instanceof ChoiceDefinition choice) {
+                // the outputs of a choice are those of its open branch; the 
branches themselves are apart
+                for (WhenDefinition when : choice.getWhenClauses()) {
+                    if (when.getLineNumber() < 0) {
+                        when.setLineNumber(line);
+                    }
+                }
+                if (choice.getOtherwise() != null && 
choice.getOtherwise().getLineNumber() < 0) {
+                    choice.getOtherwise().setLineNumber(line);
+                }
+            }
+            List<ProcessorDefinition<?>> outputs = current.getOutputs();
+            if (outputs == null || outputs.isEmpty()) {
+                return;
+            }
+            ProcessorDefinition<?> last = outputs.get(outputs.size() - 1);
+            if (last.getLineNumber() < 0) {
+                last.setLineNumber(line);
+            }
+            current = last;
+        }
+    }
+
+    /** Gives the line of a call to the switch cases and fallback it added. */
+    private static void lineOfNewCases(SwitchDefinition sw, int line) {
+        for (SwitchCaseDefinition c : sw.getCases()) {
+            if (c.getLineNumber() < 0) {
+                c.setLineNumber(line);
+            }
+        }
+        if (sw.getOtherwiseDefinition() != null && 
sw.getOtherwiseDefinition().getLineNumber() < 0) {
+            sw.getOtherwiseDefinition().setLineNumber(line);
+        }
+    }
+
     /** How well a method's parameter types match the values: exactly the 
value's type counts most, Object least. */
     private static int specificity(Method m, Object[] values) {
         Class<?>[] types = m.getParameterTypes();
diff --git 
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
 
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
index 79169e1784ee..0a399f581fff 100644
--- 
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
+++ 
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
@@ -21,9 +21,12 @@ import java.util.List;
 import org.apache.camel.model.ChoiceDefinition;
 import org.apache.camel.model.OnExceptionDefinition;
 import org.apache.camel.model.ProcessDefinition;
+import org.apache.camel.model.ProcessorDefinition;
 import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.SetHeaderDefinition;
 import org.apache.camel.model.SplitDefinition;
+import org.apache.camel.model.SwitchCaseDefinition;
+import org.apache.camel.model.SwitchDefinition;
 import org.apache.camel.model.ToDefinition;
 import org.apache.camel.model.TryDefinition;
 import org.junit.jupiter.api.Test;
@@ -143,6 +146,58 @@ class LwJavaParserTest {
         assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue();
     }
 
+    @Test
+    void everyStepHasTheLineOfItsCall() {
+        // CAMEL-25192: the TUI Source tab links a to(...) line to the route 
it sends to
+        JavaParseResult result = new LwJavaParser().parse("""
+                public class R extends RouteBuilder {
+                    public void configure() {
+                        from("direct:a")
+                            .setHeader("x").constant("y")
+                            .choice()
+                                .when(header("x").isEqualTo("y"))
+                                    .to("direct:b")
+                                .otherwise()
+                                    .toD("direct:${header.x}")
+                            .end()
+                            .wireTap("direct:tap");
+                        from("direct:b").to("log:b");
+                    }
+                }
+                """);
+        List<RouteDefinition> routes = result.routes().getRoutes();
+        RouteDefinition a = routes.get(0);
+        assertThat(a.getLineNumber()).isEqualTo(3);
+        assertThat(a.getInput().getLineNumber()).isEqualTo(3);
+        
assertThat(a.getOutputs()).extracting(ProcessorDefinition::getLineNumber).containsExactly(4,
 5, 11);
+        ChoiceDefinition choice = (ChoiceDefinition) a.getOutputs().get(1);
+        
assertThat(choice.getWhenClauses().get(0).getLineNumber()).isEqualTo(6);
+        
assertThat(choice.getWhenClauses().get(0).getOutputs().get(0).getLineNumber()).isEqualTo(7);
+        assertThat(choice.getOtherwise().getLineNumber()).isEqualTo(8);
+        
assertThat(choice.getOtherwise().getOutputs().get(0).getLineNumber()).isEqualTo(9);
+        
assertThat(routes.get(1).getOutputs().get(0).getLineNumber()).isEqualTo(12);
+    }
+
+    @Test
+    void switchCasesHaveTheLineOfTheirCall() {
+        JavaParseResult result = new LwJavaParser().parse("""
+                from("direct:tickets")
+                    .doSwitch(header("department"))
+                        .doCase("billing", "direct:billing")
+                        .doCase("technical").id("tech").to("direct:technical")
+                        .otherwise("direct:review")
+                    .end();
+                """);
+        assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue();
+        SwitchDefinition sw = (SwitchDefinition) 
result.routes().getRoutes().get(0).getOutputs().get(0);
+        assertThat(sw.getLineNumber()).isEqualTo(2);
+        assertThat(sw.getCases()).extracting(SwitchCaseDefinition::getUri)
+                .containsExactly("direct:billing", "direct:technical");
+        
assertThat(sw.getCases()).extracting(SwitchCaseDefinition::getLineNumber).containsExactly(3,
 4);
+        
assertThat(sw.getOtherwiseDefinition().getUri()).isEqualTo("direct:review");
+        assertThat(sw.getOtherwiseDefinition().getLineNumber()).isEqualTo(5);
+    }
+
     @Test
     void globalErrorHandlingAndRest() {
         JavaParseResult result = new LwJavaParser().parse("""
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
index 84b57ea2524f..fcd4b63f3333 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java
@@ -46,6 +46,8 @@ import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.RouteTemplateDefinition;
 import org.apache.camel.model.RoutingSlipDefinition;
 import org.apache.camel.model.SendDefinition;
+import org.apache.camel.model.SwitchCaseDefinition;
+import org.apache.camel.model.SwitchDefinition;
 import org.apache.camel.model.ToDynamicDefinition;
 import org.apache.camel.model.TryDefinition;
 import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition;
@@ -75,13 +77,7 @@ final class JavaRouteReader {
 
     /** The routes of the source, or an empty list when the parser finds none. 
*/
     static List<Route> read(String file, String content, CamelCatalog catalog, 
ConstantResolver constants) {
-        LwJavaParser parser = new LwJavaParser();
-        if (catalog != null) {
-            parser.setEndpointDslResolver(new 
CatalogEndpointDslResolver(catalog));
-        }
-        parser.setConstantResolver(constants != null
-                ? constants : new ProjectConstantResolver(Map.of(), catalog));
-        JavaParseResult result = parser.parse(content);
+        JavaParseResult result = parse(content, catalog, constants);
         JavaRouteReader reader = new JavaRouteReader(file, catalog);
         List<Integer> starts = new ArrayList<>();
         result.routes().getRoutes().forEach(r -> 
starts.add(r.getLineNumber()));
@@ -100,6 +96,17 @@ final class JavaRouteReader {
         return reader.routes;
     }
 
+    /** The source read by the Java DSL parser, with the endpoint DSL and 
constants resolved through the catalog. */
+    static JavaParseResult parse(String content, CamelCatalog catalog, 
ConstantResolver constants) {
+        LwJavaParser parser = new LwJavaParser();
+        if (catalog != null) {
+            parser.setEndpointDslResolver(new 
CatalogEndpointDslResolver(catalog));
+        }
+        parser.setConstantResolver(constants != null
+                ? constants : new ProjectConstantResolver(Map.of(), catalog));
+        return parser.parse(content);
+    }
+
     /** Whether something the parser did not work out lies between this 
route's line and the next route's. */
     private static boolean partial(JavaParseResult result, int line, 
List<Integer> starts) {
         int end = Integer.MAX_VALUE;
@@ -160,6 +167,14 @@ final class JavaRouteReader {
             add(consumes, ProjectRoutes.endpoint(poll.getUri(), null, true, 
catalog));
         } else if (p instanceof KameletDefinition k && k.getName() != null) {
             add(produces, mark(ProjectRoutes.endpoint("kamelet:" + 
k.getName(), null, false, catalog), error));
+        } else if (p instanceof SwitchDefinition sw) {
+            // the fixed destinations of a switch: its cases and fallback, 
which are not outputs
+            for (SwitchCaseDefinition c : sw.getCases()) {
+                add(produces, mark(ProjectRoutes.endpoint(c.getUri(), null, 
false, catalog), error));
+            }
+            if (sw.getOtherwiseDefinition() != null) {
+                add(produces, 
mark(ProjectRoutes.endpoint(sw.getOtherwiseDefinition().getUri(), null, false, 
catalog), error));
+            }
         } else if (p instanceof RecipientListDefinition || p instanceof 
RoutingSlipDefinition
                 || p instanceof DynamicRouterDefinition) {
             String eip = p.getShortName();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
index b10c7226feca..160576526282 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -39,6 +40,7 @@ import org.xml.sax.InputSource;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.java.in.ConstantResolver;
+import org.apache.camel.java.in.JavaParseResult;
 import org.apache.camel.util.URISupport;
 import org.yaml.snakeyaml.DumperOptions;
 import org.yaml.snakeyaml.LoaderOptions;
@@ -181,6 +183,16 @@ public final class ProjectRoutes {
         return List.of();
     }
 
+    /**
+     * A Java DSL source read into the Camel model by the Java DSL parser, 
without compiling or running it: the endpoint
+     * DSL and the constants of the project's Java sources (by path, read when 
first needed) are resolved as in the
+     * project overview. For tools that need the steps and their lines, such 
as the TUI Source tab.
+     */
+    public static JavaParseResult parseJava(
+            String content, Map<String, Supplier<String>> javaSources, 
CamelCatalog catalog) {
+        return JavaRouteReader.parse(content, catalog, new 
ProjectConstantResolver(javaSources, catalog));
+    }
+
     // ---- endpoints ----
 
     /**
@@ -575,6 +587,8 @@ public final class ProjectRoutes {
                         }
                     } else if (DYNAMIC_KEYS.contains(key)) {
                         sent.add(dynamicEndpoint(key));
+                    } else if ("switch".equals(key) && value instanceof 
MappingNode sw) {
+                        switchEndpoints(sw, sent);
                     }
                     if (value instanceof MappingNode || value instanceof 
SequenceNode) {
                         walk(value, produces, consumes, steps, depth + 1,
@@ -587,6 +601,20 @@ public final class ProjectRoutes {
             }
         }
 
+        /** The fixed destinations of a switch: the uri of each case and of 
the fallback. */
+        private void switchEndpoints(MappingNode sw, List<Endpoint> sent) {
+            if (child(sw, "case") instanceof SequenceNode cases) {
+                for (org.yaml.snakeyaml.nodes.Node c : cases.getValue()) {
+                    if (c instanceof MappingNode cm && value(cm, "uri") 
instanceof String uri) {
+                        add(sent, endpoint(uri, null, false, catalog));
+                    }
+                }
+            }
+            if (child(sw, "otherwise") instanceof MappingNode o && value(o, 
"uri") instanceof String uri) {
+                add(sent, endpoint(uri, null, false, catalog));
+            }
+        }
+
         private Endpoint stepEndpoint(org.yaml.snakeyaml.nodes.Node value, 
boolean dynamic) {
             if (value instanceof ScalarNode s) {
                 return endpoint(s.getValue(), null, dynamic, catalog);
@@ -746,6 +774,11 @@ public final class ProjectRoutes {
                     add(produces, endpoint("kamelet:" + attr(s, "name"), null, 
false, catalog));
                 } else if (DYNAMIC_KEYS.contains(name)) {
                     produces.add(dynamicEndpoint(name));
+                } else if (("case".equals(name) || "otherwise".equals(name)) 
&& attr(s, "uri") != null
+                        && s.getParentNode() instanceof Element parent && 
"switch".equals(localName(parent))) {
+                    // the fixed destinations of a switch
+                    Endpoint ep = endpoint(attr(s, "uri"), null, false, 
catalog);
+                    add(produces, handlesFailure(s, e) ? onErrorOf(ep) : ep);
                 }
             }
             String kind = template != null ? "routeTemplate" : "route";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
index 2052cfec0410..ae452c58fa4a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectOverviewTest.java
@@ -156,6 +156,66 @@ class ProjectOverviewTest {
         assertThat(o.route("intake").line()).isEqualTo(6);
     }
 
+    @Test
+    void switchDestinationsInEveryDsl() {
+        // CAMEL-25193: the cases and the fallback of a switch are what the 
route sends to
+        Map<String, String> sources = Map.of(
+                "tickets.camel.yaml", """
+                        - route:
+                            id: yaml-tickets
+                            from:
+                              uri: direct:yaml
+                              steps:
+                                - switch:
+                                    selector:
+                                      header:
+                                        expression: department
+                                    case:
+                                      - value: billing
+                                        uri: direct:billing
+                                    otherwise:
+                                      uri: direct:review
+                        """,
+                "tickets.xml", """
+                        <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                          <route id="xml-tickets">
+                            <from uri="direct:xml"/>
+                            <switch>
+                              <selector><header>department</header></selector>
+                              <case value="billing" uri="direct:billing"/>
+                              <otherwise uri="direct:review"/>
+                            </switch>
+                          </route>
+                        </routes>
+                        """,
+                "Tickets.java", """
+                        public class Tickets extends RouteBuilder {
+                            public void configure() {
+                                from("direct:java").routeId("java-tickets")
+                                    .doSwitch(header("department"))
+                                        .doCase("billing", "direct:billing")
+                                        .otherwise("direct:review")
+                                    .end();
+                            }
+                        }
+                        """,
+                "handlers.camel.yaml", """
+                        - route:
+                            id: billing
+                            from:
+                              uri: direct:billing
+                              steps:
+                                - to:
+                                    uri: log:billing
+                        """);
+        Overview o = ProjectOverview.analyze(Path.of("tickets"), sources, 
CATALOG);
+        for (String id : List.of("yaml-tickets", "xml-tickets", 
"java-tickets")) {
+            
assertThat(o.route(id).produces()).as(id).extracting(ProjectRoutes.Endpoint::uri)
+                    .containsExactly("direct:billing", "direct:review");
+            assertThat(o.links()).as(id).contains(new Link(id, "billing", 
"direct:billing", "call"));
+        }
+    }
+
     @Test
     void linksRoutesOverSharedEndpoints() {
         List<Link> links = overview().links();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index d5e0021e5f0f..33d7d5645917 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -246,7 +246,7 @@ final class ArchitectureView {
         }
     }
 
-    private static CamelCatalog catalog() {
+    static CamelCatalog catalog() {
         CamelCatalog c = catalog;
         if (c == null) {
             c = new DefaultCamelCatalog();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoRoutePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoRoutePopup.java
index a2124cd8b64c..5f140b8eca5f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoRoutePopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoRoutePopup.java
@@ -157,7 +157,7 @@ class GotoRoutePopup {
         int maxUriW = 0;
         for (RouteItem entry : filteredEntries) {
             maxIdW = Math.max(maxIdW, entry.routeId().length());
-            maxUriW = Math.max(maxUriW, entry.fromUri().length());
+            maxUriW = Math.max(maxUriW, 
entry.fromUri().equals(entry.routeId()) ? 0 : entry.fromUri().length());
         }
 
         Style normalStyle = Style.EMPTY;
@@ -183,7 +183,8 @@ class GotoRoutePopup {
                 spans.add(Span.styled(padded, normalStyle));
             }
 
-            String uri = entry.fromUri();
+            // a route without an id is named by its endpoint: no need to show 
it twice
+            String uri = entry.fromUri().equals(entry.routeId()) ? "" : 
entry.fromUri();
             String paddedUri = uri + " ".repeat(Math.max(0, maxUriW - 
uri.length()));
             spans.add(Span.styled("  " + paddedUri, dimStyle));
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
new file mode 100644
index 000000000000..c4569180ca64
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScanner.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes;
+import org.apache.camel.model.CatchDefinition;
+import org.apache.camel.model.EnrichDefinition;
+import org.apache.camel.model.ExpressionNode;
+import org.apache.camel.model.PollEnrichDefinition;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.SendDefinition;
+import org.apache.camel.model.SwitchCaseDefinition;
+import org.apache.camel.model.SwitchDefinition;
+import org.apache.camel.model.ToDynamicDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.model.language.ConstantExpression;
+
+/**
+ * The routes of a Java DSL source for the jump links of the Source tab: each 
route's from endpoint and the endpoints it
+ * sends to, with their lines. The source is read into the model by the Java 
DSL parser of camel-java-io, without
+ * compiling or running it, so the endpoint DSL and constants are resolved as 
in the project overview.
+ */
+final class JavaRouteScanner {
+
+    private static final int MAX_DEPTH = 50;
+
+    /** A route: its id (null when it has none), its from endpoint and the 
line of from, from 0. */
+    record Route(String id, String fromUri, int line, List<To> tos) {
+    }
+
+    /** An endpoint a route sends to, and the line of the step, from 0. */
+    record To(String uri, int line) {
+    }
+
+    private JavaRouteScanner() {
+    }
+
+    /** Whether the file is a Java source with a route builder. */
+    static boolean isJavaRoutes(String fileName, String content) {
+        return fileName.endsWith(".java") && content != null && 
content.contains("RouteBuilder");
+    }
+
+    /**
+     * The routes of a Java source.
+     *
+     * @param javaSources the Java sources of the project by path, for the 
constants of other classes
+     */
+    static List<Route> scan(String content, Map<String, Supplier<String>> 
javaSources, CamelCatalog catalog) {
+        List<Route> answer = new ArrayList<>();
+        for (RouteDefinition r : ProjectRoutes.parseJava(content, javaSources, 
catalog).routes().getRoutes()) {
+            if (r.getInput() == null || r.getInput().getUri() == null) {
+                continue;
+            }
+            int line = r.getInput().getLineNumber() > 0 ? 
r.getInput().getLineNumber() : r.getLineNumber();
+            List<To> tos = new ArrayList<>();
+            Set<Object> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
+            for (ProcessorDefinition<?> p : r.getOutputs()) {
+                walk(p, tos, seen, 0);
+            }
+            answer.add(new Route(r.getRouteId(), uri(r.getInput().getUri()), 
Math.max(0, line - 1), tos));
+        }
+        return answer;
+    }
+
+    private static void walk(ProcessorDefinition<?> p, List<To> tos, 
Set<Object> seen, int depth) {
+        if (p == null || depth > MAX_DEPTH || !seen.add(p)) {
+            return;
+        }
+        String uri = null;
+        if (p instanceof ToDynamicDefinition d) {
+            // toD and wireTap
+            uri = d.getUri();
+        } else if (p instanceof SendDefinition<?> s) {
+            uri = s.getUri();
+        } else if ((p instanceof EnrichDefinition || p instanceof 
PollEnrichDefinition)
+                && ((ExpressionNode) p).getExpression() instanceof 
ConstantExpression c) {
+            uri = c.getExpression();
+        }
+        if (uri != null && p.getLineNumber() > 0) {
+            tos.add(new To(uri(uri), p.getLineNumber() - 1));
+        }
+        if (p instanceof SwitchDefinition sw) {
+            // the destinations of a switch are its cases and fallback, not 
outputs
+            for (SwitchCaseDefinition c : sw.getCases()) {
+                add(tos, c.getUri(), c.getLineNumber());
+            }
+            if (sw.getOtherwiseDefinition() != null) {
+                add(tos, sw.getOtherwiseDefinition().getUri(), 
sw.getOtherwiseDefinition().getLineNumber());
+            }
+        }
+        for (ProcessorDefinition<?> child : p.getOutputs()) {
+            walk(child, tos, seen, depth + 1);
+        }
+        if (p instanceof TryDefinition t) {
+            for (CatchDefinition c : t.getCatchClauses()) {
+                walk(c, tos, seen, depth + 1);
+            }
+            walk(t.getFinallyClause(), tos, seen, depth + 1);
+        }
+    }
+
+    private static void add(List<To> tos, String uri, int line) {
+        if (uri != null && line > 0) {
+            tos.add(new To(uri(uri), line - 1));
+        }
+    }
+
+    /** direct://orders from the endpoint DSL as direct:orders, as a route 
written as text has it. */
+    static String uri(String uri) {
+        int colon = uri.indexOf(':');
+        if (colon > 0 && uri.startsWith("//", colon + 1)) {
+            return uri.substring(0, colon + 1) + uri.substring(colon + 3);
+        }
+        return uri;
+    }
+}
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 171435b528ce..c097940799e6 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
@@ -31,6 +31,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -105,7 +106,7 @@ class SourceTab extends AbstractTab {
         }
         configureEditAssist(file);
         sourceViewer.loadFile(file);
-        if (isCamelSourceFile(file)) {
+        if (hasJumpLinks(file)) {
             sourceViewer.setJumpLinks(computeJumpLinks(file));
         }
         if (!sourceViewer.isEditable()) {
@@ -120,8 +121,9 @@ class SourceTab extends AbstractTab {
         return sourceViewer.isEditMode();
     }
 
+    /** Steps whose uri: lines are destinations; for switch those of its cases 
and fallback. */
     private static final Set<String> LINKABLE_KEYWORDS = Set.of(
-            "to", "toD", "wireTap", "enrich", "pollEnrich", 
"deadLetterChannel");
+            "to", "toD", "wireTap", "enrich", "pollEnrich", 
"deadLetterChannel", "switch");
 
     record RouteEntry(String routeId, String fromUri, String filePath, int 
fromLine) {
     }
@@ -131,8 +133,8 @@ class SourceTab extends AbstractTab {
 
     private List<RouteEntry> routeIndex = Collections.emptyList();
     private List<ToEntry> toIndex = Collections.emptyList();
-    private final GotoRoutePopup gotoRoutePopup = new GotoRoutePopup();
-    private final GotoSourceNodePopup gotoSourceNodePopup = new 
GotoSourceNodePopup();
+    final GotoRoutePopup gotoRoutePopup = new GotoRoutePopup();
+    final GotoSourceNodePopup gotoSourceNodePopup = new GotoSourceNodePopup();
     private final FileActionsPopup fileActionsPopup = new FileActionsPopup();
 
     SourceTab(MonitorContext ctx) {
@@ -144,7 +146,7 @@ class SourceTab extends AbstractTab {
         });
         sourceViewer.setOnFileCreated(this::refreshFiles);
         sourceViewer.setOnFileLoaded(p -> {
-            if (isCamelSourceFile(p)) {
+            if (hasJumpLinks(p)) {
                 sourceViewer.setJumpLinks(computeJumpLinks(p));
             }
         });
@@ -157,9 +159,9 @@ class SourceTab extends AbstractTab {
     }
 
     boolean isSourceViewerTextInputActive() {
-        // also treat the file-actions menu as active input so global 
single-key shortcuts (q, ?, ...)
-        // do not fire while the menu, its name prompt, or delete confirmation 
is open
-        return sourceViewer.isTextInputActive() || 
fileActionsPopup.isVisible();
+        // also treat the file-actions menu and the go-to popups as active 
input so global single-key shortcuts
+        // (q, ?, digits switching tabs, ...) do not fire while the menu, a 
prompt or a filter is open
+        return sourceViewer.isTextInputActive() || 
fileActionsPopup.isVisible() || isGotoPopupVisible();
     }
 
     void handlePaste(String text) {
@@ -315,7 +317,13 @@ class SourceTab extends AbstractTab {
 
     @Override
     public boolean isOverlayActive() {
-        return fileActionsPopup.isVisible() || (focusOnViewer && 
sourceViewer.isTextInputActive());
+        return fileActionsPopup.isVisible() || isGotoPopupVisible()
+                || (focusOnViewer && sourceViewer.isTextInputActive());
+    }
+
+    /** Whether the go to route or go to line popup is open: it takes typed 
text, digits too. */
+    private boolean isGotoPopupVisible() {
+        return gotoRoutePopup.isVisible() || gotoSourceNodePopup.isVisible();
     }
 
     @Override
@@ -421,6 +429,13 @@ class SourceTab extends AbstractTab {
             sourceViewer.renderFooter(spans);
             if (!sourceViewer.isEditMode()) {
                 TuiHelper.hint(spans, "Tab", "files");
+                // also after a jump, which moves the focus to the viewer; not 
while g is typed into search
+                if (!sourceViewer.isTextInputActive()) {
+                    if (!routeIndex.isEmpty()) {
+                        TuiHelper.hint(spans, "g", "go to route");
+                    }
+                    TuiHelper.hint(spans, "Ctrl+G", "go to");
+                }
             }
         } else {
             TuiHelper.hint(spans, "Enter", "open");
@@ -527,7 +542,7 @@ class SourceTab extends AbstractTab {
         }
     }
 
-    private boolean loadDirectory(Path dir) {
+    boolean loadDirectory(Path dir) {
         return loadDirectory(dir, null);
     }
 
@@ -591,7 +606,7 @@ class SourceTab extends AbstractTab {
         String viewedPath = sourceViewer.getCurrentFilePath();
         if (viewedPath != null) {
             Path viewedFile = Path.of(viewedPath);
-            if (isCamelSourceFile(viewedFile)) {
+            if (hasJumpLinks(viewedFile)) {
                 sourceViewer.setJumpLinks(computeJumpLinks(viewedFile));
             }
         }
@@ -787,7 +802,7 @@ class SourceTab extends AbstractTab {
                 Path filePath = Path.of(entry.path());
                 configureEditAssist(filePath);
                 sourceViewer.loadFile(filePath);
-                if (isCamelSourceFile(filePath)) {
+                if (hasJumpLinks(filePath)) {
                     sourceViewer.setJumpLinks(computeJumpLinks(filePath));
                 }
                 focusOnViewer = true;
@@ -846,6 +861,23 @@ class SourceTab extends AbstractTab {
         return false;
     }
 
+    /** Whether the file gets route jump links: Camel YAML and XML, and Java 
sources with a route builder. */
+    private boolean hasJumpLinks(Path path) {
+        return isCamelSourceFile(path) || isJavaRouteFile(path);
+    }
+
+    private static boolean isJavaRouteFile(Path path) {
+        String name = path.getFileName().toString();
+        if (!name.endsWith(".java")) {
+            return false;
+        }
+        try {
+            return JavaRouteScanner.isJavaRoutes(name, Files.readString(path, 
StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
     private void renderFileList(Frame frame, Rect area) {
         Style fileBorderStyle = ctx.paneBorder(!focusOnViewer);
         if (entries.isEmpty()) {
@@ -995,11 +1027,23 @@ class SourceTab extends AbstractTab {
     private void buildRouteIndex() {
         List<RouteEntry> fromEntries = new ArrayList<>();
         List<ToEntry> toEntries = new ArrayList<>();
+        // the Java sources of the folder, for the constants a route takes 
from another class
+        Map<String, Supplier<String>> javaSources = new LinkedHashMap<>();
+        for (FilesBrowser.FileEntry entry : entries) {
+            if (!entry.directory() && entry.name().endsWith(".java")) {
+                Path path = Path.of(entry.path());
+                javaSources.put(entry.path(), () -> readQuietly(path));
+            }
+        }
         for (FilesBrowser.FileEntry entry : entries) {
             if (entry.directory()) {
                 continue;
             }
             Path path = Path.of(entry.path());
+            if (isJavaRouteFile(path)) {
+                scanJavaRoutes(path, javaSources, fromEntries, toEntries);
+                continue;
+            }
             if (!isCamelSourceFile(path)) {
                 continue;
             }
@@ -1011,6 +1055,35 @@ class SourceTab extends AbstractTab {
         toIndex = toEntries;
     }
 
+    /** The routes of a Java source, read by the Java DSL parser without 
compiling or running it. */
+    private void scanJavaRoutes(
+            Path file, Map<String, Supplier<String>> javaSources, 
List<RouteEntry> fromEntries, List<ToEntry> toEntries) {
+        String content = readQuietly(file);
+        if (content == null) {
+            return;
+        }
+        String filePath = file.toString();
+        for (JavaRouteScanner.Route route : JavaRouteScanner.scan(content, 
javaSources, ArchitectureView.catalog())) {
+            int before = fromEntries.size();
+            emitRouteEntry(fromEntries, route.id(), route.fromUri(), filePath, 
route.line());
+            String routeId = fromEntries.size() > before ? 
fromEntries.get(before).routeId() : "";
+            for (JavaRouteScanner.To to : route.tos()) {
+                String toUri = stripQueryParams(to.uri());
+                if (toUri != null && !toUri.isEmpty()) {
+                    toEntries.add(new ToEntry(routeId, toUri, filePath, 
to.line()));
+                }
+            }
+        }
+    }
+
+    private static String readQuietly(Path file) {
+        try {
+            return Files.readString(file, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
     private List<YamlRouteNodeScanner.NodeEntry> buildSourceNodeIndex() {
         List<YamlRouteNodeScanner.NodeEntry> nodes = new ArrayList<>();
         for (FilesBrowser.FileEntry entry : entries) {
@@ -1123,8 +1196,8 @@ class SourceTab extends AbstractTab {
             }
 
             // uri: under a linkable block → index it as a to entry
-            if (inLinkableBlock && trimmed.startsWith("uri:")) {
-                String val = extractYamlValue(trimmed, "uri");
+            if (inLinkableBlock && isUriLine(trimmed)) {
+                String val = extractYamlValue(uriLine(trimmed), "uri");
                 if (val != null && !val.isEmpty()) {
                     String toUri = stripQueryParams(val);
                     if (toUri != null && !toUri.isEmpty()) {
@@ -1142,17 +1215,16 @@ class SourceTab extends AbstractTab {
             return;
         }
         if (routeId == null || routeId.isEmpty()) {
-            // derive route id from the from URI
+            // a route without an id is named by its from endpoint, with the 
component so file:src/main/data
+            // does not read as a folder name
             int colon = baseUri.indexOf(':');
-            routeId = colon >= 0 ? baseUri.substring(colon + 1) : baseUri;
-            if (routeId.startsWith("//")) {
-                routeId = routeId.substring(2);
-            }
+            routeId = colon > 0 && baseUri.startsWith("//", colon + 1)
+                    ? baseUri.substring(0, colon + 1) + 
baseUri.substring(colon + 3) : baseUri;
         }
         index.add(new RouteEntry(routeId, baseUri, filePath, fromLine));
     }
 
-    private Map<Integer, SourceViewer.JumpLink> computeJumpLinks(Path 
currentFile) {
+    Map<Integer, SourceViewer.JumpLink> computeJumpLinks(Path currentFile) {
         if (routeIndex.isEmpty()) {
             return Collections.emptyMap();
         }
@@ -1172,6 +1244,21 @@ class SourceTab extends AbstractTab {
             fromUriToRoute.put(re.fromUri(), re);
         }
 
+        if (currentFilePath.endsWith(".java")) {
+            // the steps of a Java route are in the index with their lines, as 
the parser read them
+            for (ToEntry te : toIndex) {
+                if (currentFilePath.equals(te.filePath())) {
+                    RouteEntry target = fromUriToRoute.get(te.toUri());
+                    if (target != null && 
!target.routeId().equals(te.routeId())) {
+                        result.putIfAbsent(te.toLine(),
+                                new SourceViewer.JumpLink(target.routeId(), 
target.filePath(), target.fromLine()));
+                    }
+                }
+            }
+            addReverseLinks(currentFilePath, result);
+            return result;
+        }
+
         // forward links: to/toD/wireTap → target route's from
         boolean inLinkableBlock = false;
         int linkableBlockIndent = -1;
@@ -1222,8 +1309,8 @@ class SourceTab extends AbstractTab {
                 }
             }
 
-            if (uri == null && inLinkableBlock && trimmed.startsWith("uri:")) {
-                String val = extractYamlValue(trimmed, "uri");
+            if (uri == null && inLinkableBlock && isUriLine(trimmed)) {
+                String val = extractYamlValue(uriLine(trimmed), "uri");
                 if (val != null && !val.isEmpty()) {
                     uri = val;
                 }
@@ -1238,7 +1325,12 @@ class SourceTab extends AbstractTab {
             }
         }
 
-        // reverse links: from URI ← routes that send to it (jump to the 
caller's to: line)
+        addReverseLinks(currentFilePath, result);
+        return result;
+    }
+
+    /** Reverse links: a from line to a route that sends to it (jumps to the 
caller's to line). */
+    private void addReverseLinks(String currentFilePath, Map<Integer, 
SourceViewer.JumpLink> result) {
         for (RouteEntry re : routeIndex) {
             if (!currentFilePath.equals(re.filePath())) {
                 continue;
@@ -1256,8 +1348,6 @@ class SourceTab extends AbstractTab {
                 }
             }
         }
-
-        return result;
     }
 
     private void openFileAt(String targetFilePath, int targetLine) {
@@ -1291,6 +1381,15 @@ class SourceTab extends AbstractTab {
         openFileAt(link.filePath(), link.targetLine());
     }
 
+    /** A uri: line, also as the first key of a list item: - uri: 
direct:billing in the cases of a switch. */
+    private static boolean isUriLine(String trimmed) {
+        return trimmed.startsWith("uri:") || trimmed.startsWith("- uri:");
+    }
+
+    private static String uriLine(String trimmed) {
+        return trimmed.startsWith("- ") ? trimmed.substring(2).trim() : 
trimmed;
+    }
+
     static String extractYamlValue(String trimmed, String key) {
         String prefix = key + ":";
         if (!trimmed.startsWith(prefix)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
new file mode 100644
index 000000000000..5fb57ba8389c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaRouteScannerTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class JavaRouteScannerTest {
+
+    private static final String ORDERS = """
+            package com.acme;
+
+            import org.apache.camel.builder.RouteBuilder;
+
+            public class Orders extends RouteBuilder {
+                @Override
+                public void configure() {
+                    from("platform-http:/orders").routeId("intake")
+                        .choice()
+                            .when(header("type").isEqualTo("vip"))
+                                .to("direct:vip")
+                            .otherwise()
+                                .to(Endpoints.STANDARD)
+                        .end()
+                        .wireTap("direct:audit?block=false");
+
+                    from("direct:vip")
+                        .doTry()
+                            .toD("kafka:vip-${header.region}")
+                        .doCatch(Exception.class)
+                            .to("direct:audit")
+                        .end();
+                }
+            }
+            """;
+
+    private static final String ENDPOINTS = """
+            package com.acme;
+
+            public final class Endpoints {
+                public static final String STANDARD = "direct:standard";
+            }
+            """;
+
+    @Test
+    void routesAndTheirStepsWithLines() {
+        Map<String, Supplier<String>> sources = Map.of("/p/Orders.java", () -> 
ORDERS, "/p/Endpoints.java", () -> ENDPOINTS);
+        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(ORDERS, 
sources, new DefaultCamelCatalog());
+
+        
assertThat(routes).extracting(JavaRouteScanner.Route::id).containsExactly("intake",
 null);
+        JavaRouteScanner.Route intake = routes.get(0);
+        assertThat(intake.fromUri()).isEqualTo("platform-http:/orders");
+        assertThat(intake.line()).isEqualTo(7);
+        // lines from 0, as the Source tab counts them; the constant of the 
other class is resolved
+        assertThat(intake.tos()).containsExactly(
+                new JavaRouteScanner.To("direct:vip", 10),
+                new JavaRouteScanner.To("direct:standard", 12),
+                new JavaRouteScanner.To("direct:audit?block=false", 14));
+
+        JavaRouteScanner.Route vip = routes.get(1);
+        assertThat(vip.fromUri()).isEqualTo("direct:vip");
+        assertThat(vip.line()).isEqualTo(16);
+        assertThat(vip.tos()).containsExactly(
+                new JavaRouteScanner.To("kafka:vip-${header.region}", 18),
+                new JavaRouteScanner.To("direct:audit", 20));
+    }
+
+    @Test
+    void theEndpointDsl() {
+        String source = """
+                public class R extends EndpointRouteBuilder {
+                    public void configure() {
+                        from(direct("in")).to(direct("out"));
+                    }
+                }
+                """;
+        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(source, 
Map.of(), new DefaultCamelCatalog());
+        // direct://in as a route written as text has it, so it links to 
from("direct:in") in another file
+        assertThat(routes.get(0).fromUri()).isEqualTo("direct:in");
+        assertThat(routes.get(0).tos()).containsExactly(new 
JavaRouteScanner.To("direct:out", 2));
+    }
+
+    @Test
+    void switchCasesAndFallback() {
+        String source = """
+                public class R extends RouteBuilder {
+                    public void configure() {
+                        from("direct:tickets")
+                            .doSwitch(header("department"))
+                                .doCase("billing", "direct:billing")
+                                .doCase("technical").to("direct:technical")
+                                .otherwise("direct:review")
+                            .end();
+                    }
+                }
+                """;
+        List<JavaRouteScanner.Route> routes = JavaRouteScanner.scan(source, 
Map.of(), new DefaultCamelCatalog());
+        assertThat(routes.get(0).tos()).containsExactly(
+                new JavaRouteScanner.To("direct:billing", 4),
+                new JavaRouteScanner.To("direct:technical", 5),
+                new JavaRouteScanner.To("direct:review", 6));
+    }
+
+    @Test
+    void onlyJavaSourcesWithARouteBuilder() {
+        assertThat(JavaRouteScanner.isJavaRoutes("Orders.java", 
ORDERS)).isTrue();
+        assertThat(JavaRouteScanner.isJavaRoutes("Endpoints.java", 
ENDPOINTS)).isFalse();
+        assertThat(JavaRouteScanner.isJavaRoutes("orders.yaml", 
ORDERS)).isFalse();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
new file mode 100644
index 000000000000..bae5283c492f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25192: the jump links of the Source tab between the routes of a 
folder, in YAML and Java, and to the
+ * destinations of a switch.
+ */
+class SourceTabJumpLinksTest {
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void yamlSwitchLinksToTheRoutesOfItsCasesAndFallback() throws Exception {
+        Path tickets = Files.writeString(dir.resolve("tickets.camel.yaml"), """
+                - route:
+                    id: tickets
+                    from:
+                      uri: direct:tickets
+                      steps:
+                        - switch:
+                            selector:
+                              header:
+                                expression: department
+                            case:
+                              - value: billing
+                                uri: direct:billing
+                              - uri: direct:technical
+                                value: technical
+                            otherwise:
+                              uri: direct:review
+                """);
+        Files.writeString(dir.resolve("handlers.camel.yaml"), """
+                - route:
+                    id: billing
+                    from:
+                      uri: direct:billing
+                      steps:
+                        - to:
+                            uri: log:billing
+                - route:
+                    id: technical
+                    from:
+                      uri: direct:technical
+                      steps:
+                        - to:
+                            uri: log:technical
+                - route:
+                    from:
+                      uri: direct:review
+                      steps:
+                        - to:
+                            uri: log:review
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+
+        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(tickets);
+        assertThat(links.get(11).routeId()).isEqualTo("billing");
+        assertThat(links.get(12).routeId()).isEqualTo("technical");
+        // a route without an id is named by its from endpoint
+        assertThat(links.get(15).routeId()).isEqualTo("direct:review");
+    }
+
+    @Test
+    void javaRoutesLinkToAndFromYamlRoutes() throws Exception {
+        Path orders = Files.writeString(dir.resolve("Orders.java"), """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class Orders extends RouteBuilder {
+                    public void configure() {
+                        from("file:inbox?noop=true")
+                            .to("direct:billing");
+
+                        from("direct:tickets")
+                            .doSwitch(header("department"))
+                                .doCase("billing", "direct:billing")
+                            .end();
+                    }
+                }
+                """);
+        Path billing = Files.writeString(dir.resolve("billing.camel.yaml"), """
+                - route:
+                    id: billing
+                    from:
+                      uri: direct:billing
+                      steps:
+                        - to:
+                            uri: log:billing
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+
+        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(orders);
+        assertThat(links.get(5).routeId()).isEqualTo("billing");
+        assertThat(links.get(5).filePath()).isEqualTo(billing.toString());
+        assertThat(links.get(9).routeId()).isEqualTo("billing");
+
+        // and back: the from: line of the YAML route links to a Java caller
+        SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+        assertThat(back.filePath()).isEqualTo(orders.toString());
+        assertThat(back.routeId()).isEqualTo("file:inbox");
+    }
+
+    private static SourceTab newTab() {
+        return new SourceTab(new MonitorContext(new 
AtomicReference<>(List.of()), new AtomicReference<>(List.of())));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
index 26b93862d92e..5a0659bc0e0d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditTest.java
@@ -339,6 +339,25 @@ class SourceViewerEditTest {
         assertThat(tab.isOverlayActive()).isFalse();
     }
 
+    @Test
+    void gotoPopupsTakeTypedDigits() {
+        // CAMEL-25192: a digit typed in go to route / go to line must not 
switch tabs
+        MonitorContext ctx = new MonitorContext(
+                new AtomicReference<>(List.of()),
+                new AtomicReference<>(List.of()));
+        SourceTab tab = new SourceTab(ctx);
+        assertThat(tab.isSourceViewerTextInputActive()).isFalse();
+
+        tab.gotoRoutePopup.open(List.of(new SourceTab.RouteEntry("a", 
"direct:a", "a.java", 0)));
+        assertThat(tab.isSourceViewerTextInputActive()).isTrue();
+        assertThat(tab.isOverlayActive()).isTrue();
+        tab.gotoRoutePopup.close();
+
+        tab.gotoSourceNodePopup.open(List.of(), 10);
+        assertThat(tab.isSourceViewerTextInputActive()).isTrue();
+        assertThat(tab.isOverlayActive()).isTrue();
+    }
+
     @Test
     void saveMessageClearedOnSubsequentLoad() throws Exception {
         viewer.loadFile(sourceFile);

Reply via email to