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

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

commit 46a66ac7194cc5d17f360b171dd18dd12d527949
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 7 11:59:21 2026 +0200

    CAMEL-25411: camel-jbang - Source editor: complete the properties of a 
Kamelet under its kamelet: endpoint
    
    Tab under parameters: (or in the query) of a kamelet: endpoint listed only
    the options of the kamelet component itself (routeId, timeout...). It now
    lists the properties of the Kamelet first, from the project's own Kamelet
    file or the catalog, required first, with their type, default and
    description, then a dim divider and the options of the kamelet component,
    which are for special uses. There is no divider when only one kind is
    listed, as for a Kamelet without properties.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../jbang/core/commands/tui/AutocompletePopup.java | 61 ++++++++++++++++++----
 .../jbang/core/commands/tui/SourceEditAssist.java  | 47 +++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  4 +-
 .../commands/tui/SourceKameletValidationTest.java  | 53 +++++++++++++++++++
 4 files changed, 155 insertions(+), 10 deletions(-)

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 245bcb6fcde6..0f83c7bcef33 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
@@ -98,6 +98,12 @@ class AutocompletePopup {
     private Rect popupRect;
     private String titlePrefix;
     private boolean fullKeys;
+    // the row of the filtered list a divider is drawn above, -1 for none: the 
properties of a Kamelet above it, the
+    // options of the kamelet component itself, for special uses only, below 
it (CAMEL-25411)
+    private int dividerAt = -1;
+
+    static final String KAMELET_GROUP = "kamelet ";
+    static final String KAMELET_COMPONENT_OPTIONS = "kamelet component 
options";
 
     AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText) {
         this(items, initialPrefix, lineKeyText, false);
@@ -224,11 +230,16 @@ class AutocompletePopup {
         if (me.isClick()) {
             if (popupRect != null && popupRect.contains(me.x(), me.y())) {
                 int idx = TuiHelper.listItemAt(popupRect, 0,
-                        (filteredItems != null ? filteredItems.size() : 0) + 2,
+                        (filteredItems != null ? listRows() : 0) + 2,
                         me.x(), me.y());
-                if (idx >= 2 && filteredItems != null && idx - 2 < 
filteredItems.size()) {
-                    listState.select(idx - 2);
-                    selectedItem = filteredItems.get(idx - 2);
+                int row = idx - 2;
+                if (dividerAt >= 0 && row >= dividerAt) {
+                    // the divider is not an item
+                    row = row == dividerAt ? -1 : row - 1;
+                }
+                if (row >= 0 && filteredItems != null && row < 
filteredItems.size()) {
+                    listState.select(row);
+                    selectedItem = filteredItems.get(row);
                     return Result.CLOSED;
                 }
                 return Result.CONSUMED;
@@ -244,7 +255,7 @@ class AutocompletePopup {
         }
 
         int popupW = Math.max(70, area.width() - 4);
-        int contentH = filteredItems.size() + 2;
+        int contentH = listRows() + 2;
         int maxH = area.height() - 2;
         int popupH = Math.min(contentH + 2, maxH);
         popupH = Math.max(popupH, 12);
@@ -314,7 +325,13 @@ class AutocompletePopup {
         Style dimStyle = Style.EMPTY.dim();
         Style deprecatedStyle = Style.EMPTY.dim().crossedOut();
 
-        for (CompletionItem ci : filteredItems) {
+        for (int row = 0; row < filteredItems.size(); row++) {
+            CompletionItem ci = filteredItems.get(row);
+            if (row == dividerAt) {
+                String text = "── " + KAMELET_COMPONENT_OPTIONS + " ";
+                items.add(ListItem.from(Line.from(Span.styled(
+                        text + "─".repeat(Math.max(1, listRect.width() - 2 - 
text.length())), dimStyle))));
+            }
             List<Span> spans = new ArrayList<>();
 
             if (ci.deprecated()) {
@@ -362,7 +379,7 @@ class AutocompletePopup {
         ListState renderState = new ListState();
         Integer sel = listState.selected();
         if (sel != null) {
-            renderState.select(sel + 2);
+            renderState.select(sel + 2 + (dividerAt >= 0 && sel >= dividerAt ? 
1 : 0));
         }
 
         int total = allItems.size();
@@ -385,9 +402,9 @@ class AutocompletePopup {
         frame.renderStatefulWidget(list, listRect, renderState);
 
         int visibleRows = Math.max(1, listRect.height() - 2);
-        if (shown + 2 > visibleRows) {
+        if (listRows() + 2 > visibleRows) {
             scrollbarState
-                    .contentLength(shown)
+                    .contentLength(listRows())
                     .viewportContentLength(visibleRows)
                     .position(sel != null ? sel : 0);
             frame.renderStatefulWidget(Scrollbar.builder().build(), listRect, 
scrollbarState);
@@ -565,9 +582,35 @@ class AutocompletePopup {
             // what was typed exactly first, then what starts with it: seda 
before hazelcast-seda
             filteredItems.sort(Comparator.comparingInt(item -> 
rank(item.key(), f)));
         }
+        dividerAt = -1;
+        boolean kamelet = 
filteredItems.stream().anyMatch(AutocompletePopup::isKameletProperty);
+        if (kamelet && 
!filteredItems.stream().allMatch(AutocompletePopup::isKameletProperty)) {
+            // the properties of the Kamelet stay above the divider while 
filtering (the sort is stable)
+            filteredItems.sort(Comparator.comparing(item -> 
!isKameletProperty(item)));
+            for (int i = 0; i < filteredItems.size(); i++) {
+                if (!isKameletProperty(filteredItems.get(i))) {
+                    dividerAt = i;
+                    break;
+                }
+            }
+        }
         listState.select(filteredItems.isEmpty() ? null : 0);
     }
 
+    private static boolean isKameletProperty(CompletionItem item) {
+        return item.group() != null && item.group().startsWith(KAMELET_GROUP);
+    }
+
+    /** Package-private for tests: the row of the filtered list the divider is 
drawn above, -1 for none. */
+    int dividerAt() {
+        return dividerAt;
+    }
+
+    /** The rows of the list: the items and the divider. */
+    private int listRows() {
+        return filteredItems.size() + (dividerAt >= 0 ? 1 : 0);
+    }
+
     private static int rank(String key, String filter) {
         if (key.equalsIgnoreCase(filter)) {
             return 0;
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 3a9c306f10d3..2c4faf30563b 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
@@ -40,6 +40,7 @@ import java.util.regex.Pattern;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.KameletChecks;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteNodes;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
@@ -928,6 +929,14 @@ final class SourceEditAssist {
     }
 
     List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String 
context) {
+        return provideYamlKeyCompletions(context, null);
+    }
+
+    /**
+     * As {@link #provideYamlKeyCompletions(String)}, with the directory of 
the file: the options of a kamelet: endpoint
+     * start with the properties of its Kamelet, from the project's own 
Kamelet file or the catalog (CAMEL-25411).
+     */
+    List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String 
context, Path directory) {
         if (context == null) {
             return List.of();
         }
@@ -994,6 +1003,9 @@ final class SourceEditAssist {
         }
 
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        if ("kamelet".equals(componentName)) {
+            items.addAll(kameletPropertyCompletions(uri, directory, 
existingKeys));
+        }
         for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
             if (!includeEndpointOption(opt, isConsumer)) {
                 continue;
@@ -1008,11 +1020,46 @@ final class SourceEditAssist {
         }
 
         
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                // the properties of the Kamelet before the options of the 
kamelet component
+                .thenComparing(i -> i.group() == null || 
!i.group().startsWith(AutocompletePopup.KAMELET_GROUP))
                 .thenComparing((a, b) -> Boolean.compare(b.required(), 
a.required()))
                 .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
         return items;
     }
 
+    /**
+     * The properties of the Kamelet a kamelet: uri names, the ones not set 
yet: what goes under parameters: beside the
+     * options of the kamelet component itself (routeId, timeout...), which 
are all a model or a user saw before.
+     */
+    static List<AutocompletePopup.CompletionItem> kameletPropertyCompletions(
+            String uri, Path directory, Set<String> existingKeys) {
+        if (uri == null || !uri.startsWith("kamelet:")) {
+            return List.of();
+        }
+        String name = uri.substring("kamelet:".length());
+        int end = name.length();
+        for (char c : new char[] { '?', '/' }) {
+            int i = name.indexOf(c);
+            if (i >= 0) {
+                end = Math.min(end, i);
+            }
+        }
+        KameletDefinitions.Definition def = 
KameletDefinitions.find(name.substring(0, end).trim(), directory);
+        if (def == null) {
+            return List.of();
+        }
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (KameletDefinitions.Property p : def.properties()) {
+            if (existingKeys.contains(p.name())) {
+                continue;
+            }
+            items.add(new AutocompletePopup.CompletionItem(
+                    p.name(), p.description(), p.type() != null ? p.type() : 
"string", p.defaultValue(), false, null,
+                    AutocompletePopup.KAMELET_GROUP + def.name(), 
p.required()));
+        }
+        return items;
+    }
+
     /** The quick doc of the XML element or attribute at the cursor 
(CAMEL-25244). */
     List<SourceViewer.DocEntry> provideXmlQuickDoc(List<String> lines, int 
row, int col) {
         return XmlQuickDoc.at(validationCatalog(), lines, row, col);
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 966f04171377..20c1823c7692 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
@@ -907,7 +907,9 @@ class SourceTab extends AbstractTab {
             sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs);
             sourceViewer.setDeprecatedLineScanner(null);
             if (SourceEditAssist.isYamlFile(filePath)) {
-                
sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions);
+                // the options of a kamelet: endpoint start with the 
properties of its Kamelet (CAMEL-25411)
+                sourceViewer.setAutocompleteProvider(
+                        c -> assist.provideYamlKeyCompletions(c, 
filePath.toAbsolutePath().getParent()));
                 
sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions);
                 
sourceViewer.setEndpointValidator(assist::validateYamlEndpoints);
                 // in a Kamelet's template, a property written as 
${header.tag} is said to be the placeholder {{tag}}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
index 67b4620f950f..63559ca75b36 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
@@ -117,6 +117,59 @@ class SourceKameletValidationTest {
                 ROUTE.replace("tagg:", "tag:"))).isEmpty();
     }
 
+    @Test
+    void theParametersOfAKameletEndpointCompleteItsProperties() throws 
Exception {
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
KAMELET, StandardCharsets.UTF_8);
+        SourceEditAssist assist = new SourceEditAssist(
+                new MonitorContext(
+                        new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())));
+        List<AutocompletePopup.CompletionItem> items
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        // the property of the Kamelet first, then the options of the kamelet 
component
+        assertThat(items.get(0).key()).isEqualTo("tag");
+        assertThat(items.get(0).required()).isTrue();
+        assertThat(items.get(0).group()).isEqualTo("kamelet tag-order-action");
+        
assertThat(items).extracting(AutocompletePopup.CompletionItem::key).contains("routeId",
 "timeout");
+        // a property already given is not offered again
+        
assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:producer:tag|kamelet:tag-order-action",
 tempDir))
+                
.extracting(AutocompletePopup.CompletionItem::key).doesNotContain("tag");
+        // a Kamelet of the catalog, in the query of the uri
+        
assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:consumer|kamelet:timer-source?period=1000",
 tempDir))
+                
.extracting(AutocompletePopup.CompletionItem::key).startsWith("message");
+    }
+
+    @Test
+    void aDividerSetsTheKameletComponentOptionsApart() throws Exception {
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
KAMELET, StandardCharsets.UTF_8);
+        SourceEditAssist assist = new SourceEditAssist(
+                new MonitorContext(
+                        new AtomicReference<>(List.of()), new 
AtomicReference<>(List.of())));
+        List<AutocompletePopup.CompletionItem> items
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        // tag above the divider, the options of the kamelet component below it
+        assertThat(new AutocompletePopup(items, "", 
"").dividerAt()).isEqualTo(1);
+        // filtering keeps the property of the Kamelet above: "t" matches tag 
and timeout
+        AutocompletePopup filtered = new AutocompletePopup(items, "t", "t");
+        assertThat(filtered.dividerAt()).isEqualTo(1);
+        // only the Kamelet's properties left, or none: no divider
+        assertThat(new AutocompletePopup(items, "tag", 
"tag").dividerAt()).isEqualTo(-1);
+        assertThat(new AutocompletePopup(items, "routeId", 
"routeId").dividerAt()).isEqualTo(-1);
+        // a Kamelet without properties: only the options of the component, no 
divider
+        String noProperties = KAMELET.replace("""
+                    required:
+                      - tag
+                    properties:
+                      tag:
+                        title: Tag
+                        type: string
+                """, "").replace("{{tag}}", "tagged");
+        Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), 
noProperties, StandardCharsets.UTF_8);
+        List<AutocompletePopup.CompletionItem> none
+                = 
assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action",
 tempDir);
+        assertThat(none).isNotEmpty();
+        assertThat(new AutocompletePopup(none, "", 
"").dividerAt()).isEqualTo(-1);
+    }
+
     @Test
     void aKameletFileWithAProblemIsMarkedOnLoadAndNotSaved() throws Exception {
         Path file = tempDir.resolve("tag-order-action.kamelet.yaml");

Reply via email to