remote server: Fix error log about inability to open buffer (#19824)

Turns out that we used client-side `fs` to check whether something is a
directory or not, which obviously doesn't work with SSH projects.

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-10-28 16:35:37 +01:00 committed by GitHub
parent 5e89fba681
commit cc81f19c68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 213 additions and 59 deletions

View file

@ -706,10 +706,11 @@ pub(crate) async fn find_file(
) -> Option<ResolvedPath> { ) -> Option<ResolvedPath> {
project project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.resolve_existing_file_path(&candidate_file_path, buffer, cx) project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
}) })
.ok()? .ok()?
.await .await
.filter(|s| s.is_file())
} }
if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await { if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
@ -1612,4 +1613,46 @@ mod tests {
assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs"); assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
}); });
} }
#[gpui::test]
async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
..Default::default()
},
cx,
)
.await;
// Insert a new file
let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
.await;
cx.set_state(indoc! {"
You can't open ../diˇr because it's a directory.
"});
// File does not exist
let screen_coord = cx.pixel_position(indoc! {"
You can't open ../diˇr because it's a directory.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
// No highlight
cx.update_editor(|editor, cx| {
assert!(editor
.snapshot(cx)
.text_highlight_ranges::<HoveredLinkState>()
.unwrap_or_default()
.1
.is_empty());
});
// Does not open the directory
cx.simulate_click(screen_coord, Modifiers::secondary_key());
cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
}
} }

View file

@ -790,9 +790,9 @@ impl FileFinderDelegate {
let mut path_matches = Vec::new(); let mut path_matches = Vec::new();
let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| { let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| {
this.abs_file_path_exists(query.path_query(), cx) this.resolve_abs_file_path(query.path_query(), cx)
}) { }) {
task.await task.await.is_some()
} else { } else {
false false
}; };

View file

