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"));
+ }
}