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 c31e0cf696ce CAMEL-24617: TUI Source tab - add YAML DSL refactoring 
menu (Ctrl+R)
c31e0cf696ce is described below

commit c31e0cf696ce5e26a49deda737438b54ca214b78
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 3 17:13:56 2026 +0200

    CAMEL-24617: TUI Source tab - add YAML DSL refactoring menu (Ctrl+R)
    
    Adds a Ctrl+R refactoring popup to the Source tab's YAML editor with two
    initial actions:
    
    - Replace URI: replaces the component URI on the current line (inline or
      block form); strips query parameters from the old URI and removes the
      accompanying parameters: block if present.
    - Extract to property: replaces a literal value with a quoted
      {{key}} placeholder and appends key=value to application.properties.
    
    CAMEL-24617: TUI refactor - add Extract to new file action
    
    Adds the third refactoring action "Extract to new file" to the Ctrl+R
    menu in the Source tab YAML editor. When the cursor is on any EIP step
    (or any line within its block), the action:
    
    - Identifies the step block using YamlBlockEditor.findBlock
    - Replaces it in the original file with "- to: direct:<name>"
    - Creates a new YAML file with a standalone route:
        from: direct:<name> + the extracted steps
    
    The action is filtered out for "- route:" and "- from:" structural
    elements that are not extractable steps.
    
    CAMEL-24617: TUI refactor extract-to-file - fix file list refresh and naming
    
    - New file uses .camel.yaml extension (Camel YAML DSL convention)
    - File list panel refreshes immediately after extraction via onFileCreated 
callback
    - Route name is sanitized: spaces and illegal chars replaced with hyphens,
      consecutive hyphens collapsed, leading/trailing hyphens stripped
    
    CAMEL-24617: TUI refactor extract-to-file - append route if file already 
exists
    
    If the target .camel.yaml file already exists, append the extracted
    route as an additional - route: block (separated by a blank line)
    instead of failing. Camel YAML files support multiple routes per file.
    
    The file-list refresh callback is only fired when a new file is created,
    not on append.
    
    CAMEL-24617: TUI refactor extract-to-file - use canonical block-form 
replacement
    
    Replace the extracted block with the normalized YAML DSL notation:
    
      - to:
          uri: direct:<name>
    
    instead of the inline shorthand "- to: direct:<name>".
    
    CAMEL-24617: TUI refactor extract-to-file - fix jump links after extraction
    
    Two fixes for forward/reverse jump links becoming available immediately:
    
    1. Auto-save the original file as part of extraction so both files are on
       disk when the route index is rebuilt. Without this, the original file's
       "- to: direct:<name>" block was never scanned (file was dirty) and the
       new file's reverse jump link could not be resolved.
    
    2. After every route index rebuild in loadDirectory (triggered by
       refreshFiles/onFileCreated), recompute jump links for the currently
       viewed file so forward and reverse links appear without reopening it.
    
    CAMEL-24617: TUI - fix jump links cleared on file load after save
    
    loadFile() unconditionally cleared jumpLinks. After saveEdit() calls
    loadFile() to reload the file into view mode, the jump links computed
    during refreshFiles() were wiped, requiring a manual file switch to
    restore them.
    
    Fix: add an onFileLoaded(Path) callback fired at the end of loadFile()
    (after jumpLinks is cleared). SourceTab wires it to computeJumpLinks()
    so jump links are immediately recomputed for every file load, including
    after save+reload.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/RefactorPopup.java | 293 ++++++++++++++++
 .../dsl/jbang/core/commands/tui/SourceTab.java     |  18 +-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  | 380 ++++++++++++++++++++-
 .../commands/tui/SourceViewerRefactorTest.java     | 346 +++++++++++++++++++
 4 files changed, 1035 insertions(+), 2 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
