From a3402e360ee00fa703520068f79251253d8487bf Mon Sep 17 00:00:00 2001 From: iret77 <63622643+iret77@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:04:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(fm):=20F3/F4=20open=20files=20in=20the=20e?= =?UTF-8?q?ditor=20(view/edit)=20=E2=80=94=20local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F3 (View) / F4 (Edit) now open the file under the cursor in a code editor pane instead of downloading it. A directory is entered as before. - New WorkspaceAction::OpenFileInEditor { node_id, path }: local files (empty node_id) open directly via add_tab_for_code_file (full view + edit); remote files report that native editor integration is coming rather than downloading a local copy that wouldn't save back. - SftpBrowserView::open_cursor_in_editor dispatches it; F3/F4 (key + bar) route here. Enter still activates (dir → navigate, file → download), so Download stays available. Scope: local files fully work. Remote view/edit is the honest follow-up — the correct path is zaplex's native remote editor (buffer-sync via the SSH daemon, saves back safely), which needs the file manager's SFTP host resolved to the daemon HostId; a download-to-temp shortcut would risk silent data loss on save and is deliberately avoided. Verified: cargo check -p warp 0/0; 252 sftp tests green (1 new: F3/F4 on a directory navigates). Co-Authored-By: Claude Fable 5 --- app/src/sftp_manager/browser.rs | 40 ++++++++++++++++--- .../sftp_manager/browser_integration_tests.rs | 22 ++++++++++ app/src/sftp_manager/context_menu.rs | 1 + app/src/workspace/action.rs | 9 +++++ app/src/workspace/view.rs | 19 +++++++++ 5 files changed, 85 insertions(+), 6 deletions(-) diff --git a/app/src/sftp_manager/browser.rs b/app/src/sftp_manager/browser.rs index 130f355c98..0c65fe601b 100644 --- a/app/src/sftp_manager/browser.rs +++ b/app/src/sftp_manager/browser.rs @@ -56,8 +56,8 @@ use super::types::{ /// same verbs are reachable by keyboard (pros) and click (beginners). F5/F6 /// cross-pane copy/move light up fully once a second file panel exists. const FUNCTION_BAR: &[(&str, &str, fn() -> SftpBrowserAction)] = &[ - ("F3", "View", || SftpBrowserAction::ActivateCursor), - ("F4", "Edit", || SftpBrowserAction::ActivateCursor), + ("F3", "View", || SftpBrowserAction::OpenCursorInEditor), + ("F4", "Edit", || SftpBrowserAction::OpenCursorInEditor), ("F5", "Copy", || SftpBrowserAction::CopyToOtherPane), ("F6", "Move", || SftpBrowserAction::MoveToOtherPane), ("F7", "MkDir", || SftpBrowserAction::CreateFolder), @@ -157,9 +157,12 @@ pub enum SftpBrowserAction { CursorPageUp, /// Move the cursor down one page. CursorPageDown, - /// Activate the row under the cursor (Enter / F3): a directory is entered, - /// a file is opened (details). + /// Activate the row under the cursor (Enter): a directory is entered, a + /// file is downloaded. ActivateCursor, + /// Open the row under the cursor in the code editor (F3 View / F4 Edit): a + /// directory is entered; a file opens in an editor pane. + OpenCursorInEditor, /// Enter the directory under the cursor (Right arrow); no-op on a file. EnterCursorDir, /// Toggle the row under the cursor in the multi-selection (Space). @@ -807,6 +810,30 @@ impl SftpBrowserView { } } + /// F3/F4: open the row under the cursor in the code editor. A directory is + /// entered; a file is opened in an editor pane (view + edit). The workspace + /// resolves local vs remote (`node_id`). + fn open_cursor_in_editor(&mut self, ctx: &mut ViewContext) { + let Some(index) = self.cursor_entry_index() else { + return; + }; + let Some(entry) = self.entries.get(index) else { + return; + }; + if matches!( + entry.file_type, + FileEntryType::Directory | FileEntryType::Symlink + ) { + let path = entry.path.clone(); + self.navigate_to(path, ctx); + return; + } + ctx.dispatch_typed_action(&crate::WorkspaceAction::OpenFileInEditor { + node_id: self.node_id.clone(), + path: entry.path.clone(), + }); + } + /// Enter the directory under the cursor (Right arrow); a file is a no-op. fn enter_cursor_dir(&mut self, ctx: &mut ViewContext) { if let Some(index) = self.cursor_entry_index() { @@ -2541,6 +2568,7 @@ impl TypedActionView for SftpBrowserView { self.move_cursor(CursorMove::PageDown(page), ctx); } SftpBrowserAction::ActivateCursor => self.activate_cursor(ctx), + SftpBrowserAction::OpenCursorInEditor => self.open_cursor_in_editor(ctx), SftpBrowserAction::EnterCursorDir => self.enter_cursor_dir(ctx), SftpBrowserAction::ToggleSelectCursor => self.toggle_select_cursor(ctx), SftpBrowserAction::RenameCursor => self.rename_cursor(ctx), @@ -2810,8 +2838,8 @@ impl View for SftpBrowserView { // Rename (F2, the common file-manager convention; MC's F6 // is repurposed here for the cross-pane move). "f2" => Some(SftpBrowserAction::RenameCursor), - // Function-key bar (MC parity) - "f3" | "f4" => Some(SftpBrowserAction::ActivateCursor), + // Function-key bar (MC parity): F3 View / F4 Edit open the editor. + "f3" | "f4" => Some(SftpBrowserAction::OpenCursorInEditor), "f5" => Some(SftpBrowserAction::CopyToOtherPane), "f6" => Some(SftpBrowserAction::MoveToOtherPane), "f7" => Some(SftpBrowserAction::CreateFolder), diff --git a/app/src/sftp_manager/browser_integration_tests.rs b/app/src/sftp_manager/browser_integration_tests.rs index 983317e25f..c34a552f15 100644 --- a/app/src/sftp_manager/browser_integration_tests.rs +++ b/app/src/sftp_manager/browser_integration_tests.rs @@ -2209,3 +2209,25 @@ fn test_copy_conflict_overwrite_all_applies_to_batch() { assert_eq!(std::fs::read(root.join("right/b.txt")).unwrap(), b"B2"); }); } + +// ============================================================ +// F3/F4 open-in-editor (FM Pflicht 2 — directory branch) +// ============================================================ + +/// F3/F4 on a directory enters it (the file branch dispatches a workspace +/// action, which the file-manager harness can't observe here). +#[test] +fn test_open_in_editor_on_directory_navigates() { + warpui::App::test((), |mut app| async move { + initialize_app(&mut app); + let (_, view, _temp) = create_connected_view(&mut app, &[("subdir/inner.txt", b"x")]); + + // /subdir is the only entry → cursor on it → F3/F4 enters it. + view.update(&mut app, |v, ctx| { + v.handle_action(&SftpBrowserAction::OpenCursorInEditor, ctx); + }); + view.read(&app, |v, _| { + assert_eq!(v.current_path, PathBuf::from("/subdir")); + }); + }); +} diff --git a/app/src/sftp_manager/context_menu.rs b/app/src/sftp_manager/context_menu.rs index c2bc7c6f0f..c362bfe04c 100644 --- a/app/src/sftp_manager/context_menu.rs +++ b/app/src/sftp_manager/context_menu.rs @@ -167,6 +167,7 @@ pub fn render_context_menu(state: &ContextMenuState, appearance: &Appearance) -> | SftpBrowserAction::CloseFileManager | SftpBrowserAction::OverwriteConflict { .. } | SftpBrowserAction::SkipConflict { .. } + | SftpBrowserAction::OpenCursorInEditor | SftpBrowserAction::CancelTransfer(_) | SftpBrowserAction::ToggleTransferPanel | SftpBrowserAction::ConfirmCloseTransferPanel => "sftp_ctx:unknown", diff --git a/app/src/workspace/action.rs b/app/src/workspace/action.rs index 8092e88e76..fcd630acfb 100644 --- a/app/src/workspace/action.rs +++ b/app/src/workspace/action.rs @@ -646,6 +646,14 @@ pub enum WorkspaceAction { /// Isolate the fork's file effects in a fresh sibling worktree. into_worktree: bool, }, + /// Open a file from the file manager in a code editor pane. `node_id` empty + /// = a local file (opened directly); non-empty = a remote host — native + /// remote editing (buffer-sync via the SSH daemon) is a follow-up, so a + /// remote path is reported rather than silently downloaded. + OpenFileInEditor { + node_id: String, + path: PathBuf, + }, FixSettingsWithOz { error_description: String, }, @@ -882,6 +890,7 @@ impl WorkspaceAction { | OpenSettingsFile | AskAgent { .. } | ForkAgentSession { .. } + | OpenFileInEditor { .. } | FixSettingsWithOz { .. } => false, #[cfg(debug_assertions)] ShowHoaOnboardingFlow => false, diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index a42fea9c5d..5472f0135c 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -19733,6 +19733,25 @@ impl TypedActionView for Workspace { ctx, ); } + OpenFileInEditor { node_id, path } => { + if node_id.is_empty() { + // Local file: open it directly in a code pane (view + edit). + self.add_tab_for_code_file(path.clone(), None, ctx); + } else { + // Remote file: native editing (buffer-sync via the SSH daemon) + // needs the host's daemon connection + HostId; not yet wired + // from the SFTP file manager. Report honestly rather than + // download-and-edit a local copy that wouldn't save back. + let message = format!( + "Opening remote files in the editor is coming — {} is on {node_id}. \ + Use the file manager's Download for now.", + path.display() + ); + self.toast_stack.update(ctx, |view, ctx| { + view.add_ephemeral_toast(DismissibleToast::error(message), ctx); + }); + } + } FixSettingsWithOz { error_description } => { // Repurposed (Oz-repurpose P1): the fix goes to the USER's own // CLI coding agent, not the retired in-app agent mode.