Allow opening the FS root dir as a remote project (#30190)
### Todo * [x] Allow opening `ssh://username@host:/` from the CLI * [x] Allow selecting `/` in the `open path` picker * [x] Allow selecting the home directory in the `open path` picker Release Notes: - Changed the initial state of the SSH project picker to show the full path to your home directory on the remote machine, instead of `~`. - Added the ability to open `/` as a project folder over SSH --------- Co-authored-by: Agus Zubiaga <hi@aguz.me>
This commit is contained in:
parent
6ac2f4e6a5
commit
37010aac6b
7 changed files with 98 additions and 54 deletions
|
@ -1,7 +1,7 @@
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::DirectoryLister;
|
use project::{DirectoryItem, DirectoryLister};
|
||||||
use std::{
|
use std::{
|
||||||
path::{MAIN_SEPARATOR_STR, Path, PathBuf},
|
path::{MAIN_SEPARATOR_STR, Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
|
@ -137,6 +137,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
} else {
|
} else {
|
||||||
(query, String::new())
|
(query, String::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
|
@ -171,6 +172,13 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
this.update(cx, |this, _| {
|
this.update(cx, |this, _| {
|
||||||
this.delegate.directory_state = Some(match paths {
|
this.delegate.directory_state = Some(match paths {
|
||||||
Ok(mut paths) => {
|
Ok(mut paths) => {
|
||||||
|
if dir == "/" {
|
||||||
|
paths.push(DirectoryItem {
|
||||||
|
is_dir: true,
|
||||||
|
path: Default::default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
|
paths.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
|
||||||
let match_candidates = paths
|
let match_candidates = paths
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -309,12 +317,16 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
let Some(candidate) = directory_state.match_candidates.get(*m) else {
|
let Some(candidate) = directory_state.match_candidates.get(*m) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let result = Path::new(
|
let result = if directory_state.path == "/" && candidate.path.string.is_empty() {
|
||||||
|
PathBuf::from("/")
|
||||||
|
} else {
|
||||||
|
Path::new(
|
||||||
self.lister
|
self.lister
|
||||||
.resolve_tilde(&directory_state.path, cx)
|
.resolve_tilde(&directory_state.path, cx)
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.join(&candidate.path.string);
|
.join(&candidate.path.string)
|
||||||
|
};
|
||||||
if let Some(tx) = self.tx.take() {
|
if let Some(tx) = self.tx.take() {
|
||||||
tx.send(Some(vec![result])).ok();
|
tx.send(Some(vec![result])).ok();
|
||||||
}
|
}
|
||||||
|
@ -355,7 +367,11 @@ impl PickerDelegate for OpenPathDelegate {
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(HighlightedLabel::new(
|
.child(HighlightedLabel::new(
|
||||||
candidate.path.string.clone(),
|
if directory_state.path == "/" {
|
||||||
|
format!("/{}", candidate.path.string)
|
||||||
|
} else {
|
||||||
|
candidate.path.string.clone()
|
||||||
|
},
|
||||||
highlight_positions,
|
highlight_positions,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -3904,11 +3904,7 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_abs_path(
|
pub fn resolve_abs_path(&self, path: &str, cx: &App) -> Task<Option<ResolvedPath>> {
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Option<ResolvedPath>> {
|
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
|
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
@ -5124,6 +5120,13 @@ impl ResolvedPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_abs_path(self) -> Option<PathBuf> {
|
||||||
|
match self {
|
||||||
|
Self::AbsPath { path, .. } => Some(path),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn project_path(&self) -> Option<&ProjectPath> {
|
pub fn project_path(&self) -> Option<&ProjectPath> {
|
||||||
match self {
|
match self {
|
||||||
Self::ProjectPath { project_path, .. } => Some(&project_path),
|
Self::ProjectPath { project_path, .. } => Some(&project_path),
|
||||||
|
|
|
@ -262,7 +262,7 @@ impl WorktreeStore {
|
||||||
if abs_path.starts_with("/~") {
|
if abs_path.starts_with("/~") {
|
||||||
abs_path = abs_path[1..].to_string();
|
abs_path = abs_path[1..].to_string();
|
||||||
}
|
}
|
||||||
if abs_path.is_empty() || abs_path == "/" {
|
if abs_path.is_empty() {
|
||||||
abs_path = "~/".to_string();
|
abs_path = "~/".to_string();
|
||||||
}
|
}
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
|
|
|
@ -2855,13 +2855,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||||
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
|
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
|
||||||
let Some(path_name) = worktree_abs_path
|
let Some(path_name) = worktree_abs_path.file_name() else {
|
||||||
.file_name()
|
|
||||||
.with_context(|| {
|
|
||||||
format!("Worktree abs path has no file name, root entry: {entry:?}")
|
|
||||||
})
|
|
||||||
.log_err()
|
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let path = ArcCow::Borrowed(Path::new(path_name));
|
let path = ArcCow::Borrowed(Path::new(path_name));
|
||||||
|
|
|
@ -124,20 +124,20 @@ impl ProjectPicker {
|
||||||
ix: usize,
|
ix: usize,
|
||||||
connection: SshConnectionOptions,
|
connection: SshConnectionOptions,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
home_dir: PathBuf,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<RemoteServerProjects>,
|
cx: &mut Context<RemoteServerProjects>,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let lister = project::DirectoryLister::Project(project.clone());
|
let lister = project::DirectoryLister::Project(project.clone());
|
||||||
let query = lister.default_query(cx);
|
|
||||||
let delegate = file_finder::OpenPathDelegate::new(tx, lister);
|
let delegate = file_finder::OpenPathDelegate::new(tx, lister);
|
||||||
|
|
||||||
let picker = cx.new(|cx| {
|
let picker = cx.new(|cx| {
|
||||||
let picker = Picker::uniform_list(delegate, window, cx)
|
let picker = Picker::uniform_list(delegate, window, cx)
|
||||||
.width(rems(34.))
|
.width(rems(34.))
|
||||||
.modal(false);
|
.modal(false);
|
||||||
picker.set_query(query, window, cx);
|
picker.set_query(home_dir.to_string_lossy().to_string(), window, cx);
|
||||||
picker
|
picker
|
||||||
});
|
});
|
||||||
let connection_string = connection.connection_string().into();
|
let connection_string = connection.connection_string().into();
|
||||||
|
@ -345,6 +345,7 @@ impl RemoteServerProjects {
|
||||||
ix: usize,
|
ix: usize,
|
||||||
connection_options: remote::SshConnectionOptions,
|
connection_options: remote::SshConnectionOptions,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
home_dir: PathBuf,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
@ -354,6 +355,7 @@ impl RemoteServerProjects {
|
||||||
ix,
|
ix,
|
||||||
connection_options,
|
connection_options,
|
||||||
project,
|
project,
|
||||||
|
home_dir,
|
||||||
workspace,
|
workspace,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -467,6 +469,7 @@ impl RemoteServerProjects {
|
||||||
let connection_options = ssh_connection.into();
|
let connection_options = ssh_connection.into();
|
||||||
workspace.update(cx, |_, cx| {
|
workspace.update(cx, |_, cx| {
|
||||||
cx.defer_in(window, move |workspace, window, cx| {
|
cx.defer_in(window, move |workspace, window, cx| {
|
||||||
|
let app_state = workspace.app_state().clone();
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
SshConnectionModal::new(&connection_options, Vec::new(), window, cx)
|
SshConnectionModal::new(&connection_options, Vec::new(), window, cx)
|
||||||
});
|
});
|
||||||
|
@ -489,31 +492,23 @@ impl RemoteServerProjects {
|
||||||
cx.spawn_in(window, async move |workspace, cx| {
|
cx.spawn_in(window, async move |workspace, cx| {
|
||||||
let session = connect.await;
|
let session = connect.await;
|
||||||
|
|
||||||
workspace
|
workspace.update(cx, |workspace, cx| {
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) {
|
if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) {
|
||||||
prompt.update(cx, |prompt, cx| prompt.finished(cx))
|
prompt.update(cx, |prompt, cx| prompt.finished(cx))
|
||||||
}
|
}
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
|
|
||||||
let Some(Some(session)) = session else {
|
let Some(Some(session)) = session else {
|
||||||
workspace
|
return workspace.update_in(cx, |workspace, window, cx| {
|
||||||
.update_in(cx, |workspace, window, cx| {
|
|
||||||
let weak = cx.entity().downgrade();
|
let weak = cx.entity().downgrade();
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
RemoteServerProjects::new(window, cx, weak)
|
RemoteServerProjects::new(window, cx, weak)
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.log_err();
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
workspace
|
let project = cx.update(|_, cx| {
|
||||||
.update_in(cx, |workspace, window, cx| {
|
project::Project::ssh(
|
||||||
let app_state = workspace.app_state().clone();
|
|
||||||
let weak = cx.entity().downgrade();
|
|
||||||
let project = project::Project::ssh(
|
|
||||||
session,
|
session,
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
app_state.node_runtime.clone(),
|
app_state.node_runtime.clone(),
|
||||||
|
@ -521,12 +516,24 @@ impl RemoteServerProjects {
|
||||||
app_state.languages.clone(),
|
app_state.languages.clone(),
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let home_dir = project
|
||||||
|
.read_with(cx, |project, cx| project.resolve_abs_path("~", cx))?
|
||||||
|
.await
|
||||||
|
.and_then(|path| path.into_abs_path())
|
||||||
|
.unwrap_or(PathBuf::from("/"));
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
workspace.toggle_modal(window, cx, |window, cx| {
|
workspace.toggle_modal(window, cx, |window, cx| {
|
||||||
RemoteServerProjects::project_picker(
|
RemoteServerProjects::project_picker(
|
||||||
ix,
|
ix,
|
||||||
connection_options,
|
connection_options,
|
||||||
project,
|
project,
|
||||||
|
home_dir,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
weak,
|
weak,
|
||||||
|
@ -534,8 +541,9 @@ impl RemoteServerProjects {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
.detach()
|
.detach();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2648,7 +2648,7 @@ impl Snapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn root_entry(&self) -> Option<&Entry> {
|
pub fn root_entry(&self) -> Option<&Entry> {
|
||||||
self.entry_for_path("")
|
self.entries_by_path.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: what's the difference between `root_dir` and `abs_path`?
|
/// TODO: what's the difference between `root_dir` and `abs_path`?
|
||||||
|
|
|
@ -530,8 +530,8 @@ pub async fn derive_paths_with_position(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
use super::*;
|
||||||
|
use crate::zed::{open_listener::open_local_workspace, tests::init_test};
|
||||||
use cli::{
|
use cli::{
|
||||||
CliResponse,
|
CliResponse,
|
||||||
ipc::{self},
|
ipc::{self},
|
||||||
|
@ -539,10 +539,33 @@ mod tests {
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
use util::path;
|
use util::path;
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::zed::{open_listener::open_local_workspace, tests::init_test};
|
#[gpui::test]
|
||||||
|
fn test_parse_ssh_url(cx: &mut TestAppContext) {
|
||||||
|
let _app_state = init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
SshSettings::register(cx);
|
||||||
|
});
|
||||||
|
let request =
|
||||||
|
cx.update(|cx| OpenRequest::parse(vec!["ssh://me@localhost:/".into()], cx).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
request.ssh_connection.unwrap(),
|
||||||
|
SshConnectionOptions {
|
||||||
|
host: "localhost".into(),
|
||||||
|
username: Some("me".into()),
|
||||||
|
port: None,
|
||||||
|
password: None,
|
||||||
|
args: None,
|
||||||
|
port_forwards: None,
|
||||||
|
nickname: None,
|
||||||
|
upload_binary_over_ssh: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(request.open_paths, vec!["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_workspace_with_directory(cx: &mut TestAppContext) {
|
async fn test_open_workspace_with_directory(cx: &mut TestAppContext) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue