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 36a44f7f1e0127935d0f4d92abb6abc9fdd9cdda
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Aug 6 07:26:18 2026 +0200

    camel-tui - Add placeholder completion for tree values and smart structural 
key insertion
    
    - Property placeholder suggestions ({{key}}) from application.properties
      now appear in tree-driven value completion, filtered by type compatibility
      (same as component endpoint options)
    - Selecting a structural key (object/array type like expression, steps)
      auto-inserts with newline + indentation for the next nesting level
    - Scalar keys (string, boolean, enum) insert with trailing space as before
    
    camel-tui - Show discard confirmation as popup instead of notification
    
    Replace the notification-based unsaved changes warning with a centered
    popup dialog matching the validation error popup style. Shows title
    "Discard Changes?" with Esc to discard or any key to cancel.
    
    Also fix blank line after list item (- setVariable:) not triggering
    tab completion by accounting for the list item indent offset.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 19 +++++--
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 55 ++++++++++++++++---
 .../core/commands/tui/YamlCompletionTest.java      | 63 ++++++++++++++++++++++
 3 files changed, 126 insertions(+), 11 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 346b279eee22..e49dcd0daba4 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
@@ -1162,12 +1162,12 @@ class SourceTab extends AbstractTab {
 
         JsonObject node = getTreeNode(nodeName);
         if (node == null) {
-            return List.of();
+            return loadPropertyPlaceholders();
         }
 
         JsonArray children = (JsonArray) node.get("children");
         if (children == null) {
-            return List.of();
+            return loadPropertyPlaceholders();
         }
 
         // find the matching child
@@ -1180,7 +1180,7 @@ class SourceTab extends AbstractTab {
             }
         }
         if (matchedChild == null) {
-            return List.of();
+            return loadPropertyPlaceholders();
         }
 
         List<AutocompletePopup.CompletionItem> items = new ArrayList<>();
@@ -1189,19 +1189,32 @@ class SourceTab extends AbstractTab {
         Object defVal = matchedChild.get("default");
         String group = (String) matchedChild.get("group");
 
+        java.util.function.Predicate<String> valueFilter = null;
         JsonArray enumValues = (JsonArray) matchedChild.get("enum");
         if (enumValues != null && !enumValues.isEmpty()) {
+            Set<String> validValues = new HashSet<>();
             for (Object e : enumValues) {
                 String value = String.valueOf(e);
+                validValues.add(value.toLowerCase());
                 boolean isDefault = value.equals(String.valueOf(defVal));
                 items.add(new AutocompletePopup.CompletionItem(
                         value, desc, type, isDefault ? value : defVal, false, 
null, group));
             }
+            valueFilter = v -> validValues.contains(v.toLowerCase());
         } else if ("boolean".equalsIgnoreCase(type)) {
+            valueFilter = v -> "true".equalsIgnoreCase(v) || 
"false".equalsIgnoreCase(v);
             items.add(new AutocompletePopup.CompletionItem(
                     "true", desc, "boolean", defVal, false, null, group));
             items.add(new AutocompletePopup.CompletionItem(
                     "false", desc, "boolean", defVal, false, null, group));
+        } else if ("number".equalsIgnoreCase(type) || 
"integer".equalsIgnoreCase(type)) {
+            valueFilter = SourceTab::isNumericValue;
+        }
+
+        for (AutocompletePopup.CompletionItem ph : loadPropertyPlaceholders()) 
{
+            if (valueFilter == null || (ph.description() != null && 
valueFilter.test(ph.description()))) {
+                items.add(ph);
+            }
         }
         return items;
     }
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 9ec634423ae6..ffc6a7dabd19 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
@@ -253,6 +253,10 @@ class SourceViewer {
         return editMode;
     }
 
+    TextAreaState editState() {
+        return editState;
+    }
+
     boolean isEditable() {
         return editableFile != null;
     }
