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) {
