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

davsclaus pushed a commit to branch feature/CAMEL-25208-code-assist
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d20c07ef8e84cf80cff37dbcbc7dfd87bf5b9058
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 23:06:33 2026 +0200

    CAMEL-25208: camel-jbang - expressions on their own line; completion 
replaces the whole name
    
    Found testing in the TUI:
    - An expression has the line of its step in the model; its text is often
      on a later line: the <simple> element under a <when>, a simple("...")
      of a Java call written over several lines. Its problems and quick doc
      are now on the line its text is on.
    - Tab after siz in ?siz=100 wrote size==100: the = (or the : after a
      component) that follows is not doubled, and Tab in the middle of a name
      replaces all of it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/ai/RouteAssist.java    |  4 +--
 .../dsl/jbang/core/commands/ai/RouteNodes.java     | 42 ++++++++++++++++++++++
 .../jbang/core/commands/ai/RouteAssistTest.java    | 16 ++++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 22 +++++++++++-
 .../commands/tui/JavaStringCompletionTest.java     | 32 +++++++++++++++++
 .../commands/tui/SourceRouteValidationTest.java    |  2 +-
 6 files changed, 113 insertions(+), 5 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssist.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssist.java
index ef94930d4a36..06a1d1a0944a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssist.java
@@ -110,7 +110,7 @@ public final class RouteAssist {
                 answer.add(new Diagnostic(u.line(), Severity.ERROR, 
u.reason()));
             }
         }
-        List<RouteNodes.Node> nodes = RouteNodes.of(model, catalog);
+        List<RouteNodes.Node> nodes = 
RouteNodes.withExpressionLines(RouteNodes.of(model, catalog), content);
         Set<String> consumed = checkConsumers && directory != null
                 ? EndpointConsumerChecks.consumed(directory, fileName) : null;
         for (String msg : ModelChecks.check(model, nodes, content, catalog, 
consumed)) {
@@ -132,7 +132,7 @@ public final class RouteAssist {
     public static List<RouteNodes.Node> nodes(
             String fileName, String content, CamelCatalog catalog, Map<String, 
Supplier<String>> javaSources) {
         RouteModel model = RouteModel.read(fileName, content, catalog, 
javaSources);
-        return model != null ? RouteNodes.of(model, catalog) : List.of();
+        return model != null ? 
RouteNodes.withExpressionLines(RouteNodes.of(model, catalog), content) : 
List.of();
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteNodes.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteNodes.java
index baaaae1dc7dc..df9301f737b8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteNodes.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteNodes.java
@@ -128,6 +128,48 @@ public final class RouteNodes {
         return answer;
     }
 
+    /**
+     * The nodes with each expression on the line its text is on, which is 
often not the line of its step: the
+     * {@code <simple>} element under a {@code <when>}, the {@code 
simple("...")} of a Java call written over several
+     * lines. The first line at or after the step's that has the start of the 
text (or, in XML, the element of its
+     * language) is taken; the step's line stays when there is none.
+     */
+    public static List<Node> withExpressionLines(List<Node> nodes, String 
content) {
+        String[] lines = content.split("\n", -1);
+        List<Node> answer = new ArrayList<>(nodes.size());
+        for (Node n : nodes) {
+            if (n.kind() == Kind.EXPRESSION && n.line() > 0 && n.text() != 
null && !n.text().isBlank()) {
+                int found = expressionLine(lines, n);
+                if (found > 0 && found != n.line()) {
+                    n = new Node(
+                            n.kind(), found, n.eip(), n.uri(), n.language(), 
n.text(), n.option(), n.predicate(),
+                            n.parents());
+                }
+            }
+            answer.add(n);
+        }
+        answer.sort(Comparator.comparingInt(Node::line));
+        return answer;
+    }
+
+    private static int expressionLine(String[] lines, Node n) {
+        String text = n.text().strip();
+        // the start of the text up to what a DSL may write escaped (a quote, 
a < or & in XML)
+        int cut = 0;
+        while (cut < text.length() && cut < 20 && 
"\"'<>&\\\n".indexOf(text.charAt(cut)) < 0) {
+            cut++;
+        }
+        String start = text.substring(0, cut);
+        String tag = "<" + n.language();
+        for (int i = n.line() - 1; i < lines.length && i < n.line() - 1 + 15; 
i++) {
+            if (start.length() >= 3 && lines[i].contains(start) || 
lines[i].contains(tag + ">")
+                    || lines[i].contains(tag + " ")) {
+                return i + 1;
+            }
+        }
+        return 0;
+    }
+
     /** The nodes on a line. */
     public static List<Node> at(List<Node> nodes, int line) {
         List<Node> answer = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssistTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssistTest.java
index e0a57529fd40..015e7ac22f5c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssistTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RouteAssistTest.java
@@ -150,6 +150,19 @@ class RouteAssistTest {
         assertThat(errors.get(0).message()).startsWith("Simple syntax error");
     }
 
+    @Test
+    void aSimpleExpressionOnTheLineAfterItsStep() {
+        String src = java("""
+                        from("timer:tick")
+                            .filter(
+                                simple("${header.foo} =="))
+                            .to("seda:out");
+                """);
+        List<Diagnostic> errors = errors("MyRoute.java", src);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0).line()).isEqualTo(8);
+    }
+
     @Test
     void aSimpleExpressionOfTheFluentBuilder() {
         String src = java("""
@@ -306,7 +319,8 @@ class RouteAssistTest {
                 """;
         List<Diagnostic> errors = errors("camel.xml", xml);
         assertThat(errors).hasSize(1);
-        assertThat(errors.get(0).line()).isEqualTo(4);
+        // the line of the <simple> element, not of the filter it belongs to
+        assertThat(errors.get(0).line()).isEqualTo(5);
         assertThat(errors.get(0).message()).startsWith("Simple syntax error");
     }
 
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 e3a1dd6e4650..c061b4f7181e 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
@@ -1098,7 +1098,27 @@ class SourceViewer {
         for (int i = 0; i < jc.prefix().length(); i++) {
             editState.deleteBackward();
         }
-        editState.insert(item.key() + jc.suffix());
+        // Tab in the middle of a word replaces all of it: the rest of the 
name after the cursor goes too
+        String line = editState.getLine(jc.row());
+        int from = editState.cursorCol();
+        int end = from;
+        while (end < line.length() && isUriWordChar(line.charAt(end), 
jc.suffix())) {
+            end++;
+        }
+        for (int i = from; i < end; i++) {
+            editState.deleteForward();
+        }
+        // the : or = the name is followed by is not doubled when it is there 
already
+        boolean hasSuffix = !jc.suffix().isEmpty() && 
line.startsWith(jc.suffix(), end);
+        editState.insert(item.key() + (hasSuffix ? "" : jc.suffix()));
+    }
+
+    /** A character of the name or value being completed: up to the : of a 
component, the = of an option, the & or ". */
+    private static boolean isUriWordChar(char c, String suffix) {
+        if (suffix.isEmpty()) {
+            return c != '&' && c != '"' && c != '\\';
+        }
+        return Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == 
'.';
     }
 
     private void openPropertiesAutocomplete() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
index ad7a28b4e0a2..5f461b288d54 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java
@@ -144,6 +144,38 @@ class JavaStringCompletionTest {
         assertThat(line(viewer, 3)).isEqualTo("            
.to(\"file:out?fileExist=Append\");");
     }
 
+    @Test
+    void aNameInTheMiddleOfTheUriIsReplacedWhole() throws Exception {
+        // found in the live TUI: Tab after siz in ?siz=100 wrote size==100
+        SourceViewer viewer = viewer("""
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .to("seda:orders?siz=100&blockWhenFull=true");
+                    }
+                }
+                """);
+        cursorAt(viewer, 3, "=100&blockWhenFull=true\");");
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE));
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
+        assertThat(line(viewer, 3)).isEqualTo("            
.to(\"seda:orders?size=100&blockWhenFull=true\");");
+
+        // Tab with the cursor inside a name: the rest of it is replaced as 
well
+        viewer = viewer("""
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .to("sedx:orders");
+                    }
+                }
+                """);
+        cursorAt(viewer, 3, "x:orders\");");
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE));
+        type(viewer, "da");
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
+        assertThat(line(viewer, 3)).isEqualTo("            
.to(\"seda:orders\");");
+    }
+
     @Test
     void noCompletionOutsideAnEndpoint() throws Exception {
         SourceViewer viewer = viewer("""
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
index bbde4724d29b..dd56ee0d9d9f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java
@@ -90,7 +90,7 @@ class SourceRouteValidationTest {
                 </routes>
                 """);
         assertThat(errors).hasSize(1);
-        assertThat(errors.get(0)).startsWith("Line 4: Simple syntax error");
+        assertThat(errors.get(0)).startsWith("Line 5: Simple syntax error");
     }
 
     @Test

Reply via email to