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 6422a66f0615 camel-jbang - TUI tab completion for EIP options in YAML 
editor
6422a66f0615 is described below

commit 6422a66f06150216081b7a1ff5af0553a4e97193
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Aug 4 15:01:32 2026 +0200

    camel-jbang - TUI tab completion for EIP options in YAML editor
    
    camel-jbang - TUI fix EIP tab completion on empty lines
    
    camel-jbang - TUI fix EIP completion indent on empty lines
    
    camel-jbang - TUI scope line highlight in YAML editor
    
    camel-jbang - TUI fix scope highlight to use bold and fix blank-line indent
    
    camel-jbang - TUI auto-indent on Enter in YAML editor
    
    camel-jbang - TUI filter incompatible placeholders from enum/boolean value 
completions and add e shortcut in Files tab
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 138 +++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 399 ++++++++++++++++--
 .../core/commands/tui/YamlCompletionTest.java      | 469 +++++++++++++++++++++
 3 files changed, 966 insertions(+), 40 deletions(-)

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 584a97808d38..f0e9872cdb65 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
@@ -63,6 +63,7 @@ import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.tooling.model.MainModel;
 import org.apache.camel.util.json.JsonArray;
@@ -323,6 +324,7 @@ class SourceTab extends AbstractTab {
                 ## File List (left panel)
                 - **Up/Down** — navigate files
                 - **Enter** — open file or directory
+                - **e** — open file directly in edit mode
                 - **Backspace** — go to parent directory
 
                 ## Source Viewer (right panel)
@@ -355,6 +357,9 @@ class SourceTab extends AbstractTab {
                 - Inside `parameters:` blocks, key completion shows endpoint 
options from the
                   Camel catalog, filtered by consumer/producer role. Required 
options appear
                   first (marked with `*`). Already-specified options are 
excluded.
+                - Inside EIP blocks (e.g. `split:`, `aggregate:`, `filter:`), 
Tab shows
+                  the EIP's configurable options (attribute-type only, 
excluding structural
+                  elements like `steps:` and `expression:`).
                 - Value completion shows enum choices, boolean values, and 
`{{placeholder}}`
                   suggestions from your `.properties` files
 
@@ -517,6 +522,13 @@ class SourceTab extends AbstractTab {
             openSelectedEntry();
             return true;
         }
+        if (ke.isChar('e')) {
+            openSelectedEntry();
+            if (sourceViewer.isVisible() && sourceViewer.isEditable()) {
+                sourceViewer.enterEditMode();
+            }
+            return true;
+        }
         return false;
     }
 
@@ -933,6 +945,11 @@ class SourceTab extends AbstractTab {
             return provideComponentNameCompletions(context.substring(9));
         }
 
+        // EIP option completion
+        if (context.startsWith("yaml-eip:")) {
+            return provideEipKeyCompletions(context.substring(9));
+        }
+
         if (!context.startsWith("yaml:")) {
             return List.of();
         }
@@ -1042,6 +1059,51 @@ class SourceTab extends AbstractTab {
         return items;
     }
 
+    private static final Set<String> EIP_BOILERPLATE = Set.of("id", "note", 
"description", "disabled");
+
+    private List<AutocompletePopup.CompletionItem> 
provideEipKeyCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "eipName" or "eipName:existingKey1,existingKey2,..."
+        String[] parts = contextAfterPrefix.split(":", 2);
+        String eipName = parts[0];
+
+        Set<String> existingKeys = Set.of();
+        if (parts.length > 1 && !parts[1].isEmpty()) {
+            existingKeys = new HashSet<>(Arrays.asList(parts[1].split(",")));
+        }
+
+        EipModel model = catalog.eipModel(eipName);
+        if (model == null) {
+            return List.of();
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (EipModel.EipOptionModel opt : model.getOptions()) {
+            if (!"attribute".equals(opt.getKind())) {
+                continue;
+            }
+            if (EIP_BOILERPLATE.contains(opt.getName())) {
+                continue;
+            }
+            if (existingKeys.contains(opt.getName()) && !opt.isMultiValue()) {
+                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 static boolean 
includeEndpointOption(ComponentModel.EndpointOptionModel opt, boolean 
isConsumer) {
         String label = opt.getLabel();
         if (label == null || label.isEmpty()) {
@@ -1058,7 +1120,16 @@ class SourceTab extends AbstractTab {
     }
 
     private List<AutocompletePopup.CompletionItem> 
provideYamlValueCompletions(String context) {
-        if (context == null || !context.startsWith("yaml:")) {
+        if (context == null) {
+            return List.of();
+        }
+
+        // EIP value completion
+        if (context.startsWith("yaml-eip-value:")) {
+            return provideEipValueCompletions(context.substring(15));
+        }
+
+        if (!context.startsWith("yaml:")) {
             return List.of();
         }
         CamelCatalog catalog = getCatalog();
@@ -1126,6 +1197,71 @@ class SourceTab extends AbstractTab {
         return items;
     }
 
+    private List<AutocompletePopup.CompletionItem> 
provideEipValueCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "eipName:optionName"
+        String[] parts = contextAfterPrefix.split(":", 2);
+        if (parts.length < 2) {
+            return List.of();
+        }
+        String eipName = parts[0];
+        String optionName = parts[1];
+
+        EipModel model = catalog.eipModel(eipName);
+        if (model == null) {
+            return loadPropertyPlaceholders();
+        }
+
+        EipModel.EipOptionModel opt = null;
+        for (EipModel.EipOptionModel o : model.getOptions()) {
+            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;
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 0a39599c2851..d9ce3d7d766e 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
@@ -432,7 +432,13 @@ class SourceViewer {
             return true;
         }
         if (ke.isConfirm()) {
+            int prevRow = editState.cursorRow();
+            String prevLine = editState.getLine(prevRow);
+            int indent = countLeadingSpaces(prevLine);
             editState.insert('\n');
+            if (indent > 0) {
+                editState.insert(" ".repeat(indent));
+            }
             dirty = true;
             return true;
         }
@@ -762,6 +768,258 @@ class SourceViewer {
         return null;
     }
 
+    record YamlEipContext(String eipName) {
+    }
+
+    private static final java.util.Set<String> STRUCTURAL_KEYS
+            = java.util.Set.of("steps", "uri", "parameters", "from", "route", 
"routeConfiguration",
+                    "routeTemplate", "templatedRoute", "rest", "beans");
+
+    YamlEipContext findEnclosingEip(int fromRow) {
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // blank lines: derive indent from context
+        if (cursorLine.isBlank()) {
+            if (cursorIndent == 0) {
+                // truly empty line — look at successor first, then predecessor
+                int succIndent = -1;
+                int lineCount = editState.lineCount();
+                for (int i = fromRow + 1; i < lineCount; i++) {
+                    String next = editState.getLine(i);
+                    if (!next.isBlank()) {
+                        succIndent = countLeadingSpaces(next);
+                        break;
+                    }
+                }
+                if (succIndent > 0) {
+                    cursorIndent = succIndent;
+                } else {
+                    for (int i = fromRow - 1; i >= 0; i--) {
+                        String prev = editState.getLine(i);
+                        if (!prev.isBlank()) {
+                            // treat cursor as sibling of predecessor
+                            cursorIndent = countLeadingSpaces(prev);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // if cursor is inside a parameters: block, defer to component 
completion
+        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) {
+                return null;
+            }
+            if (i < fromRow && indent < cursorIndent) {
+                break;
+            }
+        }
+
+        // walk up to find the parent EIP
+        for (int i = fromRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                String eipName = extractEipName(line.trim());
+                if (eipName != null && !STRUCTURAL_KEYS.contains(eipName)) {
+                    String camelName = dashToCamelCase(eipName);
+                    return new YamlEipContext(camelName);
+                }
+                // keep walking up if we hit a structural key
+                cursorIndent = indent;
+            }
+        }
+        return null;
+    }
+
+    java.util.Set<String> collectExistingSiblingKeys(int fromRow) {
+        java.util.Set<String> keys = new java.util.LinkedHashSet<>();
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // for blank lines, derive indent from nearest non-blank sibling
+        if (cursorLine.isBlank()) {
+            for (int i = fromRow - 1; i >= 0; i--) {
+                String prev = editState.getLine(i);
+                if (!prev.isBlank()) {
+                    cursorIndent = countLeadingSpaces(prev);
+                    break;
+                }
+            }
+        }
+
+        // scan upward for siblings at same indent
+        for (int i = fromRow - 1; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                break;
+            }
+            if (indent == cursorIndent) {
+                String trimmed = line.trim();
+                int colonIdx = trimmed.indexOf(':');
+                if (colonIdx > 0) {
+                    keys.add(trimmed.substring(0, colonIdx).trim());
+                }
+            }
+        }
+        // scan downward for siblings at same indent
+        int lineCount = editState.lineCount();
+        for (int i = fromRow + 1; i < lineCount; i++) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                break;
+            }
+            if (indent == cursorIndent) {
+                String trimmed = line.trim();
+                int colonIdx = trimmed.indexOf(':');
+                if (colonIdx > 0) {
+                    keys.add(trimmed.substring(0, colonIdx).trim());
+                }
+            }
+        }
+        return keys;
+    }
+
+    static String dashToCamelCase(String text) {
+        if (text == null || !text.contains("-")) {
+            return text;
+        }
+        StringBuilder sb = new StringBuilder(text.length());
+        boolean upper = false;
+        for (int i = 0; i < text.length(); i++) {
+            char c = text.charAt(i);
+            if (c == '-') {
+                upper = true;
+            } else {
+                sb.append(upper ? Character.toUpperCase(c) : c);
+                upper = false;
+            }
+        }
+        return sb.toString();
+    }
+
+    int findScopeLineRow(int cursorRow) {
+        if (cursorRow < 0 || cursorRow >= editState.lineCount()) {
+            return -1;
+        }
+        String cursorLine = editState.getLine(cursorRow);
+        String trimmed = cursorLine.trim();
+        if (trimmed.startsWith("- ")) {
+            trimmed = trimmed.substring(2).trim();
+        }
+
+        // if cursor is on a uri: line, scope is this row
+        if (trimmed.startsWith("uri:")) {
+            return cursorRow;
+        }
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx > 0) {
+            String key = trimmed.substring(0, colonIdx).trim();
+            // inline producer/consumer EIP (to:, enrich:) — exclude 
structural keys like from:
+            if (!STRUCTURAL_KEYS.contains(key)
+                    && (CONSUMER_EIPS.contains(key) || 
PRODUCER_EIPS.contains(key))) {
+                return cursorRow;
+            }
+            // EIP definition line in a list (e.g., "- split:", "- log:")
+            if (!STRUCTURAL_KEYS.contains(key) && 
cursorLine.trim().startsWith("- ")) {
+                return cursorRow;
+            }
+        }
+
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // for blank lines, derive indent from context
+        if (cursorLine.isBlank()) {
+            if (cursorIndent == 0) {
+                int lineCount = editState.lineCount();
+                for (int i = cursorRow + 1; i < lineCount; i++) {
+                    String next = editState.getLine(i);
+                    if (!next.isBlank()) {
+                        cursorIndent = countLeadingSpaces(next);
+                        break;
+                    }
+                }
+                if (cursorIndent == 0) {
+                    for (int i = cursorRow - 1; i >= 0; i--) {
+                        String prev = editState.getLine(i);
+                        if (!prev.isBlank()) {
+                            cursorIndent = countLeadingSpaces(prev);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // walk up looking for the scope line
+        int parametersRow = -1;
+        int parametersIndent = -1;
+        for (int i = cursorRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            String t = line.trim();
+
+            if (t.startsWith("parameters:") && indent < cursorIndent) {
+                parametersRow = i;
+                parametersIndent = indent;
+                break;
+            }
+            if (i < cursorRow && indent < cursorIndent) {
+                String eipName = extractEipName(t);
+                if (eipName != null && !STRUCTURAL_KEYS.contains(eipName)) {
+                    return i;
+                }
+                cursorIndent = indent;
+            }
+        }
+
+        // inside parameters: block — find the uri: line at the same indent
+        if (parametersRow >= 0) {
+            for (int i = parametersRow - 1; i >= 0; i--) {
+                String line = editState.getLine(i);
+                if (line.isBlank()) {
+                    continue;
+                }
+                int indent = countLeadingSpaces(line);
+                String t = line.trim();
+                if (indent == parametersIndent && (t.startsWith("uri:") || 
t.startsWith("- uri:"))) {
+                    return i;
+                }
+                if (indent < parametersIndent) {
+                    // check for inline uri on the EIP line itself
+                    String eipName = extractEipName(t);
+                    if (eipName != null && (CONSUMER_EIPS.contains(eipName) || 
PRODUCER_EIPS.contains(eipName))) {
+                        return i;
+                    }
+                    break;
+                }
+            }
+        }
+        return -1;
+    }
+
     private static int countLeadingSpaces(String line) {
         int count = 0;
         for (int i = 0; i < line.length(); i++) {
@@ -906,41 +1164,77 @@ class SourceViewer {
         }
 
         YamlEndpointContext ctx = findEnclosingComponent(row);
-        if (ctx == null) {
+        if (ctx != null) {
+            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";
+                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");
+                }
+            }
             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);
+        // EIP option completion — cursor is inside an EIP block (not in 
parameters:)
+        YamlEipContext eipCtx = findEnclosingEip(row);
+        if (eipCtx != null && autocompleteProvider != null) {
+            int colonIdx = trimmed.indexOf(':');
+            if (colonIdx > 0) {
+                // value completion for EIP option
+                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-eip-value:" + eipCtx.eipName() + 
":" + optionName;
+                    List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(context);
+                    if (values != null && !values.isEmpty()) {
+                        autocompletePopup = new AutocompletePopup(values, "", 
valueText, true);
+                    }
+                }
+            } else {
+                // key completion for EIP options
+                String filter = trimmed;
+                java.util.Set<String> existing = 
collectExistingSiblingKeys(row);
+                String context = "yaml-eip:" + eipCtx.eipName();
+                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(eipCtx.eipName() + " 
options");
                 }
-            }
-        } 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";
-            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");
             }
         }
     }
@@ -982,19 +1276,33 @@ 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
+        // blank lines: derive indent from context
         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;
-                    }
+            // look at next non-blank line first (sibling indent)
+            int lineCount = editState.lineCount();
+            for (int i = row + 1; i < lineCount; i++) {
+                String next = editState.getLine(i);
+                if (!next.isBlank()) {
+                    indent = countLeadingSpaces(next);
                     break;
                 }
             }
+            if (indent == 0) {
+                // no successor — derive from predecessor
+                for (int i = row - 1; i >= 0; i--) {
+                    String prev = editState.getLine(i);
+                    if (!prev.isBlank()) {
+                        indent = countLeadingSpaces(prev);
+                        String trimmed = prev.trim();
+                        if (trimmed.endsWith(":")) {
+                            // cursor is inside this block — indent deeper
+                            indent += trimmed.startsWith("- ") ? 4 : 2;
+                        }
+                        break;
+                    }
+                }
+            }
         }
         String indentStr = " ".repeat(indent);
 
@@ -1348,6 +1656,19 @@ class SourceViewer {
                 .build();
         textArea.renderWithCursor(inner, frame.buffer(), editState, frame);
 
+        // scope line highlight — shows which EIP or uri: line the cursor 
belongs to
+        if (isCamelYamlFile()) {
+            int scopeRow = findScopeLineRow(editState.cursorRow());
+            if (scopeRow >= 0 && scopeRow != editState.cursorRow()) {
+                int relativeRow = scopeRow - editState.scrollRow();
+                if (relativeRow >= 0 && relativeRow < inner.height()) {
+                    int screenY = inner.top() + relativeRow;
+                    Rect lineRect = new Rect(inner.left(), screenY, 
inner.width(), 1);
+                    frame.buffer().setStyle(lineRect, Style.EMPTY.bold());
+                }
+            }
+        }
+
         if (autocompletePopup != null) {
             int cursorRow = editState.cursorRow() - editState.scrollRow();
             int cursorCol = editState.cursorCol() - editState.scrollCol();
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 426700c51514..4b4822cdc3ef 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
@@ -21,12 +21,14 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.EipModel;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -663,6 +665,389 @@ class YamlCompletionTest {
         assertThat(items).isNotEmpty();
     }
 
+    // --- EIP context detection ---
+
+    @Test
+    void findEnclosingEipDetectsSplit() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          expression:",
+                "            simple: \"${body}\"",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(6);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("split");
+    }
+
+    @Test
+    void findEnclosingEipReturnsNullInsideParameters() 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();
+
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(4);
+        assertThat(ctx).isNull();
+    }
+
+    @Test
+    void findEnclosingEipConvertsKebabCase() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - circuit-breaker:",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("circuitBreaker");
+    }
+
+    @Test
+    void findEnclosingEipSkipsStructuralKeys() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          expression:",
+                "            simple: \"${body}\"",
+                "          steps:",
+                "            - log:",
+                "                ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor inside log: block (nested in split's steps)
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(8);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("log");
+    }
+
+    @Test
+    void findEnclosingEipOnEmptyLineBetweenEipAndOptions() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - log:",
+                "",
+                "            message: \"${body}\"",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // empty line between log: and message: should detect log as enclosing 
EIP
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("log");
+    }
+
+    // --- EIP option completion ---
+
+    @Test
+    void eipCompletionIncludesOnlyAttributes() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split");
+
+        // streaming is an attribute — should be included
+        assertThat(items).anyMatch(i -> i.key().equals("streaming"));
+        assertThat(items).anyMatch(i -> i.key().equals("parallelProcessing"));
+    }
+
+    @Test
+    void eipCompletionExcludesBoilerplate() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split");
+
+        assertThat(items).noneMatch(i -> i.key().equals("id"));
+        assertThat(items).noneMatch(i -> i.key().equals("note"));
+        assertThat(items).noneMatch(i -> i.key().equals("description"));
+        assertThat(items).noneMatch(i -> i.key().equals("disabled"));
+    }
+
+    @Test
+    void eipCompletionExcludesExpressionAndElement() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split");
+
+        // expression and outputs are not attribute kind
+        assertThat(items).noneMatch(i -> i.key().equals("expression"));
+        assertThat(items).noneMatch(i -> i.key().equals("outputs"));
+    }
+
+    @Test
+    void eipCompletionExcludesExistingOptions() {
+        Set<String> existing = Set.of("streaming", "delimiter");
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split", existing);
+
+        assertThat(items).noneMatch(i -> i.key().equals("streaming"));
+        assertThat(items).noneMatch(i -> i.key().equals("delimiter"));
+        assertThat(items).isNotEmpty();
+    }
+
+    @Test
+    void eipCompletionForLogEip() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("log");
+
+        assertThat(items).anyMatch(i -> i.key().equals("message"));
+        assertThat(items).anyMatch(i -> i.key().equals("loggingLevel"));
+        assertThat(items).anyMatch(i -> i.key().equals("logName"));
+    }
+
+    @Test
+    void eipValueCompletionForEnum() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipValueCompletions("log", "loggingLevel");
+
+        assertThat(items).anyMatch(i -> i.key().equals("INFO"));
+        assertThat(items).anyMatch(i -> i.key().equals("ERROR"));
+        assertThat(items).anyMatch(i -> i.key().equals("DEBUG"));
+    }
+
+    @Test
+    void eipValueCompletionForBoolean() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipValueCompletions("split", "streaming");
+
+        assertThat(items).anyMatch(i -> i.key().equals("true"));
+        assertThat(items).anyMatch(i -> i.key().equals("false"));
+    }
+
+    @Test
+    void eipValueCompletionFiltersIncompatiblePlaceholders() {
+        List<AutocompletePopup.CompletionItem> placeholders = List.of(
+                new AutocompletePopup.CompletionItem(
+                        "{{greeting.message}}", "Hello World", "placeholder",
+                        null, false, null, "application.properties"),
+                new AutocompletePopup.CompletionItem(
+                        "{{log.level}}", "WARN", "placeholder",
+                        null, false, null, "application.properties"));
+
+        // enum option: only placeholders whose value matches a valid enum 
choice should be included
+        List<AutocompletePopup.CompletionItem> items = 
provideEipValueCompletions("log", "loggingLevel", placeholders);
+
+        assertThat(items).anyMatch(i -> i.key().equals("INFO"));
+        assertThat(items).anyMatch(i -> i.key().equals("ERROR"));
+        // {{log.level}} has value "WARN" which IS a valid enum value
+        assertThat(items).anyMatch(i -> i.key().equals("{{log.level}}"));
+        // {{greeting.message}} has value "Hello World" which is NOT a valid 
enum value
+        assertThat(items).noneMatch(i -> 
i.key().equals("{{greeting.message}}"));
+    }
+
+    @Test
+    void eipValueCompletionAllowsPlaceholdersForStringOptions() {
+        List<AutocompletePopup.CompletionItem> placeholders = List.of(
+                new AutocompletePopup.CompletionItem(
+                        "{{greeting.message}}", "Hello World", "placeholder",
+                        null, false, null, "application.properties"));
+
+        // string option (logName): no type filter, all placeholders should be 
included
+        List<AutocompletePopup.CompletionItem> items = 
provideEipValueCompletions("log", "logName", placeholders);
+
+        assertThat(items).anyMatch(i -> 
i.key().equals("{{greeting.message}}"));
+    }
+
+    // --- collectExistingSiblingKeys ---
+
+    @Test
+    void collectExistingSiblingKeysFindsKeys() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          streaming: true",
+                "          delimiter: \",\"",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        Set<String> keys = viewer.collectExistingSiblingKeys(6);
+        assertThat(keys).containsExactlyInAnyOrder("streaming", "delimiter");
+    }
+
+    // --- dashToCamelCase ---
+
+    @Test
+    void dashToCamelCaseConverts() {
+        
assertThat(SourceViewer.dashToCamelCase("circuit-breaker")).isEqualTo("circuitBreaker");
+        
assertThat(SourceViewer.dashToCamelCase("wire-tap")).isEqualTo("wireTap");
+        assertThat(SourceViewer.dashToCamelCase("split")).isEqualTo("split");
+        assertThat(SourceViewer.dashToCamelCase(null)).isNull();
+    }
+
+    // --- findScopeLineRow ---
+
+    @Test
+    void findScopeLineRowDetectsUriLine() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - to:",
+                "          uri: kafka",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on uri: line → scope is that row
+        assertThat(viewer.findScopeLineRow(1)).isEqualTo(1);
+        assertThat(viewer.findScopeLineRow(4)).isEqualTo(4);
+    }
+
+    @Test
+    void findScopeLineRowInsideParameters() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: kafka",
+                "    parameters:",
+                "      brokers: localhost",
+                "      groupId: test",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor inside parameters: block → scope is the uri: line
+        assertThat(viewer.findScopeLineRow(3)).isEqualTo(1);
+        assertThat(viewer.findScopeLineRow(4)).isEqualTo(1);
+    }
+
+    @Test
+    void findScopeLineRowInsideEip() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          expression:",
+                "            simple: \"${body}\"",
+                "          streaming: true",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on streaming: → scope is split: line
+        assertThat(viewer.findScopeLineRow(6)).isEqualTo(3);
+    }
+
+    @Test
+    void findScopeLineRowOnScopeLineItself() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          streaming: true",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on the split: line itself → returns that row
+        assertThat(viewer.findScopeLineRow(3)).isEqualTo(3);
+    }
+
+    @Test
+    void findScopeLineRowNoScope() 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();
+
+        // cursor on top-level from: → no parent scope
+        assertThat(viewer.findScopeLineRow(0)).isEqualTo(-1);
+    }
+
+    @Test
+    void findScopeLineRowInlineEip() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - to: kafka:topic",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        // cursor on inline to: line → scope is that row
+        assertThat(viewer.findScopeLineRow(3)).isEqualTo(3);
+    }
+
     // --- Helpers that replicate SourceTab logic for testing ---
 
     private List<AutocompletePopup.CompletionItem> 
