project_panel: Add warning error for leading or trailing whitespace when creating file or directory (#28215)

- Show yellow warning (instead or error) for leading/trailing
whitespace.
- Do not block user from creating it.
- If you rename existing file/dir which contains leading/trailing
whitespace, it will show error right away.

<img width="250" alt="image"
src="https://github.com/user-attachments/assets/562895ee-3a86-4ecd-bb38-703d1d8b8599"
/>

Release Notes:

- Added warning for leading or trailing whitespace while renaming or
creating new file or directory in Project Panel.
This commit is contained in:
Smit Barmase 2025-04-07 17:47:54 +05:30 committed by GitHub
parent 3b46fca64c
commit 956f359045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -119,6 +119,13 @@ struct FoldedDirectoryDragTarget {
is_delimiter_target: bool, is_delimiter_target: bool,
} }
#[derive(Clone, Debug)]
enum ValidationState {
None,
Warning(String),
Error(String),
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct EditState { struct EditState {
worktree_id: WorktreeId, worktree_id: WorktreeId,
@ -128,7 +135,7 @@ struct EditState {
depth: usize, depth: usize,
processing_filename: Option<String>, processing_filename: Option<String>,
previously_focused: Option<SelectedEntry>, previously_focused: Option<SelectedEntry>,
validation_error: bool, validation_state: ValidationState,
} }
impl EditState { impl EditState {
@ -1143,7 +1150,9 @@ impl ProjectPanel {
Some(state) => state, Some(state) => state,
None => return, None => return,
}; };
let filename = self.filename_editor.read(cx).text(cx); let filename = self.filename_editor.read(cx).text(cx);
if !filename.is_empty() { if !filename.is_empty() {
if let Some(worktree) = self if let Some(worktree) = self
.project .project
@ -1158,7 +1167,10 @@ impl ProjectPanel {
.entry_for_path(new_path.as_path()) .entry_for_path(new_path.as_path())
.is_some() .is_some()
{ {
edit_state.validation_error = true; edit_state.validation_state = ValidationState::Error(format!(
"File or directory '{}' already exists at location. Please choose a different name.",
filename
));
cx.notify(); cx.notify();
return; return;
} }
@ -1171,7 +1183,9 @@ impl ProjectPanel {
if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path()) if let Some(existing) = worktree.read(cx).entry_for_path(new_path.as_path())
{ {
if existing.id != entry.id { if existing.id != entry.id {
edit_state.validation_error = true; edit_state.validation_state = ValidationState::Error(
"File or directory already exists".to_string(),
);
cx.notify(); cx.notify();
return; return;
} }
@ -1179,8 +1193,17 @@ impl ProjectPanel {
}; };
} }
} }
if filename.trim() != filename {
edit_state.validation_state = ValidationState::Warning(
"File or directory name contains leading or trailing whitespace.".to_string(),
);
cx.notify();
return;
} }
edit_state.validation_error = false; }
edit_state.validation_state = ValidationState::None;
cx.notify(); cx.notify();
} }
@ -1403,7 +1426,7 @@ impl ProjectPanel {
processing_filename: None, processing_filename: None,
previously_focused: self.selection, previously_focused: self.selection,
depth: 0, depth: 0,
validation_error: false, validation_state: ValidationState::None,
}); });
self.filename_editor.update(cx, |editor, cx| { self.filename_editor.update(cx, |editor, cx| {
editor.clear(window, cx); editor.clear(window, cx);
@ -1453,7 +1476,7 @@ impl ProjectPanel {
processing_filename: None, processing_filename: None,
previously_focused: None, previously_focused: None,
depth: 0, depth: 0,
validation_error: false, validation_state: ValidationState::None,
}); });
let file_name = entry let file_name = entry
.path .path
@ -3687,15 +3710,25 @@ impl ProjectPanel {
item_colors.hover item_colors.hover
}; };
let validation_error = let validation_color_and_message = if show_editor {
show_editor && self.edit_state.as_ref().is_some_and(|e| e.validation_error); match self
.edit_state
.as_ref()
.map_or(ValidationState::None, |e| e.validation_state.clone())
{
ValidationState::Error(msg) => Some((Color::Error.color(cx), msg.clone())),
ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg.clone())),
ValidationState::None => None,
}
} else {
None
};
let border_color = let border_color =
if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
if validation_error { match validation_color_and_message {
Color::Error.color(cx) Some((color, _)) => color,
} else { None => item_colors.focused,
item_colors.focused
} }
} else { } else {
bg_color bg_color
@ -3703,10 +3736,9 @@ impl ProjectPanel {
let border_hover_color = let border_hover_color =
if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) {
if validation_error { match validation_color_and_message {
Color::Error.color(cx) Some((color, _)) => color,
} else { None => item_colors.focused,
item_colors.focused
} }
} else { } else {
bg_hover_color bg_hover_color
@ -4177,8 +4209,9 @@ impl ProjectPanel {
)) ))
.overflow_x(), .overflow_x(),
) )
.when( .when_some(
validation_error, |this| { validation_color_and_message,
|this, (color, message)| {
this this
.relative() .relative()
.child( .child(
@ -4192,13 +4225,12 @@ impl ProjectPanel {
.py_1() .py_1()
.px_2() .px_2()
.border_1() .border_1()
.border_color(Color::Error.color(cx)) .border_color(color)
.bg(cx.theme().colors().background) .bg(cx.theme().colors().background)
.child( .child(
Label::new(format!("{} already exists", self.filename_editor.read(cx).text(cx))) Label::new(message)
.color(Color::Error) .color(Color::from(color))
.size(LabelSize::Small) .size(LabelSize::Small)
.truncate()
) )
) )
) )