windows: Add support for SSH (#29145)
Closes #19892 This PR builds on top of #20587 and improves upon it. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
8bd739d869
commit
0ca0914cca
26 changed files with 1435 additions and 354 deletions
|
@ -15,16 +15,14 @@ use std::{
|
|||
};
|
||||
use ui::{Context, LabelLike, ListItem, Window};
|
||||
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
|
||||
use util::{maybe, paths::compare_paths};
|
||||
use util::{
|
||||
maybe,
|
||||
paths::{PathStyle, compare_paths},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OpenPathPrompt;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const PROMPT_ROOT: &str = "C:\\";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const PROMPT_ROOT: &str = "/";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenPathDelegate {
|
||||
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
|
||||
|
@ -34,6 +32,8 @@ pub struct OpenPathDelegate {
|
|||
string_matches: Vec<StringMatch>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
prompt_root: String,
|
||||
path_style: PathStyle,
|
||||
replace_prompt: Task<()>,
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ impl OpenPathDelegate {
|
|||
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
|
||||
lister: DirectoryLister,
|
||||
creating_path: bool,
|
||||
path_style: PathStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
tx: Some(tx),
|
||||
|
@ -53,6 +54,11 @@ impl OpenPathDelegate {
|
|||
string_matches: Vec::new(),
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
prompt_root: match path_style {
|
||||
PathStyle::Posix => "/".to_string(),
|
||||
PathStyle::Windows => "C:\\".to_string(),
|
||||
},
|
||||
path_style,
|
||||
replace_prompt: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +191,8 @@ impl OpenPathPrompt {
|
|||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
||||
let delegate =
|
||||
OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
|
||||
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
|
||||
let query = lister.default_query(cx);
|
||||
picker.set_query(query, window, cx);
|
||||
|
@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let lister = &self.lister;
|
||||
let last_item = Path::new(&query)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
||||
(dir.to_string(), last_item.into_owned())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir == "" {
|
||||
dir = PROMPT_ROOT.to_string();
|
||||
}
|
||||
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
|
||||
|
||||
let query = match &self.directory_state {
|
||||
DirectoryState::List { parent_path, .. } => {
|
||||
|
@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
|
||||
let parent_path_is_root = self.prompt_root == dir;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(query) = query {
|
||||
let paths = query.await;
|
||||
|
@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
DirectoryState::None { create: false }
|
||||
| DirectoryState::List { .. } => match paths {
|
||||
Ok(paths) => DirectoryState::List {
|
||||
entries: path_candidates(&dir, paths),
|
||||
entries: path_candidates(parent_path_is_root, paths),
|
||||
parent_path: dir.clone(),
|
||||
error: None,
|
||||
},
|
||||
|
@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
DirectoryState::None { create: true }
|
||||
| DirectoryState::Create { .. } => match paths {
|
||||
Ok(paths) => {
|
||||
let mut entries = path_candidates(&dir, paths);
|
||||
let mut entries = path_candidates(parent_path_is_root, paths);
|
||||
let mut exists = false;
|
||||
let mut is_dir = false;
|
||||
let mut new_id = None;
|
||||
|
@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<String> {
|
||||
let candidate = self.get_entry(self.selected_index)?;
|
||||
let path_style = self.path_style;
|
||||
Some(
|
||||
maybe!({
|
||||
match &self.directory_state {
|
||||
|
@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
parent_path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
path_style.separator()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
parent_path,
|
||||
candidate.path.string,
|
||||
if candidate.is_dir {
|
||||
MAIN_SEPARATOR_STR
|
||||
path_style.separator()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
DirectoryState::None { .. } => return,
|
||||
DirectoryState::List { parent_path, .. } => {
|
||||
let confirmed_path =
|
||||
if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
|
||||
PathBuf::from(PROMPT_ROOT)
|
||||
if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
|
||||
PathBuf::from(&self.prompt_root)
|
||||
} else {
|
||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||
.join(&candidate.path.string)
|
||||
|
@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
return;
|
||||
}
|
||||
let prompted_path =
|
||||
if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
|
||||
PathBuf::from(PROMPT_ROOT)
|
||||
if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
|
||||
PathBuf::from(&self.prompt_root)
|
||||
} else {
|
||||
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
|
||||
.join(&user_input.file.string)
|
||||
|
@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
if parent_path == PROMPT_ROOT {
|
||||
format!("{}{}", PROMPT_ROOT, candidate.path.string)
|
||||
if parent_path == &self.prompt_root {
|
||||
format!("{}{}", self.prompt_root, candidate.path.string)
|
||||
} else {
|
||||
candidate.path.string.clone()
|
||||
},
|
||||
|
@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
user_input,
|
||||
..
|
||||
} => {
|
||||
let (label, delta) = if parent_path == PROMPT_ROOT {
|
||||
let (label, delta) = if parent_path == &self.prompt_root {
|
||||
(
|
||||
format!("{}{}", PROMPT_ROOT, candidate.path.string),
|
||||
PROMPT_ROOT.len(),
|
||||
format!("{}{}", self.prompt_root, candidate.path.string),
|
||||
self.prompt_root.len(),
|
||||
)
|
||||
} else {
|
||||
(candidate.path.string.clone(), 0)
|
||||
|
@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
|
||||
if *parent_path == PROMPT_ROOT {
|
||||
fn path_candidates(
|
||||
parent_path_is_root: bool,
|
||||
mut children: Vec<DirectoryItem>,
|
||||
) -> Vec<CandidateInfo> {
|
||||
if parent_path_is_root {
|
||||
children.push(DirectoryItem {
|
||||
is_dir: true,
|
||||
path: PathBuf::default(),
|
||||
|
@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Ve
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||
let last_item = Path::new(&query)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
|
||||
(dir.to_string(), last_item.into_owned())
|
||||
} else {
|
||||
(query.to_string(), String::new())
|
||||
};
|
||||
match path_style {
|
||||
PathStyle::Posix => {
|
||||
if dir.is_empty() {
|
||||
dir = "/".to_string();
|
||||
}
|
||||
}
|
||||
PathStyle::Windows => {
|
||||
if dir.len() < 3 {
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||
match path_style {
|
||||
PathStyle::Posix => {
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if !dir.ends_with('/') {
|
||||
dir.push('/');
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
PathStyle::Windows => {
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
(query, String::new())
|
||||
};
|
||||
if dir.len() < 3 {
|
||||
dir = "C:\\".to_string();
|
||||
}
|
||||
if !dir.ends_with('\\') {
|
||||
dir.push('\\');
|
||||
}
|
||||
(dir, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use util::paths::PathStyle;
|
||||
|
||||
use crate::open_path_prompt::get_dir_and_suffix;
|
||||
|
||||
#[test]
|
||||
fn test_get_dir_and_suffix_with_windows_style() {
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\");
|
||||
assert_eq!(suffix, "Use");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||
assert_eq!(suffix, "Docum");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\");
|
||||
assert_eq!(suffix, "Documents");
|
||||
|
||||
let (dir, suffix) =
|
||||
get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
|
||||
assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
|
||||
assert_eq!(suffix, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_dir_and_suffix_with_posix_style() {
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "Use");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Docum");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Documents");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
|
||||
assert_eq!(dir, "/Users/Junkui/Documents/");
|
||||
assert_eq!(suffix, "");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate};
|
|||
use project::Project;
|
||||
use serde_json::json;
|
||||
use ui::rems;
|
||||
use util::path;
|
||||
use util::{path, paths::PathStyle};
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
use crate::OpenPathDelegate;
|
||||
|
@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
let query = path!("/root");
|
||||
insert_query(query, &picker, cx).await;
|
||||
|
@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
|
||||
let query = path!("/root");
|
||||
|
@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
|
@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
|
||||
|
||||
// Support both forward and backward slashes.
|
||||
let query = "C:/root/";
|
||||
|
@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(not(target_os = "windows"), ignore)]
|
||||
async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": "A",
|
||||
"dir1": {},
|
||||
"dir2": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
|
||||
|
||||
let query = "/root/";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(
|
||||
collect_match_candidates(&picker, cx),
|
||||
vec!["a", "dir1", "dir2"]
|
||||
);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
|
||||
|
||||
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
|
||||
let query = "/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
|
||||
|
||||
let query = "/root/d";
|
||||
insert_query(query, &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
|
||||
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
|
|||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let (picker, cx) = build_open_path_prompt(project, true, cx);
|
||||
let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
|
||||
|
||||
insert_query(path!("/root"), &picker, cx).await;
|
||||
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
|
||||
|
@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
|||
fn build_open_path_prompt(
|
||||
project: Entity<Project>,
|
||||
creating_path: bool,
|
||||
path_style: PathStyle,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
|
||||
let (tx, _) = futures::channel::oneshot::channel();
|
||||
let lister = project::DirectoryLister::Project(project.clone());
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
|
||||
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue