Skip to content

Commit 239c86b

Browse files
kranthik10warp-agentacarl005
authored
Fix /open-file WSL path joining (warpdotdev#9322)
## Description Fixes warpdotdev#9191. `/open-file` now resolves the shell argument with the active shell path encoding before converting it to a host `PathBuf`. This preserves Unix separators for WSL paths like `/home/ubuntu/subdir/test.txt` instead of joining the final component with a Windows separator. The change keeps line and column parsing intact and adds a regression test for WSL-style relative paths. ## Testing - `cargo fmt -- --check` - `git diff --check` - Attempted `cargo test -p warp --features local_fs open_file_command_uses_shell_separators_for_wsl_paths`, but the local Cargo build failed before compiling the app because `warpui` could not execute Apple's `metal` compiler (`xcodebuild -downloadComponent MetalToolchain` is required on this machine). ## Server API dependencies None. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: Fixed `/open-file` handling for relative WSL paths so Unix separators are preserved before opening files on Windows hosts. Co-Authored-By: Warp <agent@warp.dev> --------- Co-authored-by: Warp <agent@warp.dev> Co-authored-by: Andy <8334252+acarl005@users.noreply.github.com>
1 parent 429dbf2 commit 239c86b

1 file changed

Lines changed: 100 additions & 13 deletions

File tree

  • app/src/terminal/input/slash_commands

app/src/terminal/input/slash_commands/mod.rs

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ pub use cloud_mode_v2_view::{CloudModeV2SlashCommandView, Section as CloudModeV2
77
pub use data_source::*;
88
pub use view::{CloseReason, InlineSlashCommandView, SlashCommandsEvent};
99

10+
#[cfg(feature = "local_fs")]
11+
use std::path::PathBuf;
12+
1013
use ai::skills::SkillReference;
1114
use warp_core::features::FeatureFlag;
1215
use warp_core::send_telemetry_from_ctx;
1316
use warp_core::ui::appearance::Appearance;
1417
use warp_core::ui::theme::AnsiColorIdentifier;
18+
#[cfg(feature = "local_fs")]
19+
use warp_util::path::{CleanPathResult, LineAndColumnArg};
1520
use warpui::clipboard::ClipboardContent;
1621
use warpui::{SingletonEntity, ViewContext};
1722

@@ -43,6 +48,8 @@ use crate::terminal::input::slash_command_model::{
4348
use crate::terminal::input::{
4449
CompletionsTrigger, Event, Input, InputAction, InputSuggestionsMode, UserQueryMenuAction,
4550
};
51+
#[cfg(feature = "local_fs")]
52+
use crate::terminal::model::session::Session;
4653
use crate::terminal::view::TerminalAction;
4754
use crate::ui_components::color_dot;
4855
use crate::view_components::DismissibleToast;
@@ -109,6 +116,33 @@ impl SlashCommandTrigger {
109116
}
110117
}
111118

119+
#[cfg(feature = "local_fs")]
120+
fn open_file_command_path(
121+
session: &Session,
122+
current_dir: &str,
123+
raw_arg: &str,
124+
) -> (PathBuf, Option<LineAndColumnArg>) {
125+
let parsed_path = CleanPathResult::with_line_and_column_number(raw_arg.trim());
126+
// The argument may contain shell-escaped characters (e.g. `\ ` for spaces) from auto-suggest.
127+
// Unescape them so the path matches the actual filesystem entry.
128+
let unescaped_path = session.shell_family().unescape(&parsed_path.path);
129+
// Expand `~` to the user's home directory.
130+
let expanded_path = shellexpand::tilde(&unescaped_path);
131+
132+
let shell_path = session
133+
.convert_directory_to_typed_path_buf(current_dir.to_owned())
134+
.join(session.convert_directory_to_typed_path_buf(expanded_path.into_owned()))
135+
.normalize();
136+
let file_path = session
137+
.maybe_convert_to_native_path(&shell_path.to_path())
138+
.unwrap_or_else(|err| {
139+
log::warn!("unable to convert /open-file path to native path: {err:?}");
140+
PathBuf::from(shell_path.to_string_lossy().into_owned())
141+
});
142+
143+
(file_path, parsed_path.line_and_column_num)
144+
}
145+
112146
impl Input {
113147
pub(super) fn select_slash_command(
114148
&mut self,
@@ -525,9 +559,6 @@ impl Input {
525559
#[cfg(feature = "local_fs")]
526560
match argument {
527561
Some(args) if !args.is_empty() => {
528-
use shellexpand::tilde;
529-
use warp_util::path::CleanPathResult;
530-
531562
let Some(session_id) = self.active_block_session_id() else {
532563
return false;
533564
};
@@ -555,20 +586,14 @@ impl Input {
555586
.active_block_metadata
556587
.as_ref()
557588
.and_then(|metadata| metadata.current_working_directory())
558-
.map(std::path::PathBuf::from);
589+
.map(str::to_owned);
559590

560591
let Some(current_dir) = current_dir else {
561592
return false;
562593
};
563594

564-
let parsed_path = CleanPathResult::with_line_and_column_number(args.trim());
565-
// The argument may contain shell-escaped characters (e.g. `\ ` for
566-
// spaces) from auto-suggest. Unescape them so the path matches the
567-
// actual filesystem entry.
568-
let unescaped_path = session.shell_family().unescape(&parsed_path.path);
569-
// Expand `~` to the user's home directory.
570-
let expanded_path = tilde(&unescaped_path);
571-
let file_path = current_dir.join(&*expanded_path);
595+
let (file_path, line_col) =
596+
open_file_command_path(&session, &current_dir, args);
572597

573598
match std::fs::metadata(&file_path) {
574599
Ok(metadata) if metadata.is_file() => {
@@ -577,7 +602,7 @@ impl Input {
577602
ctx.dispatch_typed_action(&TerminalAction::OpenCodeInWarp {
578603
path: file_path,
579604
layout: external_editor::settings::EditorLayout::SplitPane,
580-
line_col: parsed_path.line_and_column_num,
605+
line_col,
581606
});
582607
}
583608
Ok(_) => {
@@ -1219,6 +1244,68 @@ impl Input {
12191244
}
12201245
}
12211246

1247+
#[cfg(all(test, feature = "local_fs", windows))]
1248+
mod tests {
1249+
use super::*;
1250+
use std::sync::Arc;
1251+
1252+
use crate::terminal::model::session::command_executor::testing::TestCommandExecutor;
1253+
use crate::terminal::model::session::SessionInfo;
1254+
use crate::terminal::shell::ShellType;
1255+
use crate::terminal::ShellLaunchData;
1256+
1257+
fn wsl_session() -> Session {
1258+
Session::new(
1259+
SessionInfo::new_for_test().with_shell_type(ShellType::Bash),
1260+
Arc::new(TestCommandExecutor::default()),
1261+
)
1262+
.with_shell_launch_data(ShellLaunchData::WSL {
1263+
distro: "Ubuntu".to_owned(),
1264+
})
1265+
}
1266+
1267+
#[test]
1268+
fn open_file_command_converts_wsl_paths_to_host_paths() {
1269+
let session = wsl_session();
1270+
let cases = [
1271+
(
1272+
"/home/ubuntu",
1273+
"subdir/test.txt",
1274+
r"\\WSL$\Ubuntu\home\ubuntu\subdir\test.txt",
1275+
None,
1276+
),
1277+
(
1278+
"/home/ubuntu/project",
1279+
"../test.txt",
1280+
r"\\WSL$\Ubuntu\home\ubuntu\test.txt",
1281+
None,
1282+
),
1283+
(
1284+
"/home/ubuntu",
1285+
"subdir/file\\ name.txt",
1286+
r"\\WSL$\Ubuntu\home\ubuntu\subdir\file name.txt",
1287+
None,
1288+
),
1289+
(
1290+
"/home/ubuntu",
1291+
"subdir/test.txt:4:2",
1292+
r"\\WSL$\Ubuntu\home\ubuntu\subdir\test.txt",
1293+
Some(LineAndColumnArg {
1294+
line_num: 4,
1295+
column_num: Some(2),
1296+
}),
1297+
),
1298+
];
1299+
1300+
for (current_dir, raw_arg, expected_path, expected_line_col) in cases {
1301+
let (path, line_col) = open_file_command_path(&session, current_dir, raw_arg);
1302+
1303+
assert_eq!(path, PathBuf::from(expected_path));
1304+
assert_eq!(line_col, expected_line_col);
1305+
}
1306+
}
1307+
}
1308+
12221309
/// Returns true when the conversation with `conversation_id` is associated with a cloud Oz
12231310
/// `AmbientAgentTask`. Used as the defensive runtime gate for `/continue-locally` so a
12241311
/// keybinding-triggered execution can't fall through onto a non-cloud-Oz conversation after

0 commit comments

Comments
 (0)