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