provideKeyCompletions(String componentName, String role) {
@@ -773,6 +1158,90 @@ class YamlCompletionTest {
         return items;
     }
 
+    private static final Set<String> EIP_BOILERPLATE = Set.of("id", "note", 
"description", "disabled");
+
+    private List<AutocompletePopup.CompletionItem> 
provideEipKeyCompletions(String eipName) {
+        return provideEipKeyCompletions(eipName, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideEipKeyCompletions(String eipName, Set<String> existingKeys) {
+        EipModel model = catalog.eipModel(eipName);
+        if (model == null) {
+            return List.of();
+        }
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (EipModel.EipOptionModel opt : model.getOptions()) {
+            if (!"attribute".equals(opt.getKind())) {
+                continue;
+            }
+            if (EIP_BOILERPLATE.contains(opt.getName())) {
+                continue;
+            }
+            if (existingKeys.contains(opt.getName()) && !opt.isMultiValue()) {
+                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(ci -> !ci.required())
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideEipValueCompletions(String eipName, String optionName) {
+        return provideEipValueCompletions(eipName, optionName, List.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> provideEipValueCompletions(
+            String eipName, String optionName, 
List<AutocompletePopup.CompletionItem> placeholders) {
+        EipModel model = catalog.eipModel(eipName);
+        if (model == null) {
+            return List.of();
+        }
+        EipModel.EipOptionModel opt = null;
+        for (EipModel.EipOptionModel o : model.getOptions()) {
+            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()) {
+                Set<String> validValues = new 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()));
+            }
+        }
+        for (AutocompletePopup.CompletionItem ph : placeholders) {
+            if (valueFilter == null || (ph.description() != null && 
valueFilter.test(ph.description()))) {
+                items.add(ph);
+            }
+        }
+        return items;
+    }
+
     private List<AutocompletePopup.CompletionItem> loadPlaceholders(Path dir) {
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
         try (var stream = Files.list(dir)) {

Reply via email to