This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25411-complete in repository https://gitbox.apache.org/repos/asf/camel.git
commit 46a66ac7194cc5d17f360b171dd18dd12d527949 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Oct 7 11:59:21 2026 +0200 CAMEL-25411: camel-jbang - Source editor: complete the properties of a Kamelet under its kamelet: endpoint Tab under parameters: (or in the query) of a kamelet: endpoint listed only the options of the kamelet component itself (routeId, timeout...). It now lists the properties of the Kamelet first, from the project's own Kamelet file or the catalog, required first, with their type, default and description, then a dim divider and the options of the kamelet component, which are for special uses. There is no divider when only one kind is listed, as for a Kamelet without properties. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../jbang/core/commands/tui/AutocompletePopup.java | 61 ++++++++++++++++++---- .../jbang/core/commands/tui/SourceEditAssist.java | 47 +++++++++++++++++ .../dsl/jbang/core/commands/tui/SourceTab.java | 4 +- .../commands/tui/SourceKameletValidationTest.java | 53 +++++++++++++++++++ 4 files changed, 155 insertions(+), 10 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 index 245bcb6fcde6..0f83c7bcef33 100644 --- 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 @@ -98,6 +98,12 @@ class AutocompletePopup { private Rect popupRect; private String titlePrefix; private boolean fullKeys; + // the row of the filtered list a divider is drawn above, -1 for none: the properties of a Kamelet above it, the + // options of the kamelet component itself, for special uses only, below it (CAMEL-25411) + private int dividerAt = -1; + + static final String KAMELET_GROUP = "kamelet "; + static final String KAMELET_COMPONENT_OPTIONS = "kamelet component options"; AutocompletePopup(List<CompletionItem> items, String initialPrefix, String lineKeyText) { this(items, initialPrefix, lineKeyText, false); @@ -224,11 +230,16 @@ class AutocompletePopup { if (me.isClick()) { if (popupRect != null && popupRect.contains(me.x(), me.y())) { int idx = TuiHelper.listItemAt(popupRect, 0, - (filteredItems != null ? filteredItems.size() : 0) + 2, + (filteredItems != null ? listRows() : 0) + 2, me.x(), me.y()); - if (idx >= 2 && filteredItems != null && idx - 2 < filteredItems.size()) { - listState.select(idx - 2); - selectedItem = filteredItems.get(idx - 2); + int row = idx - 2; + if (dividerAt >= 0 && row >= dividerAt) { + // the divider is not an item + row = row == dividerAt ? -1 : row - 1; + } + if (row >= 0 && filteredItems != null && row < filteredItems.size()) { + listState.select(row); + selectedItem = filteredItems.get(row); return Result.CLOSED; } return Result.CONSUMED; @@ -244,7 +255,7 @@ class AutocompletePopup { } int popupW = Math.max(70, area.width() - 4); - int contentH = filteredItems.size() + 2; + int contentH = listRows() + 2; int maxH = area.height() - 2; int popupH = Math.min(contentH + 2, maxH); popupH = Math.max(popupH, 12); @@ -314,7 +325,13 @@ class AutocompletePopup { Style dimStyle = Style.EMPTY.dim(); Style deprecatedStyle = Style.EMPTY.dim().crossedOut(); - for (CompletionItem ci : filteredItems) { + for (int row = 0; row < filteredItems.size(); row++) { + CompletionItem ci = filteredItems.get(row); + if (row == dividerAt) { + String text = "── " + KAMELET_COMPONENT_OPTIONS + " "; + items.add(ListItem.from(Line.from(Span.styled( + text + "─".repeat(Math.max(1, listRect.width() - 2 - text.length())), dimStyle)))); + } List<Span> spans = new ArrayList<>(); if (ci.deprecated()) { @@ -362,7 +379,7 @@ class AutocompletePopup { ListState renderState = new ListState(); Integer sel = listState.selected(); if (sel != null) { - renderState.select(sel + 2); + renderState.select(sel + 2 + (dividerAt >= 0 && sel >= dividerAt ? 1 : 0)); } int total = allItems.size(); @@ -385,9 +402,9 @@ class AutocompletePopup { frame.renderStatefulWidget(list, listRect, renderState); int visibleRows = Math.max(1, listRect.height() - 2); - if (shown + 2 > visibleRows) { + if (listRows() + 2 > visibleRows) { scrollbarState - .contentLength(shown) + .contentLength(listRows()) .viewportContentLength(visibleRows) .position(sel != null ? sel : 0); frame.renderStatefulWidget(Scrollbar.builder().build(), listRect, scrollbarState); @@ -565,9 +582,35 @@ class AutocompletePopup { // what was typed exactly first, then what starts with it: seda before hazelcast-seda filteredItems.sort(Comparator.comparingInt(item -> rank(item.key(), f))); } + dividerAt = -1; + boolean kamelet = filteredItems.stream().anyMatch(AutocompletePopup::isKameletProperty); + if (kamelet && !filteredItems.stream().allMatch(AutocompletePopup::isKameletProperty)) { + // the properties of the Kamelet stay above the divider while filtering (the sort is stable) + filteredItems.sort(Comparator.comparing(item -> !isKameletProperty(item))); + for (int i = 0; i < filteredItems.size(); i++) { + if (!isKameletProperty(filteredItems.get(i))) { + dividerAt = i; + break; + } + } + } listState.select(filteredItems.isEmpty() ? null : 0); } + private static boolean isKameletProperty(CompletionItem item) { + return item.group() != null && item.group().startsWith(KAMELET_GROUP); + } + + /** Package-private for tests: the row of the filtered list the divider is drawn above, -1 for none. */ + int dividerAt() { + return dividerAt; + } + + /** The rows of the list: the items and the divider. */ + private int listRows() { + return filteredItems.size() + (dividerAt >= 0 ? 1 : 0); + } + private static int rank(String key, String filter) { if (key.equalsIgnoreCase(filter)) { return 0; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java index 3a9c306f10d3..2c4faf30563b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java @@ -40,6 +40,7 @@ import java.util.regex.Pattern; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.ai.KameletChecks; +import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions; import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist; import org.apache.camel.dsl.jbang.core.commands.ai.RouteNodes; import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator; @@ -928,6 +929,14 @@ final class SourceEditAssist { } List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String context) { + return provideYamlKeyCompletions(context, null); + } + + /** + * As {@link #provideYamlKeyCompletions(String)}, with the directory of the file: the options of a kamelet: endpoint + * start with the properties of its Kamelet, from the project's own Kamelet file or the catalog (CAMEL-25411). + */ + List<AutocompletePopup.CompletionItem> provideYamlKeyCompletions(String context, Path directory) { if (context == null) { return List.of(); } @@ -994,6 +1003,9 @@ final class SourceEditAssist { } List<AutocompletePopup.CompletionItem> items = new ArrayList<>(); + if ("kamelet".equals(componentName)) { + items.addAll(kameletPropertyCompletions(uri, directory, existingKeys)); + } for (ComponentModel.EndpointOptionModel opt : model.getEndpointOptions()) { if (!includeEndpointOption(opt, isConsumer)) { continue; @@ -1008,11 +1020,46 @@ final class SourceEditAssist { } items.sort(Comparator.comparing(AutocompletePopup.CompletionItem::deprecated) + // the properties of the Kamelet before the options of the kamelet component + .thenComparing(i -> i.group() == null || !i.group().startsWith(AutocompletePopup.KAMELET_GROUP)) .thenComparing((a, b) -> Boolean.compare(b.required(), a.required())) .thenComparing(AutocompletePopup.CompletionItem::key, String.CASE_INSENSITIVE_ORDER)); return items; } + /** + * The properties of the Kamelet a kamelet: uri names, the ones not set yet: what goes under parameters: beside the + * options of the kamelet component itself (routeId, timeout...), which are all a model or a user saw before. + */ + static List<AutocompletePopup.CompletionItem> kameletPropertyCompletions( + String uri, Path directory, Set<String> existingKeys) { + if (uri == null || !uri.startsWith("kamelet:")) { + return List.of(); + } + String name = uri.substring("kamelet:".length()); + int end = name.length(); + for (char c : new char[] { '?', '/' }) { + int i = name.indexOf(c); + if (i >= 0) { + end = Math.min(end, i); + } + } + KameletDefinitions.Definition def = KameletDefinitions.find(name.substring(0, end).trim(), directory); + if (def == null) { + return List.of(); + } + List<AutocompletePopup.CompletionItem> items = new ArrayList<>(); + for (KameletDefinitions.Property p : def.properties()) { + if (existingKeys.contains(p.name())) { + continue; + } + items.add(new AutocompletePopup.CompletionItem( + p.name(), p.description(), p.type() != null ? p.type() : "string", p.defaultValue(), false, null, + AutocompletePopup.KAMELET_GROUP + def.name(), p.required())); + } + return items; + } + /** The quick doc of the XML element or attribute at the cursor (CAMEL-25244). */ List<SourceViewer.DocEntry> provideXmlQuickDoc(List<String> lines, int row, int col) { return XmlQuickDoc.at(validationCatalog(), lines, row, col); 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 966f04171377..20c1823c7692 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 @@ -907,7 +907,9 @@ class SourceTab extends AbstractTab { sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs); sourceViewer.setDeprecatedLineScanner(null); if (SourceEditAssist.isYamlFile(filePath)) { - sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions); + // the options of a kamelet: endpoint start with the properties of its Kamelet (CAMEL-25411) + sourceViewer.setAutocompleteProvider( + c -> assist.provideYamlKeyCompletions(c, filePath.toAbsolutePath().getParent())); sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions); sourceViewer.setEndpointValidator(assist::validateYamlEndpoints); // in a Kamelet's template, a property written as ${header.tag} is said to be the placeholder {{tag}} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java index 67b4620f950f..63559ca75b36 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java @@ -117,6 +117,59 @@ class SourceKameletValidationTest { ROUTE.replace("tagg:", "tag:"))).isEmpty(); } + @Test + void theParametersOfAKameletEndpointCompleteItsProperties() throws Exception { + Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), KAMELET, StandardCharsets.UTF_8); + SourceEditAssist assist = new SourceEditAssist( + new MonitorContext( + new AtomicReference<>(List.of()), new AtomicReference<>(List.of()))); + List<AutocompletePopup.CompletionItem> items + = assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action", tempDir); + // the property of the Kamelet first, then the options of the kamelet component + assertThat(items.get(0).key()).isEqualTo("tag"); + assertThat(items.get(0).required()).isTrue(); + assertThat(items.get(0).group()).isEqualTo("kamelet tag-order-action"); + assertThat(items).extracting(AutocompletePopup.CompletionItem::key).contains("routeId", "timeout"); + // a property already given is not offered again + assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:producer:tag|kamelet:tag-order-action", tempDir)) + .extracting(AutocompletePopup.CompletionItem::key).doesNotContain("tag"); + // a Kamelet of the catalog, in the query of the uri + assertThat(assist.provideYamlKeyCompletions("yaml:kamelet:consumer|kamelet:timer-source?period=1000", tempDir)) + .extracting(AutocompletePopup.CompletionItem::key).startsWith("message"); + } + + @Test + void aDividerSetsTheKameletComponentOptionsApart() throws Exception { + Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), KAMELET, StandardCharsets.UTF_8); + SourceEditAssist assist = new SourceEditAssist( + new MonitorContext( + new AtomicReference<>(List.of()), new AtomicReference<>(List.of()))); + List<AutocompletePopup.CompletionItem> items + = assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action", tempDir); + // tag above the divider, the options of the kamelet component below it + assertThat(new AutocompletePopup(items, "", "").dividerAt()).isEqualTo(1); + // filtering keeps the property of the Kamelet above: "t" matches tag and timeout + AutocompletePopup filtered = new AutocompletePopup(items, "t", "t"); + assertThat(filtered.dividerAt()).isEqualTo(1); + // only the Kamelet's properties left, or none: no divider + assertThat(new AutocompletePopup(items, "tag", "tag").dividerAt()).isEqualTo(-1); + assertThat(new AutocompletePopup(items, "routeId", "routeId").dividerAt()).isEqualTo(-1); + // a Kamelet without properties: only the options of the component, no divider + String noProperties = KAMELET.replace(""" + required: + - tag + properties: + tag: + title: Tag + type: string + """, "").replace("{{tag}}", "tagged"); + Files.writeString(tempDir.resolve("tag-order-action.kamelet.yaml"), noProperties, StandardCharsets.UTF_8); + List<AutocompletePopup.CompletionItem> none + = assist.provideYamlKeyCompletions("yaml:kamelet:producer|kamelet:tag-order-action", tempDir); + assertThat(none).isNotEmpty(); + assertThat(new AutocompletePopup(none, "", "").dividerAt()).isEqualTo(-1); + } + @Test void aKameletFileWithAProblemIsMarkedOnLoadAndNotSaved() throws Exception { Path file = tempDir.resolve("tag-order-action.kamelet.yaml");
