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 078162263bed CAMEL-25241: camel-jbang - Tab completes the Java DSL 
route chain in the source editor (#27238)
078162263bed is described below

commit 078162263bedc5c1248a7f98934c4397ea894a7a
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 00:03:46 2026 +0200

    CAMEL-25241: camel-jbang - Tab completes the Java DSL route chain in the 
source editor (#27238)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang/camel-tui-source-java-completion.png     | Bin 0 -> 337003 bytes
 .../ROOT/pages/camel-jbang-tui-ai-agents.adoc      |   3 +-
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  36 +-
 .../jbang/core/commands/tui/JavaChainContext.java  | 243 +++++++++++
 .../core/commands/tui/JavaDslCompletions.java      | 447 +++++++++++++++++++++
 .../jbang/core/commands/tui/SourceEditAssist.java  |   5 +
 .../dsl/jbang/core/commands/tui/SourceTab.java     |   2 +
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  98 ++++-
 .../src/main/resources/tui/help/source.md          |  11 +
 .../core/commands/tui/JavaDslCompletionTest.java   | 256 ++++++++++++
 10 files changed, 1096 insertions(+), 5 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-completion.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-completion.png
new file mode 100644
index 000000000000..afbffd544203
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-completion.png
 differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
index 0b5d04b27567..e007b72799e0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai-agents.adoc
@@ -75,7 +75,8 @@ gives it the TUI's own MCP server, and shows the agent's 
answer, tool calls and
 drives the TUI through the same `tui_*` tools that external MCP clients use, 
and can also read and edit your
 route sources with its own tools.
 
-Press *Ctrl+P* in the AI panel and pick one of the agents:
+Any agent that speaks ACP works. Press *Ctrl+P* in the AI panel and pick one 
of the agents known to work, or
+`acp:custom` to run any other ACP agent:
 
 [options="header"]
 |===
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 095f684d2694..414d4e75d112 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -13,8 +13,9 @@ routes.
   as a file opens and while you type, in YAML, Java and XML routes; a YAML or 
XML file with problems is not saved.
 * <<_quick_fixes,*Quick fixes*>> -- *Shift+F9* applies the fix a problem 
names, such as an option typo.
 * <<_fix_with_ai,*Fix with AI*>> -- *Shift+F8* asks the AI to fix the problem 
of a line; you confirm every change.
-* <<_completion,*Completion*>> -- *Tab* completes components, endpoint options 
and their values, XML elements and
-  attributes, and simple expressions.
+* <<_completion,*Completion*>> -- *Tab* completes components, endpoint options 
and their values, Java DSL chains, XML
+  elements and attributes, and simple expressions; light help for hand-written 
edits, see
+  <<_known_limitations,Known limitations>>.
 * <<_quick_documentation,*Quick documentation*>> -- the documentation of the 
line the cursor is on, at the bottom, with
   the values of its property placeholders and where its bean is declared.
 * <<_navigation,*Navigation*>> -- jump between routes and to beans, see where 
an endpoint is used with *u*, find any node
@@ -121,6 +122,12 @@ What can be completed:
   string given to them in Java, their `uri` attribute in XML): the component 
name before the `:`, the endpoint options
   after `?` or `&` (consumer or producer options, depending on where the 
endpoint is, and without the ones already
   given), and the value of an option after `=`.
+* *Java routes* -- after a dot in a route chain, the methods that compile 
there: first the options of the EIP the
+  chain is on (after `.split(body())` the `parallelProcessing`, `streaming`... 
of the split), then the EIPs, and the
+  `end()`, `endChoice()` or `endDoTry()` that closes the block you are in. The 
chain is followed from its `from` through
+  its blocks, so after `.end()` the options of the split are gone again, and 
after `.split().` the languages of the
+  expression come. The method goes in with its parentheses, the cursor inside 
them when it takes arguments
+  (`.split(|)`). The documentation comes from the catalog.
 * *XML routes* -- after `<`, or on an empty line, the elements that go inside 
the parent element (the EIPs of a route,
   `when` and `otherwise` in a `choice`, the languages where an expression 
goes, and once it has one, no other). The
   chosen element is inserted with its required attributes and its end tag 
(`<to uri=""/>`, `<split></split>`), the
@@ -143,6 +150,31 @@ image::jbang/camel-tui-source-xml-completion.png[In the 
start tag of log, Tab li
 
 image::jbang/camel-tui-source-simple-completion.png[After ${header. Tab lists 
the headers the route sets, then the ones of the Kafka component]
 
+image::jbang/camel-tui-source-java-completion.png[After a split in a Java 
route, Tab lists the options of the split first, then the EIPs]
+
+=== Known limitations
+
+*Tab* completion is light assistance for smaller, hand-written edits. It reads 
the line you are on and the text above
+the cursor, so it keeps working while the file is half typed, but it is no 
compiler and does not know the whole
+project:
+
+* *Java routes* -- the chain is followed from `from(...)`, `onException(...)`, 
`intercept...()` and `onCompletion()` in a
+  route builder. A route kept in a variable (`RouteDefinition route = 
from(...); route.`), the REST DSL (`rest()`),
+  the expression builders inside the arguments (`body().tokenize(",")`), 
lambdas, and your own builder methods get no
+  completion. The methods come from the Camel version of the TUI, their 
documentation from the catalog of your
+  project. Deprecated methods are not offered.
+* *XML routes* -- the routes of the XML DSL (`camel-xml-io`); the wrapper 
elements of Spring XML are not completed.
+  Values are completed for options with a fixed set of values (enums, 
`true`/`false`) and `+{{placeholders}}+`.
+* *Simple expressions* -- header, property and variable names are the ones the 
file sets or reads and the headers of
+  the components it uses, not the ones other routes or your own code set at 
runtime.
+* Nothing is completed from your own Java classes and beans, and there are no 
refactorings (rename, extract) or imports.
+
+For more than that, an AI coding agent is the more powerful companion: working 
together with you in the TUI, it knows
+Camel, reads and writes the whole project, runs the routes and checks them, 
and explains what it does as it goes. See
+xref:camel-jbang-tui-ai.adoc[the AI panel] to ask about and change your routes 
from the TUI, and
+xref:camel-jbang-tui-ai-agents.adoc#_using_a_coding_agent_acp[using a coding 
agent] to work in the TUI with any coding
+agent that speaks the Agent Client Protocol (ACP); Claude Code, Codex and 
opencode are some known to work.
+
 == Quick Documentation
 
 The panel at the bottom shows the documentation of the line the cursor is on, 
from the Camel catalog: the component
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaChainContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaChainContext.java
new file mode 100644
index 000000000000..73c4775962fc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaChainContext.java
@@ -0,0 +1,243 @@
+/*
+ * 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.List;
+
+/**
+ * The route chain the cursor is in, in a Java route, read from the text above 
it so it works while the file does not
+ * parse (CAMEL-25241): the calls of the statement up to the cursor, 
from(...).split(...).to(...)., and the name being
+ * typed after the last dot. Only the calls of the chain itself count; what is 
inside their arguments is skipped.
+ *
+ * @param calls  the calls of the chain before the cursor, the first one 
starting it (from, onException...)
+ * @param prefix the name typed after the last dot, which the completion 
replaces
+ */
+record JavaChainContext(List<Call> calls, String prefix) {
+
+    /**
+     * A call of the chain.
+     *
+     * @param name      the method name
+     * @param arguments the number of arguments given
+     */
+    record Call(String name, int arguments) {
+    }
+
+    /** The chain at the cursor, or null when the cursor is not after a dot of 
a route chain. */
+    static JavaChainContext at(List<String> lines, int row, int col) {
+        if (lines == null || row < 0 || row >= lines.size()) {
+            return null;
+        }
+        String line = lines.get(row);
+        if (col < 0 || col > line.length()) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int r = 0; r < row; r++) {
+            sb.append(lines.get(r)).append('\n');
+        }
+        sb.append(line, 0, col);
+        String text = code(sb.toString());
+        if (text == null) {
+            return null;
+        }
+        // the statement the cursor is in starts after the last ; { or } 
outside of any parentheses
+        int depth = 0;
+        int start = 0;
+        for (int i = 0; i < text.length(); i++) {
+            char c = text.charAt(i);
+            if (c == '(') {
+                depth++;
+            } else if (c == ')') {
+                depth = Math.max(0, depth - 1);
+            } else if (depth == 0 && (c == ';' || c == '{' || c == '}')) {
+                start = i + 1;
+            }
+        }
+        if (depth > 0) {
+            // in the arguments of a call
+            return null;
+        }
+        String statement = text.substring(start);
+        int end = statement.length();
+        int p = end;
+        while (p > 0 && Character.isJavaIdentifierPart(statement.charAt(p - 
1))) {
+            p--;
+        }
+        String prefix = statement.substring(p, end);
+        int dot = p - 1;
+        while (dot >= 0 && Character.isWhitespace(statement.charAt(dot))) {
+            dot--;
+        }
+        if (dot < 0 || statement.charAt(dot) != '.') {
+            return null;
+        }
+        List<Call> calls = calls(statement.substring(0, dot));
+        if (calls == null || calls.isEmpty()) {
+            return null;
+        }
+        return new JavaChainContext(calls, prefix);
+    }
+
+    /** The calls of a chain: name(args).name(args)..., null when the text is 
no such chain. */
+    private static List<Call> calls(String chain) {
+        List<Call> calls = new ArrayList<>();
+        int i = 0;
+        int n = chain.length();
+        boolean first = true;
+        while (i < n) {
+            while (i < n && Character.isWhitespace(chain.charAt(i))) {
+                i++;
+            }
+            if (i == n) {
+                break;
+            }
+            if (!first) {
+                if (chain.charAt(i) != '.') {
+                    return null;
+                }
+                i++;
+                while (i < n && Character.isWhitespace(chain.charAt(i))) {
+                    i++;
+                }
+            }
+            // a type argument before the name: .<String> foo()
+            if (i < n && chain.charAt(i) == '<') {
+                int close = chain.indexOf('>', i);
+                if (close < 0) {
+                    return null;
+                }
+                i = close + 1;
+                while (i < n && Character.isWhitespace(chain.charAt(i))) {
+                    i++;
+                }
+            }
+            int nameStart = i;
+            while (i < n && Character.isJavaIdentifierPart(chain.charAt(i))) {
+                i++;
+            }
+            String name = chain.substring(nameStart, i);
+            if (name.isEmpty()) {
+                return null;
+            }
+            while (i < n && Character.isWhitespace(chain.charAt(i))) {
+                i++;
+            }
+            if (i >= n || chain.charAt(i) != '(') {
+                // this.from(...) or a field: only the leading this is skipped
+                if (first && "this".equals(name) && i < n && chain.charAt(i) 
== '.') {
+                    i++;
+                    continue;
+                }
+                return null;
+            }
+            int depth = 0;
+            // the commas of a lambda or anonymous class body ({...}) and of 
type arguments (<String, String>) are
+            // no argument separators
+            int braces = 0;
+            int angles = 0;
+            int arguments = 0;
+            boolean any = false;
+            int j = i;
+            for (; j < n; j++) {
+                char c = chain.charAt(j);
+                if (c == '(') {
+                    depth++;
+                } else if (c == ')') {
+                    depth--;
+                    if (depth == 0) {
+                        break;
+                    }
+                } else if (c == '{') {
+                    braces++;
+                } else if (c == '}') {
+                    braces = Math.max(0, braces - 1);
+                } else if (c == '<' && j > 0 && 
Character.isJavaIdentifierPart(chain.charAt(j - 1))
+                        && j + 1 < n && 
(Character.isJavaIdentifierStart(chain.charAt(j + 1)) || chain.charAt(j + 1) == 
'?'
+                                || chain.charAt(j + 1) == '>')) {
+                    angles++;
+                } else if (c == '>' && angles > 0) {
+                    angles--;
+                } else if (depth == 1 && braces == 0 && angles == 0 && c == 
',') {
+                    arguments++;
+                }
+                if (depth >= 1 && c != '(' && !Character.isWhitespace(c)) {
+                    any = true;
+                }
+            }
+            if (j >= n) {
+                return null;
+            }
+            calls.add(new Call(name, any ? arguments + 1 : 0));
+            i = j + 1;
+            first = false;
+        }
+        return calls;
+    }
+
+    /**
+     * The text with its comments, strings and character literals blanked 
(kept as spaces, or "" for strings), so their
+     * brackets and dots do not count; null when the cursor is in one of them.
+     */
+    static String code(String text) {
+        StringBuilder sb = new StringBuilder(text.length());
+        int i = 0;
+        int n = text.length();
+        while (i < n) {
+            char c = text.charAt(i);
+            if (c == '/' && i + 1 < n && text.charAt(i + 1) == '/') {
+                int nl = text.indexOf('\n', i);
+                if (nl < 0) {
+                    return null;
+                }
+                i = nl;
+            } else if (c == '/' && i + 1 < n && text.charAt(i + 1) == '*') {
+                int close = text.indexOf("*/", i + 2);
+                if (close < 0) {
+                    return null;
+                }
+                sb.append(' ');
+                i = close + 2;
+            } else if (text.startsWith("\"\"\"", i)) {
+                int close = text.indexOf("\"\"\"", i + 3);
+                if (close < 0) {
+                    return null;
+                }
+                sb.append("\"\"");
+                i = close + 3;
+            } else if (c == '"' || c == '\'') {
+                int j = i + 1;
+                while (j < n && text.charAt(j) != c && text.charAt(j) != '\n') 
{
+                    if (text.charAt(j) == '\\') {
+                        j++;
+                    }
+                    j++;
+                }
+                if (j >= n || text.charAt(j) != c) {
+                    return null;
+                }
+                sb.append(c).append(c);
+                i = j + 1;
+            } else {
+                sb.append(c);
+                i++;
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletions.java
new file mode 100644
index 000000000000..25a0f0329175
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletions.java
@@ -0,0 +1,447 @@
+/*
+ * 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.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.CircuitBreakerDefinition;
+import org.apache.camel.model.OutputNode;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.tooling.model.LanguageModel;
+
+/**
+ * The Tab completions of the Java DSL route chain in the Source editor 
(CAMEL-25241): after a dot, the methods that
+ * compile there. The chain is walked from its start (from, onException...) 
over the model classes with reflection, with
+ * a stack of the open blocks as the DSL has at runtime: a block EIP (split, 
filter, choice, doTry...) opens one, end()
+ * closes it, endChoice() and endDoTry() go back to the choice or try, a 
builder clause (.split().simple(..),
+ * .marshal().json()) returns to its EIP. An overload the text cannot tell 
apart (recipientList(header("x")) or
+ * recipientList(",")) is kept both ways until a following call rules one out. 
The options of the EIP the chain is on
+ * come first, then the EIPs; the documentation comes from the catalog. This 
is light assistance for hand-written edits,
+ * not a Java compiler: a chain it cannot follow gets no completion.
+ */
+final class JavaDslCompletions {
+
+    /** Names that are no DSL, although public on the model classes. */
+    private static final Set<String> NOT_DSL = Set.of(
+            "copyDefinition", "toString", "equals", "hashCode", "getClass", 
"clone", "wait", "notify", "notifyAll",
+            "createChildProcessor", "preCreateProcessor", "configureChild", 
"addOutput", "clearOutput");
+
+    /** The methods that close blocks, with what they do: they have no EIP in 
the catalog. */
+    private static final Map<String, String> END_DOCS = Map.of(
+            "end", "Ends the current block (split, filter, choice, doTry...), 
going back to the one around it",
+            "endParent", "Ends the current block and goes back to the parent 
of the block",
+            "endChoice", "Ends the current when or otherwise, going back to 
the choice to add another when or otherwise",
+            "endDoTry", "Ends the current doCatch or doFinally, going back to 
the doTry",
+            "endDoCatch", "Ends the current block, going back to the doCatch",
+            "endCircuitBreaker", "Ends the current onFallback, going back to 
the circuitBreaker");
+
+    /** The most states kept for overloads the text cannot tell apart. */
+    private static final int MAX_STATES = 8;
+
+    // the DSL methods of a class by name, without and with the deprecated ones
+    private static final Map<Class<?>, Map<String, List<Method>>> METHODS = 
new ConcurrentHashMap<>();
+    private static final Map<Class<?>, Map<String, List<Method>>> ALL_METHODS 
= new ConcurrentHashMap<>();
+    // whether a model class is a configuration ended by end(), as a clause
+    private static final Map<Class<?>, Boolean> CLAUSES = new 
ConcurrentHashMap<>();
+    // the EIP name of a definition class, by the java type the catalog has 
for it
+    private static final Map<String, String> EIP_BY_TYPE = new 
ConcurrentHashMap<>();
+
+    private JavaDslCompletions() {
+    }
+
+    /**
+     * Where the chain is after its calls.
+     *
+     * @param blocks  the open blocks, the outermost (the route) first
+     * @param current the definition the next call is on: the innermost block, 
or the EIP just added that has options
+     * @param clause  the builder clause the chain is on (.split().|), whose 
methods return to {@code after}; else null
+     * @param after   the definition a clause returns to
+     */
+    record State(List<Class<?>> blocks, Class<?> current, Class<?> clause, 
Class<?> after) {
+
+        Class<?> top() {
+            return blocks.get(blocks.size() - 1);
+        }
+
+        State on(Class<?> definition) {
+            return new State(blocks, definition, null, null);
+        }
+
+        State push(Class<?> block) {
+            List<Class<?>> b = new ArrayList<>(blocks);
+            b.add(block);
+            return new State(b, block, null, null);
+        }
+
+        State popTo(Class<?> block) {
+            int at = blocks.lastIndexOf(block);
+            if (at < 0) {
+                return null;
+            }
+            List<Class<?>> b = new ArrayList<>(blocks.subList(0, at + 1));
+            return new State(b, block, null, null);
+        }
+
+        State pop() {
+            if (blocks.size() == 1) {
+                return on(top());
+            }
+            List<Class<?>> b = new ArrayList<>(blocks.subList(0, blocks.size() 
- 1));
+            return new State(b, b.get(b.size() - 1), null, null);
+        }
+    }
+
+    /** The state after the calls of the chain, or null when the chain is not 
one this can follow. */
+    static State resolve(List<JavaChainContext.Call> calls) {
+        List<State> states = states(calls);
+        return states.isEmpty() ? null : states.get(0);
+    }
+
+    /** The possible states after the calls of the chain (one unless overloads 
leave it open); empty when it is lost. */
+    static List<State> states(List<JavaChainContext.Call> calls) {
+        if (calls.isEmpty()) {
+            return List.of();
+        }
+        List<State> states = new ArrayList<>();
+        for (Method m : candidates(RouteBuilder.class, calls.get(0))) {
+            Class<?> start = raw(m.getGenericReturnType());
+            if (start != null && 
ProcessorDefinition.class.isAssignableFrom(start)) {
+                states.add(new State(List.of(start), start, null, null));
+            }
+        }
+        for (int i = 1; i < calls.size() && !states.isEmpty(); i++) {
+            List<State> next = new ArrayList<>();
+            for (State s : states) {
+                for (State n : step(s, calls.get(i))) {
+                    if (next.size() < MAX_STATES && !next.contains(n)) {
+                        next.add(n);
+                    }
+                }
+            }
+            states = next;
+        }
+        return states;
+    }
+
+    /** The states one call leads to from a state: none when the call does not 
compile there. */
+    private static List<State> step(State s, JavaChainContext.Call call) {
+        String name = call.name();
+        if (s.clause() != null) {
+            List<State> found = new ArrayList<>();
+            for (Method m : candidates(s.clause(), call)) {
+                // an option of the clause (marshal().variableSend(..)) stays 
on it, the others return to the EIP
+                State n = raw(m.getGenericReturnType()) == s.clause() ? s : 
s.on(s.after());
+                if (!found.contains(n)) {
+                    found.add(n);
+                }
+            }
+            return found;
+        }
+        if (END_DOCS.containsKey(name)) {
+            State n = switch (name) {
+                case "endChoice" -> s.popTo(ChoiceDefinition.class);
+                case "endDoTry", "endDoCatch" -> s.popTo(TryDefinition.class);
+                case "endCircuitBreaker" -> 
s.popTo(CircuitBreakerDefinition.class);
+                // after an EIP that is no block (recipientList...), end() 
ends that EIP
+                default -> s.current() != s.top() ? s.on(s.top()) : s.pop();
+            };
+            return n != null ? List.of(n) : List.of();
+        }
+        List<State> found = new ArrayList<>();
+        List<Method> methods = candidates(s.current(), call);
+        State from = s;
+        if (methods.isEmpty() && s.current() != s.top()) {
+            // an EIP after one whose options were being set: it goes into the 
innermost block
+            from = s.on(s.top());
+            methods = candidates(from.current(), call);
+        }
+        for (Method m : methods) {
+            State base = isOption(m) ? from : from.on(from.top());
+            Type ret = m.getGenericReturnType();
+            Class<?> raw = raw(ret);
+            if (raw == null) {
+                continue;
+            }
+            if (isClause(raw)) {
+                Type arg = argument(ret);
+                Class<?> of = raw(arg);
+                State target = creates(m, arg, of, base) ? (isBlock(of) ? 
base.push(of) : base.on(of)) : base;
+                found.add(new State(target.blocks(), target.current(), raw, 
target.current()));
+            } else if (ProcessorDefinition.class.isAssignableFrom(raw)) {
+                if (!creates(m, ret, raw, base)) {
+                    // Type, or an option returning its definition: the chain 
stays where it is
+                    found.add(base);
+                } else if (isBlock(raw)) {
+                    found.add(base.push(raw));
+                } else {
+                    found.add(base.on(raw));
+                }
+            }
+        }
+        return found;
+    }
+
+    /**
+     * Whether the call adds a new definition of the returned class: an EIP 
returning a definition class (also of the
+     * class the chain is on: a choice in a choice) does, an option returning 
its own definition and a method returning
+     * Type (to, log...) do not.
+     */
+    private static boolean creates(Method m, Type returned, Class<?> raw, 
State base) {
+        if (raw == null || returned instanceof TypeVariable<?> || raw == 
ProcessorDefinition.class
+                || !ProcessorDefinition.class.isAssignableFrom(raw)) {
+            return false;
+        }
+        return !isOption(m) || !raw.isAssignableFrom(base.current());
+    }
+
+    /** The completions after the calls of the chain; empty when the chain is 
not one this can follow. */
+    static List<AutocompletePopup.CompletionItem> provide(CamelCatalog 
catalog, JavaChainContext context) {
+        Map<String, AutocompletePopup.CompletionItem> own = new TreeMap<>();
+        Map<String, AutocompletePopup.CompletionItem> ends = new TreeMap<>();
+        Map<String, AutocompletePopup.CompletionItem> eips = new TreeMap<>();
+        for (State state : states(context.calls())) {
+            boolean clause = state.clause() != null;
+            Map<String, BaseOptionModel> options = clause ? Map.of() : 
optionDocs(catalog, state.current());
+            for (Map.Entry<String, List<Method>> e : methods(clause ? 
state.clause() : state.current(), false)
+                    .entrySet()) {
+                String name = e.getKey();
+                List<Method> overloads = e.getValue();
+                if (END_DOCS.containsKey(name)) {
+                    if (clause) {
+                        // the end() of a configuration 
(resilience4jConfiguration()...)
+                        ends.putIfAbsent(name, item(name, "Ends the 
configuration, going back to the "
+                                                          + 
state.after().getSimpleName(),
+                                "end", overloads));
+                    } else if (closes(state, name)) {
+                        ends.putIfAbsent(name, item(name, END_DOCS.get(name), 
"end", overloads));
+                    }
+                } else if (clause) {
+                    LanguageModel language = catalog != null ? 
catalog.languageModel(name) : null;
+                    own.putIfAbsent(name, item(name, language != null ? 
language.getDescription() : null,
+                            state.clause().getSimpleName(), overloads));
+                } else if (isOption(overloads.get(0))) {
+                    BaseOptionModel option = options.get(name);
+                    own.putIfAbsent(name, item(name, option != null ? 
option.getDescription() : null, "option", overloads));
+                } else {
+                    EipModel eip = catalog != null ? catalog.eipModel(name) : 
null;
+                    eips.putIfAbsent(name, item(name, eip != null ? 
eip.getDescription() : null, "EIP", overloads));
+                }
+            }
+        }
+        List<AutocompletePopup.CompletionItem> items = new 
ArrayList<>(own.values());
+        items.addAll(ends.values());
+        eips.keySet().removeAll(own.keySet());
+        items.addAll(eips.values());
+        return items;
+    }
+
+    /** Whether the end method closes something: end() always compiles, 
endChoice() only inside a choice... */
+    private static boolean closes(State state, String name) {
+        return switch (name) {
+            case "endChoice" -> 
state.blocks().contains(ChoiceDefinition.class);
+            case "endDoTry", "endDoCatch" -> 
state.blocks().contains(TryDefinition.class);
+            case "endCircuitBreaker" -> 
state.blocks().contains(CircuitBreakerDefinition.class);
+            default -> true;
+        };
+    }
+
+    private static AutocompletePopup.CompletionItem item(String name, String 
doc, String kind, List<Method> overloads) {
+        StringBuilder sb = new StringBuilder(doc != null ? doc : "");
+        sb.append(sb.isEmpty() ? "" : "\n\n");
+        overloads.stream().limit(6).forEach(m -> 
sb.append(signature(m)).append('\n'));
+        if (overloads.size() > 6) {
+            sb.append("... ").append(overloads.size() - 6).append(" more\n");
+        }
+        boolean args = overloads.stream().anyMatch(m -> m.getParameterCount() 
> 0);
+        // the cursor goes inside the parentheses when the method can take 
something (split(|)), else after them
+        String insert = name + (args ? "(" + XmlCompletions.CARET + ")" : "()" 
+ XmlCompletions.CARET);
+        return new AutocompletePopup.CompletionItem(
+                name, sb.toString().strip(), kind, null, false, null, null, 
false, insert);
+    }
+
+    private static String signature(Method m) {
+        StringBuilder sb = new StringBuilder(m.getName()).append('(');
+        Class<?>[] types = m.getParameterTypes();
+        for (int i = 0; i < types.length; i++) {
+            sb.append(i > 0 ? ", " : "").append(types[i].getSimpleName());
+        }
+        return sb.append(')').toString();
+    }
+
+    /** The catalog options of the EIP of a definition class, by name, for the 
documentation of its methods. */
+    private static Map<String, BaseOptionModel> optionDocs(CamelCatalog 
catalog, Class<?> definition) {
+        Map<String, BaseOptionModel> found = new HashMap<>();
+        if (catalog == null) {
+            return found;
+        }
+        if (EIP_BY_TYPE.isEmpty()) {
+            for (String name : catalog.findModelNames()) {
+                EipModel m = catalog.eipModel(name);
+                if (m != null && m.getJavaType() != null) {
+                    EIP_BY_TYPE.putIfAbsent(m.getJavaType(), name);
+                }
+            }
+        }
+        String name = EIP_BY_TYPE.get(definition.getName());
+        EipModel eip = name != null ? catalog.eipModel(name) : null;
+        if (eip != null) {
+            for (BaseOptionModel o : eip.getOptions()) {
+                found.put(o.getName(), o);
+            }
+        }
+        return found;
+    }
+
+    /** Whether the definition holds the EIPs that follow it, until its end(). 
*/
+    static boolean isBlock(Class<?> definition) {
+        return OutputNode.class.isAssignableFrom(definition) || definition == 
ChoiceDefinition.class;
+    }
+
+    /**
+     * Whether the type is a clause that returns to its EIP: a builder clause 
(ExpressionClause, DataFormatClause...),
+     * or a configuration of the model ended by end() 
(circuitBreaker().resilience4jConfiguration()...end()).
+     */
+    private static boolean isClause(Class<?> type) {
+        if (ProcessorDefinition.class.isAssignableFrom(type)) {
+            return false;
+        }
+        if (type.getName().startsWith("org.apache.camel.builder.")) {
+            return type.getTypeParameters().length == 1;
+        }
+        return type.getName().startsWith("org.apache.camel.model.") && 
CLAUSES.computeIfAbsent(type, t -> {
+            for (Method m : t.getMethods()) {
+                if ("end".equals(m.getName()) && m.getParameterCount() == 0
+                        && 
ProcessorDefinition.class.isAssignableFrom(m.getReturnType())) {
+                    return true;
+                }
+            }
+            return false;
+        });
+    }
+
+    /** Whether the method is an option of its definition (declared below 
ProcessorDefinition) rather than an EIP. */
+    static boolean isOption(Method m) {
+        Class<?> declaring = m.getDeclaringClass();
+        if (ProcessorDefinition.class.isAssignableFrom(declaring)) {
+            return declaring != ProcessorDefinition.class;
+        }
+        // id, description... of the identified definitions
+        return declaring.getName().startsWith("org.apache.camel.model.");
+    }
+
+    /** The methods of the class the call can be: by name and number of 
arguments, deprecated ones included. */
+    private static List<Method> candidates(Class<?> type, 
JavaChainContext.Call call) {
+        List<Method> overloads = methods(type, true).get(call.name());
+        if (overloads == null) {
+            return List.of();
+        }
+        List<Method> found = new ArrayList<>();
+        for (Method m : overloads) {
+            if (m.getParameterCount() == call.arguments()
+                    || m.isVarArgs() && call.arguments() >= 
m.getParameterCount() - 1) {
+                found.add(m);
+            }
+        }
+        return found;
+    }
+
+    /**
+     * The DSL methods of a class by name: public, of the Camel model or 
builders, returning where the chain goes on.
+     * The deprecated ones are followed in a chain but not offered.
+     */
+    static Map<String, List<Method>> methods(Class<?> type, boolean 
deprecated) {
+        return (deprecated ? ALL_METHODS : METHODS).computeIfAbsent(type, t -> 
{
+            Map<String, List<Method>> found = new LinkedHashMap<>();
+            for (Method m : t.getMethods()) {
+                if (isDsl(t, m) && (deprecated || 
!m.isAnnotationPresent(Deprecated.class))) {
+                    found.computeIfAbsent(m.getName(), k -> new 
ArrayList<>()).add(m);
+                }
+            }
+            return found;
+        });
+    }
+
+    private static boolean isDsl(Class<?> type, Method m) {
+        if (Modifier.isStatic(m.getModifiers()) || m.isBridge() || 
m.isSynthetic() || NOT_DSL.contains(m.getName())) {
+            return false;
+        }
+        String pkg = m.getDeclaringClass().getName();
+        if (!pkg.startsWith("org.apache.camel.model.") && 
!pkg.startsWith("org.apache.camel.builder.")) {
+            return false;
+        }
+        if (m.getParameterCount() == 0 && (m.getName().startsWith("get") || 
m.getName().startsWith("is"))) {
+            return false;
+        }
+        for (Class<?> p : m.getParameterTypes()) {
+            if (CamelContext.class.isAssignableFrom(p)) {
+                return false;
+            }
+        }
+        Type ret = m.getGenericReturnType();
+        if (ret instanceof TypeVariable<?>) {
+            // Type of the definitions, or T of a clause: where the chain goes 
on
+            return true;
+        }
+        Class<?> raw = raw(ret);
+        if (isClause(type)) {
+            // a clause ends with the methods that return to its EIP (T, or 
end() of a configuration); its options
+            // return the clause itself
+            return raw == type || raw != null && 
ProcessorDefinition.class.isAssignableFrom(raw);
+        }
+        return raw != null && (ProcessorDefinition.class.isAssignableFrom(raw) 
|| isClause(raw));
+    }
+
+    private static Class<?> raw(Type type) {
+        if (type instanceof Class<?> c) {
+            return c;
+        }
+        if (type instanceof ParameterizedType p) {
+            return raw(p.getRawType());
+        }
+        if (type instanceof TypeVariable<?> v) {
+            Type[] bounds = v.getBounds();
+            return bounds.length > 0 ? raw(bounds[0]) : Object.class;
+        }
+        return null;
+    }
+
+    private static Type argument(Type type) {
+        if (type instanceof ParameterizedType p && 
p.getActualTypeArguments().length > 0) {
+            return p.getActualTypeArguments()[0];
+        }
+        return null;
+    }
+}
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 a20544adeef8..460773832678 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
@@ -1016,6 +1016,11 @@ final class SourceEditAssist {
         return SimpleQuickDoc.at(validationCatalog(), lines, row, col);
     }
 
+    /** The completions of the Java DSL route chain at the cursor 
(CAMEL-25241). */
+    List<AutocompletePopup.CompletionItem> 
provideJavaCompletions(JavaChainContext context) {
+        return JavaDslCompletions.provide(validationCatalog(), context);
+    }
+
     /** The completions of the XML DSL at the cursor (CAMEL-25240). */
     List<AutocompletePopup.CompletionItem> 
provideXmlCompletions(XmlCompletionContext context, List<String> lines) {
         return XmlCompletions.provide(validationCatalog(), context, 
this::loadPropertyPlaceholders);
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 788cd694a316..719313f05fcc 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
@@ -851,6 +851,8 @@ class SourceTab extends AbstractTab {
         sourceViewer.setCursorQuickDocProvider(
                 routeFile || isCamelSourceFile(filePath) ? 
assist::provideSimpleQuickDoc : null);
         // the elements, attributes and values of an XML route (CAMEL-25240)
+        // the route chain of a Java route (CAMEL-25241)
+        sourceViewer.setJavaCompletion(routeFile && name.endsWith(".java") ? 
assist::provideJavaCompletions : null);
         sourceViewer.setXmlCompletion(routeFile && 
name.toLowerCase().endsWith(".xml") ? assist::provideXmlCompletions : null);
         if (isCamelSourceFile(filePath)) {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
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 a489b4286ded..35d89bc36dbd 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
@@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.IntConsumer;
 
 import com.networknt.schema.Error;
@@ -226,6 +227,8 @@ class SourceViewer {
     private CursorQuickDocProvider cursorQuickDocProvider;
     private BiFunction<XmlCompletionContext, List<String>, 
List<AutocompletePopup.CompletionItem>> xmlCompletion;
     private XmlCompletion pendingXmlCompletion;
+    private Function<JavaChainContext, List<AutocompletePopup.CompletionItem>> 
javaCompletion;
+    private JavaCompletion pendingJavaCompletion;
     private List<String> routeProblems = List.of();
     private List<String> validationErrors;
     private int validationErrorScroll;
@@ -453,6 +456,17 @@ class SourceViewer {
     private record XmlCompletion(int row, int endCol, XmlCompletionContext 
context) {
     }
 
+    /**
+     * Tab completion of the Java DSL route chain (CAMEL-25241): after a dot, 
the options of the EIP the chain is on and
+     * the EIPs. Null for a file that is no Java route.
+     */
+    void setJavaCompletion(Function<JavaChainContext, 
List<AutocompletePopup.CompletionItem>> provider) {
+        this.javaCompletion = provider;
+    }
+
+    private record JavaCompletion(int row, int endCol, JavaChainContext 
context) {
+    }
+
     /** Opens the AI panel to fix the problem of the cursor line (Shift+F8); 
null when there is no AI panel. */
     void setAskAi(MonitorContext.AskAi askAi) {
         this.askAi = askAi;
@@ -474,6 +488,7 @@ class SourceViewer {
         simpleCompletion = null;
         cursorQuickDocProvider = null;
         xmlCompletion = null;
+        javaCompletion = null;
     }
 
     void reset() {
@@ -518,6 +533,7 @@ class SourceViewer {
         simpleCompletion = null;
         cursorQuickDocProvider = null;
         xmlCompletion = null;
+        javaCompletion = null;
     }
 
     boolean isMarkdownMode() {
@@ -1230,14 +1246,19 @@ class SourceViewer {
         pendingUriCompletion = null;
         pendingSimpleCompletion = null;
         pendingXmlCompletion = null;
+        pendingJavaCompletion = null;
         if (openSimpleAutocomplete()) {
             return;
         }
         if (isCamelYamlFile()) {
             openYamlAutocomplete();
         } else if (uriCompletion != null) {
-            if (!openUriAutocomplete() && "xml".equals(uriCompletion)) {
-                openXmlAutocomplete();
+            if (!openUriAutocomplete()) {
+                if ("xml".equals(uriCompletion)) {
+                    openXmlAutocomplete();
+                } else if ("java".equals(uriCompletion)) {
+                    openJavaAutocomplete();
+                }
             }
         } else {
             openPropertiesAutocomplete();
@@ -1475,6 +1496,75 @@ class SourceViewer {
         }
     }
 
+    /** The completion of the Java DSL route chain after a dot, read from the 
text above the cursor like the uris. */
+    private void openJavaAutocomplete() {
+        if (javaCompletion == null) {
+            return;
+        }
+        int row = editState.cursorRow();
+        int col = editState.cursorCol();
+        List<String> all = new ArrayList<>(editState.lineCount());
+        for (int i = 0; i < editState.lineCount(); i++) {
+            all.add(editState.getLine(i));
+        }
+        JavaChainContext c = JavaChainContext.at(all, row, col);
+        if (c == null) {
+            return;
+        }
+        List<AutocompletePopup.CompletionItem> items = javaCompletion.apply(c);
+        if (items != null && !items.isEmpty()) {
+            autocompletePopup = new AutocompletePopup(items, c.prefix(), 
c.prefix(), false);
+            autocompletePopup.setFullKeys(true);
+            autocompletePopup.setTitlePrefix("Java DSL");
+            pendingJavaCompletion = new JavaCompletion(row, col, c);
+        }
+    }
+
+    /**
+     * Replaces the name typed after the dot with the chosen method, with its 
parentheses (the cursor inside them when
+     * it takes arguments) unless they follow already.
+     */
+    private void insertJavaCompletion(AutocompletePopup.CompletionItem item) {
+        JavaCompletion jc = pendingJavaCompletion;
+        pendingJavaCompletion = null;
+        if (editState.cursorRow() != jc.row()) {
+            return;
+        }
+        int col = editState.cursorCol();
+        for (; col < jc.endCol(); col++) {
+            editState.moveCursorRight();
+        }
+        for (; col > jc.endCol(); col--) {
+            editState.moveCursorLeft();
+        }
+        for (int i = 0; i < jc.context().prefix().length(); i++) {
+            editState.deleteBackward();
+        }
+        String line = editState.getLine(jc.row());
+        int from = editState.cursorCol();
+        int end = from;
+        while (end < line.length() && 
Character.isJavaIdentifierPart(line.charAt(end))) {
+            end++;
+        }
+        for (int i = from; i < end; i++) {
+            editState.deleteForward();
+        }
+        String text = item.insertText();
+        int caret = text.indexOf(XmlCompletions.CARET);
+        if (caret >= 0) {
+            text = text.substring(0, caret) + text.substring(caret + 1);
+        }
+        if (end < line.length() && line.charAt(end) == '(') {
+            // the arguments are there already: only the name changes
+            text = item.key();
+            caret = -1;
+        }
+        editState.insert(text);
+        for (int i = caret >= 0 ? text.length() - caret : 0; i > 0; i--) {
+            editState.moveCursorLeft();
+        }
+    }
+
     /** Replaces the prefix the completion was opened on with the chosen item 
and what follows it (: or =). */
     private void insertUriCompletion(AutocompletePopup.CompletionItem item) {
         UriCompletion jc = pendingUriCompletion;
@@ -1693,6 +1783,10 @@ class SourceViewer {
             insertXmlCompletion(item);
             return;
         }
+        if (pendingJavaCompletion != null) {
+            insertJavaCompletion(item);
+            return;
+        }
         String currentLine = editState.getLine(editState.cursorRow());
         if (isCamelYamlFile()) {
             insertYamlCompletion(item, valueMode, currentLine, listItem);
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 92ca88aec00c..b09321117da3 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
@@ -101,6 +101,17 @@ Press **F4** to enter edit mode, then **Tab** for 
context-aware completion:
   `?` or `&` (`&amp;` in XML; filtered by consumer/producer role, already given
   ones left out), and the value of an option after `=`
 
+**Java DSL routes:**
+- After a dot in a route chain, Tab lists the methods that compile there: the
+  options of the EIP the chain is on first (`.split(body()).` offers
+  `parallelProcessing`, `streaming`...), then the EIPs, and the `end()`,
+  `endChoice()` or `endDoTry()` that closes the block you are in
+- The method is inserted with its parentheses, the cursor inside them when it
+  takes arguments; the documentation comes from the catalog
+- Light help for hand-written edits: routes in variables, the REST DSL and
+  expression builders inside arguments are not completed; an AI coding agent
+  helps with more: the F8 AI panel, or any agent that speaks ACP
+
 **XML DSL routes:**
 - After `<`, or on an empty line, Tab lists the elements that go inside the
   parent element (the EIPs of a route, `when` and `otherwise` in a `choice`,
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletionTest.java
new file mode 100644
index 000000000000..479259046812
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaDslCompletionTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.RecipientListDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.SplitDefinition;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tab completion of the Java DSL route chain in the Source editor 
(CAMEL-25241).
+ */
+class JavaDslCompletionTest {
+
+    private static CamelCatalog catalog;
+
+    @TempDir
+    Path tempDir;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+    }
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    /** The context at the end of the last line. */
+    private static JavaChainContext at(String... lines) {
+        int row = lines.length - 1;
+        return JavaChainContext.at(List.of(lines), row, lines[row].length());
+    }
+
+    private static List<String> keys(String... lines) {
+        return JavaDslCompletions.provide(catalog, 
at(lines)).stream().map(AutocompletePopup.CompletionItem::key)
+                .toList();
+    }
+
+    private static JavaDslCompletions.State state(String... lines) {
+        return JavaDslCompletions.resolve(at(lines).calls());
+    }
+
+    @Test
+    void theChainAtTheCursor() {
+        JavaChainContext c = at(
+                "    public void configure() {",
+                "        from(\"timer:tick?period=1000\") // a comment with (",
+                "            .split(body().tokenize(\",\")) /* and ) */",
+                "            .str");
+        
assertThat(c.calls()).extracting(JavaChainContext.Call::name).containsExactly("from",
 "split");
+        assertThat(c.calls().get(1).arguments()).isEqualTo(1);
+        assertThat(c.prefix()).isEqualTo("str");
+
+        // the commas of an anonymous class body and of type arguments are no 
argument separators
+        c = at("this.from(\"a\").process(new Processor() { Map<String, String> 
m; void x() { f(1, 2); } }).");
+        
assertThat(c.calls()).extracting(JavaChainContext.Call::name).containsExactly("from",
 "process");
+        assertThat(c.calls().get(1).arguments()).isEqualTo(1);
+
+        // in an argument, in a string, in a comment, not after a dot
+        assertThat(at("from(\"a\").split(body().")).isNull();
+        assertThat(at("from(\"a.")).isNull();
+        assertThat(at("from(\"a\") // .")).isNull();
+        assertThat(at("from(\"a\") ")).isNull();
+    }
+
+    @Test
+    void theOptionsOfTheEipComeFirst() {
+        List<String> keys = keys("from(\"a\").split(body()).");
+        assertThat(keys).contains("parallelProcessing", "streaming", "to", 
"log", "end");
+        assertThat(keys.indexOf("streaming")).isLessThan(keys.indexOf("to"));
+        // getters, internals and deprecated methods are no DSL to offer
+        assertThat(keys).doesNotContain("getOutputs", "addOutput", 
"copyDefinition", "parallelAggregate");
+    }
+
+    @Test
+    void theBlocksOfTheChain() {
+        // to() returns the split itself: its options still apply; end() goes 
back to the route
+        
assertThat(state("from(\"a\").split(body()).to(\"b\").").current()).isEqualTo(SplitDefinition.class);
+        
assertThat(state("from(\"a\").split(body()).to(\"b\").end().").current()).isEqualTo(RouteDefinition.class);
+
+        // when and otherwise stay on the choice; endChoice goes back to it 
from inside
+        
assertThat(keys("from(\"a\").choice().when(header(\"x\")).to(\"b\").")).contains("when",
 "otherwise",
+                "endChoice");
+        assertThat(state(
+                
"from(\"a\").choice().when(header(\"x\")).split(body()).endChoice().").current())
+                .isEqualTo(ChoiceDefinition.class);
+        // a choice in a choice is a new block: end() ends the inner one only
+        assertThat(state(
+                
"from(\"a\").choice().when(header(\"x\")).choice().when(header(\"y\")).end().").blocks())
+                .containsExactly(RouteDefinition.class, 
ChoiceDefinition.class);
+        assertThat(keys("from(\"a\").to(\"b\").")).doesNotContain("endChoice", 
"endDoTry");
+    }
+
+    @Test
+    void anEipThatIsNoBlockHasItsOptionsUntilTheNextEip() {
+        
assertThat(state("from(\"a\").recipientList(header(\"x\")).").current())
+                .isEqualTo(RecipientListDefinition.class);
+        
assertThat(keys("from(\"a\").recipientList(header(\"x\")).")).contains("parallelProcessing",
 "to");
+        // the next EIP goes into the route, and end() ends the recipient list
+        
assertThat(state("from(\"a\").recipientList(header(\"x\")).to(\"b\").").current())
+                .isEqualTo(RouteDefinition.class);
+        assertThat(state(
+                
"from(\"a\").split(body()).recipientList(header(\"x\")).end().").current())
+                .isEqualTo(SplitDefinition.class);
+    }
+
+    @Test
+    void clausesReturnToTheirEip() {
+        // the expression clause of split: its languages, then back on the 
split
+        assertThat(keys("from(\"a\").split().")).contains("simple", "header", 
"jsonpath").doesNotContain("to");
+        
assertThat(state("from(\"a\").split().simple(\"${body}\").").current()).isEqualTo(SplitDefinition.class);
+        // a data format clause, its options stay on it
+        assertThat(keys("from(\"a\").marshal().")).contains("json", "csv");
+        
assertThat(state("from(\"a\").marshal().variableSend(\"v\").json().").current())
+                .isEqualTo(RouteDefinition.class);
+        // an overload the text cannot tell apart is decided by what follows it
+        
assertThat(state("from(\"a\").recipientList(\",\").header(\"x\").").current())
+                .isEqualTo(RecipientListDefinition.class);
+        // a configuration ended by end()
+        
assertThat(keys("from(\"a\").circuitBreaker().resilience4jConfiguration().")).contains("end",
+                "timeoutEnabled");
+    }
+
+    @Test
+    void anEipIsInsertedWithTheCursorInItsArguments() throws Exception {
+        SourceViewer viewer = viewer("""
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .spl
+                    }
+                }
+                """);
+        cursorAt(viewer, 3);
+        tab(viewer);
+        enter(viewer);
+        assertThat(line(viewer, 3)).isEqualTo("            .split()");
+        type(viewer, "body()");
+        assertThat(line(viewer, 3)).isEqualTo("            .split(body())");
+    }
+
+    @Test
+    void anOptionOfTheEip() throws Exception {
+        SourceViewer viewer = viewer("""
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .split(body()).strea
+                    }
+                }
+                """);
+        cursorAt(viewer, 3);
+        tab(viewer);
+        enter(viewer);
+        assertThat(line(viewer, 3)).isEqualTo("            
.split(body()).streaming()");
+    }
+
+    @Test
+    void theUriStringStaysWithTheUriCompletion() throws Exception {
+        SourceViewer viewer = viewer("""
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .to("sed");
+                    }
+                }
+                """);
+        for (int i = 0; i < 3; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+        for (int i = 0; i < 3; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, 
KeyModifiers.NONE));
+        }
+        tab(viewer);
+        type(viewer, "a");
+        enter(viewer);
+        assertThat(line(viewer, 3)).isEqualTo("            .to(\"seda:\");");
+    }
+
+    private SourceViewer viewer(String src) throws Exception {
+        Path file = tempDir.resolve("MyRoute.java");
+        Files.writeString(file, src, StandardCharsets.UTF_8);
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of());
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        SourceEditAssist assist = new SourceEditAssist(new 
MonitorContext(data, infraData));
+        SourceViewer viewer = new SourceViewer();
+        viewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
+        
viewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
+        viewer.setUriCompletion("java");
+        viewer.setSimpleCompletion(assist::provideSimpleCompletions);
+        viewer.setJavaCompletion(c -> JavaDslCompletions.provide(catalog, c));
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+        return viewer;
+    }
+
+    /** Puts the cursor at the end of the line. */
+    private static void cursorAt(SourceViewer viewer, int row) {
+        for (int i = 0; i < row; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+    }
+
+    private static void tab(SourceViewer viewer) {
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE));
+    }
+
+    private static void enter(SourceViewer viewer) {
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
+    }
+
+    private static void type(SourceViewer viewer, String text) {
+        for (char ch : text.toCharArray()) {
+            viewer.handleKeyEvent(KeyEvent.ofChar(ch, KeyModifiers.NONE));
+        }
+    }
+
+    private static String line(SourceViewer viewer, int row) {
+        return viewer.editText().split("\n")[row];
+    }
+}

Reply via email to