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:
parent
5e89fba681
commit
cc81f19c68
8 changed files with 213 additions and 59 deletions
|
@ -706,10 +706,11 @@ pub(crate) async fn find_file(
|
|||
) -> Option<ResolvedPath> {
|
||||
project
|
||||
.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()?
|
||||
.await
|
||||
.filter(|s| s.is_file())
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -790,9 +790,9 @@ impl FileFinderDelegate {
|
|||
let mut path_matches = Vec::new();
|
||||
|
||||
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 {
|
||||
false
|
||||
};
|
||||
|
|
|
@ -3094,7 +3094,7 @@ impl Project {
|
|||
}
|
||||
|
||||
/// 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,
|
||||
path: &str,
|
||||
buffer: &Model<Buffer>,
|
||||
|
@ -3102,47 +3102,56 @@ impl Project {
|
|||
) -> Task<Option<ResolvedPath>> {
|
||||
let path_buf = PathBuf::from(path);
|
||||
if path_buf.is_absolute() || path.starts_with("~") {
|
||||
self.resolve_abs_file_path(path, cx)
|
||||
self.resolve_abs_path(path, cx)
|
||||
} else {
|
||||
self.resolve_path_in_worktrees(path_buf, buffer, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn abs_file_path_exists(&self, path: &str, cx: &mut ModelContext<Self>) -> Task<bool> {
|
||||
let resolve_task = self.resolve_abs_file_path(path, cx);
|
||||
pub fn resolve_abs_file_path(
|
||||
&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 {
|
||||
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,
|
||||
path: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Option<ResolvedPath>> {
|
||||
if self.is_local() {
|
||||
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
|
||||
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
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() {
|
||||
let request = ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
.request(proto::CheckFileExists {
|
||||
.request(proto::GetPathMetadata {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
path: path.to_string(),
|
||||
});
|
||||
cx.background_executor().spawn(async move {
|
||||
let response = request.await.log_err()?;
|
||||
if response.exists {
|
||||
Some(ResolvedPath::AbsPath(PathBuf::from(response.path)))
|
||||
Some(ResolvedPath::AbsPath {
|
||||
path: PathBuf::from(response.path),
|
||||
is_dir: response.is_dir,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -3181,10 +3190,14 @@ impl Project {
|
|||
resolved.strip_prefix(root_entry_path).unwrap_or(&resolved);
|
||||
|
||||
worktree.entry_for_path(stripped).map(|entry| {
|
||||
ResolvedPath::ProjectPath(ProjectPath {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
};
|
||||
ResolvedPath::ProjectPath {
|
||||
project_path,
|
||||
is_dir: entry.is_dir(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
@ -4149,24 +4162,41 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
|
|||
/// or an AbsPath and that *exists*.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ResolvedPath {
|
||||
ProjectPath(ProjectPath),
|
||||
AbsPath(PathBuf),
|
||||
ProjectPath {
|
||||
project_path: ProjectPath,
|
||||
is_dir: bool,
|
||||
},
|
||||
AbsPath {
|
||||
path: PathBuf,
|
||||
is_dir: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResolvedPath {
|
||||
pub fn abs_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::AbsPath(path) => Some(path.as_path()),
|
||||
Self::AbsPath { path, .. } => Some(path.as_path()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_path(&self) -> Option<&ProjectPath> {
|
||||
match self {
|
||||
Self::ProjectPath(path) => Some(&path),
|
||||
Self::ProjectPath { project_path, .. } => Some(&project_path),
|
||||
_ => 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 {
|
||||
|
|
|
@ -259,9 +259,6 @@ message Envelope {
|
|||
CloseBuffer close_buffer = 245;
|
||||
UpdateUserSettings update_user_settings = 246;
|
||||
|
||||
CheckFileExists check_file_exists = 255;
|
||||
CheckFileExistsResponse check_file_exists_response = 256;
|
||||
|
||||
ShutdownRemoteServer shutdown_remote_server = 257;
|
||||
|
||||
RemoveWorktree remove_worktree = 258;
|
||||
|
@ -284,13 +281,16 @@ message Envelope {
|
|||
GitBranchesResponse git_branches_response = 271;
|
||||
|
||||
UpdateGitBranch update_git_branch = 272;
|
||||
|
||||
ListToolchains list_toolchains = 273;
|
||||
ListToolchainsResponse list_toolchains_response = 274;
|
||||
ActivateToolchain activate_toolchain = 275;
|
||||
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 158 to 161;
|
||||
|
@ -305,6 +305,7 @@ message Envelope {
|
|||
reserved 221;
|
||||
reserved 224 to 229;
|
||||
reserved 247 to 254;
|
||||
reserved 255 to 256;
|
||||
}
|
||||
|
||||
// Messages
|
||||
|
@ -2357,14 +2358,15 @@ message UpdateUserSettings {
|
|||
}
|
||||
}
|
||||
|
||||
message CheckFileExists {
|
||||
message GetPathMetadata {
|
||||
uint64 project_id = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message CheckFileExistsResponse {
|
||||
message GetPathMetadataResponse {
|
||||
bool exists = 1;
|
||||
string path = 2;
|
||||
bool is_dir = 3;
|
||||
}
|
||||
|
||||
message ShutdownRemoteServer {}
|
||||
|
|
|
@ -343,8 +343,6 @@ messages!(
|
|||
(FindSearchCandidatesResponse, Background),
|
||||
(CloseBuffer, Foreground),
|
||||
(UpdateUserSettings, Foreground),
|
||||
(CheckFileExists, Background),
|
||||
(CheckFileExistsResponse, Background),
|
||||
(ShutdownRemoteServer, Foreground),
|
||||
(RemoveWorktree, Foreground),
|
||||
(LanguageServerLog, Foreground),
|
||||
|
@ -363,7 +361,9 @@ messages!(
|
|||
(ListToolchainsResponse, Foreground),
|
||||
(ActivateToolchain, Foreground),
|
||||
(ActiveToolchain, Foreground),
|
||||
(ActiveToolchainResponse, Foreground)
|
||||
(ActiveToolchainResponse, Foreground),
|
||||
(GetPathMetadata, Background),
|
||||
(GetPathMetadataResponse, Background)
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -472,7 +472,6 @@ request_messages!(
|
|||
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
|
||||
(AddWorktree, AddWorktreeResponse),
|
||||
(CheckFileExists, CheckFileExistsResponse),
|
||||
(ShutdownRemoteServer, Ack),
|
||||
(RemoveWorktree, Ack),
|
||||
(OpenServerSettings, OpenBufferResponse),
|
||||
|
@ -483,7 +482,8 @@ request_messages!(
|
|||
(UpdateGitBranch, Ack),
|
||||
(ListToolchains, ListToolchainsResponse),
|
||||
(ActivateToolchain, Ack),
|
||||
(ActiveToolchain, ActiveToolchainResponse)
|
||||
(ActiveToolchain, ActiveToolchainResponse),
|
||||
(GetPathMetadata, GetPathMetadataResponse)
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
@ -555,7 +555,6 @@ entity_messages!(
|
|||
SynchronizeContexts,
|
||||
LspExtSwitchSourceHeader,
|
||||
UpdateUserSettings,
|
||||
CheckFileExists,
|
||||
LanguageServerLog,
|
||||
Toast,
|
||||
HideToast,
|
||||
|
@ -566,7 +565,8 @@ entity_messages!(
|
|||
UpdateGitBranch,
|
||||
ListToolchains,
|
||||
ActivateToolchain,
|
||||
ActiveToolchain
|
||||
ActiveToolchain,
|
||||
GetPathMetadata
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
|
|
@ -150,7 +150,7 @@ impl HeadlessProject {
|
|||
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_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_ping);
|
||||
|
||||
|
@ -525,18 +525,20 @@ impl HeadlessProject {
|
|||
Ok(proto::ListRemoteDirectoryResponse { entries })
|
||||
}
|
||||
|
||||
pub async fn handle_check_file_exists(
|
||||
pub async fn handle_get_path_metadata(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::CheckFileExists>,
|
||||
envelope: TypedEnvelope<proto::GetPathMetadata>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<proto::CheckFileExistsResponse> {
|
||||
) -> Result<proto::GetPathMetadataResponse> {
|
||||
let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
|
||||
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 {
|
||||
exists,
|
||||
Ok(proto::GetPathMetadataResponse {
|
||||
exists: metadata.is_some(),
|
||||
is_dir,
|
||||
path: expanded,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -604,7 +604,10 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
|
|||
}
|
||||
|
||||
#[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());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
|
@ -639,10 +642,11 @@ async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut
|
|||
|
||||
let path = project
|
||||
.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
|
||||
.unwrap();
|
||||
assert!(path.is_file());
|
||||
assert_eq!(
|
||||
path.abs_path().unwrap().to_string_lossy(),
|
||||
"/code/project1/README.md"
|
||||
|
@ -650,15 +654,80 @@ async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut
|
|||
|
||||
let path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.resolve_existing_file_path("../README.md", &buffer, cx)
|
||||
project.resolve_path_in_buffer("../README.md", &buffer, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(path.is_file());
|
||||
assert_eq!(
|
||||
path.project_path().unwrap().clone(),
|
||||
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)]
|
||||
|
|
|
@ -1218,7 +1218,7 @@ impl Workspace {
|
|||
notify_if_database_failed(window, &mut cx);
|
||||
let opened_items = window
|
||||
.update(&mut cx, |_workspace, cx| {
|
||||
open_items(serialized_workspace, project_paths, app_state, cx)
|
||||
open_items(serialized_workspace, project_paths, cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
@ -2058,8 +2058,10 @@ impl Workspace {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
|
||||
match path {
|
||||
ResolvedPath::ProjectPath(project_path) => self.open_path(project_path, None, true, cx),
|
||||
ResolvedPath::AbsPath(path) => self.open_abs_path(path, false, cx),
|
||||
ResolvedPath::ProjectPath { project_path, .. } => {
|
||||
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(
|
||||
serialized_workspace: Option<SerializedWorkspace>,
|
||||
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
|
||||
let restored_items = serialized_workspace.map(|serialized_workspace| {
|
||||
|
@ -4619,14 +4620,20 @@ fn open_items(
|
|||
.enumerate()
|
||||
.map(|(ix, (abs_path, project_path))| {
|
||||
let workspace = workspace.clone();
|
||||
cx.spawn(|mut cx| {
|
||||
let fs = app_state.fs.clone();
|
||||
async move {
|
||||
let file_project_path = project_path?;
|
||||
if fs.is_dir(&abs_path).await {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
cx.spawn(|mut cx| async move {
|
||||
let file_project_path = project_path?;
|
||||
let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
// 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,
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
|
@ -4634,9 +4641,10 @@ fn open_items(
|
|||
})
|
||||
.log_err()?
|
||||
.await,
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -5580,7 +5588,7 @@ pub fn open_ssh_project(
|
|||
.update(&mut cx, |_, cx| {
|
||||
cx.activate_window();
|
||||
|
||||
open_items(serialized_workspace, project_paths_to_open, app_state, cx)
|
||||
open_items(serialized_workspace, project_paths_to_open, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue