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 de1489abdb9d CAMEL-25219: camel-jbang - Tab completes simple 
expressions in the source editor (#27232)
de1489abdb9d is described below

commit de1489abdb9d41d417e23f562dc6c5a3211063d3
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 22:38:27 2026 +0200

    CAMEL-25219: camel-jbang - Tab completes simple expressions in the source 
editor (#27232)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang/camel-tui-source-simple-completion.png   | Bin 0 -> 370850 bytes
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |  10 +-
 .../jbang/core/commands/tui/AutocompletePopup.java |  34 ++-
 .../core/commands/tui/SimpleCompletionContext.java | 141 ++++++++++
 .../jbang/core/commands/tui/SimpleCompletions.java | 286 +++++++++++++++++++
 .../jbang/core/commands/tui/SourceEditAssist.java  |   5 +
 .../dsl/jbang/core/commands/tui/SourceTab.java     |   3 +
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 115 ++++++++
 .../src/main/resources/tui/help/source.md          |  10 +
 .../core/commands/tui/SimpleCompletionTest.java    | 307 +++++++++++++++++++++
 10 files changed, 906 insertions(+), 5 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-completion.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-completion.png
new file mode 100644
index 000000000000..913c6d966f9b
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-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 cb1ec9113de0..a775c0c57404 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,7 +13,7 @@ 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.
+* <<_completion,*Completion*>> -- *Tab* completes components, endpoint options 
and their values, and simple expressions.
 * <<_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
@@ -122,10 +122,18 @@ What can be completed:
   given), and the value of an option after `=`.
 * *YAML routes* -- EIP names, component names (only the components that can 
consume on a `from`, and produce on a
   `to`), endpoint options in a `parameters:` block (the required ones first), 
EIP options, and option values.
+* *Simple expressions*, in YAML, Java and XML routes alike -- after `+${+` the 
functions of the
+  xref:components:languages:simple-language.adoc[Simple] language, with their 
parameters and examples in the details
+  panel, inserted the way they are written (`+${body}+`, `+${date:+`, 
`+${random(+`). After `+${header.+` (and
+  `exchangeProperty.`, `variable.`) the names the file sets or reads, then the 
headers of the components it uses. After a
+  function and a space the operators: comparisons, `&&` and `||` where the EIP 
takes a predicate (`when`, `filter`,
+  `validate`, `onWhen`, ...), chaining (`~>`) and a default value (`?:`) in 
other expressions.
 * *`application.properties`* -- Camel options (`+camel.main.*+`, 
`+camel.component.*+`, ...) and Spring Boot properties
   (`+server.*+`, `+spring.*+`, ...), with their values and `+{{placeholder}}+` 
suggestions. This works even when the
   project is not running.
 
+image::jbang/camel-tui-source-simple-completion.png[After ${header. Tab lists 
the headers the route sets, then the ones of the Kafka component]
+
 == 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/AutocompletePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
index 8de140e54531..245bcb6fcde6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
@@ -45,12 +45,26 @@ import dev.tamboui.widgets.scrollbar.ScrollbarState;
 
 class AutocompletePopup {
 
+    /**
+     * An entry of the popup; insert is the text that goes into the line when 
it differs from the key shown (a simple
+     * function is listed as date:command and inserted as date:), null 
otherwise.
+     */
     record CompletionItem(String key, String description, String type, Object 
defaultValue,
-            boolean deprecated, String deprecationNote, String group, boolean 
required) {
+            boolean deprecated, String deprecationNote, String group, boolean 
required, String insert) {
+
+        CompletionItem(String key, String description, String type, Object 
defaultValue,
+                       boolean deprecated, String deprecationNote, String 
group, boolean required) {
+            this(key, description, type, defaultValue, deprecated, 
deprecationNote, group, required, null);
+        }
 
         CompletionItem(String key, String description, String type, Object 
defaultValue,
                        boolean deprecated, String deprecationNote, String 
group) {
-            this(key, description, type, defaultValue, deprecated, 
deprecationNote, group, false);
+            this(key, description, type, defaultValue, deprecated, 
deprecationNote, group, false, null);
+        }
+
+        /** The text that goes into the line. */
+        String insertText() {
+            return insert != null ? insert : key;
         }
     }
 
@@ -83,6 +97,7 @@ class AutocompletePopup {
     private CompletionItem selectedItem;
     private Rect popupRect;
     private String titlePrefix;
+    private boolean fullKeys;
 
     AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText) {
         this(items, initialPrefix, lineKeyText, false);
@@ -314,7 +329,7 @@ class AutocompletePopup {
             Style keyStyle = ci.deprecated() ? deprecatedStyle : ci.required() 
? boldStyle : normalStyle;
 
             String displayKey = key;
-            if (!key.startsWith("{{") && !key.endsWith(".")) {
+            if (!fullKeys && !key.startsWith("{{") && !key.endsWith(".")) {
                 int lastDot = key.lastIndexOf('.');
                 if (lastDot >= 0) {
                     displayKey = key.substring(lastDot + 1);
@@ -435,7 +450,10 @@ class AutocompletePopup {
             }
             if (selected.description() != null) {
                 lines.add(Line.empty());
-                lines.add(Line.from(Span.styled(selected.description(), 
dimStyle)));
+                // a simple function's description is followed by its 
parameters and examples, each on a line
+                for (String part : selected.description().split("\n", -1)) {
+                    lines.add(part.isEmpty() ? Line.empty() : 
Line.from(Span.styled(part, dimStyle)));
+                }
             }
         }
 
@@ -499,6 +517,14 @@ class AutocompletePopup {
         return null;
     }
 
+    /**
+     * Lists the keys whole: header.name of the simple functions is not a 
property key whose group (up to its last dot)
+     * goes without saying.
+     */
+    void setFullKeys(boolean fullKeys) {
+        this.fullKeys = fullKeys;
+    }
+
     boolean isValueMode() {
         return valueMode;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
new file mode 100644
index 000000000000..c808e6cb9482
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionContext.java
@@ -0,0 +1,141 @@
+/*
+ * 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;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The simple expression the cursor is in, read from the line alone so it 
works on a line being typed, which does not
+ * parse, and the same in YAML, Java and XML (CAMEL-25219): ${hea| is in a 
function, ${header.Camel| in a header name,
+ * ${body} | after a function where an operator goes.
+ *
+ * @param kind    what goes at the cursor
+ * @param prefix  the text before the cursor that the completion replaces
+ * @param closing what follows a chosen name: } after header.x, ']} after 
header['x
+ * @param owners  the words before the expression, nearest first (when, 
filter, setBody, log...), to tell whether the
+ *                expression is a predicate; the language words (simple, 
expression) are left out
+ */
+record SimpleCompletionContext(Kind kind, String prefix, String closing, 
List<String> owners) {
+
+    enum Kind {
+        FUNCTION,
+        HEADER,
+        PROPERTY,
+        VARIABLE,
+        OPERATOR
+    }
+
+    /** How many lines above the cursor are looked at for the EIP the 
expression belongs to. */
+    private static final int OWNER_LINES = 8;
+    private static final Set<String> LANGUAGE_WORDS = Set.of("simple", 
"expression", "language", "camel");
+    private static final Pattern NAME = Pattern.compile(
+            
"(header|headers|in\\.header|in\\.headers|exchangeProperty|variable|variables)(\\.|\\[['\"]?)([\\w.\\-]*)");
+    private static final Pattern FUNCTION = Pattern.compile("[\\w.:\\-]*");
+    private static final Pattern OPERATOR = 
Pattern.compile("\\s+([^\\s'\"$]*)");
+    private static final Pattern WORD = Pattern.compile("[A-Za-z_][\\w-]*");
+
+    /** The context at the cursor, or null when the cursor is not where a 
simple completion goes. */
+    static SimpleCompletionContext 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;
+        }
+        // the ${ still open at the cursor, nested ones included: 
${abs(${header.pr|
+        List<Integer> open = new ArrayList<>();
+        int firstOpen = -1;
+        int lastClose = -1;
+        for (int i = 0; i < col; i++) {
+            char c = line.charAt(i);
+            if (c == '$' && i + 1 < col && line.charAt(i + 1) == '{') {
+                if (firstOpen < 0) {
+                    firstOpen = i;
+                }
+                open.add(i + 2);
+                i++;
+            } else if (c == '}' && !open.isEmpty()) {
+                open.remove(open.size() - 1);
+                lastClose = i;
+            }
+        }
+        if (firstOpen < 0) {
+            return null;
+        }
+        if (!open.isEmpty()) {
+            String inner = line.substring(open.get(open.size() - 1), col);
+            Matcher m = NAME.matcher(inner);
+            if (m.matches()) {
+                Kind kind = switch (m.group(1)) {
+                    case "exchangeProperty" -> Kind.PROPERTY;
+                    case "variable", "variables" -> Kind.VARIABLE;
+                    default -> Kind.HEADER;
+                };
+                String bracket = m.group(2);
+                String closing = ".".equals(bracket) ? "}" : 
bracket.substring(1) + "]}";
+                return new SimpleCompletionContext(kind, m.group(3), closing, 
List.of());
+            }
+            if (FUNCTION.matcher(inner).matches()) {
+                return new SimpleCompletionContext(Kind.FUNCTION, inner, "", 
List.of());
+            }
+            // in the arguments of a function, which take expressions of their 
own
+            return null;
+        }
+        Matcher m = OPERATOR.matcher(line.substring(lastClose + 1, col));
+        if (!m.matches()) {
+            return null;
+        }
+        return new SimpleCompletionContext(Kind.OPERATOR, m.group(1), " ", 
owners(lines, row, firstOpen));
+    }
+
+    /** The words before the expression that starts at col of row, nearest 
first. */
+    private static List<String> owners(List<String> lines, int row, int col) {
+        List<String> owners = new ArrayList<>();
+        for (int r = row; r >= 0 && r > row - OWNER_LINES; r--) {
+            String text = r == row ? lines.get(r).substring(0, col) : 
lines.get(r);
+            Matcher m = WORD.matcher(text);
+            List<String> words = new ArrayList<>();
+            while (m.find()) {
+                if (!LANGUAGE_WORDS.contains(m.group())) {
+                    words.add(m.group());
+                }
+            }
+            for (int i = words.size() - 1; i >= 0; i--) {
+                owners.add(words.get(i));
+            }
+        }
+        return owners;
+    }
+
+    /** Whether the expression is a predicate: the nearest EIP word before it 
takes one. */
+    boolean isPredicate(Set<String> predicateWords, Set<String> eipWords) {
+        for (String w : owners) {
+            if (predicateWords.contains(w)) {
+                return true;
+            }
+            if (eipWords.contains(w)) {
+                return false;
+            }
+        }
+        return false;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
new file mode 100644
index 000000000000..b43640d1d7fc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletions.java
@@ -0,0 +1,286 @@
+/*
+ * 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.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.tooling.model.LanguageModel;
+
+/**
+ * The Tab completions of simple expressions in the Source editor, for YAML, 
Java and XML routes alike (CAMEL-25219):
+ * the functions of the simple language after ${, the header, exchange 
property and variable names after ${header., and
+ * the operators after a function. All from the catalog's simple language 
model, so they follow the Camel version of the
+ * project.
+ */
+final class SimpleCompletions {
+
+    /** The operators after a function of a predicate (when, filter...): 
comparisons, and joining predicates. */
+    private static final Set<String> PREDICATE_OPERATOR_KINDS = 
Set.of("binary", "logical");
+    /** The operators after a function of an expression (setBody, log...): 
chaining, and a default value. */
+    private static final Set<String> EXPRESSION_OPERATOR_KINDS = 
Set.of("chain", "other");
+    private static final int MAX_EXAMPLES = 3;
+    private static final Pattern IDENT = Pattern.compile("[A-Za-z0-9-]+");
+    private static final Pattern SCHEME = 
Pattern.compile("(?<![\\w.+-])([a-z][a-z0-9-]*):[^\\s:]");
+
+    // the predicate and EIP words of the last catalog: reading them goes 
through every model
+    private static volatile Words words;
+
+    private record Words(CamelCatalog catalog, Set<String> predicate, 
Set<String> eip) {
+    }
+
+    private SimpleCompletions() {
+    }
+
+    /** The completions at the context, given the lines of the file being 
edited. */
+    static List<AutocompletePopup.CompletionItem> provide(
+            CamelCatalog catalog, SimpleCompletionContext context, 
List<String> lines) {
+        if (catalog == null || context == null) {
+            return List.of();
+        }
+        LanguageModel simple = catalog.languageModel("simple");
+        if (simple == null) {
+            return List.of();
+        }
+        return switch (context.kind()) {
+            case FUNCTION -> functions(simple);
+            case OPERATOR -> {
+                Words w = words(catalog);
+                yield operators(simple, context.isPredicate(w.predicate(), 
w.eip()));
+            }
+            case HEADER -> names(catalog, lines, "Header", "header", true);
+            case PROPERTY -> names(catalog, lines, "Property", 
"exchangeProperty", false);
+            case VARIABLE -> names(catalog, lines, "Variable", "variable", 
false);
+        };
+    }
+
+    static List<AutocompletePopup.CompletionItem> functions(LanguageModel 
simple) {
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (LanguageModel.LanguageFunctionModel f : simple.getFunctions()) {
+            String insert = insertOf(f);
+            items.add(new AutocompletePopup.CompletionItem(
+                    displayOf(f.getName(), insert), describe(f), 
f.getJavaType(), null,
+                    f.isDeprecated(), f.getDeprecationNote(), f.getGroup(), 
false, insert));
+        }
+        items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    /**
+     * What goes into the line for a function: the whole of it with its } when 
it takes nothing (body}), else the start
+     * of it in the syntax its examples use, up to where the user goes on 
(header. date: substring( bean:). The names of
+     * the catalog are not that syntax: date(command) is written ${date:now}.
+     */
+    static String insertOf(LanguageModel.LanguageFunctionModel f) {
+        String key = f.getName();
+        Matcher m = IDENT.matcher(key);
+        if (!m.lookingAt()) {
+            return key;
+        }
+        String ident = m.group();
+        if (f.getParams().isEmpty() && key.indexOf('(') < 0) {
+            return key + "}";
+        }
+        boolean optional = 
f.getParams().stream().noneMatch(LanguageModel.FunctionParamModel::isRequired);
+        String stem = null;
+        for (String example : f.getExamples()) {
+            if (!example.startsWith("${" + ident)) {
+                continue;
+            }
+            String rest = example.substring(2 + ident.length());
+            if (optional && (rest.startsWith("}") || rest.startsWith("()}"))) {
+                // used as is: ${uuid()}, ${messageHistory()}
+                return ident + rest.substring(0, rest.indexOf('}') + 1);
+            }
+            if (stem == null && !rest.isEmpty() && 
"(:.[".indexOf(rest.charAt(0)) >= 0) {
+                stem = ident + rest.charAt(0);
+            }
+        }
+        if (stem != null) {
+            return stem;
+        }
+        return key.length() > ident.length() ? key.substring(0, ident.length() 
+ 1) : key + "}";
+    }
+
+    /** The name a function is listed under: the catalog name in the syntax it 
is inserted with (date:command). */
+    static String displayOf(String key, String insert) {
+        if (insert.endsWith("}")) {
+            return key;
+        }
+        char delimiter = insert.charAt(insert.length() - 1);
+        int at = insert.length() - 1;
+        if (key.length() <= at || key.charAt(at) == delimiter) {
+            return key;
+        }
+        String rest = key.substring(at + 1);
+        if (key.charAt(at) == '(' && rest.endsWith(")")) {
+            rest = rest.substring(0, rest.length() - 1);
+        }
+        return insert + rest;
+    }
+
+    private static String describe(LanguageModel.LanguageFunctionModel f) {
+        StringBuilder sb = new StringBuilder(f.getDescription() != null ? 
f.getDescription() : "");
+        if (!f.getParams().isEmpty()) {
+            sb.append("\n\nParameters:");
+            for (LanguageModel.FunctionParamModel p : f.getParams()) {
+                sb.append("\n  ").append(p.getName());
+                if (p.isRequired()) {
+                    sb.append(" (required)");
+                } else if (p.getDefaultValue() != null) {
+                    sb.append(" (default 
").append(p.getDefaultValue()).append(')');
+                }
+                if (p.getDescription() != null) {
+                    sb.append(": ").append(p.getDescription());
+                }
+            }
+        }
+        appendExamples(sb, f.getExamples());
+        return sb.toString();
+    }
+
+    private static void appendExamples(StringBuilder sb, List<String> 
examples) {
+        if (!examples.isEmpty()) {
+            sb.append("\n\nExamples:");
+            examples.stream().limit(MAX_EXAMPLES).forEach(e -> sb.append("\n  
").append(e));
+        }
+    }
+
+    static List<AutocompletePopup.CompletionItem> operators(LanguageModel 
simple, boolean predicate) {
+        Set<String> kinds = predicate ? PREDICATE_OPERATOR_KINDS : 
EXPRESSION_OPERATOR_KINDS;
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (LanguageModel.LanguageOperatorModel op : simple.getOperators()) {
+            // ! goes before a function, not after one
+            if (!kinds.contains(op.getOperatorKind()) || 
"!".equals(op.getName())) {
+                continue;
+            }
+            StringBuilder sb = new StringBuilder(op.getDescription() != null ? 
op.getDescription() : "");
+            if (op.getOperatorSyntax() != null) {
+                sb.append("\n\nSyntax: ").append(op.getOperatorSyntax());
+            }
+            appendExamples(sb, op.getExamples());
+            items.add(new AutocompletePopup.CompletionItem(
+                    op.getName(), sb.toString(), null, null, 
op.isDeprecated(), op.getDeprecationNote(),
+                    op.getDisplayName(), false));
+        }
+        return items;
+    }
+
+    /**
+     * The header (exchange property, variable) names of the file: the ones it 
sets or reads, then for headers the ones
+     * of the components it uses, from the catalog.
+     */
+    static List<AutocompletePopup.CompletionItem> names(
+            CamelCatalog catalog, List<String> lines, String setWord, String 
readWord, boolean componentHeaders) {
+        String text = String.join("\n", lines);
+        Map<String, AutocompletePopup.CompletionItem> items = new 
LinkedHashMap<>();
+        // Java: setHeader("foo", ...; YAML and XML: setHeader with a name: 
foo or name="foo" below or after it
+        for (String verb : List.of("set" + setWord, "remove" + setWord)) {
+            addNames(items, text, Pattern.compile("\\b" + verb + 
"\\s*\\(\\s*\"([^\"]+)\""), "Set in this file");
+            addNames(items, text,
+                    Pattern.compile(
+                            "\\b" + verb + 
"\\b[^(]{0,300}?\\bname\\s*[:=]\\s*['\"]?([\\w.\\-]+)"),
+                    "Set in this file");
+        }
+        addNames(items, text, Pattern.compile("\\$\\{(?:in\\.)?" + readWord + 
"s?(?:\\.|\\[['\"]?)([\\w\\-]+)"),
+                "Used in this file");
+        addNames(items, text, Pattern.compile("\\b" + readWord + 
"\\s*\\(\\s*\"([^\"]+)\""), "Used in this file");
+        if (componentHeaders) {
+            Set<String> components = new 
HashSet<>(catalog.findComponentNames());
+            Matcher m = SCHEME.matcher(text);
+            Set<String> seen = new HashSet<>();
+            while (m.find()) {
+                String scheme = m.group(1);
+                if (!components.contains(scheme) || !seen.add(scheme)) {
+                    continue;
+                }
+                ComponentModel model = catalog.componentModel(scheme);
+                if (model == null) {
+                    continue;
+                }
+                for (ComponentModel.EndpointHeaderModel h : 
model.getEndpointHeaders()) {
+                    items.putIfAbsent(h.getName(), new 
AutocompletePopup.CompletionItem(
+                            h.getName(), h.getDescription(), h.getJavaType(), 
h.getDefaultValue(),
+                            h.isDeprecated(), h.getDeprecationNote(), scheme, 
false));
+                }
+            }
+        }
+        return new ArrayList<>(items.values());
+    }
+
+    private static void addNames(
+            Map<String, AutocompletePopup.CompletionItem> items, String text, 
Pattern pattern, String description) {
+        Matcher m = pattern.matcher(text);
+        while (m.find()) {
+            String name = m.group(1);
+            items.putIfAbsent(name, new AutocompletePopup.CompletionItem(
+                    name, description, null, null, false, null, null, false));
+        }
+    }
+
+    private static Words words(CamelCatalog catalog) {
+        Words w = words;
+        if (w == null || w.catalog() != catalog) {
+            w = new Words(catalog, predicateWords(catalog), eipWords(catalog));
+            words = w;
+        }
+        return w;
+    }
+
+    /**
+     * The words that make the expression after them a predicate: the EIPs 
whose expression is one (when, filter,
+     * validate...) and the predicate options of the others (onWhen, handled, 
retryWhile, completionPredicate...).
+     */
+    static Set<String> predicateWords(CamelCatalog catalog) {
+        Set<String> words = new HashSet<>();
+        for (String name : catalog.findModelNames()) {
+            EipModel model = catalog.eipModel(name);
+            if (model == null) {
+                continue;
+            }
+            for (BaseOptionModel option : model.getOptions()) {
+                if (option.isAsPredicate()) {
+                    words.add("expression".equals(option.getName()) ? name : 
option.getName());
+                }
+            }
+        }
+        return words;
+    }
+
+    /** The EIP names: the nearest one before an expression tells what it 
belongs to. */
+    static Set<String> eipWords(CamelCatalog catalog) {
+        Set<String> words = new HashSet<>();
+        for (String name : catalog.findModelNames()) {
+            EipModel model = catalog.eipModel(name);
+            if (model != null && model.getLabel() != null && 
model.getLabel().contains("eip")) {
+                words.add(name);
+            }
+        }
+        return words;
+    }
+}
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 34e1b800e9c6..062f631d5e11 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
@@ -1011,6 +1011,11 @@ final class SourceEditAssist {
         return items;
     }
 
+    /** The completions of the simple expression at the cursor (CAMEL-25219). 
*/
+    List<AutocompletePopup.CompletionItem> 
provideSimpleCompletions(SimpleCompletionContext context, List<String> lines) {
+        return SimpleCompletions.provide(validationCatalog(), context, lines);
+    }
+
     List<AutocompletePopup.CompletionItem> 
provideComponentNameCompletions(String role) {
         CamelCatalog catalog = validationCatalog();
         if (catalog == null) {
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 7c53bc357ec9..05a641c5e5d1 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
@@ -845,6 +845,9 @@ class SourceTab extends AbstractTab {
         sourceViewer.setAskAi(ctx.askAiCallback);
         String name = filePath.getFileName().toString();
         sourceViewer.setUriCompletion(!routeFile ? null : 
name.endsWith(".java") ? "java" : "xml");
+        // simple expressions in every route file: YAML, Java and XML 
(CAMEL-25219)
+        sourceViewer.setSimpleCompletion(
+                routeFile || isCamelSourceFile(filePath) ? 
assist::provideSimpleCompletions : null);
         if (isCamelSourceFile(filePath)) {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
             sourceViewer.setDeprecatedLineScanner(null);
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 2ebc6e3d5ae1..229f7d4f56f3 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
@@ -29,6 +29,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 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.IntConsumer;
 
@@ -214,6 +215,8 @@ class SourceViewer {
     private String uriCompletion;
     /** The completion being chosen in a uri: its row, the column its prefix 
ends at, the prefix and suffix. */
     private UriCompletion pendingUriCompletion;
+    private BiFunction<SimpleCompletionContext, List<String>, 
List<AutocompletePopup.CompletionItem>> simpleCompletion;
+    private SimpleCompletion pendingSimpleCompletion;
     private List<String> routeProblems = List.of();
     private List<String> validationErrors;
     private int validationErrorScroll;
@@ -407,6 +410,19 @@ class SourceViewer {
     private record UriCompletion(int row, int endCol, String prefix, String 
suffix) {
     }
 
+    /**
+     * Tab completion in the simple expressions of a YAML, Java or XML route 
file (CAMEL-25219): the functions after ${,
+     * the header names after ${header., the operators after a function. Given 
the context at the cursor and the lines
+     * of the file; null for a file without routes.
+     */
+    void setSimpleCompletion(
+            BiFunction<SimpleCompletionContext, List<String>, 
List<AutocompletePopup.CompletionItem>> provider) {
+        this.simpleCompletion = provider;
+    }
+
+    private record SimpleCompletion(int row, int endCol, 
SimpleCompletionContext 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;
@@ -425,6 +441,7 @@ class SourceViewer {
         simpleValidator = null;
         routeValidator = null;
         uriCompletion = null;
+        simpleCompletion = null;
     }
 
     void reset() {
@@ -466,6 +483,7 @@ class SourceViewer {
         simpleValidator = null;
         routeValidator = null;
         uriCompletion = null;
+        simpleCompletion = null;
     }
 
     boolean isMarkdownMode() {
@@ -1176,6 +1194,10 @@ class SourceViewer {
 
     private void openAutocomplete() {
         pendingUriCompletion = null;
+        pendingSimpleCompletion = null;
+        if (openSimpleAutocomplete()) {
+            return;
+        }
         if (isCamelYamlFile()) {
             openYamlAutocomplete();
         } else if (uriCompletion != null) {
@@ -1246,6 +1268,95 @@ class SourceViewer {
         }
     }
 
+    /**
+     * The completion of the simple expression the cursor is in, read from the 
line alone like the uris: true when the
+     * cursor is in one, so no other completion is tried, also when nothing 
matches there.
+     */
+    private boolean openSimpleAutocomplete() {
+        if (simpleCompletion == null) {
+            return false;
+        }
+        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));
+        }
+        SimpleCompletionContext c = SimpleCompletionContext.at(all, row, col);
+        if (c == null) {
+            return false;
+        }
+        List<AutocompletePopup.CompletionItem> items = 
simpleCompletion.apply(c, all);
+        if (items != null && !items.isEmpty()) {
+            // not in value mode, which puts a popup with a long description 
over the line being edited
+            autocompletePopup = new AutocompletePopup(items, c.prefix(), 
c.prefix(), false);
+            autocompletePopup.setFullKeys(true);
+            autocompletePopup.setTitlePrefix(switch (c.kind()) {
+                case FUNCTION -> "Simple functions";
+                case HEADER -> "Headers";
+                case PROPERTY -> "Exchange properties";
+                case VARIABLE -> "Variables";
+                case OPERATOR -> "Operators";
+            });
+            pendingSimpleCompletion = new SimpleCompletion(row, col, c);
+        }
+        return true;
+    }
+
+    /**
+     * Replaces the prefix the simple completion was opened on with the chosen 
item, and what closes it (the } of a
+     * function or name, the space after an operator) unless that is there 
already. A chosen header. or
+     * exchangeProperty. opens the names right away.
+     */
+    private void insertSimpleCompletion(AutocompletePopup.CompletionItem item) 
{
+        SimpleCompletion sc = pendingSimpleCompletion;
+        pendingSimpleCompletion = null;
+        if (editState.cursorRow() != sc.row()) {
+            return;
+        }
+        SimpleCompletionContext c = sc.context();
+        int col = editState.cursorCol();
+        for (; col < sc.endCol(); col++) {
+            editState.moveCursorRight();
+        }
+        for (; col > sc.endCol(); col--) {
+            editState.moveCursorLeft();
+        }
+        for (int i = 0; i < c.prefix().length(); i++) {
+            editState.deleteBackward();
+        }
+        // Tab in the middle of a word replaces all of it
+        String line = editState.getLine(sc.row());
+        int from = editState.cursorCol();
+        int end = from;
+        boolean operator = c.kind() == SimpleCompletionContext.Kind.OPERATOR;
+        while (end < line.length() && isSimpleWordChar(line.charAt(end), 
operator)) {
+            end++;
+        }
+        for (int i = from; i < end; i++) {
+            editState.deleteForward();
+        }
+        String text = item.insertText();
+        String closing = c.closing();
+        if (c.kind() == SimpleCompletionContext.Kind.FUNCTION && 
text.endsWith("}")) {
+            text = text.substring(0, text.length() - 1);
+            closing = "}";
+        }
+        boolean hasClosing = !closing.isEmpty() && line.startsWith(closing, 
end);
+        editState.insert(text + (hasClosing ? "" : closing));
+        if (c.kind() == SimpleCompletionContext.Kind.FUNCTION && 
(text.endsWith(".") || text.endsWith("["))) {
+            openAutocomplete();
+        }
+    }
+
+    /** A character of the function, name or operator being completed. */
+    private static boolean isSimpleWordChar(char c, boolean operator) {
+        if (operator) {
+            return !Character.isWhitespace(c) && c != '$' && c != '\'' && c != 
'"';
+        }
+        return Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == 
'.' || c == ':';
+    }
+
     /** 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;
@@ -1456,6 +1567,10 @@ class SourceViewer {
             insertUriCompletion(item);
             return;
         }
+        if (pendingSimpleCompletion != null) {
+            insertSimpleCompletion(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 c563f8f24b66..4e39454073f4 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
@@ -100,6 +100,16 @@ 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 `=`
 
+**Simple expressions (YAML, Java and XML routes):**
+- After `${`, Tab lists the functions of the simple language, with their
+  parameters and examples; the chosen one is inserted as it is written
+  (`${body}`, `${date:`, `${random(`)
+- After `${header.` (also `exchangeProperty.` and `variable.`), Tab lists the
+  names the file sets or reads, then the headers of the components it uses
+- After a function and a space, Tab lists the operators: comparisons and
+  `&&` `||` where the EIP takes a predicate (`when`, `filter`, `validate`,
+  `onWhen`...), chaining (`~>`) and the default value (`?:`) elsewhere
+
 Use **Up/Down** to navigate, **Enter** to accept, **Esc** to dismiss, and
 type to filter the completion list (an exact or prefix match comes first).
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionTest.java
new file mode 100644
index 000000000000..e01d45e42e46
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SimpleCompletionTest.java
@@ -0,0 +1,307 @@
+/*
+ * 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.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+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.tooling.model.LanguageModel;
+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 in the simple expressions of YAML, Java and XML route files 
(CAMEL-25219).
+ */
+class SimpleCompletionTest {
+
+    private static CamelCatalog catalog;
+
+    @TempDir
+    Path tempDir;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+    }
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+    }
+
+    private static SimpleCompletionContext at(String line) {
+        return SimpleCompletionContext.at(List.of(line), 0, line.length());
+    }
+
+    @Test
+    void theFunctionTheCursorIsIn() {
+        SimpleCompletionContext c = at("        .setBody(simple(\"Hello 
${hea");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.FUNCTION);
+        assertThat(c.prefix()).isEqualTo("hea");
+
+        assertThat(at("      simple: \"${").prefix()).isEmpty();
+        // nested in the argument of another function
+        assertThat(at("${abs(${bo").prefix()).isEqualTo("bo");
+        // in the arguments of a function, after a closed one, with no ${ at 
all
+        assertThat(at("${substring(1, ")).isNull();
+        assertThat(at("${body}")).isNull();
+        assertThat(at("        .log(\"Hello")).isNull();
+    }
+
+    @Test
+    void theNameTheCursorIsIn() {
+        SimpleCompletionContext c = at("${header.Camel");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.HEADER);
+        assertThat(c.prefix()).isEqualTo("Camel");
+        assertThat(c.closing()).isEqualTo("}");
+
+        c = at("${headers['ord");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.HEADER);
+        assertThat(c.prefix()).isEqualTo("ord");
+        assertThat(c.closing()).isEqualTo("']}");
+
+        
assertThat(at("${exchangeProperty.").kind()).isEqualTo(SimpleCompletionContext.Kind.PROPERTY);
+        
assertThat(at("${variable.x").kind()).isEqualTo(SimpleCompletionContext.Kind.VARIABLE);
+    }
+
+    @Test
+    void theOperatorAfterAFunction() {
+        SimpleCompletionContext c = at("        .filter(simple(\"${header.foo} 
con");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.OPERATOR);
+        assertThat(c.prefix()).isEqualTo("con");
+        assertThat(c.owners()).startsWith("filter");
+
+        // right after the } or past the string
+        assertThat(at("${header.foo}")).isNull();
+        assertThat(at("        .log(\"${body}\" + ")).isNull();
+    }
+
+    @Test
+    void aPredicateIsToldByTheEipBeforeIt() {
+        assertThat(predicate(List.of("        .filter(simple(\"${body} 
"))).isTrue();
+        assertThat(predicate(List.of("        
.choice().when().simple(\"${body} "))).isTrue();
+        assertThat(predicate(List.of("        .setBody(simple(\"${body} 
"))).isFalse();
+        assertThat(predicate(List.of("        .log(\"${body} "))).isFalse();
+        // onException options
+        assertThat(predicate(List.of("        
onException(Exception.class).handled(simple(\"${body} "))).isTrue();
+        // YAML and XML: the EIP is on a line above
+        assertThat(predicate(List.of("- filter:", "    simple: \"${body} 
"))).isTrue();
+        assertThat(predicate(List.of("- filter:", "    expression:", "      
simple:", "        expression: \"${body} ")))
+                .isTrue();
+        assertThat(predicate(List.of("- log:", "    message: \"${body} 
"))).isFalse();
+        assertThat(predicate(List.of("<when>", "  <simple>${body} 
"))).isTrue();
+        assertThat(predicate(List.of("<setBody>", "  <simple>${body} 
"))).isFalse();
+    }
+
+    private static boolean predicate(List<String> lines) {
+        int row = lines.size() - 1;
+        SimpleCompletionContext c = SimpleCompletionContext.at(lines, row, 
lines.get(row).length());
+        List<String> ops = SimpleCompletions.provide(catalog, c, 
lines).stream()
+                .map(AutocompletePopup.CompletionItem::key).toList();
+        boolean predicate = ops.contains("==");
+        // the operators of an expression are never mixed with the ones of a 
predicate
+        assertThat(ops.contains("~>")).isNotEqualTo(predicate);
+        return predicate;
+    }
+
+    @Test
+    void functionsAreInsertedInTheirSyntax() {
+        LanguageModel simple = catalog.languageModel("simple");
+        Map<String, AutocompletePopup.CompletionItem> items = 
SimpleCompletions.functions(simple).stream()
+                
.collect(Collectors.toMap(AutocompletePopup.CompletionItem::key, 
Function.identity()));
+
+        assertThat(items.get("body").insertText()).isEqualTo("body}");
+        assertThat(items.get("header.name").insertText()).isEqualTo("header.");
+        
assertThat(items.get("random(min,max)").insertText()).isEqualTo("random(");
+        assertThat(items.get("uuid(type)").insertText()).isEqualTo("uuid()}");
+        
assertThat(items.get("properties:key:default").insertText()).isEqualTo("properties:");
+        // the catalog names date(command) and bean(name.method), written 
${date:now} and ${bean:foo}
+        assertThat(items.get("date:command").insertText()).isEqualTo("date:");
+        
assertThat(items.get("bean:name.method").insertText()).isEqualTo("bean:");
+        assertThat(items).doesNotContainKey("date(command)");
+        // the details show the parameters and examples
+        
assertThat(items.get("random(min,max)").description()).contains("Parameters:", 
"min (required)", "Examples:");
+    }
+
+    @Test
+    void functionsAreListedWithTheirWholeName() {
+        AutocompletePopup popup = new AutocompletePopup(
+                SimpleCompletions.functions(catalog.languageModel("simple")), 
"header.", "header.", false);
+        popup.setFullKeys(true);
+        Rect area = new Rect(0, 0, 120, 30);
+        Buffer buffer = Buffer.empty(area);
+        popup.render(Frame.forTesting(buffer), area, 2, 10);
+
+        // not name, as a property key in its group would be shown
+        assertThat(HealthTabRenderTest.bufferToString(buffer)).contains("   
header.name ");
+    }
+
+    @Test
+    void theHeaderNamesOfTheFile() {
+        List<String> lines = List.of(
+                "from(\"kafka:orders\")",
+                "    .setHeader(\"orderId\", simple(\"${body}\"))",
+                "    .filter(simple(\"${header.priority} == 'high'\"))",
+                "- setHeader:",
+                "    name: region",
+                "<setHeader id=\"x\" name=\"customer\">");
+        List<String> names = SimpleCompletions.names(catalog, lines, "Header", 
"header", true).stream()
+                .map(AutocompletePopup.CompletionItem::key).toList();
+        // the ones the file sets or reads first, then the ones of the kafka 
component
+        assertThat(names).startsWith("orderId", "region", "customer", 
"priority");
+        assertThat(names).contains("CamelKafkaKey");
+    }
+
+    @Test
+    void aJavaFunction() throws Exception {
+        SourceViewer viewer = viewer("MyRoute.java", """
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .setHeader("orderId", constant(1))
+                            .filter(simple("${headerA"));
+                    }
+                }
+                """);
+        cursorAt(viewer, 4, "\"));");
+        tab(viewer);
+        enter(viewer);
+        assertThat(line(viewer, 4)).isEqualTo("            
.filter(simple(\"${headerAs(\"));");
+    }
+
+    @Test
+    void aJavaHeaderThenAnOperator() throws Exception {
+        // header. opens the header names right away
+        SourceViewer v2 = viewer("MyRoute.java", """
+                public class MyRoute extends RouteBuilder {
+                    public void configure() {
+                        from("timer:tick")
+                            .setHeader("orderId", constant(1))
+                            .filter(simple("${heade"));
+                    }
+                }
+                """);
+        cursorAt(v2, 4, "\"));");
+        tab(v2);
+        type(v2, "r.");
+        enter(v2);
+        assertThat(line(v2, 4)).isEqualTo("            
.filter(simple(\"${header.\"));");
+        type(v2, "ord");
+        enter(v2);
+        assertThat(line(v2, 4)).isEqualTo("            
.filter(simple(\"${header.orderId}\"));");
+
+        // then the operators of a predicate
+        type(v2, " ");
+        tab(v2);
+        type(v2, "contains");
+        enter(v2);
+        assertThat(line(v2, 4)).isEqualTo("            
.filter(simple(\"${header.orderId} contains \"));");
+    }
+
+    @Test
+    void aYamlFunction() throws Exception {
+        SourceViewer viewer = viewer("route.camel.yaml", """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - setBody:
+                          simple: "Hello ${bod"
+                """);
+        cursorAt(viewer, 4, "\"");
+        tab(viewer);
+        enter(viewer);
+        assertThat(line(viewer, 4)).isEqualTo("          simple: \"Hello 
${body}\"");
+    }
+
+    @Test
+    void anXmlFunctionKeepsTheClosingBrace() throws Exception {
+        SourceViewer viewer = viewer("routes.xml", """
+                <routes xmlns="http://camel.apache.org/schema/xml-io";>
+                    <route>
+                        <from uri="timer:tick"/>
+                        <log message="Got ${exchangeI}"/>
+                    </route>
+                </routes>
+                """);
+        cursorAt(viewer, 3, "}\"/>");
+        tab(viewer);
+        enter(viewer);
+        assertThat(line(viewer, 3)).isEqualTo("        <log message=\"Got 
${exchangeId}\"/>");
+    }
+
+    private SourceViewer viewer(String fileName, String src) throws Exception {
+        Path file = tempDir.resolve(fileName);
+        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.setSimpleCompletion(assist::provideSimpleCompletions);
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+        return viewer;
+    }
+
+    /** Puts the cursor on the line, before the given text at its end. */
+    private static void cursorAt(SourceViewer viewer, int row, String tail) {
+        for (int i = 0; i < row; i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, 
KeyModifiers.NONE));
+        }
+        viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+        for (int i = 0; i < tail.length(); i++) {
+            viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, 
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