mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Fix excluded file creation (#12620)
Fixes https://github.com/zed-industries/zed/issues/10890 * removes `unwrap()` that caused panics for text elements with no text, remaining after edit state is cleared but project entries are not updated, having the fake, "new entry" * improves discoverability of the FS errors during file/directory creation: now those are shown as workspace notifications * stops printing anyhow backtraces in workspace notifications, printing the more readable chain of contexts instead * better indicates when new entries are created as excluded ones Release Notes: - Improve excluded entry creation workflow in the project panel ([10890](https://github.com/zed-industries/zed/issues/10890))
This commit is contained in:
parent
edd613062a
commit
47122a3115
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2404,6 +2404,7 @@ dependencies = [
|
|||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
"worktree",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -7825,6 +7826,7 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
"worktree",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -107,4 +107,5 @@ theme.workspace = true
|
|||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
worktree = { workspace = true, features = ["test-support"] }
|
||||||
headless.workspace = true
|
headless.workspace = true
|
||||||
|
@ -3022,7 +3022,6 @@ async fn test_fs_operations(
|
|||||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||||
|
|
||||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||||
|
|
||||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||||
|
|
||||||
let entry = project_b
|
let entry = project_b
|
||||||
@ -3031,6 +3030,7 @@ async fn test_fs_operations(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
worktree_a.read_with(cx_a, |worktree, _| {
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
@ -3059,6 +3059,7 @@ async fn test_fs_operations(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
worktree_a.read_with(cx_a, |worktree, _| {
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
@ -3087,6 +3088,7 @@ async fn test_fs_operations(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
worktree_a.read_with(cx_a, |worktree, _| {
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
@ -3115,20 +3117,25 @@ async fn test_fs_operations(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
project_b
|
project_b
|
||||||
.update(cx_b, |project, cx| {
|
.update(cx_b, |project, cx| {
|
||||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
project_b
|
project_b
|
||||||
.update(cx_b, |project, cx| {
|
.update(cx_b, |project, cx| {
|
||||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
worktree_a.read_with(cx_a, |worktree, _| {
|
worktree_a.read_with(cx_a, |worktree, _| {
|
||||||
|
@ -294,7 +294,7 @@ impl CosmicTextSystemState {
|
|||||||
.0,
|
.0,
|
||||||
)
|
)
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap();
|
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||||
Ok(Bounds {
|
Ok(Bounds {
|
||||||
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||||
size: size(image.placement.width.into(), image.placement.height.into()),
|
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||||
@ -328,7 +328,7 @@ impl CosmicTextSystemState {
|
|||||||
.0,
|
.0,
|
||||||
)
|
)
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap();
|
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||||
|
|
||||||
if params.is_emoji {
|
if params.is_emoji {
|
||||||
// Convert from RGBA to BGRA.
|
// Convert from RGBA to BGRA.
|
||||||
|
@ -68,7 +68,7 @@ use project_settings::{LspSettings, ProjectSettings};
|
|||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use search_history::SearchHistory;
|
use search_history::SearchHistory;
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use worktree::LocalSnapshot;
|
use worktree::{CreatedEntry, LocalSnapshot};
|
||||||
|
|
||||||
use http::{HttpClient, Url};
|
use http::{HttpClient, Url};
|
||||||
use rpc::{ErrorCode, ErrorExt as _};
|
use rpc::{ErrorCode, ErrorExt as _};
|
||||||
@ -1414,10 +1414,12 @@ impl Project {
|
|||||||
project_path: impl Into<ProjectPath>,
|
project_path: impl Into<ProjectPath>,
|
||||||
is_directory: bool,
|
is_directory: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Entry>>> {
|
) -> Task<Result<CreatedEntry>> {
|
||||||
let project_path = project_path.into();
|
let project_path = project_path.into();
|
||||||
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
|
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
|
||||||
return Task::ready(Ok(None));
|
return Task::ready(Err(anyhow!(format!(
|
||||||
|
"No worktree for path {project_path:?}"
|
||||||
|
))));
|
||||||
};
|
};
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
worktree.update(cx, |worktree, cx| {
|
worktree.update(cx, |worktree, cx| {
|
||||||
@ -1448,8 +1450,15 @@ impl Project {
|
|||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(Some),
|
.map(CreatedEntry::Included),
|
||||||
None => Ok(None),
|
None => {
|
||||||
|
let abs_path = worktree.update(&mut cx, |worktree, _| {
|
||||||
|
worktree
|
||||||
|
.absolutize(&project_path.path)
|
||||||
|
.with_context(|| format!("absolutizing {project_path:?}"))
|
||||||
|
})??;
|
||||||
|
Ok(CreatedEntry::Excluded { abs_path })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1506,9 +1515,9 @@ impl Project {
|
|||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
new_path: impl Into<Arc<Path>>,
|
new_path: impl Into<Arc<Path>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Entry>>> {
|
) -> Task<Result<CreatedEntry>> {
|
||||||
let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
|
let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
|
||||||
return Task::ready(Ok(None));
|
return Task::ready(Err(anyhow!(format!("No worktree for entry {entry_id:?}"))));
|
||||||
};
|
};
|
||||||
let new_path = new_path.into();
|
let new_path = new_path.into();
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
@ -1540,8 +1549,15 @@ impl Project {
|
|||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map(Some),
|
.map(CreatedEntry::Included),
|
||||||
None => Ok(None),
|
None => {
|
||||||
|
let abs_path = worktree.update(&mut cx, |worktree, _| {
|
||||||
|
worktree
|
||||||
|
.absolutize(&new_path)
|
||||||
|
.with_context(|| format!("absolutizing {new_path:?}"))
|
||||||
|
})??;
|
||||||
|
Ok(CreatedEntry::Excluded { abs_path })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -8617,7 +8633,10 @@ impl Project {
|
|||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
Ok(proto::ProjectEntryResponse {
|
Ok(proto::ProjectEntryResponse {
|
||||||
entry: entry.as_ref().map(|e| e.into()),
|
entry: match &entry {
|
||||||
|
CreatedEntry::Included(entry) => Some(entry.into()),
|
||||||
|
CreatedEntry::Excluded { .. } => None,
|
||||||
|
},
|
||||||
worktree_scan_id: worktree_scan_id as u64,
|
worktree_scan_id: worktree_scan_id as u64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -8644,7 +8663,10 @@ impl Project {
|
|||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
Ok(proto::ProjectEntryResponse {
|
Ok(proto::ProjectEntryResponse {
|
||||||
entry: entry.as_ref().map(|e| e.into()),
|
entry: match &entry {
|
||||||
|
CreatedEntry::Included(entry) => Some(entry.into()),
|
||||||
|
CreatedEntry::Excluded { .. } => None,
|
||||||
|
},
|
||||||
worktree_scan_id: worktree_scan_id as u64,
|
worktree_scan_id: worktree_scan_id as u64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3127,6 +3127,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
|
|||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.await
|
.await
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
@ -4465,6 +4466,7 @@ async fn test_create_entry(cx: &mut gpui::TestAppContext) {
|
|||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.await
|
.await
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Can't create paths outside the project
|
// Can't create paths outside the project
|
||||||
|
@ -34,6 +34,7 @@ ui.workspace = true
|
|||||||
unicase.workspace = true
|
unicase.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
worktree.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -35,9 +35,10 @@ use unicase::UniCase;
|
|||||||
use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt};
|
use util::{maybe, NumericPrefixWithSuffix, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::DetachAndPromptErr,
|
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
||||||
OpenInTerminal, Workspace,
|
OpenInTerminal, Workspace,
|
||||||
};
|
};
|
||||||
|
use worktree::CreatedEntry;
|
||||||
|
|
||||||
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
|
||||||
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
|
||||||
@ -711,7 +712,7 @@ impl ProjectPanel {
|
|||||||
|
|
||||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(task) = self.confirm_edit(cx) {
|
if let Some(task) = self.confirm_edit(cx) {
|
||||||
task.detach_and_log_err(cx);
|
task.detach_and_notify_err(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,29 +795,66 @@ impl ProjectPanel {
|
|||||||
edit_state.processing_filename = Some(filename);
|
edit_state.processing_filename = Some(filename);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
Some(cx.spawn(|this, mut cx| async move {
|
Some(cx.spawn(|project_panel, mut cx| async move {
|
||||||
let new_entry = edit_task.await;
|
let new_entry = edit_task.await;
|
||||||
this.update(&mut cx, |this, cx| {
|
project_panel.update(&mut cx, |project_panel, cx| {
|
||||||
this.edit_state.take();
|
project_panel.edit_state.take();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(new_entry) = new_entry? {
|
match new_entry {
|
||||||
this.update(&mut cx, |this, cx| {
|
Err(e) => {
|
||||||
if let Some(selection) = &mut this.selection {
|
project_panel.update(&mut cx, |project_panel, cx| {
|
||||||
if selection.entry_id == edited_entry_id {
|
project_panel.marked_entries.clear();
|
||||||
selection.worktree_id = worktree_id;
|
project_panel.update_visible_entries(None, cx);
|
||||||
selection.entry_id = new_entry.id;
|
}).ok();
|
||||||
this.marked_entries.clear();
|
Err(e)?;
|
||||||
this.expand_to_selection(cx);
|
}
|
||||||
|
Ok(CreatedEntry::Included(new_entry)) => {
|
||||||
|
project_panel.update(&mut cx, |project_panel, cx| {
|
||||||
|
if let Some(selection) = &mut project_panel.selection {
|
||||||
|
if selection.entry_id == edited_entry_id {
|
||||||
|
selection.worktree_id = worktree_id;
|
||||||
|
selection.entry_id = new_entry.id;
|
||||||
|
project_panel.marked_entries.clear();
|
||||||
|
project_panel.expand_to_selection(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
project_panel.update_visible_entries(None, cx);
|
||||||
|
if is_new_entry && !is_dir {
|
||||||
|
project_panel.open_entry(new_entry.id, false, true, false, cx);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(CreatedEntry::Excluded { abs_path }) => {
|
||||||
|
if let Some(open_task) = project_panel
|
||||||
|
.update(&mut cx, |project_panel, cx| {
|
||||||
|
project_panel.marked_entries.clear();
|
||||||
|
project_panel.update_visible_entries(None, cx);
|
||||||
|
|
||||||
|
if is_dir {
|
||||||
|
project_panel.project.update(cx, |_, cx| {
|
||||||
|
cx.emit(project::Event::Notification(format!(
|
||||||
|
"Created an excluded directory at {abs_path:?}.\nAlter `file_scan_exclusions` in the settings to show it in the panel"
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
project_panel
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_abs_path(abs_path, true, cx)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let _ = open_task.await?;
|
||||||
}
|
}
|
||||||
this.update_visible_entries(None, cx);
|
}
|
||||||
if is_new_entry && !is_dir {
|
|
||||||
this.open_entry(new_entry.id, false, true, false, cx);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}))
|
||||||
@ -2369,13 +2407,16 @@ impl ClipboardEntry {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
|
use gpui::{Empty, TestAppContext, View, VisualTestContext, WindowHandle};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use project::{FakeFs, WorktreeSettings};
|
use project::{FakeFs, WorktreeSettings};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use workspace::AppState;
|
use workspace::{
|
||||||
|
item::{Item, ProjectItem},
|
||||||
|
register_project_item, AppState,
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
|
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
|
||||||
@ -4488,6 +4529,199 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
register_project_item::<TestProjectItemView>(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor().clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
".dockerignore": "",
|
||||||
|
".git": {
|
||||||
|
"HEAD": "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
|
||||||
|
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||||
|
let panel = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let panel = ProjectPanel::new(workspace, cx);
|
||||||
|
workspace.add_panel(panel.clone(), cx);
|
||||||
|
panel
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
select_path(&panel, "root1", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..10, cx),
|
||||||
|
&["v root1 <== selected", " .dockerignore",]
|
||||||
|
);
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
assert!(
|
||||||
|
workspace.active_item(cx).is_none(),
|
||||||
|
"Should have no active items in the beginning"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let excluded_file_path = ".git/COMMIT_EDITMSG";
|
||||||
|
let excluded_dir_path = "excluded_dir";
|
||||||
|
|
||||||
|
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(panel.filename_editor.read(cx).is_focused(cx));
|
||||||
|
});
|
||||||
|
panel
|
||||||
|
.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.filename_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
|
||||||
|
panel.confirm_edit(cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..13, cx),
|
||||||
|
&["v root1", " .dockerignore"],
|
||||||
|
"Excluded dir should not be shown after opening a file in it"
|
||||||
|
);
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(
|
||||||
|
!panel.filename_editor.read(cx).is_focused(cx),
|
||||||
|
"Should have closed the file name editor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let active_entry_path = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.expect("should have opened and activated the excluded item")
|
||||||
|
.act_as::<TestProjectItemView>(cx)
|
||||||
|
.expect(
|
||||||
|
"should have opened the corresponding project item for the excluded item",
|
||||||
|
)
|
||||||
|
.read(cx)
|
||||||
|
.path
|
||||||
|
.clone();
|
||||||
|
assert_eq!(
|
||||||
|
active_entry_path.path.as_ref(),
|
||||||
|
Path::new(excluded_file_path),
|
||||||
|
"Should open the excluded file"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
workspace.notification_ids().is_empty(),
|
||||||
|
"Should have no notifications after opening an excluded file"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await,
|
||||||
|
"Should have created the excluded file"
|
||||||
|
);
|
||||||
|
|
||||||
|
select_path(&panel, "root1", cx);
|
||||||
|
panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(panel.filename_editor.read(cx).is_focused(cx));
|
||||||
|
});
|
||||||
|
panel
|
||||||
|
.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.filename_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(excluded_file_path, cx));
|
||||||
|
panel.confirm_edit(cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..13, cx),
|
||||||
|
&["v root1", " .dockerignore"],
|
||||||
|
"Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file"
|
||||||
|
);
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(
|
||||||
|
!panel.filename_editor.read(cx).is_focused(cx),
|
||||||
|
"Should have closed the file name editor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let notifications = workspace.notification_ids();
|
||||||
|
assert_eq!(
|
||||||
|
notifications.len(),
|
||||||
|
1,
|
||||||
|
"Should receive one notification with the error message"
|
||||||
|
);
|
||||||
|
workspace.dismiss_notification(notifications.first().unwrap(), cx);
|
||||||
|
assert!(workspace.notification_ids().is_empty());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
select_path(&panel, "root1", cx);
|
||||||
|
panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(panel.filename_editor.read(cx).is_focused(cx));
|
||||||
|
});
|
||||||
|
panel
|
||||||
|
.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.filename_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(excluded_dir_path, cx));
|
||||||
|
panel.confirm_edit(cx).unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..13, cx),
|
||||||
|
&["v root1", " .dockerignore"],
|
||||||
|
"Should not change the project panel after trying to create an excluded directory"
|
||||||
|
);
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
assert!(
|
||||||
|
!panel.filename_editor.read(cx).is_focused(cx),
|
||||||
|
"Should have closed the file name editor"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let notifications = workspace.notification_ids();
|
||||||
|
assert_eq!(
|
||||||
|
notifications.len(),
|
||||||
|
1,
|
||||||
|
"Should receive one notification explaining that no directory is actually shown"
|
||||||
|
);
|
||||||
|
workspace.dismiss_notification(notifications.first().unwrap(), cx);
|
||||||
|
assert!(workspace.notification_ids().is_empty());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
fs.is_dir(Path::new("/root1/excluded_dir")).await,
|
||||||
|
"Should have created the excluded directory"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn toggle_expand_dir(
|
fn toggle_expand_dir(
|
||||||
panel: &View<ProjectPanel>,
|
panel: &View<ProjectPanel>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
@ -4716,4 +4950,68 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestProjectItemView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
path: ProjectPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestProjectItem {
|
||||||
|
path: ProjectPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl project::Item for TestProjectItem {
|
||||||
|
fn try_open(
|
||||||
|
_project: &Model<Project>,
|
||||||
|
path: &ProjectPath,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<gpui::Result<Model<Self>>>> {
|
||||||
|
let path = path.clone();
|
||||||
|
Some(cx.spawn(|mut cx| async move { cx.new_model(|_| Self { path }) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||||
|
Some(self.path.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectItem for TestProjectItemView {
|
||||||
|
type Item = TestProjectItem;
|
||||||
|
|
||||||
|
fn for_project_item(
|
||||||
|
_: Model<Project>,
|
||||||
|
project_item: Model<Self::Item>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
path: project_item.update(cx, |project_item, _| project_item.path.clone()),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for TestProjectItemView {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<()> for TestProjectItemView {}
|
||||||
|
|
||||||
|
impl FocusableView for TestProjectItemView {
|
||||||
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TestProjectItemView {
|
||||||
|
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1309,6 +1309,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
(wt, entry)
|
(wt, entry)
|
||||||
|
@ -122,6 +122,15 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn notification_ids(&self) -> Vec<NotificationId> {
|
||||||
|
self.notifications
|
||||||
|
.iter()
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show_notification<V: Notification>(
|
pub fn show_notification<V: Notification>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: NotificationId,
|
id: NotificationId,
|
||||||
@ -144,7 +153,7 @@ impl Workspace {
|
|||||||
|
|
||||||
pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
|
pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
|
||||||
where
|
where
|
||||||
E: std::fmt::Debug,
|
E: std::fmt::Debug + std::fmt::Display,
|
||||||
{
|
{
|
||||||
struct WorkspaceErrorNotification;
|
struct WorkspaceErrorNotification;
|
||||||
|
|
||||||
@ -153,7 +162,7 @@ impl Workspace {
|
|||||||
cx,
|
cx,
|
||||||
|cx| {
|
|cx| {
|
||||||
cx.new_view(|_cx| {
|
cx.new_view(|_cx| {
|
||||||
simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
|
simple_message_notification::MessageNotification::new(format!("Error: {err:#}"))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -464,7 +473,7 @@ pub trait NotifyResultExt {
|
|||||||
|
|
||||||
impl<T, E> NotifyResultExt for Result<T, E>
|
impl<T, E> NotifyResultExt for Result<T, E>
|
||||||
where
|
where
|
||||||
E: std::fmt::Debug,
|
E: std::fmt::Debug + std::fmt::Display,
|
||||||
{
|
{
|
||||||
type Ok = T;
|
type Ok = T;
|
||||||
|
|
||||||
@ -483,7 +492,7 @@ where
|
|||||||
match self {
|
match self {
|
||||||
Ok(value) => Some(value),
|
Ok(value) => Some(value),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("TODO {err:?}");
|
log::error!("{err:?}");
|
||||||
cx.update_root(|view, cx| {
|
cx.update_root(|view, cx| {
|
||||||
if let Ok(workspace) = view.downcast::<Workspace>() {
|
if let Ok(workspace) = view.downcast::<Workspace>() {
|
||||||
workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
|
workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
|
||||||
@ -502,7 +511,7 @@ pub trait NotifyTaskExt {
|
|||||||
|
|
||||||
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
|
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
|
||||||
where
|
where
|
||||||
E: std::fmt::Debug + Sized + 'static,
|
E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
|
||||||
R: 'static,
|
R: 'static,
|
||||||
{
|
{
|
||||||
fn detach_and_notify_err(self, cx: &mut WindowContext) {
|
fn detach_and_notify_err(self, cx: &mut WindowContext) {
|
||||||
|
@ -97,6 +97,25 @@ pub enum Worktree {
|
|||||||
Remote(RemoteWorktree),
|
Remote(RemoteWorktree),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An entry, created in the worktree.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CreatedEntry {
|
||||||
|
/// Got created and indexed by the worktree, receiving a corresponding entry.
|
||||||
|
Included(Entry),
|
||||||
|
/// Got created, but not indexed due to falling under exclusion filters.
|
||||||
|
Excluded { abs_path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
impl CreatedEntry {
|
||||||
|
pub fn to_included(self) -> Option<Entry> {
|
||||||
|
match self {
|
||||||
|
CreatedEntry::Included(entry) => Some(entry),
|
||||||
|
CreatedEntry::Excluded { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LocalWorktree {
|
pub struct LocalWorktree {
|
||||||
snapshot: LocalSnapshot,
|
snapshot: LocalSnapshot,
|
||||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||||
@ -1322,22 +1341,34 @@ impl LocalWorktree {
|
|||||||
path: impl Into<Arc<Path>>,
|
path: impl Into<Arc<Path>>,
|
||||||
is_dir: bool,
|
is_dir: bool,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Task<Result<Option<Entry>>> {
|
) -> Task<Result<CreatedEntry>> {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
let lowest_ancestor = self.lowest_ancestor(&path);
|
let abs_path = match self.absolutize(&path) {
|
||||||
let abs_path = self.absolutize(&path);
|
Ok(path) => path,
|
||||||
|
Err(e) => return Task::ready(Err(e.context(format!("absolutizing path {path:?}")))),
|
||||||
|
};
|
||||||
|
let path_excluded = self.is_path_excluded(&abs_path);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
let task_abs_path = abs_path.clone();
|
||||||
let write = cx.background_executor().spawn(async move {
|
let write = cx.background_executor().spawn(async move {
|
||||||
if is_dir {
|
if is_dir {
|
||||||
fs.create_dir(&abs_path?).await
|
fs.create_dir(&task_abs_path)
|
||||||
} else {
|
|
||||||
fs.save(&abs_path?, &Default::default(), Default::default())
|
|
||||||
.await
|
.await
|
||||||
|
.with_context(|| format!("creating directory {task_abs_path:?}"))
|
||||||
|
} else {
|
||||||
|
fs.save(&task_abs_path, &Rope::default(), LineEnding::default())
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("creating file {task_abs_path:?}"))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let lowest_ancestor = self.lowest_ancestor(&path);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
write.await?;
|
write.await?;
|
||||||
|
if path_excluded {
|
||||||
|
return Ok(CreatedEntry::Excluded { abs_path });
|
||||||
|
}
|
||||||
|
|
||||||
let (result, refreshes) = this.update(&mut cx, |this, cx| {
|
let (result, refreshes) = this.update(&mut cx, |this, cx| {
|
||||||
let mut refreshes = Vec::new();
|
let mut refreshes = Vec::new();
|
||||||
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
|
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
|
||||||
@ -1362,7 +1393,10 @@ impl LocalWorktree {
|
|||||||
refresh.await.log_err();
|
refresh.await.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
result.await
|
Ok(result
|
||||||
|
.await?
|
||||||
|
.map(CreatedEntry::Included)
|
||||||
|
.unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1448,19 +1482,22 @@ impl LocalWorktree {
|
|||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
new_path: impl Into<Arc<Path>>,
|
new_path: impl Into<Arc<Path>>,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Task<Result<Option<Entry>>> {
|
) -> Task<Result<CreatedEntry>> {
|
||||||
let old_path = match self.entry_for_id(entry_id) {
|
let old_path = match self.entry_for_id(entry_id) {
|
||||||
Some(entry) => entry.path.clone(),
|
Some(entry) => entry.path.clone(),
|
||||||
None => return Task::ready(Ok(None)),
|
None => return Task::ready(Err(anyhow!("no entry to rename for id {entry_id:?}"))),
|
||||||
};
|
};
|
||||||
let new_path = new_path.into();
|
let new_path = new_path.into();
|
||||||
let abs_old_path = self.absolutize(&old_path);
|
let abs_old_path = self.absolutize(&old_path);
|
||||||
let abs_new_path = self.absolutize(&new_path);
|
let Ok(abs_new_path) = self.absolutize(&new_path) else {
|
||||||
|
return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
|
||||||
|
};
|
||||||
|
let abs_path = abs_new_path.clone();
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let case_sensitive = self.fs_case_sensitive;
|
let case_sensitive = self.fs_case_sensitive;
|
||||||
let rename = cx.background_executor().spawn(async move {
|
let rename = cx.background_executor().spawn(async move {
|
||||||
let abs_old_path = abs_old_path?;
|
let abs_old_path = abs_old_path?;
|
||||||
let abs_new_path = abs_new_path?;
|
let abs_new_path = abs_new_path;
|
||||||
|
|
||||||
let abs_old_path_lower = abs_old_path.to_str().map(|p| p.to_lowercase());
|
let abs_old_path_lower = abs_old_path.to_str().map(|p| p.to_lowercase());
|
||||||
let abs_new_path_lower = abs_new_path.to_str().map(|p| p.to_lowercase());
|
let abs_new_path_lower = abs_new_path.to_str().map(|p| p.to_lowercase());
|
||||||
@ -1480,16 +1517,20 @@ impl LocalWorktree {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.with_context(|| format!("Renaming {abs_old_path:?} into {abs_new_path:?}"))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
rename.await?;
|
rename.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
Ok(this
|
||||||
this.as_local_mut()
|
.update(&mut cx, |this, cx| {
|
||||||
.unwrap()
|
this.as_local_mut()
|
||||||
.refresh_entry(new_path.clone(), Some(old_path), cx)
|
.unwrap()
|
||||||
})?
|
.refresh_entry(new_path.clone(), Some(old_path), cx)
|
||||||
.await
|
})?
|
||||||
|
.await?
|
||||||
|
.map(CreatedEntry::Included)
|
||||||
|
.unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1212,6 +1212,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(entry.is_dir());
|
assert!(entry.is_dir());
|
||||||
|
|
||||||
@ -1268,6 +1269,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(entry.is_file());
|
assert!(entry.is_file());
|
||||||
|
|
||||||
@ -1310,6 +1312,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(entry.is_file());
|
assert!(entry.is_file());
|
||||||
|
|
||||||
@ -1329,6 +1332,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(entry.is_file());
|
assert!(entry.is_file());
|
||||||
|
|
||||||
@ -1346,6 +1350,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_included()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(entry.is_file());
|
assert!(entry.is_file());
|
||||||
|
|
||||||
@ -1673,7 +1678,7 @@ fn randomly_mutate_worktree(
|
|||||||
);
|
);
|
||||||
let task = worktree.rename_entry(entry.id, new_path, cx);
|
let task = worktree.rename_entry(entry.id, new_path, cx);
|
||||||
cx.background_executor().spawn(async move {
|
cx.background_executor().spawn(async move {
|
||||||
task.await?.unwrap();
|
task.await?.to_included().unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user