diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1378022bcf..b7d390062f 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -331,7 +331,8 @@ "context": "ProjectPanel", "bindings": { "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry" + "right": "project_panel::ExpandSelectedEntry", + "f2": "project_panel::Rename" } } ] \ No newline at end of file diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index 0dde598727..3b19c2e63e 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#26232a5c", + "text": { + "family": "Zed Mono", + "color": "#e2dfe7", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + } } }, "chat_panel": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 495371914a..2e33fb774f 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#e2dfe72e", + "text": { + "family": "Zed Mono", + "color": "#26232a", + "size": 14 + }, + "selection": { + "cursor": "#576ddb", + "selection": "#576ddb3d" + } } }, "chat_panel": { diff --git a/assets/themes/dark.json b/assets/themes/dark.json index 528e3ca91f..ba9b7189d3 100644 --- a/assets/themes/dark.json +++ b/assets/themes/dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#ffffff1f", + "text": { + "family": "Zed Mono", + "color": "#f1f1f1", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + } } }, "chat_panel": { diff --git a/assets/themes/light.json b/assets/themes/light.json index 240b2627c8..7cbd315c8a 100644 --- a/assets/themes/light.json +++ b/assets/themes/light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#0000000f", + "text": { + "family": "Zed Mono", + "color": "#2b2b2b", + "size": 14 + }, + "selection": { + "cursor": "#2472f2", + "selection": "#2472f23d" + } } }, "chat_panel": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 52c3e753c2..8672518b4c 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#0736425c", + "text": { + "family": "Zed Mono", + "color": "#eee8d5", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + } } }, "chat_panel": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index f11c1515a3..66b43e613d 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#eee8d52e", + "text": { + "family": "Zed Mono", + "color": "#073642", + "size": 14 + }, + "selection": { + "cursor": "#268bd2", + "selection": "#268bd23d" + } } }, "chat_panel": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index 023dc64ddd..66f5182172 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#2932565c", + "text": { + "family": "Zed Mono", + "color": "#dfe2f1", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + } } }, "chat_panel": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 38679f2a96..34a3389728 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -972,6 +972,18 @@ "size": 14 } } + }, + "filename_editor": { + "background": "#dfe2f12e", + "text": { + "family": "Zed Mono", + "color": "#293256", + "size": 14 + }, + "selection": { + "cursor": "#3d8fd1", + "selection": "#3d8fd13d" + } } }, "chat_panel": { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 28ef9e5af0..f4453774f7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -875,6 +875,12 @@ impl Element for EditorElement { .max(constraint.min_along(Axis::Vertical)) .min(line_height * max_lines as f32), ) + } else if let EditorMode::SingleLine = snapshot.mode { + size.set_y( + line_height + .min(constraint.max_along(Axis::Vertical)) + .max(constraint.min_along(Axis::Vertical)), + ) } else if size.y().is_infinite() { size.set_y(scroll_height); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f8e25ff2cc..891f963e1d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -225,6 +225,8 @@ impl DiagnosticSummary { pub struct ProjectEntryId(usize); impl ProjectEntryId { + pub const MAX: Self = Self(usize::MAX); + pub fn new(counter: &AtomicUsize) -> Self { Self(counter.fetch_add(1, SeqCst)) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 812783aaaf..7651e332da 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1568,7 +1568,7 @@ pub struct Entry { pub is_ignored: bool, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { PendingDir, Dir, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 37f14d7962..d038ac941f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -11,7 +11,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Entry, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use settings::Settings; use std::{ cmp::Ordering, @@ -25,6 +25,8 @@ use workspace::{ Workspace, }; +const NEW_FILE_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + pub struct ProjectPanel { project: ModelHandle, list: UniformListState, @@ -56,14 +58,7 @@ struct EntryDetails { kind: EntryKind, is_expanded: bool, is_selected: bool, -} - -#[derive(Debug, PartialEq, Eq)] -enum EntryKind { - File, - Dir, - FileRenameEditor, - NewFileEditor, + is_editing: bool, } #[derive(Clone)] @@ -122,8 +117,17 @@ impl ProjectPanel { }) .detach(); - let editor = cx.add_view(|cx| Editor::single_line(None, cx)); - cx.subscribe(&editor, |this, _, event, cx| { + let filename_editor = cx.add_view(|cx| { + Editor::single_line( + Some(|theme| { + let mut style = theme.project_panel.filename_editor.clone(); + style.container.background_color.take(); + style + }), + cx, + ) + }); + cx.subscribe(&filename_editor, |this, _, event, cx| { if let editor::Event::Blurred = event { this.editor_blurred(cx); } @@ -137,7 +141,7 @@ impl ProjectPanel { expanded_dir_ids: Default::default(), selection: None, edit_state: None, - filename_editor: editor, + filename_editor, handle: cx.weak_handle(), }; this.update_visible_entries(None, cx); @@ -399,8 +403,10 @@ impl ProjectPanel { .path .file_name() .map_or(String::new(), |s| s.to_string_lossy().to_string()); - self.filename_editor - .update(cx, |editor, cx| editor.set_text(filename, cx)); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(filename, cx); + editor.select_all(&Default::default(), cx); + }); cx.focus(&self.filename_editor); self.update_visible_entries(None, cx); cx.notify(); @@ -542,7 +548,7 @@ impl ProjectPanel { visible_worktree_entries.push(entry.clone()); if Some(entry.id) == new_file_parent_id { visible_worktree_entries.push(Entry { - id: entry.id, + id: NEW_FILE_ENTRY_ID, kind: project::EntryKind::File(Default::default()), path: entry.path.join("\0").into(), inode: 0, @@ -662,30 +668,24 @@ impl ProjectPanel { .to_string_lossy() .to_string(), depth: entry.path.components().count(), - kind: if entry.is_dir() { - EntryKind::Dir - } else { - EntryKind::File - }, + kind: entry.kind, is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), is_selected: self.selection.map_or(false, |e| { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), + is_editing: false, }; if let Some(edit_state) = self.edit_state { - if edit_state.worktree_id == *worktree_id && edit_state.entry_id == entry.id - { - if edit_state.new_file { - if entry.is_file() { - details.kind = EntryKind::NewFileEditor; - details.filename = Default::default(); - details.is_expanded = false; - details.is_selected = false; - } - } else { - details.kind = EntryKind::FileRenameEditor; + if edit_state.new_file { + if entry.id == NEW_FILE_ENTRY_ID { + details.is_editing = true; + details.filename.clear(); } - } + } else { + if entry.id == edit_state.entry_id { + details.is_editing = true; + } + }; } callback(entry.id, details, cx); } @@ -702,21 +702,14 @@ impl ProjectPanel { cx: &mut ViewContext, ) -> ElementBox { let kind = details.kind; - let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; - - if kind == EntryKind::FileRenameEditor || kind == EntryKind::NewFileEditor { - return ChildView::new(editor.clone()) - .constrained() - .with_height(theme.entry.default.height) - .contained() - .with_margin_left( - padding + theme.entry.default.icon_spacing + theme.entry.default.icon_size, - ) - .boxed(); - } - MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { + let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; let style = theme.entry.style_for(state, details.is_selected); + let row_container_style = if details.is_editing { + theme.filename_editor.container + } else { + style.container + }; Flex::row() .with_child( ConstrainedBox::new(if kind == EntryKind::Dir { @@ -739,18 +732,26 @@ impl ProjectPanel { .with_width(style.icon_size) .boxed(), ) - .with_child( + .with_child(if details.is_editing { + ChildView::new(editor.clone()) + .contained() + .with_margin_left(theme.entry.default.icon_spacing) + .aligned() + .left() + .flex(1.0, true) + .boxed() + } else { Label::new(details.filename, style.text.clone()) .contained() .with_margin_left(style.icon_spacing) .aligned() .left() - .boxed(), - ) + .boxed() + }) .constrained() .with_height(theme.entry.default.height) .contained() - .with_style(style.container) + .with_style(row_container_style) .with_padding_left(padding) .boxed() }) @@ -871,168 +872,43 @@ mod tests { let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx)); assert_eq!( - visible_entry_details(&panel, 0..50, cx), + visible_entries_as_strings(&panel, 0..50, cx), &[ - EntryDetails { - filename: "root1".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - ], + "v root1", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] ); toggle_expand_dir(&panel, "root1/b", cx); assert_eq!( - visible_entry_details(&panel, 0..50, cx), + visible_entries_as_strings(&panel, 0..50, cx), &[ - EntryDetails { - filename: "root1".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false, - }, - EntryDetails { - filename: "a".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "b".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: true, - }, - EntryDetails { - filename: "3".to_string(), - depth: 2, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "4".to_string(), - depth: 2, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false, - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, - EntryDetails { - filename: "d".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "e".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, + "v root1", + " > a", + " v b <== selected", + " > 3", + " > 4", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", ] ); assert_eq!( - visible_entry_details(&panel, 5..8, cx), - [ - EntryDetails { - filename: "C".to_string(), - depth: 1, - kind: EntryKind::Dir, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: ".dockerignore".to_string(), - depth: 1, - kind: EntryKind::File, - is_expanded: false, - is_selected: false - }, - EntryDetails { - filename: "root2".to_string(), - depth: 0, - kind: EntryKind::Dir, - is_expanded: true, - is_selected: false - }, + visible_entries_as_strings(&panel, 5..8, cx), + &[ + // + " > C", + " .dockerignore", + "v root2", ] ); } @@ -1109,7 +985,7 @@ mod tests { " > a", " > b", " > C", - " [NEW FILE EDITOR]", + " [EDITOR: '']", " .dockerignore", "v root2", " > d", @@ -1151,7 +1027,7 @@ mod tests { " v b <== selected", " > 3", " > 4", - " [NEW FILE EDITOR]", + " [EDITOR: '']", " > C", " .dockerignore", " the-new-filename", @@ -1192,7 +1068,7 @@ mod tests { " v b", " > 3", " > 4", - " [RENAME EDITOR] <== selected", + " [EDITOR: 'another-filename'] <== selected", " > C", " .dockerignore", " the-new-filename", @@ -1265,19 +1141,17 @@ mod tests { }); } - fn visible_entry_details( + fn visible_entries_as_strings( panel: &ViewHandle, range: Range, cx: &mut TestAppContext, - ) -> Vec { + ) -> Vec { let mut result = Vec::new(); let mut project_entries = HashSet::new(); let mut has_editor = false; panel.update(cx, |panel, cx| { panel.for_each_visible_entry(range, cx, |project_entry, details, _| { - if details.kind == EntryKind::NewFileEditor - || details.kind == EntryKind::FileRenameEditor - { + if details.is_editing { assert!(!has_editor, "duplicate editor entry"); has_editor = true; } else { @@ -1288,21 +1162,7 @@ mod tests { details ); } - result.push(details) - }); - }); - result - } - - fn visible_entries_as_strings( - panel: &ViewHandle, - range: Range, - cx: &mut TestAppContext, - ) -> Vec { - visible_entry_details(panel, range, cx) - .into_iter() - .map(|details| { let indent = " ".repeat(details.depth); let icon = if details.kind == EntryKind::Dir { if details.is_expanded { @@ -1313,10 +1173,9 @@ mod tests { } else { " " }; - let name = if details.kind == EntryKind::FileRenameEditor { - "[RENAME EDITOR]" - } else if details.kind == EntryKind::NewFileEditor { - "[NEW FILE EDITOR]" + let editor_text = format!("[EDITOR: '{}']", details.filename); + let name = if details.is_editing { + &editor_text } else { &details.filename }; @@ -1325,8 +1184,10 @@ mod tests { } else { "" }; - format!("{indent}{icon}{name}{selected}") - }) - .collect() + result.push(format!("{indent}{icon}{name}{selected}")); + }); + }); + + result } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c2b1fc26a0..d64c093144 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -204,11 +204,12 @@ pub struct ChatPanel { pub hovered_sign_in_prompt: TextStyle, } -#[derive(Debug, Deserialize, Default)] +#[derive(Deserialize, Default)] pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, pub entry: Interactive, + pub filename_editor: FieldEditor, pub indent_width: f32, } diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 55a1e1b81c..bacc3590e5 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,6 +1,6 @@ import Theme from "../themes/theme"; import { panel } from "./app"; -import { backgroundColor, iconColor, text } from "./components"; +import { backgroundColor, iconColor, player, text } from "./components"; export default function projectPanel(theme: Theme) { return { @@ -26,5 +26,10 @@ export default function projectPanel(theme: Theme) { text: text(theme, "mono", "active", { size: "sm" }), } }, + filenameEditor: { + background: backgroundColor(theme, 500, "active"), + text: text(theme, "mono", "primary", { size: "sm" }), + selection: player(theme, 1).selection, + }, }; }