You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/02 09:27:41 UTC
(camel) branch main updated (dc0cc1d20bf3 -> 353c25b5bcbc)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
from dc0cc1d20bf3 CAMEL-23794 - revert fedora base image version to stable version 43 for SMB Tests
new 072b29d068f7 chore: Clarify Simple OGNL method calls vs built-in substring function in docs
new 353c25b5bcbc CAMEL-24393: Smart paste with auto-indent in TUI source editor
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../camel/catalog/docs/simple-functions.adoc | 9 +++
.../org/apache/camel/catalog/docs/simple-ognl.adoc | 17 ++++
.../modules/languages/pages/simple-functions.adoc | 9 +++
.../docs/modules/languages/pages/simple-ognl.adoc | 17 ++++
.../dsl/jbang/core/commands/tui/SourceViewer.java | 92 +++++++++++++++++-----
.../jbang/core/commands/tui/TuiBackendHelper.java | 3 +-
.../commands/tui/SourceViewerEditorOpsTest.java | 92 ++++++++++++++++++++++
.../commands/tui/SourceViewerPasteIndentTest.java | 15 ++++
8 files changed, 235 insertions(+), 19 deletions(-)
(camel) 02/02: CAMEL-24393: Smart paste with auto-indent in TUI source editor
Posted by da...@apache.org.
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 353c25b5bcbcb9ca2f3660a22f78a8ac751649a4
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Sep 2 11:27:11 2026 +0200
CAMEL-24393: Smart paste with auto-indent in TUI source editor
Enable bracketed paste in the TUI so multi-line pastes arrive as a single
PasteEvent, and auto-indent pasted YAML blocks to match the surrounding
route structure in the source editor:
- Enable bracketedPaste on the TuiRunner (TuiBackendHelper).
- Wire paste through adjustPasteIndent/reindentBlock so pasted blocks are
reindented to the target column while preserving relative indentation.
- Normalize \r\n and bare \r line endings so pastes are not collapsed into
a single line.
- Align a paste inserted before an existing line with that line's own indent.
- On a blank line (including one carrying ENTER auto-indent whitespace),
infer the block indent from the surrounding context and align a pasted
list item with the nearest sibling step; strip the auto-indent whitespace
first so it is not stacked on top of the reindented block.
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../dsl/jbang/core/commands/tui/SourceViewer.java | 92 +++++++++++++++++-----
.../jbang/core/commands/tui/TuiBackendHelper.java | 3 +-
.../commands/tui/SourceViewerEditorOpsTest.java | 92 ++++++++++++++++++++++
.../commands/tui/SourceViewerPasteIndentTest.java | 15 ++++
4 files changed, 183 insertions(+), 19 deletions(-)
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 07a4fdf13369..bd40a71105bf 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
@@ -1585,29 +1585,64 @@ class SourceViewer {
}
private String adjustPasteIndent(String text, int cursorRow) {
- int targetIndent = editState.cursorCol();
- // when cursor is at col 0, infer indent from the previous non-blank line
- if (targetIndent == 0) {
- for (int i = cursorRow - 1; i >= 0; i--) {
- String l = editState.getLine(i);
- if (!l.isBlank()) {
- targetIndent = countLeadingSpaces(l);
- String trimmed = l.trim();
- if (trimmed.startsWith("- ")) {
- trimmed = trimmed.substring(2).trim();
- }
- // if previous line is a parent key, indent children deeper
- if (trimmed.endsWith(":")) {
- targetIndent += 2;
- }
- break;
+ String current = editState.getLine(cursorRow);
+ int targetIndent;
+ if (current == null || current.isBlank()) {
+ // on a blank line (including one carrying ENTER auto-indent whitespace, which handlePaste
+ // strips before inserting): infer the block indent from the context above the cursor and
+ // apply it to every pasted line
+ int fallback = current == null ? 0 : countLeadingSpaces(current);
+ targetIndent = inferBlankLineIndent(text, cursorRow, fallback);
+ } else if (editState.cursorCol() == 0) {
+ // inserting before an existing line: match that line's own indent
+ targetIndent = countLeadingSpaces(current);
+ } else {
+ // pasting into the middle of existing content: keep the cursor column
+ targetIndent = editState.cursorCol();
+ }
+ return reindentBlock(text, targetIndent);
+ }
+
+ private int inferBlankLineIndent(String text, int cursorRow, int fallback) {
+ int prevIndent = -1;
+ boolean prevIsParentKey = false;
+ int listIndent = -1;
+ for (int i = cursorRow - 1; i >= 0; i--) {
+ String l = editState.getLine(i);
+ if (l.isBlank()) {
+ continue;
+ }
+ if (prevIndent < 0) {
+ // nearest non-blank line: its indent, and whether it opens a child block
+ prevIndent = countLeadingSpaces(l);
+ String t = l.trim();
+ if (t.startsWith("- ")) {
+ t = t.substring(2).trim();
}
+ prevIsParentKey = t.endsWith(":");
+ }
+ if (l.trim().startsWith("- ")) {
+ // nearest existing list item — the sibling level for a pasted list item
+ listIndent = countLeadingSpaces(l);
+ break;
}
}
- return reindentBlock(text, targetIndent);
+ String firstTrimmed = firstNonBlankTrimmed(text);
+ boolean pasteIsListItem = firstTrimmed.startsWith("- ") || firstTrimmed.equals("-");
+ if (pasteIsListItem && listIndent >= 0) {
+ // align a pasted step with the nearest existing sibling step
+ return listIndent;
+ } else if (prevIndent >= 0) {
+ // otherwise follow the previous line, indenting deeper under a parent key
+ return prevIndent + (prevIsParentKey ? 2 : 0);
+ }
+ return fallback;
}
static String reindentBlock(String text, int targetIndent) {
+ // normalize line endings: some terminals deliver pasted line breaks as \r\n or bare \r,
+ // which would otherwise collapse a multi-line paste into a single line
+ text = text.replace("\r\n", "\n").replace('\r', '\n');
text = text.replace("\t", " ");
String[] pasteLines = text.split("\n", -1);
int minIndent = Integer.MAX_VALUE;
@@ -1641,6 +1676,15 @@ class SourceViewer {
return sb.toString();
}
+ private static String firstNonBlankTrimmed(String text) {
+ for (String line : text.split("\r\n|\r|\n", -1)) {
+ if (!line.isBlank()) {
+ return line.trim();
+ }
+ }
+ return "";
+ }
+
private static int countLeadingSpaces(String line) {
int count = 0;
for (int i = 0; i < line.length(); i++) {
@@ -2366,7 +2410,19 @@ class SourceViewer {
}
if (text != null && !text.isEmpty()) {
recordEditChange();
- editState.insert(text);
+ int row = editState.cursorRow();
+ String current = editState.getLine(row);
+ String adjusted = adjustPasteIndent(text, row);
+ if (current != null && current.isBlank() && !current.isEmpty()) {
+ // strip the blank line's leading whitespace (e.g. from ENTER auto-indent) so the
+ // reindented block's own indent is not stacked on top of it
+ editState.moveCursorToLineStart();
+ int n = current.length();
+ for (int i = 0; i < n; i++) {
+ editState.deleteForward();
+ }
+ }
+ editState.insert(adjusted);
}
return;
}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
index bbb0b052957f..2b5fa294e30f 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
@@ -41,7 +41,8 @@ final class TuiBackendHelper {
}
static TuiRunner createTuiRunner(Backend backend) throws Exception {
- return TuiRunner.create(TuiConfig.builder().backend(applyRecording(backend)).mouseCapture(true).build());
+ return TuiRunner.create(
+ TuiConfig.builder().backend(applyRecording(backend)).mouseCapture(true).bracketedPaste(true).build());
}
/**
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditorOpsTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditorOpsTest.java
index cdbaec126b85..027c81702304 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditorOpsTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditorOpsTest.java
@@ -151,6 +151,98 @@ class SourceViewerEditorOpsTest {
assertThat(footer).contains("Ctrl+K");
}
+ @Test
+ void pasteAutoIndentsToMatchSurroundingBlock() {
+ // place cursor at column 0 of the "log:warn" line; its siblings are indented 8 spaces
+ String[] lines = viewer.editText().split("\n", -1);
+ int row = -1;
+ for (int i = 0; i < lines.length; i++) {
+ if (lines[i].contains("log:warn")) {
+ row = i;
+ break;
+ }
+ }
+ assertThat(row).isGreaterThanOrEqualTo(0);
+ SourceEditorNavigation.positionCursor(viewer.editState(), row, 0);
+
+ // paste an unindented step; it should be reindented to align with the 8-space siblings
+ viewer.handlePaste("- to: log:error\n");
+
+ assertThat(viewer.editText()).contains(" - to: log:error\n");
+ }
+
+ @Test
+ void pasteAlignsWithCurrentLineNotDeeperPredecessor() throws Exception {
+ // a "- log:" step (indent 8) preceded by a far deeper line (indent 16); pasting before it
+ // must align with the step's own indent, not the deeper predecessor
+ Path nested = tempDir.resolve("nested.camel.yaml");
+ Files.writeString(nested, """
+ - route:
+ from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ expression:
+ simple:
+ expression: "hi"
+ - log:
+ message: "x"
+ """, StandardCharsets.UTF_8);
+ viewer.loadFile(nested);
+ viewer.enterEditMode();
+
+ String[] lines = viewer.editText().split("\n", -1);
+ int row = -1;
+ for (int i = 0; i < lines.length; i++) {
+ if (lines[i].contains("- log:")) {
+ row = i;
+ break;
+ }
+ }
+ assertThat(row).isGreaterThanOrEqualTo(0);
+ SourceEditorNavigation.positionCursor(viewer.editState(), row, 0);
+
+ viewer.handlePaste("- to:\n uri: mock:dead\n");
+
+ assertThat(viewer.editText()).contains(" - to:\n uri: mock:dead\n - log:");
+ }
+
+ @Test
+ void pasteListItemOnBlankLineAlignsWithNearestSiblingStep() throws Exception {
+ // last line is a deep leaf (indent 12); pasting a step on a blank line after it must align
+ // with the nearest sibling step (indent 8), not the deeper leaf above it
+ Path nested = tempDir.resolve("nested-append.camel.yaml");
+ Files.writeString(nested, """
+ - route:
+ from:
+ uri: timer:tick
+ steps:
+ - log:
+ message: "${body}"
+ """, StandardCharsets.UTF_8);
+ viewer.loadFile(nested);
+ viewer.enterEditMode();
+
+ // place the cursor at the end of the deep leaf line (message:, indent 12) and press ENTER —
+ // the editor auto-indents the new line to col 12; pasting a step must still align it with the
+ // nearest sibling step (indent 8), not the auto-indent column
+ int row = -1;
+ String[] lines = viewer.editText().split("\n", -1);
+ for (int i = 0; i < lines.length; i++) {
+ if (lines[i].contains("message:")) {
+ row = i;
+ break;
+ }
+ }
+ assertThat(row).isGreaterThanOrEqualTo(0);
+ SourceEditorNavigation.positionCursor(viewer.editState(), row, viewer.editState().getLine(row).length());
+ viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+ viewer.handlePaste("- to:\n uri: mock:dead\n");
+
+ assertThat(viewer.editText()).contains(" - to:\n uri: mock:dead");
+ }
+
private void moveCursorToLineContaining(String needle) {
String[] lines = viewer.editText().split("\n", -1);
for (int row = 0; row < lines.length; row++) {
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerPasteIndentTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerPasteIndentTest.java
index 8ba71db33bc1..41bef860d4d4 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerPasteIndentTest.java
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerPasteIndentTest.java
@@ -88,6 +88,21 @@ class SourceViewerPasteIndentTest {
assertThat(result).isEqualTo("- log:\n message: Hello");
}
+ @Test
+ void carriageReturnLineEndingsNormalized() {
+ // bare \r line endings (some terminals) must not collapse into a single line
+ String paste = "- to:\r uri: mock:dead";
+ String result = SourceViewer.reindentBlock(paste, 8);
+ assertThat(result).isEqualTo(" - to:\n uri: mock:dead");
+ }
+
+ @Test
+ void crlfLineEndingsNormalized() {
+ String paste = "- to:\r\n uri: mock:dead";
+ String result = SourceViewer.reindentBlock(paste, 8);
+ assertThat(result).isEqualTo(" - to:\n uri: mock:dead");
+ }
+
@Test
void trailingNewlinePreserved() {
String paste = "- log:\n message: Hello\n";
(camel) 01/02: chore: Clarify Simple OGNL method calls vs built-in substring function in docs
Posted by da...@apache.org.
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 072b29d068f76bf8ac4dd42c4f26fe02f0da10e2
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Sep 2 11:10:59 2026 +0200
chore: Clarify Simple OGNL method calls vs built-in substring function in docs
OGNL invokes the real Java method (e.g. String.substring), which has
different semantics than the same-named built-in Simple functions such
as substring(head,tail) that support negative indices. Add clarifying
notes to the simple-ognl and simple-functions doc pages to avoid the
name-collision trap.
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../org/apache/camel/catalog/docs/simple-functions.adoc | 9 +++++++++
.../org/apache/camel/catalog/docs/simple-ognl.adoc | 17 +++++++++++++++++
.../docs/modules/languages/pages/simple-functions.adoc | 9 +++++++++
.../main/docs/modules/languages/pages/simple-ognl.adoc | 17 +++++++++++++++++
4 files changed, 52 insertions(+)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
index 0a6959376091..046c176a5a17 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
@@ -578,6 +578,15 @@ The `substring`, `substringBefore`, and `substringAfter` functions are all simil
Suppose the message body contains `ABCDEFGHIJK` then `${substring(3)}` returns `DEFGHIJK`, and `${substring(-3)}` returns `ABCDEFGH`.
If you want to clip the first and last character you can use `${substring(1,-1)}` returning `BCDEFGHIJ`.
+[NOTE]
+====
+These `substring` functions are Camel functions with their own semantics (such as negative numbers clipping
+from the end). They are *not* the same as calling Java's `java.lang.String.substring` via
+xref:simple-ognl.adoc[OGNL]. For example `${substring(-3)}` (function) clips the last three characters,
+whereas `${body.substring(-3)}` (OGNL) invokes `String.substring(-3)` directly and throws
+`StringIndexOutOfBoundsException`.
+====
+
Now suppose the message body contains `Hello great big World how are you`.
Then `${substringBefore('World')}` return `"Hello great big "`.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
index 56a429400d4f..a79ab80ca0fa 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
@@ -29,6 +29,23 @@ Camel's OGNL support is for invoking methods only. You cannot access fields. Cam
When using *OGNL* then `camel-bean` JAR is required to be on the classpath.
====
+[NOTE]
+====
+OGNL invokes the *real Java method* using standard Java semantics. For example `${body.substring(2)}`
+calls `java.lang.String.substring(int)` directly.
+
+This is *not* the same as the built-in xref:simple-functions.adoc[Simple function] of the same name.
+A function such as `${substring(2)}` (with no object prefix) is a Camel function with its own semantics;
+for example it accepts negative numbers to clip from the end. By contrast `${body.substring(-2)}` calls the
+JDK method, which throws `StringIndexOutOfBoundsException` because `java.lang.String` does not accept a
+negative index.
+
+So when you want Camel's function semantics, use the built-in functions (optionally passing the source as an
+argument, such as `${substring(0,-2,${header.foo})}`), and reserve OGNL dot-notation for invoking genuine
+Java methods on the object. For fluent, function-based transformations you can also chain built-in functions
+with the xref:simple-advanced.adoc[chain operator] `~>`.
+====
+
=== Built-in Functions supporting OGNL
The following functions support _OGNL syntax_:
diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
index 0a6959376091..046c176a5a17 100644
--- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
+++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
@@ -578,6 +578,15 @@ The `substring`, `substringBefore`, and `substringAfter` functions are all simil
Suppose the message body contains `ABCDEFGHIJK` then `${substring(3)}` returns `DEFGHIJK`, and `${substring(-3)}` returns `ABCDEFGH`.
If you want to clip the first and last character you can use `${substring(1,-1)}` returning `BCDEFGHIJ`.
+[NOTE]
+====
+These `substring` functions are Camel functions with their own semantics (such as negative numbers clipping
+from the end). They are *not* the same as calling Java's `java.lang.String.substring` via
+xref:simple-ognl.adoc[OGNL]. For example `${substring(-3)}` (function) clips the last three characters,
+whereas `${body.substring(-3)}` (OGNL) invokes `String.substring(-3)` directly and throws
+`StringIndexOutOfBoundsException`.
+====
+
Now suppose the message body contains `Hello great big World how are you`.
Then `${substringBefore('World')}` return `"Hello great big "`.
diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
index 56a429400d4f..a79ab80ca0fa 100644
--- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
+++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
@@ -29,6 +29,23 @@ Camel's OGNL support is for invoking methods only. You cannot access fields. Cam
When using *OGNL* then `camel-bean` JAR is required to be on the classpath.
====
+[NOTE]
+====
+OGNL invokes the *real Java method* using standard Java semantics. For example `${body.substring(2)}`
+calls `java.lang.String.substring(int)` directly.
+
+This is *not* the same as the built-in xref:simple-functions.adoc[Simple function] of the same name.
+A function such as `${substring(2)}` (with no object prefix) is a Camel function with its own semantics;
+for example it accepts negative numbers to clip from the end. By contrast `${body.substring(-2)}` calls the
+JDK method, which throws `StringIndexOutOfBoundsException` because `java.lang.String` does not accept a
+negative index.
+
+So when you want Camel's function semantics, use the built-in functions (optionally passing the source as an
+argument, such as `${substring(0,-2,${header.foo})}`), and reserve OGNL dot-notation for invoking genuine
+Java methods on the object. For fluent, function-based transformations you can also chain built-in functions
+with the xref:simple-advanced.adoc[chain operator] `~>`.
+====
+
=== Built-in Functions supporting OGNL
The following functions support _OGNL syntax_: