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 9008dd64be28 CAMEL-25242: camel-jbang - source editor: complete the 
arguments of simple functions (#27239)
9008dd64be28 is described below

commit 9008dd64be28137a6c543ac47bafdf9d0d59e791
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 08:05:03 2026 +0200

    CAMEL-25242: camel-jbang - source editor: complete the arguments of simple 
functions (#27239)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel-tui-source-simple-bean-completion.png    | Bin 0 -> 240079 bytes
 .../ROOT/pages/camel-jbang-tui-source-editor.adoc  |   5 ++
 .../dsl/jbang/core/commands/tui/ProjectBeans.java  |   9 ++
 .../core/commands/tui/SimpleCompletionContext.java |  52 +++++++++++-
 .../jbang/core/commands/tui/SimpleCompletions.java |  68 +++++++++++++++
 .../jbang/core/commands/tui/SourceEditAssist.java  |  25 +++++-
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  18 +++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  31 +++++--
 .../src/main/resources/tui/help/source.md          |   4 +
 .../core/commands/tui/SimpleCompletionTest.java    |  93 +++++++++++++++++++++
 10 files changed, 291 insertions(+), 14 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-bean-completion.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-bean-completion.png
new file mode 100644
index 000000000000..5a3d8c762edc
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-simple-bean-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 821b9e109982..f0b0721d5902 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
@@ -144,6 +144,9 @@ What can be completed:
   `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.
+  In the arguments of a function: after `+${date:+` the commands (`now`, 
`exchangeCreated`, `header.`...), after the
+  next `:` common date patterns, and the time zones of 
`+${date-with-timezone(..)}+`; after `+${bean:+` the beans
+  your project declares; after `+${properties:+` the keys of its `.properties` 
files, with their values.
 * *`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.
@@ -152,6 +155,8 @@ image::jbang/camel-tui-source-xml-completion.png[In the 
start tag of log, Tab li
 
 image::jbang/camel-tui-source-simple-completion.png[After ${header. Tab lists 
the headers the route sets, then the ones of the Kafka component]
 
+image::jbang/camel-tui-source-simple-bean-completion.png[After ${bean: Tab 
lists the beans the project declares]
+
 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]
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
index 676d00790fff..648e966ab1d6 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectBeans.java
@@ -20,6 +20,8 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -75,6 +77,13 @@ final class ProjectBeans {
         this.byClass = byClass;
     }
 
+    /** The beans the files declare by name, sorted by name. */
+    List<Location> beans() {
+        List<Location> found = new ArrayList<>(byName.values());
+        found.sort(Comparator.comparing(Location::label, 
String.CASE_INSENSITIVE_ORDER));
+        return found;
+    }
+
     /** Reads the beans and classes the files declare. */
     static ProjectBeans scan(List<Path> files) {
         Map<String, Location> byName = new HashMap<>();
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
index 21b1768896d6..70340c08cb0e 100644
--- 
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
@@ -40,7 +40,17 @@ record SimpleCompletionContext(Kind kind, String prefix, 
String closing, List<St
         HEADER,
         PROPERTY,
         VARIABLE,
-        OPERATOR
+        OPERATOR,
+        /** The command of ${date:..} or ${date-with-timezone(..)}: now, 
exchangeCreated, header.xxx... */
+        DATE_COMMAND,
+        /** The pattern after the command: yyyy-MM-dd... */
+        DATE_PATTERN,
+        /** The time zone of ${date-with-timezone(command:zone:pattern)} */
+        TIME_ZONE,
+        /** The bean of ${bean:name.method} */
+        BEAN,
+        /** The property key of ${properties:key} and ${propertiesExist:key} */
+        PROPERTY_KEY
     }
 
     /** How many lines above the cursor are looked at for the EIP the 
expression belongs to. */
@@ -49,6 +59,14 @@ record SimpleCompletionContext(Kind kind, String prefix, 
String closing, List<St
     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.:\\-]*");
+    // the arguments of the functions that take a name or command the editor 
knows the values of
+    private static final Pattern DATE_COMMAND = 
Pattern.compile("date:([^:}]*)");
+    private static final Pattern DATE_PATTERN = 
Pattern.compile("date:[^:}]+:(.*)");
+    private static final Pattern ZONED_COMMAND = 
Pattern.compile("date-with-timezone\\(([^:)]*)");
+    private static final Pattern ZONED_ZONE = 
Pattern.compile("date-with-timezone\\([^:)]+:([^:)]*)");
+    private static final Pattern ZONED_PATTERN = 
Pattern.compile("date-with-timezone\\([^:)]+:[^:)]+:([^)]*)");
+    private static final Pattern BEAN = Pattern.compile("bean:([\\w\\-]*)");
+    private static final Pattern PROPERTY_KEY = 
Pattern.compile("(?:properties|propertiesExist):([\\w.\\-]*)");
     private static final Pattern OPERATOR = 
Pattern.compile("\\s+([^\\s'\"$]*)");
     private static final Pattern WORD = Pattern.compile("[A-Za-z_][\\w-]*");
 
@@ -83,6 +101,10 @@ record SimpleCompletionContext(Kind kind, String prefix, 
String closing, List<St
         }
         if (!open.isEmpty()) {
             String inner = line.substring(open.get(open.size() - 1), col);
+            SimpleCompletionContext argument = argument(inner);
+            if (argument != null) {
+                return argument;
+            }
             Matcher m = NAME.matcher(inner);
             if (m.matches()) {
                 Kind kind = switch (m.group(1)) {
@@ -107,6 +129,34 @@ record SimpleCompletionContext(Kind kind, String prefix, 
String closing, List<St
         return new SimpleCompletionContext(Kind.OPERATOR, m.group(1), " ", 
owners(lines, row, firstOpen));
     }
 
+    /** The argument of a function the cursor is in, when the editor knows its 
values: ${date:|, ${bean:|... */
+    private static SimpleCompletionContext argument(String inner) {
+        Matcher m;
+        if ((m = DATE_PATTERN.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.DATE_PATTERN, m.group(1), 
"}", List.of());
+        }
+        if ((m = DATE_COMMAND.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.DATE_COMMAND, m.group(1), 
"", List.of());
+        }
+        if ((m = ZONED_PATTERN.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.DATE_PATTERN, m.group(1), 
")}", List.of());
+        }
+        if ((m = ZONED_ZONE.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.TIME_ZONE, m.group(1), 
":", List.of());
+        }
+        if ((m = ZONED_COMMAND.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.DATE_COMMAND, m.group(1), 
"", List.of());
+        }
+        if ((m = BEAN.matcher(inner)).matches()) {
+            // no } after the bean: a .method may follow
+            return new SimpleCompletionContext(Kind.BEAN, m.group(1), "", 
List.of());
+        }
+        if ((m = PROPERTY_KEY.matcher(inner)).matches()) {
+            return new SimpleCompletionContext(Kind.PROPERTY_KEY, m.group(1), 
"}", List.of());
+        }
+        return null;
+    }
+
     /**
      * A function of a simple expression on a line: the text between ${ and } 
(to the end of the line when it is not
      * closed yet), nested functions included.
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
index afd772196ecd..97f654a15462 100644
--- 
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
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.HashSet;
@@ -23,6 +24,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -54,12 +56,48 @@ final class SimpleCompletions {
     private record Words(CamelCatalog catalog, Set<String> predicate, 
Set<String> eip) {
     }
 
+    /** The commands of ${date:..}; an offset (now-24h, header.due+1h30m) may 
follow any of them. */
+    private static final Map<String, String> DATE_COMMANDS = dateCommands();
+    /** Patterns of java.text.SimpleDateFormat that dates are often formatted 
with. */
+    private static final List<String> DATE_PATTERNS = List.of(
+            "yyyy-MM-dd", "yyyyMMdd", "yyyy-MM-dd HH:mm:ss", 
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
+            "yyyyMMddHHmmss", "HH:mm:ss", "HH:mm", "dd-MM-yyyy", "MM/dd/yyyy", 
"EEE, dd MMM yyyy HH:mm:ss Z");
+
+    /**
+     * What the project knows that simple functions take: the beans it 
declares (${bean:..}) and the keys of its
+     * .properties files (${properties:..}).
+     */
+    record Project(
+            Supplier<List<AutocompletePopup.CompletionItem>> beans,
+            Supplier<List<AutocompletePopup.CompletionItem>> properties) {
+
+        static final Project NONE = new Project(List::of, List::of);
+    }
+
     private SimpleCompletions() {
     }
 
+    private static Map<String, String> dateCommands() {
+        Map<String, String> commands = new LinkedHashMap<>();
+        commands.put("now", "The current date and time");
+        commands.put("millis", "The current time in milliseconds");
+        commands.put("exchangeCreated", "When the exchange was created");
+        commands.put("header.", "The date (Long or Date) in the header with 
the given name");
+        commands.put("variable.", "The date (Long or Date) in the variable 
with the given name");
+        commands.put("exchangeProperty.", "The date (Long or Date) in the 
exchange property with the given name");
+        commands.put("file", "The last modified time of the file (with a file 
consumer)");
+        return commands;
+    }
+
     /** 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) {
+        return provide(catalog, context, lines, Project.NONE);
+    }
+
+    /** The completions at the context, given the lines of the file being 
edited and what the project declares. */
+    static List<AutocompletePopup.CompletionItem> provide(
+            CamelCatalog catalog, SimpleCompletionContext context, 
List<String> lines, Project project) {
         if (catalog == null || context == null) {
             return List.of();
         }
@@ -76,9 +114,39 @@ final class SimpleCompletions {
             case HEADER -> names(catalog, lines, "Header", "header", true);
             case PROPERTY -> names(catalog, lines, "Property", 
"exchangeProperty", false);
             case VARIABLE -> names(catalog, lines, "Variable", "variable", 
false);
+            case DATE_COMMAND -> dateCommandItems();
+            case DATE_PATTERN -> DATE_PATTERNS.stream()
+                    .map(p -> new AutocompletePopup.CompletionItem(
+                            p, "Formats the date with this 
java.text.SimpleDateFormat pattern", "pattern", null, false,
+                            null, null, false))
+                    .toList();
+            case TIME_ZONE -> timeZones();
+            case BEAN -> project.beans().get();
+            case PROPERTY_KEY -> project.properties().get();
         };
     }
 
+    private static List<AutocompletePopup.CompletionItem> dateCommandItems() {
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        DATE_COMMANDS.forEach((command, doc) -> items.add(new 
AutocompletePopup.CompletionItem(
+                command, doc + ". An offset may follow, such as -24h or 
+1h30m.", "command", null, false, null, null,
+                false)));
+        return items;
+    }
+
+    /** The time zone ids of the JVM, UTC first. */
+    private static List<AutocompletePopup.CompletionItem> timeZones() {
+        List<String> zones = new ArrayList<>(ZoneId.getAvailableZoneIds());
+        zones.sort(String.CASE_INSENSITIVE_ORDER);
+        zones.remove("UTC");
+        zones.add(0, "UTC");
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (String zone : zones) {
+            items.add(new AutocompletePopup.CompletionItem(zone, "Time zone", 
"zone", null, false, null, null, false));
+        }
+        return items;
+    }
+
     static List<AutocompletePopup.CompletionItem> functions(LanguageModel 
simple) {
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
         for (LanguageModel.LanguageFunctionModel f : simple.getFunctions()) {
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 30f9bedfccf8..8a687a7227e6 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
@@ -1033,7 +1033,30 @@ final class SourceEditAssist {
 
     /** 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);
+        return provideSimpleCompletions(context, lines, List::of);
+    }
+
+    /**
+     * The completions of the simple expression at the cursor, the arguments 
of ${bean:..} from the beans the project
+     * declares and of ${properties:..} from its .properties files 
(CAMEL-25242).
+     */
+    List<AutocompletePopup.CompletionItem> provideSimpleCompletions(
+            SimpleCompletionContext context, List<String> lines, 
Supplier<List<AutocompletePopup.CompletionItem>> beans) {
+        return SimpleCompletions.provide(validationCatalog(), context, lines,
+                new SimpleCompletions.Project(beans, this::propertyKeys));
+    }
+
+    /** The keys of the project's .properties files, with their values. */
+    private List<AutocompletePopup.CompletionItem> propertyKeys() {
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (AutocompletePopup.CompletionItem p : loadPropertyPlaceholders()) {
+            String key = p.key();
+            if (key.startsWith("{{") && key.endsWith("}}")) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        key.substring(2, key.length() - 2), p.description(), 
"property", null, false, null, p.group()));
+            }
+        }
+        return items;
     }
 
     List<AutocompletePopup.CompletionItem> 
provideComponentNameCompletions(String role) {
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 5a8b3faaefe9..7131d9ada5dc 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
@@ -846,8 +846,8 @@ class SourceTab extends AbstractTab {
         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);
+        sourceViewer.setSimpleCompletion(routeFile || 
isCamelSourceFile(filePath)
+                ? (c, lines) -> assist.provideSimpleCompletions(c, lines, 
this::beanItems) : null);
         boolean xmlRoute = routeFile && name.toLowerCase().endsWith(".xml");
         sourceViewer.setCursorQuickDocProvider(!routeFile && 
!isCamelSourceFile(filePath) ? null
                 : xmlRoute ? (lines, row, col) -> {
@@ -856,9 +856,9 @@ class SourceTab extends AbstractTab {
                     return !simple.isEmpty() ? simple : 
assist.provideXmlQuickDoc(lines, row, col);
                 }
                 : assist::provideSimpleQuickDoc);
-        // the elements, attributes and values of an XML route (CAMEL-25240)
         // the route chain of a Java route (CAMEL-25241)
         sourceViewer.setJavaCompletion(routeFile && name.endsWith(".java") ? 
assist::provideJavaCompletions : null);
+        // the elements, attributes and values of an XML route (CAMEL-25240)
         sourceViewer.setXmlCompletion(routeFile && 
name.toLowerCase().endsWith(".xml") ? assist::provideXmlCompletions : null);
         if (isCamelSourceFile(filePath)) {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
@@ -1475,6 +1475,18 @@ class SourceTab extends AbstractTab {
         return result;
     }
 
+    /** The beans the project declares, for ${bean:..} in simple expressions 
(CAMEL-25242). */
+    private List<AutocompletePopup.CompletionItem> beanItems() {
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (ProjectBeans.Location bean : projectBeans().beans()) {
+            String where = "Declared in " + 
Path.of(bean.filePath()).getFileName() + ":" + (bean.line() + 1);
+            items.add(new AutocompletePopup.CompletionItem(
+                    bean.label(), bean.type() != null ? where + " as " + 
bean.type() : where, bean.type(), null, false,
+                    null, null, false));
+        }
+        return items;
+    }
+
     /** The beans and classes the project declares, read again when a file of 
the project changed. */
     ProjectBeans projectBeans() {
         long now = System.currentTimeMillis();
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 35d89bc36dbd..692fa4bee868 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
@@ -1356,6 +1356,11 @@ class SourceViewer {
                 case PROPERTY -> "Exchange properties";
                 case VARIABLE -> "Variables";
                 case OPERATOR -> "Operators";
+                case DATE_COMMAND -> "Date commands";
+                case DATE_PATTERN -> "Date patterns";
+                case TIME_ZONE -> "Time zones";
+                case BEAN -> "Beans";
+                case PROPERTY_KEY -> "Properties";
             });
             pendingSimpleCompletion = new SimpleCompletion(row, col, c);
         }
@@ -1388,8 +1393,7 @@ class SourceViewer {
         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)) {
+        while (end < line.length() && isSimpleWordChar(line.charAt(end), 
c.kind())) {
             end++;
         }
         for (int i = from; i < end; i++) {
@@ -1403,17 +1407,26 @@ class SourceViewer {
         }
         boolean hasClosing = !closing.isEmpty() && line.startsWith(closing, 
end);
         editState.insert(text + (hasClosing ? "" : closing));
-        if (c.kind() == SimpleCompletionContext.Kind.FUNCTION && 
(text.endsWith(".") || text.endsWith("["))) {
-            openAutocomplete();
+        boolean stem = c.kind() == SimpleCompletionContext.Kind.FUNCTION
+                && (text.endsWith(".") || text.endsWith("[") || 
text.endsWith(":") || text.endsWith("("));
+        if (stem || c.kind() == SimpleCompletionContext.Kind.TIME_ZONE) {
+            // header. opens the header names, date: the date commands, bean: 
the beans, a time zone the patterns;
+            // only the simple completion, nothing else when the function has 
no values to offer
+            openSimpleAutocomplete();
         }
     }
 
     /** 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 == ':';
+    private static boolean isSimpleWordChar(char c, 
SimpleCompletionContext.Kind kind) {
+        return switch (kind) {
+            case OPERATOR -> !Character.isWhitespace(c) && c != '$' && c != 
'\'' && c != '"';
+            case FUNCTION, HEADER, PROPERTY, VARIABLE ->
+                Character.isLetterOrDigit(c) || c == '-' || c == '_' || c == 
'.' || c == ':';
+            // the argument only: the .method after a bean, the :pattern after 
a date command stay
+            case PROPERTY_KEY -> Character.isLetterOrDigit(c) || c == '-' || c 
== '_' || c == '.';
+            case DATE_PATTERN -> c != '}' && c != ')' && c != '"' && c != '\'';
+            default -> Character.isLetterOrDigit(c) || c == '-' || c == '_' || 
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 69f9b614c013..d47e8417cb24 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
@@ -135,6 +135,10 @@ Press **F4** to enter edit mode, then **Tab** for 
context-aware completion:
 - 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
+- In the arguments of a function: the commands after `${date:` (`now`,
+  `exchangeCreated`, `header.`...) and date patterns after the next `:`, the
+  time zones of `date-with-timezone`, the project's beans after `${bean:`, the
+  keys of its `.properties` files after `${properties:`
 
 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
index e01d45e42e46..eb7d21f9f05d 100644
--- 
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
@@ -96,6 +96,99 @@ class SimpleCompletionTest {
         
assertThat(at("${variable.x").kind()).isEqualTo(SimpleCompletionContext.Kind.VARIABLE);
     }
 
+    @Test
+    void theArgumentOfAFunction() {
+        SimpleCompletionContext c = at("${date:no");
+        
assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.DATE_COMMAND);
+        assertThat(c.prefix()).isEqualTo("no");
+        c = at("${date:now:yyyy");
+        
assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.DATE_PATTERN);
+        assertThat(c.prefix()).isEqualTo("yyyy");
+        assertThat(c.closing()).isEqualTo("}");
+
+        
assertThat(at("${date-with-timezone(no").kind()).isEqualTo(SimpleCompletionContext.Kind.DATE_COMMAND);
+        c = at("${date-with-timezone(now:Eur");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.TIME_ZONE);
+        assertThat(c.closing()).isEqualTo(":");
+        c = at("${date-with-timezone(now:UTC:HH");
+        
assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.DATE_PATTERN);
+        assertThat(c.closing()).isEqualTo(")}");
+
+        c = at("${bean:my");
+        assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.BEAN);
+        assertThat(c.closing()).isEmpty();
+        c = at("${properties:app.");
+        
assertThat(c.kind()).isEqualTo(SimpleCompletionContext.Kind.PROPERTY_KEY);
+        assertThat(c.prefix()).isEqualTo("app.");
+        
assertThat(at("${propertiesExist:x").kind()).isEqualTo(SimpleCompletionContext.Kind.PROPERTY_KEY);
+    }
+
+    @Test
+    void theValuesOfTheArguments() {
+        assertThat(keys(at("${date:"), 
SimpleCompletions.Project.NONE)).contains("now", "exchangeCreated", "header.");
+        assertThat(keys(at("${date:now:"), 
SimpleCompletions.Project.NONE)).contains("yyyy-MM-dd", "HH:mm:ss");
+        List<String> zones = keys(at("${date-with-timezone(now:"), 
SimpleCompletions.Project.NONE);
+        assertThat(zones.get(0)).isEqualTo("UTC");
+        assertThat(zones).contains("Europe/Paris");
+
+        SimpleCompletions.Project project = new SimpleCompletions.Project(
+                () -> List.of(new 
AutocompletePopup.CompletionItem("orderService", null, null, null, false, null, 
null)),
+                () -> List.of(new AutocompletePopup.CompletionItem("app.name", 
"Camel", null, null, false, null, null)));
+        assertThat(keys(at("${bean:"), 
project)).containsExactly("orderService");
+        assertThat(keys(at("${properties:"), 
project)).containsExactly("app.name");
+    }
+
+    private static List<String> keys(SimpleCompletionContext c, 
SimpleCompletions.Project project) {
+        return SimpleCompletions.provide(catalog, c, List.of(), 
project).stream()
+                .map(AutocompletePopup.CompletionItem::key).toList();
+    }
+
+    @Test
+    void anotherBeanKeepsTheMethod() throws Exception {
+        SourceViewer viewer = viewer("route.camel.yaml", """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - setBody:
+                          simple: "${bean:orderService.total}"
+                """);
+        viewer.setSimpleCompletion((c, lines) -> 
SimpleCompletions.provide(catalog, c, lines,
+                new SimpleCompletions.Project(
+                        () -> List.of(
+                                new 
AutocompletePopup.CompletionItem("orderService", null, null, null, false, null, 
null),
+                                new 
AutocompletePopup.CompletionItem("priceCalculator", null, null, null, false, 
null, null)),
+                        List::of)));
+        cursorAt(viewer, 4, "orderService.total}\"");
+        tab(viewer);
+        type(viewer, "pri");
+        enter(viewer);
+        assertThat(line(viewer, 4)).isEqualTo("          simple: 
\"${bean:priceCalculator.total}\"");
+    }
+
+    @Test
+    void aDateFromItsFunctionToItsPattern() throws Exception {
+        SourceViewer viewer = viewer("route.camel.yaml", """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - setBody:
+                          simple: "Created ${dat"
+                """);
+        cursorAt(viewer, 4, "\"");
+        tab(viewer);
+        type(viewer, "e:");
+        enter(viewer);
+        // date: opens the commands right away
+        type(viewer, "now");
+        enter(viewer);
+        assertThat(line(viewer, 4)).isEqualTo("          simple: \"Created 
${date:now\"");
+        type(viewer, ":");
+        tab(viewer);
+        type(viewer, "yyyy-MM-dd");
+        enter(viewer);
+        assertThat(line(viewer, 4)).isEqualTo("          simple: \"Created 
${date:now:yyyy-MM-dd}\"");
+    }
+
     @Test
     void theOperatorAfterAFunction() {
         SimpleCompletionContext c = at("        .filter(simple(\"${header.foo} 
con");

Reply via email to