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

commit 2ba072fa2c4d24521a032ea5624e2e656eec54ca
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Aug 5 22:21:44 2026 +0200

    camel-tui - YAML DSL tab completion for canonical format expressions, data 
formats, route and language options
    
    Add tab completion support for canonical YAML DSL format:
    - Expression key completion inside EIPs (split, filter, setHeader, etc.)
    - Language name completion inside expression: blocks (simple, jsonpath, 
xpath, etc.)
    - Language option completion inside language blocks (expression, 
resultType, trim, etc.)
    - Data format name completion inside marshal/unmarshal blocks
    - Data format option completion inside data format blocks
    - Route-level option completion (autoStartup, streamCache, logMask, etc.)
    - Value completion (boolean, enum) for language and data format options
    - Sort required options first, deprecated last
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 330 ++++++++++++
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 312 ++++++++++-
 .../core/commands/tui/YamlCompletionTest.java      | 600 ++++++++++++++++++++-
 3 files changed, 1236 insertions(+), 6 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 8d4d1ddcfb3b..bc107776263a 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
@@ -957,6 +957,26 @@ class SourceTab extends AbstractTab {
             return provideEipKeyCompletions(context.substring(9));
         }
 
+        // expression language name completion inside expression: block
+        if (context.startsWith("yaml-expr:")) {
+            return provideExpressionLanguageCompletions(context.substring(10));
+        }
+
+        // language option completion inside a language block under expression:
+        if (context.startsWith("yaml-lang-opt:")) {
+            return provideLanguageOptionCompletions(context.substring(14));
+        }
+
+        // data format name completion inside marshal/unmarshal
+        if (context.startsWith("yaml-df:")) {
+            return provideDataFormatNameCompletions(context.substring(8));
+        }
+
+        // data format option completion
+        if (context.startsWith("yaml-df-opt:")) {
+            return provideDataFormatOptionCompletions(context.substring(12));
+        }
+
         if (!context.startsWith("yaml:")) {
             return List.of();
         }
@@ -1090,6 +1110,306 @@ class SourceTab extends AbstractTab {
 
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
         for (EipModel.EipOptionModel opt : model.getOptions()) {
+            if ("expression".equals(opt.getKind()) || 
"element".equals(opt.getKind())) {
+                if (EIP_BOILERPLATE.contains(opt.getName())) {
+                    continue;
+                }
+                if (existingKeys.contains(opt.getName())) {
+                    continue;
+                }
+                items.add(new AutocompletePopup.CompletionItem(
+                        opt.getName(), opt.getDescription(), opt.getType(),
+                        opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                        opt.getGroup(), opt.isRequired()));
+                continue;
+            }
+            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 List<AutocompletePopup.CompletionItem> 
provideExpressionLanguageCompletions(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 (!"expression".equals(opt.getKind())) {
+                continue;
+            }
+            List<String> oneOfs = opt.getOneOfs();
+            if (oneOfs == null || oneOfs.isEmpty()) {
+                continue;
+            }
+            if (oneOfs.stream().anyMatch(existingKeys::contains)) {
+                continue;
+            }
+            for (String langName : oneOfs) {
+                LanguageModel langModel = catalog.languageModel(langName);
+                String desc = langModel != null
+                        ? langModel.getTitle() + " - " + 
langModel.getDescription()
+                        : langName;
+                String label = langModel != null ? langModel.getLabel() : 
"language";
+                boolean dep = langModel != null && langModel.isDeprecated();
+                String depNote = langModel != null ? 
langModel.getDeprecationNote() : null;
+                items.add(new AutocompletePopup.CompletionItem(
+                        langName, desc, "language",
+                        null, dep, depNote, label));
+            }
+        }
+
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideLanguageOptionCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "languageName" or 
"languageName:existingKey1,existingKey2,..."
+        String[] parts = contextAfterPrefix.split(":", 2);
+        String langName = parts[0];
+
+        Set<String> existingKeys = Set.of();
+        if (parts.length > 1 && !parts[1].isEmpty()) {
+            existingKeys = new HashSet<>(Arrays.asList(parts[1].split(",")));
+        }
+
+        LanguageModel model = catalog.languageModel(langName);
+        if (model == null) {
+            return List.of();
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (LanguageModel.LanguageOptionModel opt : model.getOptions()) {
+            if (!"attribute".equals(opt.getKind()) && 
!"value".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 List<AutocompletePopup.CompletionItem> 
provideLanguageValueCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "languageName:optionName"
+        String[] parts = contextAfterPrefix.split(":", 2);
+        if (parts.length < 2) {
+            return List.of();
+        }
+        String langName = parts[0];
+        String optionName = parts[1];
+
+        LanguageModel model = catalog.languageModel(langName);
+        if (model == null) {
+            return List.of();
+        }
+
+        LanguageModel.LanguageOptionModel opt = null;
+        for (LanguageModel.LanguageOptionModel o : model.getOptions()) {
+            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> 
provideDataFormatValueCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        String[] parts = contextAfterPrefix.split(":", 2);
+        if (parts.length < 2) {
+            return List.of();
+        }
+        String dfName = parts[0];
+        String optionName = parts[1];
+
+        DataFormatModel model = catalog.dataFormatModel(dfName);
+        if (model == null) {
+            return List.of();
+        }
+
+        DataFormatModel.DataFormatOptionModel opt = null;
+        for (DataFormatModel.DataFormatOptionModel o : model.getOptions()) {
+            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> 
provideDataFormatNameCompletions(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()) {
+            List<String> oneOfs = opt.getOneOfs();
+            if (oneOfs == null || oneOfs.isEmpty()) {
+                continue;
+            }
+            // only offer data format names if none is already specified
+            if (oneOfs.stream().anyMatch(existingKeys::contains)) {
+                continue;
+            }
+            for (String dfName : oneOfs) {
+                DataFormatModel dfModel = catalog.dataFormatModel(dfName);
+                String desc = dfModel != null
+                        ? dfModel.getTitle() + " - " + dfModel.getDescription()
+                        : dfName;
+                String label = dfModel != null ? dfModel.getLabel() : 
"dataformat";
+                boolean dep = dfModel != null && dfModel.isDeprecated();
+                String depNote = dfModel != null ? 
dfModel.getDeprecationNote() : null;
+                items.add(new AutocompletePopup.CompletionItem(
+                        dfName, desc, "dataformat",
+                        null, dep, depNote, label));
+            }
+        }
+
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideDataFormatOptionCompletions(String contextAfterPrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+
+        // context format: "dataFormatName" or 
"dataFormatName:existingKey1,existingKey2,..."
+        String[] parts = contextAfterPrefix.split(":", 2);
+        String dfName = parts[0];
+
+        Set<String> existingKeys = Set.of();
+        if (parts.length > 1 && !parts[1].isEmpty()) {
+            existingKeys = new HashSet<>(Arrays.asList(parts[1].split(",")));
+        }
+
+        DataFormatModel model = catalog.dataFormatModel(dfName);
+        if (model == null) {
+            return List.of();
+        }
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (DataFormatModel.DataFormatOptionModel opt : model.getOptions()) {
             if (!"attribute".equals(opt.getKind())) {
                 continue;
             }
@@ -1136,6 +1456,16 @@ class SourceTab extends AbstractTab {
             return provideEipValueCompletions(context.substring(15));
         }
 
+        // Language option value completion
+        if (context.startsWith("yaml-lang-value:")) {
+            return provideLanguageValueCompletions(context.substring(16));
+        }
+
+        // Data format option value completion
+        if (context.startsWith("yaml-df-value:")) {
+            return provideDataFormatValueCompletions(context.substring(14));
+        }
+
         if (!context.startsWith("yaml:")) {
             return List.of();
         }
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 2fbec7cc1ac4..93e966bdf096 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
@@ -821,10 +821,25 @@ class SourceViewer {
     record YamlEipContext(String eipName) {
     }
 
+    record YamlDataFormatContext(String eipName) {
+    }
+
+    record YamlDataFormatOptionContext(String dataFormatName) {
+    }
+
+    record YamlExpressionContext(String eipName) {
+    }
+
+    record YamlLanguageOptionContext(String languageName) {
+    }
+
     private static final java.util.Set<String> STRUCTURAL_KEYS
-            = java.util.Set.of("steps", "uri", "parameters", "from", "route", 
"routeConfiguration",
+            = java.util.Set.of("steps", "uri", "parameters", "from", 
"expression", "routeConfiguration",
                     "routeTemplate", "templatedRoute", "rest", "beans");
 
+    private static final java.util.Set<String> MARSHAL_EIPS
+            = java.util.Set.of("marshal", "unmarshal");
+
     YamlEipContext findEnclosingEip(int fromRow) {
         String cursorLine = editState.getLine(fromRow);
         int cursorIndent = countLeadingSpaces(cursorLine);
@@ -893,6 +908,191 @@ class SourceViewer {
         return null;
     }
 
+    YamlExpressionContext findExpressionContext(int fromRow) {
+        // cursor is inside an expression: block — find the parent EIP
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+        if (cursorLine.isBlank() && cursorIndent == 0) {
+            int lineCount = editState.lineCount();
+            for (int i = fromRow + 1; i < lineCount; i++) {
+                String next = editState.getLine(i);
+                if (!next.isBlank()) {
+                    cursorIndent = countLeadingSpaces(next);
+                    break;
+                }
+            }
+            if (cursorIndent == 0) {
+                for (int i = fromRow - 1; i >= 0; i--) {
+                    String prev = editState.getLine(i);
+                    if (!prev.isBlank()) {
+                        cursorIndent = countLeadingSpaces(prev);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // walk up to find the immediate parent key
+        for (int i = fromRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                String parentKey = extractEipName(line.trim());
+                if ("expression".equals(parentKey)) {
+                    // found expression: — now find the EIP above it
+                    YamlEipContext eipCtx = findEnclosingEip(i);
+                    if (eipCtx != null) {
+                        return new YamlExpressionContext(eipCtx.eipName());
+                    }
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    YamlLanguageOptionContext findLanguageOptionContext(int fromRow) {
+        // cursor is inside a language block (e.g., simple:) under expression: 
under an EIP
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+        if (cursorLine.isBlank() && cursorIndent == 0) {
+            int lineCount = editState.lineCount();
+            for (int i = fromRow + 1; i < lineCount; i++) {
+                String next = editState.getLine(i);
+                if (!next.isBlank()) {
+                    cursorIndent = countLeadingSpaces(next);
+                    break;
+                }
+            }
+            if (cursorIndent == 0) {
+                for (int i = fromRow - 1; i >= 0; i--) {
+                    String prev = editState.getLine(i);
+                    if (!prev.isBlank()) {
+                        cursorIndent = countLeadingSpaces(prev);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // walk up: parent should be a language name, grandparent should be 
expression:
+        String parentKey = null;
+        int parentIndent = -1;
+        for (int i = fromRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                parentKey = extractEipName(line.trim());
+                parentIndent = indent;
+                break;
+            }
+        }
+        if (parentKey == null || parentIndent < 0) {
+            return null;
+        }
+
+        // grandparent should be expression:
+        for (int i = findLineAbove(fromRow, parentIndent); i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < parentIndent) {
+                String grandparent = extractEipName(line.trim());
+                if ("expression".equals(grandparent)) {
+                    return new YamlLanguageOptionContext(parentKey);
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    YamlDataFormatContext findMarshalContext(int fromRow) {
+        YamlEipContext eipCtx = findEnclosingEip(fromRow);
+        if (eipCtx != null && MARSHAL_EIPS.contains(eipCtx.eipName())) {
+            return new YamlDataFormatContext(eipCtx.eipName());
+        }
+        return null;
+    }
+
+    YamlDataFormatOptionContext findDataFormatOptionContext(int fromRow) {
+        String cursorLine = editState.getLine(fromRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+        if (cursorLine.isBlank() && cursorIndent == 0) {
+            int lineCount = editState.lineCount();
+            for (int i = fromRow + 1; i < lineCount; i++) {
+                String next = editState.getLine(i);
+                if (!next.isBlank()) {
+                    cursorIndent = countLeadingSpaces(next);
+                    break;
+                }
+            }
+            if (cursorIndent == 0) {
+                for (int i = fromRow - 1; i >= 0; i--) {
+                    String prev = editState.getLine(i);
+                    if (!prev.isBlank()) {
+                        cursorIndent = countLeadingSpaces(prev);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // walk up to find parent key (data format name) then grandparent 
(marshal/unmarshal)
+        String parentKey = null;
+        int parentIndent = -1;
+        for (int i = fromRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < cursorIndent) {
+                parentKey = extractEipName(line.trim());
+                parentIndent = indent;
+                break;
+            }
+        }
+        if (parentKey == null || parentIndent < 0) {
+            return null;
+        }
+
+        // now find the grandparent — should be marshal or unmarshal
+        for (int i = findLineAbove(fromRow, parentIndent); i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < parentIndent) {
+                String grandparent = extractEipName(line.trim());
+                if (grandparent != null && MARSHAL_EIPS.contains(grandparent)) 
{
+                    return new YamlDataFormatOptionContext(parentKey);
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    private int findLineAbove(int fromRow, int maxIndent) {
+        for (int i = fromRow - 1; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (!line.isBlank() && countLeadingSpaces(line) < maxIndent) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     java.util.Set<String> collectExistingSiblingKeys(int fromRow) {
         java.util.Set<String> keys = new java.util.LinkedHashSet<>();
         String cursorLine = editState.getLine(fromRow);
@@ -1251,6 +1451,116 @@ class SourceViewer {
             return;
         }
 
+        // Language option completion — cursor inside a language block (e.g., 
simple:) under expression:
+        YamlLanguageOptionContext langOptCtx = findLanguageOptionContext(row);
+        if (langOptCtx != null && autocompleteProvider != null) {
+            int colonIdx = trimmed.indexOf(':');
+            if (colonIdx > 0) {
+                // value completion for language 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-lang-value:" + 
langOptCtx.languageName() + ":" + optionName;
+                    List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(context);
+                    if (values != null && !values.isEmpty()) {
+                        autocompletePopup = new AutocompletePopup(values, "", 
valueText, true);
+                    }
+                }
+            } else {
+                // key completion for language options
+                String filter = trimmed;
+                java.util.Set<String> existing = 
collectExistingSiblingKeys(row);
+                String context = "yaml-lang-opt:" + langOptCtx.languageName();
+                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(langOptCtx.languageName() 
+ " options");
+                }
+            }
+            return;
+        }
+
+        // Expression language name completion — cursor inside an expression: 
block under an EIP
+        YamlExpressionContext exprCtx = findExpressionContext(row);
+        if (exprCtx != null && autocompleteProvider != null) {
+            String filter = trimmed;
+            java.util.Set<String> existing = collectExistingSiblingKeys(row);
+            String context = "yaml-expr:" + exprCtx.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("Languages");
+            }
+            return;
+        }
+
+        // Data format option completion — cursor inside a data format block 
under marshal/unmarshal
+        YamlDataFormatOptionContext dfOptCtx = 
findDataFormatOptionContext(row);
+        if (dfOptCtx != null && autocompleteProvider != null) {
+            int colonIdx = trimmed.indexOf(':');
+            if (colonIdx > 0) {
+                // value completion for data format 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-df-value:" + 
dfOptCtx.dataFormatName() + ":" + optionName;
+                    List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(context);
+                    if (values != null && !values.isEmpty()) {
+                        autocompletePopup = new AutocompletePopup(values, "", 
valueText, true);
+                    }
+                }
+            } else {
+                // key completion for data format options
+                String filter = trimmed;
+                java.util.Set<String> existing = 
collectExistingSiblingKeys(row);
+                String context = "yaml-df-opt:" + dfOptCtx.dataFormatName();
+                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(dfOptCtx.dataFormatName() 
+ " options");
+                }
+            }
+            return;
+        }
+
+        // Data format name completion — cursor inside a marshal/unmarshal 
block
+        YamlDataFormatContext dfCtx = findMarshalContext(row);
+        if (dfCtx != null && autocompleteProvider != null) {
+            String filter = trimmed;
+            java.util.Set<String> existing = collectExistingSiblingKeys(row);
+            String context = "yaml-df:" + dfCtx.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("Data Formats");
+            }
+            return;
+        }
+
         // EIP option completion — cursor is inside an EIP block (not in 
parameters:)
         YamlEipContext eipCtx = findEnclosingEip(row);
         if (eipCtx != null && autocompleteProvider != null) {
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 4b4822cdc3ef..b7e2105bc4d1 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
@@ -28,7 +28,9 @@ 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.DataFormatModel;
 import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.tooling.model.LanguageModel;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -807,12 +809,11 @@ class YamlCompletionTest {
     }
 
     @Test
-    void eipCompletionExcludesExpressionAndElement() {
+    void eipCompletionIncludesExpressionAndElementKinds() {
         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"));
+        // canonical format: expression and element kinds are included as keys
+        assertThat(items).anyMatch(i -> i.key().equals("expression"));
     }
 
     @Test
@@ -1048,6 +1049,443 @@ class YamlCompletionTest {
         assertThat(viewer.findScopeLineRow(3)).isEqualTo(3);
     }
 
+    // --- Canonical expression completion (expression: → language → language 
options) ---
+
+    @Test
+    void eipCompletionIncludesExpressionKey() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split");
+
+        // canonical format: expression is a key, not expanded into language 
names
+        assertThat(items).anyMatch(i -> i.key().equals("expression"));
+        // language names should NOT appear at EIP level
+        assertThat(items).noneMatch(i -> i.key().equals("simple"));
+        assertThat(items).noneMatch(i -> i.key().equals("jsonpath"));
+    }
+
+    @Test
+    void eipCompletionForSetHeaderIncludesExpressionKey() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("setHeader");
+
+        assertThat(items).anyMatch(i -> i.key().equals("expression"));
+        assertThat(items).anyMatch(i -> i.key().equals("name"));
+        // no inline languages at EIP level
+        assertThat(items).noneMatch(i -> i.key().equals("simple"));
+    }
+
+    @Test
+    void eipCompletionExcludesExpressionWhenAlreadySpecified() {
+        Set<String> existing = Set.of("expression");
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("split", existing);
+
+        assertThat(items).noneMatch(i -> i.key().equals("expression"));
+        assertThat(items).anyMatch(i -> i.key().equals("streaming"));
+    }
+
+    @Test
+    void expressionContextOffersLanguageNames() {
+        List<AutocompletePopup.CompletionItem> items = 
provideExpressionLanguageCompletions("split");
+
+        assertThat(items).anyMatch(i -> i.key().equals("simple"));
+        assertThat(items).anyMatch(i -> i.key().equals("jsonpath"));
+        assertThat(items).anyMatch(i -> i.key().equals("xpath"));
+        assertThat(items).anyMatch(i -> i.key().equals("constant"));
+        var simple = items.stream().filter(i -> 
i.key().equals("simple")).findFirst();
+        assertThat(simple).isPresent();
+        assertThat(simple.get().type()).isEqualTo("language");
+    }
+
+    @Test
+    void expressionContextExcludesAlreadySpecified() {
+        Set<String> existing = Set.of("simple");
+        List<AutocompletePopup.CompletionItem> items = 
provideExpressionLanguageCompletions("split", existing);
+
+        assertThat(items).isEmpty();
+    }
+
+    @Test
+    void languageOptionCompletionForSimple() {
+        List<AutocompletePopup.CompletionItem> items = 
provideLanguageOptionCompletions("simple");
+
+        assertThat(items).anyMatch(i -> i.key().equals("expression"));
+        assertThat(items).anyMatch(i -> i.key().equals("resultType"));
+    }
+
+    @Test
+    void languageOptionCompletionForJsonpath() {
+        List<AutocompletePopup.CompletionItem> items = 
provideLanguageOptionCompletions("jsonpath");
+
+        assertThat(items).anyMatch(i -> i.key().equals("expression"));
+        assertThat(items).anyMatch(i -> i.key().equals("resultType"));
+    }
+
+    @Test
+    void languageOptionCompletionExcludesExisting() {
+        Set<String> existing = Set.of("expression");
+        List<AutocompletePopup.CompletionItem> items = 
provideLanguageOptionCompletions("simple", existing);
+
+        assertThat(items).noneMatch(i -> i.key().equals("expression"));
+        assertThat(items).anyMatch(i -> i.key().equals("resultType"));
+    }
+
+    @Test
+    void languageOptionCompletionExcludesBoilerplate() {
+        List<AutocompletePopup.CompletionItem> items = 
provideLanguageOptionCompletions("simple");
+
+        assertThat(items).noneMatch(i -> i.key().equals("id"));
+        assertThat(items).noneMatch(i -> i.key().equals("description"));
+    }
+
+    @Test
+    void eipCompletionForLogDoesNotIncludeExpression() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("log");
+
+        // log has no expression option
+        assertThat(items).noneMatch(i -> i.key().equals("expression"));
+    }
+
+    // --- Data format name completion ---
+
+    @Test
+    void dataFormatNameCompletionForMarshal() {
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatNameCompletions("marshal");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("json"));
+        assertThat(items).anyMatch(i -> i.key().equals("csv"));
+        assertThat(items).anyMatch(i -> i.key().equals("avro"));
+        // type should be "dataformat"
+        var json = items.stream().filter(i -> 
i.key().equals("json")).findFirst();
+        assertThat(json).isPresent();
+        assertThat(json.get().type()).isEqualTo("dataformat");
+    }
+
+    @Test
+    void dataFormatNameCompletionForUnmarshal() {
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatNameCompletions("unmarshal");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("json"));
+    }
+
+    @Test
+    void dataFormatNameCompletionExcludesAlreadySpecified() {
+        Set<String> existing = Set.of("json");
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatNameCompletions("marshal", existing);
+
+        // once a data format is specified, no more should appear
+        assertThat(items).isEmpty();
+    }
+
+    // --- Data format option completion ---
+
+    @Test
+    void dataFormatOptionCompletionForJackson() {
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatOptionCompletions("jackson");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("prettyPrint"));
+        assertThat(items).anyMatch(i -> i.key().equals("unmarshalType"));
+    }
+
+    @Test
+    void dataFormatOptionCompletionExcludesBoilerplate() {
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatOptionCompletions("jackson");
+
+        assertThat(items).noneMatch(i -> i.key().equals("id"));
+        assertThat(items).noneMatch(i -> i.key().equals("description"));
+    }
+
+    @Test
+    void dataFormatOptionCompletionExcludesExisting() {
+        Set<String> existing = Set.of("prettyPrint");
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatOptionCompletions("jackson", existing);
+
+        assertThat(items).noneMatch(i -> i.key().equals("prettyPrint"));
+        assertThat(items).anyMatch(i -> i.key().equals("unmarshalType"));
+    }
+
+    @Test
+    void dataFormatOptionCompletionForCsv() {
+        List<AutocompletePopup.CompletionItem> items = 
provideDataFormatOptionCompletions("csv");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("delimiter"));
+    }
+
+    // --- Expression context detection ---
+
+    @Test
+    void findExpressionContextInsideExpressionBlock() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          expression:",
+                "            ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlExpressionContext ctx = 
viewer.findExpressionContext(5);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("split");
+    }
+
+    @Test
+    void findExpressionContextReturnsNullOutsideExpression() 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();
+
+        SourceViewer.YamlExpressionContext ctx = 
viewer.findExpressionContext(5);
+        assertThat(ctx).isNull();
+    }
+
+    // --- Language option context detection ---
+
+    @Test
+    void findLanguageOptionContextInsideSimple() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          expression:",
+                "            simple:",
+                "              ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlLanguageOptionContext ctx = 
viewer.findLanguageOptionContext(6);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.languageName()).isEqualTo("simple");
+    }
+
+    @Test
+    void findLanguageOptionContextReturnsNullWhenParentIsNotExpression() 
throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          streaming:",
+                "            ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlLanguageOptionContext ctx = 
viewer.findLanguageOptionContext(5);
+        assertThat(ctx).isNull();
+    }
+
+    // --- Marshal/unmarshal context detection ---
+
+    @Test
+    void findMarshalContextInsideMarshal() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - marshal:",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatContext ctx = viewer.findMarshalContext(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("marshal");
+    }
+
+    @Test
+    void findMarshalContextInsideUnmarshal() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - unmarshal:",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatContext ctx = viewer.findMarshalContext(4);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("unmarshal");
+    }
+
+    @Test
+    void findMarshalContextReturnsNullForSplit() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatContext ctx = viewer.findMarshalContext(4);
+        assertThat(ctx).isNull();
+    }
+
+    // --- Data format option context detection ---
+
+    @Test
+    void findDataFormatOptionContextInsideCsv() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - marshal:",
+                "          csv:",
+                "            ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatOptionContext ctx = 
viewer.findDataFormatOptionContext(5);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.dataFormatName()).isEqualTo("csv");
+    }
+
+    @Test
+    void findDataFormatOptionContextInsideUnmarshal() throws IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - unmarshal:",
+                "          csv:",
+                "            delimiter: \";\"",
+                "            ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatOptionContext ctx = 
viewer.findDataFormatOptionContext(6);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.dataFormatName()).isEqualTo("csv");
+    }
+
+    @Test
+    void findDataFormatOptionContextReturnsNullInsideSplit() throws 
IOException {
+        String yaml = String.join("\n",
+                "- from:",
+                "    uri: timer:tick",
+                "    steps:",
+                "      - split:",
+                "          simple:",
+                "            ",
+                "");
+
+        Path file = tempDir.resolve("route.camel.yaml");
+        Files.writeString(file, yaml);
+
+        SourceViewer viewer = new SourceViewer();
+        viewer.loadFile(file);
+        viewer.enterEditMode();
+
+        SourceViewer.YamlDataFormatOptionContext ctx = 
viewer.findDataFormatOptionContext(5);
+        assertThat(ctx).isNull();
+    }
+
+    // --- Route-level option completion ---
+
+    @Test
+    void findEnclosingEipDetectsRoute() 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();
+
+        SourceViewer.YamlEipContext ctx = viewer.findEnclosingEip(1);
+        assertThat(ctx).isNotNull();
+        assertThat(ctx.eipName()).isEqualTo("route");
+    }
+
+    @Test
+    void routeEipCompletionIncludesRouteOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("route");
+
+        assertThat(items).anyMatch(i -> i.key().equals("autoStartup"));
+        assertThat(items).anyMatch(i -> i.key().equals("streamCache"));
+        assertThat(items).anyMatch(i -> i.key().equals("logMask"));
+        assertThat(items).anyMatch(i -> i.key().equals("messageHistory"));
+    }
+
+    @Test
+    void routeEipCompletionExcludesStructural() {
+        List<AutocompletePopup.CompletionItem> items = 
provideEipKeyCompletions("route");
+
+        // from and steps are not attribute kind, should not appear
+        assertThat(items).noneMatch(i -> i.key().equals("from"));
+    }
+
     // --- Helpers that replicate SourceTab logic for testing ---
 
     private List<AutocompletePopup.CompletionItem> 
provideKeyCompletions(String componentName, String role) {
@@ -1171,6 +1609,19 @@ class YamlCompletionTest {
         }
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
         for (EipModel.EipOptionModel opt : model.getOptions()) {
+            if ("expression".equals(opt.getKind()) || 
"element".equals(opt.getKind())) {
+                if (EIP_BOILERPLATE.contains(opt.getName())) {
+                    continue;
+                }
+                if (existingKeys.contains(opt.getName())) {
+                    continue;
+                }
+                items.add(new AutocompletePopup.CompletionItem(
+                        opt.getName(), opt.getDescription(), opt.getType(),
+                        opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                        opt.getGroup(), opt.isRequired()));
+                continue;
+            }
             if (!"attribute".equals(opt.getKind())) {
                 continue;
             }
@@ -1186,7 +1637,78 @@ class YamlCompletionTest {
                     opt.getGroup(), opt.isRequired()));
         }
         
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
-                .thenComparing(ci -> !ci.required())
+                .thenComparing((a, b) -> Boolean.compare(b.required(), 
a.required()))
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideExpressionLanguageCompletions(String eipName) {
+        return provideExpressionLanguageCompletions(eipName, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideExpressionLanguageCompletions(
+            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 (!"expression".equals(opt.getKind())) {
+                continue;
+            }
+            List<String> oneOfs = opt.getOneOfs();
+            if (oneOfs == null || oneOfs.isEmpty()) {
+                continue;
+            }
+            if (oneOfs.stream().anyMatch(existingKeys::contains)) {
+                continue;
+            }
+            for (String langName : oneOfs) {
+                LanguageModel langModel = catalog.languageModel(langName);
+                String desc = langModel != null
+                        ? langModel.getTitle() + " - " + 
langModel.getDescription()
+                        : langName;
+                String label = langModel != null ? langModel.getLabel() : 
"language";
+                boolean dep = langModel != null && langModel.isDeprecated();
+                String depNote = langModel != null ? 
langModel.getDeprecationNote() : null;
+                items.add(new AutocompletePopup.CompletionItem(
+                        langName, desc, "language", null, dep, depNote, 
label));
+            }
+        }
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideLanguageOptionCompletions(String langName) {
+        return provideLanguageOptionCompletions(langName, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideLanguageOptionCompletions(
+            String langName, Set<String> existingKeys) {
+        LanguageModel model = catalog.languageModel(langName);
+        if (model == null) {
+            return List.of();
+        }
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (LanguageModel.LanguageOptionModel opt : model.getOptions()) {
+            if (!"attribute".equals(opt.getKind()) && 
!"value".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;
     }
@@ -1242,6 +1764,74 @@ class YamlCompletionTest {
         return items;
     }
 
+    private List<AutocompletePopup.CompletionItem> 
provideDataFormatNameCompletions(String eipName) {
+        return provideDataFormatNameCompletions(eipName, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideDataFormatNameCompletions(
+            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()) {
+            List<String> oneOfs = opt.getOneOfs();
+            if (oneOfs == null || oneOfs.isEmpty()) {
+                continue;
+            }
+            if (oneOfs.stream().anyMatch(existingKeys::contains)) {
+                continue;
+            }
+            for (String dfName : oneOfs) {
+                DataFormatModel dfModel = catalog.dataFormatModel(dfName);
+                String desc = dfModel != null
+                        ? dfModel.getTitle() + " - " + dfModel.getDescription()
+                        : dfName;
+                String label = dfModel != null ? dfModel.getLabel() : 
"dataformat";
+                boolean dep = dfModel != null && dfModel.isDeprecated();
+                String depNote = dfModel != null ? 
dfModel.getDeprecationNote() : null;
+                items.add(new AutocompletePopup.CompletionItem(
+                        dfName, desc, "dataformat", null, dep, depNote, 
label));
+            }
+        }
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideDataFormatOptionCompletions(String dfName) {
+        return provideDataFormatOptionCompletions(dfName, Set.of());
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideDataFormatOptionCompletions(
+            String dfName, Set<String> existingKeys) {
+        DataFormatModel model = catalog.dataFormatModel(dfName);
+        if (model == null) {
+            return List.of();
+        }
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+        for (DataFormatModel.DataFormatOptionModel 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> loadPlaceholders(Path dir) {
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
         try (var stream = Files.list(dir)) {

Reply via email to