@ -3094,7 +3094,7 @@ impl Project {
} }
/// Returns the resolved version of `path`, that was found in `buffer`, if it exists. /// Returns the resolved version of `path`, that was found in `buffer`, if it exists.
pub fn resolve_existing_file_path( pub fn resolve_path_in_buffer(
&self, &self,
path: &str, path: &str,
buffer: &Model<Buffer>, buffer: &Model<Buffer>,
@ -3102,47 +3102,56 @@ impl Project {
) -> Task<Option<ResolvedPath>> { ) -> Task<Option<ResolvedPath>> {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
if path_buf.is_absolute() || path.starts_with("~") { if path_buf.is_absolute() || path.starts_with("~") {
self.resolve_abs_file_path(path, cx) self.resolve_abs_path(path, cx)
} else { } else {
self.resolve_path_in_worktrees(path_buf, buffer, cx) self.resolve_path_in_worktrees(path_buf, buffer, cx)
} }
} }
pub fn abs_file_path_exists(&self, path: &str, cx: &mut ModelContext<Self>) -> Task<bool> { pub fn resolve_abs_file_path(
let resolve_task = self.resolve_abs_file_path(path, cx); &self,
path: &str,
cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> {
let resolve_task = self.resolve_abs_path(path, cx);
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
let resolved_path = resolve_task.await; let resolved_path = resolve_task.await;
resolved_path.is_some() resolved_path.filter(|path| path.is_file())
}) })
} }
fn resolve_abs_file_path( pub fn resolve_abs_path(
&self, &self,
path: &str, path: &str,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> { ) -> 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();
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
let path = expanded.as_path(); let path = expanded.as_path();
let exists = fs.is_file(path).await; let metadata = fs.metadata(path).await.ok().flatten();
exists.then(|| ResolvedPath::AbsPath(expanded)) metadata.map(|metadata| ResolvedPath::AbsPath {
path: expanded,
is_dir: metadata.is_dir,
})
}) })
} else if let Some(ssh_client) = self.ssh_client.as_ref() { } else if let Some(ssh_client) = self.ssh_client.as_ref() {
let request = ssh_client let request = ssh_client
.read(cx) .read(cx)
.proto_client() .proto_client()
.request(proto::CheckFileExists { .request(proto::GetPathMetadata {
project_id: SSH_PROJECT_ID, project_id: SSH_PROJECT_ID,
path: path.to_string(), path: path.to_string(),
}); });
cx.background_executor().spawn(async move { cx.background_executor().spawn(async move {
let response = request.await.log_err()?; let response = request.await.log_err()?;
if response.exists { if response.exists {
Some(ResolvedPath::AbsPath(PathBuf::from(response.path))) Some(ResolvedPath::AbsPath {
path: PathBuf::from(response.path),
is_dir: response.is_dir,
})
} else { } else {
None None
} }
@ -3181,10 +3190,14 @@ impl Project {
resolved.strip_prefix(root_entry_path).unwrap_or(&resolved); resolved.strip_prefix(root_entry_path).unwrap_or(&resolved);
worktree.entry_for_path(stripped).map(|entry| { worktree.entry_for_path(stripped).map(|entry| {
ResolvedPath::ProjectPath(ProjectPath { let project_path = ProjectPath {
worktree_id: worktree.id(), worktree_id: worktree.id(),
path: entry.path.clone(), path: entry.path.clone(),
}) };
ResolvedPath::ProjectPath {
project_path,
is_dir: entry.is_dir(),
}
}) })
}) })
.ok()?; .ok()?;
@ -4149,24 +4162,41 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
/// or an AbsPath and that *exists*. /// or an AbsPath and that *exists*.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ResolvedPath { pub enum ResolvedPath {
ProjectPath(ProjectPath), ProjectPath {
AbsPath(PathBuf), project_path: ProjectPath,
is_dir: bool,
},
AbsPath {
path: PathBuf,
is_dir: bool,
},
} }
impl ResolvedPath { impl ResolvedPath {
pub fn abs_path(&self) -> Option<&Path> { pub fn abs_path(&self) -> Option<&Path> {
match self { match self {
Self::AbsPath(path) => Some(path.as_path()), Self::AbsPath { path, .. } => Some(path.as_path()),
_ => None, _ => None,
} }
} }
pub fn project_path(&self) -> Option<&ProjectPath> { pub fn project_path(&self) -> Option<&ProjectPath> {
match self { match self {
Self::ProjectPath(path) => Some(&path), Self::ProjectPath { project_path, .. } => Some(&project_path),
_ => None, _ => None,
} }
} }
pub fn is_file(&self) -> bool {
!self.is_dir()
}
pub fn is_dir(&self) -> bool {
match self {
Self::ProjectPath { is_dir, .. } => *is_dir,
Self::AbsPath { is_dir, .. } => *is_dir,
}
}
} }
impl Item for Buffer { impl Item for Buffer {

View file

@ -259,9 +259,6 @@ message Envelope {
CloseBuffer close_buffer = 245; CloseBuffer close_buffer = 245;
UpdateUserSettings update_user_settings = 246; UpdateUserSettings update_user_settings = 246;
CheckFileExists check_file_exists = 255;
CheckFileExistsResponse check_file_exists_response = 256;
ShutdownRemoteServer shutdown_remote_server = 257; ShutdownRemoteServer shutdown_remote_server = 257;
RemoveWorktree remove_worktree = 258; RemoveWorktree remove_worktree = 258;
@ -284,13 +281,16 @@ message Envelope {
GitBranchesResponse git_branches_response = 271; GitBranchesResponse git_branches_response = 271;
UpdateGitBranch update_git_branch = 272; UpdateGitBranch update_git_branch = 272;
ListToolchains list_toolchains = 273; ListToolchains list_toolchains = 273;
ListToolchainsResponse list_toolchains_response = 274; ListToolchainsResponse list_toolchains_response = 274;
ActivateToolchain activate_toolchain = 275; ActivateToolchain activate_toolchain = 275;
ActiveToolchain active_toolchain = 276; ActiveToolchain active_toolchain = 276;
ActiveToolchainResponse active_toolchain_response = 277; // current max ActiveToolchainResponse active_toolchain_response = 277;
}
GetPathMetadata get_path_metadata = 278;
GetPathMetadataResponse get_path_metadata_response = 279; // current max
}
reserved 87 to 88; reserved 87 to 88;
reserved 158 to 161; reserved 158 to 161;
@ -305,6 +305,7 @@ message Envelope {
reserved 221; reserved 221;
reserved 224 to 229; reserved 224 to 229;
reserved 247 to 254; reserved 247 to 254;
reserved 255 to 256;
} }
// Messages // Messages
@ -2357,14 +2358,15 @@ message UpdateUserSettings {
} }
} }
message CheckFileExists { message GetPathMetadata {
uint64 project_id = 1; uint64 project_id = 1;
string path = 2; string path = 2;
} }
message CheckFileExistsResponse { message GetPathMetadataResponse {
bool exists = 1; bool exists = 1;
string path = 2; string path = 2;
bool is_dir = 3;
} }
message ShutdownRemoteServer {} message ShutdownRemoteServer {}

View file

@ -343,8 +343,6 @@ messages!(
(FindSearchCandidatesResponse, Background), (FindSearchCandidatesResponse, Background),
(CloseBuffer, Foreground), (CloseBuffer, Foreground),
(UpdateUserSettings, Foreground), (UpdateUserSettings, Foreground),
(CheckFileExists, Background),
(CheckFileExistsResponse, Background),
(ShutdownRemoteServer, Foreground), (ShutdownRemoteServer, Foreground),
(RemoveWorktree, Foreground), (RemoveWorktree, Foreground),
(LanguageServerLog, Foreground), (LanguageServerLog, Foreground),
@ -363,7 +361,9 @@ messages!(
(ListToolchainsResponse, Foreground), (ListToolchainsResponse, Foreground),
(ActivateToolchain, Foreground), (ActivateToolchain, Foreground),
(ActiveToolchain, Foreground), (ActiveToolchain, Foreground),
(ActiveToolchainResponse, Foreground) (ActiveToolchainResponse, Foreground),
(GetPathMetadata, Background),
(GetPathMetadataResponse, Background)
); );
request_messages!( request_messages!(
@ -472,7 +472,6 @@ request_messages!(
(SynchronizeContexts, SynchronizeContextsResponse), (SynchronizeContexts, SynchronizeContextsResponse),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(AddWorktree, AddWorktreeResponse), (AddWorktree, AddWorktreeResponse),
(CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack), (ShutdownRemoteServer, Ack),
(RemoveWorktree, Ack), (RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse), (OpenServerSettings, OpenBufferResponse),
@ -483,7 +482,8 @@ request_messages!(
(UpdateGitBranch, Ack), (UpdateGitBranch, Ack),
(ListToolchains, ListToolchainsResponse), (ListToolchains, ListToolchainsResponse),
(ActivateToolchain, Ack), (ActivateToolchain, Ack),
(ActiveToolchain, ActiveToolchainResponse) (ActiveToolchain, ActiveToolchainResponse),
(GetPathMetadata, GetPathMetadataResponse)
); );
entity_messages!( entity_messages!(
@ -555,7 +555,6 @@ entity_messages!(
SynchronizeContexts, SynchronizeContexts,
LspExtSwitchSourceHeader, LspExtSwitchSourceHeader,
UpdateUserSettings, UpdateUserSettings,
CheckFileExists,
LanguageServerLog, LanguageServerLog,
Toast, Toast,
HideToast, HideToast,
@ -566,7 +565,8 @@ entity_messages!(
UpdateGitBranch, UpdateGitBranch,
ListToolchains, ListToolchains,
ActivateToolchain, ActivateToolchain,
ActiveToolchain ActiveToolchain,
GetPathMetadata
); );
entity_messages!( entity_messages!(

View file

@ -150,7 +150,7 @@ impl HeadlessProject {
session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer);
client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory);
client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists); client.add_request_handler(cx.weak_model(), Self::handle_get_path_metadata);
client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server); client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server);
client.add_request_handler(cx.weak_model(), Self::handle_ping); client.add_request_handler(cx.weak_model(), Self::handle_ping);
@ -525,18 +525,20 @@ impl HeadlessProject {
Ok(proto::ListRemoteDirectoryResponse { entries }) Ok(proto::ListRemoteDirectoryResponse { entries })
} }
pub async fn handle_check_file_exists( pub async fn handle_get_path_metadata(
this: Model<Self>, this: Model<Self>,
envelope: TypedEnvelope<proto::CheckFileExists>, envelope: TypedEnvelope<proto::GetPathMetadata>,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<proto::CheckFileExistsResponse> { ) -> Result<proto::GetPathMetadataResponse> {
let fs = cx.read_model(&this, |this, _| this.fs.clone())?; let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
let expanded = shellexpand::tilde(&envelope.payload.path).to_string(); let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
let exists = fs.is_file(&PathBuf::from(expanded.clone())).await; let metadata = fs.metadata(&PathBuf::from(expanded.clone())).await?;
let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
Ok(proto::CheckFileExistsResponse { Ok(proto::GetPathMetadataResponse {
exists, exists: metadata.is_some(),
is_dir,
path: expanded, path: expanded,
}) })
} }

View file

@ -604,7 +604,10 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
} }
#[gpui::test] #[gpui::test]
async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { async fn test_remote_resolve_path_in_buffer(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor()); let fs = FakeFs::new(server_cx.executor());
fs.insert_tree( fs.insert_tree(
"/code", "/code",
@ -639,10 +642,11 @@ async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut
let path = project let path = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx) project.resolve_path_in_buffer("/code/project1/README.md", &buffer, cx)
}) })
.await .await
.unwrap(); .unwrap();
assert!(path.is_file());
assert_eq!( assert_eq!(
path.abs_path().unwrap().to_string_lossy(), path.abs_path().unwrap().to_string_lossy(),
"/code/project1/README.md" "/code/project1/README.md"
@ -650,15 +654,80 @@ async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut
let path = project let path = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.resolve_existing_file_path("../README.md", &buffer, cx) project.resolve_path_in_buffer("../README.md", &buffer, cx)
}) })
.await .await
.unwrap(); .unwrap();
assert!(path.is_file());
assert_eq!( assert_eq!(
path.project_path().unwrap().clone(), path.project_path().unwrap().clone(),
ProjectPath::from((worktree_id, "README.md")) ProjectPath::from((worktree_id, "README.md"))
); );
let path = project
.update(cx, |project, cx| {
project.resolve_path_in_buffer("../src", &buffer, cx)
})
.await
.unwrap();
assert_eq!(
path.project_path().unwrap().clone(),
ProjectPath::from((worktree_id, "src"))
);
assert!(path.is_dir());
}
#[gpui::test]
async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path("/code/project1/README.md", cx)
})
.await
.unwrap();
assert!(path.is_file());
assert_eq!(
path.abs_path().unwrap().to_string_lossy(),
"/code/project1/README.md"
);
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path("/code/project1/src", cx)
})
.await
.unwrap();
assert!(path.is_dir());
assert_eq!(
path.abs_path().unwrap().to_string_lossy(),
"/code/project1/src"
);
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path("/code/project1/DOESNOTEXIST", cx)
})
.await;
assert!(path.is_none());
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]

View file

@ -1218,7 +1218,7 @@ impl Workspace {
notify_if_database_failed(window, &mut cx); notify_if_database_failed(window, &mut cx);
let opened_items = window let opened_items = window
.update(&mut cx, |_workspace, cx| { .update(&mut cx, |_workspace, cx| {
open_items(serialized_workspace, project_paths, app_state, cx) open_items(serialized_workspace, project_paths, cx)
})? })?
.await .await
.unwrap_or_default(); .unwrap_or_default();
@ -2058,8 +2058,10 @@ impl Workspace {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> { ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
match path { match path {
ResolvedPath::ProjectPath(project_path) => self.open_path(project_path, None, true, cx), ResolvedPath::ProjectPath { project_path, .. } => {
ResolvedPath::AbsPath(path) => self.open_abs_path(path, false, cx), self.open_path(project_path, None, true, cx)
}
ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, cx),
} }
} }
@ -4563,7 +4565,6 @@ fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
fn open_items( fn open_items(
serialized_workspace: Option<SerializedWorkspace>, serialized_workspace: Option<SerializedWorkspace>,
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>, mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> { ) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
let restored_items = serialized_workspace.map(|serialized_workspace| { let restored_items = serialized_workspace.map(|serialized_workspace| {
@ -4619,14 +4620,20 @@ fn open_items(
.enumerate() .enumerate()
.map(|(ix, (abs_path, project_path))| { .map(|(ix, (abs_path, project_path))| {
let workspace = workspace.clone(); let workspace = workspace.clone();
cx.spawn(|mut cx| { cx.spawn(|mut cx| async move {
let fs = app_state.fs.clone(); let file_project_path = project_path?;
async move { let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
let file_project_path = project_path?; workspace.project().update(cx, |project, cx| {
if fs.is_dir(&abs_path).await { project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
None })
} else { });
Some((
// We only want to open file paths here. If one of the items
// here is a directory, it was already opened further above
// with a `find_or_create_worktree`.
if let Ok(task) = abs_path_task {
if task.await.map_or(true, |p| p.is_file()) {
return Some((
ix, ix,
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
@ -4634,9 +4641,10 @@ fn open_items(
}) })
.log_err()? .log_err()?
.await, .await,
)) ));
} }
} }
None
}) })
}); });
@ -5580,7 +5588,7 @@ pub fn open_ssh_project(
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
cx.activate_window(); cx.activate_window();
open_items(serialized_workspace, project_paths_to_open, app_state, cx) open_items(serialized_workspace, project_paths_to_open, cx)
})? })?
.await?; .await?;