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 04f332134042 camel-jbang-tui: Editor options popup should use focused 
border and exclude URI path parameters
04f332134042 is described below

commit 04f332134042ac7fb811d6c71126d153f1836be9
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Aug 10 07:29:02 2026 +0200

    camel-jbang-tui: Editor options popup should use focused border and exclude 
URI path parameters
    
    camel-jbang-tui: Editor options popup uses camelCase-aware filtering
    
    camel-jbang-tui: Editor TAB completion auto-inserts parameters block below 
uri
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |   1 +
 .../jbang/core/commands/tui/AutocompletePopup.java |   4 +-
 .../dsl/jbang/core/commands/tui/FuzzyFilter.java   |  53 +++++++++-
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  24 ++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 114 ++++++++++++++++++---
 .../jbang/core/commands/tui/FuzzyFilterTest.java   |  49 +++++++++
 6 files changed, 219 insertions(+), 26 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index e54790a40cec..665c3d7f3df4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -948,6 +948,7 @@ class AiPanel {
         Block block = Block.builder()
                 .borders(Borders.ALL)
                 .borderType(BorderType.ROUNDED)
+                .borderStyle(Theme.borderFocused())
                 .title(Title.from(titleLine))
                 .build();
         frame.renderWidget(block, area);
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 5334cd8e6cb9..79b845d7673c 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
@@ -511,7 +511,7 @@ class AutocompletePopup {
             filteredItems = new ArrayList<>();
             String f = filter.filter();
             for (CompletionItem item : allItems) {
-                if (item.key().toLowerCase().startsWith(f) || 
matchesLabel(item.group(), f)) {
+                if (FuzzyFilter.camelCaseMatch(item.key(), f) || 
matchesLabel(item.group(), f)) {
                     filteredItems.add(item);
                 }
             }
@@ -524,7 +524,7 @@ class AutocompletePopup {
             return false;
         }
         for (String label : group.split(",")) {
-            if (label.trim().toLowerCase().startsWith(filter)) {
+            if (FuzzyFilter.camelCaseMatch(label.trim(), filter)) {
                 return true;
             }
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
index 067de6c2b617..e87524a6817c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilter.java
@@ -78,10 +78,11 @@ class FuzzyFilter {
             return new int[0];
         }
         String lowerText = text.toLowerCase();
-        int[] positions = new int[pattern.length()];
+        String lowerPattern = pattern.toLowerCase();
+        int[] positions = new int[lowerPattern.length()];
         int textIdx = 0;
-        for (int i = 0; i < pattern.length(); i++) {
-            char c = pattern.charAt(i);
+        for (int i = 0; i < lowerPattern.length(); i++) {
+            char c = lowerPattern.charAt(i);
             int found = lowerText.indexOf(c, textIdx);
             if (found < 0) {
                 return null;
@@ -92,6 +93,52 @@ class FuzzyFilter {
         return positions;
     }
 
+    /**
+     * CamelCase-aware matching for option names. Single char: prefix match 
only. Two or more chars: matches prefix,
+     * substring, or camelCase segment initials. Each filter char must match 
the start of a camelCase segment in order.
+     * For example "hfs" matches "headerFilterStrategy" (h-eader, f-ilter, 
s-trategy) but "hlr" does not.
+     */
+    static boolean camelCaseMatch(String key, String filter) {
+        String lower = key.toLowerCase();
+        String lowerFilter = filter.toLowerCase();
+        if (lower.startsWith(lowerFilter)) {
+            return true;
+        }
+        if (lowerFilter.length() < 2) {
+            return false;
+        }
+        if (lower.contains(lowerFilter)) {
+            return true;
+        }
+        // each filter char must match the start of a camelCase segment in 
order
+        int si = 0;
+        int segStart = 0;
+        for (int fi = 0; fi < lowerFilter.length(); fi++) {
+            char fc = lowerFilter.charAt(fi);
+            boolean found = false;
+            while (segStart < key.length()) {
+                // find the next segment boundary
+                int nextSeg = key.length();
+                for (int j = segStart + 1; j < key.length(); j++) {
+                    if (Character.isUpperCase(key.charAt(j))) {
+                        nextSeg = j;
+                        break;
+                    }
+                }
+                if (lower.charAt(segStart) == fc) {
+                    segStart = nextSeg;
+                    found = true;
+                    break;
+                }
+                segStart = nextSeg;
+            }
+            if (!found) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Build a {@link Line} with matched character positions highlighted.
      *
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 c184befb3a86..618ee3f38d77 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
@@ -1034,8 +1034,15 @@ class SourceTab extends AbstractTab {
             return List.of();
         }
 
-        // context format: 
"yaml:componentName:consumer|producer[:existingKey1,existingKey2,...]"
-        String[] parts = context.substring(5).split(":", 3);
+        // context format: 
"yaml:componentName:consumer|producer[:existingKey1,existingKey2,...][|uri]"
+        String contextBody = context.substring(5);
+        String uri = null;
+        int pipeIdx = contextBody.indexOf('|');
+        if (pipeIdx >= 0) {
+            uri = contextBody.substring(pipeIdx + 1);
+            contextBody = contextBody.substring(0, pipeIdx);
+        }
+        String[] parts = contextBody.split(":", 3);
         if (parts.length < 2) {
             return List.of();
         }
@@ -1043,9 +1050,9 @@ class SourceTab extends AbstractTab {
         String role = parts[1];
         boolean isConsumer = "consumer".equals(role);
 
-        Set<String> existingKeys = Set.of();
+        Set<String> existingKeys = new HashSet<>();
         if (parts.length > 2 && !parts[2].isEmpty()) {
-            existingKeys = new HashSet<>(Arrays.asList(parts[2].split(",")));
+            existingKeys.addAll(Arrays.asList(parts[2].split(",")));
         }
 
         ComponentModel model = catalog.componentModel(componentName);
@@ -1053,6 +1060,15 @@ class SourceTab extends AbstractTab {
             return List.of();
         }
 
+        // use the catalog to parse the URI and find parameters already set 
via the context path
+        if (uri != null) {
+            try {
+                existingKeys.addAll(catalog.endpointProperties(uri).keySet());
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+
         // build a set of multi-valued option names so we can allow duplicates
         Set<String> multiValuedOptions = new HashSet<>();
         for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
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 f0a4f9e8b561..64104630462f 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
@@ -690,7 +690,10 @@ class SourceViewer {
         return name.endsWith(".yaml") || name.endsWith(".yml");
     }
 
-    record YamlEndpointContext(String component, boolean consumer) {
+    record YamlEndpointContext(String component, boolean consumer, String uri, 
boolean needsParameters) {
+        YamlEndpointContext(String component, boolean consumer, String uri) {
+            this(component, consumer, uri, false);
+        }
     }
 
     static final java.util.Set<String> CONSUMER_EIPS
@@ -721,6 +724,9 @@ class SourceViewer {
                     } else if (pt.startsWith("- ") || pt.startsWith("steps:")) 
{
                         // inside a steps block or list item — not inside 
parameters
                         return null;
+                    } else if (pt.startsWith("uri:") || pt.startsWith("id:")) {
+                        // below uri: or id: — look for uri: sibling to offer 
component options
+                        return findComponentFromUriSibling(i);
                     } else {
                         return findEnclosingComponent(i);
                     }
@@ -764,6 +770,7 @@ class SourceViewer {
         }
 
         String foundScheme = null;
+        String foundUri = null;
         for (int i = parametersRow - 1; i >= 0; i--) {
             String line = editState.getLine(i);
             if (line.isBlank()) {
@@ -775,6 +782,7 @@ class SourceViewer {
             if (indent == parametersIndent) {
                 if (foundScheme == null && (trimmed.startsWith("uri:") || 
trimmed.startsWith("- uri:"))) {
                     foundScheme = extractSchemeFromUriLine(trimmed);
+                    foundUri = extractUriValue(trimmed);
                 }
             }
 
@@ -782,21 +790,70 @@ class SourceViewer {
                 String eipName = extractEipName(trimmed);
                 if (foundScheme == null) {
                     foundScheme = extractInlineUri(trimmed);
+                    foundUri = foundScheme;
                 }
                 if (foundScheme != null) {
                     boolean consumer = eipName != null && 
CONSUMER_EIPS.contains(eipName);
-                    return new YamlEndpointContext(foundScheme, consumer);
+                    return new YamlEndpointContext(foundScheme, consumer, 
foundUri);
                 }
                 break;
             }
         }
 
         if (foundScheme != null) {
-            return new YamlEndpointContext(foundScheme, false);
+            return new YamlEndpointContext(foundScheme, false, foundUri);
         }
         return null;
     }
 
+    /**
+     * When cursor is below a uri: line (no parameters: block), find the uri: 
among siblings and build the endpoint
+     * context. Walks up to find the parent EIP to determine consumer vs 
producer.
+     */
+    private YamlEndpointContext findComponentFromUriSibling(int 
uriOrSiblingRow) {
+        int indent = countLeadingSpaces(editState.getLine(uriOrSiblingRow));
+
+        // find the uri: line among siblings at the same indent
+        String uriValue = null;
+        String scheme = null;
+        for (int i = uriOrSiblingRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int li = countLeadingSpaces(line);
+            if (li < indent) {
+                break;
+            }
+            if (li == indent && line.trim().startsWith("uri:")) {
+                scheme = extractSchemeFromUriLine(line.trim());
+                uriValue = extractUriValue(line.trim());
+                break;
+            }
+        }
+        if (scheme == null) {
+            return null;
+        }
+
+        // find the parent EIP to determine consumer vs producer
+        boolean consumer = false;
+        for (int i = uriOrSiblingRow; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int li = countLeadingSpaces(line);
+            if (li < indent) {
+                String eipName = extractEipName(line.trim());
+                if (eipName != null) {
+                    consumer = CONSUMER_EIPS.contains(eipName);
+                }
+                break;
+            }
+        }
+        return new YamlEndpointContext(scheme, consumer, uriValue, true);
+    }
+
     java.util.Set<String> collectExistingParameters(int fromRow) {
         java.util.Set<String> keys = new java.util.LinkedHashSet<>();
         // find the parameters: row by walking up
@@ -1171,17 +1228,17 @@ class SourceViewer {
             if (i < cursorRow && indent < cursorIndent) {
                 String eipName = extractEipName(t);
                 if (eipName != null && !STRUCTURAL_KEYS.contains(eipName)) {
-                    return i;
-                }
-                // for from:/to: blocks, look for uri: sibling as scope
-                if (eipName != null && ("from".equals(eipName) || 
CONSUMER_EIPS.contains(eipName)
-                        || PRODUCER_EIPS.contains(eipName))) {
-                    for (int j = i + 1; j < cursorRow; j++) {
-                        String jl = editState.getLine(j);
-                        if (!jl.isBlank() && jl.trim().startsWith("uri:")) {
-                            return j;
+                    // for from:/to: blocks, scope to the uri: line if cursor 
is below it
+                    if ("from".equals(eipName) || 
CONSUMER_EIPS.contains(eipName)
+                            || PRODUCER_EIPS.contains(eipName)) {
+                        for (int j = i + 1; j < cursorRow; j++) {
+                            String jl = editState.getLine(j);
+                            if (!jl.isBlank() && jl.trim().startsWith("uri:")) 
{
+                                return j;
+                            }
                         }
                     }
+                    return i;
                 }
                 cursorIndent = indent;
             }
@@ -1225,6 +1282,18 @@ class SourceViewer {
     }
 
     private static String extractSchemeFromUriLine(String trimmed) {
+        String value = extractUriValue(trimmed);
+        if (value == null) {
+            return null;
+        }
+        int schemeEnd = value.indexOf(':');
+        if (schemeEnd > 0) {
+            return value.substring(0, schemeEnd);
+        }
+        return value;
+    }
+
+    static String extractUriValue(String trimmed) {
         int colonIdx = trimmed.indexOf(':');
         if (colonIdx < 0) {
             return null;
@@ -1236,10 +1305,6 @@ class SourceViewer {
         if (value.endsWith("\"") || value.endsWith("'")) {
             value = value.substring(0, value.length() - 1);
         }
-        int schemeEnd = value.indexOf(':');
-        if (schemeEnd > 0) {
-            return value.substring(0, schemeEnd);
-        }
         if (!value.isEmpty()) {
             return value;
         }
@@ -1380,13 +1445,28 @@ class SourceViewer {
                     }
                 }
             } else {
+                // auto-insert parameters: block if cursor is below uri: 
without one
+                if (ctx.needsParameters() && lineText.isBlank()) {
+                    int indent = deriveInsertionIndent(row);
+                    String indentStr = " ".repeat(indent);
+                    editState.moveCursorToLineStart();
+                    editState.insert(indentStr + "parameters:");
+                    editState.insert('\n');
+                    editState.insert(indentStr + "  ");
+                    dirty = true;
+                    trimmed = "";
+                }
+
                 String filter = trimmed;
                 String role = ctx.consumer() ? "consumer" : "producer";
-                java.util.Set<String> existing = 
collectExistingParameters(row);
+                java.util.Set<String> existing = 
collectExistingParameters(editState.cursorRow());
                 String context = "yaml:" + ctx.component() + ":" + role;
                 if (!existing.isEmpty()) {
                     context += ":" + String.join(",", existing);
                 }
+                if (ctx.uri() != null) {
+                    context += "|" + ctx.uri();
+                }
                 List<AutocompletePopup.CompletionItem> items = 
autocompleteProvider.provide(context);
                 if (items != null && !items.isEmpty()) {
                     autocompletePopup = new AutocompletePopup(items, filter, 
filter);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
index d75bcb1255f2..0b46671a3c04 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/FuzzyFilterTest.java
@@ -182,4 +182,53 @@ class FuzzyFilterTest {
         assertNotNull(positions);
         assertArrayEquals(new int[] { 0, 2 }, positions);
     }
+
+    // ---- camelCaseMatch tests ----
+
+    @Test
+    void camelCaseMatchPrefixSingleChar() {
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "h"));
+        assertFalse(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "f"));
+    }
+
+    @Test
+    void camelCaseMatchPrefix() {
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", 
"header"));
+    }
+
+    @Test
+    void camelCaseMatchSubstring() {
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "fil"));
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", 
"strategy"));
+    }
+
+    @Test
+    void camelCaseMatchSegmentInitials() {
+        // h-eader, f-ilter, s-trategy
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "hfs"));
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "hf"));
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "fs"));
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "hs"));
+    }
+
+    @Test
+    void camelCaseMatchSegmentInitialsNoMatch() {
+        // no segment starts with 'l' or 'r' after 'h'
+        assertFalse(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "hlr"));
+        assertFalse(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "xyz"));
+    }
+
+    @Test
+    void camelCaseMatchCaseInsensitive() {
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "HFS"));
+        assertTrue(FuzzyFilter.camelCaseMatch("headerFilterStrategy", "Hfs"));
+        assertTrue(FuzzyFilter.camelCaseMatch("brokers", "BROKERS"));
+    }
+
+    @Test
+    void camelCaseMatchSimpleKey() {
+        assertTrue(FuzzyFilter.camelCaseMatch("brokers", "br"));
+        assertTrue(FuzzyFilter.camelCaseMatch("brokers", "b"));
+        assertFalse(FuzzyFilter.camelCaseMatch("brokers", "x"));
+    }
 }

Reply via email to