Fall back to handling the abs path for external worktree entries (#19612)

Certain files like Rust stdlib ones can be opened by cmd-clicking on
terminal, editor contents, etc.

Those files will not belong to the current worktree, so a fake worktree,
with a single file, invisible (i.e. its dir(s) will not be shown in the
UI such as project panel), will be created on the file opening.

When the file is closed, the worktree is closed and removed along the
way, so those worktrees are considered ephemeral and their ids are not
stored in the database.
This causes issues on reopening such files when they are closed. 

The PR makes Zed to fall back to opening the file by abs path when it's
not in the project metadata, but has the abs path stored in history or
in the opened items DB data.

Release Notes:

- Handle external worktree entries [re]open better
This commit is contained in:
Kirill Bulatov 2024-10-23 17:34:23 +03:00 committed by GitHub
parent bce1b7a10a
commit b85af0e533
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 143 additions and 86 deletions

View file

@ -936,7 +936,7 @@ impl SerializableItem for Editor {
fn deserialize( fn deserialize(
project: Model<Project>, project: Model<Project>,
_workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
workspace_id: workspace::WorkspaceId, workspace_id: workspace::WorkspaceId,
item_id: ItemId, item_id: ItemId,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
@ -953,7 +953,7 @@ impl SerializableItem for Editor {
serialized_editor serialized_editor
} else { } else {
SerializedEditor { SerializedEditor {
path: serialized_editor.path, abs_path: serialized_editor.abs_path,
contents: None, contents: None,
language: None, language: None,
mtime: None, mtime: None,
@ -968,13 +968,13 @@ impl SerializableItem for Editor {
} }
}; };
let buffer_task = match serialized_editor { match serialized_editor {
SerializedEditor { SerializedEditor {
path: None, abs_path: None,
contents: Some(contents), contents: Some(contents),
language, language,
.. ..
} => cx.spawn(|_, mut cx| { } => cx.spawn(|pane, mut cx| {
let project = project.clone(); let project = project.clone();
async move { async move {
let language = if let Some(language_name) = language { let language = if let Some(language_name) = language {
@ -1001,30 +1001,34 @@ impl SerializableItem for Editor {
buffer.set_text(contents, cx); buffer.set_text(contents, cx);
})?; })?;
anyhow::Ok(buffer) pane.update(&mut cx, |_, cx| {
cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
editor
})
})
} }
}), }),
SerializedEditor { SerializedEditor {
path: Some(path), abs_path: Some(abs_path),
contents, contents,
mtime, mtime,
.. ..
} => { } => {
let project_item = project.update(cx, |project, cx| { let project_item = project.update(cx, |project, cx| {
let (worktree, path) = project let (worktree, path) = project.find_worktree(&abs_path, cx)?;
.find_worktree(&path, cx)
.with_context(|| format!("No worktree for path: {path:?}"))?;
let project_path = ProjectPath { let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(), worktree_id: worktree.read(cx).id(),
path: path.into(), path: path.into(),
}; };
Some(project.open_path(project_path, cx))
Ok(project.open_path(project_path, cx))
}); });
project_item match project_item {
.map(|project_item| { Some(project_item) => {
cx.spawn(|_, mut cx| async move { cx.spawn(|pane, mut cx| async move {
let (_, project_item) = project_item.await?; let (_, project_item) = project_item.await?;
let buffer = project_item.downcast::<Buffer>().map_err(|_| { let buffer = project_item.downcast::<Buffer>().map_err(|_| {
anyhow!("Project item at stored path was not a buffer") anyhow!("Project item at stored path was not a buffer")
@ -1051,17 +1055,6 @@ impl SerializableItem for Editor {
})?; })?;
} }
Ok(buffer)
})
})
.unwrap_or_else(|error| Task::ready(Err(error)))
}
_ => return Task::ready(Err(anyhow!("No path or contents found for buffer"))),
};
cx.spawn(|pane, mut cx| async move {
let buffer = buffer_task.await?;
pane.update(&mut cx, |_, cx| { pane.update(&mut cx, |_, cx| {
cx.new_view(|cx| { cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx); let mut editor = Editor::for_buffer(buffer, Some(project), cx);
@ -1072,6 +1065,27 @@ impl SerializableItem for Editor {
}) })
}) })
} }
None => {
let open_by_abs_path = workspace.update(cx, |workspace, cx| {
workspace.open_abs_path(abs_path.clone(), false, cx)
});
cx.spawn(|_, mut cx| async move {
let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
editor.update(&mut cx, |editor, cx| {
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
})?;
Ok(editor)
})
}
}
}
SerializedEditor {
abs_path: None,
contents: None,
..
} => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
}
}
fn serialize( fn serialize(
&mut self, &mut self,
@ -1096,12 +1110,19 @@ impl SerializableItem for Editor {
let workspace_id = workspace.database_id()?; let workspace_id = workspace.database_id()?;
let buffer = self.buffer().read(cx).as_singleton()?; let buffer = self.buffer().read(cx).as_singleton()?;
let path = buffer
let abs_path = buffer.read(cx).file().and_then(|file| {
let worktree_id = file.worktree_id(cx);
project
.read(cx) .read(cx)
.file() .worktree_for_id(worktree_id, cx)
.map(|file| file.full_path(cx)) .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok())
.and_then(|full_path| project.read(cx).find_project_path(&full_path, cx)) .or_else(|| {
.and_then(|project_path| project.read(cx).absolute_path(&project_path, cx)); let full_path = file.full_path(cx);
let project_path = project.read(cx).find_project_path(&full_path, cx)?;
project.read(cx).absolute_path(&project_path, cx)
})
});
let is_dirty = buffer.read(cx).is_dirty(); let is_dirty = buffer.read(cx).is_dirty();
let mtime = buffer.read(cx).saved_mtime(); let mtime = buffer.read(cx).saved_mtime();
@ -1120,7 +1141,7 @@ impl SerializableItem for Editor {
}; };
let editor = SerializedEditor { let editor = SerializedEditor {
path, abs_path,
contents, contents,
language, language,
mtime, mtime,
@ -1633,7 +1654,7 @@ mod tests {
let item_id = 1234 as ItemId; let item_id = 1234 as ItemId;
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: Some(PathBuf::from("/file.rs")), abs_path: Some(PathBuf::from("/file.rs")),
contents: Some("fn main() {}".to_string()), contents: Some("fn main() {}".to_string()),
language: Some("Rust".to_string()), language: Some("Rust".to_string()),
mtime: Some(now), mtime: Some(now),
@ -1664,7 +1685,7 @@ mod tests {
let item_id = 5678 as ItemId; let item_id = 5678 as ItemId;
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: Some(PathBuf::from("/file.rs")), abs_path: Some(PathBuf::from("/file.rs")),
contents: None, contents: None,
language: None, language: None,
mtime: None, mtime: None,
@ -1699,7 +1720,7 @@ mod tests {
let item_id = 9012 as ItemId; let item_id = 9012 as ItemId;
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: None, abs_path: None,
contents: Some("hello".to_string()), contents: Some("hello".to_string()),
language: Some("Rust".to_string()), language: Some("Rust".to_string()),
mtime: None, mtime: None,
@ -1737,7 +1758,7 @@ mod tests {
.checked_sub(std::time::Duration::from_secs(60 * 60 * 24)) .checked_sub(std::time::Duration::from_secs(60 * 60 * 24))
.unwrap(); .unwrap();
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: Some(PathBuf::from("/file.rs")), abs_path: Some(PathBuf::from("/file.rs")),
contents: Some("fn main() {}".to_string()), contents: Some("fn main() {}".to_string()),
language: Some("Rust".to_string()), language: Some("Rust".to_string()),
mtime: Some(old_mtime), mtime: Some(old_mtime),

View file

@ -11,7 +11,7 @@ use workspace::{ItemId, WorkspaceDb, WorkspaceId};
#[derive(Clone, Debug, PartialEq, Default)] #[derive(Clone, Debug, PartialEq, Default)]
pub(crate) struct SerializedEditor { pub(crate) struct SerializedEditor {
pub(crate) path: Option<PathBuf>, pub(crate) abs_path: Option<PathBuf>,
pub(crate) contents: Option<String>, pub(crate) contents: Option<String>,
pub(crate) language: Option<String>, pub(crate) language: Option<String>,
pub(crate) mtime: Option<SystemTime>, pub(crate) mtime: Option<SystemTime>,
@ -25,7 +25,7 @@ impl StaticColumnCount for SerializedEditor {
impl Bind for SerializedEditor { impl Bind for SerializedEditor {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> { fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let start_index = statement.bind(&self.path, start_index)?; let start_index = statement.bind(&self.abs_path, start_index)?;
let start_index = statement.bind(&self.contents, start_index)?; let start_index = statement.bind(&self.contents, start_index)?;
let start_index = statement.bind(&self.language, start_index)?; let start_index = statement.bind(&self.language, start_index)?;
@ -51,7 +51,8 @@ impl Bind for SerializedEditor {
impl Column for SerializedEditor { impl Column for SerializedEditor {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (path, start_index): (Option<PathBuf>, i32) = Column::column(statement, start_index)?; let (abs_path, start_index): (Option<PathBuf>, i32) =
Column::column(statement, start_index)?;
let (contents, start_index): (Option<String>, i32) = let (contents, start_index): (Option<String>, i32) =
Column::column(statement, start_index)?; Column::column(statement, start_index)?;
let (language, start_index): (Option<String>, i32) = let (language, start_index): (Option<String>, i32) =
@ -66,7 +67,7 @@ impl Column for SerializedEditor {
.map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32)); .map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
let editor = Self { let editor = Self {
path, abs_path,
contents, contents,
language, language,
mtime, mtime,
@ -226,7 +227,7 @@ mod tests {
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: Some(PathBuf::from("testing.txt")), abs_path: Some(PathBuf::from("testing.txt")),
contents: None, contents: None,
language: None, language: None,
mtime: None, mtime: None,
@ -244,7 +245,7 @@ mod tests {
// Now update contents and language // Now update contents and language
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: Some(PathBuf::from("testing.txt")), abs_path: Some(PathBuf::from("testing.txt")),
contents: Some("Test".to_owned()), contents: Some("Test".to_owned()),
language: Some("Go".to_owned()), language: Some("Go".to_owned()),
mtime: None, mtime: None,
@ -262,7 +263,7 @@ mod tests {
// Now set all the fields to NULL // Now set all the fields to NULL
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: None, abs_path: None,
contents: None, contents: None,
language: None, language: None,
mtime: None, mtime: None,
@ -281,7 +282,7 @@ mod tests {
// Storing and retrieving mtime // Storing and retrieving mtime
let now = SystemTime::now(); let now = SystemTime::now();
let serialized_editor = SerializedEditor { let serialized_editor = SerializedEditor {
path: None, abs_path: None,
contents: None, contents: None,
language: None, language: None,
mtime: Some(now), mtime: Some(now),

View file

@ -3356,11 +3356,12 @@ impl LspStore {
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>, diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let (worktree, relative_path) = let Some((worktree, relative_path)) =
self.worktree_store self.worktree_store.read(cx).find_worktree(&abs_path, cx)
.read(cx) else {
.find_worktree(&abs_path, cx) log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}");
.ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?; return Ok(());
};
let project_path = ProjectPath { let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(), worktree_id: worktree.read(cx).id(),

View file

@ -1362,14 +1362,13 @@ impl Workspace {
if navigated { if navigated {
break None; break None;
} }
} } else {
// If the item is no longer present in this pane, then retrieve its // If the item is no longer present in this pane, then retrieve its
// project path in order to reopen it. // path info in order to reopen it.
else {
break pane break pane
.nav_history() .nav_history()
.path_for_item(entry.item.id()) .path_for_item(entry.item.id())
.map(|(project_path, _)| (project_path, entry)); .map(|(project_path, abs_path)| (project_path, abs_path, entry));
} }
} }
}) })
@ -1377,13 +1376,17 @@ impl Workspace {
None None
}; };
if let Some((project_path, entry)) = to_load { if let Some((project_path, abs_path, entry)) = to_load {
// If the item was no longer present, then load it again from its previous path. // If the item was no longer present, then load it again from its previous path, first try the local path
let task = self.load_path(project_path, cx); let open_by_project_path = self.load_path(project_path.clone(), cx);
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
let task = task.await; let open_by_project_path = open_by_project_path.await;
let mut navigated = false; let mut navigated = false;
if let Some((project_entry_id, build_item)) = task.log_err() { match open_by_project_path
.with_context(|| format!("Navigating to {project_path:?}"))
{
Ok((project_entry_id, build_item)) => {
let prev_active_item_id = pane.update(&mut cx, |pane, _| { let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode); pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.item_id()) pane.active_item().map(|p| p.item_id())
@ -1404,6 +1407,37 @@ impl Workspace {
} }
})?; })?;
} }
Err(open_by_project_path_e) => {
// Fall back to opening by abs path, in case an external file was opened and closed,
// and its worktree is now dropped
if let Some(abs_path) = abs_path {
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.item_id())
})?;
let open_by_abs_path = workspace.update(&mut cx, |workspace, cx| {
workspace.open_abs_path(abs_path.clone(), false, cx)
})?;
match open_by_abs_path
.await
.with_context(|| format!("Navigating to {abs_path:?}"))
{
Ok(item) => {
pane.update(&mut cx, |pane, cx| {
navigated |= Some(item.item_id()) != prev_active_item_id;
pane.nav_history_mut().set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
navigated |= item.navigate(data, cx);
}
})?;
}
Err(open_by_abs_path_e) => {
log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
}
}
}
}
}
if !navigated { if !navigated {
workspace workspace