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 8e9a17077836 camel-jbang - TUI tab completion for component names and 
endpoint options
8e9a17077836 is described below

commit 8e9a170778365ba8e5e6452a8032f7db21971c59
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Aug 4 13:40:12 2026 +0200

    camel-jbang - TUI tab completion for component names and endpoint options
    
    - Tab on uri: lines shows component names filtered by context
      (consumer-only excluded for to/wireTap/enrich, producer-only excluded
      for from/pollEnrich/poll/interceptFrom)
    - Selecting a component auto-inserts parameters: block
    - Endpoint option completion now includes path options (e.g. 
destinationName)
    - Required options shown in bold with * prefix and sorted to top
    - Detail panel shows Required: true for required options
    - Already-specified options filtered from completion list (multi-valued
      options still allowed)
    - Typing non-matching characters no longer closes the autocomplete popup
    - Updated CONSUMER_EIPS/PRODUCER_EIPS with poll, interceptFrom,
      interceptSendToEndpoint
    
    camel-jbang - TUI autocomplete: title prefix, test coverage, and 
non-matching char fix
    
    camel-jbang - TUI component picker matches on labels and shows first label 
as type
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/AutocompletePopup.java |  52 +++-
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 105 ++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 170 ++++++++++-
 .../core/commands/tui/AutocompletePopupTest.java   |  16 +-
 .../core/commands/tui/YamlCompletionTest.java      | 327 ++++++++++++++++++++-
 5 files changed, 640 insertions(+), 30 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 8ea1a047af69..4b57abc28d19 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,7 +45,12 @@ import dev.tamboui.widgets.scrollbar.ScrollbarState;
 class AutocompletePopup {
 
     record CompletionItem(String key, String description, String type, Object 
defaultValue,
-            boolean deprecated, String deprecationNote, String group) {
+            boolean deprecated, String deprecationNote, String group, boolean 
required) {
+
+        CompletionItem(String key, String description, String type, Object 
defaultValue,
+                       boolean deprecated, String deprecationNote, String 
group) {
+            this(key, description, type, defaultValue, deprecated, 
deprecationNote, group, false);
+        }
     }
 
     @FunctionalInterface
@@ -76,6 +81,7 @@ class AutocompletePopup {
     private List<CompletionItem> filteredItems;
     private CompletionItem selectedItem;
     private Rect popupRect;
+    private String titlePrefix;
 
     AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText) {
         this(items, initialPrefix, lineKeyText, false);
@@ -176,7 +182,14 @@ class AutocompletePopup {
         }
         if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
             filter.appendChar(ke.string().charAt(0));
+            List<CompletionItem> prev = filteredItems;
             rebuildList();
+            if (filteredItems.isEmpty()) {
+                // undo: keep current list instead of showing empty
+                filter.deleteChar();
+                filteredItems = prev;
+                listState.select(prev != null && !prev.isEmpty() ? 0 : null);
+            }
             return Result.CONSUMED;
         }
         return Result.CONSUMED;
@@ -261,6 +274,7 @@ class AutocompletePopup {
         items.add(ListItem.from(Line.from(Span.styled(sep, 
Style.EMPTY.dim()))));
 
         Style normalStyle = Style.EMPTY;
+        Style boldStyle = Style.EMPTY.bold();
         Style dimStyle = Style.EMPTY.dim();
         Style deprecatedStyle = Style.EMPTY.dim().crossedOut();
 
@@ -269,12 +283,14 @@ class AutocompletePopup {
 
             if (ci.deprecated()) {
                 spans.add(Span.styled(" ✘ ", dimStyle));
+            } else if (ci.required()) {
+                spans.add(Span.styled(" * ", boldStyle));
             } else {
                 spans.add(Span.raw("   "));
             }
 
             String key = ci.key();
-            Style keyStyle = ci.deprecated() ? deprecatedStyle : normalStyle;
+            Style keyStyle = ci.deprecated() ? deprecatedStyle : ci.required() 
? boldStyle : normalStyle;
 
             String displayKey = key;
             if (!key.startsWith("{{") && !key.endsWith(".")) {
@@ -309,9 +325,10 @@ class AutocompletePopup {
 
         int total = allItems.size();
         int shown = filteredItems.size();
+        String label = titlePrefix != null ? titlePrefix : "Completions";
         String title = shown == total
-                ? " Completions (" + total + ") "
-                : " Completions (" + shown + "/" + total + ") ";
+                ? " " + label + " (" + total + ") "
+                : " " + label + " (" + shown + "/" + total + ") ";
 
         ListWidget list = ListWidget.builder()
                 .items(items.toArray(ListItem[]::new))
@@ -365,6 +382,11 @@ class AutocompletePopup {
                         Span.styled("Group: ", normalStyle.bold()),
                         Span.styled(selected.group(), normalStyle)));
             }
+            if (selected.required()) {
+                lines.add(Line.from(
+                        Span.styled("Required: ", normalStyle.bold()),
+                        Span.styled("true", Theme.info())));
+            }
             if (selected.deprecated()) {
                 String depText = "Deprecated";
                 if (selected.deprecationNote() != null && 
!selected.deprecationNote().isEmpty()) {
@@ -410,6 +432,14 @@ class AutocompletePopup {
         return filteredItems != null && !filteredItems.isEmpty();
     }
 
+    boolean hasFilter() {
+        return filter.hasFilter();
+    }
+
+    void setTitlePrefix(String titlePrefix) {
+        this.titlePrefix = titlePrefix;
+    }
+
     private void rebuildList() {
         if (!filter.hasFilter()) {
             filteredItems = new ArrayList<>(allItems);
@@ -417,11 +447,23 @@ class AutocompletePopup {
             filteredItems = new ArrayList<>();
             String f = filter.filter();
             for (CompletionItem item : allItems) {
-                if (item.key().toLowerCase().startsWith(f)) {
+                if (item.key().toLowerCase().startsWith(f) || 
matchesLabel(item.group(), f)) {
                     filteredItems.add(item);
                 }
             }
         }
         listState.select(filteredItems.isEmpty() ? null : 0);
     }
+
+    private static boolean matchesLabel(String group, String filter) {
+        if (group == null || group.isEmpty()) {
+            return false;
+        }
+        for (String label : group.split(",")) {
+            if (label.trim().toLowerCase().startsWith(filter)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
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 84560c6782d7..5d9e28eaf2b1 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
@@ -23,6 +23,7 @@ import java.nio.file.attribute.BasicFileAttributes;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -96,6 +97,11 @@ class SourceTab extends AbstractTab {
     private final Map<String, Map<String, BaseOptionModel>> 
languageOptionsCache = new HashMap<>();
     private final Map<String, Map<String, BaseOptionModel>> 
dataformatOptionsCache = new HashMap<>();
 
+    // Component name completion cache (keyed by catalog version)
+    private String componentsCatalogVersion;
+    private List<AutocompletePopup.CompletionItem> consumerComponents;
+    private List<AutocompletePopup.CompletionItem> producerComponents;
+
     // Spring Boot configuration metadata cache (lazy-loaded on-demand via IPC)
     private Map<String, JsonObject> springBootMetadataCache;
     private boolean springBootMetadataLoaded;
@@ -912,7 +918,16 @@ class SourceTab extends AbstractTab {
     // ---- YAML DSL completion ----
 
     private List<AutocompletePopup.CompletionItem> 
provideYamlKeyCompletions(String context) {
-        if (context == null || !context.startsWith("yaml:")) {
+        if (context == null) {
+            return List.of();
+        }
+
+        // component name completion on uri: lines
+        if (context.startsWith("yaml-uri:")) {
+            return provideComponentNameCompletions(context.substring(9));
+        }
+
+        if (!context.startsWith("yaml:")) {
             return List.of();
         }
         CamelCatalog catalog = getCatalog();
@@ -920,8 +935,8 @@ class SourceTab extends AbstractTab {
             return List.of();
         }
 
-        // context format: "yaml:componentName:consumer|producer"
-        String[] parts = context.substring(5).split(":", 2);
+        // context format: 
"yaml:componentName:consumer|producer[:existingKey1,existingKey2,...]"
+        String[] parts = context.substring(5).split(":", 3);
         if (parts.length < 2) {
             return List.of();
         }
@@ -929,26 +944,98 @@ class SourceTab extends AbstractTab {
         String role = parts[1];
         boolean isConsumer = "consumer".equals(role);
 
+        Set<String> existingKeys = Set.of();
+        if (parts.length > 2 && !parts[2].isEmpty()) {
+            existingKeys = new HashSet<>(Arrays.asList(parts[2].split(",")));
+        }
+
         ComponentModel model = catalog.componentModel(componentName);
         if (model == null) {
             return List.of();
         }
 
+        // build a set of multi-valued option names so we can allow duplicates
+        Set<String> multiValuedOptions = new HashSet<>();
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
+            if (opt.isMultiValue()) {
+                multiValuedOptions.add(opt.getName());
+            }
+        }
+
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
-        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointParameterOptions()) {
-            if (includeEndpointOption(opt, isConsumer)) {
-                items.add(new AutocompletePopup.CompletionItem(
-                        opt.getName(), opt.getDescription(), opt.getType(),
-                        opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
-                        opt.getGroup()));
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
+            if (!includeEndpointOption(opt, isConsumer)) {
+                continue;
+            }
+            if (existingKeys.contains(opt.getName()) && 
!multiValuedOptions.contains(opt.getName())) {
+                continue;
             }
+            items.add(new AutocompletePopup.CompletionItem(
+                    opt.getName(), opt.getDescription(), opt.getType(),
+                    opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                    opt.getGroup(), opt.isRequired()));
         }
 
         
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing((a, b) -> Boolean.compare(b.required(), 
a.required()))
                 .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
         return items;
     }
 
+    private List<AutocompletePopup.CompletionItem> 
provideComponentNameCompletions(String role) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        boolean isConsumer = "consumer".equals(role);
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        String version = info != null ? info.camelVersion : null;
+
+        // rebuild cache if catalog version changed
+        if (version != null && !version.equals(componentsCatalogVersion)) {
+            componentsCatalogVersion = version;
+            consumerComponents = null;
+            producerComponents = null;
+        }
+
+        List<AutocompletePopup.CompletionItem> cached = isConsumer ? 
consumerComponents : producerComponents;
+        if (cached != null) {
+            return cached;
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (String name : catalog.findComponentNames()) {
+            ComponentModel model = catalog.componentModel(name);
+            if (model == null) {
+                continue;
+            }
+            if (isConsumer && model.isProducerOnly()) {
+                continue;
+            }
+            if (!isConsumer && model.isConsumerOnly()) {
+                continue;
+            }
+            String labels = model.getLabel();
+            String firstLabel = labels != null && !labels.isEmpty()
+                    ? labels.split(",")[0].trim()
+                    : "component";
+            items.add(new AutocompletePopup.CompletionItem(
+                    name, model.getTitle() + " - " + model.getDescription(),
+                    firstLabel, null, model.isDeprecated(), 
model.getDeprecationNote(),
+                    labels));
+        }
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+
+        if (isConsumer) {
+            consumerComponents = items;
+        } else {
+            producerComponents = items;
+        }
+        return items;
+    }
+
     private static boolean 
includeEndpointOption(ComponentModel.EndpointOptionModel opt, boolean 
isConsumer) {
         String label = opt.getLabel();
         if (label == null || label.isEmpty()) {
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 93ffd93fe139..0a39599c2851 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
@@ -542,9 +542,11 @@ class SourceViewer {
     record YamlEndpointContext(String component, boolean consumer) {
     }
 
-    private static final java.util.Set<String> CONSUMER_EIPS = 
java.util.Set.of("from", "pollEnrich", "poll-enrich");
+    private static final java.util.Set<String> CONSUMER_EIPS
+            = java.util.Set.of("from", "pollEnrich", "poll-enrich", "poll", 
"interceptFrom", "intercept-from");
     private static final java.util.Set<String> PRODUCER_EIPS
-            = java.util.Set.of("to", "toD", "to-d", "wireTap", "wire-tap", 
"enrich");
+            = java.util.Set.of("to", "toD", "to-d", "wireTap", "wire-tap", 
"enrich",
+                    "interceptSendToEndpoint", "intercept-send-to-endpoint");
 
     YamlEndpointContext findEnclosingComponent(int fromRow) {
         String cursorLine = editState.getLine(fromRow);
@@ -626,6 +628,140 @@ class SourceViewer {
         return null;
     }
 
+    java.util.Set<String> collectExistingParameters(int fromRow) {
+        java.util.Set<String> keys = new java.util.LinkedHashSet<>();
+        // find the parameters: row by walking up
+        int parametersRow = -1;
+        int parametersIndent = -1;
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // blank lines: derive indent from nearest preceding non-blank line
+        if (cursorLine.isBlank()) {
+            for (int i = fromRow - 1; i >= 0; i--) {
+                String prev = editState.getLine(i);
+                if (!prev.isBlank()) {
+                    if (prev.trim().startsWith("parameters:")) {
+                        parametersRow = i;
+                        parametersIndent = countLeadingSpaces(prev);
+                    } else {
+                        cursorIndent = countLeadingSpaces(prev);
+                    }
+                    break;
+                }
+            }
+        }
+
+        if (parametersRow < 0) {
+            for (int i = fromRow; i >= 0; i--) {
+                String line = editState.getLine(i);
+                if (line.isBlank()) {
+                    continue;
+                }
+                String trimmed = line.trim();
+                int indent = countLeadingSpaces(line);
+                if (trimmed.startsWith("parameters:") && indent < 
cursorIndent) {
+                    parametersRow = i;
+                    parametersIndent = indent;
+                    break;
+                }
+                if (i < fromRow && indent < cursorIndent && 
!trimmed.startsWith("#")) {
+                    break;
+                }
+            }
+        }
+        if (parametersRow < 0) {
+            return keys;
+        }
+        int childIndent = parametersIndent + 2;
+        for (int i = parametersRow + 1; i < editState.lineCount(); i++) {
+            if (i == fromRow) {
+                continue;
+            }
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < childIndent) {
+                break;
+            }
+            if (indent == childIndent) {
+                String trimmed = line.trim();
+                int colonIdx = trimmed.indexOf(':');
+                if (colonIdx > 0) {
+                    keys.add(trimmed.substring(0, colonIdx).trim());
+                }
+            }
+        }
+        return keys;
+    }
+
+    record YamlUriContext(boolean consumer, String prefix) {
+    }
+
+    YamlUriContext findUriContext(int row) {
+        String lineText = editState.getLine(row);
+        String trimmed = lineText.trim();
+        if (trimmed.startsWith("- ")) {
+            trimmed = trimmed.substring(2).trim();
+        }
+
+        // Check if cursor is on a "uri:" line (possibly with partial value)
+        if (trimmed.startsWith("uri:")) {
+            String value = trimmed.substring(4).trim();
+            if (value.startsWith("\"") || value.startsWith("'")) {
+                value = value.substring(1);
+            }
+            if (value.endsWith("\"") || value.endsWith("'")) {
+                value = value.substring(0, value.length() - 1);
+            }
+            // if value already contains a colon, scheme is already typed
+            if (value.contains(":")) {
+                return null;
+            }
+            // walk up to find the parent EIP
+            int indent = countLeadingSpaces(lineText);
+            for (int i = row - 1; i >= 0; i--) {
+                String prev = editState.getLine(i);
+                if (prev.isBlank()) {
+                    continue;
+                }
+                int prevIndent = countLeadingSpaces(prev);
+                if (prevIndent < indent) {
+                    String eipName = extractEipName(prev.trim());
+                    if (eipName != null) {
+                        boolean consumer = CONSUMER_EIPS.contains(eipName);
+                        return new YamlUriContext(consumer, value);
+                    }
+                    break;
+                }
+            }
+            return null;
+        }
+
+        // Check if cursor is on an inline EIP line: "to: " or "from: kafka" 
(no colon in value)
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx > 0) {
+            String eipName = trimmed.substring(0, colonIdx).trim();
+            if (CONSUMER_EIPS.contains(eipName) || 
PRODUCER_EIPS.contains(eipName)) {
+                String value = trimmed.substring(colonIdx + 1).trim();
+                if (value.startsWith("\"") || value.startsWith("'")) {
+                    value = value.substring(1);
+                }
+                if (value.endsWith("\"") || value.endsWith("'")) {
+                    value = value.substring(0, value.length() - 1);
+                }
+                if (value.contains(":")) {
+                    return null;
+                }
+                boolean consumer = CONSUMER_EIPS.contains(eipName);
+                return new YamlUriContext(consumer, value);
+            }
+        }
+        return null;
+    }
+
     private static int countLeadingSpaces(String line) {
         int count = 0;
         for (int i = 0; i < line.length(); i++) {
@@ -756,6 +892,19 @@ class SourceViewer {
             trimmed = trimmed.substring(2).trim();
         }
 
+        // try component name completion on uri: lines first
+        YamlUriContext uriCtx = findUriContext(row);
+        if (uriCtx != null && autocompleteProvider != null) {
+            String role = uriCtx.consumer() ? "consumer" : "producer";
+            String context = "yaml-uri:" + role;
+            List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(context);
+            if (items != null && !items.isEmpty()) {
+                autocompletePopup = new AutocompletePopup(items, 
uriCtx.prefix(), uriCtx.prefix(), true);
+                autocompletePopup.setTitlePrefix("Components");
+            }
+            return;
+        }
+
         YamlEndpointContext ctx = findEnclosingComponent(row);
         if (ctx == null) {
             return;
@@ -783,10 +932,15 @@ class SourceViewer {
             // key completion — cursor is on an empty or partial key line
             String filter = colonIdx > 0 ? trimmed.substring(0, 
colonIdx).trim() : trimmed;
             String role = ctx.consumer() ? "consumer" : "producer";
+            java.util.Set<String> existing = collectExistingParameters(row);
             String context = "yaml:" + ctx.component() + ":" + role;
+            if (!existing.isEmpty()) {
+                context += ":" + String.join(",", existing);
+            }
             List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(context);
             if (items != null && !items.isEmpty()) {
                 autocompletePopup = new AutocompletePopup(items, filter, 
filter);
+                autocompletePopup.setTitlePrefix(ctx.component() + " options");
             }
         }
     }
@@ -861,6 +1015,18 @@ class SourceViewer {
             } else {
                 editState.insert(indentStr + item.key());
             }
+            // for component names, add parameters: block if not already 
present
+            if ("component".equals(item.type())) {
+                int nextRow = editState.cursorRow() + 1;
+                boolean hasParameters = nextRow < editState.lineCount()
+                        && 
editState.getLine(nextRow).trim().startsWith("parameters:");
+                if (!hasParameters) {
+                    editState.insert('\n');
+                    editState.insert(indentStr + "parameters:");
+                    editState.insert('\n');
+                    editState.insert(indentStr + "  ");
+                }
+            }
         } else {
             editState.insert(indentStr + item.key() + ": ");
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
index 6ecbd3484153..20e1324c5831 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
@@ -68,17 +68,15 @@ class AutocompletePopupTest {
     }
 
     @Test
-    void typingNonMatchingClosesOnBackspace() {
+    void typingNonMatchingKeepsExistingItems() {
         var items = List.of(
                 new AutocompletePopup.CompletionItem("alpha", "desc", 
"string", null, false, null, null));
         var popup = new AutocompletePopup(items, "", "");
 
+        // typing 'z' should be rejected — no items start with 'z'
         popup.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.NONE));
-        assertThat(popup.hasItems()).isFalse();
-
-        assertThat(popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE, 
KeyModifiers.NONE)))
-                .isEqualTo(AutocompletePopup.Result.CONSUMED);
         assertThat(popup.hasItems()).isTrue();
+        assertThat(popup.hasFilter()).isFalse();
     }
 
     @Test
@@ -123,8 +121,14 @@ class AutocompletePopupTest {
         var popup = new AutocompletePopup(items, "", "");
         assertThat(popup.hasItems()).isTrue();
 
+        // typing 'z' is rejected since no items match — list stays populated
         popup.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.NONE));
-        assertThat(popup.hasItems()).isFalse();
+        assertThat(popup.hasItems()).isTrue();
+
+        // typing 'o' matches "one" — filter is now active
+        popup.handleKeyEvent(KeyEvent.ofChar('o', KeyModifiers.NONE));
+        assertThat(popup.hasItems()).isTrue();
+        assertThat(popup.hasFilter()).isTrue();
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlCompletionTest.java
index d68d508242e0..426700c51514 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlCompletionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlCompletionTest.java
@@ -22,6 +22,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
@@ -254,11 +255,11 @@ class YamlCompletionTest {
     }
 
     @Test
-    void keyCompletionExcludesPathOptions() {
+    void keyCompletionIncludesPathOptions() {
         List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "producer");
 
-        // "topic" is a path option in kafka, should NOT appear in parameters 
completion
-        assertThat(items).noneMatch(i -> i.key().equals("topic"));
+        // "topic" is a path option in kafka, should appear in completion
+        assertThat(items).anyMatch(i -> i.key().equals("topic"));
     }
 
     @Test
@@ -387,28 +388,338 @@ class YamlCompletionTest {
         assertThat(items).isEmpty();
     }
 
+    // --- URI context detection (component name completion) ---
+
+    @Test
+    void findUriContextOnEmptyUriLine() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(1);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.consumer()).isTrue();
+        assertThat(ctx.prefix()).isEmpty();
+    }
+
+    @Test
+    void findUriContextOnToUri() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - to:",
+                "          uri: ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.consumer()).isFalse();
+        assertThat(ctx.prefix()).isEmpty();
+    }
+
+    @Test
+    void findUriContextWithPartialPrefix() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: ka",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(1);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.consumer()).isTrue();
+        assertThat(ctx.prefix()).isEqualTo("ka");
+    }
+
+    @Test
+    void findUriContextReturnsNullWhenSchemeAlreadyComplete() throws 
IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // scheme already has a colon — no component completion needed
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(1);
+        assertThat(ctx).isNull();
+    }
+
+    @Test
+    void findUriContextOnInlineEip() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - to: ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(3);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.consumer()).isFalse();
+    }
+
+    @Test
+    void findUriContextOnPollEnrich() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - pollEnrich:",
+                "          uri: ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlUriContext ctx = viewer.findUriContext(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.consumer()).isTrue();
+    }
+
+    // --- Component name completion (from vs to filtering) ---
+
+    @Test
+    void componentCompletionForFromExcludesProducerOnly() {
+        List<AutocompletePopup.CompletionItem> items = 
provideComponentCompletions("consumer");
+
+        // timer is consumer-only — should be in the list
+        assertThat(items).anyMatch(i -> i.key().equals("timer"));
+        // log is producer-only — should NOT be in the list
+        assertThat(items).noneMatch(i -> i.key().equals("log"));
+    }
+
+    @Test
+    void componentCompletionForToExcludesConsumerOnly() {
+        List<AutocompletePopup.CompletionItem> items = 
provideComponentCompletions("producer");
+
+        // log is producer-only — should be in the list
+        assertThat(items).anyMatch(i -> i.key().equals("log"));
+        // timer is consumer-only — should NOT be in the list
+        assertThat(items).noneMatch(i -> i.key().equals("timer"));
+    }
+
+    @Test
+    void componentCompletionIncludesBothRoles() {
+        List<AutocompletePopup.CompletionItem> consumerItems = 
provideComponentCompletions("consumer");
+        List<AutocompletePopup.CompletionItem> producerItems = 
provideComponentCompletions("producer");
+
+        // kafka supports both — should be in both lists
+        assertThat(consumerItems).anyMatch(i -> i.key().equals("kafka"));
+        assertThat(producerItems).anyMatch(i -> i.key().equals("kafka"));
+    }
+
+    @Test
+    void componentCompletionHasDescriptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideComponentCompletions("producer");
+
+        var kafka = items.stream().filter(i -> 
i.key().equals("kafka")).findFirst();
+        assertThat(kafka).isPresent();
+        assertThat(kafka.get().description()).isNotNull().isNotEmpty();
+        // type shows first label (e.g. "messaging") instead of generic 
"component"
+        assertThat(kafka.get().type()).isEqualTo("messaging");
+    }
+
+    @Test
+    void componentCompletionFilterMatchesLabels() {
+        List<AutocompletePopup.CompletionItem> items = 
provideComponentCompletions("producer");
+
+        // simulate typing "cloud" — should match components labeled "cloud"
+        var popup = new AutocompletePopup(items, "", "");
+        for (char c : "cloud".toCharArray()) {
+            popup.handleKeyEvent(dev.tamboui.tui.event.KeyEvent.ofChar(c, 
dev.tamboui.tui.event.KeyModifiers.NONE));
+        }
+        assertThat(popup.hasItems()).isTrue();
+    }
+
+    // --- Required options ---
+
+    @Test
+    void requiredOptionsAreSortedFirst() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("jms", "producer");
+
+        // find first required and first non-required
+        int firstRequired = -1;
+        int lastRequired = -1;
+        int firstNonRequired = -1;
+        for (int i = 0; i < items.size(); i++) {
+            if (items.get(i).required()) {
+                if (firstRequired < 0) {
+                    firstRequired = i;
+                }
+                lastRequired = i;
+            } else if (!items.get(i).deprecated()) {
+                if (firstNonRequired < 0) {
+                    firstNonRequired = i;
+                }
+            }
+        }
+        if (firstRequired >= 0 && firstNonRequired >= 0) {
+            assertThat(lastRequired).isLessThan(firstNonRequired);
+        }
+    }
+
+    @Test
+    void jmsDestinationNameIncluded() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("jms", "producer");
+
+        // destinationName is a path option — should be included
+        assertThat(items).anyMatch(i -> i.key().equals("destinationName"));
+    }
+
+    // --- Existing parameters filtering ---
+
+    @Test
+    void collectExistingParametersFindsKeys() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: kafka",
+                "    parameters:",
+                "      brokers: localhost",
+                "      topic: orders",
+                "      ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on blank line (line 5) inside parameters
+        Set<String> existing = viewer.collectExistingParameters(5);
+        assertThat(existing).containsExactlyInAnyOrder("brokers", "topic");
+    }
+
+    @Test
+    void collectExistingParametersOnBlankLineAfterParametersHeader() throws 
IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: kafka",
+                "    parameters:",
+                "      ",
+                "      brokers: localhost",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on blank line right after parameters: (line 3)
+        Set<String> existing = viewer.collectExistingParameters(3);
+        assertThat(existing).contains("brokers");
+    }
+
+    @Test
+    void existingParametersFilteredFromCompletions() {
+        Set<String> existing = Set.of("brokers", "topic");
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "producer", existing);
+
+        assertThat(items).noneMatch(i -> i.key().equals("brokers"));
+        assertThat(items).noneMatch(i -> i.key().equals("topic"));
+        // other options should still be present
+        assertThat(items).isNotEmpty();
+    }
+
     // --- Helpers that replicate SourceTab logic for testing ---
 
     private List<AutocompletePopup.CompletionItem> 
provideKeyCompletions(String componentName, String role) {
+        return provideKeyCompletions(componentName, role, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> provideKeyCompletions(
+            String componentName, String role, Set<String> existingKeys) {
         ComponentModel model = catalog.componentModel(componentName);
         if (model == null) {
             return List.of();
         }
         boolean isConsumer = "consumer".equals(role);
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
-        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointParameterOptions()) {
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
             if (includeEndpointOption(opt, isConsumer)) {
-                items.add(new AutocompletePopup.CompletionItem(
-                        opt.getName(), opt.getDescription(), opt.getType(),
-                        opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
-                        opt.getGroup()));
+                if (!existingKeys.contains(opt.getName()) || 
opt.isMultiValue()) {
+                    items.add(new AutocompletePopup.CompletionItem(
+                            opt.getName(), opt.getDescription(), opt.getType(),
+                            opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                            opt.getGroup(), opt.isRequired()));
+                }
             }
         }
         
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(ci -> !ci.required())
                 .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
         return items;
     }
 
+    private List<AutocompletePopup.CompletionItem> 
provideComponentCompletions(String role) {
+        boolean isConsumer = "consumer".equals(role);
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (String name : catalog.findComponentNames()) {
+            ComponentModel model = catalog.componentModel(name);
+            if (model == null) {
+                continue;
+            }
+            if (isConsumer && model.isProducerOnly()) {
+                continue;
+            }
+            if (!isConsumer && model.isConsumerOnly()) {
+                continue;
+            }
+            String labels = model.getLabel();
+            String firstLabel = labels != null && !labels.isEmpty()
+                    ? labels.split(",")[0].trim()
+                    : "component";
+            items.add(new AutocompletePopup.CompletionItem(
+                    model.getScheme(), model.getDescription(), firstLabel,
+                    null, model.isDeprecated(), model.getDeprecationNote(),
+                    labels));
+        }
+        items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
     private static boolean 
includeEndpointOption(ComponentModel.EndpointOptionModel opt, boolean 
isConsumer) {
         String label = opt.getLabel();
         if (label == null || label.isEmpty()) {

Reply via email to