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

davsclaus pushed a commit to branch fix/CAMEL-25243
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 8954f33bdd8ac72bda99d05e5a9a0f1f5af368ce
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 00:03:55 2026 +0200

    CAMEL-25243: camel-jbang - source editor: Java DSL completion inside 
arguments and for the REST DSL
    
    Follow-up of CAMEL-25241 for two of its limitations:
    - in an argument, the chain of the argument is completed from the route
      builder method it starts with: .filter(header("x"). offers isEqualTo,
      isNotNull, contains...; .split(body(). offers tokenize, convertTo...
    - chains that start with something else than a route: the REST DSL
      (rest("/api").get("/x")., with param()...endParam() and 
responseMessage()),
      restConfiguration(), routeTemplate() and its templateBean()...end(), and
      builders such as deadLetterChannel(..).
    
    On a builder or a definition that is no route EIP the chain follows the
    returned types (no blocks); a sub-builder whose end() returns its parent as 
a
    type parameter goes back to where it was entered. Getter-named methods stay
    out, except fluent ones (rest get(), header("x").isNotNull()).
    
    Recall over the Java route tests of camel-core and 191 components: arguments
    6,362 positions 99.5%, REST and templates 2,375 positions 100%, route chains
    unchanged at 99.94%.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel-tui-source-java-argument-completion.png  | Bin 0 -> 311294 bytes
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  14 +--
 .../jbang/core/commands/tui/JavaChainContext.java  |  37 +++++---
 .../core/commands/tui/JavaDslCompletions.java      |  95 +++++++++++++++++++--
 .../src/main/resources/tui/help/source.md          |   9 +-
 .../core/commands/tui/JavaDslCompletionTest.java   |  38 ++++++++-
 6 files changed, 164 insertions(+), 29 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-argument-completion.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-argument-completion.png
new file mode 100644
index 000000000000..31d24838a678
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-java-argument-completion.png
 differ
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 414d4e75d112..a49f7d6208ca 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
@@ -127,7 +127,9 @@ What can be completed:
   `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.
+  (`.split(|)`). The documentation comes from the catalog. In an argument the 
chain of the argument is completed
+  (`.filter(header("priority").` offers `isEqualTo`, `isNotNull`, 
`contains`...), and so are the REST DSL
+  (`rest("/api").get("/orders").`, with `param()` ... `endParam()`), 
`restConfiguration()` and route templates.
 * *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
@@ -152,16 +154,18 @@ image::jbang/camel-tui-source-simple-completion.png[After 
${header. Tab lists th
 
 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]
 
+image::jbang/camel-tui-source-java-argument-completion.png[In the argument of 
a filter, Tab lists the methods of the header builder]
+
 === 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
+* *Java routes* -- the chain is followed from what a route builder starts it 
with (`from(...)`, `onException(...)`,
+  `rest(...)`, `routeTemplate(...)`, or `body()`, `header(..)`... in an 
argument). A route kept in a variable
+  (`RouteDefinition route = from(...); route.`), the code of lambdas and 
processors, the fluent language builders
+  (`expression().jsonpath()...`), 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}}+`.
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
index 73c4775962fc..2f986409b96f 100644
--- 
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
@@ -24,10 +24,12 @@ import java.util.List;
  * 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
+ * @param calls    the calls of the chain before the cursor, the first one 
starting it (from, onException, rest, or
+ *                 body, header... in an argument)
+ * @param prefix   the name typed after the last dot, which the completion 
replaces
+ * @param argument whether the chain is in the argument of a call 
(.split(body().|)) rather than a statement
  */
-record JavaChainContext(List<Call> calls, String prefix) {
+record JavaChainContext(List<Call> calls, String prefix, boolean argument) {
 
     /**
      * A call of the chain.
@@ -57,23 +59,30 @@ record JavaChainContext(List<Call> calls, String prefix) {
             return null;
         }
         // the statement the cursor is in starts after the last ; { or } 
outside of any parentheses
-        int depth = 0;
+        // where the statement starts, and where each argument open at the 
cursor starts: after its ( or , or, in a
+        // lambda body inside the argument, after the last ; { or }
         int start = 0;
+        List<Integer> arguments = new ArrayList<>();
         for (int i = 0; i < text.length(); i++) {
             char c = text.charAt(i);
             if (c == '(') {
-                depth++;
+                arguments.add(i + 1);
             } else if (c == ')') {
-                depth = Math.max(0, depth - 1);
-            } else if (depth == 0 && (c == ';' || c == '{' || c == '}')) {
-                start = i + 1;
+                if (!arguments.isEmpty()) {
+                    arguments.remove(arguments.size() - 1);
+                }
+            } else if (c == ',' && !arguments.isEmpty()) {
+                arguments.set(arguments.size() - 1, i + 1);
+            } else if (c == ';' || c == '{' || c == '}') {
+                if (arguments.isEmpty()) {
+                    start = i + 1;
+                } else {
+                    arguments.set(arguments.size() - 1, i + 1);
+                }
             }
         }
-        if (depth > 0) {
-            // in the arguments of a call
-            return null;
-        }
-        String statement = text.substring(start);
+        boolean argument = !arguments.isEmpty();
+        String statement = text.substring(argument ? 
arguments.get(arguments.size() - 1) : start);
         int end = statement.length();
         int p = end;
         while (p > 0 && Character.isJavaIdentifierPart(statement.charAt(p - 
1))) {
@@ -91,7 +100,7 @@ record JavaChainContext(List<Call> calls, String prefix) {
         if (calls == null || calls.isEmpty()) {
             return null;
         }
-        return new JavaChainContext(calls, prefix);
+        return new JavaChainContext(calls, prefix, argument);
     }
 
     /** The calls of a chain: name(args).name(args)..., null when the text is 
no such chain. */
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
index 25a0f0329175..d49f9fbf80bc 100644
--- 
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
@@ -31,6 +31,8 @@ import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.model.ChoiceDefinition;
@@ -57,7 +59,7 @@ 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");
+            "createChildProcessor", "preCreateProcessor", "configureChild", 
"addOutput", "clearOutput", "evaluate");
 
     /** 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(
@@ -138,7 +140,8 @@ final class JavaDslCompletions {
         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)) {
+            // a route (from, onException...), or a builder: body(), 
header(..) in an argument, rest(..), routeTemplate(..)
+            if (start != null && 
(ProcessorDefinition.class.isAssignableFrom(start) || isBuilder(start))) {
                 states.add(new State(List.of(start), start, null, null));
             }
         }
@@ -159,6 +162,9 @@ final class JavaDslCompletions {
     /** 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 && 
!ProcessorDefinition.class.isAssignableFrom(s.current())) {
+            return plainStep(s, call);
+        }
         if (s.clause() != null) {
             List<State> found = new ArrayList<>();
             for (Method m : candidates(s.clause(), call)) {
@@ -214,6 +220,49 @@ final class JavaDslCompletions {
         return found;
     }
 
+    /**
+     * A call on a builder or a definition that is no route EIP (ValueBuilder, 
RestDefinition, ParamDefinition...): it
+     * has no blocks, the chain goes on with what the method returns, a route 
when that is one (routeTemplate().from()).
+     */
+    private static List<State> plainStep(State s, JavaChainContext.Call call) {
+        List<State> found = new ArrayList<>();
+        for (Method m : candidates(s.current(), call)) {
+            Type ret = m.getGenericReturnType();
+            Class<?> raw = raw(ret);
+            State n = null;
+            if (ret instanceof TypeVariable<?> && (raw == null || 
!isBuilder(raw)) && s.after() != null) {
+                // end() of a sub-builder returning its parent as a type 
parameter (templateBean(..)...end())
+                n = new State(List.of(s.after()), s.after(), null, null);
+            } else if (raw != null && 
(ProcessorDefinition.class.isAssignableFrom(raw) || isBuilder(raw))) {
+                // remember where a sub-builder was entered from, to go back 
there on its end()
+                Class<?> parent = raw == s.current() ? s.after() : s.current();
+                n = new State(List.of(raw), raw, null, 
ProcessorDefinition.class.isAssignableFrom(raw) ? null : parent);
+            }
+            if (n != null && !found.contains(n)) {
+                found.add(n);
+            }
+        }
+        return found;
+    }
+
+    /** Whether a chain can start with the method of the route builder: from, 
rest, body, header... */
+    static boolean isBuilderStart(String name) {
+        for (Method m : methods(RouteBuilder.class, true).getOrDefault(name, 
List.of())) {
+            Class<?> raw = raw(m.getGenericReturnType());
+            if (raw != null && 
(ProcessorDefinition.class.isAssignableFrom(raw) || isBuilder(raw))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Whether the class is one of the DSL builders or definitions a chain 
can be on besides the route EIPs. */
+    static boolean isBuilder(Class<?> type) {
+        String name = type.getName();
+        return name.startsWith("org.apache.camel.model.") || 
name.startsWith("org.apache.camel.builder.")
+                || name.startsWith("org.apache.camel.support.builder.");
+    }
+
     /**
      * 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
@@ -233,6 +282,18 @@ final class JavaDslCompletions {
         Map<String, AutocompletePopup.CompletionItem> ends = new TreeMap<>();
         Map<String, AutocompletePopup.CompletionItem> eips = new TreeMap<>();
         for (State state : states(context.calls())) {
+            if (state.clause() == null && 
!ProcessorDefinition.class.isAssignableFrom(state.current())) {
+                // a builder or a definition that is no route EIP: its 
methods, documented where the catalog has them
+                Map<String, BaseOptionModel> options = optionDocs(catalog, 
state.current());
+                for (Map.Entry<String, List<Method>> e : 
methods(state.current(), false).entrySet()) {
+                    String name = e.getKey();
+                    BaseOptionModel option = options.get(name);
+                    EipModel model = option == null && catalog != null ? 
catalog.eipModel(name) : null;
+                    String doc = option != null ? option.getDescription() : 
model != null ? model.getDescription() : null;
+                    own.putIfAbsent(name, item(name, doc, 
state.current().getSimpleName(), e.getValue()));
+                }
+                continue;
+            }
             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)
@@ -398,11 +459,10 @@ final class JavaDslCompletions {
         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.")) {
+        if (!isBuilder(m.getDeclaringClass())) {
             return false;
         }
-        if (m.getParameterCount() == 0 && (m.getName().startsWith("get") || 
m.getName().startsWith("is"))) {
+        if (m.getParameterCount() == 0 && isGetterName(m.getName()) && 
!isFluent(type, m)) {
             return false;
         }
         for (Class<?> p : m.getParameterTypes()) {
@@ -416,6 +476,12 @@ final class JavaDslCompletions {
             return true;
         }
         Class<?> raw = raw(ret);
+        if (!ProcessorDefinition.class.isAssignableFrom(type) && 
!isClause(type)) {
+            // a builder or a definition that is no route EIP: the methods 
going on in the DSL, or ending it in a
+            // predicate or expression (header("x").isEqualTo(..))
+            return raw != null && (isBuilder(raw) || 
ProcessorDefinition.class.isAssignableFrom(raw)
+                    || raw == Predicate.class || raw == Expression.class);
+        }
         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
@@ -424,6 +490,25 @@ final class JavaDslCompletions {
         return raw != null && (ProcessorDefinition.class.isAssignableFrom(raw) 
|| isClause(raw));
     }
 
+    /** getPath, isStreaming: a getter, while get() of the REST DSL or 
isNull() of a builder may be DSL. */
+    private static boolean isGetterName(String name) {
+        int at = name.startsWith("get") ? 3 : name.startsWith("is") ? 2 : -1;
+        return at > 0 && name.length() > at && 
Character.isUpperCase(name.charAt(at));
+    }
+
+    /**
+     * Whether a getter-named method of a builder is DSL nonetheless: 
returning the builder itself, or an is... making a
+     * predicate (header("x").isNotNull()). On the route EIPs the getters stay 
out (getOutputs, getParent).
+     */
+    private static boolean isFluent(Class<?> type, Method m) {
+        if (ProcessorDefinition.class.isAssignableFrom(type) || 
isClause(type)) {
+            return false;
+        }
+        Class<?> ret = m.getReturnType();
+        // isNotNull() makes a predicate, while getExpression() is a getter
+        return m.getName().startsWith("is") && ret == Predicate.class || 
ret.isAssignableFrom(type) && isBuilder(ret);
+    }
+
     private static Class<?> raw(Type type) {
         if (type instanceof Class<?> c) {
             return c;
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 b09321117da3..04f6f02cc418 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
@@ -108,9 +108,12 @@ Press **F4** to enter edit mode, then **Tab** for 
context-aware completion:
   `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
+- In an argument, the chain of the argument (`.filter(header("x").` offers
+  `isEqualTo`, `isNotNull`...); also the REST DSL (`rest("/api").get(..).`),
+  `restConfiguration()` and route templates
+- Light help for hand-written edits: routes in variables and the code of
+  lambdas 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
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
index 479259046812..1556f6273948 100644
--- 
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
@@ -89,8 +89,15 @@ class JavaDslCompletionTest {
         
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();
+        // in an argument: the chain of the argument, from its start
+        c = at("from(\"a\").split(body().");
+        assertThat(c.argument()).isTrue();
+        
assertThat(c.calls()).extracting(JavaChainContext.Call::name).containsExactly("body");
+        c = at("from(\"a\").filter(header(\"x\"), header(\"y\").");
+        assertThat(c.calls().get(0).arguments()).isEqualTo(1);
+        
assertThat(c.calls()).extracting(JavaChainContext.Call::name).containsExactly("header");
+
+        // in a string, in a comment, not after a dot
         assertThat(at("from(\"a.")).isNull();
         assertThat(at("from(\"a\") // .")).isNull();
         assertThat(at("from(\"a\") ")).isNull();
@@ -154,6 +161,33 @@ class JavaDslCompletionTest {
                 "timeoutEnabled");
     }
 
+    @Test
+    void theBuildersInArguments() {
+        assertThat(keys("from(\"a\").split(body().")).contains("tokenize", 
"convertTo", "regexReplaceAll");
+        
assertThat(keys("from(\"a\").filter(header(\"x\").")).contains("isEqualTo", 
"isNotNull", "contains");
+        // a predicate ends the chain: nothing to offer after it
+        
assertThat(keys("from(\"a\").filter(header(\"x\").isNotNull().")).isEmpty();
+        // the internals of the builders are no DSL
+        
assertThat(keys("from(\"a\").split(body().")).doesNotContain("evaluate", 
"getExpression", "matches");
+    }
+
+    @Test
+    void theRestDsl() {
+        assertThat(keys("rest(\"/api\").")).contains("get", "post", 
"description", "consumes");
+        assertThat(keys("rest(\"/api\").get(\"/orders\").")).contains("to", 
"produces", "param", "get");
+        // param() is a sub-builder that endParam() leaves
+        
assertThat(keys("rest(\"/api\").get(\"/orders\").param().")).contains("name", 
"type", "endParam");
+        
assertThat(keys("rest(\"/api\").get(\"/orders\").param().name(\"id\").endParam().")).contains("to");
+        // and a configuration
+        assertThat(keys("restConfiguration().")).contains("component", "port", 
"bindingMode");
+    }
+
+    @Test
+    void aTemplateBeanGoesBackToItsTemplateOnEnd() {
+        
assertThat(keys("routeTemplate(\"t\").templateBean(\"b\").typeClass(\"x\").end().")).contains(
+                "templateParameter", "from");
+    }
+
     @Test
     void anEipIsInsertedWithTheCursorInItsArguments() throws Exception {
         SourceViewer viewer = viewer("""

Reply via email to