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 681f1ae5b15d camel-tui: Ctrl+G go-to popup supports line number jump
681f1ae5b15d is described below

commit 681f1ae5b15d9b0f71ff188d92f262771044fa35
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Aug 13 22:45:24 2026 +0200

    camel-tui: Ctrl+G go-to popup supports line number jump
    
    The Go to Node popup (Ctrl+G) now also accepts a line number.
    Type a number like 47 and press Enter to jump directly to that line.
    The popup title and prompt update to reflect line-number mode.
    Ctrl+G now works on any file, not just files with route nodes.
    
    camel-tui: Use Ctrl+S for save in editor, drop Shift+F5
    
    Replace Shift+F5 with Ctrl+S for save-and-continue-edit, which is
    the standard save shortcut developers expect. F5 remains as
    save-and-close. Reduces footer clutter by one hint.
    
    camel-tui: Show scope breadcrumb in editor top title bar
    
    When editing a Camel YAML route file, the top title bar now shows
    a breadcrumb path like route > from > parameters next to the
    filename, giving spatial awareness in deeply nested YAML.
    
    camel-tui: Breadcrumb shows only structural parent keys
    
    Skip property keys like id, period from the breadcrumb and only
    show structural parent keys (lines ending with colon). Also skip
    implied wrappers like steps, expression, uri. Show route as the
    top-level context.
    
    camel-tui: Quick doc panel in edit mode (Ctrl+Q)
    
    Add a fixed-height documentation panel at the bottom of the editor
    that shows context-aware docs for the cursor line. Supports component
    docs for URI lines, endpoint option docs inside parameters blocks,
    and EIP option docs (e.g. message on log). Uses full descriptions
    without truncation and word wraps. Toggle with Ctrl+Q in edit mode
    and q in view mode (replaces the old i key). On by default.
    
    camel-tui: Autocomplete popup auto-sizes for long option descriptions
    
    In value completion mode, the popup now calculates the height needed
    to show the full option description and expands to fit. When the
    description is longer than the space below or above the cursor, the
    popup uses the full editor area and positions at the top to avoid
    clipping long docs like Kafka enableIdempotence.
    
    camel-tui: Quick doc panel in view mode, always on, no toggle
    
    Replace the interleaved inline doc with a fixed bottom panel in
    view mode, same as edit mode. Always on for Camel YAML and
    properties files. Uses raw code lines from codeData instead of
    formatted lines with line number prefixes. Removed toggle shortcut
    since the panel is always visible.
    
    camel-tui: Smart paste auto-adjusts YAML indentation
    
    When pasting multi-line text in the YAML editor, the indentation
    is automatically adjusted to match the cursor position. The
    internal structure (relative indentation between lines) is
    preserved while the base indent shifts to align with the context.
    
    camel-tui: Smart paste blocked on TamboUI bracketed paste support
    
    The reindentBlock logic works correctly (unit tests pass) but
    cannot be triggered because the terminal sends pasted text as
    individual character events rather than a PasteEvent block.
    TamboUI needs to enable bracketed paste mode to fix this.
    Reverted the handlePaste integration; kept reindentBlock and
    tests for when bracketed paste is available.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/core/commands/tui/AutocompletePopup.java |  22 +-
 .../core/commands/tui/GotoSourceNodePopup.java     |  55 +++-
 .../dsl/jbang/core/commands/tui/SourceTab.java     | 290 ++++++++++++++++--
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 333 ++++++++++++++++-----
 .../core/commands/tui/GotoSourceNodePopupTest.java |  73 ++++-
 .../commands/tui/SourceViewerPasteIndentTest.java  |  97 ++++++
 6 files changed, 771 insertions(+), 99 deletions(-)

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 79b845d7673c..faa66df23bb4 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
@@ -233,11 +233,31 @@ class AutocompletePopup {
         int popupH = Math.min(contentH + 2, maxH);
         popupH = Math.max(popupH, 12);
 
+        // in value mode, ensure enough height for the detail panel description
+        if (valueMode) {
+            Integer sel = listState.selected();
+            if (sel != null && sel < filteredItems.size()) {
+                CompletionItem item = filteredItems.get(sel);
+                if (item.description() != null) {
+                    int rightW = popupW - Math.max(30, popupW * 2 / 5);
+                    int descWidth = Math.max(20, rightW - 4);
+                    int metaLines = 6;
+                    int descLines = (item.description().length() + descWidth - 
1) / descWidth;
+                    int neededH = metaLines + descLines + 4;
+                    popupH = Math.max(popupH, Math.min(neededH, maxH));
+                }
+            }
+        }
+
         int x = area.left() + 2;
         int y;
         int spaceBelow = area.bottom() - (area.top() + cursorScreenRow + 1);
         int spaceAbove = cursorScreenRow;
-        if (spaceBelow >= popupH || spaceBelow >= spaceAbove) {
+        if (valueMode && popupH > Math.max(spaceBelow, spaceAbove)) {
+            // value mode with long description: use full area height, 
position at top
+            popupH = Math.min(popupH, area.height());
+            y = area.top();
+        } else if (spaceBelow >= popupH || spaceBelow >= spaceAbove) {
             y = area.top() + cursorScreenRow + 1;
             popupH = Math.min(popupH, Math.max(8, spaceBelow));
         } else {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
index 12909344d7c2..2273963ed493 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopup.java
@@ -50,21 +50,26 @@ class GotoSourceNodePopup {
     private List<YamlRouteNodeScanner.NodeEntry> allEntries;
     private List<YamlRouteNodeScanner.NodeEntry> filteredEntries;
     private YamlRouteNodeScanner.NodeEntry selectedEntry;
+    private int gotoLineNumber = -1;
+    private int totalLineCount;
 
     boolean isVisible() {
         return visible;
     }
 
-    void open(List<YamlRouteNodeScanner.NodeEntry> entries) {
+    void open(List<YamlRouteNodeScanner.NodeEntry> entries, int lineCount) {
         allEntries = entries != null ? new ArrayList<>(entries) : List.of();
+        totalLineCount = lineCount;
         visible = true;
         filter.clearFilter();
+        gotoLineNumber = -1;
         rebuildList();
     }
 
     void close() {
         visible = false;
         filter.clearFilter();
+        gotoLineNumber = -1;
     }
 
     YamlRouteNodeScanner.NodeEntry consumeSelection() {
@@ -73,6 +78,12 @@ class GotoSourceNodePopup {
         return entry;
     }
 
+    int consumeGotoLineNumber() {
+        int line = gotoLineNumber;
+        gotoLineNumber = -1;
+        return line;
+    }
+
     boolean handleKeyEvent(KeyEvent ke) {
         int size = filteredEntries != null ? filteredEntries.size() : 0;
         if (ke.isCancel()) {
@@ -108,6 +119,13 @@ class GotoSourceNodePopup {
             return true;
         }
         if (ke.isConfirm()) {
+            if (filter.hasFilter() && isLineNumber(filter.filter())) {
+                int num = Integer.parseInt(filter.filter().trim());
+                gotoLineNumber = Math.max(1, Math.min(num, totalLineCount));
+                visible = false;
+                filter.clearFilter();
+                return true;
+            }
             Integer sel = listState.selected();
             if (sel != null && filteredEntries != null && sel < 
filteredEntries.size()) {
                 selectedEntry = filteredEntries.get(sel);
@@ -143,10 +161,17 @@ class GotoSourceNodePopup {
         frame.renderWidget(Clear.INSTANCE, popup);
 
         String filterText = filter.hasFilter() ? filter.filter() : "";
+        boolean lineNumberMode = filter.hasFilter() && 
isLineNumber(filterText);
         String prompt = "> " + filterText + "█";
 
         List<ListItem> items = new ArrayList<>();
-        items.add(ListItem.from(Line.from(Span.styled(prompt, Theme.info()))));
+        if (lineNumberMode) {
+            items.add(ListItem.from(Line.from(
+                    Span.styled(prompt, Theme.info()),
+                    Span.styled("  Go to line " + filterText.trim(), 
Style.EMPTY.dim()))));
+        } else {
+            items.add(ListItem.from(Line.from(Span.styled(prompt, 
Theme.info()))));
+        }
         String sep = "─".repeat(Math.max(1, popupW - 2));
         items.add(ListItem.from(Line.from(Span.styled(sep, 
Style.EMPTY.dim()))));
 
@@ -220,9 +245,16 @@ class GotoSourceNodePopup {
 
         int total = allEntries != null ? allEntries.size() : 0;
         int shown = filteredEntries.size();
-        String title = shown == total
-                ? " Go to Node (" + total + ") "
-                : " Go to Node (" + shown + "/" + total + ") ";
+        String title;
+        if (lineNumberMode) {
+            title = " Go to Line ";
+        } else if (total == 0) {
+            title = " Go to Line (type a line number) ";
+        } else if (shown == total) {
+            title = " Go to Node (" + total + ") ";
+        } else {
+            title = " Go to Node (" + shown + "/" + total + ") ";
+        }
 
         ListWidget list = ListWidget.builder()
                 .items(items.toArray(ListItem[]::new))
@@ -341,6 +373,19 @@ class GotoSourceNodePopup {
         return -1;
     }
 
+    private static boolean isLineNumber(String text) {
+        String t = text.trim();
+        if (t.isEmpty()) {
+            return false;
+        }
+        for (int i = 0; i < t.length(); i++) {
+            if (!Character.isDigit(t.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private static String shortFileName(String filePath) {
         int lastSep = filePath.lastIndexOf('/');
         return lastSep >= 0 ? filePath.substring(lastSep + 1) : filePath;
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 84322cf740f4..40566877fff1 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
@@ -204,6 +204,11 @@ class SourceTab extends AbstractTab {
 
         if (gotoSourceNodePopup.isVisible()) {
             gotoSourceNodePopup.handleKeyEvent(ke);
+            int gotoLine = gotoSourceNodePopup.consumeGotoLineNumber();
+            if (gotoLine > 0) {
+                sourceViewer.goToLine(gotoLine - 1);
+                return true;
+            }
             YamlRouteNodeScanner.NodeEntry sel = 
gotoSourceNodePopup.consumeSelection();
             if (sel != null) {
                 openFileAt(sel.filePath(), sel.lineIndex());
@@ -211,8 +216,8 @@ class SourceTab extends AbstractTab {
             return true;
         }
 
-        if (ke.hasCtrl() && ke.isCharIgnoreCase('g') && !routeIndex.isEmpty()) 
{
-            gotoSourceNodePopup.open(buildSourceNodeIndex());
+        if (ke.hasCtrl() && ke.isCharIgnoreCase('g')) {
+            gotoSourceNodePopup.open(buildSourceNodeIndex(), 
sourceViewer.getLineCount());
             return true;
         }
 
@@ -404,7 +409,9 @@ class SourceTab extends AbstractTab {
             }
             if (!routeIndex.isEmpty()) {
                 TuiHelper.hint(spans, "g", "go to route");
-                TuiHelper.hint(spans, "Ctrl+G", "go to node");
+            }
+            if (sourceViewer.isVisible()) {
+                TuiHelper.hint(spans, "Ctrl+G", "go to");
             }
         }
     }
@@ -433,9 +440,10 @@ class SourceTab extends AbstractTab {
                 - **Up/Down** — scroll through source code
                 - **F4** — edit local file (plain text; only when file is 
writable)
                 - **Esc** — cancel edit (in edit mode) or close viewer
-                - **F5** — save file (in edit mode; Camel dev mode 
auto-reloads)
+                - **Ctrl+S** — save file and continue editing (Camel dev mode 
auto-reloads)
+                - **F5** — save file and close editor
                 - **Space** — cycle format (YAML/Java/XML) for Camel routes
-                - **i** — toggle inline Camel documentation for Camel source 
files
+                - Quick documentation panel is shown at the bottom for Camel 
source files
                 - **/** — search in source
                 - **h** — highlight text
                 - **n/N** — next/previous match
@@ -451,6 +459,7 @@ class SourceTab extends AbstractTab {
                 - **Ctrl+K** — delete current line
                 - **Ctrl+Left / Ctrl+Right** — word navigation
                 - **Home** — smart home (content indent, then column 0)
+                - Quick documentation panel is shown at the bottom (shows doc 
for current line)
                 - **F7** — show diff of unsaved changes
 
                 ## Edit Mode (Tab Completion)
@@ -495,11 +504,12 @@ class SourceTab extends AbstractTab {
                   Type to fuzzy-filter by route ID or endpoint URI, then press 
**Enter** to
                   navigate to the selected route.
 
-                ## Go to Node
-                - **Ctrl+G** — open an expanded popup showing routes and their 
individual
+                ## Go to Node / Line
+                - **Ctrl+G** — open a popup showing routes and their individual
                   processors/EIPs in a tree structure. Type to fuzzy-filter by 
route ID,
                   EIP type, or label, then press **Enter** to jump directly to 
the selected
-                  node in the source editor.
+                  node in the source editor. Type a **line number** (e.g. 
`47`) and press
+                  **Enter** to jump directly to that line.
 
                 ## General
                 - **Tab** — toggle focus between file list and source viewer
@@ -688,9 +698,11 @@ class SourceTab extends AbstractTab {
                         
sourceViewer.setEndpointValidator(this::validateYamlEndpoints);
                         
sourceViewer.setSimpleValidator(this::validateYamlSimple);
                         
sourceViewer.setListItemNodeChecker(this::isListChildrenNode);
+                        
sourceViewer.setEditQuickDocProvider(this::provideEditQuickDoc);
                     } else {
                         sourceViewer.setAutocompleteProvider(null);
                         sourceViewer.setAutocompleteValueProvider(null);
+                        sourceViewer.setEditQuickDocProvider(null);
                     }
                 } else if (isPropertiesFile(filePath)) {
                     
sourceViewer.setQuickDocProvider(this::providePropertiesQuickDocs);
@@ -698,10 +710,12 @@ class SourceTab extends AbstractTab {
                     
sourceViewer.setAutocompleteProvider(this::providePropertyCompletions);
                     
sourceViewer.setAutocompleteValueProvider(this::providePropertyValueCompletions);
                     
sourceViewer.setPropertiesValidator(this::validatePropertyLine);
+                    
sourceViewer.setEditQuickDocProvider(this::provideEditPropertyQuickDoc);
                 } else {
                     sourceViewer.setQuickDocProvider(null);
                     sourceViewer.setDeprecatedLineScanner(null);
                     sourceViewer.setAutocompleteProvider(null);
+                    sourceViewer.setEditQuickDocProvider(null);
                     sourceViewer.setAutocompleteValueProvider(null);
                 }
                 sourceViewer.loadFile(filePath);
@@ -783,6 +797,232 @@ class SourceTab extends AbstractTab {
         return result;
     }
 
+    private List<SourceViewer.DocEntry> provideEditQuickDoc(List<String> 
lines, int cursorRow) {
+        CamelCatalog catalog = getCatalog();
+        if (catalog == null || lines == null || cursorRow < 0 || cursorRow >= 
lines.size()) {
+            return List.of();
+        }
+        String line = lines.get(cursorRow);
+
+        Matcher uriMatcher = YAML_URI_PATTERN.matcher(line);
+        if (uriMatcher.find()) {
+            String uri = uriMatcher.group(1);
+            if (uri.endsWith("\"")) {
+                uri = uri.substring(0, uri.length() - 1);
+            }
+            String component = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+            ComponentModel model = catalog.componentModel(component);
+            if (model != null) {
+                String title = model.getTitle() != null ? model.getTitle() : 
component;
+                String desc = model.getDescription() != null ? 
model.getDescription() : "";
+                return List.of(SourceViewer.DocEntry.of(title + " — " + desc));
+            }
+        }
+
+        // check if inside a parameters: block — look up component endpoint 
option doc
+        SourceViewer.DocEntry optionDoc = resolveParameterOptionDoc(catalog, 
lines, cursorRow);
+        if (optionDoc != null) {
+            return List.of(optionDoc);
+        }
+
+        // check if this is an EIP option (e.g., message under log, expression 
under split)
+        SourceViewer.DocEntry eipOptionDoc = resolveEipOptionDoc(catalog, 
lines, cursorRow);
+        if (eipOptionDoc != null) {
+            return List.of(eipOptionDoc);
+        }
+
+        Matcher keyMatcher = YAML_KEY_PATTERN.matcher(line);
+        if (keyMatcher.find()) {
+            String key = keyMatcher.group(1);
+            EipModel eipModel = catalog.eipModel(key);
+            if (eipModel != null) {
+                String title = eipModel.getTitle() != null ? 
eipModel.getTitle() : key;
+                String desc = eipModel.getDescription() != null ? 
eipModel.getDescription() : "";
+                return List.of(SourceViewer.DocEntry.of(title + " — " + desc));
+            }
+        }
+
+        return List.of();
+    }
+
+    private SourceViewer.DocEntry resolveEipOptionDoc(CamelCatalog catalog, 
List<String> lines, int cursorRow) {
+        String cursorLine = lines.get(cursorRow);
+        String trimmed = cursorLine.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+            return null;
+        }
+        if (trimmed.startsWith("- ")) {
+            trimmed = trimmed.substring(2).trim();
+        }
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx <= 0) {
+            return null;
+        }
+        String optionName = trimmed.substring(0, colonIdx).trim();
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // walk up to find the parent EIP
+        for (int i = cursorRow - 1; i >= 0; i--) {
+            String l = lines.get(i);
+            if (l.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(l);
+            if (indent < cursorIndent) {
+                String t = l.trim();
+                if (t.startsWith("- ")) {
+                    t = t.substring(2).trim();
+                }
+                int ci = t.indexOf(':');
+                if (ci > 0) {
+                    String eipName = t.substring(0, ci).trim();
+                    EipModel model = catalog.eipModel(eipName);
+                    if (model != null) {
+                        for (BaseOptionModel opt : model.getOptions()) {
+                            if (optionName.equals(opt.getName())) {
+                                String desc = formatFullOptionDoc(opt);
+                                return desc != null
+                                        ? 
SourceViewer.DocEntry.withTitle(formatOptionTitle(opt), desc)
+                                        : null;
+                            }
+                        }
+                    }
+                }
+                break;
+            }
+        }
+        return null;
+    }
+
+    private SourceViewer.DocEntry resolveParameterOptionDoc(CamelCatalog 
catalog, List<String> lines, int cursorRow) {
+        String cursorLine = lines.get(cursorRow);
+        String trimmed = cursorLine.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#") || 
trimmed.startsWith("-")) {
+            return null;
+        }
+        int colonIdx = trimmed.indexOf(':');
+        if (colonIdx <= 0) {
+            return null;
+        }
+        String optionName = trimmed.substring(0, colonIdx).trim();
+        int cursorIndent = countLeadingSpaces(cursorLine);
+
+        // walk up to find parameters: and then the component URI
+        boolean foundParameters = false;
+        int parametersIndent = -1;
+        for (int i = cursorRow - 1; i >= 0; i--) {
+            String l = lines.get(i);
+            if (l.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(l);
+            if (indent < cursorIndent && !foundParameters) {
+                String t = l.trim();
+                if (t.startsWith("- ")) {
+                    t = t.substring(2).trim();
+                }
+                if (t.equals("parameters:")) {
+                    foundParameters = true;
+                    parametersIndent = indent;
+                    continue;
+                }
+                break;
+            }
+            if (foundParameters && indent <= parametersIndent) {
+                // look for uri: line at same or lower indent
+                String t = l.trim();
+                if (t.startsWith("- ")) {
+                    t = t.substring(2).trim();
+                }
+                Matcher m = YAML_URI_PATTERN.matcher(l);
+                if (m.find()) {
+                    String uri = m.group(1);
+                    if (uri.endsWith("\"")) {
+                        uri = uri.substring(0, uri.length() - 1);
+                    }
+                    String comp = uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : uri;
+                    ComponentModel model = catalog.componentModel(comp);
+                    if (model != null) {
+                        for (ComponentModel.EndpointOptionModel opt : 
model.getEndpointOptions()) {
+                            if (optionName.equals(opt.getName())) {
+                                String desc = formatFullOptionDoc(opt);
+                                return desc != null
+                                        ? 
SourceViewer.DocEntry.withTitle(formatOptionTitle(opt), desc)
+                                        : null;
+                            }
+                        }
+                    }
+                    break;
+                }
+                if (indent < parametersIndent) {
+                    break;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static int countLeadingSpaces(String line) {
+        int count = 0;
+        for (int i = 0; i < line.length(); i++) {
+            if (line.charAt(i) == ' ') {
+                count++;
+            } else {
+                break;
+            }
+        }
+        return count;
+    }
+
+    private List<SourceViewer.DocEntry> 
provideEditPropertyQuickDoc(List<String> lines, int cursorRow) {
+        if (lines == null || cursorRow < 0 || cursorRow >= lines.size()) {
+            return List.of();
+        }
+        String line = lines.get(cursorRow);
+        if (line == null) {
+            return List.of();
+        }
+        String trimmed = line.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#") || 
trimmed.startsWith("!")) {
+            return List.of();
+        }
+        int eq = trimmed.indexOf('=');
+        if (eq <= 0) {
+            return List.of();
+        }
+        String key = trimmed.substring(0, eq).trim();
+
+        CamelCatalog catalog = getCatalog();
+        if (catalog != null) {
+            ensureMainOptionsCache(catalog);
+            BaseOptionModel opt = lookupPropertyOption(catalog, key);
+            if (opt != null) {
+                String desc = formatFullOptionDoc(opt);
+                if (desc != null) {
+                    String title = formatOptionTitle(opt);
+                    return List.of(opt.isDeprecated()
+                            ? SourceViewer.DocEntry.deprecated(desc)
+                            : SourceViewer.DocEntry.withTitle(title, desc));
+                }
+            }
+        }
+
+        ensureSpringBootMetadataCache();
+        if (springBootMetadataCache != null) {
+            JsonObject sbProp = springBootMetadataCache.get(key);
+            if (sbProp != null) {
+                String doc = SpringBootMetadataHelper.formatDoc(sbProp);
+                if (doc != null) {
+                    boolean deprecated = 
Boolean.TRUE.equals(sbProp.get("deprecated"));
+                    return List.of(deprecated
+                            ? SourceViewer.DocEntry.deprecated(doc)
+                            : SourceViewer.DocEntry.of(doc));
+                }
+            }
+        }
+        return List.of();
+    }
+
     private static boolean isPropertiesFile(Path path) {
         return 
path.getFileName().toString().toLowerCase().endsWith(".properties");
     }
@@ -2173,16 +2413,30 @@ class SourceTab extends AbstractTab {
         return null;
     }
 
-    private static int countLeadingSpaces(String line) {
-        int count = 0;
-        for (int i = 0; i < line.length(); i++) {
-            if (line.charAt(i) == ' ') {
-                count++;
-            } else {
-                break;
-            }
+    private static String formatFullOptionDoc(BaseOptionModel opt) {
+        if (opt == null) {
+            return null;
         }
-        return count;
+        return opt.getDescription();
+    }
+
+    private static String formatOptionTitle(BaseOptionModel opt) {
+        List<String> parts = new ArrayList<>();
+        parts.add(opt.getName());
+        List<String> meta = new ArrayList<>();
+        if (opt.getType() != null) {
+            meta.add(opt.getType());
+        }
+        if (opt.isRequired()) {
+            meta.add("required");
+        }
+        if (opt.getDefaultValue() != null) {
+            meta.add("default: " + opt.getDefaultValue());
+        }
+        if (!meta.isEmpty()) {
+            parts.add("(" + String.join(", ", meta) + ")");
+        }
+        return String.join(" ", parts);
     }
 
     private BaseOptionModel lookupPropertyOption(CamelCatalog catalog, String 
key) {
@@ -2778,9 +3032,11 @@ class SourceTab extends AbstractTab {
                         
sourceViewer.setEndpointValidator(this::validateYamlEndpoints);
                         
sourceViewer.setSimpleValidator(this::validateYamlSimple);
                         
sourceViewer.setListItemNodeChecker(this::isListChildrenNode);
+                        
sourceViewer.setEditQuickDocProvider(this::provideEditQuickDoc);
                     } else {
                         sourceViewer.setAutocompleteProvider(null);
                         sourceViewer.setAutocompleteValueProvider(null);
+                        sourceViewer.setEditQuickDocProvider(null);
                     }
                 }
                 sourceViewer.loadFile(filePath);
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 817b7b377bcc..4e38a8a5a81e 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
@@ -68,13 +68,21 @@ import org.apache.camel.util.json.Jsoner;
  */
 class SourceViewer {
 
-    record DocEntry(String text, boolean deprecated) {
+    record DocEntry(String text, boolean deprecated, String title) {
+        DocEntry(String text, boolean deprecated) {
+            this(text, deprecated, null);
+        }
+
         static DocEntry of(String text) {
-            return new DocEntry(text, false);
+            return new DocEntry(text, false, null);
         }
 
         static DocEntry deprecated(String text) {
-            return new DocEntry(text, true);
+            return new DocEntry(text, true, null);
+        }
+
+        static DocEntry withTitle(String title, String text) {
+            return new DocEntry(text, false, title);
         }
     }
 
@@ -83,6 +91,11 @@ class SourceViewer {
         Map<Integer, List<DocEntry>> provideAll(List<JsonObject> codeData);
     }
 
+    @FunctionalInterface
+    interface EditQuickDocProvider {
+        List<DocEntry> provideForLine(List<String> lines, int cursorRow);
+    }
+
     @FunctionalInterface
     interface PropertiesValidator {
         String validate(String line);
@@ -131,6 +144,8 @@ class SourceViewer {
     private QuickDocProvider quickDocProvider;
     private boolean quickDocEnabled;
     private Map<Integer, List<DocEntry>> quickDocEntries = 
Collections.emptyMap();
+    private EditQuickDocProvider editQuickDocProvider;
+    private boolean editQuickDocEnabled = true;
     private DeprecatedLineScanner deprecatedLineScanner;
     private Set<Integer> deprecatedLines = Collections.emptySet();
     private Map<Integer, JumpLink> jumpLinks = Collections.emptyMap();
@@ -357,6 +372,13 @@ class SourceViewer {
         return selectedLine;
     }
 
+    int getLineCount() {
+        if (editMode) {
+            return editState.lineCount();
+        }
+        return lines != null ? lines.size() : 0;
+    }
+
     void goToLine(int lineIndex) {
         if (lineIndex >= 0 && lineIndex < lines.size()) {
             selectedLine = lineIndex;
@@ -403,6 +425,10 @@ class SourceViewer {
         this.quickDocProvider = provider;
     }
 
+    void setEditQuickDocProvider(EditQuickDocProvider provider) {
+        this.editQuickDocProvider = provider;
+    }
+
     void setDeprecatedLineScanner(DeprecatedLineScanner scanner) {
         this.deprecatedLineScanner = scanner;
     }
@@ -478,10 +504,6 @@ class SourceViewer {
             }
             return true;
         }
-        if (ke.isChar('i') && quickDocProvider != null) {
-            toggleQuickDoc();
-            return true;
-        }
         if (ke.isChar('w')) {
             wordWrap = !wordWrap;
             scrollX = 0;
@@ -696,7 +718,7 @@ class SourceViewer {
             diffScrollY = 0;
             return true;
         }
-        if (ke.isKey(KeyCode.F5) && ke.hasShift()) {
+        if (ke.hasCtrl() && ke.isCharIgnoreCase('s')) {
             saveContinueEdit();
             return true;
         }
@@ -1207,6 +1229,10 @@ class SourceViewer {
             = java.util.Set.of("steps", "uri", "parameters", "from", 
"expression", "routeConfiguration",
                     "routeTemplate", "templatedRoute", "rest", "beans");
 
+    private static final java.util.Set<String> BREADCRUMB_SKIP_KEYS
+            = java.util.Set.of("steps", "uri", "expression",
+                    "routeConfiguration", "routeTemplate", "templatedRoute", 
"rest", "beans");
+
     YamlEipContext findEnclosingEip(int fromRow) {
         String cursorLine = editState.getLine(fromRow);
         int cursorIndent = countLeadingSpaces(cursorLine);
@@ -1422,6 +1448,111 @@ class SourceViewer {
         return -1;
     }
 
+    private String buildBreadcrumb(int cursorRow) {
+        if (cursorRow < 0 || cursorRow >= editState.lineCount()) {
+            return "";
+        }
+        List<String> parts = new ArrayList<>();
+        String cursorLine = editState.getLine(cursorRow);
+        int cursorIndent = countLeadingSpaces(cursorLine);
+        if (cursorLine.isBlank()) {
+            cursorIndent = Integer.MAX_VALUE;
+        }
+
+        int prevIndent = cursorIndent;
+        for (int i = cursorRow - 1; i >= 0; i--) {
+            String line = editState.getLine(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = countLeadingSpaces(line);
+            if (indent < prevIndent) {
+                String trimmed = line.trim();
+                if (trimmed.startsWith("- ")) {
+                    trimmed = trimmed.substring(2).trim();
+                }
+                // only include structural parent keys (lines ending with ":" 
with no value)
+                if (!trimmed.endsWith(":")) {
+                    prevIndent = indent;
+                    continue;
+                }
+                String key = extractEipName(line.trim());
+                if (key != null) {
+                    if (!BREADCRUMB_SKIP_KEYS.contains(key)) {
+                        parts.add(key);
+                    }
+                    if ("route".equals(key)) {
+                        break;
+                    }
+                }
+                prevIndent = indent;
+            }
+        }
+
+        if (parts.isEmpty()) {
+            return "";
+        }
+        Collections.reverse(parts);
+        return String.join(" > ", parts);
+    }
+
+    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;
+                }
+            }
+        }
+        return reindentBlock(text, targetIndent);
+    }
+
+    static String reindentBlock(String text, int targetIndent) {
+        text = text.replace("\t", "  ");
+        String[] pasteLines = text.split("\n", -1);
+        int minIndent = Integer.MAX_VALUE;
+        for (String pl : pasteLines) {
+            if (!pl.isBlank()) {
+                minIndent = Math.min(minIndent, countLeadingSpaces(pl));
+            }
+        }
+        if (minIndent == Integer.MAX_VALUE) {
+            minIndent = 0;
+        }
+        int delta = targetIndent - minIndent;
+        if (delta == 0) {
+            return text;
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < pasteLines.length; i++) {
+            if (i > 0) {
+                sb.append('\n');
+            }
+            String pl = pasteLines[i];
+            if (pl.isBlank()) {
+                sb.append(pl);
+            } else if (delta > 0) {
+                sb.append(" ".repeat(delta)).append(pl);
+            } else {
+                int strip = Math.min(-delta, countLeadingSpaces(pl));
+                sb.append(pl.substring(strip));
+            }
+        }
+        return sb.toString();
+    }
+
     private static int countLeadingSpaces(String line) {
         int count = 0;
         for (int i = 0; i < line.length(); i++) {
@@ -2070,13 +2201,30 @@ class SourceViewer {
             return;
         }
 
-        int visibleLines = inner.height();
+        // quick doc panel at the bottom (same as edit mode)
+        Rect contentArea = inner;
+        Rect viewDocArea = null;
+        List<DocEntry> viewDocEntries = null;
+        int docPanelHeight = 4;
+        if (editQuickDocEnabled && editQuickDocProvider != null && 
inner.height() > 10) {
+            contentArea = new Rect(inner.left(), inner.top(), inner.width(), 
inner.height() - docPanelHeight);
+            viewDocArea = new Rect(inner.left(), inner.top() + inner.height() 
- docPanelHeight, inner.width(), docPanelHeight);
+            if (selectedLine >= 0 && selectedLine < codeData.size()) {
+                List<String> rawLines = new ArrayList<>(codeData.size());
+                for (JsonObject jo : codeData) {
+                    rawLines.add(jo.getString("code") != null ? 
jo.getString("code") : "");
+                }
+                viewDocEntries = editQuickDocProvider.provideForLine(rawLines, 
selectedLine);
+            }
+        }
+
+        int visibleLines = contentArea.height();
 
         // Reserve bottom row for horizontal scrollbar when content is wider 
than viewport
         if (!wordWrap) {
             int cursorWidth = 3;
             int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
-            if (maxLineWidth > inner.width()) {
+            if (maxLineWidth > contentArea.width()) {
                 visibleLines = Math.max(1, visibleLines - 1);
             }
         }
@@ -2089,13 +2237,13 @@ class SourceViewer {
             pendingScroll = false;
         }
 
-        int contentWidth = inner.width() - 1;
+        int contentWidth = contentArea.width() - 1;
 
-        // Auto-scroll to keep selected line visible (accounting for word wrap 
and inline doc lines)
+        // Auto-scroll to keep selected line visible
         if (selectedLine >= 0) {
             if (selectedLine < scrollY) {
                 scrollY = selectedLine;
-            } else if (wordWrap || (quickDocEnabled && 
!quickDocEntries.isEmpty())) {
+            } else if (wordWrap) {
                 while (scrollY < selectedLine
                         && countVisualRows(scrollY, selectedLine + 1, 
contentWidth) > visibleLines) {
                     scrollY++;
@@ -2106,17 +2254,11 @@ class SourceViewer {
         }
 
         int maxScroll;
-        if (wordWrap || (quickDocEnabled && !quickDocEntries.isEmpty())) {
+        if (wordWrap) {
             maxScroll = 0;
             int visualFromEnd = 0;
             for (int i = lines.size() - 1; i >= 0; i--) {
                 visualFromEnd += wrapRowCount(lines.get(i), contentWidth);
-                if (quickDocEnabled) {
-                    List<DocEntry> docs = quickDocEntries.get(i);
-                    if (docs != null) {
-                        visualFromEnd += docs.size();
-                    }
-                }
                 if (visualFromEnd >= visibleLines) {
                     maxScroll = i;
                     break;
@@ -2131,74 +2273,78 @@ class SourceViewer {
         if (!wordWrap) {
             int cursorWidth = 3;
             int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
-            int maxHScroll = Math.max(0, maxLineWidth - inner.width());
+            int maxHScroll = Math.max(0, maxLineWidth - contentArea.width());
             scrollX = Math.min(scrollX, maxHScroll);
         }
 
         int currentMatchLine = search.currentMatchLine();
 
-        int gutterWidth = quickDocEnabled && !quickDocEntries.isEmpty() ? 
computeGutterWidth() : 0;
-
         List<Line> visible = new ArrayList<>();
         for (int i = scrollY; i < lines.size() && visible.size() < 
visibleLines; i++) {
             String raw = lines.get(i);
             boolean isSelected = (i == selectedLine);
-            Line line = highlightSourceLine(raw, i, hSkip, isSelected, 
inner.width());
+            Line line = highlightSourceLine(raw, i, hSkip, isSelected, 
contentArea.width());
             line = search.applyHighlights(line, i, currentMatchLine);
             visible.add(line);
-
-            List<DocEntry> docLines = quickDocEnabled ? quickDocEntries.get(i) 
: null;
-            if (docLines != null) {
-                String code = i < codeData.size() && 
codeData.get(i).get("code") != null
-                        ? codeData.get(i).get("code").toString()
-                        : "";
-                int si = 0;
-                while (si < code.length() && code.charAt(si) == ' ') {
-                    si++;
-                }
-                for (DocEntry docEntry : docLines) {
-                    for (Line docLine : renderQuickDocLines(docEntry, si, 
gutterWidth, inner.width())) {
-                        if (visible.size() >= visibleLines) {
-                            break;
-                        }
-                        if (hSkip > 0) {
-                            docLine = applyHorizontalSkip(docLine, hSkip);
-                        }
-                        visible.add(docLine);
-                    }
-                }
-            }
         }
 
         List<Rect> hChunks = Layout.horizontal()
                 .constraints(Constraint.fill(), Constraint.length(1))
-                .split(inner);
+                .split(contentArea);
 
         Overflow overflow = wordWrap ? Overflow.WRAP_WORD : Overflow.CLIP;
         
frame.renderWidget(Paragraph.builder().text(Text.from(visible)).overflow(overflow).build(),
 hChunks.get(0));
 
         if (plainMode && selectedLine >= scrollY && selectedLine < scrollY + 
visibleLines) {
             int relRow = selectedLine - scrollY;
-            int screenY = inner.top() + relRow;
-            Rect lineRect = new Rect(inner.left(), screenY, inner.width(), 1);
+            int screenY = contentArea.top() + relRow;
+            Rect lineRect = new Rect(contentArea.left(), screenY, 
contentArea.width(), 1);
             Style selBg = focused ? Theme.selectionBg() : 
Theme.selectionBg().dim();
             frame.buffer().setStyle(lineRect, selBg);
         }
 
-        int totalDocLines = quickDocEnabled ? 
quickDocEntries.values().stream().mapToInt(List::size).sum() : 0;
-        int totalContentLines = lines.size() + totalDocLines;
-        if (totalContentLines > visibleLines) {
-            
vScrollState.contentLength(totalContentLines).viewportContentLength(visibleLines).position(scrollY);
+        if (lines.size() > visibleLines) {
+            
vScrollState.contentLength(lines.size()).viewportContentLength(visibleLines).position(scrollY);
             frame.renderStatefulWidget(Scrollbar.builder().build(), 
hChunks.get(1), vScrollState);
         }
         if (!wordWrap) {
             int cursorWidth = 3;
             int maxLineWidth = 
lines.stream().mapToInt(String::length).max().orElse(0) + cursorWidth;
-            int maxHScroll = Math.max(0, maxLineWidth - inner.width());
+            int maxHScroll = Math.max(0, maxLineWidth - contentArea.width());
             if (maxHScroll > 0) {
-                
hScrollState.contentLength(maxLineWidth).viewportContentLength(inner.width()).position(scrollX);
-                frame.renderStatefulWidget(Scrollbar.horizontal(), inner, 
hScrollState);
+                
hScrollState.contentLength(maxLineWidth).viewportContentLength(contentArea.width()).position(scrollX);
+                frame.renderStatefulWidget(Scrollbar.horizontal(), 
contentArea, hScrollState);
+            }
+        }
+
+        // quick doc panel at the bottom
+        if (viewDocArea != null) {
+            List<Line> docLines = new ArrayList<>();
+            String titleText = null;
+            if (viewDocEntries != null && !viewDocEntries.isEmpty()) {
+                titleText = viewDocEntries.get(0).title();
+            }
+            if (titleText != null) {
+                String prefix = "─── ";
+                String suffix = " ";
+                int remaining = Math.max(0, viewDocArea.width() - 
prefix.length() - titleText.length() - suffix.length());
+                docLines.add(Line.from(
+                        Span.styled(prefix, Style.EMPTY.dim()),
+                        Span.styled(titleText, Style.EMPTY.dim().bold()),
+                        Span.styled(suffix + "─".repeat(remaining), 
Style.EMPTY.dim())));
+            } else {
+                docLines.add(Line.from(Span.styled("─".repeat(Math.max(1, 
viewDocArea.width())), Style.EMPTY.dim())));
+            }
+            if (viewDocEntries != null && !viewDocEntries.isEmpty()) {
+                for (int d = 0; d < viewDocEntries.size() && d < 
viewDocArea.height() - 1; d++) {
+                    DocEntry entry = viewDocEntries.get(d);
+                    Style docStyle = entry.deprecated() ? 
Style.EMPTY.dim().italic() : Style.EMPTY.dim();
+                    docLines.add(Line.from(Span.styled(entry.text(), 
docStyle)));
+                }
             }
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(docLines)).overflow(Overflow.WRAP_WORD).build(),
+                    viewDocArea);
         }
     }
 
@@ -2210,6 +2356,12 @@ class SourceViewer {
             titleSpans.add(Span.styled(" Diff [" + info + "] ", ts));
         } else {
             titleSpans.add(Span.styled(" Edit [" + info + (dirty ? " *" : "") 
+ "] ", ts));
+            if (isCamelYamlFile()) {
+                String breadcrumb = buildBreadcrumb(editState.cursorRow());
+                if (!breadcrumb.isEmpty()) {
+                    titleSpans.add(Span.styled(" " + breadcrumb + " ", 
Style.EMPTY.dim().italic()));
+                }
+            }
         }
         Title posTitle;
         if (diffOverlay) {
@@ -2245,18 +2397,30 @@ class SourceViewer {
             return;
         }
 
+        // split inner area for quick doc panel at the bottom (fixed height to 
avoid flicker)
+        List<DocEntry> editDocEntries = null;
+        Rect editorArea = inner;
+        Rect docArea = null;
+        int docPanelHeight = 4;
+        if (editQuickDocEnabled && editQuickDocProvider != null && 
inner.height() > 10) {
+            editorArea = new Rect(inner.left(), inner.top(), inner.width(), 
inner.height() - docPanelHeight);
+            docArea = new Rect(inner.left(), inner.top() + inner.height() - 
docPanelHeight, inner.width(), docPanelHeight);
+            lastVisibleLines = Math.max(1, editorArea.height());
+            editDocEntries = editQuickDocProvider.provideForLine(editLines(), 
editState.cursorRow());
+        }
+
         TextArea textArea = TextArea.builder()
                 .cursorStyle(Style.EMPTY.reversed())
                 .showLineNumbers(!plainMode)
                 .lineNumberStyle(Style.EMPTY.dim())
                 .build();
-        textArea.renderWithCursor(inner, frame.buffer(), editState, frame);
+        textArea.renderWithCursor(editorArea, frame.buffer(), editState, 
frame);
 
         // cursor line highlight
         int cursorRelRow = editState.cursorRow() - editState.scrollRow();
-        if (cursorRelRow >= 0 && cursorRelRow < inner.height()) {
-            int screenY = inner.top() + cursorRelRow;
-            Rect lineRect = new Rect(inner.left(), screenY, inner.width(), 1);
+        if (cursorRelRow >= 0 && cursorRelRow < editorArea.height()) {
+            int screenY = editorArea.top() + cursorRelRow;
+            Rect lineRect = new Rect(editorArea.left(), screenY, 
editorArea.width(), 1);
             frame.buffer().setStyle(lineRect, Style.EMPTY.bg(Theme.zebra()));
         }
 
@@ -2265,9 +2429,9 @@ class SourceViewer {
             int scopeRow = findScopeLineRow(editState.cursorRow());
             if (scopeRow >= 0 && scopeRow != editState.cursorRow()) {
                 int relativeRow = scopeRow - editState.scrollRow();
-                if (relativeRow >= 0 && relativeRow < inner.height()) {
-                    int screenY = inner.top() + relativeRow;
-                    Rect lineRect = new Rect(inner.left(), screenY, 
inner.width(), 1);
+                if (relativeRow >= 0 && relativeRow < editorArea.height()) {
+                    int screenY = editorArea.top() + relativeRow;
+                    Rect lineRect = new Rect(editorArea.left(), screenY, 
editorArea.width(), 1);
                     frame.buffer().setStyle(lineRect, 
Style.EMPTY.bold().fg(Theme.accent()));
                 }
             }
@@ -2280,15 +2444,15 @@ class SourceViewer {
                 lineStatuses = EditDiff.diff(orig, editLines());
             }
             int gutterWidth = Math.max(2, 
String.valueOf(editState.lineCount()).length()) + 2;
-            for (int r = 0; r < inner.height(); r++) {
+            for (int r = 0; r < editorArea.height(); r++) {
                 int lineIdx = editState.scrollRow() + r;
                 if (lineIdx >= 0 && lineIdx < lineStatuses.length) {
                     EditDiff.LineStatus status = lineStatuses[lineIdx];
                     if (status != EditDiff.LineStatus.UNCHANGED) {
                         Style bg = 
Style.EMPTY.fg(dev.tamboui.style.Color.WHITE)
                                 .bg(dev.tamboui.style.Color.rgb(0x1B, 0x4D, 
0x1B));
-                        int screenY = inner.top() + r;
-                        for (int x = inner.left(); x < inner.left() + 
gutterWidth; x++) {
+                        int screenY = editorArea.top() + r;
+                        for (int x = editorArea.left(); x < editorArea.left() 
+ gutterWidth; x++) {
                             dev.tamboui.buffer.Cell cell = 
frame.buffer().get(x, screenY);
                             frame.buffer().set(x, screenY,
                                     new dev.tamboui.buffer.Cell(cell.symbol(), 
bg));
@@ -2298,10 +2462,40 @@ class SourceViewer {
             }
         }
 
+        // quick doc panel at the bottom of the editor (fixed height)
+        if (docArea != null) {
+            List<Line> docLines = new ArrayList<>();
+            String titleText = null;
+            if (editDocEntries != null && !editDocEntries.isEmpty()) {
+                titleText = editDocEntries.get(0).title();
+            }
+            if (titleText != null) {
+                String prefix = "─── ";
+                String suffix = " ";
+                int remaining = Math.max(0, docArea.width() - prefix.length() 
- titleText.length() - suffix.length());
+                docLines.add(Line.from(
+                        Span.styled(prefix, Style.EMPTY.dim()),
+                        Span.styled(titleText, Style.EMPTY.dim().bold()),
+                        Span.styled(suffix + "─".repeat(remaining), 
Style.EMPTY.dim())));
+            } else {
+                docLines.add(Line.from(Span.styled("─".repeat(Math.max(1, 
docArea.width())), Style.EMPTY.dim())));
+            }
+            if (editDocEntries != null && !editDocEntries.isEmpty()) {
+                for (int d = 0; d < editDocEntries.size() && d < 
docArea.height() - 1; d++) {
+                    DocEntry entry = editDocEntries.get(d);
+                    Style docStyle = entry.deprecated() ? 
Style.EMPTY.dim().italic() : Style.EMPTY.dim();
+                    docLines.add(Line.from(Span.styled(entry.text(), 
docStyle)));
+                }
+            }
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(docLines)).overflow(Overflow.WRAP_WORD).build(),
+                    docArea);
+        }
+
         if (autocompletePopup != null) {
             int cursorRow = editState.cursorRow() - editState.scrollRow();
             int cursorCol = editState.cursorCol() - editState.scrollCol();
-            autocompletePopup.render(frame, inner, cursorRow, cursorCol);
+            autocompletePopup.render(frame, editorArea, cursorRow, cursorCol);
         }
 
         if (validationErrors != null) {
@@ -2474,8 +2668,8 @@ class SourceViewer {
         }
         if (editMode) {
             TuiHelper.hint(spans, "Esc", "cancel");
+            TuiHelper.hint(spans, "Ctrl+S", "save");
             TuiHelper.hint(spans, "F5", "save & close");
-            TuiHelper.hint(spans, "Shift+F5", "save");
             if (dirty) {
                 TuiHelper.hint(spans, "F7", "diff");
             }
@@ -2512,9 +2706,6 @@ class SourceViewer {
         if (isEditable()) {
             TuiHelper.hint(spans, "F4", "edit");
         }
-        if (quickDocProvider != null) {
-            TuiHelper.hint(spans, "i", "quick doc" + (quickDocEnabled ? " 
[on]" : ""));
-        }
         TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
         if (isMarkdownFile || currentRouteId != null) {
             TuiHelper.hint(spans, "Space", "format");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopupTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopupTest.java
index 4eec2f5fe2b9..22ea0b70db5c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopupTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/GotoSourceNodePopupTest.java
@@ -30,7 +30,7 @@ class GotoSourceNodePopupTest {
     @Test
     void escClosesPopup() {
         var popup = new GotoSourceNodePopup();
-        popup.open(sampleEntries());
+        popup.open(sampleEntries(), 100);
         assertThat(popup.isVisible()).isTrue();
 
         popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ESCAPE, 
KeyModifiers.NONE));
@@ -40,7 +40,7 @@ class GotoSourceNodePopupTest {
     @Test
     void enterSelectsRouteEntry() {
         var popup = new GotoSourceNodePopup();
-        popup.open(sampleEntries());
+        popup.open(sampleEntries(), 100);
 
         popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
 
@@ -53,7 +53,7 @@ class GotoSourceNodePopupTest {
     @Test
     void downThenEnterSelectsProcessor() {
         var popup = new GotoSourceNodePopup();
-        popup.open(sampleEntries());
+        popup.open(sampleEntries(), 100);
 
         popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE));
         popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
@@ -67,7 +67,7 @@ class GotoSourceNodePopupTest {
     @Test
     void typingFiltersToMatchingProcessor() {
         var popup = new GotoSourceNodePopup();
-        popup.open(sampleEntries());
+        popup.open(sampleEntries(), 100);
 
         popup.handleKeyEvent(KeyEvent.ofChar('k', KeyModifiers.NONE));
         popup.handleKeyEvent(KeyEvent.ofChar('a', KeyModifiers.NONE));
@@ -104,7 +104,7 @@ class GotoSourceNodePopupTest {
                         "/tmp/routes.yaml", 8, 1, 5));
 
         var popup = new GotoSourceNodePopup();
-        popup.open(entries);
+        popup.open(entries, 100);
 
         popup.handleKeyEvent(KeyEvent.ofChar('a', KeyModifiers.NONE));
         popup.handleKeyEvent(KeyEvent.ofChar('l', KeyModifiers.NONE));
@@ -120,6 +120,69 @@ class GotoSourceNodePopupTest {
         assertThat(selected.routeFromLine()).isEqualTo(0);
     }
 
+    @Test
+    void typingLineNumberJumpsToLine() {
+        var popup = new GotoSourceNodePopup();
+        popup.open(sampleEntries(), 100);
+
+        popup.handleKeyEvent(KeyEvent.ofChar('4', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofChar('7', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.isVisible()).isFalse();
+        assertThat(popup.consumeSelection()).isNull();
+        assertThat(popup.consumeGotoLineNumber()).isEqualTo(47);
+    }
+
+    @Test
+    void lineNumberClampedToFileSize() {
+        var popup = new GotoSourceNodePopup();
+        popup.open(sampleEntries(), 30);
+
+        popup.handleKeyEvent(KeyEvent.ofChar('9', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofChar('9', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofChar('9', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeGotoLineNumber()).isEqualTo(30);
+    }
+
+    @Test
+    void lineNumberZeroClampedToOne() {
+        var popup = new GotoSourceNodePopup();
+        popup.open(sampleEntries(), 100);
+
+        popup.handleKeyEvent(KeyEvent.ofChar('0', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeGotoLineNumber()).isEqualTo(1);
+    }
+
+    @Test
+    void mixedTextNotTreatedAsLineNumber() {
+        var popup = new GotoSourceNodePopup();
+        popup.open(sampleEntries(), 100);
+
+        popup.handleKeyEvent(KeyEvent.ofChar('4', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofChar('a', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.consumeGotoLineNumber()).isEqualTo(-1);
+    }
+
+    @Test
+    void gotoLineWorksWithEmptyNodeList() {
+        var popup = new GotoSourceNodePopup();
+        popup.open(List.of(), 50);
+
+        popup.handleKeyEvent(KeyEvent.ofChar('2', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofChar('5', KeyModifiers.NONE));
+        popup.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(popup.isVisible()).isFalse();
+        assertThat(popup.consumeGotoLineNumber()).isEqualTo(25);
+    }
+
     private static List<YamlRouteNodeScanner.NodeEntry> sampleEntries() {
         return List.of(
                 new YamlRouteNodeScanner.NodeEntry(
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
new file mode 100644
index 000000000000..8ba71db33bc1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerPasteIndentTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SourceViewerPasteIndentTest {
+
+    @Test
+    void noIndentPastedAtIndent8() {
+        String paste = "- log:\n    message: Hello\n- to:\n    uri: 
kafka:orders";
+        String result = SourceViewer.reindentBlock(paste, 8);
+        assertThat(result).isEqualTo(
+                "        - log:\n            message: Hello\n        - to:\n   
         uri: kafka:orders");
+    }
+
+    @Test
+    void alreadyCorrectIndent() {
+        String paste = "    - log:\n        message: Hello";
+        String result = SourceViewer.reindentBlock(paste, 4);
+        assertThat(result).isEqualTo(paste);
+    }
+
+    @Test
+    void reduceIndent() {
+        String paste = "        - log:\n            message: Hello";
+        String result = SourceViewer.reindentBlock(paste, 4);
+        assertThat(result).isEqualTo("    - log:\n        message: Hello");
+    }
+
+    @Test
+    void preservesRelativeIndentation() {
+        String paste = "- split:\n    expression:\n      simple: ${body}\n    
steps:\n      - log:\n          message: part";
+        String result = SourceViewer.reindentBlock(paste, 6);
+        assertThat(result).isEqualTo(
+                "      - split:\n          expression:\n            simple: 
${body}\n          steps:\n            - log:\n                message: part");
+    }
+
+    @Test
+    void blankLinesPreserved() {
+        String paste = "- log:\n    message: Hello\n\n- to:\n    uri: 
direct:foo";
+        String result = SourceViewer.reindentBlock(paste, 4);
+        assertThat(result).isEqualTo(
+                "    - log:\n        message: Hello\n\n    - to:\n        uri: 
direct:foo");
+    }
+
+    @Test
+    void singleLineNoChange() {
+        String paste = "message: Hello";
+        String result = SourceViewer.reindentBlock(paste, 0);
+        assertThat(result).isEqualTo("message: Hello");
+    }
+
+    @Test
+    void singleLineIndented() {
+        String paste = "message: Hello";
+        String result = SourceViewer.reindentBlock(paste, 6);
+        assertThat(result).isEqualTo("      message: Hello");
+    }
+
+    @Test
+    void pasteWithExistingIndentShiftedUp() {
+        String paste = "            brokers: localhost:9092\n            
groupId: my-group";
+        String result = SourceViewer.reindentBlock(paste, 8);
+        assertThat(result).isEqualTo("        brokers: localhost:9092\n        
groupId: my-group");
+    }
+
+    @Test
+    void zeroTargetStripsIndent() {
+        String paste = "    - log:\n        message: Hello";
+        String result = SourceViewer.reindentBlock(paste, 0);
+        assertThat(result).isEqualTo("- log:\n    message: Hello");
+    }
+
+    @Test
+    void trailingNewlinePreserved() {
+        String paste = "- log:\n    message: Hello\n";
+        String result = SourceViewer.reindentBlock(paste, 4);
+        assertThat(result).isEqualTo("    - log:\n        message: Hello\n");
+    }
+}

Reply via email to