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 99be17200f93 chore(tui): Add Tab-completion for application.properties 
in edit mode
99be17200f93 is described below

commit 99be17200f93c6066f64b67fe0a8b5477c391e4f
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Aug 3 22:49:14 2026 +0200

    chore(tui): Add Tab-completion for application.properties in edit mode
    
    Add autocomplete popup with two-panel layout (option list + detail panel)
    for editing application.properties files. Triggered by Tab key, sources
    completion data from CamelCatalog for camel.main.*, camel.component.*,
    camel.dataformat.*, and camel.language.* options. Supports prefix-based
    filtering, left/right cursor navigation through existing text, deprecated
    option rendering with strikethrough, and auto-close when no matches.
    
    chore(tui): Add enum/boolean value completion and option docs in 
autocomplete
    
    Extends the Tab-completion popup to support value completion for enum
    and boolean properties. When the cursor is after '=' on a property line,
    pressing Tab shows the valid enum values or true/false. The detail panel
    now shows the parent option's full documentation (description, type,
    default value) so users can understand the option while browsing values.
    
    chore(tui): Show option group in autocomplete detail panel
    
    Display the option group (common, security, consumer advanced, etc.)
    in the autocomplete detail panel so users can see which category an
    option belongs to.
    
    chore(tui): Increase autocomplete popup height for long descriptions
    
    Use more vertical space for the autocomplete popup so long option
    descriptions are not clipped. The minimum height is raised to 12 and
    the max uses nearly the full editor area.
    
    chore(tui): Show group-level completions and add dirty indicator in editor
    
    Show main option groups (camel.main., camel.debug., camel.rest., etc.)
    as the first completion level instead of 545 flat options. Selecting a
    group then drills into its options. Groups include descriptions from
    catalog metadata. Also adds a * dirty indicator in the edit title when
    the file has unsaved changes, and adds unit tests for autocomplete
    popup and property completion provider logic.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/AutocompletePopup.java | 427 +++++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 263 +++++++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 143 ++++++-
 .../core/commands/tui/AutocompletePopupTest.java   | 181 ++++++++
 .../tui/PropertyCompletionProviderTest.java        | 470 +++++++++++++++++++++
 5 files changed, 1473 insertions(+), 11 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
new file mode 100644
index 000000000000..6c5c2dec4bf8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java
@@ -0,0 +1,427 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.tui.event.MouseEventKind;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+
+class AutocompletePopup {
+
+    record CompletionItem(String key, String description, String type, Object 
defaultValue,
+            boolean deprecated, String deprecationNote, String group) {
+    }
+
+    @FunctionalInterface
+    interface AutocompleteProvider {
+        List<CompletionItem> provide(String linePrefix);
+    }
+
+    @FunctionalInterface
+    interface ValueProvider {
+        List<CompletionItem> provide(String key);
+    }
+
+    enum Result {
+        CONSUMED,
+        CLOSED,
+        CURSOR_RIGHT,
+        CURSOR_LEFT
+    }
+
+    private final ListState listState = new ListState();
+    private final ScrollbarState scrollbarState = new ScrollbarState();
+    private final FuzzyFilter filter = new FuzzyFilter();
+    private final List<CompletionItem> allItems;
+    private final String lineKeyText;
+    private final int minCursorPos;
+    private int cursorPos;
+    private final boolean valueMode;
+    private List<CompletionItem> filteredItems;
+    private CompletionItem selectedItem;
+    private Rect popupRect;
+
+    AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText) {
+        this(items, initialPrefix, lineKeyText, false);
+    }
+
+    AutocompletePopup(List<CompletionItem> items, String initialPrefix, String 
lineKeyText, boolean valueMode) {
+        this.allItems = items;
+        this.lineKeyText = lineKeyText != null ? lineKeyText : "";
+        this.valueMode = valueMode;
+        this.cursorPos = initialPrefix != null ? initialPrefix.length() : 0;
+        // cursor can't go left past the group prefix (last dot in initial 
prefix)
+        int lastDot = initialPrefix != null ? initialPrefix.lastIndexOf('.') : 
-1;
+        this.minCursorPos = lastDot >= 0 ? lastDot + 1 : 0;
+        if (initialPrefix != null && !initialPrefix.isEmpty()) {
+            for (char c : initialPrefix.toCharArray()) {
+                filter.appendChar(c);
+            }
+        }
+        rebuildList();
+    }
+
+    boolean isOpen() {
+        return true;
+    }
+
+    CompletionItem consumeSelectedItem() {
+        CompletionItem item = selectedItem;
+        selectedItem = null;
+        return item;
+    }
+
+    Result handleKeyEvent(KeyEvent ke) {
+        int size = filteredItems != null ? filteredItems.size() : 0;
+
+        if (ke.isCancel()) {
+            return Result.CLOSED;
+        }
+        if (ke.isUp()) {
+            listState.selectPrevious();
+            return Result.CONSUMED;
+        }
+        if (ke.isDown()) {
+            listState.selectNext(size);
+            return Result.CONSUMED;
+        }
+        if (ke.isRight()) {
+            if (cursorPos < lineKeyText.length()) {
+                filter.appendChar(lineKeyText.charAt(cursorPos));
+                cursorPos++;
+                rebuildList();
+                return Result.CURSOR_RIGHT;
+            }
+            return Result.CONSUMED;
+        }
+        if (ke.isLeft()) {
+            if (cursorPos > minCursorPos && filter.hasFilter()) {
+                filter.deleteChar();
+                cursorPos--;
+                rebuildList();
+                return Result.CURSOR_LEFT;
+            }
+            return Result.CONSUMED;
+        }
+        if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            for (int i = 0; i < 10; i++) {
+                listState.selectPrevious();
+            }
+            return Result.CONSUMED;
+        }
+        if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            for (int i = 0; i < 10; i++) {
+                listState.selectNext(size);
+            }
+            return Result.CONSUMED;
+        }
+        if (ke.isHome() || ke.isKey(KeyCode.HOME)) {
+            listState.selectFirst();
+            return Result.CONSUMED;
+        }
+        if (ke.isEnd() || ke.isKey(KeyCode.END)) {
+            listState.selectLast(size);
+            return Result.CONSUMED;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = listState.selected();
+            if (sel != null && filteredItems != null && sel < 
filteredItems.size()) {
+                selectedItem = filteredItems.get(sel);
+            }
+            return Result.CLOSED;
+        }
+        if (ke.isKey(KeyCode.BACKSPACE)) {
+            if (filter.hasFilter()) {
+                filter.deleteChar();
+                rebuildList();
+                return Result.CONSUMED;
+            }
+            return Result.CLOSED;
+        }
+        if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
+            filter.appendChar(ke.string().charAt(0));
+            rebuildList();
+            return Result.CONSUMED;
+        }
+        return Result.CONSUMED;
+    }
+
+    Result handleMouseEvent(MouseEvent me) {
+        if (me.kind() == MouseEventKind.SCROLL_UP) {
+            listState.selectPrevious();
+            return Result.CONSUMED;
+        }
+        if (me.kind() == MouseEventKind.SCROLL_DOWN) {
+            int size = filteredItems != null ? filteredItems.size() : 0;
+            listState.selectNext(size);
+            return Result.CONSUMED;
+        }
+        if (me.isClick()) {
+            if (popupRect != null && popupRect.contains(me.x(), me.y())) {
+                int idx = TuiHelper.listItemAt(popupRect, 0,
+                        (filteredItems != null ? filteredItems.size() : 0) + 2,
+                        me.x(), me.y());
+                if (idx >= 2 && filteredItems != null && idx - 2 < 
filteredItems.size()) {
+                    listState.select(idx - 2);
+                    selectedItem = filteredItems.get(idx - 2);
+                    return Result.CLOSED;
+                }
+                return Result.CONSUMED;
+            }
+            return Result.CLOSED;
+        }
+        return Result.CONSUMED;
+    }
+
+    void render(Frame frame, Rect area, int cursorScreenRow, int 
cursorScreenCol) {
+        if (filteredItems == null || filteredItems.isEmpty()) {
+            return;
+        }
+
+        int popupW = Math.max(70, area.width() - 4);
+        int contentH = filteredItems.size() + 2;
+        int maxH = area.height() - 2;
+        int popupH = Math.min(contentH + 2, maxH);
+        popupH = Math.max(popupH, 12);
+
+        int x = area.left() + 2;
+        int y;
+        int spaceBelow = area.bottom() - (area.top() + cursorScreenRow + 1);
+        int spaceAbove = cursorScreenRow;
+        if (spaceBelow >= popupH || spaceBelow >= spaceAbove) {
+            y = area.top() + cursorScreenRow + 1;
+            popupH = Math.min(popupH, Math.max(8, spaceBelow));
+        } else {
+            popupH = Math.min(popupH, Math.max(8, spaceAbove));
+            y = area.top() + cursorScreenRow - popupH;
+        }
+
+        Rect popup = new Rect(
+                x, Math.max(area.top(), y),
+                Math.min(popupW, area.width()), Math.min(popupH, 
area.height()));
+        this.popupRect = popup;
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        int leftW = Math.max(30, popup.width() * 2 / 5);
+        int rightW = popup.width() - leftW;
+
+        Rect leftRect = new Rect(popup.x(), popup.y(), leftW, popup.height());
+        Rect rightRect = new Rect(popup.x() + leftW, popup.y(), rightW, 
popup.height());
+
+        renderList(frame, leftRect);
+        renderDetail(frame, rightRect);
+    }
+
+    private void renderList(Frame frame, Rect listRect) {
+        String filterText = filter.hasFilter() ? filter.filter() : "";
+        String prompt = "> " + filterText + "█";
+
+        int nameColW = listRect.width() - 4;
+
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from(Line.from(Span.styled(prompt, Theme.info()))));
+        String sep = "─".repeat(Math.max(1, listRect.width() - 2));
+        items.add(ListItem.from(Line.from(Span.styled(sep, 
Style.EMPTY.dim()))));
+
+        Style normalStyle = Style.EMPTY;
+        Style dimStyle = Style.EMPTY.dim();
+        Style deprecatedStyle = Style.EMPTY.dim().crossedOut();
+
+        for (CompletionItem ci : filteredItems) {
+            List<Span> spans = new ArrayList<>();
+
+            if (ci.deprecated()) {
+                spans.add(Span.styled(" ✘ ", dimStyle));
+            } else {
+                spans.add(Span.raw("   "));
+            }
+
+            String key = ci.key();
+            Style keyStyle = ci.deprecated() ? deprecatedStyle : normalStyle;
+
+            String displayKey = key;
+            if (!key.endsWith(".")) {
+                int lastDot = key.lastIndexOf('.');
+                if (lastDot >= 0) {
+                    displayKey = key.substring(lastDot + 1);
+                }
+            }
+            if (displayKey.length() > nameColW - 10) {
+                displayKey = displayKey.substring(0, Math.max(1, nameColW - 
11)) + "…";
+            }
+            spans.add(Span.styled(displayKey, keyStyle));
+
+            if (ci.type() != null) {
+                String type = simplifyType(ci.type());
+                int remaining = nameColW - displayKey.length() - 3;
+                if (remaining > 3 && type.length() <= remaining) {
+                    int pad = remaining - type.length();
+                    spans.add(Span.styled(" ".repeat(pad + 1), dimStyle));
+                    spans.add(Span.styled(type, dimStyle));
+                }
+            }
+
+            items.add(ListItem.from(Line.from(spans)));
+        }
+
+        ListState renderState = new ListState();
+        Integer sel = listState.selected();
+        if (sel != null) {
+            renderState.select(sel + 2);
+        }
+
+        int total = allItems.size();
+        int shown = filteredItems.size();
+        String title = shown == total
+                ? " Completions (" + total + ") "
+                : " Completions (" + shown + "/" + total + ") ";
+
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .title(title)
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, listRect, renderState);
+
+        int visibleRows = Math.max(1, listRect.height() - 2);
+        if (shown + 2 > visibleRows) {
+            scrollbarState
+                    .contentLength(shown)
+                    .viewportContentLength(visibleRows)
+                    .position(sel != null ? sel : 0);
+            frame.renderStatefulWidget(Scrollbar.builder().build(), listRect, 
scrollbarState);
+        }
+    }
+
+    private void renderDetail(Frame frame, Rect detailRect) {
+        Integer sel = listState.selected();
+        CompletionItem selected = null;
+        if (sel != null && filteredItems != null && sel < 
filteredItems.size()) {
+            selected = filteredItems.get(sel);
+        }
+
+        Style normalStyle = Style.EMPTY;
+        Style dimStyle = Style.EMPTY.dim();
+        List<Line> lines = new ArrayList<>();
+
+        if (selected != null) {
+            lines.add(Line.from(Span.styled(selected.key(), 
Theme.label().bold())));
+            lines.add(Line.empty());
+
+            if (selected.type() != null) {
+                lines.add(Line.from(
+                        Span.styled("Type: ", normalStyle.bold()),
+                        Span.styled(simplifyType(selected.type()), 
normalStyle)));
+            }
+            if (selected.defaultValue() != null) {
+                lines.add(Line.from(
+                        Span.styled("Default: ", normalStyle.bold()),
+                        Span.styled(String.valueOf(selected.defaultValue()), 
normalStyle)));
+            }
+            if (selected.group() != null && !selected.group().isEmpty()) {
+                lines.add(Line.from(
+                        Span.styled("Group: ", normalStyle.bold()),
+                        Span.styled(selected.group(), normalStyle)));
+            }
+            if (selected.deprecated()) {
+                String depText = "Deprecated";
+                if (selected.deprecationNote() != null && 
!selected.deprecationNote().isEmpty()) {
+                    depText += ": " + selected.deprecationNote();
+                }
+                lines.add(Line.from(Span.styled(depText, 
Theme.error().italic())));
+            }
+            if (selected.description() != null) {
+                lines.add(Line.empty());
+                lines.add(Line.from(Span.styled(selected.description(), 
dimStyle)));
+            }
+        }
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(" Details ")
+                .build();
+        frame.renderWidget(block, detailRect);
+
+        Rect inner = block.inner(detailRect);
+        if (!lines.isEmpty()) {
+            Paragraph detail = Paragraph.builder()
+                    .text(Text.from(lines))
+                    .overflow(Overflow.WRAP_WORD)
+                    .build();
+            frame.renderWidget(detail, inner);
+        }
+    }
+
+    private static String simplifyType(String type) {
+        if (type == null) {
+            return "";
+        }
+        int dot = type.lastIndexOf('.');
+        return dot >= 0 ? type.substring(dot + 1) : type;
+    }
+
+    boolean isValueMode() {
+        return valueMode;
+    }
+
+    boolean hasItems() {
+        return filteredItems != null && !filteredItems.isEmpty();
+    }
+
+    private void rebuildList() {
+        if (!filter.hasFilter()) {
+            filteredItems = new ArrayList<>(allItems);
+        } else {
+            filteredItems = new ArrayList<>();
+            String f = filter.filter();
+            for (CompletionItem item : allItems) {
+                if (item.key().toLowerCase().startsWith(f)) {
+                    filteredItems.add(item);
+                }
+            }
+        }
+        listState.select(filteredItems.isEmpty() ? null : 0);
+    }
+}
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 13e04b3ba9f5..a9e878121d25 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
@@ -91,6 +91,7 @@ class SourceTab extends AbstractTab {
     // Properties quick-doc caches (invalidated when catalog version changes)
     private String propsCatalogVersion;
     private Map<String, BaseOptionModel> mainOptionsCache;
+    private Map<String, String> mainGroupsCache;
     private final Map<String, Map<String, BaseOptionModel>> 
componentOptionsCache = new HashMap<>();
     private final Map<String, Map<String, BaseOptionModel>> 
languageOptionsCache = new HashMap<>();
     private final Map<String, Map<String, BaseOptionModel>> 
dataformatOptionsCache = new HashMap<>();
@@ -145,21 +146,17 @@ class SourceTab extends AbstractTab {
 
     @Override
     public boolean handleKeyEvent(KeyEvent ke) {
+        if (sourceViewer.isEditMode() && sourceViewer.isVisible()) {
+            return sourceViewer.handleKeyEvent(ke);
+        }
+
         if (ke.isKey(KeyCode.TAB)) {
-            // Do not steal focus or insert focus-toggle while editing
-            if (sourceViewer.isEditMode()) {
-                return true;
-            }
             if (sourceViewer.isVisible()) {
                 focusOnViewer = !focusOnViewer;
             }
             return true;
         }
 
-        if (sourceViewer.isEditMode() && sourceViewer.isVisible()) {
-            return sourceViewer.handleKeyEvent(ke);
-        }
-
         if (focusOnViewer && sourceViewer.isVisible()) {
             boolean wasVisible = sourceViewer.isVisible();
             if (sourceViewer.handleKeyEvent(ke)) {
@@ -288,7 +285,9 @@ class SourceTab extends AbstractTab {
     public void renderFooter(List<Span> spans) {
         if (focusOnViewer && sourceViewer.isVisible()) {
             sourceViewer.renderFooter(spans);
-            TuiHelper.hint(spans, "Tab", "files");
+            if (!sourceViewer.isEditMode()) {
+                TuiHelper.hint(spans, "Tab", "files");
+            }
         } else {
             TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
             TuiHelper.hint(spans, "Enter", "open");
@@ -506,12 +505,18 @@ class SourceTab extends AbstractTab {
                 if (isCamelSourceFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::provideCamelQuickDocs);
                     sourceViewer.setDeprecatedLineScanner(null);
+                    sourceViewer.setAutocompleteProvider(null);
+                    sourceViewer.setAutocompleteValueProvider(null);
                 } else if (isPropertiesFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::providePropertiesQuickDocs);
                     
sourceViewer.setDeprecatedLineScanner(this::scanDeprecatedProperties);
+                    
sourceViewer.setAutocompleteProvider(this::providePropertyCompletions);
+                    
sourceViewer.setAutocompleteValueProvider(this::providePropertyValueCompletions);
                 } else {
                     sourceViewer.setQuickDocProvider(null);
                     sourceViewer.setDeprecatedLineScanner(null);
+                    sourceViewer.setAutocompleteProvider(null);
+                    sourceViewer.setAutocompleteValueProvider(null);
                 }
                 sourceViewer.loadFile(filePath);
                 focusOnViewer = true;
@@ -593,6 +598,239 @@ class SourceTab extends AbstractTab {
         return 
path.getFileName().toString().toLowerCase().endsWith(".properties");
     }
 
+    private List<AutocompletePopup.CompletionItem> 
providePropertyCompletions(String linePrefix) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null) {
+            return List.of();
+        }
+        ensureMainOptionsCache(catalog);
+
+        String keyPrefix = linePrefix != null ? 
linePrefix.trim().toLowerCase() : "";
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+
+        // determine if the prefix matches a specific main group (e.g., 
camel.main.)
+        String matchedGroup = null;
+        if (mainGroupsCache != null) {
+            for (String groupName : mainGroupsCache.keySet()) {
+                String groupPrefix = groupName + ".";
+                if (keyPrefix.startsWith(groupPrefix)) {
+                    matchedGroup = groupName;
+                    break;
+                }
+            }
+        }
+
+        if (matchedGroup != null) {
+            // show options within the matched group
+            String groupDot = matchedGroup + ".";
+            String optFilter = keyPrefix.substring(groupDot.length());
+            if (mainOptionsCache != null) {
+                for (Map.Entry<String, BaseOptionModel> entry : 
mainOptionsCache.entrySet()) {
+                    if (entry.getKey().startsWith(groupDot)) {
+                        String optName = 
entry.getKey().substring(groupDot.length());
+                        if (optFilter.isEmpty() || 
optName.toLowerCase().contains(optFilter)) {
+                            BaseOptionModel opt = entry.getValue();
+                            items.add(new AutocompletePopup.CompletionItem(
+                                    entry.getKey(), opt.getDescription(), 
opt.getType(),
+                                    opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                                    opt.getGroup()));
+                        }
+                    }
+                }
+            }
+        } else if (keyPrefix.startsWith("camel.component.")) {
+            // camel.component.<name>. options
+            addPrefixedCompletions(items, catalog, keyPrefix, 
"camel.component.",
+                    catalog.findComponentNames(),
+                    name -> {
+                        ComponentModel m = catalog.componentModel(name);
+                        return m != null ? m.getComponentOptions() : null;
+                    });
+        } else if (keyPrefix.startsWith("camel.dataformat.")) {
+            // camel.dataformat.<name>. options
+            addPrefixedCompletions(items, catalog, keyPrefix, 
"camel.dataformat.",
+                    catalog.findDataFormatNames(),
+                    name -> {
+                        DataFormatModel m = catalog.dataFormatModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        } else if (keyPrefix.startsWith("camel.language.")) {
+            // camel.language.<name>. options
+            addPrefixedCompletions(items, catalog, keyPrefix, 
"camel.language.",
+                    catalog.findLanguageNames(),
+                    name -> {
+                        LanguageModel m = catalog.languageModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        } else {
+            // show group-level entries
+            if (mainGroupsCache != null) {
+                for (Map.Entry<String, String> entry : 
mainGroupsCache.entrySet()) {
+                    String groupKey = entry.getKey() + ".";
+                    if (keyPrefix.isEmpty() || 
groupKey.toLowerCase().contains(keyPrefix)) {
+                        items.add(new AutocompletePopup.CompletionItem(
+                                groupKey, entry.getValue(), null, null, false, 
null, null));
+                    }
+                }
+            }
+            if (keyPrefix.isEmpty() || "camel.component.".contains(keyPrefix)) 
{
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.component.", "Component configuration prefix", 
null, null, false, null, null));
+            }
+            if (keyPrefix.isEmpty() || 
"camel.dataformat.".contains(keyPrefix)) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.dataformat.", "Data format configuration 
prefix", null, null, false, null, null));
+            }
+            if (keyPrefix.isEmpty() || "camel.language.".contains(keyPrefix)) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.language.", "Language configuration prefix", 
null, null, false, null, null));
+            }
+        }
+
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+
+        return items;
+    }
+
+    private void addPrefixedCompletions(
+            List<AutocompletePopup.CompletionItem> items,
+            CamelCatalog catalog, String keyPrefix, String prefix,
+            List<String> names,
+            java.util.function.Function<String, List<? extends 
BaseOptionModel>> optionsLoader) {
+        String rest = keyPrefix.substring(prefix.length());
+        int dot = rest.indexOf('.');
+        if (dot > 0) {
+            String name = rest.substring(0, dot);
+            String optPrefix = rest.substring(dot + 1);
+            List<? extends BaseOptionModel> options = 
optionsLoader.apply(name);
+            if (options != null) {
+                for (BaseOptionModel opt : options) {
+                    String fullKey = prefix + name + "." + opt.getName();
+                    if (optPrefix.isEmpty() || 
opt.getName().toLowerCase().contains(optPrefix)) {
+                        items.add(new AutocompletePopup.CompletionItem(
+                                fullKey, opt.getDescription(), opt.getType(),
+                                opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                                opt.getGroup()));
+                    }
+                }
+            }
+        } else {
+            for (String name : names) {
+                String fullKey = prefix + name + ".";
+                if (rest.isEmpty() || name.toLowerCase().contains(rest)) {
+                    items.add(new AutocompletePopup.CompletionItem(
+                            fullKey, capitalize(prefix.split("\\.")[1]) + ": " 
+ name,
+                            null, null, false, null, null));
+                }
+            }
+        }
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
providePropertyValueCompletions(String key) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null || key == null || key.isEmpty()) {
+            return List.of();
+        }
+        ensureMainOptionsCache(catalog);
+
+        BaseOptionModel opt = lookupOption(catalog, key);
+        if (opt == null) {
+            return List.of();
+        }
+
+        String optDesc = opt.getDescription();
+        String optType = opt.getType();
+        Object optDefault = opt.getDefaultValue();
+        String optGroup = opt.getGroup();
+
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+
+        // enum values
+        List<String> enums = opt.getEnums();
+        if (enums != null && !enums.isEmpty()) {
+            for (String value : enums) {
+                boolean isDefault = value.equals(String.valueOf(optDefault));
+                items.add(new AutocompletePopup.CompletionItem(
+                        value, optDesc, optType, isDefault ? value : 
optDefault,
+                        false, null, optGroup));
+            }
+            return items;
+        }
+
+        // boolean values
+        if ("boolean".equalsIgnoreCase(optType) || 
"java.lang.Boolean".equals(opt.getJavaType())) {
+            items.add(new AutocompletePopup.CompletionItem(
+                    "true", optDesc, "boolean", optDefault, false, null, 
optGroup));
+            items.add(new AutocompletePopup.CompletionItem(
+                    "false", optDesc, "boolean", optDefault, false, null, 
optGroup));
+            return items;
+        }
+
+        return items;
+    }
+
+    private BaseOptionModel lookupOption(CamelCatalog catalog, String key) {
+        // camel.main.* options
+        if (mainOptionsCache != null && mainOptionsCache.containsKey(key)) {
+            return mainOptionsCache.get(key);
+        }
+
+        // camel.component.<name>.<option>
+        if (key.startsWith("camel.component.")) {
+            return lookupPrefixedOption(catalog, key, "camel.component.",
+                    name -> {
+                        ComponentModel m = catalog.componentModel(name);
+                        return m != null ? m.getComponentOptions() : null;
+                    });
+        }
+        // camel.dataformat.<name>.<option>
+        if (key.startsWith("camel.dataformat.")) {
+            return lookupPrefixedOption(catalog, key, "camel.dataformat.",
+                    name -> {
+                        DataFormatModel m = catalog.dataFormatModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        }
+        // camel.language.<name>.<option>
+        if (key.startsWith("camel.language.")) {
+            return lookupPrefixedOption(catalog, key, "camel.language.",
+                    name -> {
+                        LanguageModel m = catalog.languageModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        }
+        return null;
+    }
+
+    private BaseOptionModel lookupPrefixedOption(
+            CamelCatalog catalog, String key, String prefix,
+            java.util.function.Function<String, List<? extends 
BaseOptionModel>> optionsLoader) {
+        String rest = key.substring(prefix.length());
+        int dot = rest.indexOf('.');
+        if (dot > 0) {
+            String name = rest.substring(0, dot);
+            String optName = rest.substring(dot + 1);
+            List<? extends BaseOptionModel> options = 
optionsLoader.apply(name);
+            if (options != null) {
+                for (BaseOptionModel opt : options) {
+                    if (opt.getName().equals(optName)) {
+                        return opt;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private static String capitalize(String s) {
+        if (s == null || s.isEmpty()) {
+            return s;
+        }
+        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+    }
+
     private Map<Integer, List<SourceViewer.DocEntry>> 
providePropertiesQuickDocs(List<JsonObject> codeData) {
         CamelCatalog catalog = getCatalog();
         if (catalog == null || codeData.isEmpty()) {
@@ -746,6 +984,7 @@ class SourceTab extends AbstractTab {
         String version = info != null ? info.camelVersion : null;
         if (version != null && !version.equals(propsCatalogVersion)) {
             mainOptionsCache = null;
+            mainGroupsCache = null;
             componentOptionsCache.clear();
             languageOptionsCache.clear();
             dataformatOptionsCache.clear();
@@ -755,6 +994,7 @@ class SourceTab extends AbstractTab {
         }
         if (mainOptionsCache == null) {
             mainOptionsCache = new HashMap<>();
+            mainGroupsCache = new HashMap<>();
             MainModel mainModel = catalog.mainModel();
             if (mainModel != null) {
                 for (MainModel.MainOptionModel opt : mainModel.getOptions()) {
@@ -762,6 +1002,11 @@ class SourceTab extends AbstractTab {
                         mainOptionsCache.put(opt.getName(), opt);
                     }
                 }
+                for (MainModel.MainGroupModel grp : mainModel.getGroups()) {
+                    if (grp.getName() != null) {
+                        mainGroupsCache.put(grp.getName(), 
grp.getDescription());
+                    }
+                }
             }
         }
     }
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 7f28e7afd4f8..21cf225b19eb 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
@@ -127,7 +127,11 @@ class SourceViewer {
     private final TextAreaState editState = new TextAreaState();
     /** Markdown render mode prior to entering edit; restored on cancel. */
     private boolean markdownModeBeforeEdit;
+    private boolean dirty;
     private BiConsumer<String, Boolean> notificationCallback;
+    private AutocompletePopup.AutocompleteProvider autocompleteProvider;
+    private AutocompletePopup.ValueProvider autocompleteValueProvider;
+    private AutocompletePopup autocompletePopup;
 
     private record CachedSource(
             List<String> lines, List<JsonObject> codeData,
@@ -154,6 +158,14 @@ class SourceViewer {
         this.notificationCallback = callback;
     }
 
+    void setAutocompleteProvider(AutocompletePopup.AutocompleteProvider 
provider) {
+        this.autocompleteProvider = provider;
+    }
+
+    void setAutocompleteValueProvider(AutocompletePopup.ValueProvider 
provider) {
+        this.autocompleteValueProvider = provider;
+    }
+
     void hide() {
         exitEditMode();
         visible = false;
@@ -192,6 +204,9 @@ class SourceViewer {
         quickDocEntries = Collections.emptyMap();
         deprecatedLineScanner = null;
         deprecatedLines = Collections.emptySet();
+        autocompleteProvider = null;
+        autocompleteValueProvider = null;
+        autocompletePopup = null;
         editableFile = null;
     }
 
@@ -385,6 +400,25 @@ class SourceViewer {
     }
 
     private boolean handleEditKeyEvent(KeyEvent ke) {
+        if (autocompletePopup != null) {
+            boolean wasValueMode = autocompletePopup.isValueMode();
+            AutocompletePopup.Result result = 
autocompletePopup.handleKeyEvent(ke);
+            if (result == AutocompletePopup.Result.CLOSED) {
+                AutocompletePopup.CompletionItem item = 
autocompletePopup.consumeSelectedItem();
+                autocompletePopup = null;
+                if (item != null) {
+                    insertCompletion(item, wasValueMode);
+                }
+            } else if (result == AutocompletePopup.Result.CURSOR_RIGHT) {
+                editState.moveCursorRight();
+            } else if (result == AutocompletePopup.Result.CURSOR_LEFT) {
+                editState.moveCursorLeft();
+            }
+            if (autocompletePopup != null && !autocompletePopup.hasItems()) {
+                autocompletePopup = null;
+            }
+            return true;
+        }
         if (ke.isCancel()) {
             exitEditMode();
             return true;
@@ -399,6 +433,7 @@ class SourceViewer {
         }
         if (ke.isConfirm()) {
             editState.insert('\n');
+            dirty = true;
             return true;
         }
         if (ke.isUp()) {
@@ -441,14 +476,21 @@ class SourceViewer {
         }
         if (ke.isDeleteBackward()) {
             editState.deleteBackward();
+            dirty = true;
             return true;
         }
         if (ke.isDeleteForward()) {
             editState.deleteForward();
+            dirty = true;
+            return true;
+        }
+        if (ke.isKey(KeyCode.TAB) && autocompleteProvider != null && 
isPropertiesFile()) {
+            openAutocomplete();
             return true;
         }
-        if (ke.code() == KeyCode.CHAR) {
+        if (ke.code() == KeyCode.CHAR && !ke.hasCtrl() && !ke.hasAlt()) {
             editState.insert(ke.character());
+            dirty = true;
             return true;
         }
         return true;
@@ -469,6 +511,7 @@ class SourceViewer {
         markdownMode = false;
         quickDocEnabled = false;
         search.reset();
+        dirty = false;
         editMode = true;
     }
 
@@ -476,18 +519,91 @@ class SourceViewer {
         boolean wasEditing = editMode;
         editMode = false;
         editState.clear();
+        autocompletePopup = null;
         if (wasEditing && isMarkdownFile) {
             markdownMode = markdownModeBeforeEdit;
         }
         markdownModeBeforeEdit = false;
     }
 
+    private boolean isPropertiesFile() {
+        return editableFile != null
+                && 
editableFile.getFileName().toString().toLowerCase().endsWith(".properties");
+    }
+
+    private void openAutocomplete() {
+        String lineText = editState.getLine(editState.cursorRow());
+        int col = editState.cursorCol();
+        String textBeforeCursor = col <= lineText.length() ? 
lineText.substring(0, col) : lineText;
+
+        int eq = textBeforeCursor.indexOf('=');
+        if (eq >= 0 && autocompleteValueProvider != null) {
+            // cursor is after '=' — try value completion
+            String key = textBeforeCursor.substring(0, eq).trim();
+            String valuePrefix = textBeforeCursor.substring(eq + 1).trim();
+            List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(key);
+            if (values != null && !values.isEmpty()) {
+                autocompletePopup = new AutocompletePopup(values, valuePrefix, 
valuePrefix, true);
+            }
+            return;
+        }
+
+        // key completion
+        String prefix = textBeforeCursor.trim();
+
+        // load all options for the group (up to last dot) so the full list is 
available
+        int lastDot = prefix.lastIndexOf('.');
+        String groupPrefix = lastDot >= 0 ? prefix.substring(0, lastDot + 1) : 
prefix;
+
+        // extract full key text for left/right cursor navigation
+        String fullKey = lineText;
+        int eqFull = fullKey.indexOf('=');
+        if (eqFull >= 0) {
+            fullKey = fullKey.substring(0, eqFull);
+        }
+        fullKey = fullKey.trim();
+
+        List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(groupPrefix);
+        if (items != null && !items.isEmpty()) {
+            autocompletePopup = new AutocompletePopup(items, prefix, fullKey);
+        }
+    }
+
+    private void insertCompletion(AutocompletePopup.CompletionItem item, 
boolean valueMode) {
+        dirty = true;
+        String currentLine = editState.getLine(editState.cursorRow());
+        if (valueMode) {
+            // replace value portion (after =)
+            int eq = currentLine.indexOf('=');
+            if (eq >= 0) {
+                String keyPart = currentLine.substring(0, eq + 1);
+                editState.moveCursorToLineStart();
+                for (int i = 0; i < currentLine.length(); i++) {
+                    editState.deleteForward();
+                }
+                editState.insert(keyPart + item.key());
+            }
+        } else {
+            editState.moveCursorToLineStart();
+            for (int i = 0; i < currentLine.length(); i++) {
+                editState.deleteForward();
+            }
+            boolean isGroup = item.key().endsWith(".");
+            String insertText = isGroup ? item.key() : item.key() + "=";
+            editState.insert(insertText);
+            if (isGroup && autocompleteProvider != null) {
+                openAutocomplete();
+            }
+        }
+    }
+
     private void saveEdit() {
         if (!editMode || editableFile == null) {
             return;
         }
         try {
             Files.writeString(editableFile, editState.text(), 
StandardCharsets.UTF_8);
+            dirty = false;
             Path path = editableFile;
             boolean restoreMarkdownMode = markdownModeBeforeEdit;
             editMode = false;
@@ -509,6 +625,7 @@ class SourceViewer {
         }
         try {
             Files.writeString(editableFile, editState.text(), 
StandardCharsets.UTF_8);
+            dirty = false;
             notifySave("Saved: " + editableFile.getFileName(), false);
         } catch (IOException e) {
             notifySave("Save failed: " + e.getMessage(), true);
@@ -541,6 +658,18 @@ class SourceViewer {
             return false;
         }
         if (editMode) {
+            if (autocompletePopup != null) {
+                boolean wasValueMode = autocompletePopup.isValueMode();
+                AutocompletePopup.Result result = 
autocompletePopup.handleMouseEvent(me);
+                if (result == AutocompletePopup.Result.CLOSED) {
+                    AutocompletePopup.CompletionItem item = 
autocompletePopup.consumeSelectedItem();
+                    autocompletePopup = null;
+                    if (item != null) {
+                        insertCompletion(item, wasValueMode);
+                    }
+                }
+                return true;
+            }
             if (me.kind() == MouseEventKind.SCROLL_UP) {
                 editState.scrollUp(3);
                 return true;
@@ -591,6 +720,7 @@ class SourceViewer {
         if (editMode) {
             if (text != null && !text.isEmpty()) {
                 editState.insert(text);
+                dirty = true;
             }
             return;
         }
@@ -763,7 +893,7 @@ class SourceViewer {
         Style ts = titleStyle != null ? titleStyle : Style.EMPTY;
         List<Span> titleSpans = new ArrayList<>();
         String info = title != null ? title : "";
-        titleSpans.add(Span.styled(" Edit [" + info + "] ", ts));
+        titleSpans.add(Span.styled(" Edit [" + info + (dirty ? " *" : "") + "] 
", ts));
         Block.Builder blockBuilder = Block.builder()
                 .borderType(BorderType.ROUNDED).borders(Borders.ALL)
                 .title(Title.from(Line.from(titleSpans)));
@@ -782,6 +912,12 @@ class SourceViewer {
                 .lineNumberStyle(Style.EMPTY.dim())
                 .build();
         textArea.renderWithCursor(inner, frame.buffer(), editState, frame);
+
+        if (autocompletePopup != null) {
+            int cursorRow = editState.cursorRow() - editState.scrollRow();
+            int cursorCol = editState.cursorCol() - editState.scrollCol();
+            autocompletePopup.render(frame, inner, cursorRow, cursorCol);
+        }
     }
 
     void renderFooter(List<Span> spans) {
@@ -789,6 +925,9 @@ class SourceViewer {
             TuiHelper.hint(spans, "Esc", "cancel");
             TuiHelper.hint(spans, "F5", "save & close");
             TuiHelper.hint(spans, "Shift+F5", "save");
+            if (autocompleteProvider != null && isPropertiesFile()) {
+                TuiHelper.hint(spans, "Tab", "complete");
+            }
             TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "move");
             return;
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
new file mode 100644
index 000000000000..6ecbd3484153
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopupTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AutocompletePopupTest {
+
+    @Test
+    void escClosesPopup() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        assertThat(popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE, 
KeyModifiers.NONE)))
+                .isEqualTo(AutocompletePopup.Result.CLOSED);
+    }
+
+    @Test
+    void enterSelectsFirstItem() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        assertThat(popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE)))
+                .isEqualTo(AutocompletePopup.Result.CLOSED);
+
+        var selected = popup.consumeSelectedItem();
+        assertThat(selected).isNotNull();
+        assertThat(selected.key()).isEqualTo("alpha");
+    }
+
+    @Test
+    void downArrowThenEnterSelectsSecondItem() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        var selected = popup.consumeSelectedItem();
+        assertThat(selected).isNotNull();
+        assertThat(selected.key()).isEqualTo("beta");
+    }
+
+    @Test
+    void typingFiltersItems() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofChar('b', KeyModifiers.NONE));
+
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        var selected = popup.consumeSelectedItem();
+        assertThat(selected).isNotNull();
+        assertThat(selected.key()).isEqualTo("beta");
+    }
+
+    @Test
+    void typingNonMatchingClosesOnBackspace() {
+        var items = List.of(
+                new AutocompletePopup.CompletionItem("alpha", "desc", 
"string", null, false, null, null));
+        var popup = new AutocompletePopup(items, "", "");
+
+        popup.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.NONE));
+        assertThat(popup.hasItems()).isFalse();
+
+        assertThat(popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE, 
KeyModifiers.NONE)))
+                .isEqualTo(AutocompletePopup.Result.CONSUMED);
+        assertThat(popup.hasItems()).isTrue();
+    }
+
+    @Test
+    void backspaceOnEmptyFilterCloses() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        assertThat(popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE, 
KeyModifiers.NONE)))
+                .isEqualTo(AutocompletePopup.Result.CLOSED);
+    }
+
+    @Test
+    void consumeSelectedItemReturnsNullAfterFirstCall() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeSelectedItem()).isNotNull();
+        assertThat(popup.consumeSelectedItem()).isNull();
+    }
+
+    @Test
+    void valueModeFlag() {
+        var popup = new AutocompletePopup(sampleItems(), "", "", false);
+        assertThat(popup.isValueMode()).isFalse();
+
+        var valuePopup = new AutocompletePopup(sampleItems(), "", "", true);
+        assertThat(valuePopup.isValueMode()).isTrue();
+    }
+
+    @Test
+    void initialPrefixFiltersItems() {
+        var popup = new AutocompletePopup(sampleItems(), "g", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        var selected = popup.consumeSelectedItem();
+        assertThat(selected).isNotNull();
+        assertThat(selected.key()).isEqualTo("gamma");
+    }
+
+    @Test
+    void hasItemsReflectsFilterState() {
+        var items = List.of(
+                new AutocompletePopup.CompletionItem("one", null, null, null, 
false, null, null));
+        var popup = new AutocompletePopup(items, "", "");
+        assertThat(popup.hasItems()).isTrue();
+
+        popup.handleKeyEvent(KeyEvent.ofChar('z', KeyModifiers.NONE));
+        assertThat(popup.hasItems()).isFalse();
+    }
+
+    @Test
+    void completionItemRecordFields() {
+        var item = new AutocompletePopup.CompletionItem(
+                "myKey", "my description", "string", "default", true, "use 
other", "advanced");
+
+        assertThat(item.key()).isEqualTo("myKey");
+        assertThat(item.description()).isEqualTo("my description");
+        assertThat(item.type()).isEqualTo("string");
+        assertThat(item.defaultValue()).isEqualTo("default");
+        assertThat(item.deprecated()).isTrue();
+        assertThat(item.deprecationNote()).isEqualTo("use other");
+        assertThat(item.group()).isEqualTo("advanced");
+    }
+
+    @Test
+    void pageDownAndPageUpNavigate() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.PAGE_DOWN, 
KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        var selected = popup.consumeSelectedItem();
+        assertThat(selected).isNotNull();
+        assertThat(selected.key()).isEqualTo("gamma");
+    }
+
+    @Test
+    void homeSelectsFirst() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.HOME, KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeSelectedItem().key()).isEqualTo("alpha");
+    }
+
+    @Test
+    void endSelectsLast() {
+        var popup = new AutocompletePopup(sampleItems(), "", "");
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeSelectedItem().key()).isEqualTo("gamma");
+    }
+
+    private static List<AutocompletePopup.CompletionItem> sampleItems() {
+        return List.of(
+                new AutocompletePopup.CompletionItem("alpha", "First item", 
"string", null, false, null, null),
+                new AutocompletePopup.CompletionItem("beta", "Second item", 
"integer", 42, false, null, "common"),
+                new AutocompletePopup.CompletionItem("gamma", "Third item", 
"boolean", true, true, "use delta", "advanced"));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PropertyCompletionProviderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PropertyCompletionProviderTest.java
new file mode 100644
index 000000000000..73b1851f7639
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/PropertyCompletionProviderTest.java
@@ -0,0 +1,470 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+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.LanguageModel;
+import org.apache.camel.tooling.model.MainModel;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for property completion logic used by SourceTab autocomplete.
+ *
+ * Exercises the same grouping and filtering logic as the provider methods, 
using the real CamelCatalog to validate
+ * against actual metadata.
+ */
+class PropertyCompletionProviderTest {
+
+    private static CamelCatalog catalog;
+    private static Map<String, BaseOptionModel> mainOptionsCache;
+    private static Map<String, String> mainGroupsCache;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+        mainOptionsCache = new HashMap<>();
+        mainGroupsCache = new HashMap<>();
+        MainModel mainModel = catalog.mainModel();
+        for (MainModel.MainOptionModel opt : mainModel.getOptions()) {
+            if (opt.getName() != null) {
+                mainOptionsCache.put(opt.getName(), opt);
+            }
+        }
+        for (MainModel.MainGroupModel grp : mainModel.getGroups()) {
+            if (grp.getName() != null) {
+                mainGroupsCache.put(grp.getName(), grp.getDescription());
+            }
+        }
+    }
+
+    // --- Group-level completions ---
+
+    @Test
+    void emptyPrefixShowsGroupsNotIndividualOptions() {
+        List<AutocompletePopup.CompletionItem> items = provideCompletions("");
+
+        assertThat(items).isNotEmpty();
+        // should contain group entries like camel.main., camel.debug., etc.
+        assertThat(items).anyMatch(i -> i.key().equals("camel.main."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.debug."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.rest."));
+        // should also contain component/dataformat/language prefixes
+        assertThat(items).anyMatch(i -> i.key().equals("camel.component."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.dataformat."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.language."));
+        // should NOT contain individual options at this level
+        assertThat(items).noneMatch(i -> 
i.key().equals("camel.main.autoStartup"));
+    }
+
+    @Test
+    void groupsHaveDescriptions() {
+        List<AutocompletePopup.CompletionItem> items = provideCompletions("");
+
+        var mainGroup = items.stream().filter(i -> 
i.key().equals("camel.main.")).findFirst();
+        assertThat(mainGroup).isPresent();
+        assertThat(mainGroup.get().description()).isNotNull().isNotEmpty();
+
+        var debugGroup = items.stream().filter(i -> 
i.key().equals("camel.debug.")).findFirst();
+        assertThat(debugGroup).isPresent();
+        assertThat(debugGroup.get().description()).isNotNull().isNotEmpty();
+    }
+
+    @Test
+    void camelPrefixShowsGroups() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.");
+
+        assertThat(items).anyMatch(i -> i.key().equals("camel.main."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.component."));
+        // should not show individual options
+        assertThat(items).noneMatch(i -> 
i.key().equals("camel.main.autoStartup"));
+    }
+
+    @Test
+    void filteringGroupsByPartialName() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.d");
+
+        assertThat(items).anyMatch(i -> i.key().equals("camel.debug."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.dataformat."));
+        // should not show unrelated groups
+        assertThat(items).noneMatch(i -> i.key().equals("camel.rest."));
+    }
+
+    @Test
+    void vaultGroupsIncluded() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.vault");
+
+        assertThat(items).anyMatch(i -> i.key().equals("camel.vault.aws."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.vault.gcp."));
+    }
+
+    // --- Option-level completions (after selecting a group) ---
+
+    @Test
+    void camelMainDotShowsOptionsNotGroups() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.main.");
+
+        assertThat(items).isNotEmpty();
+        // should contain actual options
+        assertThat(items).anyMatch(i -> 
i.key().equals("camel.main.autoStartup"));
+        // should NOT contain group entries
+        assertThat(items).noneMatch(i -> i.key().endsWith(".") && 
!i.key().contains("="));
+    }
+
+    @Test
+    void camelDebugDotShowsDebugOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.debug.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).allMatch(i -> i.key().startsWith("camel.debug."));
+        assertThat(items).anyMatch(i -> i.key().equals("camel.debug.enabled"));
+    }
+
+    @Test
+    void optionsHaveMetadata() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.main.");
+
+        var autoStartup = items.stream()
+                .filter(i -> i.key().equals("camel.main.autoStartup"))
+                .findFirst();
+        assertThat(autoStartup).isPresent();
+        assertThat(autoStartup.get().description()).isNotNull().isNotEmpty();
+        assertThat(autoStartup.get().type()).isNotNull();
+    }
+
+    @Test
+    void filteringOptionsWithinGroup() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.main.auto");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).allMatch(i -> 
i.key().toLowerCase().contains("auto"));
+        assertThat(items).anyMatch(i -> 
i.key().equals("camel.main.autoStartup"));
+    }
+
+    @Test
+    void camelRestDotShowsRestOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.rest.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).allMatch(i -> i.key().startsWith("camel.rest."));
+    }
+
+    // --- Component completions ---
+
+    @Test
+    void camelComponentDotShowsComponentNames() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.component.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> 
i.key().equals("camel.component.kafka."));
+        assertThat(items).anyMatch(i -> 
i.key().equals("camel.component.timer."));
+    }
+
+    @Test
+    void camelComponentKafkaDotShowsKafkaOptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.component.kafka.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).allMatch(i -> 
i.key().startsWith("camel.component.kafka."));
+        assertThat(items).anyMatch(i -> 
i.key().equals("camel.component.kafka.brokers"));
+    }
+
+    @Test
+    void componentOptionsHaveDescriptions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.component.kafka.");
+
+        var brokers = items.stream()
+                .filter(i -> i.key().equals("camel.component.kafka.brokers"))
+                .findFirst();
+        assertThat(brokers).isPresent();
+        assertThat(brokers.get().description()).isNotNull().isNotEmpty();
+    }
+
+    // --- Dataformat completions ---
+
+    @Test
+    void camelDataformatDotShowsDataformatNames() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.dataformat.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().contains("json"));
+    }
+
+    // --- Language completions ---
+
+    @Test
+    void camelLanguageDotShowsLanguageNames() {
+        List<AutocompletePopup.CompletionItem> items = 
provideCompletions("camel.language.");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().contains("simple"));
+    }
+
+    // --- Value completions ---
+
+    @Test
+    void booleanOptionReturnsValueCompletions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("camel.main.autoStartup");
+
+        assertThat(items).hasSize(2);
+        assertThat(items).anyMatch(i -> i.key().equals("true"));
+        assertThat(items).anyMatch(i -> i.key().equals("false"));
+        // value completions should carry the parent option's description
+        assertThat(items).allMatch(i -> i.description() != null && 
!i.description().isEmpty());
+    }
+
+    @Test
+    void enumOptionReturnsEnumValues() {
+        // camel.main.startupRecorder is an enum option
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("camel.main.startupRecorder");
+
+        if (!items.isEmpty()) {
+            assertThat(items).allMatch(i -> i.description() != null && 
!i.description().isEmpty());
+            assertThat(items).allMatch(i -> i.group() != null || i.type() != 
null);
+        }
+    }
+
+    @Test
+    void componentEnumOptionReturnsValues() {
+        List<AutocompletePopup.CompletionItem> items
+                = 
provideValueCompletions("camel.component.kafka.autoOffsetReset");
+
+        assertThat(items).isNotEmpty();
+        assertThat(items).anyMatch(i -> i.key().equals("latest"));
+        assertThat(items).anyMatch(i -> i.key().equals("earliest"));
+        // each value carries the parent option's description
+        assertThat(items).allMatch(i -> i.description() != null && 
!i.description().isEmpty());
+    }
+
+    @Test
+    void unknownKeyReturnsEmptyValueCompletions() {
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("camel.main.nonExistent");
+        assertThat(items).isEmpty();
+    }
+
+    @Test
+    void stringOptionReturnsEmptyValueCompletions() {
+        // camel.main.name is a string option with no enums
+        List<AutocompletePopup.CompletionItem> items = 
provideValueCompletions("camel.main.name");
+        assertThat(items).isEmpty();
+    }
+
+    // --- Helper methods that mirror SourceTab's provider logic ---
+
+    private List<AutocompletePopup.CompletionItem> provideCompletions(String 
linePrefix) {
+        String keyPrefix = linePrefix != null ? 
linePrefix.trim().toLowerCase() : "";
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+
+        // determine if the prefix matches a specific main group
+        String matchedGroup = null;
+        for (String groupName : mainGroupsCache.keySet()) {
+            String groupPrefix = groupName + ".";
+            if (keyPrefix.startsWith(groupPrefix)) {
+                matchedGroup = groupName;
+                break;
+            }
+        }
+
+        if (matchedGroup != null) {
+            String groupDot = matchedGroup + ".";
+            String optFilter = keyPrefix.substring(groupDot.length());
+            for (Map.Entry<String, BaseOptionModel> entry : 
mainOptionsCache.entrySet()) {
+                if (entry.getKey().startsWith(groupDot)) {
+                    String optName = 
entry.getKey().substring(groupDot.length());
+                    if (optFilter.isEmpty() || 
optName.toLowerCase().contains(optFilter)) {
+                        BaseOptionModel opt = entry.getValue();
+                        items.add(new AutocompletePopup.CompletionItem(
+                                entry.getKey(), opt.getDescription(), 
opt.getType(),
+                                opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                                opt.getGroup()));
+                    }
+                }
+            }
+        } else if (keyPrefix.startsWith("camel.component.")) {
+            addPrefixedCompletions(items, keyPrefix, "camel.component.",
+                    catalog.findComponentNames(),
+                    name -> {
+                        ComponentModel m = catalog.componentModel(name);
+                        return m != null ? m.getComponentOptions() : null;
+                    });
+        } else if (keyPrefix.startsWith("camel.dataformat.")) {
+            addPrefixedCompletions(items, keyPrefix, "camel.dataformat.",
+                    catalog.findDataFormatNames(),
+                    name -> {
+                        DataFormatModel m = catalog.dataFormatModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        } else if (keyPrefix.startsWith("camel.language.")) {
+            addPrefixedCompletions(items, keyPrefix, "camel.language.",
+                    catalog.findLanguageNames(),
+                    name -> {
+                        LanguageModel m = catalog.languageModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        } else {
+            for (Map.Entry<String, String> entry : mainGroupsCache.entrySet()) 
{
+                String groupKey = entry.getKey() + ".";
+                if (keyPrefix.isEmpty() || 
groupKey.toLowerCase().contains(keyPrefix)) {
+                    items.add(new AutocompletePopup.CompletionItem(
+                            groupKey, entry.getValue(), null, null, false, 
null, null));
+                }
+            }
+            if (keyPrefix.isEmpty() || "camel.component.".contains(keyPrefix)) 
{
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.component.", "Component configuration prefix", 
null, null, false, null, null));
+            }
+            if (keyPrefix.isEmpty() || 
"camel.dataformat.".contains(keyPrefix)) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.dataformat.", "Data format configuration 
prefix", null, null, false, null, null));
+            }
+            if (keyPrefix.isEmpty() || "camel.language.".contains(keyPrefix)) {
+                items.add(new AutocompletePopup.CompletionItem(
+                        "camel.language.", "Language configuration prefix", 
null, null, false, null, null));
+            }
+        }
+
+        
items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated)
+                .thenComparing(AutocompletePopup.CompletionItem::key, 
String.CASE_INSENSITIVE_ORDER));
+        return items;
+    }
+
+    private List<AutocompletePopup.CompletionItem> 
provideValueCompletions(String key) {
+        BaseOptionModel opt = lookupOption(key);
+        if (opt == null) {
+            return List.of();
+        }
+
+        String optDesc = opt.getDescription();
+        String optType = opt.getType();
+        Object optDefault = opt.getDefaultValue();
+        String optGroup = opt.getGroup();
+        List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
+
+        List<String> enums = opt.getEnums();
+        if (enums != null && !enums.isEmpty()) {
+            for (String value : enums) {
+                boolean isDefault = value.equals(String.valueOf(optDefault));
+                items.add(new AutocompletePopup.CompletionItem(
+                        value, optDesc, optType, isDefault ? value : 
optDefault,
+                        false, null, optGroup));
+            }
+            return items;
+        }
+
+        if ("boolean".equalsIgnoreCase(optType) || 
"java.lang.Boolean".equals(opt.getJavaType())) {
+            items.add(new AutocompletePopup.CompletionItem(
+                    "true", optDesc, "boolean", optDefault, false, null, 
optGroup));
+            items.add(new AutocompletePopup.CompletionItem(
+                    "false", optDesc, "boolean", optDefault, false, null, 
optGroup));
+            return items;
+        }
+
+        return items;
+    }
+
+    private BaseOptionModel lookupOption(String key) {
+        if (mainOptionsCache.containsKey(key)) {
+            return mainOptionsCache.get(key);
+        }
+        if (key.startsWith("camel.component.")) {
+            return lookupPrefixedOption(key, "camel.component.",
+                    name -> {
+                        ComponentModel m = catalog.componentModel(name);
+                        return m != null ? m.getComponentOptions() : null;
+                    });
+        }
+        if (key.startsWith("camel.dataformat.")) {
+            return lookupPrefixedOption(key, "camel.dataformat.",
+                    name -> {
+                        DataFormatModel m = catalog.dataFormatModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        }
+        if (key.startsWith("camel.language.")) {
+            return lookupPrefixedOption(key, "camel.language.",
+                    name -> {
+                        LanguageModel m = catalog.languageModel(name);
+                        return m != null ? m.getOptions() : null;
+                    });
+        }
+        return null;
+    }
+
+    private static BaseOptionModel lookupPrefixedOption(
+            String key, String prefix,
+            java.util.function.Function<String, List<? extends 
BaseOptionModel>> optionsLoader) {
+        String rest = key.substring(prefix.length());
+        int dot = rest.indexOf('.');
+        if (dot <= 0) {
+            return null;
+        }
+        String name = rest.substring(0, dot);
+        String optionName = rest.substring(dot + 1);
+        List<? extends BaseOptionModel> options = optionsLoader.apply(name);
+        if (options == null) {
+            return null;
+        }
+        return options.stream()
+                .filter(o -> o.getName().equals(optionName))
+                .findFirst().orElse(null);
+    }
+
+    private static void addPrefixedCompletions(
+            List<AutocompletePopup.CompletionItem> items,
+            String keyPrefix, String prefix,
+            List<String> names,
+            java.util.function.Function<String, List<? extends 
BaseOptionModel>> optionsLoader) {
+        String rest = keyPrefix.substring(prefix.length());
+        int dot = rest.indexOf('.');
+        if (dot > 0) {
+            String name = rest.substring(0, dot);
+            String optPrefix = rest.substring(dot + 1);
+            List<? extends BaseOptionModel> options = 
optionsLoader.apply(name);
+            if (options != null) {
+                for (BaseOptionModel opt : options) {
+                    String fullKey = prefix + name + "." + opt.getName();
+                    if (optPrefix.isEmpty() || 
opt.getName().toLowerCase().contains(optPrefix)) {
+                        items.add(new AutocompletePopup.CompletionItem(
+                                fullKey, opt.getDescription(), opt.getType(),
+                                opt.getDefaultValue(), opt.isDeprecated(), 
opt.getDeprecationNote(),
+                                opt.getGroup()));
+                    }
+                }
+            }
+        } else {
+            for (String name : names) {
+                String fullKey = prefix + name + ".";
+                if (rest.isEmpty() || name.toLowerCase().contains(rest)) {
+                    items.add(new AutocompletePopup.CompletionItem(
+                            fullKey, null, null, null, false, null, null));
+                }
+            }
+        }
+    }
+}

Reply via email to