@@ -282,9 +286,6 @@ class SourceViewer {
         }
         if (dirty && !pendingDiscard) {
             pendingDiscard = true;
-            if (notificationCallback != null) {
-                notificationCallback.accept("Unsaved changes will be lost — 
press Esc again to discard", true);
-            }
             return true;
         }
         pendingDiscard = false;
@@ -483,9 +484,6 @@ class SourceViewer {
         if (ke.isCancel()) {
             if (dirty && !pendingDiscard) {
                 pendingDiscard = true;
-                if (notificationCallback != null) {
-                    notificationCallback.accept("Unsaved changes will be lost 
— press Esc again to discard", true);
-                }
                 return true;
             }
             pendingDiscard = false;
@@ -861,6 +859,12 @@ class SourceViewer {
                     String prev = editState.getLine(i);
                     if (!prev.isBlank()) {
                         cursorIndent = countLeadingSpaces(prev);
+                        String pt = prev.trim();
+                        if (pt.startsWith("- ") && pt.endsWith(":")) {
+                            cursorIndent += 4;
+                        } else if (pt.endsWith(":")) {
+                            cursorIndent += 2;
+                        }
                         break;
                     }
                 }
@@ -1406,7 +1410,7 @@ class SourceViewer {
         }
     }
 
-    private void insertYamlCompletion(AutocompletePopup.CompletionItem item, 
boolean valueMode, String currentLine) {
+    void insertYamlCompletion(AutocompletePopup.CompletionItem item, boolean 
valueMode, String currentLine) {
         int indent = countLeadingSpaces(currentLine);
         // blank lines: derive indent from context
         if (currentLine.isBlank() && indent == 0) {
@@ -1472,7 +1476,13 @@ class SourceViewer {
                 }
             }
         } else {
-            editState.insert(indentStr + item.key() + ": ");
+            editState.insert(indentStr + item.key() + ":");
+            if ("object".equals(item.type()) || "array".equals(item.type())) {
+                editState.insert('\n');
+                editState.insert(indentStr + "  ");
+            } else {
+                editState.insert(' ');
+            }
         }
     }
 
@@ -1936,6 +1946,9 @@ class SourceViewer {
         if (validationErrors != null) {
             renderValidationPopup(frame, area);
         }
+        if (pendingDiscard) {
+            renderDiscardPopup(frame, area);
+        }
     }
 
     private void renderValidationPopup(Frame frame, Rect area) {
@@ -1983,6 +1996,32 @@ class SourceViewer {
         }
     }
 
+    private void renderDiscardPopup(Frame frame, Rect area) {
+        String msg = "Unsaved changes will be lost.";
+        int popupW = Math.min(msg.length() + 6, area.width() - 4);
+        int popupH = 5;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
+        Rect popup = new Rect(x, y, popupW, popupH);
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .title(Title.from(Line.from(Span.styled(" Discard Changes? ", 
Theme.warning().bold()))))
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" Esc", Theme.hintKey()), Span.raw(" 
discard  "),
+                        Span.styled("any key", Theme.hintKey()), Span.raw(" 
cancel "))))
+                .build();
+        frame.renderWidget(block, popup);
+        Rect inner = block.inner(popup);
+
+        frame.renderWidget(
+                Paragraph.builder().text(Text.from(
+                        Line.from(Span.styled(msg, Theme.warning())))).build(),
+                inner);
+    }
+
     private static void wrapText(String text, int width, List<Line> out) {
         if (width <= 0) {
             width = 40;
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 c6679e959319..6c09dd13d2d7 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
@@ -1143,6 +1143,69 @@ class YamlCompletionTest {
         assertThat(viewer.findParentYamlKey(4)).isEqualTo("circuitBreaker");
     }
 
+    // --- Insertion behavior ---
+
+    @Test
+    void insertStructuralKeyAddsNewlineAndIndent() throws IOException {
+        // line "          " has 10 spaces — structural key should insert 
key:\n + 12 spaces
+        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();
+        // move cursor to the blank line (line 4)
+        viewer.editState().moveCursorToStart();
+        for (int i = 0; i < 4; i++) {
+            viewer.editState().moveCursorDown();
+        }
+
+        AutocompletePopup.CompletionItem item = new 
AutocompletePopup.CompletionItem(
+                "expression", "The expression", "object", null, false, null, 
"common", true);
+        viewer.insertYamlCompletion(item, false, "          ");
+
+        String result = viewer.editState().text();
+        assertThat(result).contains("expression:\n            ");
+    }
+
+    @Test
+    void insertScalarKeyAddsSpaceAfterColon() 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();
+        viewer.editState().moveCursorToStart();
+        for (int i = 0; i < 4; i++) {
+            viewer.editState().moveCursorDown();
+        }
+
+        AutocompletePopup.CompletionItem item = new 
AutocompletePopup.CompletionItem(
+                "streaming", "Enable streaming", "boolean", "false", false, 
null, "common");
+        viewer.insertYamlCompletion(item, false, "          ");
+
+        String result = viewer.editState().text();
+        assertThat(result).contains("streaming: ");
+        assertThat(result).doesNotContain("streaming:\n");
+    }
+
     // --- Helpers that replicate SourceTab logic for testing ---
 
     private List<AutocompletePopup.CompletionItem> 
provideKeyCompletions(String componentName, String role) {

Reply via email to