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) {
+ }
+}