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 d7f3713fd867 chore(tui): Add Tab-completion for YAML DSL parameters in 
edit mode
d7f3713fd867 is described below

commit d7f3713fd86743123e3b53c1f0c7736a2b0d84f3
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Aug 4 08:54:16 2026 +0200

    chore(tui): Add Tab-completion for YAML DSL parameters in edit mode
    
    Tab completion now works inside YAML route `parameters:` blocks.
    Key completion shows endpoint options from the Camel catalog with
    consumer/producer filtering. Value completion shows enum choices,
    boolean values, and property placeholders. Placeholders are filtered
    to only show items whose actual value matches a valid choice
    (enum match, boolean, or numeric for integer/long types).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/AutocompletePopup.java |   2 +-
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 272 ++++++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 275 +++++++++++-
 .../core/commands/tui/YamlCompletionTest.java      | 495 +++++++++++++++++++++
 4 files changed, 1030 insertions(+), 14 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 6c5c2dec4bf8..8ea1a047af69 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
@@ -277,7 +277,7 @@ class AutocompletePopup {
             Style keyStyle = ci.deprecated() ? deprecatedStyle : normalStyle;
 
             String displayKey = key;
-            if (!key.endsWith(".")) {
+            if (!key.startsWith("{{") && !key.endsWith(".")) {
                 int lastDot = key.lastIndexOf('.');
                 if (lastDot >= 0) {
                     displayKey = key.substring(lastDot + 1);
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 a9e878121d25..84560c6782d7 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
@@ -332,6 +332,23 @@ class SourceTab extends AbstractTab {
                 - **w** — toggle word wrap
                 - **Esc/c** — close source viewer
 
+                ## Edit Mode (Tab Completion)
+                Press **e** to enter edit mode, then **Tab** for context-aware 
completion:
+
+                **application.properties:**
+                - Key completion for `camel.main.*`, `camel.component.*`, 
`camel.dataformat.*`,
+                  and `camel.language.*` options from the Camel catalog
+                - Value completion with enum choices, boolean values, and 
`{{placeholder}}` suggestions
+
+                **YAML DSL routes:**
+                - Inside `parameters:` blocks, key completion shows endpoint 
options from the
+                  Camel catalog, filtered by consumer/producer role
+                - Value completion shows enum choices, boolean values, and 
`{{placeholder}}`
+                  suggestions from your `.properties` files
+
+                Use **Up/Down** to navigate, **Enter** to accept, **Esc** to 
dismiss, and
+                type to filter the completion list.
+
                 ## General
                 - **Tab** — toggle focus between file list and source viewer
                 - The focused panel title is highlighted; the unfocused panel 
dims
@@ -505,8 +522,13 @@ class SourceTab extends AbstractTab {
                 if (isCamelSourceFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::provideCamelQuickDocs);
                     sourceViewer.setDeprecatedLineScanner(null);
-                    sourceViewer.setAutocompleteProvider(null);
-                    sourceViewer.setAutocompleteValueProvider(null);
+                    if (isYamlFile(filePath)) {
+                        
sourceViewer.setAutocompleteProvider(this::provideYamlKeyCompletions);
+                        
sourceViewer.setAutocompleteValueProvider(this::provideYamlValueCompletions);
+                    } else {
+                        sourceViewer.setAutocompleteProvider(null);
+                        sourceViewer.setAutocompleteValueProvider(null);
+                    }
                 } else if (isPropertiesFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::providePropertiesQuickDocs);
                     
sourceViewer.setDeprecatedLineScanner(this::scanDeprecatedProperties);
@@ -598,6 +620,11 @@ class SourceTab extends AbstractTab {
         return 
path.getFileName().toString().toLowerCase().endsWith(".properties");
     }
 
+    private static boolean isYamlFile(Path path) {
+        String name = path.getFileName().toString().toLowerCase();
+        return name.endsWith(".yaml") || name.endsWith(".yml");
+    }
+
     private List<AutocompletePopup.CompletionItem> 
providePropertyCompletions(String linePrefix) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null) {
@@ -731,13 +758,13 @@ class SourceTab extends AbstractTab {
     private List<AutocompletePopup.CompletionItem> 
providePropertyValueCompletions(String key) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || key == null || key.isEmpty()) {
-            return List.of();
+            return loadPropertyPlaceholders();
         }
         ensureMainOptionsCache(catalog);
 
         BaseOptionModel opt = lookupOption(catalog, key);
         if (opt == null) {
-            return List.of();
+            return loadPropertyPlaceholders();
         }
 
         String optDesc = opt.getDescription();
@@ -746,28 +773,36 @@ class SourceTab extends AbstractTab {
         String optGroup = opt.getGroup();
 
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        java.util.function.Predicate<String> valueFilter = null;
 
         // enum values
         List<String> enums = opt.getEnums();
         if (enums != null && !enums.isEmpty()) {
+            java.util.Set<String> validValues = new java.util.HashSet<>();
             for (String value : enums) {
+                validValues.add(value.toLowerCase());
                 boolean isDefault = value.equals(String.valueOf(optDefault));
                 items.add(new AutocompletePopup.CompletionItem(
                         value, optDesc, optType, isDefault ? value : 
optDefault,
                         false, null, optGroup));
             }
-            return items;
-        }
-
-        // boolean values
-        if ("boolean".equalsIgnoreCase(optType) || 
"java.lang.Boolean".equals(opt.getJavaType())) {
+            valueFilter = v -> validValues.contains(v.toLowerCase());
+        } else if ("boolean".equalsIgnoreCase(optType) || 
"java.lang.Boolean".equals(opt.getJavaType())) {
+            valueFilter = v -> "true".equalsIgnoreCase(v) || 
"false".equalsIgnoreCase(v);
             items.add(new AutocompletePopup.CompletionItem(
                     "true", optDesc, "boolean", optDefault, false, null, 
optGroup));
             items.add(new AutocompletePopup.CompletionItem(
                     "false", optDesc, "boolean", optDefault, false, null, 
optGroup));
-            return items;
+        } else if (isNumericType(optType, opt.getJavaType())) {
+            valueFilter = SourceTab::isNumericValue;
         }
 
+        // only include placeholders whose actual value is compatible with the 
option type
+        for (AutocompletePopup.CompletionItem ph : loadPropertyPlaceholders()) 
{
+            if (valueFilter == null || (ph.description() != null && 
valueFilter.test(ph.description()))) {
+                items.add(ph);
+            }
+        }
         return items;
     }
 
@@ -824,6 +859,49 @@ class SourceTab extends AbstractTab {
         return null;
     }
 
+    private static boolean isNumericType(String type, String javaType) {
+        if (type != null) {
+            switch (type.toLowerCase()) {
+                case "integer":
+                case "int":
+                case "long":
+                case "short":
+                case "byte":
+                case "float":
+                case "double":
+                case "number":
+                    return true;
+            }
+        }
+        if (javaType != null) {
+            switch (javaType) {
+                case "int":
+                case "long":
+                case "short":
+                case "byte":
+                case "float":
+                case "double":
+                case "java.lang.Integer":
+                case "java.lang.Long":
+                case "java.lang.Short":
+                case "java.lang.Byte":
+                case "java.lang.Float":
+                case "java.lang.Double":
+                    return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isNumericValue(String value) {
+        try {
+            Double.parseDouble(value);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
     private static String capitalize(String s) {
         if (s == null || s.isEmpty()) {
             return s;
@@ -831,6 +909,180 @@ class SourceTab extends AbstractTab {
         return Character.toUpperCase(s.charAt(0)) + s.substring(1);
     }
 
+    // ---- YAML DSL completion ----
+
+    private List<AutocompletePopup.CompletionItem> 
provideYamlKeyCompletions(String context) {
+        if (context == null || !context.startsWith("yaml:")) {
+            return List.of();
+        }
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "yaml:componentName:consumer|producer"
+        String[] parts = context.substring(5).split(":", 2);
+        if (parts.length < 2) {
+            return List.of();
+        }
+        String componentName = parts[0];
+        String role = parts[1];
+        boolean isConsumer = "consumer".equals(role);
+
+        ComponentModel model = catalog.componentModel(componentName);
+        if (model == null) {
+            return List.of();
+        }
+
+        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()));
+            }
+        }
+
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(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()) {
+            return true;
+        }
+        if (label.contains("consumer") && label.contains("producer")) {
+            return true;
+        }
+        if (isConsumer) {
+            return !label.contains("producer");
+        } else {
+            return !label.contains("consumer");
+        }
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideYamlValueCompletions(String context) {
+        if (context == null || !context.startsWith("yaml:")) {
+            return List.of();
+        }
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "yaml:componentName:optionName"
+        String[] parts = context.substring(5).split(":", 2);
+        if (parts.length < 2) {
+            return List.of();
+        }
+        String componentName = parts[0];
+        String optionName = parts[1];
+
+        ComponentModel model = catalog.componentModel(componentName);
+        if (model == null) {
+            return loadPropertyPlaceholders();
+        }
+
+        ComponentModel.EndpointOptionModel opt = null;
+        for (ComponentModel.EndpointOptionModel o : 
model.getEndpointOptions()) {
+            if (o.getName().equals(optionName)) {
+                opt = o;
+                break;
+            }
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        java.util.function.Predicate<String> valueFilter = null;
+
+        if (opt != null) {
+            List<String> enums = opt.getEnums();
+            if (enums != null && !enums.isEmpty()) {
+                java.util.Set<String> validValues = new java.util.HashSet<>();
+                for (String value : enums) {
+                    validValues.add(value.toLowerCase());
+                    boolean isDefault = 
value.equals(String.valueOf(opt.getDefaultValue()));
+                    items.add(new AutocompletePopup.CompletionItem(
+                            value, opt.getDescription(), opt.getType(),
+                            isDefault ? value : opt.getDefaultValue(),
+                            false, null, opt.getGroup()));
+                }
+                valueFilter = v -> validValues.contains(v.toLowerCase());
+            } else if ("boolean".equalsIgnoreCase(opt.getType())
+                    || "java.lang.Boolean".equals(opt.getJavaType())) {
+                valueFilter = v -> "true".equalsIgnoreCase(v) || 
"false".equalsIgnoreCase(v);
+                items.add(new AutocompletePopup.CompletionItem(
+                        "true", opt.getDescription(), "boolean", 
opt.getDefaultValue(),
+                        false, null, opt.getGroup()));
+                items.add(new AutocompletePopup.CompletionItem(
+                        "false", opt.getDescription(), "boolean", 
opt.getDefaultValue(),
+                        false, null, opt.getGroup()));
+            } else if (isNumericType(opt.getType(), opt.getJavaType())) {
+                valueFilter = SourceTab::isNumericValue;
+            }
+        }
+
+        // only include placeholders whose actual value is compatible with the 
option type
+        for (AutocompletePopup.CompletionItem ph : loadPropertyPlaceholders()) 
{
+            if (valueFilter == null || (ph.description() != null && 
valueFilter.test(ph.description()))) {
+                items.add(ph);
+            }
+        }
+        return items;
+    }
+
+    // ---- Property placeholder loading ----
+
+    private List<AutocompletePopup.CompletionItem> placeholderCache;
+    private long placeholderCacheTime;
+    private Path placeholderCacheDir;
+
+    private List<AutocompletePopup.CompletionItem> loadPropertyPlaceholders() {
+        if (rootDir == null || !java.nio.file.Files.isDirectory(rootDir)) {
+            return List.of();
+        }
+
+        long now = System.currentTimeMillis();
+        if (placeholderCache != null && rootDir.equals(placeholderCacheDir) && 
(now - placeholderCacheTime) < 5000) {
+            return placeholderCache;
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        try (var stream = java.nio.file.Files.list(rootDir)) {
+            stream.filter(p -> 
p.getFileName().toString().endsWith(".properties"))
+                    .forEach(p -> {
+                        try {
+                            for (String line : 
java.nio.file.Files.readAllLines(p)) {
+                                String trimmed = line.trim();
+                                if (trimmed.isEmpty() || 
trimmed.startsWith("#") || trimmed.startsWith("!")) {
+                                    continue;
+                                }
+                                int eq = trimmed.indexOf('=');
+                                if (eq > 0) {
+                                    String key = trimmed.substring(0, 
eq).trim();
+                                    String value = trimmed.substring(eq + 
1).trim();
+                                    items.add(new 
AutocompletePopup.CompletionItem(
+                                            "{{" + key + "}}", value, 
"placeholder",
+                                            null, false, null, 
p.getFileName().toString()));
+                                }
+                            }
+                        } catch (IOException e) {
+                            // skip unreadable files
+                        }
+                    });
+        } catch (IOException e) {
+            return List.of();
+        }
+
+        items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        placeholderCache = items;
+        placeholderCacheTime = now;
+        placeholderCacheDir = rootDir;
+        return items;
+    }
+
     private Map<Integer, List<SourceViewer.DocEntry>> 
providePropertiesQuickDocs(List<JsonObject> codeData) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || codeData.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 21cf225b19eb..93ffd93fe139 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
@@ -484,7 +484,7 @@ class SourceViewer {
             dirty = true;
             return true;
         }
-        if (ke.isKey(KeyCode.TAB) && autocompleteProvider != null && 
isPropertiesFile()) {
+        if (ke.isKey(KeyCode.TAB) && autocompleteProvider != null) {
             openAutocomplete();
             return true;
         }
@@ -531,7 +531,186 @@ class SourceViewer {
                 && 
editableFile.getFileName().toString().toLowerCase().endsWith(".properties");
     }
 
+    private boolean isCamelYamlFile() {
+        if (editableFile == null) {
+            return false;
+        }
+        String name = editableFile.getFileName().toString().toLowerCase();
+        return name.endsWith(".yaml") || name.endsWith(".yml");
+    }
+
+    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> PRODUCER_EIPS
+            = java.util.Set.of("to", "toD", "to-d", "wireTap", "wire-tap", 
"enrich");
+
+    YamlEndpointContext findEnclosingComponent(int fromRow) {
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // blank lines: find the nearest preceding non-blank line for context
+        if (cursorLine.isBlank()) {
+            for (int i = fromRow - 1; i >= 0; i--) {
+                String prev = editState.getLine(i);
+                if (!prev.isBlank()) {
+                    if (prev.trim().startsWith("parameters:")) {
+                        // blank line right after parameters: — cursor is 
inside the block
+                        cursorIndent = countLeadingSpaces(prev) + 1;
+                        fromRow = i;
+                    } else {
+                        return findEnclosingComponent(i);
+                    }
+                    break;
+                }
+            }
+        }
+
+        int parametersRow = -1;
+        int parametersIndent = -1;
+
+        for (int i = fromRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            String trimmed = line.trim();
+
+            if (trimmed.startsWith("parameters:") && indent < cursorIndent) {
+                parametersRow = i;
+                parametersIndent = indent;
+                break;
+            }
+            if (i < fromRow && indent < cursorIndent && 
!trimmed.startsWith("#")) {
+                break;
+            }
+        }
+
+        if (parametersRow < 0) {
+            return null;
+        }
+
+        String foundScheme = null;
+        for (int i = parametersRow - 1; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            String trimmed = line.trim();
+
+            if (indent == parametersIndent) {
+                if (foundScheme == null && (trimmed.startsWith("uri:") || 
trimmed.startsWith("- uri:"))) {
+                    foundScheme = extractSchemeFromUriLine(trimmed);
+                }
+            }
+
+            if (indent < parametersIndent) {
+                String eipName = extractEipName(trimmed);
+                if (foundScheme == null) {
+                    foundScheme = extractInlineUri(trimmed);
+                }
+                if (foundScheme != null) {
+                    boolean consumer = eipName != null && 
CONSUMER_EIPS.contains(eipName);
+                    return new YamlEndpointContext(foundScheme, consumer);
+                }
+                break;
+            }
+        }
+
+        if (foundScheme != null) {
+            return new YamlEndpointContext(foundScheme, false);
+        }
+        return null;
+    }
+
+    private static int countLeadingSpaces(String line) {
+        int count = 0;
+        for (int i = 0; i < line.length(); i++) {
+            if (line.charAt(i) == ' ') {
+                count++;
+            } else {
+                break;
+            }
+        }
+        return count;
+    }
+
+    private static String extractSchemeFromUriLine(String trimmed) {
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx < 0) {
+            return null;
+        }
+        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);
+        }
+        int schemeEnd = value.indexOf(':');
+        if (schemeEnd > 0) {
+            return value.substring(0, schemeEnd);
+        }
+        if (!value.isEmpty()) {
+            return value;
+        }
+        return null;
+    }
+
+    private static String extractEipName(String trimmed) {
+        String line = trimmed;
+        if (line.startsWith("- ")) {
+            line = line.substring(2).trim();
+        }
+        int colonIdx = line.indexOf(':');
+        if (colonIdx > 0) {
+            return line.substring(0, colonIdx).trim();
+        }
+        return null;
+    }
+
+    private static String extractInlineUri(String trimmed) {
+        String line = trimmed;
+        if (line.startsWith("- ")) {
+            line = line.substring(2).trim();
+        }
+        int colonIdx = line.indexOf(':');
+        if (colonIdx <= 0) {
+            return null;
+        }
+        String eipPart = line.substring(0, colonIdx).trim();
+        if (!CONSUMER_EIPS.contains(eipPart) && 
!PRODUCER_EIPS.contains(eipPart)) {
+            return null;
+        }
+        String uriPart = line.substring(colonIdx + 1).trim();
+        if (uriPart.isEmpty()) {
+            return null;
+        }
+        if (uriPart.startsWith("\"") || uriPart.startsWith("'")) {
+            uriPart = uriPart.substring(1);
+        }
+        if (uriPart.endsWith("\"") || uriPart.endsWith("'")) {
+            uriPart = uriPart.substring(0, uriPart.length() - 1);
+        }
+        int schemeEnd = uriPart.indexOf(':');
+        if (schemeEnd > 0) {
+            return uriPart.substring(0, schemeEnd);
+        }
+        return null;
+    }
+
     private void openAutocomplete() {
+        if (isCamelYamlFile()) {
+            openYamlAutocomplete();
+        } else {
+            openPropertiesAutocomplete();
+        }
+    }
+
+    private void openPropertiesAutocomplete() {
         String lineText = editState.getLine(editState.cursorRow());
         int col = editState.cursorCol();
         String textBeforeCursor = col <= lineText.length() ? 
lineText.substring(0, col) : lineText;
@@ -569,11 +748,61 @@ class SourceViewer {
         }
     }
 
+    private void openYamlAutocomplete() {
+        int row = editState.cursorRow();
+        String lineText = editState.getLine(row);
+        String trimmed = lineText.trim();
+        if (trimmed.startsWith("- ")) {
+            trimmed = trimmed.substring(2).trim();
+        }
+
+        YamlEndpointContext ctx = findEnclosingComponent(row);
+        if (ctx == null) {
+            return;
+        }
+
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx > 0) {
+            // value completion — cursor is on a line with key: or key: value
+            String optionName = trimmed.substring(0, colonIdx).trim();
+            String valueText = trimmed.substring(colonIdx + 1).trim();
+            if (valueText.startsWith("\"") || valueText.startsWith("'")) {
+                valueText = valueText.substring(1);
+            }
+            if (valueText.endsWith("\"") || valueText.endsWith("'")) {
+                valueText = valueText.substring(0, valueText.length() - 1);
+            }
+            if (autocompleteValueProvider != null) {
+                String context = "yaml:" + ctx.component() + ":" + optionName;
+                List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(context);
+                if (values != null && !values.isEmpty()) {
+                    autocompletePopup = new AutocompletePopup(values, "", 
valueText, true);
+                }
+            }
+        } else {
+            // 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";
+            String context = "yaml:" + ctx.component() + ":" + role;
+            List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(context);
+            if (items != null && !items.isEmpty()) {
+                autocompletePopup = new AutocompletePopup(items, filter, 
filter);
+            }
+        }
+    }
+
     private void insertCompletion(AutocompletePopup.CompletionItem item, 
boolean valueMode) {
         dirty = true;
         String currentLine = editState.getLine(editState.cursorRow());
+        if (isCamelYamlFile()) {
+            insertYamlCompletion(item, valueMode, currentLine);
+        } else {
+            insertPropertiesCompletion(item, valueMode, currentLine);
+        }
+    }
+
+    private void insertPropertiesCompletion(AutocompletePopup.CompletionItem 
item, boolean valueMode, String currentLine) {
         if (valueMode) {
-            // replace value portion (after =)
             int eq = currentLine.indexOf('=');
             if (eq >= 0) {
                 String keyPart = currentLine.substring(0, eq + 1);
@@ -597,6 +826,46 @@ class SourceViewer {
         }
     }
 
+    private void insertYamlCompletion(AutocompletePopup.CompletionItem item, 
boolean valueMode, String currentLine) {
+        int indent = countLeadingSpaces(currentLine);
+        // blank lines: derive indent from the nearest preceding non-blank line
+        if (currentLine.isBlank() && indent == 0) {
+            int row = editState.cursorRow();
+            for (int i = row - 1; i >= 0; i--) {
+                String prev = editState.getLine(i);
+                if (!prev.isBlank()) {
+                    indent = countLeadingSpaces(prev);
+                    if (prev.trim().startsWith("parameters:")) {
+                        indent += 2;
+                    }
+                    break;
+                }
+            }
+        }
+        String indentStr = " ".repeat(indent);
+
+        editState.moveCursorToLineStart();
+        for (int i = 0; i < currentLine.length(); i++) {
+            editState.deleteForward();
+        }
+
+        if (valueMode) {
+            String trimmed = currentLine.trim();
+            if (trimmed.startsWith("- ")) {
+                trimmed = trimmed.substring(2).trim();
+            }
+            int colonIdx = trimmed.indexOf(':');
+            if (colonIdx > 0) {
+                String keyPart = trimmed.substring(0, colonIdx);
+                editState.insert(indentStr + keyPart + ": " + item.key());
+            } else {
+                editState.insert(indentStr + item.key());
+            }
+        } else {
+            editState.insert(indentStr + item.key() + ": ");
+        }
+    }
+
     private void saveEdit() {
         if (!editMode || editableFile == null) {
             return;
@@ -925,7 +1194,7 @@ class SourceViewer {
             TuiHelper.hint(spans, "Esc", "cancel");
             TuiHelper.hint(spans, "F5", "save & close");
             TuiHelper.hint(spans, "Shift+F5", "save");
-            if (autocompleteProvider != null && isPropertiesFile()) {
+            if (autocompleteProvider != null) {
                 TuiHelper.hint(spans, "Tab", "complete");
             }
             TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "move");
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
new file mode 100644
index 000000000000..d68d508242e0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlCompletionTest.java
@@ -0,0 +1,495 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class YamlCompletionTest {
+
+    private static CamelCatalog catalog;
+
+    @TempDir
+    Path tempDir;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+    }
+
+    // --- Context detection via SourceViewer ---
+
+    @Test
+    void findComponentInsideParametersBlock() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: \"timer:tick\"",
+                "      parameters:",
+                "        period: 1000",
+                "      steps:",
+                "        - to:",
+                "            uri: \"kafka:myTopic\"",
+                "            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 "brokers: localhost" (line 9, 0-based)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(9);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("kafka");
+        assertThat(ctx.consumer()).isFalse();
+    }
+
+    @Test
+    void findConsumerComponentInsideFromParameters() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: \"timer:tick\"",
+                "      parameters:",
+                "        period: 1000",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on "period: 1000" (line 4, 0-based)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("timer");
+        assertThat(ctx.consumer()).isTrue();
+    }
+
+    @Test
+    void findComponentWithUriWithoutPath() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: timer",
+                "      parameters:",
+                "        timerName: tick",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on "timerName: tick" (line 4, 0-based)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("timer");
+        assertThat(ctx.consumer()).isTrue();
+    }
+
+    @Test
+    void findComponentOnBlankLineInsideParameters() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: timer",
+                "      parameters:",
+                "        timerName: tick",
+                "",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on the blank line (line 5, 0-based) — still inside 
parameters block
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(5);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("timer");
+        assertThat(ctx.consumer()).isTrue();
+    }
+
+    @Test
+    void findComponentOnBlankLineAfterParameters() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: kafka:orders",
+                "    parameters:",
+                "",
+                "");
+
+        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)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(3);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("kafka");
+        assertThat(ctx.consumer()).isTrue();
+    }
+
+    @Test
+    void returnsNullOutsideParametersBlock() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: \"timer:tick\"",
+                "      steps:",
+                "        - log: \"hello\"",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on "- log:" (line 4, 0-based) — inside steps, not parameters
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(4);
+        assertThat(ctx).isNull();
+    }
+
+    @Test
+    void returnsNullOnRouteLevel() throws IOException {
+        String yaml = String.join("\n",
+                "- route:",
+                "    from:",
+                "      uri: \"timer:tick\"",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on "from:" (line 1, 0-based)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(1);
+        assertThat(ctx).isNull();
+    }
+
+    @Test
+    void findComponentWithInlineUri() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: \"timer:tick\"",
+                "    steps:",
+                "      - to: \"kafka:orders\"",
+                "        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 "brokers: localhost" (line 5, 0-based)
+        SourceViewer.YamlEndpointContext ctx = 
viewer.findEnclosingComponent(5);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.component()).isEqualTo("kafka");
+        assertThat(ctx.consumer()).isFalse();
+    }
+
+    // --- Key completion from catalog ---
+
+    @Test
+    void keyCompletionReturnsKafkaEndpointOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "producer");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("brokers"));
+        // should have descriptions and types
+        var brokers = items.stream().filter(i -> 
i.key().equals("brokers")).findFirst();
+        assertThat(brokers).isPresent();
+        assertThat(brokers.get().description()).isNotNull().isNotEmpty();
+        assertThat(brokers.get().type()).isNotNull();
+    }
+
+    @Test
+    void keyCompletionExcludesPathOptions() {
+        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"));
+    }
+
+    @Test
+    void keyCompletionFiltersConsumerOnlyOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "producer");
+
+        // consumer-only options should not appear for a producer
+        ComponentModel model = catalog.componentModel("kafka");
+        List<String> consumerOnlyOptions = new ArrayList<>();
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointParameterOptions()) {
+            String label = opt.getLabel();
+            if (label != null && label.contains("consumer") && 
!label.contains("producer")) {
+                consumerOnlyOptions.add(opt.getName());
+            }
+        }
+        if (!consumerOnlyOptions.isEmpty()) {
+            for (String opt : consumerOnlyOptions) {
+                assertThat(items).noneMatch(i -> i.key().equals(opt));
+            }
+        }
+    }
+
+    @Test
+    void keyCompletionShowsConsumerOptionsForConsumer() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "consumer");
+
+        // should include consumer options
+        ComponentModel model = catalog.componentModel("kafka");
+        List<String> consumerOptions = new ArrayList<>();
+        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointParameterOptions()) {
+            String label = opt.getLabel();
+            if (label != null && label.contains("consumer") && 
!label.contains("producer")) {
+                consumerOptions.add(opt.getName());
+            }
+        }
+        if (!consumerOptions.isEmpty()) {
+            for (String opt : consumerOptions) {
+                assertThat(items).anyMatch(i -> i.key().equals(opt));
+            }
+        }
+    }
+
+    @Test
+    void keyCompletionSortedNonDeprecatedFirst() {
+        List<AutocompletePopup.CompletionItem> items = 
provideKeyCompletions("kafka", "producer");
+
+        int lastNonDeprecatedIdx = -1;
+        int firstDeprecatedIdx = items.size();
+        for (int i = 0; i < items.size(); i++) {
+            if (!items.get(i).deprecated()) {
+                lastNonDeprecatedIdx = i;
+            } else if (i < firstDeprecatedIdx) {
+                firstDeprecatedIdx = i;
+            }
+        }
+        if (firstDeprecatedIdx < items.size()) {
+            assertThat(lastNonDeprecatedIdx).isLessThan(firstDeprecatedIdx);
+        }
+    }
+
+    // --- Value completion ---
+
+    @Test
+    void valueCompletionReturnsBooleanValues() {
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("kafka", "autoCommitEnable");
+
+        assertThat(items).anyMatch(i -> i.key().equals("true"));
+        assertThat(items).anyMatch(i -> i.key().equals("false"));
+    }
+
+    @Test
+    void valueCompletionReturnsEnumValues() {
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("kafka", "autoOffsetReset");
+
+        // kafka autoOffsetReset has enum values: latest, earliest, none
+        assertThat(items).anyMatch(i -> i.key().equals("latest"));
+        assertThat(items).anyMatch(i -> i.key().equals("earliest"));
+    }
+
+    // --- Property placeholder loading ---
+
+    @Test
+    void loadPlaceholdersFromPropertiesFile() throws IOException {
+        Path propsFile = tempDir.resolve("application.properties");
+        Files.writeString(propsFile, String.join("\n",
+                "# Kafka settings",
+                "kafka.brokers=localhost:9092",
+                "kafka.topic=my-orders",
+                "",
+                "# App settings",
+                "myapp.timeout=30000",
+                ""));
+
+        List<AutocompletePopup.CompletionItem> items = 
loadPlaceholders(tempDir);
+
+        assertThat(items).hasSize(3);
+        assertThat(items).anyMatch(i -> i.key().equals("{{kafka.brokers}}"));
+        assertThat(items).anyMatch(i -> i.key().equals("{{kafka.topic}}"));
+        assertThat(items).anyMatch(i -> i.key().equals("{{myapp.timeout}}"));
+
+        // descriptions should show the property values
+        var brokers = items.stream().filter(i -> 
i.key().equals("{{kafka.brokers}}")).findFirst();
+        assertThat(brokers).isPresent();
+        assertThat(brokers.get().description()).isEqualTo("localhost:9092");
+    }
+
+    @Test
+    void loadPlaceholdersSkipsCommentsAndBlanks() throws IOException {
+        Path propsFile = tempDir.resolve("application.properties");
+        Files.writeString(propsFile, String.join("\n",
+                "# comment",
+                "! another comment",
+                "",
+                "valid.key=value",
+                ""));
+
+        List<AutocompletePopup.CompletionItem> items = 
loadPlaceholders(tempDir);
+
+        assertThat(items).hasSize(1);
+        assertThat(items.get(0).key()).isEqualTo("{{valid.key}}");
+    }
+
+    @Test
+    void loadPlaceholdersReturnsEmptyForNoPropertiesFiles() {
+        List<AutocompletePopup.CompletionItem> items = 
loadPlaceholders(tempDir);
+        assertThat(items).isEmpty();
+    }
+
+    // --- Helpers that replicate SourceTab logic for testing ---
+
+    private List<AutocompletePopup.CompletionItem> 
provideKeyCompletions(String componentName, String role) {
+        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()) {
+            if (includeEndpointOption(opt, isConsumer)) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        opt.getName(), opt.getDescription(), opt.getType(),
+                        opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                        opt.getGroup()));
+            }
+        }
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(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()) {
+            return true;
+        }
+        if (label.contains("consumer") && label.contains("producer")) {
+            return true;
+        }
+        if (isConsumer) {
+            return !label.contains("producer");
+        } else {
+            return !label.contains("consumer");
+        }
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideValueCompletions(String componentName, String optionName) {
+        ComponentModel model = catalog.componentModel(componentName);
+        if (model == null) {
+            return List.of();
+        }
+
+        ComponentModel.EndpointOptionModel opt = null;
+        for (ComponentModel.EndpointOptionModel o : 
model.getEndpointOptions()) {
+            if (o.getName().equals(optionName)) {
+                opt = o;
+                break;
+            }
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        if (opt != null) {
+            List<String> enums = opt.getEnums();
+            if (enums != null && !enums.isEmpty()) {
+                for (String value : enums) {
+                    boolean isDefault = 
value.equals(String.valueOf(opt.getDefaultValue()));
+                    items.add(new AutocompletePopup.CompletionItem(
+                            value, opt.getDescription(), opt.getType(),
+                            isDefault ? value : opt.getDefaultValue(),
+                            false, null, opt.getGroup()));
+                }
+            } else if ("boolean".equalsIgnoreCase(opt.getType())
+                    || "java.lang.Boolean".equals(opt.getJavaType())) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        "true", opt.getDescription(), "boolean", 
opt.getDefaultValue(),
+                        false, null, opt.getGroup()));
+                items.add(new AutocompletePopup.CompletionItem(
+                        "false", opt.getDescription(), "boolean", 
opt.getDefaultValue(),
+                        false, null, opt.getGroup()));
+            }
+        }
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> loadPlaceholders(Path dir) {
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        try (var stream = Files.list(dir)) {
+            stream.filter(p -> 
p.getFileName().toString().endsWith(".properties"))
+                    .forEach(p -> {
+                        try {
+                            for (String line : Files.readAllLines(p)) {
+                                String trimmed = line.trim();
+                                if (trimmed.isEmpty() || 
trimmed.startsWith("#") || trimmed.startsWith("!")) {
+                                    continue;
+                                }
+                                int eq = trimmed.indexOf('=');
+                                if (eq > 0) {
+                                    String key = trimmed.substring(0, 
eq).trim();
+                                    String value = trimmed.substring(eq + 
1).trim();
+                                    items.add(new 
AutocompletePopup.CompletionItem(
+                                            "{{" + key + "}}", value, 
"placeholder",
+                                            null, false, null, 
p.getFileName().toString()));
+                                }
+                            }
+                        } catch (IOException e) {
+                            // skip
+                        }
+                    });
+        } catch (IOException e) {
+            return List.of();
+        }
+        items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+}

Reply via email to