new file mode 100644
index 000000000000..d9355a2d1976
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RefactorPopup.java
@@ -0,0 +1,293 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Padding;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.input.TextInput;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+
+/**
+ * Refactoring menu for the Source tab YAML editor (opened with F5 or Ctrl+R 
in view mode). Presents applicable
+ * refactoring actions for the current cursor line and drives the name-entry 
prompt itself, emitting a single
+ * {@link Request} for the host viewer to execute.
+ */
+class RefactorPopup {
+
+    enum Action {
+        EXTRACT_TO_FILE,
+        REPLACE_URI,
+        EXTRACT_TO_PROPERTY
+    }
+
+    /** A completed, ready-to-execute request. */
+    record Request(Action action, String value) {
+    }
+
+    private record MenuItem(Action action, String icon, String label, String 
inputTitle, String inputPlaceholder,
+            String prefilledValue) {
+    }
+
+    private enum Phase {
+        MENU,
+        INPUT
+    }
+
+    private boolean visible;
+    private Phase phase = Phase.MENU;
+
+    private List<MenuItem> items = List.of();
+    private final ListState menuState = new ListState();
+
+    private String inputTitle;
+    private String inputPlaceholder;
+    private TextInputState inputState;
+    private Action inputAction;
+
+    private Request result;
+
+    void open(List<Action> applicable, String currentUri) {
+        this.visible = true;
+        this.phase = Phase.MENU;
+        this.result = null;
+        this.inputState = null;
+        buildMenu(applicable, currentUri);
+        menuState.select(items.isEmpty() ? null : 0);
+    }
+
+    void close() {
+        visible = false;
+        phase = Phase.MENU;
+        inputState = null;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    Request consumeResult() {
+        Request r = result;
+        result = null;
+        return r;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        return switch (phase) {
+            case MENU -> handleMenuKey(ke);
+            case INPUT -> handleInputKey(ke);
+        };
+    }
+
+    private void buildMenu(List<Action> applicable, String currentUri) {
+        List<MenuItem> list = new ArrayList<>();
+        for (Action action : applicable) {
+            list.add(switch (action) {
+                case EXTRACT_TO_FILE -> new MenuItem(
+                        Action.EXTRACT_TO_FILE, "📄", "Extract to new file…",
+                        "Route name", "my-sub-route", "");
+                case REPLACE_URI -> new MenuItem(
+                        Action.REPLACE_URI, "🔀", "Replace URI…",
+                        "New URI", "component:path", currentUri != null ? 
currentUri : "");
+                case EXTRACT_TO_PROPERTY -> new MenuItem(
+                        Action.EXTRACT_TO_PROPERTY, "📦", "Extract to 
property…",
+                        "Property key", "my.property.key", "");
+            });
+        }
+        this.items = list;
+    }
+
+    private boolean handleMenuKey(KeyEvent ke) {
+        if (ke.isCancel()) {
+            close();
+            return true;
+        }
+        if (ke.isUp()) {
+            menuState.selectPrevious();
+            return true;
+        }
+        if (ke.isDown()) {
+            menuState.selectNext(items.size());
+            return true;
+        }
+        if (ke.isConfirm()) {
+            Integer sel = menuState.selected();
+            if (sel != null && sel < items.size()) {
+                startInput(items.get(sel));
+            }
+            return true;
+        }
+        return true;
+    }
+
+    private void startInput(MenuItem item) {
+        this.inputAction = item.action();
+        this.inputTitle = item.inputTitle();
+        this.inputPlaceholder = item.inputPlaceholder();
+        this.inputState = new TextInputState(item.prefilledValue() != null ? 
item.prefilledValue() : "");
+        this.inputState.moveCursorToEnd();
+        this.phase = Phase.INPUT;
+    }
+
+    private boolean handleInputKey(KeyEvent ke) {
+        if (ke.isCancel()) {
+            phase = Phase.MENU;
+            inputState = null;
+            return true;
+        }
+        if (ke.isConfirm()) {
+            String text = inputState.text().trim();
+            if (!text.isEmpty()) {
+                result = new Request(inputAction, text);
+                close();
+            }
+            return true;
+        }
+        if (ke.isDeleteBackward()) {
+            inputState.deleteBackward();
+        } else if (ke.isDeleteForward()) {
+            inputState.deleteForward();
+        } else if (ke.isLeft()) {
+            inputState.moveCursorLeft();
+        } else if (ke.isRight()) {
+            inputState.moveCursorRight();
+        } else if (ke.isHome()) {
+            inputState.moveCursorToStart();
+        } else if (ke.isEnd()) {
+            inputState.moveCursorToEnd();
+        } else if (ke.code() == KeyCode.CHAR) {
+            char ch = ke.string().charAt(0);
+            if (ch >= 0x20 && ch != 0x7F) {
+                inputState.insert(ch);
+            }
+        }
+        return true;
+    }
+
+    void render(Frame frame, Rect area) {
+        if (!visible) {
+            return;
+        }
+        switch (phase) {
+            case MENU -> renderMenu(frame, area);
+            case INPUT -> renderInput(frame, area);
+            default -> {
+            }
+        }
+    }
+
+    private void renderMenu(Frame frame, Rect area) {
+        int popupW = Math.max(34, Math.min(48, area.width() - 4));
+        popupW = Math.min(popupW, area.width() - 2);
+        int popupH = items.size() + 4;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
+        Rect popup = new Rect(x, y, popupW, Math.min(popupH, area.height() - 
2));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        List<ListItem> listItems = new ArrayList<>();
+        for (MenuItem item : items) {
+            List<Span> spans = new ArrayList<>();
+            spans.add(Span.raw("  "));
+            spans.add(Span.raw(item.icon()));
+            spans.add(Span.raw("  "));
+            spans.add(Span.raw(item.label()));
+            listItems.add(ListItem.from(Line.from(spans)));
+        }
+
+        ListWidget list = ListWidget.builder()
+                .items(listItems.toArray(ListItem[]::new))
+                .highlightStyle(Theme.selectionBg())
+                .highlightSymbol("")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                        .borderStyle(Theme.borderFocused())
+                        .padding(Padding.vertical(1))
+                        .title(Title.from(Line.from(
+                                Span.styled(" 🔧 Refactor ", 
Theme.title().bold()))))
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, popup, menuState);
+    }
+
+    private void renderInput(Frame frame, Rect area) {
+        int popupW = Math.max(50, Math.min(64, area.width() - 4));
+        popupW = Math.min(popupW, area.width() - 2);
+        int popupH = 5;
+        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
+        int y = area.top() + Math.max(0, (area.height() - popupH) / 3);
+        Rect popup = new Rect(x, y, popupW, Math.min(popupH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, popup);
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED).borders(Borders.ALL)
+                .borderStyle(Theme.borderFocused())
+                .title(Title.from(Line.from(Span.styled(" " + inputTitle + " 
", Theme.title().bold()))))
+                .build();
+        frame.renderWidget(block, popup);
+        Rect inner = block.inner(popup);
+
+        int pad = 2;
+        int fieldW = Math.max(1, inner.width() - 2 * pad);
+        int fieldY = inner.top() + Math.max(0, (inner.height() - 1) / 2);
+        Rect field = new Rect(inner.left() + pad, fieldY, fieldW, 1);
+
+        TextInput textInput = TextInput.builder()
+                .cursorStyle(Style.EMPTY.reversed())
+                .placeholder(inputPlaceholder)
+                .build();
+        textInput.renderWithCursor(field, frame.buffer(), inputState, frame);
+    }
+
+    void renderFooter(List<Span> spans) {
+        if (!visible) {
+            return;
+        }
+        switch (phase) {
+            case MENU -> {
+                TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "navigate");
+                TuiHelper.hint(spans, "Enter", "select");
+                TuiHelper.hintLast(spans, "Esc", "close");
+            }
+            case INPUT -> {
+                TuiHelper.hint(spans, "Enter", "confirm");
+                TuiHelper.hintLast(spans, "Esc", "back");
+            }
+            default -> {
+            }
+        }
+    }
+}
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 d17c3557a428..dfecbff2fb6a 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
@@ -147,6 +147,12 @@ class SourceTab extends AbstractTab {
                 ctx.notificationCallback.accept(msg, error);
             }
         });
+        sourceViewer.setOnFileCreated(this::refreshFiles);
+        sourceViewer.setOnFileLoaded(p -> {
+            if (isCamelSourceFile(p)) {
+                sourceViewer.setJumpLinks(computeJumpLinks(p));
+            }
+        });
         sourceViewer.setValidateOnSave(ctx.validateOnSave);
         sourceViewer.setOnJumpLink(this::handleJumpLink);
     }
@@ -474,7 +480,8 @@ class SourceTab extends AbstractTab {
                 - **F4** — edit local file (plain text; only when file is 
writable)
                 - **Esc** — cancel edit (in edit mode) or close viewer
                 - **Ctrl+S** — save file and continue editing (Camel dev mode 
auto-reloads)
-                - **F5** — save file and close editor
+                - **F5** — save file and close editor (in edit mode)
+                - **Ctrl+R** — open refactoring menu in edit mode (YAML files 
only; choose an action for the current line)
                 - **Space** — cycle format (YAML/Java/XML) for Camel routes
                 - Quick documentation panel is shown at the bottom for Camel 
source files
                 - **/** — search in source
@@ -679,6 +686,15 @@ class SourceTab extends AbstractTab {
         listState.select(sel);
         currentDir = dir;
         buildRouteIndex();
+        // Recompute jump links for the currently viewed file so 
forward/reverse links
+        // are immediately usable after the route index is rebuilt (e.g. after 
extraction).
+        String viewedPath = sourceViewer.getCurrentFilePath();
+        if (viewedPath != null) {
+            Path viewedFile = Path.of(viewedPath);
+            if (isCamelSourceFile(viewedFile)) {
+                sourceViewer.setJumpLinks(computeJumpLinks(viewedFile));
+            }
+        }
         return true;
     }
 
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 b92554b5e451..87fdee6d8b01 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
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -169,10 +170,13 @@ class SourceViewer {
     private boolean diffOverlay;
     private int diffScrollY;
     private BiConsumer<String, Boolean> notificationCallback;
+    private Runnable onFileCreated;
+    private Consumer<Path> onFileLoaded;
     private AutocompletePopup.AutocompleteProvider autocompleteProvider;
     private AutocompletePopup.ValueProvider autocompleteValueProvider;
     private java.util.function.Predicate<String> listItemNodeChecker;
     private AutocompletePopup autocompletePopup;
+    private RefactorPopup refactorPopup;
     private boolean validateOnSave = true;
     private org.apache.camel.dsl.yaml.validator.YamlValidator yamlValidator;
     private PropertiesValidator propertiesValidator;
@@ -212,6 +216,14 @@ class SourceViewer {
         this.notificationCallback = callback;
     }
 
+    void setOnFileCreated(Runnable callback) {
+        this.onFileCreated = callback;
+    }
+
+    void setOnFileLoaded(Consumer<Path> callback) {
+        this.onFileLoaded = callback;
+    }
+
     void setAutocompleteProvider(AutocompletePopup.AutocompleteProvider 
provider) {
         this.autocompleteProvider = provider;
     }
@@ -344,6 +356,10 @@ class SourceViewer {
             autocompletePopup = null;
             return true;
         }
+        if (refactorPopup != null && refactorPopup.isVisible()) {
+            refactorPopup.close();
+            return true;
+        }
         if (pendingDiscard) {
             pendingDiscard = false;
             exitEditMode();
@@ -634,6 +650,14 @@ class SourceViewer {
             }
             return true;
         }
+        if (refactorPopup != null && refactorPopup.isVisible()) {
+            refactorPopup.handleKeyEvent(ke);
+            RefactorPopup.Request req = refactorPopup.consumeResult();
+            if (req != null) {
+                applyRefactoring(req);
+            }
+            return true;
+        }
         if (autocompletePopup != null) {
             boolean wasValueMode = autocompletePopup.isValueMode();
             boolean wasListItem = autocompletePopup.isListItemInsertion();
@@ -695,6 +719,10 @@ class SourceViewer {
             applyBlockEdit(YamlBlockEditor.deleteLine(editLines(), 
editState.cursorRow()));
             return true;
         }
+        if (ke.hasCtrl() && ke.isCharIgnoreCase('r') && isCamelYamlFile()) {
+            openRefactorPopup();
+            return true;
+        }
         if (ke.isKey(KeyCode.LEFT) && ke.hasCtrl()) {
             SourceEditorNavigation.moveWordLeft(editState);
             return true;
@@ -857,6 +885,7 @@ class SourceViewer {
         editState.clear();
         editHistory.clear();
         autocompletePopup = null;
+        refactorPopup = null;
         validationErrors = null;
         inlineErrors = Collections.emptyMap();
         lastBackgroundValidationTime = 0;
@@ -2844,6 +2873,9 @@ class SourceViewer {
         if (pendingDiscard) {
             renderDiscardPopup(frame, area);
         }
+        if (refactorPopup != null && refactorPopup.isVisible()) {
+            refactorPopup.render(frame, area);
+        }
     }
 
     private void applySyntaxHighlightOverlay(Frame frame, Rect editorArea) {
@@ -3060,6 +3092,10 @@ class SourceViewer {
             return;
         }
         if (editMode) {
+            if (refactorPopup != null && refactorPopup.isVisible()) {
+                refactorPopup.renderFooter(spans);
+                return;
+            }
             TuiHelper.hint(spans, "Esc", "cancel");
             TuiHelper.hint(spans, "Ctrl+S", "save");
             TuiHelper.hint(spans, "F5", "save & close");
@@ -3078,7 +3114,9 @@ class SourceViewer {
             if (!inlineErrors.isEmpty()) {
                 TuiHelper.hint(spans, "F9", "next error");
             }
-            TuiHelper.hint(spans, TuiIcons.HINT_SCROLL, "move");
+            if (isCamelYamlFile()) {
+                TuiHelper.hint(spans, "Ctrl+R", "refactor");
+            }
             return;
         }
         if (markdownMode) {
@@ -3115,6 +3153,343 @@ class SourceViewer {
         }
     }
 
+    // ---- Refactoring (F5 / Ctrl+R in view mode) ----
+
+    private void openRefactorPopup() {
+        int row = editState.cursorRow();
+        if (row < 0 || row >= editState.lineCount()) {
+            return;
+        }
+        String rawLine = editState.getLine(row);
+        List<RefactorPopup.Action> actions = new ArrayList<>();
+        // Extract to new file: available on any EIP step block in a YAML route
+        if (isCamelYamlFile()) {
+            List<String> lines = editLines();
+            YamlBlockEditor.BlockRange block = 
YamlBlockEditor.findBlock(lines, row, true);
+            if (block != null && !block.isEmpty() && 
isExtractableStep(lines.get(block.startRow()))) {
+                actions.add(RefactorPopup.Action.EXTRACT_TO_FILE);
+            }
+        }
+        String currentUri = extractUriFromLine(rawLine);
+        if (currentUri != null) {
+            actions.add(RefactorPopup.Action.REPLACE_URI);
+        }
+        if (currentUri == null && extractValueFromLine(rawLine) != null) {
+            actions.add(RefactorPopup.Action.EXTRACT_TO_PROPERTY);
+        }
+        if (actions.isEmpty()) {
+            return;
+        }
+        refactorPopup = new RefactorPopup();
+        refactorPopup.open(actions, currentUri);
+    }
+
+    private void applyRefactoring(RefactorPopup.Request req) {
+        int row = editState.cursorRow();
+        if (row < 0 || row >= editState.lineCount()) {
+            return;
+        }
+        String rawLine = editState.getLine(row);
+        switch (req.action()) {
+            case EXTRACT_TO_FILE -> applyExtractToFile(row, req.value());
+            case REPLACE_URI -> applyReplaceUri(row, rawLine, req.value());
+            case EXTRACT_TO_PROPERTY -> applyExtractToProperty(row, rawLine, 
req.value());
+        }
+    }
+
+    private void applyExtractToFile(int cursorRow, String name) {
+        if (editableFile == null) {
+            notifySave("Cannot extract: file is not writable", true);
+            return;
+        }
+        name = sanitizeFileName(name);
+        if (name.isEmpty()) {
+            notifySave("Cannot extract: invalid file name", true);
+            return;
+        }
+        List<String> lines = editLines();
+        YamlBlockEditor.BlockRange block = YamlBlockEditor.findBlock(lines, 
cursorRow, true);
+        if (block == null || block.isEmpty()) {
+            return;
+        }
+        String stepLine = lines.get(block.startRow());
+        if (!isExtractableStep(stepLine)) {
+            return;
+        }
+        int stepIndent = YamlBlockEditor.leadingSpaces(stepLine);
+        List<String> blockLines = new 
ArrayList<>(lines.subList(block.startRow(), block.endRow() + 1));
+        String newFileContent = buildExtractedRouteYaml(name, blockLines, 
stepIndent);
+        // Use canonical block-form notation:
+        //   - to:
+        //       uri: direct:<name>
+        String indentStr = " ".repeat(stepIndent);
+        String toLine = indentStr + "- to:";
+        String uriLine = indentStr + "    uri: direct:" + name;
+        recordEditChange();
+        List<String> newLines = new ArrayList<>(lines);
+        newLines.subList(block.startRow(), block.endRow() + 1).clear();
+        newLines.add(block.startRow(), uriLine);
+        newLines.add(block.startRow(), toLine);
+        editState.setText(YamlBlockEditor.fromLines(newLines));
+        SourceEditorNavigation.positionCursor(editState, block.startRow(), 
stepIndent);
+        // Auto-save the original file so the route index captures the 
refactoring change on disk.
+        // Without this, the new file's reverse jump link cannot be resolved 
until a manual save.
+        try {
+            Files.writeString(editableFile, editState.text(), 
StandardCharsets.UTF_8);
+            dirty = false;
+            originalEditText = editState.text();
+            lineStatuses = null;
+        } catch (IOException ignored) {
+            // best effort; extraction still proceeds
+        }
+        String newFileName = name + ".camel.yaml";
+        Path newFile = editableFile.getParent().resolve(newFileName);
+        boolean existed = Files.exists(newFile);
+        try {
+            if (existed) {
+                // Append as an additional route (blank line separator before 
the new block)
+                Files.writeString(newFile, "\n" + newFileContent, 
StandardCharsets.UTF_8, StandardOpenOption.APPEND);
+            } else {
+                Files.writeString(newFile, newFileContent, 
StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
+            }
+        } catch (IOException e) {
+            notifySave("Failed to write " + newFileName + ": " + 
e.getMessage(), true);
+            return;
+        }
+        if (!existed && onFileCreated != null) {
+            onFileCreated.run();
+        }
+        notifySave(existed ? "Added route to " + newFileName : "Extracted to " 
+ newFileName, false);
+    }
+
+    /**
+     * Sanitizes a user-supplied string into a safe file base-name: replaces 
whitespace and illegal characters with
+     * hyphens, collapses consecutive hyphens, and strips leading/trailing 
hyphens.
+     */
+    static String sanitizeFileName(String name) {
+        if (name == null) {
+            return "";
+        }
+        // Replace whitespace and any char that is not alphanumeric, hyphen, 
underscore, or dot with a hyphen
+        String sanitized = name.trim().replaceAll("[^a-zA-Z0-9._-]", "-");
+        // Collapse consecutive hyphens
+        sanitized = sanitized.replaceAll("-{2,}", "-");
+        // Strip leading/trailing hyphens
+        sanitized = sanitized.replaceAll("^-+|-+$", "");
+        return sanitized;
+    }
+
+    /**
+     * Builds the YAML content for a new standalone route file containing the 
extracted step block.
+     */
+    static String buildExtractedRouteYaml(String name, List<String> 
blockLines, int stepIndent) {
+        // Standard Camel YAML route indentation: step items at column 6.
+        String stepPrefix = "      ";
+        StringBuilder sb = new StringBuilder();
+        sb.append("- route:\n");
+        sb.append("    from:\n");
+        sb.append("      uri: direct:").append(name).append("\n");
+        sb.append("    steps:\n");
+        for (String line : blockLines) {
+            String stripped = line.length() >= stepIndent ? 
line.substring(stepIndent) : line.stripLeading();
+            sb.append(stepPrefix).append(stripped).append("\n");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Returns {@code true} if the line represents an EIP step list item that 
can be extracted to a new file. Excludes
+     * {@code - route:} and {@code - from:} which are structural, not steps.
+     */
+    static boolean isExtractableStep(String line) {
+        if (line == null) {
+            return false;
+        }
+        String trimmed = line.trim();
+        return trimmed.startsWith("- ") && !trimmed.equals("- ")
+                && !trimmed.startsWith("- route:") && !trimmed.startsWith("- 
from:");
+    }
+
+    private void applyReplaceUri(int row, String rawLine, String newUri) {
+        String newLine = replaceUriOnLine(rawLine, newUri);
+        recordEditChange();
+        List<String> lines = editLines();
+        lines.set(row, newLine);
+        removeParametersBlock(lines, row, rawLine);
+        editState.setText(YamlBlockEditor.fromLines(lines));
+        SourceEditorNavigation.positionCursor(editState, row, 
countLeadingSpaces(newLine));
+        notifySave("Replaced URI with: " + newUri, false);
+    }
+
+    /**
+     * Removes the {@code parameters:} sibling block immediately after a 
{@code uri:} line when the URI is replaced.
+     * Only applies to block-form {@code uri:} lines; inline-form URIs carry 
no separate parameters block.
+     */
+    static void removeParametersBlock(List<String> lines, int uriRow, String 
uriLine) {
+        if (uriLine == null || !uriLine.trim().startsWith("uri:")) {
+            return;
+        }
+        int uriIndent = YamlBlockEditor.leadingSpaces(uriLine);
+        int paramsStart = -1;
+        int paramsEnd = -1;
+        for (int i = uriRow + 1; i < lines.size(); i++) {
+            String line = lines.get(i);
+            if (line.isBlank()) {
+                continue;
+            }
+            int indent = YamlBlockEditor.leadingSpaces(line);
+            if (indent < uriIndent) {
+                break;
+            }
+            if (indent == uriIndent) {
+                if (line.trim().startsWith("parameters:")) {
+                    paramsStart = i;
+                    paramsEnd = i;
+                    // Extend to all child lines (indented deeper than 
uriIndent)
+                    for (int j = i + 1; j < lines.size(); j++) {
+                        String next = lines.get(j);
+                        if (next.isBlank()) {
+                            continue;
+                        }
+                        if (YamlBlockEditor.leadingSpaces(next) <= uriIndent) {
+                            break;
+                        }
+                        paramsEnd = j;
+                    }
+                }
+                break; // another sibling key — stop regardless
+            }
+        }
+        if (paramsStart >= 0) {
+            lines.subList(paramsStart, paramsEnd + 1).clear();
+        }
+    }
+
+    private void applyExtractToProperty(int row, String rawLine, String 
propKey) {
+        String value = extractValueFromLine(rawLine);
+        if (value == null) {
+            return;
+        }
+        String newLine = replaceValueWithPlaceholder(rawLine, propKey);
+        recordEditChange();
+        List<String> lines = editLines();
+        lines.set(row, newLine);
+        editState.setText(YamlBlockEditor.fromLines(lines));
+        SourceEditorNavigation.positionCursor(editState, row, 
countLeadingSpaces(newLine));
+        if (editableFile != null) {
+            try {
+                Path propsFile = 
editableFile.getParent().resolve("application.properties");
+                Files.writeString(propsFile, propKey + "=" + value + "\n", 
StandardCharsets.UTF_8,
+                        StandardOpenOption.CREATE,
+                        StandardOpenOption.APPEND);
+            } catch (IOException e) {
+                notifySave("Warning: could not write application.properties: " 
+ e.getMessage(), true);
+                return;
+            }
+        }
+        notifySave("Extracted to property: " + propKey, false);
+    }
+
+    /**
+     * Extracts the URI (without query parameters) from a YAML endpoint line, 
or {@code null} if the line is not a
+     * recognized endpoint/URI line.
+     */
+    static String extractUriFromLine(String line) {
+        if (line == null) {
+            return null;
+        }
+        String trimmed = line.trim();
+        for (String prefix : List.of(
+                "- to:", "- from:", "from:", "- toD:", "- to-d:", "- 
wireTap:", "- wire-tap:",
+                "- enrich:", "- pollEnrich:", "- poll-enrich:", "- poll:", 
"uri:")) {
+            if (trimmed.startsWith(prefix)) {
+                String val = trimmed.substring(prefix.length()).trim();
+                val = unquoteYaml(val);
+                if (val.isEmpty() || val.startsWith("{") || 
val.startsWith("#")) {
+                    return null;
+                }
+                int q = val.indexOf('?');
+                return q >= 0 ? val.substring(0, q) : val;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Replaces the URI on a YAML endpoint line, stripping any existing query 
parameters.
+     */
+    static String replaceUriOnLine(String line, String newUri) {
+        if (line == null) {
+            return line;
+        }
+        String trimmed = line.trim();
+        int indent = countLeadingSpaces(line);
+        String indentStr = line.substring(0, indent);
+        for (String prefix : List.of(
+                "- to:", "- from:", "from:", "- toD:", "- to-d:", "- 
wireTap:", "- wire-tap:",
+                "- enrich:", "- pollEnrich:", "- poll-enrich:", "- poll:", 
"uri:")) {
+            if (trimmed.startsWith(prefix)) {
+                return indentStr + prefix + " " + newUri;
+            }
+        }
+        return line;
+    }
+
+    /**
+     * Extracts the plain-string value from a {@code key: value} YAML line, or 
{@code null} if the line does not carry
+     * an extractable literal (empty, structural, already a placeholder, or a 
URI endpoint line).
+     */
+    static String extractValueFromLine(String line) {
+        if (line == null) {
+            return null;
+        }
+        String trimmed = line.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#") || 
trimmed.startsWith("- ")) {
+            return null;
+        }
+        int colon = trimmed.indexOf(':');
+        if (colon <= 0) {
+            return null;
+        }
+        String val = trimmed.substring(colon + 1).trim();
+        if (val.isEmpty() || val.startsWith("[") || val.startsWith("*")) {
+            return null;
+        }
+        val = unquoteYaml(val);
+        // Skip YAML maps and existing property placeholders
+        if (val.startsWith("{") || (val.startsWith("{{") && 
val.endsWith("}}"))) {
+            return null;
+        }
+        return val;
+    }
+
+    /**
+     * Replaces the value on a YAML {@code key: value} line with {@code 
{{propKey}}}, preserving indentation and key.
+     */
+    static String replaceValueWithPlaceholder(String line, String propKey) {
+        if (line == null) {
+            return line;
+        }
+        String trimmed = line.trim();
+        int indent = countLeadingSpaces(line);
+        String indentStr = line.substring(0, indent);
+        int colon = trimmed.indexOf(':');
+        if (colon <= 0) {
+            return line;
+        }
+        return indentStr + trimmed.substring(0, colon + 1) + " \"{{" + propKey 
+ "}}\"";
+    }
+
+    private static String unquoteYaml(String val) {
+        if (val.length() >= 2 && val.startsWith("\"") && val.endsWith("\"")) {
+            return val.substring(1, val.length() - 1);
+        }
+        if (val.length() >= 2 && val.startsWith("'") && val.endsWith("'")) {
+            return val.substring(1, val.length() - 1);
+        }
+        return val;
+    }
+
     /**
      * Load source for a route, scrolling to the given source line number.
      */
@@ -3165,6 +3540,9 @@ class SourceViewer {
             editableFile = Files.isWritable(filePath) ? filePath : null;
             scanDeprecatedLines();
             jumpLinks = Collections.emptyMap();
+            if (onFileLoaded != null) {
+                onFileLoaded.accept(filePath);
+            }
         } catch (IOException e) {
             title = fileName;
             lines = List.of("(Failed to read file: " + e.getMessage() + ")");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerRefactorTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerRefactorTest.java
new file mode 100644
index 000000000000..5e71f7cb05ee
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewerRefactorTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SourceViewerRefactorTest {
+
+    // ---- extractUriFromLine ----
+
+    @Test
+    void extractUriInlineToWithQueryParams() {
+        assertThat(SourceViewer.extractUriFromLine("    - to: 
timer:tick?period=1000"))
+                .isEqualTo("timer:tick");
+    }
+
+    @Test
+    void extractUriInlineFrom() {
+        assertThat(SourceViewer.extractUriFromLine("  - from: timer:tick"))
+                .isEqualTo("timer:tick");
+    }
+
+    @Test
+    void extractUriBlockUriLine() {
+        assertThat(SourceViewer.extractUriFromLine("    uri: 
log:out?showAll=true"))
+                .isEqualTo("log:out");
+    }
+
+    @Test
+    void extractUriRouteFromNoDash() {
+        // route-level "from:" without a leading dash (inside "- route:")
+        assertThat(SourceViewer.extractUriFromLine("    from: 
timer:tick?period=1000"))
+                .isEqualTo("timer:tick");
+    }
+
+    @Test
+    void extractUriInlineToD() {
+        assertThat(SourceViewer.extractUriFromLine("    - toD: 
${header.target}"))
+                .isEqualTo("${header.target}");
+    }
+
+    @Test
+    void extractUriQuoted() {
+        assertThat(SourceViewer.extractUriFromLine("    - to: 
\"http://example.com/path?q=1\"";))
+                .isEqualTo("http://example.com/path";);
+    }
+
+    @Test
+    void extractUriNotAUriLine() {
+        assertThat(SourceViewer.extractUriFromLine("    constant: Hello 
World")).isNull();
+    }
+
+    @Test
+    void extractUriEmptyBlock() {
+        assertThat(SourceViewer.extractUriFromLine("    - to:")).isNull();
+    }
+
+    @Test
+    void extractUriNull() {
+        assertThat(SourceViewer.extractUriFromLine(null)).isNull();
+    }
+
+    // ---- replaceUriOnLine ----
+
+    @Test
+    void replaceUriInlineTo() {
+        assertThat(SourceViewer.replaceUriOnLine("    - to: 
timer:tick?period=1000", "log:out"))
+                .isEqualTo("    - to: log:out");
+    }
+
+    @Test
+    void replaceUriInlineFrom() {
+        assertThat(SourceViewer.replaceUriOnLine("  - from: timer:tick", 
"direct:start"))
+                .isEqualTo("  - from: direct:start");
+    }
+
+    @Test
+    void replaceUriBlockUri() {
+        assertThat(SourceViewer.replaceUriOnLine("      uri: 
log:out?showAll=true", "kafka:my-topic"))
+                .isEqualTo("      uri: kafka:my-topic");
+    }
+
+    @Test
+    void replaceUriPreservesIndent() {
+        String line = "        - to: mock:result";
+        assertThat(SourceViewer.replaceUriOnLine(line, "log:replaced"))
+                .isEqualTo("        - to: log:replaced");
+    }
+
+    // ---- extractValueFromLine ----
+
+    @Test
+    void extractValuePlainString() {
+        assertThat(SourceViewer.extractValueFromLine("    constant: Hello 
World"))
+                .isEqualTo("Hello World");
+    }
+
+    @Test
+    void extractValueQuoted() {
+        assertThat(SourceViewer.extractValueFromLine("    message: \"some 
text\""))
+                .isEqualTo("some text");
+    }
+
+    @Test
+    void extractValueSingleQuoted() {
+        assertThat(SourceViewer.extractValueFromLine("    constant: 'fixed 
text'"))
+                .isEqualTo("fixed text");
+    }
+
+    @Test
+    void extractValueAlreadyPlaceholder() {
+        assertThat(SourceViewer.extractValueFromLine("    constant: 
{{my.key}}")).isNull();
+    }
+
+    @Test
+    void extractValueAlreadyPlaceholderQuoted() {
+        assertThat(SourceViewer.extractValueFromLine("    expression: 
\"{{greeting.message}}\"")).isNull();
+    }
+
+    @Test
+    void extractValueListItem() {
+        assertThat(SourceViewer.extractValueFromLine("    - to: 
timer:tick")).isNull();
+    }
+
+    @Test
+    void extractValueEmptyValue() {
+        assertThat(SourceViewer.extractValueFromLine("    steps:")).isNull();
+    }
+
+    @Test
+    void extractValueYamlMap() {
+        assertThat(SourceViewer.extractValueFromLine("    parameters: {period: 
1000}")).isNull();
+    }
+
+    // ---- replaceValueWithPlaceholder ----
+
+    @Test
+    void replaceValueSimple() {
+        assertThat(SourceViewer.replaceValueWithPlaceholder("    constant: 
Hello World", "greeting.message"))
+                .isEqualTo("    constant: \"{{greeting.message}}\"");
+    }
+
+    @Test
+    void replaceValuePreservesIndent() {
+        assertThat(SourceViewer.replaceValueWithPlaceholder("      message: 
some text", "my.msg"))
+                .isEqualTo("      message: \"{{my.msg}}\"");
+    }
+
+    @Test
+    void replaceValueQuotedOriginal() {
+        assertThat(SourceViewer.replaceValueWithPlaceholder("    constant: 
\"Hello\"", "my.key"))
+                .isEqualTo("    constant: \"{{my.key}}\"");
+    }
+
+    // ---- removeParametersBlock ----
+
+    private static List<String> lines(String... ls) {
+        return new ArrayList<>(Arrays.asList(ls));
+    }
+
+    @Test
+    void removeParametersBlockBasic() {
+        List<String> input = lines(
+                "    - from:",
+                "        uri: timer:tick",
+                "        parameters:",
+                "          period: \"1000\"",
+                "          fixedRate: true",
+                "    - to: log:out");
+        int uriRow = 1;
+        SourceViewer.removeParametersBlock(input, uriRow, input.get(uriRow));
+        assertThat(input).containsExactly(
+                "    - from:",
+                "        uri: timer:tick",
+                "    - to: log:out");
+    }
+
+    @Test
+    void removeParametersBlockSingleParam() {
+        List<String> input = lines(
+                "        uri: log:out",
+                "        parameters:",
+                "          showAll: true");
+        SourceViewer.removeParametersBlock(input, 0, input.get(0));
+        assertThat(input).containsExactly("        uri: log:out");
+    }
+
+    @Test
+    void removeParametersBlockNotPresentSkips() {
+        List<String> input = lines(
+                "        uri: log:out",
+                "        id: my-step");
+        SourceViewer.removeParametersBlock(input, 0, input.get(0));
+        assertThat(input).containsExactly(
+                "        uri: log:out",
+                "        id: my-step");
+    }
+
+    @Test
+    void removeParametersBlockInlineLineSkips() {
+        // Only block-form "uri:" lines trigger removal; inline "- to:" should 
be a no-op
+        List<String> input = lines(
+                "    - to: timer:tick?period=1000",
+                "    - log: \"done\"");
+        SourceViewer.removeParametersBlock(input, 0, input.get(0));
+        assertThat(input).containsExactly(
+                "    - to: timer:tick?period=1000",
+                "    - log: \"done\"");
+    }
+
+    @Test
+    void removeParametersBlockNullLineSkips() {
+        List<String> input = lines("        uri: log:out");
+        SourceViewer.removeParametersBlock(input, 0, null);
+        assertThat(input).containsExactly("        uri: log:out");
+    }
+
+    // ---- isExtractableStep ----
+
+    @Test
+    void isExtractableStepSetBody() {
+        assertThat(SourceViewer.isExtractableStep("      - 
setBody:")).isTrue();
+    }
+
+    @Test
+    void isExtractableStepChoice() {
+        assertThat(SourceViewer.isExtractableStep("    - choice:")).isTrue();
+    }
+
+    @Test
+    void isExtractableStepToIsExtractable() {
+        // "- to:" steps can be extracted (wrapped in a new route)
+        assertThat(SourceViewer.isExtractableStep("      - to: 
log:out")).isTrue();
+    }
+
+    @Test
+    void isExtractableStepRouteExcluded() {
+        assertThat(SourceViewer.isExtractableStep("- route:")).isFalse();
+    }
+
+    @Test
+    void isExtractableStepFromExcluded() {
+        assertThat(SourceViewer.isExtractableStep("  - from: 
timer:tick")).isFalse();
+    }
+
+    @Test
+    void isExtractableStepNullFalse() {
+        assertThat(SourceViewer.isExtractableStep(null)).isFalse();
+    }
+
+    @Test
+    void isExtractableStepNonListItem() {
+        assertThat(SourceViewer.isExtractableStep("    steps:")).isFalse();
+    }
+
+    // ---- buildExtractedRouteYaml ----
+
+    @Test
+    void buildExtractedRouteYamlWrapsBlock() {
+        List<String> block = lines(
+                "      - setBody:",
+                "          expression:",
+                "            constant: Hello");
+        String result = SourceViewer.buildExtractedRouteYaml("my-sub", block, 
6);
+        assertThat(result).isEqualTo(
+                "- route:\n" +
+                                     "    from:\n" +
+                                     "      uri: direct:my-sub\n" +
+                                     "    steps:\n" +
+                                     "      - setBody:\n" +
+                                     "          expression:\n" +
+                                     "            constant: Hello\n");
+    }
+
+    @Test
+    void buildExtractedRouteYamlPreservesChildIndent() {
+        List<String> block = lines(
+                "      - choice:",
+                "          when:",
+                "            - simple: \"${body} != null\"",
+                "              steps:",
+                "                - to: log:info");
+        String result = SourceViewer.buildExtractedRouteYaml("check-body", 
block, 6);
+        assertThat(result).startsWith("- route:\n    from:\n      uri: 
direct:check-body\n    steps:\n");
+        assertThat(result).contains("      - choice:\n");
+        assertThat(result).contains("          when:\n");
+    }
+
+    // ---- sanitizeFileName ----
+
+    @Test
+    void sanitizeFileNamePlain() {
+        
assertThat(SourceViewer.sanitizeFileName("my-sub-route")).isEqualTo("my-sub-route");
+    }
+
+    @Test
+    void sanitizeFileNameSpacesReplaced() {
+        assertThat(SourceViewer.sanitizeFileName("my sub 
route")).isEqualTo("my-sub-route");
+    }
+
+    @Test
+    void sanitizeFileNameSpecialCharsReplaced() {
+        assertThat(SourceViewer.sanitizeFileName("hello 
world!@#")).isEqualTo("hello-world");
+    }
+
+    @Test
+    void sanitizeFileNameColonsAndSlashesReplaced() {
+        
assertThat(SourceViewer.sanitizeFileName("timer:tick/sub")).isEqualTo("timer-tick-sub");
+    }
+
+    @Test
+    void sanitizeFileNameConsecutiveHyphensCollapsed() {
+        assertThat(SourceViewer.sanitizeFileName("a  b")).isEqualTo("a-b");
+    }
+
+    @Test
+    void sanitizeFileNameLeadingTrailingHyphensStripped() {
+        assertThat(SourceViewer.sanitizeFileName("  -my-route-  
")).isEqualTo("my-route");
+    }
+
+    @Test
+    void sanitizeFileNameNullEmpty() {
+        assertThat(SourceViewer.sanitizeFileName(null)).isEmpty();
+        assertThat(SourceViewer.sanitizeFileName("   ")).isEmpty();
+    }
+}

Reply via email to