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 8acd165a86ef CAMEL-24395: camel-jbang - keep the code in place when 
switching between viewing and editing a source file (#27296)
8acd165a86ef is described below

commit 8acd165a86ef68da05bfa030c44a0e3150b15ba0
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 21:13:47 2026 +0200

    CAMEL-24395: camel-jbang - keep the code in place when switching between 
viewing and editing a source file (#27296)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  47 +++++-
 .../commands/tui/SourceViewerEditPositionTest.java | 165 +++++++++++++++++++++
 2 files changed, 207 insertions(+), 5 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 63a1c5feb592..62ad7cfc51ee 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
@@ -238,6 +238,8 @@ class SourceViewer {
      */
     private Map<Integer, String> viewErrors = Collections.emptyMap();
     private boolean editInitialScroll;
+    /** The top line of the view when F4 was pressed, so the editor opens on 
the same screen; -1 when not known. */
+    private int editStartTop = -1;
     private long lastBackgroundValidationTime;
     private String lastBackgroundValidationContent;
     private static final long BACKGROUND_VALIDATION_INTERVAL_MS = 2000;
@@ -1134,6 +1136,7 @@ class SourceViewer {
         }
         editState.moveCursorToLineStart();
         editInitialScroll = true;
+        editStartTop = markdownMode ? -1 : scrollY;
         markdownModeBeforeEdit = markdownMode;
         markdownMode = false;
         quickDocEnabled = false;
@@ -1206,6 +1209,8 @@ class SourceViewer {
 
     private void exitEditMode() {
         boolean wasEditing = editMode;
+        int cursorRow = editState.cursorRow();
+        int top = editState.scrollRow();
         editMode = false;
         editState.clear();
         editHistory.clear();
@@ -1223,6 +1228,29 @@ class SourceViewer {
             markdownMode = markdownModeBeforeEdit;
         }
         markdownModeBeforeEdit = false;
+        editStartTop = -1;
+        if (wasEditing) {
+            keepEditorPosition(cursorRow, top);
+        }
+    }
+
+    /** The top line the editor opens on: the top line of the view, moved only 
as far as the cursor must stay seen. */
+    static int editorTopKeepingCursor(int viewTop, int cursorRow, int 
viewportHeight) {
+        int top = Math.min(Math.max(0, viewTop), cursorRow);
+        return Math.max(top, cursorRow - Math.max(1, viewportHeight) + 1);
+    }
+
+    /**
+     * The view continues where the editor was: the cursor line is selected 
and the same line is at the top, so leaving
+     * the editor does not move the code on the screen.
+     */
+    private void keepEditorPosition(int cursorRow, int top) {
+        if (lines.isEmpty()) {
+            return;
+        }
+        selectedLine = Math.min(Math.max(0, cursorRow), lines.size() - 1);
+        scrollY = Math.min(Math.max(0, top), selectedLine);
+        pendingScroll = false;
     }
 
     private boolean isPropertiesFile() {
@@ -1925,11 +1953,15 @@ class SourceViewer {
             dirty = false;
             Path path = editableFile;
             boolean restoreMarkdownMode = markdownModeBeforeEdit;
+            int cursorRow = editState.cursorRow();
+            int top = editState.scrollRow();
             notifySaved(editableFile);
             editMode = false;
             editState.clear();
             markdownModeBeforeEdit = false;
+            editStartTop = -1;
             loadFile(path);
+            keepEditorPosition(cursorRow, top);
             if (isMarkdownFile) {
                 markdownMode = restoreMarkdownMode;
             }
@@ -2626,14 +2658,19 @@ class SourceViewer {
                 .showLineNumbers(!plainMode)
                 .lineNumberStyle(Style.EMPTY.dim())
                 .build();
-        // on first render, position cursor at 2/3 of viewport before TextArea 
renders
+        // on first render, keep the top line of the view (F4), else position 
the cursor at 2/3 of the viewport
         if (editInitialScroll) {
             editInitialScroll = false;
             int viewportH = textAreaRect.height();
-            int twoThirds = viewportH * 2 / 3;
-            int targetScroll = Math.max(0, editState.cursorRow() - twoThirds);
-            if (targetScroll > 0) {
-                editState.scrollDown(targetScroll, viewportH);
+            int targetScroll = editStartTop >= 0
+                    ? editorTopKeepingCursor(editStartTop, 
editState.cursorRow(), viewportH)
+                    : Math.max(0, editState.cursorRow() - viewportH * 2 / 3);
+            editStartTop = -1;
+            int delta = targetScroll - editState.scrollRow();
+            if (delta > 0) {
+                editState.scrollDown(delta, viewportH);
+            } else if (delta < 0) {
+                editState.scrollUp(-delta);
             }
         }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditPositionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditPositionTest.java
new file mode 100644
index 000000000000..efd944896faa
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerEditPositionTest.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Switching between viewing and editing a file (F4, Esc, F5) keeps the code 
where it is on the screen (CAMEL-24395).
+ */
+class SourceViewerEditPositionTest {
+
+    private static final Pattern LINE_NUMBER = Pattern.compile("^│(>>| . |   
)\\s*(\\d+) \\|");
+
+    @TempDir
+    Path tempDir;
+
+    private SourceViewer viewer;
+    private Path sourceFile;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        Theme.resetForTesting();
+        viewer = new SourceViewer();
+        viewer.setValidateOnSave(false);
+        List<String> lines = new ArrayList<>();
+        for (int i = 1; i <= 100; i++) {
+            lines.add("key" + i + "=value" + i);
+        }
+        sourceFile = tempDir.resolve("notes.properties");
+        Files.writeString(sourceFile, String.join("\n", lines) + "\n");
+        viewer.loadFile(sourceFile);
+    }
+
+    @Test
+    void editorOpensOnTheScreenOfTheView() {
+        viewer.goToLine(49);
+        render();
+        // move the selection up within the screen, away from where the editor 
used to put the cursor
+        for (int i = 0; i < 8; i++) {
+            key(KeyCode.UP);
+        }
+        Screen view = render();
+        assertThat(view.selectedLine()).isEqualTo(42);
+
+        key(KeyCode.F4);
+        Screen edit = render();
+
+        assertThat(viewer.isEditMode()).isTrue();
+        assertThat(edit.topLine()).isEqualTo(view.topLine());
+        assertThat(edit.selectedLine()).isEqualTo(42);
+        assertThat(edit.selectedRow()).isEqualTo(view.selectedRow());
+    }
+
+    @Test
+    void leavingTheEditorKeepsTheCursorLineAndTheScreen() {
+        viewer.goToLine(49);
+        render();
+        key(KeyCode.F4);
+        render();
+        for (int i = 0; i < 10; i++) {
+            key(KeyCode.UP);
+        }
+        Screen edit = render();
+        assertThat(edit.selectedLine()).isEqualTo(40);
+
+        key(KeyCode.ESCAPE);
+        Screen view = render();
+
+        assertThat(viewer.isEditMode()).isFalse();
+        assertThat(viewer.getSelectedLine()).isEqualTo(39);
+        assertThat(view.topLine()).isEqualTo(edit.topLine());
+        assertThat(view.selectedRow()).isEqualTo(edit.selectedRow());
+    }
+
+    @Test
+    void saveAndCloseKeepsTheCursorLine() throws Exception {
+        viewer.goToLine(49);
+        render();
+        key(KeyCode.F4);
+        render();
+        for (int i = 0; i < 5; i++) {
+            key(KeyCode.DOWN);
+        }
+        viewer.handleKeyEvent(KeyEvent.ofChar('x', KeyModifiers.NONE));
+        Screen edit = render();
+
+        key(KeyCode.F5);
+        Screen view = render();
+
+        assertThat(viewer.isEditMode()).isFalse();
+        assertThat(Files.readString(sourceFile)).contains("xkey55=value55");
+        assertThat(view.selectedLine()).isEqualTo(55);
+        assertThat(view.topLine()).isEqualTo(edit.topLine());
+    }
+
+    @Test
+    void editorTopMovesOnlyAsFarAsTheCursorMustBeSeen() {
+        assertThat(SourceViewer.editorTopKeepingCursor(27, 49, 
24)).isEqualTo(27);
+        assertThat(SourceViewer.editorTopKeepingCursor(0, 49, 
24)).isEqualTo(26);
+        assertThat(SourceViewer.editorTopKeepingCursor(60, 49, 
24)).isEqualTo(49);
+    }
+
+    private void key(KeyCode code) {
+        viewer.handleKeyEvent(KeyEvent.ofKey(code, KeyModifiers.NONE));
+    }
+
+    private Screen render() {
+        Rect area = new Rect(0, 0, 60, 20);
+        Buffer buffer = Buffer.empty(area);
+        viewer.render(Frame.forTesting(buffer), area);
+        String[] rows = TuiTestHelper.bufferToString(buffer).split("\n");
+        int top = -1;
+        int selectedRow = -1;
+        int selected = -1;
+        for (int r = 0; r < rows.length; r++) {
+            Matcher m = LINE_NUMBER.matcher(rows[r]);
+            if (m.find()) {
+                int number = Integer.parseInt(m.group(2));
+                if (top < 0) {
+                    top = number;
+                }
+                if (">>".equals(m.group(1))) {
+                    selectedRow = r;
+                    selected = number;
+                }
+            }
+        }
+        return new Screen(top, selectedRow, selected);
+    }
+
+    /** The first line number on the screen, and the screen row and number of 
the line marked with >>. */
+    private record Screen(int topLine, int selectedRow, int selectedLine) {
+    }
+}

Reply via email to