project_panel: Show error when file or directory already exists while renaming or creating new one (#28177)
Closes #14425 <img width="289" alt="image" src="https://github.com/user-attachments/assets/2994c401-23e3-419a-90fc-1a83959fdf21" /> Release Notes: - Improved the project panel to show an error when a file or directory already exists while renaming or creating a new one.
This commit is contained in:
parent
8cfb9beb17
commit
d6d9c383cb
2 changed files with 143 additions and 16 deletions
|
@ -22,7 +22,7 @@ use gpui::{
|
|||
Hsla, InteractiveElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
|
||||
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy,
|
||||
Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions,
|
||||
anchored, deferred, div, impl_actions, point, px, size, uniform_list,
|
||||
anchored, deferred, div, impl_actions, point, px, size, transparent_black, uniform_list,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use language::DiagnosticSeverity;
|
||||
|
@ -53,9 +53,9 @@ use std::{
|
|||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors,
|
||||
IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*, v_flex,
|
||||
Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors,
|
||||
IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar,
|
||||
ScrollbarState, Tooltip, prelude::*, v_flex,
|
||||
};
|
||||
use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
|
||||
use workspace::{
|
||||
|
@ -128,6 +128,7 @@ struct EditState {
|
|||
depth: usize,
|
||||
processing_filename: Option<String>,
|
||||
previously_focused: Option<SelectedEntry>,
|
||||
validation_error: bool,
|
||||
}
|
||||
|
||||
impl EditState {
|
||||
|
@ -408,7 +409,11 @@ impl ProjectPanel {
|
|||
cx.subscribe(
|
||||
&filename_editor,
|
||||
|project_panel, _, editor_event, cx| match editor_event {
|
||||
EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => {
|
||||
EditorEvent::BufferEdited => {
|
||||
project_panel.populate_validation_error(cx);
|
||||
project_panel.autoscroll(cx);
|
||||
}
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
project_panel.autoscroll(cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
|
@ -1133,14 +1138,58 @@ impl ProjectPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn populate_validation_error(&mut self, cx: &mut Context<Self>) {
|
||||
let edit_state = match self.edit_state.as_mut() {
|
||||
Some(state) => state,
|
||||
None => return,
|
||||
};
|
||||
let filename = self.filename_editor.read(cx).text(cx);
|
||||
if !filename.is_empty() {
|
||||
if let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(edit_state.worktree_id, cx)
|
||||
{
|
||||
if let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id) {
|
||||
if edit_state.is_new_entry() {
|
||||
let new_path = entry.path.join(filename.trim_start_matches('/'));
|
||||
if worktree
|
||||
.read(cx)
|
||||
.entry_for_path(new_path.as_path())
|
||||
.is_some()
|
||||
{
|
||||
edit_state.validation_error = true;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let new_path = if let Some(parent) = entry.path.clone().parent() {
|
||||
parent.join(&filename)
|
||||
} else {
|
||||
filename.clone().into()
|
||||
};
|
||||
if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path())
|
||||
{
|
||||
if existing.id != entry.id {
|
||||
edit_state.validation_error = true;
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
edit_state.validation_error = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm_edit(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let edit_state = self.edit_state.as_mut()?;
|
||||
window.focus(&self.focus_handle);
|
||||
|
||||
let worktree_id = edit_state.worktree_id;
|
||||
let is_new_entry = edit_state.is_new_entry();
|
||||
let filename = self.filename_editor.read(cx).text(cx);
|
||||
|
@ -1155,7 +1204,6 @@ impl ProjectPanel {
|
|||
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
|
||||
|
||||
let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
|
||||
let edit_task;
|
||||
let edited_entry_id;
|
||||
if is_new_entry {
|
||||
|
@ -1164,7 +1212,11 @@ impl ProjectPanel {
|
|||
entry_id: NEW_ENTRY_ID,
|
||||
});
|
||||
let new_path = entry.path.join(filename.trim_start_matches('/'));
|
||||
if path_already_exists(new_path.as_path()) {
|
||||
if worktree
|
||||
.read(cx)
|
||||
.entry_for_path(new_path.as_path())
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -1178,7 +1230,10 @@ impl ProjectPanel {
|
|||
} else {
|
||||
filename.clone().into()
|
||||
};
|
||||
if path_already_exists(new_path.as_path()) {
|
||||
if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) {
|
||||
if existing.id == entry.id {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
edited_entry_id = entry.id;
|
||||
|
@ -1187,6 +1242,7 @@ impl ProjectPanel {
|
|||
});
|
||||
};
|
||||
|
||||
window.focus(&self.focus_handle);
|
||||
edit_state.processing_filename = Some(filename);
|
||||
cx.notify();
|
||||
|
||||
|
@ -1347,6 +1403,7 @@ impl ProjectPanel {
|
|||
processing_filename: None,
|
||||
previously_focused: self.selection,
|
||||
depth: 0,
|
||||
validation_error: false,
|
||||
});
|
||||
self.filename_editor.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
|
@ -1396,6 +1453,7 @@ impl ProjectPanel {
|
|||
processing_filename: None,
|
||||
previously_focused: None,
|
||||
depth: 0,
|
||||
validation_error: false,
|
||||
});
|
||||
let file_name = entry
|
||||
.path
|
||||
|
@ -3629,16 +3687,27 @@ impl ProjectPanel {
|
|||
item_colors.hover
|
||||
};
|
||||
|
||||
let validation_error =
|
||||
show_editor && self.edit_state.as_ref().is_some_and(|e| e.validation_error);
|
||||
|
||||
let border_color =
|
||||
if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
|
||||
item_colors.focused
|
||||
if validation_error {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
item_colors.focused
|
||||
}
|
||||
} else {
|
||||
bg_color
|
||||
};
|
||||
|
||||
let border_hover_color =
|
||||
if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
|
||||
item_colors.focused
|
||||
if validation_error {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
item_colors.focused
|
||||
}
|
||||
} else {
|
||||
bg_hover_color
|
||||
};
|
||||
|
@ -4108,6 +4177,40 @@ impl ProjectPanel {
|
|||
))
|
||||
.overflow_x(),
|
||||
)
|
||||
.when(
|
||||
|
||||
validation_error, |el| {
|
||||
el
|
||||
.relative()
|
||||
.child(
|
||||
deferred(
|
||||
// Wizardry of highest order to make error border align with entry border
|
||||
div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.top_full()
|
||||
.left_neg_0p5()
|
||||
.right_neg_1()
|
||||
.border_x_1()
|
||||
.border_color(transparent_black())
|
||||
.child(
|
||||
div()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.border_1()
|
||||
.border_color(Color::Error.color(cx))
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
Label::new(format!("{} already exists", self.filename_editor.read(cx).text(cx)))
|
||||
.color(Color::Error)
|
||||
.size(LabelSize::Small)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
|
|
|
@ -1835,13 +1835,21 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
|
|||
assert!(
|
||||
panel.confirm_edit(window, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new directory name"
|
||||
)
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting new directory name"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
//
|
||||
"v src",
|
||||
"v src <== selected",
|
||||
" > test"
|
||||
],
|
||||
"File list should be unchanged after failed folder create confirmation"
|
||||
|
@ -1880,13 +1888,21 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
|
|||
assert!(
|
||||
panel.confirm_edit(window, cx).is_none(),
|
||||
"Should not allow to confirm on conflicting new file name"
|
||||
)
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting new file name"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" v test <== selected",
|
||||
" first.rs",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
|
@ -1930,6 +1946,14 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
|
|||
"Should not allow to confirm on conflicting file rename"
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
assert!(
|
||||
panel.edit_state.is_some(),
|
||||
"Edit state should not be None after conflicting file rename"
|
||||
);
|
||||
panel.cancel(&menu::Cancel, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue