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

davsclaus pushed a commit to branch quick-fix/source-editor-ideas
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 61ea0cf8930255beac15014677993e5aa910fd2b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 12:51:56 2026 +0200

    chore: camel-jbang - source editor: problems marked when a file opens, 
placeholder values and bean jumps, usages of an endpoint
    
    - the problems of a Camel file are marked as soon as it opens (view and 
editor), F9 goes to the next
    - the quick doc starts with the values of the property placeholders of the 
line, and where its bean is declared
    - a line that refers to a bean jumps to its declaration (@BindToRegistry, 
@Named, @Component, @Bean, YAML/XML beans)
    - u lists the usages of the endpoint of the line: the routes consuming from 
it and the steps sending to it
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |   9 +
 .../jbang/core/commands/tui/GotoRoutePopup.java    |  16 +-
 .../dsl/jbang/core/commands/tui/ProjectBeans.java  | 238 +++++++++++++++++++++
 .../jbang/core/commands/tui/SourceEditAssist.java  |  43 ++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 139 +++++++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  84 +++++++-
 .../src/main/resources/tui/help/source.md          |  14 +-
 .../commands/tui/SourceEditorProjectInfoTest.java  | 190 ++++++++++++++++
 8 files changed, 715 insertions(+), 18 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 4552e5cefb1e..71babec14554 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -213,6 +213,13 @@ Camel routes:
 A quick documentation panel at the bottom shows the documentation of the line 
the cursor is on: for YAML, Java
 and XML routes the component and options of an endpoint, the EIP of a step and 
the language of its expression.
 Java routes are read by the Java DSL parser without compiling them, so 
constants and the endpoint DSL are resolved.
+What the project knows about the line comes first: the values of its property 
placeholders (`{{orders.period}} = 500
+(application.properties)`, or the default that is used when it is not set), 
and where the bean it refers to is declared.
+
+A line that refers to a bean (`bean:name`, `.bean(MyBean.class)`, `ref: name`, 
`#class:com.foo.MyBean`...) shows a
+`↵ name` jump link when the project declares the bean (`@BindToRegistry`, 
`@Named`, `@Component`, `@Bean`, or the beans
+of a YAML or XML file); *Enter* goes to its declaration, as it goes to the 
route a `to` sends to. Press *u* on a line
+with an endpoint to see its usages: the routes that consume from it and the 
steps that send to it, across the project.
 
 The editor shows *gutter change markers* -- a green background on line numbers 
that have been
 modified or added since the file was opened. This gives an at-a-glance view of 
what you've changed.
@@ -237,6 +244,8 @@ file is not saved and errors are displayed in a popup. This 
catches mistakes ear
 Java and XML routes get the same Camel checks, on the Camel model the routes 
are read into: the endpoint URIs
 (also those built with the endpoint DSL, or from a constant), Simple 
expressions, and a `to` whose URI holds
 `+${...}+` and should be a `toD`. Their problems are marked on their lines 
while you type (*F9* jumps to the next).
+The problems of a file are marked as soon as it opens, in the view too: a red 
`✗` on the line, the count in the title,
+and the problem of the selected line in the panel at the bottom.
 An XML file with problems is not saved, as a YAML file; a Java file is saved 
and its problems are said, as it is
 the application's code, and a check that cannot know something (a value 
computed at runtime) must not stop you.
 
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 5f140b8eca5f..2dd9fce760a6 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
@@ -46,6 +46,7 @@ class GotoRoutePopup {
     private List<RouteItem> allEntries;
     private List<RouteItem> filteredEntries;
     private RouteItem selectedEntry;
+    private String name = "Go to Route";
 
     record RouteItem(String routeId, String fromUri, String filePath, int 
fromLine) {
     }
@@ -55,10 +56,17 @@ class GotoRoutePopup {
     }
 
     void open(List<SourceTab.RouteEntry> routeIndex) {
-        allEntries = new ArrayList<>();
+        List<RouteItem> items = new ArrayList<>();
         for (SourceTab.RouteEntry re : routeIndex) {
-            allEntries.add(new RouteItem(re.routeId(), re.fromUri(), 
re.filePath(), re.fromLine()));
+            items.add(new RouteItem(re.routeId(), re.fromUri(), re.filePath(), 
re.fromLine()));
         }
+        openItems(items, "Go to Route");
+    }
+
+    /** Opens the popup with the given places, such as the usages of an 
endpoint, under the given name. */
+    void openItems(List<RouteItem> items, String name) {
+        this.name = name;
+        allEntries = new ArrayList<>(items);
         visible = true;
         filter.clearFilter();
         rebuildList();
@@ -206,8 +214,8 @@ class GotoRoutePopup {
         int total = allEntries != null ? allEntries.size() : 0;
         int shown = filteredEntries.size();
         String title = shown == total
-                ? " Go to Route (" + total + ") "
-                : " Go to Route (" + shown + "/" + total + ") ";
+                ? " " + name + " (" + total + ") "
+                : " " + name + " (" + shown + "/" + total + ") ";
 
         ListWidget list = ListWidget.builder()
                 .items(items.toArray(ListItem[]::new))
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
new file mode 100644
index 000000000000..676d00790fff
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
@@ -0,0 +1,238 @@
+/*
+ * 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.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The beans of a project for the Source tab: where each bean name and Java 
class is declared, so a line of a route that
+ * refers to a bean (bean:name, .bean(MyBean.class), ref: name, 
#class:com.foo.MyBean...) can jump to it, and the quick
+ * doc can say where it is. Bean names come from {@code @BindToRegistry}, 
{@code @Named}, {@code @Component},
+ * {@code @Service} and {@code @Bean} in Java, and from the beans of YAML and 
XML files.
+ */
+final class ProjectBeans {
+
+    /**
+     * Where a bean or class is declared.
+     *
+     * @param label    the bean name, or the simple name of the class
+     * @param type     the class of the bean, when known
+     * @param filePath the file it is declared in
+     * @param line     the line of the declaration, from 0
+     */
+    record Location(String label, String type, String filePath, int line) {
+    }
+
+    static final ProjectBeans NONE = new ProjectBeans(Map.of(), Map.of());
+
+    private static final Pattern PACKAGE = 
Pattern.compile("^\\s*package\\s+([\\w.]+)\\s*;");
+    private static final Pattern CLASS = Pattern.compile(
+            
"^\\s*(?:(?:public|protected|private|final|abstract|static|sealed)\\s+)*(?:class|record|enum|interface)\\s+(\\w+)");
+    private static final Pattern NAMED = Pattern.compile(
+            
"@(?:[\\w.]+\\.)?(BindToRegistry|Named|Component|Service|Bean)\\b(?:\\s*\\(\\s*(?:(?:value|name)\\s*=\\s*)?\"([^\"]+)\")?");
+    private static final Pattern XML_BEAN = 
Pattern.compile("<bean\\s[^>]*?name=\"([^\"]+)\"(?:[^>]*?type=\"([^\"]+)\")?");
+    private static final Pattern YAML_NAME = 
Pattern.compile("^\\s*-?\\s*name:\\s*[\"']?([^\"'\\s#]+)");
+    private static final Pattern YAML_TYPE = 
Pattern.compile("^\\s*type:\\s*[\"']?(?:#class:|#type:)?([\\w.$]+)");
+
+    // what a line of a route refers to
+    private static final Pattern REF_CLASS = 
Pattern.compile("#(?:class|type):([\\w.$]+)");
+    private static final Pattern REF_BEAN_URI = 
Pattern.compile("(?<![\\w.$])bean:([A-Za-z_][\\w-]*)");
+    private static final Pattern REF_JAVA_CLASS = Pattern.compile(
+            
"\\.(?:bean|process)\\(\\s*(?:new\\s+)?(\\w+)\\s*(?:\\.class|\\()");
+    private static final Pattern REF_JAVA_NAME = 
Pattern.compile("\\.(?:bean|process)\\(\\s*\"([^\"]+)\"");
+    private static final Pattern REF_YAML
+            = 
Pattern.compile("^\\s*-?\\s*(ref|beanType|aggregationStrategy):\\s*[\"']?([\\w.$-]+)");
+    private static final Pattern REF_XML = 
Pattern.compile("\\s(ref|beanType)=\"([\\w.$-]+)\"");
+
+    private final Map<String, Location> byName;
+    private final Map<String, Location> byClass;
+
+    private ProjectBeans(Map<String, Location> byName, Map<String, Location> 
byClass) {
+        this.byName = byName;
+        this.byClass = byClass;
+    }
+
+    /** Reads the beans and classes the files declare. */
+    static ProjectBeans scan(List<Path> files) {
+        Map<String, Location> byName = new HashMap<>();
+        Map<String, Location> byClass = new HashMap<>();
+        for (Path file : files) {
+            String name = 
file.getFileName().toString().toLowerCase(Locale.ROOT);
+            List<String> lines;
+            try {
+                lines = Files.readAllLines(file, StandardCharsets.UTF_8);
+            } catch (IOException | RuntimeException e) {
+                continue;
+            }
+            if (name.endsWith(".java")) {
+                scanJava(file.toString(), lines, byName, byClass);
+            } else if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+                scanYaml(file.toString(), lines, byName);
+            } else if (name.endsWith(".xml")) {
+                scanXml(file.toString(), lines, byName);
+            }
+        }
+        return new ProjectBeans(byName, byClass);
+    }
+
+    private static void scanJava(
+            String filePath, List<String> lines, Map<String, Location> byName, 
Map<String, Location> byClass) {
+        String pkg = "";
+        String className = null;
+        int classLine = -1;
+        for (int i = 0; i < lines.size(); i++) {
+            Matcher m = PACKAGE.matcher(lines.get(i));
+            if (m.find()) {
+                pkg = m.group(1) + ".";
+            }
+            m = CLASS.matcher(lines.get(i));
+            if (m.find()) {
+                className = m.group(1);
+                classLine = i;
+                break;
+            }
+        }
+        if (className == null) {
+            return;
+        }
+        String fqcn = pkg + className;
+        Location cls = new Location(className, fqcn, filePath, classLine);
+        byClass.putIfAbsent(className, cls);
+        byClass.putIfAbsent(fqcn, cls);
+        for (int i = 0; i < lines.size(); i++) {
+            Matcher m = NAMED.matcher(lines.get(i));
+            while (m.find()) {
+                boolean onClass = i < classLine;
+                String bean = m.group(2);
+                if (bean == null && onClass && !"Bean".equals(m.group(1))) {
+                    // @BindToRegistry, @Named, @Component on the class: the 
class name, starting in lower case
+                    bean = Character.toLowerCase(className.charAt(0)) + 
className.substring(1);
+                }
+                if (bean != null) {
+                    byName.putIfAbsent(bean, new Location(bean, onClass ? fqcn 
: null, filePath, i));
+                }
+            }
+        }
+    }
+
+    private static void scanYaml(String filePath, List<String> lines, 
Map<String, Location> byName) {
+        int beansIndent = -1;
+        String pending = null;
+        int pendingLine = -1;
+        for (int i = 0; i < lines.size(); i++) {
+            String line = lines.get(i);
+            String trimmed = line.trim();
+            if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+                continue;
+            }
+            int indent = line.length() - line.stripLeading().length();
+            if (beansIndent >= 0 && indent <= beansIndent) {
+                beansIndent = -1;
+            }
+            if (trimmed.equals("- beans:") || trimmed.equals("beans:")) {
+                beansIndent = indent;
+                continue;
+            }
+            if (beansIndent < 0) {
+                continue;
+            }
+            Matcher m = YAML_NAME.matcher(line);
+            if (m.find()) {
+                pending = m.group(1);
+                pendingLine = i;
+                byName.putIfAbsent(pending, new Location(pending, null, 
filePath, i));
+                continue;
+            }
+            m = YAML_TYPE.matcher(line);
+            if (m.find() && pending != null) {
+                byName.put(pending, new Location(pending, m.group(1), 
filePath, pendingLine));
+                pending = null;
+            }
+        }
+    }
+
+    private static void scanXml(String filePath, List<String> lines, 
Map<String, Location> byName) {
+        for (int i = 0; i < lines.size(); i++) {
+            Matcher m = XML_BEAN.matcher(lines.get(i));
+            while (m.find()) {
+                String type = m.group(2) != null ? 
m.group(2).replace("#class:", "") : null;
+                byName.putIfAbsent(m.group(1), new Location(m.group(1), type, 
filePath, i));
+            }
+        }
+    }
+
+    /** The bean or class a line of a route refers to and the project 
declares, or null. */
+    Location refOn(String line) {
+        if (line == null || (byName.isEmpty() && byClass.isEmpty())) {
+            return null;
+        }
+        Matcher m = REF_CLASS.matcher(line);
+        if (m.find()) {
+            return byClass(m.group(1));
+        }
+        m = REF_JAVA_CLASS.matcher(line);
+        if (m.find()) {
+            return byClass(m.group(1));
+        }
+        m = REF_JAVA_NAME.matcher(line);
+        if (m.find()) {
+            return byName(m.group(1));
+        }
+        m = REF_BEAN_URI.matcher(line);
+        if (m.find()) {
+            return byName(m.group(1));
+        }
+        m = REF_YAML.matcher(line);
+        if (!m.find()) {
+            m = REF_XML.matcher(line);
+            if (!m.find()) {
+                return null;
+            }
+        }
+        return "beanType".equals(m.group(1)) ? byClass(m.group(2)) : 
byName(m.group(2));
+    }
+
+    private Location byName(String name) {
+        Location l = byName.get(name);
+        // a bean named after its class, as a bean without a name is
+        return l != null ? l : 
byClass.get(Character.toUpperCase(name.charAt(0)) + name.substring(1));
+    }
+
+    private Location byClass(String name) {
+        Location l = byClass.get(name);
+        if (l == null && name.contains(".")) {
+            l = byClass.get(name.substring(name.lastIndexOf('.') + 1));
+        }
+        return l;
+    }
+
+    /** The quick doc of a bean: what it is and where, and that Enter goes 
there. */
+    static String describe(Location l) {
+        String file = Path.of(l.filePath()).getFileName().toString();
+        String type = l.type() != null && !l.type().equals(l.label()) ? " (" + 
l.type() + ")" : "";
+        return l.label() + type + " is declared in " + file + ":" + (l.line() 
+ 1) + " — Enter goes there";
+    }
+}
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 5043cc166a5f..682d4cb50998 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
@@ -1471,6 +1471,49 @@ final class SourceEditAssist {
         return items;
     }
 
+    private static final Pattern PLACEHOLDER
+            = Pattern.compile("\\{\\{([^}:]+?)(?::([^}]*))?\\}\\}");
+
+    /**
+     * The values of the property placeholders on a line, from the .properties 
files of the project: {{key}} = value
+     * (application.properties), or that it is not set and the default it 
falls back to. {{env:NAME}} and {{sys:NAME}}
+     * come from the environment and the JVM, so they are said as such.
+     */
+    List<SourceViewer.DocEntry> placeholderDocs(String line) {
+        if (line == null || !line.contains("{{")) {
+            return List.of();
+        }
+        List<SourceViewer.DocEntry> answer = new ArrayList<>();
+        Matcher m = PLACEHOLDER.matcher(line);
+        while (m.find() && answer.size() < 3) {
+            String key = m.group(1).trim();
+            String def = m.group(2);
+            String text;
+            if (key.equals("env") || key.equals("sys")) {
+                // {{env:HOME}}: the key is the function, the name is what 
follows the colon
+                text = "{{" + key + ":" + def + "}} is read from the " + 
(key.equals("env") ? "environment" : "JVM")
+                       + " when the route starts";
+            } else {
+                AutocompletePopup.CompletionItem item = null;
+                for (AutocompletePopup.CompletionItem ph : 
loadPropertyPlaceholders()) {
+                    if (ph.key().equals("{{" + key + "}}")) {
+                        item = ph;
+                        break;
+                    }
+                }
+                if (item != null) {
+                    text = "{{" + key + "}} = " + item.description() + "  (" + 
item.group() + ")";
+                } else if (def != null) {
+                    text = "{{" + key + "}} is not set in the project's 
properties: the default " + def + " is used";
+                } else {
+                    text = "{{" + key + "}} is not set in the project's 
.properties files";
+                }
+            }
+            answer.add(new SourceViewer.DocEntry(text, false, "Placeholder"));
+        }
+        return answer;
+    }
+
     Map<Integer, List<SourceViewer.DocEntry>> 
providePropertiesQuickDocs(List<JsonObject> codeData) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || codeData.isEmpty()) {
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 4c6cde703bac..dc6b9702ec87 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
@@ -134,6 +134,9 @@ class SourceTab extends AbstractTab {
     }
 
     private List<RouteEntry> routeIndex = Collections.emptyList();
+    private ProjectBeans projectBeans = ProjectBeans.NONE;
+    private long projectBeansStamp;
+    private long projectBeansChecked;
     private List<ToEntry> toIndex = Collections.emptyList();
     final GotoRoutePopup gotoRoutePopup = new GotoRoutePopup();
     final GotoSourceNodePopup gotoSourceNodePopup = new GotoSourceNodePopup();
@@ -268,6 +271,11 @@ class SourceTab extends AbstractTab {
             return true;
         }
 
+        if (focusOnViewer && sourceViewer.isVisible() && 
!sourceViewer.isEditMode() && ke.isChar('u')) {
+            openUsages();
+            return true;
+        }
+
         if (!focusOnViewer) {
             return handleFileListKey(ke);
         }
@@ -437,6 +445,9 @@ class SourceTab extends AbstractTab {
                         TuiHelper.hint(spans, "g", "go to route");
                     }
                     TuiHelper.hint(spans, "Ctrl+G", "go to");
+                    if (endpointOnSelectedLine() != null) {
+                        TuiHelper.hint(spans, "u", "usages");
+                    }
                 }
             }
         } else {
@@ -837,14 +848,14 @@ class SourceTab extends AbstractTab {
                 
sourceViewer.setEndpointValidator(assist::validateYamlEndpoints);
                 sourceViewer.setSimpleValidator(assist::validateYamlSimple);
                 
sourceViewer.setListItemNodeChecker(assist::isListChildrenNode);
-                
sourceViewer.setEditQuickDocProvider(assist::provideEditQuickDoc);
+                
sourceViewer.setEditQuickDocProvider(withProjectDocs(assist::provideEditQuickDoc));
             } else {
                 // XML: the quick doc of the routes read into the model, Tab 
in the uri attributes (CAMEL-25208)
                 sourceViewer.setQuickDocProvider(cd -> 
assist.provideRouteQuickDocs(filePath, cd));
                 
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
                 
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
                 sourceViewer.setEditQuickDocProvider(
-                        (lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row));
+                        withProjectDocs((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)
@@ -853,14 +864,15 @@ class SourceTab extends AbstractTab {
             // Tab in the endpoint uri of from, to, toD...: component names, 
options and values, as YAML uris have
             
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
             
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
-            sourceViewer.setEditQuickDocProvider((lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row));
+            sourceViewer.setEditQuickDocProvider(
+                    withProjectDocs((lines, row) -> 
assist.provideRouteEditQuickDoc(filePath, lines, row)));
         } else if (SourceEditAssist.isPropertiesFile(filePath)) {
             
sourceViewer.setQuickDocProvider(assist::providePropertiesQuickDocs);
             
sourceViewer.setDeprecatedLineScanner(assist::scanDeprecatedProperties);
             
sourceViewer.setAutocompleteProvider(assist::providePropertyCompletions);
             
sourceViewer.setAutocompleteValueProvider(assist::providePropertyValueCompletions);
             sourceViewer.setPropertiesValidator(assist::validatePropertyLine);
-            
sourceViewer.setEditQuickDocProvider(assist::provideEditPropertyQuickDoc);
+            
sourceViewer.setEditQuickDocProvider(withProjectDocs(assist::provideEditPropertyQuickDoc));
         } else {
             sourceViewer.setQuickDocProvider(null);
             sourceViewer.setDeprecatedLineScanner(null);
@@ -1349,7 +1361,71 @@ class SourceTab extends AbstractTab {
         index.add(new RouteEntry(routeId, baseUri, filePath, fromLine));
     }
 
+    /** The jump links of a file: to the routes its steps send to or are 
called from, and to the beans it refers to. */
     Map<Integer, SourceViewer.JumpLink> computeJumpLinks(Path currentFile) {
+        Map<Integer, SourceViewer.JumpLink> result = new 
LinkedHashMap<>(computeRouteJumpLinks(currentFile));
+        ProjectBeans beans = projectBeans();
+        List<String> lines;
+        try {
+            lines = Files.readAllLines(currentFile, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return result;
+        }
+        for (int i = 0; i < lines.size(); i++) {
+            ProjectBeans.Location bean = beans.refOn(lines.get(i));
+            if (bean != null && 
!(bean.filePath().equals(currentFile.toString()) && bean.line() == i)) {
+                result.putIfAbsent(i, new SourceViewer.JumpLink(bean.label(), 
bean.filePath(), bean.line()));
+            }
+        }
+        return result;
+    }
+
+    /** The beans and classes the project declares, read again when a file of 
the project changed. */
+    ProjectBeans projectBeans() {
+        long now = System.currentTimeMillis();
+        if (now - projectBeansChecked < 2000) {
+            // the quick doc asks on every frame: look at the files at most 
every 2 seconds
+            return projectBeans;
+        }
+        projectBeansChecked = now;
+        List<Path> files = routeSources();
+        long stamp = files.size();
+        for (Path f : files) {
+            stamp = 31 * stamp + modified(f) + f.hashCode();
+        }
+        if (stamp != projectBeansStamp) {
+            projectBeans = ProjectBeans.scan(files);
+            projectBeansStamp = stamp;
+        }
+        return projectBeans;
+    }
+
+    /**
+     * The quick doc of a line with what the project knows about it put first: 
the values of its property placeholders
+     * and where the bean it refers to is declared.
+     */
+    private SourceViewer.EditQuickDocProvider 
withProjectDocs(SourceViewer.EditQuickDocProvider provider) {
+        return (lines, row) -> {
+            List<SourceViewer.DocEntry> base = provider.provideForLine(lines, 
row);
+            if (row < 0 || row >= lines.size()) {
+                return base;
+            }
+            List<SourceViewer.DocEntry> extra = new 
ArrayList<>(assist.placeholderDocs(lines.get(row)));
+            ProjectBeans.Location bean = projectBeans().refOn(lines.get(row));
+            if (bean != null) {
+                extra.add(new 
SourceViewer.DocEntry(ProjectBeans.describe(bean), false, "Bean"));
+            }
+            if (extra.isEmpty()) {
+                return base;
+            }
+            if (base != null) {
+                extra.addAll(base);
+            }
+            return extra;
+        };
+    }
+
+    private Map<Integer, SourceViewer.JumpLink> computeRouteJumpLinks(Path 
currentFile) {
         if (routeIndex.isEmpty()) {
             return Collections.emptyMap();
         }
@@ -1475,6 +1551,61 @@ class SourceTab extends AbstractTab {
         }
     }
 
+    /**
+     * The endpoint a route consumes from or sends to on the line selected in 
the viewer, as the route index has it
+     * (direct:billing, seda:orders); null when the line has none.
+     */
+    String endpointOnSelectedLine() {
+        String file = sourceViewer.getCurrentFilePath();
+        int line = sourceViewer.getSelectedLine();
+        if (file == null) {
+            return null;
+        }
+        for (RouteEntry re : routeIndex) {
+            if (file.equals(re.filePath()) && re.fromLine() == line) {
+                return re.fromUri();
+            }
+        }
+        for (ToEntry te : toIndex) {
+            if (file.equals(te.filePath()) && te.toLine() == line) {
+                return te.toUri();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Lists where the endpoint of the selected line is used: the routes that 
consume from it and those that send to it.
+     */
+    private void openUsages() {
+        buildRouteIndex();
+        String uri = endpointOnSelectedLine();
+        if (uri == null) {
+            if (ctx.notificationCallback != null) {
+                ctx.notificationCallback.accept("No endpoint of a route on 
this line", false);
+            }
+            return;
+        }
+        gotoRoutePopup.openItems(usagesOf(uri), "Usages of " + uri);
+    }
+
+    /** The routes that consume from the endpoint and the steps that send to 
it. */
+    List<GotoRoutePopup.RouteItem> usagesOf(String uri) {
+        List<GotoRoutePopup.RouteItem> items = new ArrayList<>();
+        for (RouteEntry re : routeIndex) {
+            if (uri.equals(re.fromUri())) {
+                items.add(new GotoRoutePopup.RouteItem(re.routeId(), "from " + 
uri, re.filePath(), re.fromLine()));
+            }
+        }
+        for (ToEntry te : toIndex) {
+            if (uri.equals(te.toUri())) {
+                String routeId = te.routeId().isEmpty() ? "route" : 
te.routeId();
+                items.add(new GotoRoutePopup.RouteItem(routeId, "to " + uri, 
te.filePath(), te.toLine()));
+            }
+        }
+        return items;
+    }
+
     private void openFileAt(String targetFilePath, int targetLine) {
         String currentFile = sourceViewer.getCurrentFilePath();
         if (currentFile != null && currentFile.equals(targetFilePath)) {
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 67dd394cbc8c..5237c8bbbf34 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
@@ -196,6 +196,10 @@ class SourceViewer {
     private List<String> validationErrors;
     private int validationErrorScroll;
     private Map<Integer, String> inlineErrors = Collections.emptyMap();
+    /**
+     * The problems of the file shown, found when it was loaded: marked in the 
view, and the editor starts with them.
+     */
+    private Map<Integer, String> viewErrors = Collections.emptyMap();
     private boolean editInitialScroll;
     private long lastBackgroundValidationTime;
     private String lastBackgroundValidationContent;
@@ -528,6 +532,10 @@ class SourceViewer {
             enterEditMode();
             return true;
         }
+        if (ke.isKey(KeyCode.F9) && !ke.hasShift() && !viewErrors.isEmpty()) {
+            goToNextProblem(viewErrors, selectedLine);
+            return true;
+        }
         if (isMarkdownFile && ke.isChar(' ')) {
             markdownMode = !markdownMode;
             return true;
@@ -922,6 +930,9 @@ class SourceViewer {
         originalEditText = editState.text();
         lineStatuses = null;
         diffOverlay = false;
+        // the problems found when the file was loaded are marked at once, 
before the first change
+        inlineErrors = viewErrors;
+        lastBackgroundValidationContent = originalEditText;
         editMode = true;
         editHistory.seedInitial(editState);
         refreshEditFindMatches();
@@ -1518,9 +1529,13 @@ class SourceViewer {
     }
 
     private void jumpToNextError() {
-        List<Integer> errorLines = new ArrayList<>(inlineErrors.keySet());
+        goToNextProblem(inlineErrors, editState.cursorRow());
+    }
+
+    /** Goes to the first problem after the row, or wraps around to the first 
one. */
+    private void goToNextProblem(Map<Integer, String> problems, int cursorRow) 
{
+        List<Integer> errorLines = new ArrayList<>(problems.keySet());
         Collections.sort(errorLines);
-        int cursorRow = editState.cursorRow();
         // find the first error line after the cursor
         for (int line : errorLines) {
             if (line > cursorRow) {
@@ -1549,6 +1564,12 @@ class SourceViewer {
         lastBackgroundValidationTime = now;
         lastBackgroundValidationContent = content;
 
+        List<String> msgs = validateContent(content);
+        inlineErrors = msgs.isEmpty() ? Collections.emptyMap() : 
buildInlineErrors(msgs, content);
+    }
+
+    /** The problems the checks of the file type (Camel YAML, properties, Java 
or XML routes) find in the content. */
+    private List<String> validateContent(String content) {
         List<String> msgs = new ArrayList<>();
         if (isCamelYamlFile()) {
             if (endpointValidator != null) {
@@ -1571,7 +1592,25 @@ class SourceViewer {
                 msgs.addAll(routeErrors);
             }
         }
-        inlineErrors = msgs.isEmpty() ? Collections.emptyMap() : 
buildInlineErrors(msgs, content);
+        return msgs;
+    }
+
+    /** The problems of the file just loaded, by line; none when the checks 
fail or do not apply. */
+    private Map<Integer, String> problemsOnLoad(String content) {
+        if (isMarkdownFile) {
+            return Collections.emptyMap();
+        }
+        try {
+            List<String> msgs = validateContent(content);
+            return msgs.isEmpty() ? Collections.emptyMap() : 
buildInlineErrors(msgs, content);
+        } catch (RuntimeException e) {
+            return Collections.emptyMap();
+        }
+    }
+
+    /** Package-private for tests: the problems marked in the view, by line 
from 0. */
+    Map<Integer, String> viewErrors() {
+        return viewErrors;
     }
 
     /**
@@ -1894,7 +1933,17 @@ class SourceViewer {
             if (viewDocEntries != null && !viewDocEntries.isEmpty()) {
                 titleText = viewDocEntries.get(0).title();
             }
-            if (titleText != null) {
+            String problem = viewErrors.get(selectedLine);
+            if (problem != null) {
+                // the problem of the line goes before its documentation, as 
the Error panel of the editor shows it
+                int remaining = Math.max(0, viewDocArea.width() - " Error 
".length() - 3);
+                docLines.add(Line.from(
+                        Span.styled("───", Theme.error()),
+                        Span.styled(" Error ", Theme.error().bold()),
+                        Span.styled("─".repeat(remaining), Theme.error())));
+                docLines.add(Line.from(Span.styled(problem, Theme.error())));
+                docLines.add(Line.from(Span.styled("F4 edit   F9 next 
problem", Style.EMPTY.dim())));
+            } else if (titleText != null) {
                 String prefix = "─── ";
                 String suffix = " ";
                 int remaining = Math.max(0, viewDocArea.width() - 
prefix.length() - titleText.length() - suffix.length());
@@ -1905,7 +1954,7 @@ class SourceViewer {
             } else {
                 docLines.add(Line.from(Span.styled("─".repeat(Math.max(1, 
viewDocArea.width())), Style.EMPTY.dim())));
             }
-            if (viewDocEntries != null && !viewDocEntries.isEmpty()) {
+            if (problem == null && viewDocEntries != null && 
!viewDocEntries.isEmpty()) {
                 for (int d = 0; d < viewDocEntries.size() && d < 
viewDocArea.height() - 1; d++) {
                     DocEntry entry = viewDocEntries.get(d);
                     Style docStyle = entry.deprecated() ? 
Style.EMPTY.dim().italic() : Style.EMPTY.dim();
@@ -2330,6 +2379,9 @@ class SourceViewer {
         if (isEditable()) {
             TuiHelper.hint(spans, "F4", "edit");
         }
+        if (!viewErrors.isEmpty()) {
+            TuiHelper.hint(spans, "F9", "next problem");
+        }
         if (isMarkdownFile || currentRouteId != null) {
             TuiHelper.hint(spans, "Space", "format");
         }
@@ -2535,6 +2587,7 @@ class SourceViewer {
             }
             editableFile = Files.isWritable(filePath) ? filePath : null;
             scanDeprecatedLines();
+            viewErrors = problemsOnLoad(String.join("\n", rawLines));
             jumpLinks = Collections.emptyMap();
             if (onFileLoaded != null) {
                 onFileLoaded.accept(filePath);
@@ -2548,6 +2601,7 @@ class SourceViewer {
             markdownMode = false;
             rawMarkdownContent = null;
             editableFile = null;
+            viewErrors = Collections.emptyMap();
         }
     }
 
@@ -2561,6 +2615,7 @@ class SourceViewer {
     void loadSource(MonitorContext ctx, String routeId, int targetLine, String 
sourceLocationHint) {
         // Process-sourced views are never editable (may be remote / not a 
local file)
         editableFile = null;
+        viewErrors = Collections.emptyMap();
         editMode = false;
         editState.clear();
 
@@ -2775,6 +2830,11 @@ class SourceViewer {
             return Title.from(Line.from(spans));
         }
         if (currentRouteId == null) {
+            if (!viewErrors.isEmpty()) {
+                return Title.from(Line.from(
+                        Span.styled(" Source [" + info + "] ", ts),
+                        Span.styled(" errors: " + viewErrors.size() + " ", 
Theme.error())));
+            }
             return Title.from(Span.styled(" Source [" + info + "] ", ts));
         }
 
@@ -2973,15 +3033,21 @@ class SourceViewer {
         } else if (isSelected) {
             spans.add(Span.styled(">> ", focused ? Theme.label().bold() : 
Theme.label().dim()));
             if (!prefix.isEmpty()) {
-                spans.add(Span.styled(prefix, (focused ? Theme.label().bold() 
: Theme.label().dim()).patch(selBg)));
+                Style numberStyle = viewErrors.containsKey(lineIndex)
+                        ? Theme.error().bold() : focused ? 
Theme.label().bold() : Theme.label().dim();
+                spans.add(Span.styled(prefix, numberStyle.patch(selBg)));
             }
             for (Span s : highlighted.spans()) {
                 spans.add(Span.styled(s.content(), s.style().patch(selBg)));
             }
         } else {
-            spans.add(isDeprecated
-                    ? Span.styled(" ⚠ ", Theme.warning())
-                    : Span.raw("   "));
+            if (viewErrors.containsKey(lineIndex)) {
+                spans.add(Span.styled(" ✗ ", Theme.error().bold()));
+            } else {
+                spans.add(isDeprecated
+                        ? Span.styled(" ⚠ ", Theme.warning())
+                        : Span.raw("   "));
+            }
             if (!prefix.isEmpty()) {
                 spans.add(Span.styled(prefix, Style.EMPTY.dim()));
             }
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 4f71eee20eb8..cd710dd6efcd 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
@@ -21,7 +21,14 @@ explorer on the left and a source viewer on the right.
 - **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)
+  the EIP of a step, the language of an expression). Before it: the values of
+  the `{{placeholders}}` of the line from the project's .properties files, and
+  where the bean the line refers to is declared
+- The problems of a Camel file are marked as soon as it opens: a red ✗ on
+  their lines and the count in the title; the panel at the bottom says the
+  problem of the selected line, and **F9** goes to the next one
+- **u** — usages: the routes that consume from the endpoint of the line and the
+  steps that send to it (direct:, seda:...), across the project; **Enter** 
goes there
 - **/** — search in source
 - **h** — highlight text
 - **n/N** — next/previous match
@@ -92,6 +99,11 @@ project and files of different DSLs (build output and 
`src/test` are left out);
 Java routes are read without compiling them. The case and otherwise of a
 switch link like a `to`. Jump indicators are hidden in plain mode.
 
+A line that refers to a bean (`bean:name`, `.bean(MyBean.class)`, `ref: name`,
+`#class:com.foo.MyBean`...) shows a **↵ name** indicator when the project
+declares it (`@BindToRegistry`, `@Named`, `@Component`, `@Bean`, or the beans 
of
+a YAML or XML file); **Enter** goes to its declaration.
+
 ## Go to Route
 - **g** — open a filterable popup listing all routes of the project's source 
files.
   Type to fuzzy-filter by route ID or endpoint URI, then press **Enter** to
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
new file mode 100644
index 000000000000..8dd034cf3d80
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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 dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * What the source editor tells about a line from the rest of the project: 
problems marked as soon as a file opens, the
+ * values of property placeholders, where a bean is declared (and a jump to 
it), and where an endpoint is used.
+ */
+class SourceEditorProjectInfoTest {
+
+    @TempDir
+    Path dir;
+
+    private static SourceTab newTab() {
+        return new SourceTab(new MonitorContext(new 
AtomicReference<>(List.of()), new AtomicReference<>(List.of())));
+    }
+
+    @Test
+    void theProblemsOfAFileAreMarkedWhenItOpens() throws Exception {
+        Path file = Files.writeString(dir.resolve("MyRoute.java"), 
"a\nb\nc\nd\n");
+        SourceViewer viewer = new SourceViewer();
+        viewer.setRouteValidator(content -> List.of("Line 2: timer: Unknown 
option 'peroid'", "Line 4: oops"));
+        viewer.loadFile(file);
+
+        assertThat(viewer.viewErrors()).containsOnlyKeys(1, 3);
+        assertThat(viewer.viewErrors().get(1)).contains("peroid");
+
+        // F9 goes to the next problem in the view, and around to the first
+        viewer.goToLine(0);
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F9));
+        assertThat(viewer.getSelectedLine()).isEqualTo(1);
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F9));
+        assertThat(viewer.getSelectedLine()).isEqualTo(3);
+
+        // the editor starts with them, before the first change
+        viewer.enterEditMode();
+        assertThat(viewer.inlineErrors()).containsOnlyKeys(1, 3);
+    }
+
+    @Test
+    void aFileWithoutProblemsHasNoMarkers() throws Exception {
+        Path file = Files.writeString(dir.resolve("MyRoute.java"), "a\n");
+        SourceViewer viewer = new SourceViewer();
+        viewer.setRouteValidator(content -> List.of());
+        viewer.loadFile(file);
+        assertThat(viewer.viewErrors()).isEmpty();
+    }
+
+    @Test
+    void thePlaceholdersOfALineAreExplained() throws Exception {
+        Files.writeString(dir.resolve("application.properties"), 
"orders.period = 500\n");
+        SourceEditAssist assist = new SourceEditAssist(
+                new MonitorContext(new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())));
+        assist.setRootDir(dir);
+
+        List<SourceViewer.DocEntry> docs = assist.placeholderDocs(
+                
"from(\"timer:orders?period={{orders.period}}&delay={{orders.delay:100}}&x={{env:HOME}}\")");
+        
assertThat(docs).extracting(SourceViewer.DocEntry::text).containsExactly(
+                "{{orders.period}} = 500  (application.properties)",
+                "{{orders.delay}} is not set in the project's properties: the 
default 100 is used",
+                "{{env:HOME}} is read from the environment when the route 
starts");
+        assertThat(docs.get(0).title()).isEqualTo("Placeholder");
+        assertThat(assist.placeholderDocs("from(\"timer:orders\")")).isEmpty();
+    }
+
+    @Test
+    void theBeansARouteRefersToAreFound() throws Exception {
+        Files.writeString(dir.resolve("OrderService.java"), """
+                package com.acme;
+
+                import org.apache.camel.BindToRegistry;
+
+                @BindToRegistry("orders")
+                public class OrderService {
+                    public String ship(String body) {
+                        return body;
+                    }
+                }
+                """);
+        Files.writeString(dir.resolve("beans.camel.yaml"), """
+                - beans:
+                    - name: audit
+                      type: "#class:com.acme.Audit"
+                """);
+        ProjectBeans beans = 
ProjectBeans.scan(List.of(dir.resolve("OrderService.java"), 
dir.resolve("beans.camel.yaml")));
+
+        assertThat(beans.refOn("    
.to(\"bean:orders?method=ship\")").line()).isEqualTo(4);
+        assertThat(beans.refOn("    .bean(OrderService.class, 
\"ship\")").type()).isEqualTo("com.acme.OrderService");
+        assertThat(beans.refOn("    
.bean(\"orders\")").label()).isEqualTo("orders");
+        assertThat(beans.refOn("            ref: 
audit").filePath()).endsWith("beans.camel.yaml");
+        assertThat(beans.refOn("            ref: 
audit").type()).isEqualTo("com.acme.Audit");
+        assertThat(beans.refOn("  <bean 
ref=\"orders\"/>").label()).isEqualTo("orders");
+        assertThat(beans.refOn("    .to(\"log:info\")")).isNull();
+        assertThat(beans.refOn("    .to(\"bean:unknown\")")).isNull();
+        assertThat(ProjectBeans.describe(beans.refOn("    
.to(\"bean:orders\")")))
+                .isEqualTo("orders (com.acme.OrderService) is declared in 
OrderService.java:5 — Enter goes there");
+    }
+
+    @Test
+    void aRouteLineThatRefersToABeanJumpsToIt() throws Exception {
+        Files.writeString(dir.resolve("OrderService.java"), """
+                package com.acme;
+
+                @org.apache.camel.BindToRegistry("orders")
+                public class OrderService {
+                }
+                """);
+        Path route = Files.writeString(dir.resolve("route.camel.yaml"), """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: bean:orders
+                """);
+        SourceTab tab = newTab();
+        assertThat(tab.loadDirectory(dir)).isTrue();
+
+        Map<Integer, SourceViewer.JumpLink> links = 
tab.computeJumpLinks(route);
+        assertThat(links.get(5).routeId()).isEqualTo("orders");
+        assertThat(links.get(5).filePath()).endsWith("OrderService.java");
+        assertThat(links.get(5).targetLine()).isEqualTo(2);
+    }
+
+    @Test
+    void theUsagesOfAnEndpointAreItsConsumersAndProducers() throws Exception {
+        Files.writeString(dir.resolve("orders.camel.yaml"), """
+                - route:
+                    id: orders
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: direct:billing
+                - route:
+                    id: refunds
+                    from:
+                      uri: timer:refund
+                      steps:
+                        - to:
+                            uri: direct: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();
+        tab.computeJumpLinks(dir.resolve("billing.camel.yaml"));
+
+        assertThat(tab.usagesOf("direct:billing"))
+                .extracting(i -> i.routeId() + " " + i.fromUri() + " @" + 
(i.fromLine() + 1))
+                .containsExactlyInAnyOrder(
+                        "billing from direct:billing @3",
+                        "orders to direct:billing @7",
+                        "refunds to direct:billing @14");
+    }
+}

Reply via email to