11 KiB
TECH.md — CODE-1779: Drag-and-drop file paths in WSL (and Git Bash)
See PRODUCT.md for user-visible behavior.
Context
There are two drop paths relevant to this ticket:
-
Drop onto the terminal grid (e.g. during a long-running command). Handled by
TerminalView::drag_and_drop_filesinapp/src/terminal/view.rs (23007-23085). This path already applieswarp_util::path::convert_windows_path_to_wslwhensession.is_wsl()before shell-escaping and writing to the PTY. For MSYS2 long-running it deliberately keeps Windows-native paths and skips shell escaping (so native Windows binaries receive the form they expect). Not changing this path. -
Drop onto the input editor (typical case when there's no long-running command). The editor element's
drag_and_drop_fileinapp/src/editor/view/element.rs (663-681)dispatchesEditorAction::DragAndDropFiles, which routes toEditorView::drag_and_drop_filesinapp/src/editor/view/mod.rs (7893-7914). That function emitsEvent::DroppedImageFilesfor image paths (terminal input handles attachment) and, for everything else, callswarpui::clipboard_utils::escaped_paths_strfollowed byself.user_insert. No path conversion happens — this is the bug for both WSL and MSYS2.
Why the fix doesn't belong inside EditorView: EditorView is a general-purpose text editor used across dozens of surfaces (notebooks, settings, code editors, modals, etc. — see the many EditorView::new(...) / single_line(...) call sites). It already carries shell_family for escaping, which is a shell concern but not tied to any particular parent; WSL / MSYS2 path conversion is a terminal-session concern and must not leak into the editor.
Existing helpers:
warp_util::path::convert_windows_path_to_wslincrates/warp_util/src/path.rs (673-691), with tests atcrates/warp_util/src/path_test.rs (629-649).- No equivalent exists yet for MSYS2. We'll add
convert_windows_path_to_msys2alongside it (same shape,/<drive>/…instead of/mnt/<drive>/…). Session::is_wsl()atapp/src/terminal/model/session.rs:972andSession::is_msys2()atapp/src/terminal/model/session.rs:980both already exist and returnfalseon non-Windows platforms.TerminalInput::active_sessionis available inapp/src/terminal/input.rs:11011.
Shell-family setup on the editor already happens in TerminalInput::set_active_block_metadata at app/src/terminal/input.rs (13034-13065), which is called on every active-block (and therefore session) change. That's the natural place to install/clear the transformer too.
Proposed changes
1. Add convert_windows_path_to_msys2 to warp_util::path
In crates/warp_util/src/path.rs, alongside convert_windows_path_to_wsl, add:
/// Converts a Windows-native path string to an MSYS2 / Git Bash POSIX-style path.
///
/// Drive-letter paths (e.g. `C:\Users\aloke\file.txt`) are mapped to
/// `/<drive>/Users/aloke/file.txt`. Paths that don't start with a drive letter
/// are returned as-is with backslashes replaced by forward slashes.
pub fn convert_windows_path_to_msys2(windows_path: &str) -> String { /* ... */ }
Implementation mirrors convert_windows_path_to_wsl but emits /<drive> instead of /mnt/<drive>. Consider factoring both into a shared internal helper that takes a &'static str drive prefix ("/mnt/" vs "/") to avoid duplication.
Add unit tests in crates/warp_util/src/path_test.rs matching PRODUCT.md invariant (2):
C:\Users\andy\file.txt→/c/Users/andy/file.txt- Spaces preserved
C:\andC:both →/c- Uppercase drive lowercased
- UNC fallback converts backslashes to slashes
2. Add a generic "path transformer" hook to EditorView
In app/src/editor/view/mod.rs:
- Add a public type alias
pub type PathTransformerFn = Rc<dyn Fn(&str) -> String>;near the existingCursorColorsFn(line ~1587).Rcis already imported. - Add
pub drag_drop_path_transformer: Option<PathTransformerFn>toEditorOptions(defaultNonein bothDefault for EditorOptionsandFrom<SingleLineEditorOptions> for EditorOptions). - Add a mirrored private field on
EditorView, initialize it fromoptions.drag_drop_path_transformerinnew_internal, and add aset_drag_drop_path_transformersetter next to the existingset_shell_family. No public getter is needed: the only caller that would want to read the transformer back is the parent that installed it, and that parent already knows which transformation applies to the current session. - In
EditorView::drag_and_drop_files, after the image-files branch returns early, run each remaining (non-image) path through the transformer if present, then pass the transformed list — notpaths_as_strings— intoescaped_paths_str. Image paths stay untransformed becauseEvent::DroppedImageFilesconsumers (terminal input) need to read the original file from the host.
Design note: we deliberately picked a closure over an event/delegation flag. The editor already takes other domain-agnostic closures (render_decorator_elements, cursor_colors_fn, keymap_context_modifier), and a pure Fn(&str) -> String hook is the smallest, most focused extension point for what amounts to "rewrite each path string before inserting." It keeps image attachment, shell escaping, and insertion flow inside the editor unchanged, and it imposes no terminal vocabulary on the editor. A single closure type also naturally covers multiple concrete transformations (WSL, MSYS2, and anything else we add later).
3. Install the transformer from TerminalInput
In app/src/terminal/input.rs:
- In
set_active_block_metadata(line ~13034), alongside the existingeditor.set_shell_family(...)call, select a transformer based on the session and install or clear it:session.is_wsl()→Rc::new(|p: &str| warp_util::path::convert_windows_path_to_wsl(p)).session.is_msys2()→Rc::new(|p: &str| warp_util::path::convert_windows_path_to_msys2(p)).- Otherwise →
None. Both session predicates returnfalseoff-Windows, so nocfg!(windows)gating is needed at the call site. Checkis_wsl()beforeis_msys2()for clarity (they are mutually exclusive in practice, but the order documents priority).
- In the existing
EditorEvent::DroppedImageFileshandler (line ~9493), when the image-attach fallback inserts paths as text, apply the same WSL / MSYS2 conversion toimage_filepaths(branching onsession.is_wsl()/session.is_msys2()just as step above does) before callingescaped_paths_str. This preserves PRODUCT.md invariant (7) for both WSL and MSYS2.
We rely on set_active_block_metadata being called whenever the active session changes; it already updates shell_family and path_separators, so the transformer follows the same lifecycle and always reflects the currently active session.
4. No changes needed elsewhere
TerminalView::drag_and_drop_files(terminal grid path) already converts correctly for WSL, and deliberately does not convert for MSYS2 long-running commands. Per PRODUCT.md invariant (8), leave it alone.- No other
EditorView::new(...)call site sets the new option, so the defaultNonetransformer means identical behavior for notebooks, settings, etc.
Testing and validation
Covers the invariants in PRODUCT.md.
-
Unit tests for
convert_windows_path_to_msys2(invariant 2, MSYS2 cases). Incrates/warp_util/src/path_test.rs, mirror the existingtest_convert_windows_path_to_wsltest with MSYS2-equivalent expectations (/c/...,/d/..., etc.). -
Existing coverage for the WSL conversion itself (invariant 2, WSL cases).
test_convert_windows_path_to_wslincrates/warp_util/src/path_test.rs (629-649)already verifies drive-letter lowercasing, spaces, UNC, and empty-suffix behavior. No new tests needed there. -
Unit test — transformer wiring (invariants 1, 2, 5, 9). In
app/src/editor/view/mod_test.rs, add a test that:- Creates an
EditorViewwithshell_family: Some(Posix)and runs two scenarios:drag_drop_path_transformer: Some(Rc::new(|p| warp_util::path::convert_windows_path_to_wsl(p)))— WSL behavior.drag_drop_path_transformer: Some(Rc::new(|p| warp_util::path::convert_windows_path_to_msys2(p)))— MSYS2 behavior.
- Each scenario dispatches
EditorAction::DragAndDropFileswith representative inputs (C:\foo,D:\bar baz.txt, a UNC path, and a path with no drive letter) and asserts the buffer contents match PRODUCT.md invariant (2) with shell escaping applied on top. - A third scenario clears the transformer and asserts the original Windows paths are inserted verbatim (PRODUCT.md invariant 5).
- Creates an
-
Image-attach fallback (invariants 6, 7). In
app/src/terminal/input_test.rs, if there is already coverage for theDroppedImageFilesfallback path, extend it to assert that when a transformer is installed, the fallback text is the transformed paths. If no such test exists yet, add a focused one that mocks an over-limit scenario and inspects the buffer. Exercise both a WSL transformer and an MSYS2 transformer. Image attachment itself is unchanged and needs no new test. -
Manual verification (invariants 1, 3, 8, 10). On a Windows machine with both a WSL distro and Git Bash installed:
- Drag a file from Explorer into a WSL tab's input editor → buffer reads
/mnt/c/…with shell escaping. - Drag a file from Explorer into a Git Bash tab's input editor → buffer reads
/c/…with shell escaping. - Drag the same file onto a WSL tab while a long-running command is active → grid receives
/mnt/c/…(regression check for existing behavior). - Drag the same file onto a Git Bash tab while a long-running command is active → grid receives
C:\…without shell escaping (regression check: deliberate existing behavior per PRODUCT.md invariant 8). - Drag into a local PowerShell tab → buffer reads
C:\…unchanged. - Drag into a notebook editor → buffer reads
C:\…unchanged. - Drag an image file in Agent Mode with an empty buffer (WSL or Git Bash) → image attaches normally; no text inserted.
- Drag a file from Explorer into a WSL tab's input editor → buffer reads
Risks and mitigations
-
Stale transformer after session switch. Because we install/clear the transformer in
set_active_block_metadata, it always matches the active session. If that function is ever bypassed for a session change, the transformer could lag. Mitigation: piggy-back on the exact same call site asset_shell_family, which already has this assumption and has proven stable. -
Cross-platform builds.
convert_windows_path_to_wslis pure string manipulation and compiles on all platforms, as doesSession::is_wsl()(returnsfalseoff-Windows). Nocfg!(windows)gating needed at the call site, though in practice the transformer will only ever be non-Noneon Windows because no other platform has WSL sessions.