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 <[email protected]>
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 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../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";

Reply via email to