From 1a1451a94367a336550f8c832d4368cfd410e36c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 29 Nov 2023 22:04:41 -0500 Subject: [PATCH 01/28] Fix bug preventing spaces from being used in filename --- assets/keymaps/default.json | 7 ++++++- crates/project_panel/src/project_panel.rs | 14 +++++++++++++- crates/project_panel2/src/project_panel.rs | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index ef6a655bdc..2a8d19f882 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -530,12 +530,17 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "enter": "project_panel::Rename", - "space": "project_panel::Open", "backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" } }, + { + "context": "ProjectPanel && not_editing", + "bindings": { + "space": "project_panel::Open" + } + }, { "context": "CollabPanel && not_editing", "bindings": { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index eb124bfca2..875d4d4f83 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1627,9 +1627,21 @@ impl View for ProjectPanel { } } - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { + fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { Self::reset_to_default_keymap_context(keymap); keymap.add_identifier("menu"); + + if let Some(window) = cx.active_window() { + window.read_with(cx, |cx| { + let identifier = if self.filename_editor.is_focused(cx) { + "editing" + } else { + "not_editing" + }; + + keymap.add_identifier(identifier); + }); + } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index dc584d52ff..b9b50e0d7f 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -3011,3 +3011,21 @@ mod tests { .unwrap(); } } + +// TODO - implement this in the new keymap system +// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { +// Self::reset_to_default_keymap_context(keymap); +// keymap.add_identifier("menu"); + +// if let Some(window) = cx.active_window() { +// window.read_with(cx, |cx| { +// let identifier = if self.filename_editor.is_focused(cx) { +// "editing" +// } else { +// "not_editing" +// }; + +// keymap.add_identifier(identifier); +// }); +// } +// } From d9b8c07b1e2bac2185dd5b4a08e210a7fc2b9e56 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Nov 2023 16:03:22 +0100 Subject: [PATCH 02/28] Lift Send and Sync restriction for measurements --- crates/gpui2/src/elements/text.rs | 4 +-- crates/gpui2/src/elements/uniform_list.rs | 2 -- crates/gpui2/src/taffy.rs | 33 ++++++++++++++--------- crates/gpui2/src/window.rs | 4 +-- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index d57daca062..2f432bcb75 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -152,15 +152,13 @@ impl TextState { .to_pixels(font_size.into(), cx.rem_size()); let text = SharedString::from(text); - let rem_size = cx.rem_size(); - let runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] }; - let layout_id = cx.request_measured_layout(Default::default(), rem_size, { + let layout_id = cx.request_measured_layout(Default::default(), { let element_state = self.clone(); move |known_dimensions, available_space| { diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 2d5a46f3d9..ed29caa43d 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -109,7 +109,6 @@ impl Element for UniformList { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let max_items = self.item_count; - let rem_size = cx.rem_size(); let item_size = state .as_ref() .map(|s| s.item_size) @@ -120,7 +119,6 @@ impl Element for UniformList { .layout(state.map(|s| s.interactive), cx, |style, cx| { cx.request_measured_layout( style, - rem_size, move |known_dimensions: Size>, available_space: Size| { let desired_height = item_size.height * max_items; diff --git a/crates/gpui2/src/taffy.rs b/crates/gpui2/src/taffy.rs index 81a057055a..7e25b5c5b2 100644 --- a/crates/gpui2/src/taffy.rs +++ b/crates/gpui2/src/taffy.rs @@ -9,13 +9,15 @@ use taffy::{ Taffy, }; -type Measureable = dyn Fn(Size>, Size) -> Size + Send + Sync; - pub struct TaffyLayoutEngine { - taffy: Taffy>, + taffy: Taffy, children_to_parents: HashMap, absolute_layout_bounds: HashMap>, computed_layouts: HashSet, + nodes_to_measure: HashMap< + LayoutId, + Box>, Size) -> Size>, + >, } static EXPECT_MESSAGE: &'static str = @@ -28,6 +30,7 @@ impl TaffyLayoutEngine { children_to_parents: HashMap::default(), absolute_layout_bounds: HashMap::default(), computed_layouts: HashSet::default(), + nodes_to_measure: HashMap::default(), } } @@ -36,6 +39,7 @@ impl TaffyLayoutEngine { self.children_to_parents.clear(); self.absolute_layout_bounds.clear(); self.computed_layouts.clear(); + self.nodes_to_measure.clear(); } pub fn request_layout( @@ -65,18 +69,17 @@ impl TaffyLayoutEngine { &mut self, style: Style, rem_size: Pixels, - measure: impl Fn(Size>, Size) -> Size - + Send - + Sync - + 'static, + measure: impl FnMut(Size>, Size) -> Size + 'static, ) -> LayoutId { let style = style.to_taffy(rem_size); - let measurable = Box::new(measure); - self.taffy - .new_leaf_with_context(style, measurable) + let layout_id = self + .taffy + .new_leaf_with_context(style, ()) .expect(EXPECT_MESSAGE) - .into() + .into(); + self.nodes_to_measure.insert(layout_id, Box::new(measure)); + layout_id } // Used to understand performance @@ -155,12 +158,14 @@ impl TaffyLayoutEngine { } // let started_at = std::time::Instant::now(); + dbg!(">>>>>>>>>>>>>"); + self.taffy .compute_layout_with_measure( id.into(), available_space.into(), - |known_dimensions, available_space, _node_id, context| { - let Some(measure) = context else { + |known_dimensions, available_space, node_id, _context| { + let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else { return taffy::geometry::Size::default(); }; @@ -173,6 +178,8 @@ impl TaffyLayoutEngine { }, ) .expect(EXPECT_MESSAGE); + + dbg!("<<<<<<"); // println!("compute_layout took {:?}", started_at.elapsed()); } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 76932f28e4..6f0d1381d0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -618,13 +618,13 @@ impl<'a> WindowContext<'a> { /// The given closure is invoked at layout time with the known dimensions and available space and /// returns a `Size`. pub fn request_measured_layout< - F: Fn(Size>, Size) -> Size + Send + Sync + 'static, + F: FnMut(Size>, Size) -> Size + 'static, >( &mut self, style: Style, - rem_size: Pixels, measure: F, ) -> LayoutId { + let rem_size = self.rem_size(); self.window .layout_engine .request_measured_layout(style, rem_size, measure) From 5243401afae05aea5f86f82c8ae28db092c72850 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Nov 2023 17:12:40 +0100 Subject: [PATCH 03/28] Pass a `WindowContext` in `request_measured_layout` Co-Authored-By: Nathan Sobo --- crates/gpui2/src/elements/text.rs | 6 ++--- crates/gpui2/src/elements/uniform_list.rs | 3 +-- crates/gpui2/src/taffy.rs | 28 ++++++++++++++++------- crates/gpui2/src/view.rs | 4 +--- crates/gpui2/src/window.rs | 27 ++++++++++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 2f432bcb75..490d0b61a1 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -144,7 +144,6 @@ impl TextState { runs: Option>, cx: &mut WindowContext, ) -> LayoutId { - let text_system = cx.text_system().clone(); let text_style = cx.text_style(); let font_size = text_style.font_size.to_pixels(cx.rem_size()); let line_height = text_style @@ -161,7 +160,7 @@ impl TextState { let layout_id = cx.request_measured_layout(Default::default(), { let element_state = self.clone(); - move |known_dimensions, available_space| { + move |known_dimensions, available_space, cx| { let wrap_width = if text_style.white_space == WhiteSpace::Normal { known_dimensions.width.or(match available_space.width { crate::AvailableSpace::Definite(x) => Some(x), @@ -179,7 +178,8 @@ impl TextState { } } - let Some(lines) = text_system + let Some(lines) = cx + .text_system() .shape_text( &text, font_size, &runs, wrap_width, // Wrap if we know the width. ) diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index ed29caa43d..d8f4cc6804 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -119,8 +119,7 @@ impl Element for UniformList { .layout(state.map(|s| s.interactive), cx, |style, cx| { cx.request_measured_layout( style, - move |known_dimensions: Size>, - available_space: Size| { + move |known_dimensions, available_space, _cx| { let desired_height = item_size.height * max_items; let width = known_dimensions diff --git a/crates/gpui2/src/taffy.rs b/crates/gpui2/src/taffy.rs index 7e25b5c5b2..2bceb1bc13 100644 --- a/crates/gpui2/src/taffy.rs +++ b/crates/gpui2/src/taffy.rs @@ -1,4 +1,7 @@ -use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style}; +use crate::{ + AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style, + WindowContext, +}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::fmt::Debug; @@ -16,7 +19,13 @@ pub struct TaffyLayoutEngine { computed_layouts: HashSet, nodes_to_measure: HashMap< LayoutId, - Box>, Size) -> Size>, + Box< + dyn FnMut( + Size>, + Size, + &mut WindowContext, + ) -> Size, + >, >, } @@ -69,7 +78,8 @@ impl TaffyLayoutEngine { &mut self, style: Style, rem_size: Pixels, - measure: impl FnMut(Size>, Size) -> Size + 'static, + measure: impl FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, ) -> LayoutId { let style = style.to_taffy(rem_size); @@ -129,7 +139,12 @@ impl TaffyLayoutEngine { Ok(edges) } - pub fn compute_layout(&mut self, id: LayoutId, available_space: Size) { + pub fn compute_layout( + &mut self, + id: LayoutId, + available_space: Size, + cx: &mut WindowContext, + ) { // Leaving this here until we have a better instrumentation approach. // println!("Laying out {} children", self.count_all_children(id)?); // println!("Max layout depth: {}", self.max_depth(0, id)?); @@ -158,8 +173,6 @@ impl TaffyLayoutEngine { } // let started_at = std::time::Instant::now(); - dbg!(">>>>>>>>>>>>>"); - self.taffy .compute_layout_with_measure( id.into(), @@ -174,12 +187,11 @@ impl TaffyLayoutEngine { height: known_dimensions.height.map(Pixels), }; - measure(known_dimensions, available_space.into()).into() + measure(known_dimensions, available_space.into(), cx).into() }, ) .expect(EXPECT_MESSAGE); - dbg!("<<<<<<"); // println!("compute_layout took {:?}", started_at.elapsed()); } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index f31b0ae753..6e1d59fc5a 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -209,9 +209,7 @@ impl AnyView { ) { cx.with_absolute_element_offset(origin, |cx| { let (layout_id, rendered_element) = (self.layout)(self, cx); - cx.window - .layout_engine - .compute_layout(layout_id, available_space); + cx.compute_layout(layout_id, available_space); (self.paint)(self, rendered_element, cx); }) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6f0d1381d0..d706787da8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -209,7 +209,7 @@ pub struct Window { sprite_atlas: Arc, rem_size: Pixels, viewport_size: Size, - pub(crate) layout_engine: TaffyLayoutEngine, + layout_engine: Option, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, pub(crate) previous_frame: Frame, @@ -327,7 +327,7 @@ impl Window { sprite_atlas, rem_size: px(16.), viewport_size: content_size, - layout_engine: TaffyLayoutEngine::new(), + layout_engine: Some(TaffyLayoutEngine::new()), root_view: None, element_id_stack: GlobalElementId::default(), previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())), @@ -606,9 +606,11 @@ impl<'a> WindowContext<'a> { self.app.layout_id_buffer.extend(children.into_iter()); let rem_size = self.rem_size(); - self.window - .layout_engine - .request_layout(style, rem_size, &self.app.layout_id_buffer) + self.window.layout_engine.as_mut().unwrap().request_layout( + style, + rem_size, + &self.app.layout_id_buffer, + ) } /// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children, @@ -618,7 +620,8 @@ impl<'a> WindowContext<'a> { /// The given closure is invoked at layout time with the known dimensions and available space and /// returns a `Size`. pub fn request_measured_layout< - F: FnMut(Size>, Size) -> Size + 'static, + F: FnMut(Size>, Size, &mut WindowContext) -> Size + + 'static, >( &mut self, style: Style, @@ -627,13 +630,15 @@ impl<'a> WindowContext<'a> { let rem_size = self.rem_size(); self.window .layout_engine + .as_mut() + .unwrap() .request_measured_layout(style, rem_size, measure) } pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { - self.window - .layout_engine - .compute_layout(layout_id, available_space) + let mut layout_engine = self.window.layout_engine.take().unwrap(); + layout_engine.compute_layout(layout_id, available_space, self); + self.window.layout_engine = Some(layout_engine); } /// Obtain the bounds computed for the given LayoutId relative to the window. This method should not @@ -643,6 +648,8 @@ impl<'a> WindowContext<'a> { let mut bounds = self .window .layout_engine + .as_mut() + .unwrap() .layout_bounds(layout_id) .map(Into::into); bounds.origin += self.element_offset(); @@ -1189,7 +1196,7 @@ impl<'a> WindowContext<'a> { self.text_system().start_frame(); let window = &mut *self.window; - window.layout_engine.clear(); + window.layout_engine.as_mut().unwrap().clear(); mem::swap(&mut window.previous_frame, &mut window.current_frame); let frame = &mut window.current_frame; From 81098312efa3acba7d637d83cb7c765a08e88b36 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 30 Nov 2023 11:20:51 -0500 Subject: [PATCH 04/28] Add some text to clarify no api key state Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- crates/assistant/src/assistant_panel.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index cac8bf6c54..e472e8c8df 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1218,6 +1218,31 @@ impl View for AssistantPanel { let style = &theme.assistant; if let Some(api_key_editor) = self.api_key_editor.as_ref() { Flex::column() + .with_child( + Text::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " - Having a subscription for another service like GitHub Copilot won't work.", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " - You can create a api key at: platform.openai.com/api-keys", + style.api_key_prompt.text.clone(), + ), + ) + .with_child( + Text::new( + " ", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) .with_child( Text::new( "Paste your OpenAI API key and press Enter to use the assistant", @@ -1231,6 +1256,20 @@ impl View for AssistantPanel { .with_style(style.api_key_editor.container) .aligned(), ) + .with_child( + Text::new( + " ", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) + .with_child( + Text::new( + "Click on the Z button in the status bar to close this panel.", + style.api_key_prompt.text.clone(), + ) + .aligned(), + ) .contained() .with_style(style.api_key_prompt.container) .aligned() From 0e1597d3853598befb17181d2e6cf3da88ef4116 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Nov 2023 18:00:41 +0100 Subject: [PATCH 05/28] WIP --- crates/editor2/src/editor.rs | 37 ++--- crates/editor2/src/element.rs | 135 +++++++++++++----- crates/storybook2/src/stories.rs | 2 + .../src/stories/auto_height_editor.rs | 34 +++++ crates/storybook2/src/story_selector.rs | 2 + 5 files changed, 155 insertions(+), 55 deletions(-) create mode 100644 crates/storybook2/src/stories/auto_height_editor.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 99d8a4bb93..14f21a774e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1734,21 +1734,11 @@ impl Editor { // Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) // } - // pub fn auto_height( - // max_lines: usize, - // field_editor_style: Option>, - // cx: &mut ViewContext, - // ) -> Self { - // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); - // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - // Self::new( - // EditorMode::AutoHeight { max_lines }, - // buffer, - // None, - // field_editor_style, - // cx, - // ) - // } + pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx) + } pub fn for_buffer( buffer: Model, @@ -2908,6 +2898,7 @@ impl Editor { } pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + dbg!("!!!!!!!!!!"); self.transact(cx, |this, cx| { let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { let selections = this.selections.all::(cx); @@ -8374,6 +8365,18 @@ impl Editor { cx.notify(); } + pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext) { + let rem_size = cx.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + self.style = Some(style); + } + pub fn set_wrap_width(&self, width: Option, cx: &mut AppContext) -> bool { self.display_map .update(cx, |map, cx| map.set_wrap_width(width, cx)) @@ -9397,7 +9400,7 @@ impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let settings = ThemeSettings::get_global(cx); let text_style = match self.mode { - EditorMode::SingleLine => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().text, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features, @@ -9410,8 +9413,6 @@ impl Render for Editor { white_space: WhiteSpace::Normal, }, - EditorMode::AutoHeight { max_lines } => todo!(), - EditorMode::Full => TextStyle { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 24402c7e37..cf48c3aa29 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,9 +20,9 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, + div, point, px, relative, size, transparent_black, Action, AnyElement, AsyncWindowContext, + AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, + ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement, IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, @@ -1662,11 +1662,6 @@ impl EditorElement { cx: &mut WindowContext, ) -> LayoutState { self.editor.update(cx, |editor, cx| { - // let mut size = constraint.max; - // if size.x.is_infinite() { - // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); - // } - let snapshot = editor.snapshot(cx); let style = self.style.clone(); @@ -1702,6 +1697,7 @@ impl EditorElement { }; editor.gutter_width = gutter_width; + let text_width = bounds.size.width - gutter_width; let overscroll = size(em_width, px(0.)); let snapshot = { @@ -1714,6 +1710,8 @@ impl EditorElement { SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), }; + dbg!(bounds.size.width, gutter_width, gutter_margin, overscroll.width, em_width, em_advance, wrap_width); + println!("setting wrap width during paint: {wrap_width:?}"); if editor.set_wrap_width(Some(wrap_width), cx) { editor.snapshot(cx) } else { @@ -1728,25 +1726,6 @@ impl EditorElement { .collect::>(); let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; - // todo!("this should happen during layout") - let editor_mode = snapshot.mode; - if let EditorMode::AutoHeight { max_lines } = editor_mode { - todo!() - // size.set_y( - // scroll_height - // .min(constraint.max_along(Axis::Vertical)) - // .max(constraint.min_along(Axis::Vertical)) - // .max(line_height) - // .min(line_height * max_lines as f32), - // ) - } else if let EditorMode::SingleLine = editor_mode { - bounds.size.height = line_height.min(bounds.size.height); - } - // todo!() - // else if size.y.is_infinite() { - // // size.set_y(scroll_height); - // } - // let gutter_size = size(gutter_width, bounds.size.height); let text_size = size(text_width, bounds.size.height); @@ -2064,7 +2043,7 @@ impl EditorElement { .unwrap(); LayoutState { - mode: editor_mode, + mode: snapshot.mode, position_map: Arc::new(PositionMap { size: bounds.size, scroll_position: point( @@ -2617,19 +2596,44 @@ impl Element for EditorElement { cx: &mut gpui::WindowContext, ) -> (gpui::LayoutId, Self::State) { self.editor.update(cx, |editor, cx| { - editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. + editor.set_style(self.style.clone(), cx); - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = match editor.mode { + let layout_id = match editor.mode { EditorMode::SingleLine => { - self.style.text.line_height_in_pixels(cx.rem_size()).into() + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = self.style.text.line_height_in_pixels(rem_size).into(); + cx.request_layout(&style, None) + } + EditorMode::AutoHeight { max_lines } => { + let editor_handle = cx.view().clone(); + let max_line_number_width = + self.max_line_number_width(&editor.snapshot(cx), cx); + cx.request_measured_layout( + Style::default(), + move |known_dimensions, available_space, cx| { + editor_handle + .update(cx, |editor, cx| { + dbg!(compute_auto_height_layout( + editor, + max_lines, + max_line_number_width, + known_dimensions, + cx, + )) + }) + .unwrap_or_default() + }, + ) + } + EditorMode::Full => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + cx.request_layout(&style, None) } - EditorMode::AutoHeight { .. } => todo!(), - EditorMode::Full => relative(1.).into(), }; - let layout_id = cx.request_layout(&style, None); (layout_id, ()) }) @@ -4134,3 +4138,60 @@ pub fn register_action( } }) } + +fn compute_auto_height_layout( + editor: &mut Editor, + max_lines: usize, + max_line_number_width: Pixels, + known_dimensions: Size>, + cx: &mut ViewContext, +) -> Option> { + let mut width = known_dimensions.width?; + if let Some(height) = known_dimensions.height { + return Some(size(width, height)); + } + + let style = editor.style.as_ref().unwrap(); + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + let mut snapshot = editor.snapshot(cx); + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size).unwrap(); + let gutter_padding_factor = 3.5; + gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = max_line_number_width + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_padding = Pixels::ZERO; + gutter_width = Pixels::ZERO; + gutter_margin = Pixels::ZERO; + }; + + editor.gutter_width = gutter_width; + let text_width = width - gutter_width; + let overscroll = size(em_width, px(0.)); + + let editor_width = text_width - gutter_margin - overscroll.width - em_width; + println!("setting wrap width during layout: {editor_width:?}"); + if editor.set_wrap_width(Some(editor_width), cx) { + snapshot = editor.snapshot(cx); + } + + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; + let height = scroll_height + .max(line_height) + .min(line_height * max_lines as f32); + + Some(size(width, height)) +} diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs index 0eaf3d126c..2d63d1d491 100644 --- a/crates/storybook2/src/stories.rs +++ b/crates/storybook2/src/stories.rs @@ -1,3 +1,4 @@ +mod auto_height_editor; mod focus; mod kitchen_sink; mod picker; @@ -5,6 +6,7 @@ mod scroll; mod text; mod z_index; +pub use auto_height_editor::*; pub use focus::*; pub use kitchen_sink::*; pub use picker::*; diff --git a/crates/storybook2/src/stories/auto_height_editor.rs b/crates/storybook2/src/stories/auto_height_editor.rs new file mode 100644 index 0000000000..2f3089a4e6 --- /dev/null +++ b/crates/storybook2/src/stories/auto_height_editor.rs @@ -0,0 +1,34 @@ +use editor::Editor; +use gpui::{ + div, white, Div, KeyBinding, ParentElement, Render, Styled, View, ViewContext, VisualContext, + WindowContext, +}; + +pub struct AutoHeightEditorStory { + editor: View, +} + +impl AutoHeightEditorStory { + pub fn new(cx: &mut WindowContext) -> View { + cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]); + cx.build_view(|cx| Self { + editor: cx.build_view(|cx| { + let mut editor = Editor::auto_height(3, cx); + editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + editor + }), + }) + } +} + +impl Render for AutoHeightEditorStory { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .size_full() + .bg(white()) + .text_sm() + .child(div().w_32().bg(gpui::black()).child(self.editor.clone())) + } +} diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 0354097c0b..35d8ea49b4 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -12,6 +12,7 @@ use ui::prelude::*; #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ComponentStory { + AutoHeightEditor, Avatar, Button, Checkbox, @@ -33,6 +34,7 @@ pub enum ComponentStory { impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { + Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(), Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(), Self::Button => cx.build_view(|_| ui::ButtonStory).into(), Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), From 0a8a84f65690d863c8ac5283492b44a971348f7b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 30 Nov 2023 12:04:23 -0500 Subject: [PATCH 06/28] Rename button-related enums (#3463) This PR renames the `ButtonStyle2` and `ButtonSize2` enums to `ButtonStyle` and `ButtonSize`, respectively. Release Notes: - N/A --- crates/collab_ui2/src/collab_titlebar_item.rs | 8 +- crates/search2/src/search.rs | 10 +- crates/ui2/src/components/button/button.rs | 6 +- .../ui2/src/components/button/button_like.rs | 108 +++++++++--------- .../ui2/src/components/button/icon_button.rs | 6 +- crates/ui2/src/components/stories/button.rs | 6 +- 6 files changed, 74 insertions(+), 70 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index d76242afa3..f18e4cb2db 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,7 +37,7 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip}; +use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle, IconButton, KeyBinding, Tooltip}; use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; @@ -154,7 +154,7 @@ impl Render for CollabTitlebarItem { .id("project_owner_indicator") .child( Button::new("player", "player") - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .color(Some(Color::Player(0))), ) .tooltip(move |cx| Tooltip::text("Toggle following", cx)), @@ -167,7 +167,7 @@ impl Render for CollabTitlebarItem { .id("titlebar_project_menu_button") .child( Button::new("project_name", "project_name") - .style(ButtonStyle2::Subtle), + .style(ButtonStyle::Subtle), ) .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), ) @@ -179,7 +179,7 @@ impl Render for CollabTitlebarItem { .id("titlebar_git_menu_button") .child( Button::new("branch_name", "branch_name") - .style(ButtonStyle2::Subtle) + .style(ButtonStyle::Subtle) .color(Some(Color::Muted)), ) .tooltip(move |cx| { diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 65a4ddfd42..13def6b4a7 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -4,7 +4,7 @@ use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; use ui::prelude::*; -use ui::{ButtonStyle2, Icon, IconButton}; +use ui::{ButtonStyle, Icon, IconButton}; //pub use project_search::{ProjectSearchBar, ProjectSearchView}; // use theme::components::{ // action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, @@ -91,8 +91,8 @@ impl SearchOptions { cx.dispatch_action(action.boxed_clone()); } }) - .style(ButtonStyle2::Subtle) - .when(active, |button| button.style(ButtonStyle2::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } } @@ -103,8 +103,8 @@ fn toggle_replace_button(active: bool) -> impl IntoElement { cx.dispatch_action(Box::new(ToggleReplace)); cx.notify(); }) - .style(ButtonStyle2::Subtle) - .when(active, |button| button.style(ButtonStyle2::Filled)) + .style(ButtonStyle::Subtle) + .when(active, |button| button.style(ButtonStyle::Filled)) } fn render_replace_button( diff --git a/crates/ui2/src/components/button/button.rs b/crates/ui2/src/components/button/button.rs index b4e666e9ad..4bfa71d092 100644 --- a/crates/ui2/src/components/button/button.rs +++ b/crates/ui2/src/components/button/button.rs @@ -1,7 +1,7 @@ use gpui::AnyView; use crate::prelude::*; -use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle}; +use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle}; #[derive(IntoElement)] pub struct Button { @@ -54,12 +54,12 @@ impl ButtonCommon for Button { self.base.id() } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.base = self.base.style(style); self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.base = self.base.size(size); self } diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 74cb506e70..207d59ecf1 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -1,4 +1,4 @@ -use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; +use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; use smallvec::SmallVec; use crate::h_stack; @@ -6,13 +6,13 @@ use crate::prelude::*; pub trait ButtonCommon: Clickable + Disableable { fn id(&self) -> &ElementId; - fn style(self, style: ButtonStyle2) -> Self; - fn size(self, size: ButtonSize2) -> Self; + fn style(self, style: ButtonStyle) -> Self; + fn size(self, size: ButtonSize) -> Self; fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self; } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ButtonStyle2 { +pub enum ButtonStyle { #[default] Filled, // Tinted, @@ -21,54 +21,57 @@ pub enum ButtonStyle2 { } #[derive(Debug, Clone)] -pub struct ButtonStyle { +pub(crate) struct ButtonLikeStyles { pub background: Hsla, + #[allow(unused)] pub border_color: Hsla, + #[allow(unused)] pub label_color: Hsla, + #[allow(unused)] pub icon_color: Hsla, } -impl ButtonStyle2 { - pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle { +impl ButtonStyle { + pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_background, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, } } - pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_hover, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), // TODO: These are not great label_color: Color::Muted.color(cx), // TODO: These are not great @@ -77,23 +80,23 @@ impl ButtonStyle2 { } } - pub fn active(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_active, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_active, - border_color: gpui::transparent_black(), + border_color: transparent_black(), label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), // TODO: These are not great label_color: Color::Muted.color(cx), // TODO: These are not great @@ -102,22 +105,23 @@ impl ButtonStyle2 { } } - pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle { + #[allow(unused)] + pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_background, border_color: cx.theme().colors().border_focused, label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: cx.theme().colors().border_focused, label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), border_color: cx.theme().colors().border_focused, label_color: Color::Accent.color(cx), icon_color: Color::Accent.color(cx), @@ -125,23 +129,23 @@ impl ButtonStyle2 { } } - pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle { + pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles { match self { - ButtonStyle2::Filled => ButtonStyle { + ButtonStyle::Filled => ButtonLikeStyles { background: cx.theme().colors().element_disabled, border_color: cx.theme().colors().border_disabled, label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, - ButtonStyle2::Subtle => ButtonStyle { + ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_disabled, border_color: cx.theme().colors().border_disabled, label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, - ButtonStyle2::Transparent => ButtonStyle { - background: gpui::transparent_black(), - border_color: gpui::transparent_black(), + ButtonStyle::Transparent => ButtonLikeStyles { + background: transparent_black(), + border_color: transparent_black(), label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, @@ -150,19 +154,19 @@ impl ButtonStyle2 { } #[derive(Default, PartialEq, Clone, Copy)] -pub enum ButtonSize2 { +pub enum ButtonSize { #[default] Default, Compact, None, } -impl ButtonSize2 { +impl ButtonSize { fn height(self) -> Rems { match self { - ButtonSize2::Default => rems(22. / 16.), - ButtonSize2::Compact => rems(18. / 16.), - ButtonSize2::None => rems(16. / 16.), + ButtonSize::Default => rems(22. / 16.), + ButtonSize::Compact => rems(18. / 16.), + ButtonSize::None => rems(16. / 16.), } } } @@ -170,10 +174,10 @@ impl ButtonSize2 { #[derive(IntoElement)] pub struct ButtonLike { id: ElementId, - pub(super) style: ButtonStyle2, + pub(super) style: ButtonStyle, pub(super) disabled: bool, pub(super) selected: bool, - size: ButtonSize2, + size: ButtonSize, tooltip: Option AnyView>>, on_click: Option>, children: SmallVec<[AnyElement; 2]>, @@ -183,10 +187,10 @@ impl ButtonLike { pub fn new(id: impl Into) -> Self { Self { id: id.into(), - style: ButtonStyle2::default(), + style: ButtonStyle::default(), disabled: false, selected: false, - size: ButtonSize2::Default, + size: ButtonSize::Default, tooltip: None, children: SmallVec::new(), on_click: None, @@ -220,12 +224,12 @@ impl ButtonCommon for ButtonLike { &self.id } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.style = style; self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.size = size; self } diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index 7746c3f8be..a62832059d 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -1,7 +1,7 @@ use gpui::{Action, AnyView}; use crate::prelude::*; -use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize}; +use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize}; #[derive(IntoElement)] pub struct IconButton { @@ -65,12 +65,12 @@ impl ButtonCommon for IconButton { self.base.id() } - fn style(mut self, style: ButtonStyle2) -> Self { + fn style(mut self, style: ButtonStyle) -> Self { self.base = self.base.style(style); self } - fn size(mut self, size: ButtonSize2) -> Self { + fn size(mut self, size: ButtonSize) -> Self { self.base = self.base.size(size); self } diff --git a/crates/ui2/src/components/stories/button.rs b/crates/ui2/src/components/stories/button.rs index 918c321c25..db8aa40cf7 100644 --- a/crates/ui2/src/components/stories/button.rs +++ b/crates/ui2/src/components/stories/button.rs @@ -2,7 +2,7 @@ use gpui::{Div, Render}; use story::Story; use crate::prelude::*; -use crate::{Button, ButtonStyle2}; +use crate::{Button, ButtonStyle}; pub struct ButtonStory; @@ -15,8 +15,8 @@ impl Render for ButtonStory { .child(Story::label("Default")) .child(Button::new("default_filled", "Click me")) .child(Story::label("Default (Subtle)")) - .child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle)) + .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle)) .child(Story::label("Default (Transparent)")) - .child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent)) + .child(Button::new("default_transparent", "Click me").style(ButtonStyle::Transparent)) } } From 75afb65b261c1170927d7454db8911313a7ba622 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 30 Nov 2023 10:11:39 -0700 Subject: [PATCH 07/28] Show cursor position in status bar --- crates/editor2/src/items.rs | 148 ++++++++++++++-------------- crates/gpui2/src/view.rs | 4 + crates/workspace2/src/status_bar.rs | 25 ++--- crates/zed2/src/zed2.rs | 4 +- 4 files changed, 86 insertions(+), 95 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index eca3b99d78..b5da219711 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -8,9 +8,9 @@ use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, - FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, WindowContext, + div, point, AnyElement, AppContext, AsyncAppContext, Div, Entity, EntityId, EventEmitter, + FocusHandle, IntoElement, Model, ParentElement, Pixels, Render, SharedString, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, @@ -20,6 +20,7 @@ use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPat use rpc::proto::{self, update_view, PeerId}; use settings::Settings; use smallvec::SmallVec; +use std::fmt::Write; use std::{ borrow::Cow, cmp::{self, Ordering}, @@ -31,8 +32,11 @@ use std::{ use text::Selection; use theme::{ActiveTheme, Theme}; use ui::{Color, Label}; -use util::{paths::PathExt, ResultExt, TryFutureExt}; -use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; +use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; +use workspace::{ + item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, + StatusItemView, +}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, @@ -1120,86 +1124,78 @@ pub struct CursorPosition { _observe_active_editor: Option, } -// impl Default for CursorPosition { -// fn default() -> Self { -// Self::new() -// } -// } +impl Default for CursorPosition { + fn default() -> Self { + Self::new() + } +} -// impl CursorPosition { -// pub fn new() -> Self { -// Self { -// position: None, -// selected_count: 0, -// _observe_active_editor: None, -// } -// } +impl CursorPosition { + pub fn new() -> Self { + Self { + position: None, + selected_count: 0, + _observe_active_editor: None, + } + } -// fn update_position(&mut self, editor: View, cx: &mut ViewContext) { -// let editor = editor.read(cx); -// let buffer = editor.buffer().read(cx).snapshot(cx); + fn update_position(&mut self, editor: View, cx: &mut ViewContext) { + let editor = editor.read(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); -// self.selected_count = 0; -// let mut last_selection: Option> = None; -// for selection in editor.selections.all::(cx) { -// self.selected_count += selection.end - selection.start; -// if last_selection -// .as_ref() -// .map_or(true, |last_selection| selection.id > last_selection.id) -// { -// last_selection = Some(selection); -// } -// } -// self.position = last_selection.map(|s| s.head().to_point(&buffer)); + self.selected_count = 0; + let mut last_selection: Option> = None; + for selection in editor.selections.all::(cx) { + self.selected_count += selection.end - selection.start; + if last_selection + .as_ref() + .map_or(true, |last_selection| selection.id > last_selection.id) + { + last_selection = Some(selection); + } + } + self.position = last_selection.map(|s| s.head().to_point(&buffer)); -// cx.notify(); -// } -// } + cx.notify(); + } +} -// impl Entity for CursorPosition { -// type Event = (); -// } +impl Render for CursorPosition { + type Element = Div; -// impl View for CursorPosition { -// fn ui_name() -> &'static str { -// "CursorPosition" -// } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().when_some(self.position, |el, position| { + let mut text = format!( + "{}{FILE_ROW_COLUMN_DELIMITER}{}", + position.row + 1, + position.column + 1 + ); + if self.selected_count > 0 { + write!(text, " ({} selected)", self.selected_count).unwrap(); + } -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// if let Some(position) = self.position { -// let theme = &theme::current(cx).workspace.status_bar; -// let mut text = format!( -// "{}{FILE_ROW_COLUMN_DELIMITER}{}", -// position.row + 1, -// position.column + 1 -// ); -// if self.selected_count > 0 { -// write!(text, " ({} selected)", self.selected_count).unwrap(); -// } -// Label::new(text, theme.cursor_position.clone()).into_any() -// } else { -// Empty::new().into_any() -// } -// } -// } + el.child(Label::new(text)) + }) + } +} -// impl StatusItemView for CursorPosition { -// fn set_active_pane_item( -// &mut self, -// active_pane_item: Option<&dyn ItemHandle>, -// cx: &mut ViewContext, -// ) { -// if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { -// self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); -// self.update_position(editor, cx); -// } else { -// self.position = None; -// self._observe_active_editor = None; -// } +impl StatusItemView for CursorPosition { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self._observe_active_editor = Some(cx.observe(&editor, Self::update_position)); + self.update_position(editor, cx); + } else { + self.position = None; + self._observe_active_editor = None; + } -// cx.notify(); -// } -// } + cx.notify(); + } +} fn path_for_buffer<'a>( buffer: &Model, diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index f31b0ae753..08fe56808a 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -240,6 +240,10 @@ impl Element for AnyView { } fn paint(self, _: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + debug_assert!( + state.is_some(), + "state is None. Did you include an AnyView twice in the tree?" + ); (self.paint)(&self, state.take().unwrap(), cx) } } diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 397859648b..8e448ae062 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -52,22 +52,13 @@ impl Render for StatusBar { h_stack() .gap_4() .child( - h_stack() - .gap_1() - .child( - // TODO: Line / column numbers - div() - .border() - .border_color(gpui::red()) - .child(Button::new("status_line_column_numbers", "15:22")), - ) - .child( - // TODO: Language picker - div() - .border() - .border_color(gpui::red()) - .child(Button::new("status_buffer_language", "Rust")), - ), + h_stack().gap_1().child( + // TODO: Language picker + div() + .border() + .border_color(gpui::red()) + .child(Button::new("status_buffer_language", "Rust")), + ), ) .child( h_stack() @@ -133,7 +124,7 @@ impl StatusBar { h_stack() .items_center() .gap_2() - .children(self.right_items.iter().map(|item| item.to_any())) + .children(self.right_items.iter().rev().map(|item| item.to_any())) } } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 07b75bf8d4..080f19e60d 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -147,7 +147,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // let feedback_button = cx.add_view(|_| { // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) // }); - // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + let cursor_position = cx.build_view(|_| editor::items::CursorPosition::new()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(activity_indicator, cx); @@ -156,7 +156,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // status_bar.add_right_item(copilot, cx); // status_bar.add_right_item(active_buffer_language, cx); // status_bar.add_right_item(vim_mode_indicator, cx); - // status_bar.add_right_item(cursor_position, cx); + status_bar.add_right_item(cursor_position, cx); }); auto_update::notify_of_any_new_update(cx); From b34b197d898bf2897a6aeec8c038d17f1ddf9871 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Nov 2023 10:06:08 -0800 Subject: [PATCH 08/28] Update main.rs --- crates/zed2/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 493988c945..4c7e914e37 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -746,9 +746,9 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { } #[cfg(not(debug_assertions))] -async fn watch_languages(_: Arc, _: Arc) -> Option<()> { +async fn watch_languages(_: Arc, _: Arc) -> Option<()> { None } #[cfg(not(debug_assertions))] -fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} +fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} From 865baaa1a1710b9ab99c87e4026a0889c98415c9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 30 Nov 2023 13:25:26 -0500 Subject: [PATCH 09/28] Remove unused `GraphicSlot` enum --- crates/ui2/src/slot.rs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 crates/ui2/src/slot.rs diff --git a/crates/ui2/src/slot.rs b/crates/ui2/src/slot.rs deleted file mode 100644 index 3438e34621..0000000000 --- a/crates/ui2/src/slot.rs +++ /dev/null @@ -1,12 +0,0 @@ -use gpui::{ImageSource, SharedString}; - -use crate::Icon; - -/// A slot utility that provides a way to to pass either -/// an icon or an image to a component. -#[derive(Debug, Clone)] -pub enum GraphicSlot { - Icon(Icon), - Avatar(ImageSource), - PublicActor(SharedString), -} From e78538e16298dc08bc4c896be05fa79c9a841cce Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 30 Nov 2023 13:26:12 -0500 Subject: [PATCH 10/28] Implement `Selectable` for `ListItem` and `ListHeader` --- crates/command_palette2/src/command_palette.rs | 2 +- crates/file_finder2/src/file_finder.rs | 2 +- crates/project_panel2/src/project_panel.rs | 5 ++--- crates/theme_selector2/src/theme_selector.rs | 8 ++++---- crates/ui2/src/components/list/list_header.rs | 4 +++- crates/ui2/src/components/list/list_item.rs | 12 +++++++----- crates/ui2/src/ui2.rs | 2 -- crates/welcome2/src/base_keymap_picker.rs | 6 +++--- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index f9b58b1d56..04688b0549 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -11,7 +11,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; +use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 7a00b2644a..ea00c5a635 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -15,7 +15,7 @@ use std::{ }, }; use text::Point; -use ui::{v_stack, HighlightedLabel, ListItem}; +use ui::{prelude::*, v_stack, HighlightedLabel, ListItem}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index dc584d52ff..fd0473358e 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -29,8 +29,7 @@ use std::{ path::Path, sync::Arc, }; -use theme::ActiveTheme as _; -use ui::{v_stack, ContextMenu, IconElement, Label, ListItem}; +use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -2845,7 +2844,7 @@ mod tests { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.selection = Some(Selection { + panel.selection = Some(crate::Selection { worktree_id: worktree.id(), entry_id, }); diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index 7b0a0c3d3a..be55194e76 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, - SharedString, View, ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View, + ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; -use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; -use ui::ListItem; +use theme::{Theme, ThemeRegistry, ThemeSettings}; +use ui::{prelude::*, ListItem}; use util::ResultExt; use workspace::{ui::HighlightedLabel, Workspace}; diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 8761acd608..431665ffd3 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -62,8 +62,10 @@ impl ListHeader { self.meta = meta; self } +} - pub fn selected(mut self, selected: bool) -> Self { +impl Selectable for ListHeader { + fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index 7ad1d5fb72..85198416cd 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -83,11 +83,6 @@ impl ListItem { self } - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - pub fn left_child(mut self, left_content: impl IntoElement) -> Self { self.left_slot = Some(left_content.into_any_element()); self @@ -109,6 +104,13 @@ impl ListItem { } } +impl Selectable for ListItem { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + impl ParentElement for ListItem { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children diff --git a/crates/ui2/src/ui2.rs b/crates/ui2/src/ui2.rs index 7b054890a6..6c5669741b 100644 --- a/crates/ui2/src/ui2.rs +++ b/crates/ui2/src/ui2.rs @@ -18,7 +18,6 @@ mod disableable; mod fixed; pub mod prelude; mod selectable; -mod slot; mod styled_ext; mod styles; pub mod utils; @@ -29,6 +28,5 @@ pub use disableable::*; pub use fixed::*; pub use prelude::*; pub use selectable::*; -pub use slot::*; pub use styled_ext::*; pub use styles::*; diff --git a/crates/welcome2/src/base_keymap_picker.rs b/crates/welcome2/src/base_keymap_picker.rs index f28aa7dc30..4e829972f0 100644 --- a/crates/welcome2/src/base_keymap_picker.rs +++ b/crates/welcome2/src/base_keymap_picker.rs @@ -1,14 +1,14 @@ use super::base_keymap_setting::BaseKeymap; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task, - View, ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View, + ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::Fs; use settings::{update_settings_file, Settings}; use std::sync::Arc; -use ui::ListItem; +use ui::{prelude::*, ListItem}; use util::ResultExt; use workspace::{ui::HighlightedLabel, Workspace}; From f922ad9f7fe98aa54c530c3e5a9e61ac36cdcfcc Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 30 Nov 2023 14:24:00 -0500 Subject: [PATCH 11/28] Fix bug preventing spaces from being used in filename (zed2) Co-Authored-By: Mikayla Maki --- crates/project_panel2/src/project_panel.rs | 108 ++++++++++----------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index b9b50e0d7f..caca5402fa 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,9 +10,9 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, - PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + InteractiveElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, + Point, PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, + View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -1355,7 +1355,7 @@ impl ProjectPanel { details: EntryDetails, // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> ListItem { + ) -> Div { let kind = details.kind; let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; @@ -1374,44 +1374,46 @@ impl ProjectPanel { }) .unwrap_or(theme.status().info); - ListItem::new(entry_id.to_proto() as usize) - .indent_level(details.depth) - .indent_step_size(px(settings.indent_size)) - .selected(is_selected) - .child(if let Some(icon) = &details.icon { - div().child(IconElement::from_path(icon.to_string())) - } else { - div() - }) - .child( - if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - div().h_full().w_full().child(editor.clone()) + div().key_context(self.dispatch_context(cx)).child( + ListItem::new(entry_id.to_proto() as usize) + .indent_level(details.depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) + .child(if let Some(icon) = &details.icon { + div().child(IconElement::from_path(icon.to_string())) } else { div() - .text_color(filename_text_color) - .child(Label::new(details.filename.clone())) - } - .ml_1(), - ) - .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { - if event.down.button == MouseButton::Right { - return; - } - if !show_editor { - if kind.is_dir() { - this.toggle_expanded(entry_id, cx); + }) + .child( + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { + div().h_full().w_full().child(editor.clone()) } else { - if event.down.modifiers.command { - this.split_entry(entry_id, cx); + div() + .text_color(filename_text_color) + .child(Label::new(details.filename.clone())) + } + .ml_1(), + ) + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { + if event.down.button == MouseButton::Right { + return; + } + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); } else { - this.open_entry(entry_id, event.up.click_count > 1, cx); + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } } } - } - })) - .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - })) + })) + .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + })), + ) // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, @@ -1421,6 +1423,22 @@ impl ProjectPanel { // ); // }) } + + fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("menu"); + dispatch_context.add("not_editing"); + + let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) { + "editing" + } else { + "not_editing" + }; + + dispatch_context.add(identifier); + + dispatch_context + } } impl Render for ProjectPanel { @@ -3011,21 +3029,3 @@ mod tests { .unwrap(); } } - -// TODO - implement this in the new keymap system -// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { -// Self::reset_to_default_keymap_context(keymap); -// keymap.add_identifier("menu"); - -// if let Some(window) = cx.active_window() { -// window.read_with(cx, |cx| { -// let identifier = if self.filename_editor.is_focused(cx) { -// "editing" -// } else { -// "not_editing" -// }; - -// keymap.add_identifier(identifier); -// }); -// } -// } From 16dc978bb4d50b9d49eb1c57c2d13da52806c575 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2023 11:36:36 -0800 Subject: [PATCH 12/28] Bump Tree-sitter for a crash fix --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ec386837b..3110a9ff43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9970,7 +9970,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0" dependencies = [ "cc", "regex", diff --git a/Cargo.toml b/Cargo.toml index 674bfbd606..5dc30ca40b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,7 +201,7 @@ tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", re tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 From c63ca09eed5905c33375bd1960e2db69d47e2269 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2023 12:10:25 -0800 Subject: [PATCH 13/28] Reintroduce pane navigation history in zed2 --- crates/editor2/src/items.rs | 59 +++++++++++++++++------------------ crates/workspace2/src/pane.rs | 46 +++++++++++++++++++++------ 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index eca3b99d78..43b3294471 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -529,39 +529,38 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - todo!(); - // if let Ok(data) = data.downcast::() { - // let newest_selection = self.selections.newest::(cx); - // let buffer = self.buffer.read(cx).read(cx); - // let offset = if buffer.can_resolve(&data.cursor_anchor) { - // data.cursor_anchor.to_point(&buffer) - // } else { - // buffer.clip_point(data.cursor_position, Bias::Left) - // }; + if let Ok(data) = data.downcast::() { + let newest_selection = self.selections.newest::(cx); + let buffer = self.buffer.read(cx).read(cx); + let offset = if buffer.can_resolve(&data.cursor_anchor) { + data.cursor_anchor.to_point(&buffer) + } else { + buffer.clip_point(data.cursor_position, Bias::Left) + }; - // let mut scroll_anchor = data.scroll_anchor; - // if !buffer.can_resolve(&scroll_anchor.anchor) { - // scroll_anchor.anchor = buffer.anchor_before( - // buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), - // ); - // } + let mut scroll_anchor = data.scroll_anchor; + if !buffer.can_resolve(&scroll_anchor.anchor) { + scroll_anchor.anchor = buffer.anchor_before( + buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left), + ); + } - // drop(buffer); + drop(buffer); - // if newest_selection.head() == offset { - // false - // } else { - // let nav_history = self.nav_history.take(); - // self.set_scroll_anchor(scroll_anchor, cx); - // self.change_selections(Some(Autoscroll::fit()), cx, |s| { - // s.select_ranges([offset..offset]) - // }); - // self.nav_history = nav_history; - // true - // } - // } else { - // false - // } + if newest_selection.head() == offset { + false + } else { + let nav_history = self.nav_history.take(); + self.set_scroll_anchor(scroll_anchor, cx); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([offset..offset]) + }); + self.nav_history = nav_history; + true + } + } else { + false + } } fn tab_tooltip_text(&self, cx: &AppContext) -> Option { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 1bf83f6395..aa91cfc91b 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -457,6 +457,28 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } + fn navigate_backward(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + let pane = cx.view().downgrade(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_back(pane, cx).detach_and_log_err(cx) + }) + }) + } + } + + fn navigate_forward(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + let pane = cx.view().downgrade(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_forward(pane, cx).detach_and_log_err(cx) + }) + }) + } + } + fn history_updated(&mut self, cx: &mut ViewContext) { self.toolbar.update(cx, |_, cx| cx.notify()); } @@ -1483,12 +1505,20 @@ impl Pane { .child( div().border().border_color(gpui::red()).child( IconButton::new("navigate_backward", Icon::ArrowLeft) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) .disabled(!self.can_navigate_backward()), ), ) .child( div().border().border_color(gpui::red()).child( IconButton::new("navigate_forward", Icon::ArrowRight) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) .disabled(!self.can_navigate_forward()), ), ), @@ -1935,18 +1965,14 @@ impl Render for Pane { v_stack() .key_context("Pane") .track_focus(&self.focus_handle) - .on_action(cx.listener(|pane: &mut Pane, _: &SplitLeft, cx| { - pane.split(SplitDirection::Left, cx) - })) + .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx))) + .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))) .on_action( - cx.listener(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)), + cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)), ) - .on_action(cx.listener(|pane: &mut Pane, _: &SplitRight, cx| { - pane.split(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| { - pane.split(SplitDirection::Down, cx) - })) + .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))) + .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx))) + .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx))) // cx.add_action(Pane::toggle_zoom); // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { // pane.activate_item(action.0, true, true, cx); From e5a5b1e84c8641447d23a3f2f5f7d1e8cdac49d9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 30 Nov 2023 15:55:31 -0500 Subject: [PATCH 14/28] Rework `ListHeader` to be more open (#3467) This PR reworks the `ListHeader` component to be more open. The `meta` method can now be used to append meta items of any element to the `ListHeader`, and they will be rendered with the appropriate spacing between them. Release Notes: - N/A --- crates/collab_ui2/src/collab_panel.rs | 2 +- crates/storybook2/src/story_selector.rs | 2 + .../components/{button/mod.rs => button.rs} | 0 crates/ui2/src/components/list.rs | 66 +------------------ crates/ui2/src/components/list/list.rs | 60 +++++++++++++++++ crates/ui2/src/components/list/list_header.rs | 42 +++--------- crates/ui2/src/components/stories.rs | 2 + crates/ui2/src/components/stories/list.rs | 4 +- .../ui2/src/components/stories/list_header.rs | 33 ++++++++++ 9 files changed, 112 insertions(+), 99 deletions(-) rename crates/ui2/src/components/{button/mod.rs => button.rs} (100%) create mode 100644 crates/ui2/src/components/list/list.rs create mode 100644 crates/ui2/src/components/stories/list_header.rs diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index b62056a3be..b90df68c2a 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2511,7 +2511,7 @@ impl CollabPanel { } else { el.child( ListHeader::new(text) - .when_some(button, |el, button| el.right_button(button)) + .when_some(button, |el, button| el.meta(button)) .selected(is_selected), ) } diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 0354097c0b..994b31c336 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -23,6 +23,7 @@ pub enum ComponentStory { Keybinding, Label, List, + ListHeader, ListItem, Scroll, Text, @@ -44,6 +45,7 @@ impl ComponentStory { Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), Self::Label => cx.build_view(|_| ui::LabelStory).into(), Self::List => cx.build_view(|_| ui::ListStory).into(), + Self::ListHeader => cx.build_view(|_| ui::ListHeaderStory).into(), Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(), Self::Scroll => ScrollStory::view(cx).into(), Self::Text => TextStory::view(cx).into(), diff --git a/crates/ui2/src/components/button/mod.rs b/crates/ui2/src/components/button.rs similarity index 100% rename from crates/ui2/src/components/button/mod.rs rename to crates/ui2/src/components/button.rs diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index aafd045391..88650b6ae8 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,73 +1,11 @@ +mod list; mod list_header; mod list_item; mod list_separator; mod list_sub_header; -use gpui::{AnyElement, Div}; -use smallvec::SmallVec; - -use crate::prelude::*; -use crate::{v_stack, Label}; - +pub use list::*; pub use list_header::*; pub use list_item::*; pub use list_separator::*; pub use list_sub_header::*; - -#[derive(IntoElement)] -pub struct List { - /// Message to display when the list is empty - /// Defaults to "No items" - empty_message: SharedString, - header: Option, - toggle: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl List { - pub fn new() -> Self { - Self { - empty_message: "No items".into(), - header: None, - toggle: None, - children: SmallVec::new(), - } - } - - pub fn empty_message(mut self, empty_message: impl Into) -> Self { - self.empty_message = empty_message.into(); - self - } - - pub fn header(mut self, header: ListHeader) -> Self { - self.header = Some(header); - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } -} - -impl ParentElement for List { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl RenderOnce for List { - type Rendered = Div; - - fn render(self, _cx: &mut WindowContext) -> Self::Rendered { - v_stack() - .w_full() - .py_1() - .children(self.header.map(|header| header)) - .map(|this| match (self.children.is_empty(), self.toggle) { - (false, _) => this.children(self.children), - (true, Some(false)) => this, - (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), - }) - } -} diff --git a/crates/ui2/src/components/list/list.rs b/crates/ui2/src/components/list/list.rs new file mode 100644 index 0000000000..fdfe256bd6 --- /dev/null +++ b/crates/ui2/src/components/list/list.rs @@ -0,0 +1,60 @@ +use gpui::{AnyElement, Div}; +use smallvec::SmallVec; + +use crate::{prelude::*, v_stack, Label, ListHeader}; + +#[derive(IntoElement)] +pub struct List { + /// Message to display when the list is empty + /// Defaults to "No items" + empty_message: SharedString, + header: Option, + toggle: Option, + children: SmallVec<[AnyElement; 2]>, +} + +impl List { + pub fn new() -> Self { + Self { + empty_message: "No items".into(), + header: None, + toggle: None, + children: SmallVec::new(), + } + } + + pub fn empty_message(mut self, empty_message: impl Into) -> Self { + self.empty_message = empty_message.into(); + self + } + + pub fn header(mut self, header: impl Into>) -> Self { + self.header = header.into(); + self + } + + pub fn toggle(mut self, toggle: impl Into>) -> Self { + self.toggle = toggle.into(); + self + } +} + +impl ParentElement for List { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for List { + type Rendered = Div; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + v_stack().w_full().py_1().children(self.header).map(|this| { + match (self.children.is_empty(), self.toggle) { + (false, _) => this.children(self.children), + (true, Some(false)) => this, + (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), + } + }) + } +} diff --git a/crates/ui2/src/components/list/list_header.rs b/crates/ui2/src/components/list/list_header.rs index 431665ffd3..799b1c5dae 100644 --- a/crates/ui2/src/components/list/list_header.rs +++ b/crates/ui2/src/components/list/list_header.rs @@ -1,22 +1,16 @@ use std::rc::Rc; -use gpui::{ClickEvent, Div}; +use gpui::{AnyElement, ClickEvent, Div}; +use smallvec::SmallVec; use crate::prelude::*; -use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label}; - -pub enum ListHeaderMeta { - Tools(Vec), - // TODO: This should be a button - Button(Label), - Text(Label), -} +use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label}; #[derive(IntoElement)] pub struct ListHeader { label: SharedString, left_icon: Option, - meta: Option, + meta: SmallVec<[AnyElement; 2]>, toggle: Option, on_toggle: Option>, inset: bool, @@ -28,7 +22,7 @@ impl ListHeader { Self { label: label.into(), left_icon: None, - meta: None, + meta: SmallVec::new(), inset: false, toggle: None, on_toggle: None, @@ -49,17 +43,13 @@ impl ListHeader { self } - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; + pub fn left_icon(mut self, left_icon: impl Into>) -> Self { + self.left_icon = left_icon.into(); self } - pub fn right_button(self, button: IconButton) -> Self { - self.meta(Some(ListHeaderMeta::Tools(vec![button]))) - } - - pub fn meta(mut self, meta: Option) -> Self { - self.meta = meta; + pub fn meta(mut self, meta: impl IntoElement) -> Self { + self.meta.push(meta.into_any_element()); self } } @@ -75,18 +65,6 @@ impl RenderOnce for ListHeader { type Rendered = Div; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let meta = match self.meta { - Some(ListHeaderMeta::Tools(icons)) => div().child( - h_stack() - .gap_2() - .items_center() - .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))), - ), - Some(ListHeaderMeta::Button(label)) => div().child(label), - Some(ListHeaderMeta::Text(label)) => div().child(label), - None => div(), - }; - h_stack().w_full().relative().child( div() .h_5() @@ -120,7 +98,7 @@ impl RenderOnce for ListHeader { .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ), ) - .child(meta), + .child(h_stack().gap_2().items_center().children(self.meta)), ) } } diff --git a/crates/ui2/src/components/stories.rs b/crates/ui2/src/components/stories.rs index e870515caf..113c2679b7 100644 --- a/crates/ui2/src/components/stories.rs +++ b/crates/ui2/src/components/stories.rs @@ -8,6 +8,7 @@ mod icon_button; mod keybinding; mod label; mod list; +mod list_header; mod list_item; pub use avatar::*; @@ -20,4 +21,5 @@ pub use icon_button::*; pub use keybinding::*; pub use label::*; pub use list::*; +pub use list_header::*; pub use list_item::*; diff --git a/crates/ui2/src/components/stories/list.rs b/crates/ui2/src/components/stories/list.rs index 75a16deb40..1795601944 100644 --- a/crates/ui2/src/components/stories/list.rs +++ b/crates/ui2/src/components/stories/list.rs @@ -22,12 +22,12 @@ impl Render for ListStory { .child(Story::label("With sections")) .child( List::new() - .child(ListHeader::new("Fruits")) + .header(ListHeader::new("Produce")) + .child(ListSubHeader::new("Fruits")) .child(ListItem::new("apple").child("Apple")) .child(ListItem::new("banana").child("Banana")) .child(ListItem::new("cherry").child("Cherry")) .child(ListSeparator) - .child(ListHeader::new("Vegetables")) .child(ListSubHeader::new("Root Vegetables")) .child(ListItem::new("carrot").child("Carrot")) .child(ListItem::new("potato").child("Potato")) diff --git a/crates/ui2/src/components/stories/list_header.rs b/crates/ui2/src/components/stories/list_header.rs new file mode 100644 index 0000000000..056eaa2762 --- /dev/null +++ b/crates/ui2/src/components/stories/list_header.rs @@ -0,0 +1,33 @@ +use gpui::{Div, Render}; +use story::Story; + +use crate::{prelude::*, IconButton}; +use crate::{Icon, ListHeader}; + +pub struct ListHeaderStory; + +impl Render for ListHeaderStory { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + Story::container() + .child(Story::title_for::()) + .child(Story::label("Default")) + .child(ListHeader::new("Section 1")) + .child(Story::label("With left icon")) + .child(ListHeader::new("Section 2").left_icon(Icon::Bell)) + .child(Story::label("With left icon and meta")) + .child( + ListHeader::new("Section 3") + .left_icon(Icon::BellOff) + .meta(IconButton::new("action_1", Icon::Bolt)), + ) + .child(Story::label("With multiple meta")) + .child( + ListHeader::new("Section 4") + .meta(IconButton::new("action_1", Icon::Bolt)) + .meta(IconButton::new("action_2", Icon::ExclamationTriangle)) + .meta(IconButton::new("action_3", Icon::Plus)), + ) + } +} From a003a91212bc420c7e60348a90c59116519844c5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2023 14:44:26 -0800 Subject: [PATCH 15/28] Restore auto-save on focus change, re-enable workspace tests --- crates/gpui2/src/app/test_context.rs | 37 +- crates/gpui2/src/platform.rs | 5 + crates/gpui2/src/platform/test/platform.rs | 8 +- crates/gpui2/src/platform/test/window.rs | 16 +- crates/workspace2/Cargo.toml | 42 +- crates/workspace2/src/item.rs | 566 +++--- crates/workspace2/src/pane.rs | 4 +- crates/workspace2/src/pane_group.rs | 4 +- crates/workspace2/src/persistence.rs | 4 +- crates/workspace2/src/persistence/model.rs | 4 +- crates/workspace2/src/searchable.rs | 2 +- crates/workspace2/src/workspace2.rs | 1960 +++++++++---------- crates/workspace2/src/workspace_settings.rs | 2 +- 13 files changed, 1350 insertions(+), 1304 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 9637720a67..31917e9bf3 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -2,8 +2,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, - TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle, - WindowOptions, + TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, VisualContext, WindowContext, + WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -509,6 +509,39 @@ impl<'a> VisualTestContext<'a> { pub fn simulate_input(&mut self, input: &str) { self.cx.simulate_input(self.window, input) } + + pub fn simulate_activation(&mut self) { + self.simulate_window_events(&mut |handlers| { + handlers + .active_status_change + .iter_mut() + .for_each(|f| f(true)); + }) + } + + pub fn simulate_deactivation(&mut self) { + self.simulate_window_events(&mut |handlers| { + handlers + .active_status_change + .iter_mut() + .for_each(|f| f(false)); + }) + } + + fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) { + let handlers = self + .cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .handlers + .clone() + }) + .unwrap(); + f(&mut *handlers.lock()); + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 3027c05fbd..7375f47939 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -158,6 +158,11 @@ pub(crate) trait PlatformWindow { fn draw(&self, scene: Scene); fn sprite_atlas(&self) -> Arc; + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestWindow> { + None + } } pub trait PlatformDispatcher: Send + Sync { diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index df1ed9b2a6..4532b33f50 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -189,13 +189,9 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_become_active(&self, _callback: Box) { - unimplemented!() - } + fn on_become_active(&self, _callback: Box) {} - fn on_resign_active(&self, _callback: Box) { - unimplemented!() - } + fn on_resign_active(&self, _callback: Box) {} fn on_quit(&self, _callback: Box) {} diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index e355c3aa4b..e7ca7a1616 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -11,11 +11,11 @@ use std::{ }; #[derive(Default)] -struct Handlers { - active_status_change: Vec>, - input: Vec bool>>, - moved: Vec>, - resize: Vec, f32)>>, +pub(crate) struct TestWindowHandlers { + pub(crate) active_status_change: Vec>, + pub(crate) input: Vec bool>>, + pub(crate) moved: Vec>, + pub(crate) resize: Vec, f32)>>, } pub struct TestWindow { @@ -23,7 +23,7 @@ pub struct TestWindow { current_scene: Mutex>, display: Rc, pub(crate) input_handler: Option>>>, - handlers: Mutex, + pub(crate) handlers: Arc>, platform: Weak, sprite_atlas: Arc, } @@ -167,6 +167,10 @@ impl PlatformWindow for TestWindow { fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } + + fn as_test(&self) -> Option<&TestWindow> { + Some(self) + } } pub struct TestAtlasState { diff --git a/crates/workspace2/Cargo.toml b/crates/workspace2/Cargo.toml index c327132a78..2dd7c6468e 100644 --- a/crates/workspace2/Cargo.toml +++ b/crates/workspace2/Cargo.toml @@ -10,29 +10,29 @@ doctest = false [features] test-support = [ - "call2/test-support", - "client2/test-support", - "project2/test-support", - "settings2/test-support", + "call/test-support", + "client/test-support", + "project/test-support", + "settings/test-support", "gpui/test-support", - "fs2/test-support" + "fs/test-support" ] [dependencies] -db2 = { path = "../db2" } -client2 = { path = "../client2" } +db = { path = "../db2", package = "db2" } +client = { path = "../client2", package = "client2" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } -fs2 = { path = "../fs2" } +fs = { path = "../fs2", package = "fs2" } gpui = { package = "gpui2", path = "../gpui2" } -install_cli2 = { path = "../install_cli2" } -language2 = { path = "../language2" } +install_cli = { path = "../install_cli2", package = "install_cli2" } +language = { path = "../language2", package = "language2" } #menu = { path = "../menu" } node_runtime = { path = "../node_runtime" } -project2 = { path = "../project2" } -settings2 = { path = "../settings2" } -terminal2 = { path = "../terminal2" } -theme2 = { path = "../theme2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +terminal = { path = "../terminal2", package = "terminal2" } +theme = { path = "../theme2", package = "theme2" } util = { path = "../util" } ui = { package = "ui2", path = "../ui2" } @@ -54,13 +54,13 @@ smallvec.workspace = true uuid.workspace = true [dev-dependencies] -call2 = { path = "../call2", features = ["test-support"] } -client2 = { path = "../client2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -project2 = { path = "../project2", features = ["test-support"] } -settings2 = { path = "../settings2", features = ["test-support"] } -fs2 = { path = "../fs2", features = ["test-support"] } -db2 = { path = "../db2", features = ["test-support"] } +call = { path = "../call2", package = "call2", features = ["test-support"] } +client = { path = "../client2", package = "client2", features = ["test-support"] } +gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } +project = { path = "../project2", package = "project2", features = ["test-support"] } +settings = { path = "../settings2", package = "settings2", features = ["test-support"] } +fs = { path = "../fs2", package = "fs2", features = ["test-support"] } +db = { path = "../db2", package = "db2", features = ["test-support"] } indoc.workspace = true env_logger.workspace = true diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 2cd2ac0bd6..73de7d05c3 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -7,7 +7,7 @@ use crate::{ ViewId, Workspace, WorkspaceId, }; use anyhow::Result; -use client2::{ +use client::{ proto::{self, PeerId}, Client, }; @@ -16,10 +16,10 @@ use gpui::{ HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView, WindowContext, }; -use project2::{Project, ProjectEntryId, ProjectPath}; +use project::{Project, ProjectEntryId, ProjectPath}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -33,7 +33,7 @@ use std::{ }, time::Duration, }; -use theme2::Theme; +use theme::Theme; #[derive(Deserialize)] pub struct ItemSettings { @@ -110,7 +110,7 @@ pub trait Item: FocusableView + EventEmitter { fn for_each_project_item( &self, _: &AppContext, - _: &mut dyn FnMut(EntityId, &dyn project2::Item), + _: &mut dyn FnMut(EntityId, &dyn project::Item), ) { } fn is_singleton(&self, _cx: &AppContext) -> bool { @@ -222,7 +222,7 @@ pub trait ItemHandle: 'static + Send { fn for_each_project_item( &self, _: &AppContext, - _: &mut dyn FnMut(EntityId, &dyn project2::Item), + _: &mut dyn FnMut(EntityId, &dyn project::Item), ); fn is_singleton(&self, cx: &AppContext) -> bool; fn boxed_clone(&self) -> Box; @@ -347,7 +347,7 @@ impl ItemHandle for View { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project2::Item), + f: &mut dyn FnMut(EntityId, &dyn project::Item), ) { self.read(cx).for_each_project_item(cx, f) } @@ -375,6 +375,7 @@ impl ItemHandle for View { pane: View, cx: &mut ViewContext, ) { + let weak_item = self.downgrade(); let history = pane.read(cx).nav_history_for_item(self); self.update(cx, |this, cx| { this.set_nav_history(history, cx); @@ -491,16 +492,15 @@ impl ItemHandle for View { } })); - // todo!() - // cx.observe_focus(self, move |workspace, item, focused, cx| { - // if !focused - // && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange - // { - // Pane::autosave_item(&item, workspace.project.clone(), cx) - // .detach_and_log_err(cx); - // } - // }) - // .detach(); + cx.on_blur(&self.focus_handle(cx), move |workspace, cx| { + if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange { + if let Some(item) = weak_item.upgrade() { + Pane::autosave_item(&item, workspace.project.clone(), cx) + .detach_and_log_err(cx); + } + } + }) + .detach(); let item_id = self.item_id(); cx.observe_release(self, move |workspace, _, _| { @@ -640,7 +640,7 @@ impl WeakItemHandle for WeakView { } pub trait ProjectItem: Item { - type Item: project2::Item; + type Item: project::Item; fn for_project_item( project: Model, @@ -759,300 +759,310 @@ impl FollowableItemHandle for View { } } -// #[cfg(any(test, feature = "test-support"))] -// pub mod test { -// use super::{Item, ItemEvent}; -// use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; -// use gpui::{ -// elements::Empty, AnyElement, AppContext, Element, Entity, Model, Task, View, -// ViewContext, View, WeakViewHandle, -// }; -// use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId}; -// use smallvec::SmallVec; -// use std::{any::Any, borrow::Cow, cell::Cell, path::Path}; +#[cfg(any(test, feature = "test-support"))] +pub mod test { + use super::{Item, ItemEvent}; + use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use gpui::{ + AnyElement, AppContext, Context as _, Div, EntityId, EventEmitter, FocusableView, + IntoElement, Model, Render, SharedString, Task, View, ViewContext, VisualContext, WeakView, + }; + use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; + use std::{any::Any, cell::Cell, path::Path}; -// pub struct TestProjectItem { -// pub entry_id: Option, -// pub project_path: Option, -// } + pub struct TestProjectItem { + pub entry_id: Option, + pub project_path: Option, + } -// pub struct TestItem { -// pub workspace_id: WorkspaceId, -// pub state: String, -// pub label: String, -// pub save_count: usize, -// pub save_as_count: usize, -// pub reload_count: usize, -// pub is_dirty: bool, -// pub is_singleton: bool, -// pub has_conflict: bool, -// pub project_items: Vec>, -// pub nav_history: Option, -// pub tab_descriptions: Option>, -// pub tab_detail: Cell>, -// } + pub struct TestItem { + pub workspace_id: WorkspaceId, + pub state: String, + pub label: String, + pub save_count: usize, + pub save_as_count: usize, + pub reload_count: usize, + pub is_dirty: bool, + pub is_singleton: bool, + pub has_conflict: bool, + pub project_items: Vec>, + pub nav_history: Option, + pub tab_descriptions: Option>, + pub tab_detail: Cell>, + focus_handle: gpui::FocusHandle, + } -// impl Entity for TestProjectItem { -// type Event = (); -// } + impl project::Item for TestProjectItem { + fn entry_id(&self, _: &AppContext) -> Option { + self.entry_id + } -// impl project2::Item for TestProjectItem { -// fn entry_id(&self, _: &AppContext) -> Option { -// self.entry_id -// } + fn project_path(&self, _: &AppContext) -> Option { + self.project_path.clone() + } + } -// fn project_path(&self, _: &AppContext) -> Option { -// self.project_path.clone() -// } -// } + pub enum TestItemEvent { + Edit, + } -// pub enum TestItemEvent { -// Edit, -// } + // impl Clone for TestItem { + // fn clone(&self) -> Self { + // Self { + // state: self.state.clone(), + // label: self.label.clone(), + // save_count: self.save_count, + // save_as_count: self.save_as_count, + // reload_count: self.reload_count, + // is_dirty: self.is_dirty, + // is_singleton: self.is_singleton, + // has_conflict: self.has_conflict, + // project_items: self.project_items.clone(), + // nav_history: None, + // tab_descriptions: None, + // tab_detail: Default::default(), + // workspace_id: self.workspace_id, + // focus_handle: self.focus_handle.clone(), + // } + // } + // } -// impl Clone for TestItem { -// fn clone(&self) -> Self { -// Self { -// state: self.state.clone(), -// label: self.label.clone(), -// save_count: self.save_count, -// save_as_count: self.save_as_count, -// reload_count: self.reload_count, -// is_dirty: self.is_dirty, -// is_singleton: self.is_singleton, -// has_conflict: self.has_conflict, -// project_items: self.project_items.clone(), -// nav_history: None, -// tab_descriptions: None, -// tab_detail: Default::default(), -// workspace_id: self.workspace_id, -// } -// } -// } + impl TestProjectItem { + pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { + let entry_id = Some(ProjectEntryId::from_proto(id)); + let project_path = Some(ProjectPath { + worktree_id: WorktreeId::from_usize(0), + path: Path::new(path).into(), + }); + cx.build_model(|_| Self { + entry_id, + project_path, + }) + } -// impl TestProjectItem { -// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model { -// let entry_id = Some(ProjectEntryId::from_proto(id)); -// let project_path = Some(ProjectPath { -// worktree_id: WorktreeId::from_usize(0), -// path: Path::new(path).into(), -// }); -// cx.add_model(|_| Self { -// entry_id, -// project_path, -// }) -// } + pub fn new_untitled(cx: &mut AppContext) -> Model { + cx.build_model(|_| Self { + project_path: None, + entry_id: None, + }) + } + } -// pub fn new_untitled(cx: &mut AppContext) -> Model { -// cx.add_model(|_| Self { -// project_path: None, -// entry_id: None, -// }) -// } -// } + impl TestItem { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + state: String::new(), + label: String::new(), + save_count: 0, + save_as_count: 0, + reload_count: 0, + is_dirty: false, + has_conflict: false, + project_items: Vec::new(), + is_singleton: true, + nav_history: None, + tab_descriptions: None, + tab_detail: Default::default(), + workspace_id: 0, + focus_handle: cx.focus_handle(), + } + } -// impl TestItem { -// pub fn new() -> Self { -// Self { -// state: String::new(), -// label: String::new(), -// save_count: 0, -// save_as_count: 0, -// reload_count: 0, -// is_dirty: false, -// has_conflict: false, -// project_items: Vec::new(), -// is_singleton: true, -// nav_history: None, -// tab_descriptions: None, -// tab_detail: Default::default(), -// workspace_id: 0, -// } -// } + pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext) -> Self { + let mut this = Self::new(cx); + this.workspace_id = id; + this + } -// pub fn new_deserialized(id: WorkspaceId) -> Self { -// let mut this = Self::new(); -// this.workspace_id = id; -// this -// } + pub fn with_label(mut self, state: &str) -> Self { + self.label = state.to_string(); + self + } -// pub fn with_label(mut self, state: &str) -> Self { -// self.label = state.to_string(); -// self -// } + pub fn with_singleton(mut self, singleton: bool) -> Self { + self.is_singleton = singleton; + self + } -// pub fn with_singleton(mut self, singleton: bool) -> Self { -// self.is_singleton = singleton; -// self -// } + pub fn with_dirty(mut self, dirty: bool) -> Self { + self.is_dirty = dirty; + self + } -// pub fn with_dirty(mut self, dirty: bool) -> Self { -// self.is_dirty = dirty; -// self -// } + pub fn with_conflict(mut self, has_conflict: bool) -> Self { + self.has_conflict = has_conflict; + self + } -// pub fn with_conflict(mut self, has_conflict: bool) -> Self { -// self.has_conflict = has_conflict; -// self -// } + pub fn with_project_items(mut self, items: &[Model]) -> Self { + self.project_items.clear(); + self.project_items.extend(items.iter().cloned()); + self + } -// pub fn with_project_items(mut self, items: &[Model]) -> Self { -// self.project_items.clear(); -// self.project_items.extend(items.iter().cloned()); -// self -// } + pub fn set_state(&mut self, state: String, cx: &mut ViewContext) { + self.push_to_nav_history(cx); + self.state = state; + } -// pub fn set_state(&mut self, state: String, cx: &mut ViewContext) { -// self.push_to_nav_history(cx); -// self.state = state; -// } + fn push_to_nav_history(&mut self, cx: &mut ViewContext) { + if let Some(history) = &mut self.nav_history { + history.push(Some(Box::new(self.state.clone())), cx); + } + } + } -// fn push_to_nav_history(&mut self, cx: &mut ViewContext) { -// if let Some(history) = &mut self.nav_history { -// history.push(Some(Box::new(self.state.clone())), cx); -// } -// } -// } + impl Render for TestItem { + type Element = Div; -// impl Entity for TestItem { -// type Event = TestItemEvent; -// } + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + gpui::div() + } + } -// impl View for TestItem { -// fn ui_name() -> &'static str { -// "TestItem" -// } + impl EventEmitter for TestItem {} -// fn render(&mut self, _: &mut ViewContext) -> AnyElement { -// Empty::new().into_any() -// } -// } + impl FocusableView for TestItem { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } + } -// impl Item for TestItem { -// fn tab_description(&self, detail: usize, _: &AppContext) -> Option> { -// self.tab_descriptions.as_ref().and_then(|descriptions| { -// let description = *descriptions.get(detail).or_else(|| descriptions.last())?; -// Some(description.into()) -// }) -// } + impl Item for TestItem { + fn tab_description(&self, detail: usize, _: &AppContext) -> Option { + self.tab_descriptions.as_ref().and_then(|descriptions| { + let description = *descriptions.get(detail).or_else(|| descriptions.last())?; + Some(description.into()) + }) + } -// fn tab_content( -// &self, -// detail: Option, -// _: &theme2::Tab, -// _: &AppContext, -// ) -> AnyElement { -// self.tab_detail.set(detail); -// Empty::new().into_any() -// } + fn tab_content( + &self, + detail: Option, + cx: &ui::prelude::WindowContext, + ) -> AnyElement { + self.tab_detail.set(detail); + gpui::div().into_any_element() + } -// fn for_each_project_item( -// &self, -// cx: &AppContext, -// f: &mut dyn FnMut(usize, &dyn project2::Item), -// ) { -// self.project_items -// .iter() -// .for_each(|item| f(item.id(), item.read(cx))) -// } + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(EntityId, &dyn project::Item), + ) { + self.project_items + .iter() + .for_each(|item| f(item.entity_id(), item.read(cx))) + } -// fn is_singleton(&self, _: &AppContext) -> bool { -// self.is_singleton -// } + fn is_singleton(&self, _: &AppContext) -> bool { + self.is_singleton + } -// fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { -// self.nav_history = Some(history); -// } + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } -// fn navigate(&mut self, state: Box, _: &mut ViewContext) -> bool { -// let state = *state.downcast::().unwrap_or_default(); -// if state != self.state { -// self.state = state; -// true -// } else { -// false -// } -// } + fn navigate(&mut self, state: Box, _: &mut ViewContext) -> bool { + let state = *state.downcast::().unwrap_or_default(); + if state != self.state { + self.state = state; + true + } else { + false + } + } -// fn deactivated(&mut self, cx: &mut ViewContext) { -// self.push_to_nav_history(cx); -// } + fn deactivated(&mut self, cx: &mut ViewContext) { + self.push_to_nav_history(cx); + } -// fn clone_on_split( -// &self, -// _workspace_id: WorkspaceId, -// _: &mut ViewContext, -// ) -> Option -// where -// Self: Sized, -// { -// Some(self.clone()) -// } + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.build_view(|cx| Self { + state: self.state.clone(), + label: self.label.clone(), + save_count: self.save_count, + save_as_count: self.save_as_count, + reload_count: self.reload_count, + is_dirty: self.is_dirty, + is_singleton: self.is_singleton, + has_conflict: self.has_conflict, + project_items: self.project_items.clone(), + nav_history: None, + tab_descriptions: None, + tab_detail: Default::default(), + workspace_id: self.workspace_id, + focus_handle: cx.focus_handle(), + })) + } -// fn is_dirty(&self, _: &AppContext) -> bool { -// self.is_dirty -// } + fn is_dirty(&self, _: &AppContext) -> bool { + self.is_dirty + } -// fn has_conflict(&self, _: &AppContext) -> bool { -// self.has_conflict -// } + fn has_conflict(&self, _: &AppContext) -> bool { + self.has_conflict + } -// fn can_save(&self, cx: &AppContext) -> bool { -// !self.project_items.is_empty() -// && self -// .project_items -// .iter() -// .all(|item| item.read(cx).entry_id.is_some()) -// } + fn can_save(&self, cx: &AppContext) -> bool { + !self.project_items.is_empty() + && self + .project_items + .iter() + .all(|item| item.read(cx).entry_id.is_some()) + } -// fn save( -// &mut self, -// _: Model, -// _: &mut ViewContext, -// ) -> Task> { -// self.save_count += 1; -// self.is_dirty = false; -// Task::ready(Ok(())) -// } + fn save( + &mut self, + _: Model, + _: &mut ViewContext, + ) -> Task> { + self.save_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } -// fn save_as( -// &mut self, -// _: Model, -// _: std::path::PathBuf, -// _: &mut ViewContext, -// ) -> Task> { -// self.save_as_count += 1; -// self.is_dirty = false; -// Task::ready(Ok(())) -// } + fn save_as( + &mut self, + _: Model, + _: std::path::PathBuf, + _: &mut ViewContext, + ) -> Task> { + self.save_as_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } -// fn reload( -// &mut self, -// _: Model, -// _: &mut ViewContext, -// ) -> Task> { -// self.reload_count += 1; -// self.is_dirty = false; -// Task::ready(Ok(())) -// } + fn reload( + &mut self, + _: Model, + _: &mut ViewContext, + ) -> Task> { + self.reload_count += 1; + self.is_dirty = false; + Task::ready(Ok(())) + } -// fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> { -// [ItemEvent::UpdateTab, ItemEvent::Edit].into() -// } + fn serialized_item_kind() -> Option<&'static str> { + Some("TestItem") + } -// fn serialized_item_kind() -> Option<&'static str> { -// Some("TestItem") -// } - -// fn deserialize( -// _project: Model, -// _workspace: WeakViewHandle, -// workspace_id: WorkspaceId, -// _item_id: ItemId, -// cx: &mut ViewContext, -// ) -> Task>> { -// let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id)); -// Task::Ready(Some(anyhow::Ok(view))) -// } -// } -// } + fn deserialize( + _project: Model, + _workspace: WeakView, + workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let view = cx.build_view(|cx| Self::new_deserialized(workspace_id, cx)); + Task::Ready(Some(anyhow::Ok(view))) + } + } +} diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index aa91cfc91b..f16a4fcf09 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -12,9 +12,9 @@ use gpui::{ ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; -use project2::{Project, ProjectEntryId, ProjectPath}; +use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; -use settings2::Settings; +use settings::Settings; use std::{ any::Any, cmp, fmt, mem, diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index 7562c00aeb..c98fac00c6 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -1,7 +1,7 @@ use crate::{AppState, FollowerState, Pane, Workspace}; use anyhow::{anyhow, bail, Result}; use collections::HashMap; -use db2::sqlez::{ +use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; @@ -9,7 +9,7 @@ use gpui::{ point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext, }; use parking_lot::Mutex; -use project2::Project; +use project::Project; use serde::Deserialize; use std::sync::Arc; use ui::prelude::*; diff --git a/crates/workspace2/src/persistence.rs b/crates/workspace2/src/persistence.rs index 9790495087..b842355991 100644 --- a/crates/workspace2/src/persistence.rs +++ b/crates/workspace2/src/persistence.rs @@ -5,7 +5,7 @@ pub mod model; use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; -use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::WindowBounds; use util::{unzip_option, ResultExt}; @@ -552,7 +552,7 @@ impl WorkspaceDb { #[cfg(test)] mod tests { use super::*; - use db2::open_test_db; + use db::open_test_db; use gpui; #[gpui::test] diff --git a/crates/workspace2/src/persistence/model.rs b/crates/workspace2/src/persistence/model.rs index fde052706b..74304d3c8e 100644 --- a/crates/workspace2/src/persistence/model.rs +++ b/crates/workspace2/src/persistence/model.rs @@ -3,12 +3,12 @@ use crate::{ }; use anyhow::{Context, Result}; use async_recursion::async_recursion; -use db2::sqlez::{ +use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds}; -use project2::Project; +use project::Project; use std::{ path::{Path, PathBuf}, sync::Arc, diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs index 6d1c112b71..eadd602c84 100644 --- a/crates/workspace2/src/searchable.rs +++ b/crates/workspace2/src/searchable.rs @@ -4,7 +4,7 @@ use gpui::{ AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView, WindowContext, }; -use project2::search::SearchQuery; +use project::search::SearchQuery; use crate::{ item::{Item, WeakItemHandle}, diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index cbd3e4309c..597b448267 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -16,7 +16,7 @@ mod workspace_settings; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; -use client2::{ +use client::{ proto::{self, PeerId}, Client, TypedEnvelope, User, UserStore, }; @@ -37,7 +37,7 @@ use gpui::{ }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; -use language2::{LanguageRegistry, Rope}; +use language::{LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; @@ -49,9 +49,9 @@ pub use persistence::{ WorkspaceDb, DB, }; use postage::stream::Stream; -use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; +use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; -use settings2::Settings; +use settings::Settings; use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ @@ -62,7 +62,7 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, time::Duration, }; -use theme2::{ActiveTheme, ThemeSettings}; +use theme::{ActiveTheme, ThemeSettings}; pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use util::ResultExt; @@ -301,7 +301,7 @@ pub struct AppState { pub client: Arc, pub user_store: Model, pub workspace_store: Model, - pub fs: Arc, + pub fs: Arc, pub call_factory: CallFactory, pub build_window_options: fn(Option, Option, &mut AppContext) -> WindowOptions, @@ -312,7 +312,7 @@ pub struct WorkspaceStore { workspaces: HashSet>, followers: Vec, client: Arc, - _subscriptions: Vec, + _subscriptions: Vec, } #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] @@ -388,22 +388,22 @@ impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { use node_runtime::FakeNodeRuntime; - use settings2::SettingsStore; + use settings::SettingsStore; if !cx.has_global::() { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); } - let fs = fs2::FakeFs::new(cx.background_executor().clone()); + let fs = fs::FakeFs::new(cx.background_executor().clone()); let languages = Arc::new(LanguageRegistry::test()); let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); - theme2::init(theme2::LoadThemes::JustBase, cx); - client2::init(&client, cx); + theme::init(theme::LoadThemes::JustBase, cx); + client::init(&client, cx); crate::init_settings(cx); Arc::new(Self { @@ -567,29 +567,29 @@ impl Workspace { cx.observe(&project, |_, _, cx| cx.notify()).detach(); cx.subscribe(&project, move |this, _, event, cx| { match event { - project2::Event::RemoteIdChanged(_) => { + project::Event::RemoteIdChanged(_) => { this.update_window_title(cx); } - project2::Event::CollaboratorLeft(peer_id) => { + project::Event::CollaboratorLeft(peer_id) => { this.collaborator_left(*peer_id, cx); } - project2::Event::WorktreeRemoved(_) | project2::Event::WorktreeAdded => { + project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { this.update_window_title(cx); this.serialize_workspace(cx); } - project2::Event::DisconnectedFromHost => { + project::Event::DisconnectedFromHost => { this.update_window_edited(cx); cx.blur(); } - project2::Event::Closed => { + project::Event::Closed => { cx.remove_window(); } - project2::Event::DeletedEntry(entry_id) => { + project::Event::DeletedEntry(entry_id) => { for pane in this.panes.iter() { pane.update(cx, |pane, cx| { pane.handle_deleted_project_item(*entry_id, cx) @@ -597,7 +597,7 @@ impl Workspace { } } - project2::Event::Notification(message) => this.show_notification(0, cx, |cx| { + project::Event::Notification(message) => this.show_notification(0, cx, |cx| { cx.build_view(|_| MessageNotification::new(message.clone())) }), @@ -1450,7 +1450,7 @@ impl Workspace { .map(|entry| entry.id); if let Some(entry_id) = entry_id { workspace.project.update(cx, |_, cx| { - cx.emit(project2::Event::ActiveEntryChanged(Some(entry_id))); + cx.emit(project::Event::ActiveEntryChanged(Some(entry_id))); }) } }) @@ -1812,8 +1812,7 @@ impl Workspace { }); cx.subscribe(&pane, Self::handle_pane_event).detach(); self.panes.push(pane.clone()); - // todo!() - // cx.focus(&pane); + cx.focus_view(&pane); cx.emit(Event::PaneAdded(pane.clone())); pane } @@ -1988,7 +1987,7 @@ impl Workspace { where T: ProjectItem, { - use project2::Item as _; + use project::Item as _; let entry_id = project_item.read(cx).entry_id(cx); if let Some(item) = entry_id @@ -2013,7 +2012,7 @@ impl Workspace { where T: ProjectItem, { - use project2::Item as _; + use project::Item as _; let entry_id = project_item.read(cx).entry_id(cx); if let Some(item) = entry_id @@ -3673,7 +3672,7 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA workspace .update(cx, |workspace, cx| { - if (*db2::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { + if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { workspace.show_notification_once(0, cx, |cx| { cx.build_view(|_| { MessageNotification::new("Failed to load the database file.") @@ -4554,960 +4553,959 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { Some(size((width as f64).into(), (height as f64).into())) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// dock::test::TestPanel, -// item::test::{TestItem, TestItemEvent, TestProjectItem}, -// }; -// use fs::FakeFs; -// use gpui::{executor::Deterministic, test::EmptyView, TestAppContext}; -// use project::{Project, ProjectEntryId}; -// use serde_json::json; -// use settings::SettingsStore; -// use std::{cell::RefCell, rc::Rc}; - -// #[gpui::test] -// async fn test_tab_disambiguation(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); - -// // Adding an item with no ambiguity renders the tab without detail. -// let item1 = window.build_view(cx, |_| { -// let mut item = TestItem::new(); -// item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); -// item -// }); -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item1.clone()), cx); -// }); -// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None)); - -// // Adding an item that creates ambiguity increases the level of detail on -// // both tabs. -// let item2 = window.build_view(cx, |_| { -// let mut item = TestItem::new(); -// item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); -// item -// }); -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item2.clone()), cx); -// }); -// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); -// item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - -// // Adding an item that creates ambiguity increases the level of detail only -// // on the ambiguous tabs. In this case, the ambiguity can't be resolved so -// // we stop at the highest detail available. -// let item3 = window.build_view(cx, |_| { -// let mut item = TestItem::new(); -// item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); -// item -// }); -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item3.clone()), cx); -// }); -// item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); -// item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); -// item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); -// } - -// #[gpui::test] -// async fn test_tracking_active_path(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// "one.txt": "", -// "two.txt": "", -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "three.txt": "", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["root1".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let worktree_id = project.read_with(cx, |project, cx| { -// project.worktrees().next().unwrap().read(cx).id() -// }); - -// let item1 = window.build_view(cx, |cx| { -// TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) -// }); -// let item2 = window.build_view(cx, |cx| { -// TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) -// }); - -// // Add an item to an empty pane -// workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); -// project.read_with(cx, |project, cx| { -// assert_eq!( -// project.active_entry(), -// project -// .entry_for_path(&(worktree_id, "one.txt").into(), cx) -// .map(|e| e.id) -// ); -// }); -// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); - -// // Add a second item to a non-empty pane -// workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); -// assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1")); -// project.read_with(cx, |project, cx| { -// assert_eq!( -// project.active_entry(), -// project -// .entry_for_path(&(worktree_id, "two.txt").into(), cx) -// .map(|e| e.id) -// ); -// }); - -// // Close the active item -// pane.update(cx, |pane, cx| { -// pane.close_active_item(&Default::default(), cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); -// project.read_with(cx, |project, cx| { -// assert_eq!( -// project.active_entry(), -// project -// .entry_for_path(&(worktree_id, "one.txt").into(), cx) -// .map(|e| e.id) -// ); -// }); - -// // Add a project folder -// project -// .update(cx, |project, cx| { -// project.find_or_create_local_worktree("/root2", true, cx) -// }) -// .await -// .unwrap(); -// assert_eq!( -// window.current_title(cx).as_deref(), -// Some("one.txt — root1, root2") -// ); - -// // Remove a project folder -// project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); -// assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2")); -// } - -// #[gpui::test] -// async fn test_close_window(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree("/root", json!({ "one": "" })).await; - -// let project = Project::test(fs, ["root".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); - -// // When there are no dirty items, there's nothing to do. -// let item1 = window.build_view(cx, |_| TestItem::new()); -// workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); -// let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); -// assert!(task.await.unwrap()); - -// // When there are dirty untitled items, prompt to save each one. If the user -// // cancels any prompt, then abort. -// let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true)); -// let item3 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) -// }); -// workspace.update(cx, |w, cx| { -// w.add_item(Box::new(item2.clone()), cx); -// w.add_item(Box::new(item3.clone()), cx); -// }); -// let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(2, cx); // cancel save all -// cx.foreground().run_until_parked(); -// window.simulate_prompt_answer(2, cx); // cancel save all -// cx.foreground().run_until_parked(); -// assert!(!window.has_pending_prompt(cx)); -// assert!(!task.await.unwrap()); -// } - -// #[gpui::test] -// async fn test_close_pane_items(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, None, cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let item1 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) -// }); -// let item2 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_conflict(true) -// .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) -// }); -// let item3 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_conflict(true) -// .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) -// }); -// let item4 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_project_items(&[TestProjectItem::new_untitled(cx)]) -// }); -// let pane = workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item1.clone()), cx); -// workspace.add_item(Box::new(item2.clone()), cx); -// workspace.add_item(Box::new(item3.clone()), cx); -// workspace.add_item(Box::new(item4.clone()), cx); -// workspace.active_pane().clone() -// }); - -// let close_items = pane.update(cx, |pane, cx| { -// pane.activate_item(1, true, true, cx); -// assert_eq!(pane.active_item().unwrap().id(), item2.id()); -// let item1_id = item1.id(); -// let item3_id = item3.id(); -// let item4_id = item4.id(); -// pane.close_items(cx, SaveIntent::Close, move |id| { -// [item1_id, item3_id, item4_id].contains(&id) -// }) -// }); -// cx.foreground().run_until_parked(); - -// assert!(window.has_pending_prompt(cx)); -// // Ignore "Save all" prompt -// window.simulate_prompt_answer(2, cx); -// cx.foreground().run_until_parked(); -// // There's a prompt to save item 1. -// pane.read_with(cx, |pane, _| { -// assert_eq!(pane.items_len(), 4); -// assert_eq!(pane.active_item().unwrap().id(), item1.id()); -// }); -// // Confirm saving item 1. -// window.simulate_prompt_answer(0, cx); -// cx.foreground().run_until_parked(); - -// // Item 1 is saved. There's a prompt to save item 3. -// pane.read_with(cx, |pane, cx| { -// assert_eq!(item1.read(cx).save_count, 1); -// assert_eq!(item1.read(cx).save_as_count, 0); -// assert_eq!(item1.read(cx).reload_count, 0); -// assert_eq!(pane.items_len(), 3); -// assert_eq!(pane.active_item().unwrap().id(), item3.id()); -// }); -// assert!(window.has_pending_prompt(cx)); - -// // Cancel saving item 3. -// window.simulate_prompt_answer(1, cx); -// cx.foreground().run_until_parked(); - -// // Item 3 is reloaded. There's a prompt to save item 4. -// pane.read_with(cx, |pane, cx| { -// assert_eq!(item3.read(cx).save_count, 0); -// assert_eq!(item3.read(cx).save_as_count, 0); -// assert_eq!(item3.read(cx).reload_count, 1); -// assert_eq!(pane.items_len(), 2); -// assert_eq!(pane.active_item().unwrap().id(), item4.id()); -// }); -// assert!(window.has_pending_prompt(cx)); - -// // Confirm saving item 4. -// window.simulate_prompt_answer(0, cx); -// cx.foreground().run_until_parked(); - -// // There's a prompt for a path for item 4. -// cx.simulate_new_path_selection(|_| Some(Default::default())); -// close_items.await.unwrap(); - -// // The requested items are closed. -// pane.read_with(cx, |pane, cx| { -// assert_eq!(item4.read(cx).save_count, 0); -// assert_eq!(item4.read(cx).save_as_count, 1); -// assert_eq!(item4.read(cx).reload_count, 0); -// assert_eq!(pane.items_len(), 1); -// assert_eq!(pane.active_item().unwrap().id(), item2.id()); -// }); -// } - -// #[gpui::test] -// async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// // Create several workspace items with single project entries, and two -// // workspace items with multiple project entries. -// let single_entry_items = (0..=4) -// .map(|project_entry_id| { -// window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_project_items(&[TestProjectItem::new( -// project_entry_id, -// &format!("{project_entry_id}.txt"), -// cx, -// )]) -// }) -// }) -// .collect::>(); -// let item_2_3 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_singleton(false) -// .with_project_items(&[ -// single_entry_items[2].read(cx).project_items[0].clone(), -// single_entry_items[3].read(cx).project_items[0].clone(), -// ]) -// }); -// let item_3_4 = window.build_view(cx, |cx| { -// TestItem::new() -// .with_dirty(true) -// .with_singleton(false) -// .with_project_items(&[ -// single_entry_items[3].read(cx).project_items[0].clone(), -// single_entry_items[4].read(cx).project_items[0].clone(), -// ]) -// }); - -// // Create two panes that contain the following project entries: -// // left pane: -// // multi-entry items: (2, 3) -// // single-entry items: 0, 1, 2, 3, 4 -// // right pane: -// // single-entry items: 1 -// // multi-entry items: (3, 4) -// let left_pane = workspace.update(cx, |workspace, cx| { -// let left_pane = workspace.active_pane().clone(); -// workspace.add_item(Box::new(item_2_3.clone()), cx); -// for item in single_entry_items { -// workspace.add_item(Box::new(item), cx); -// } -// left_pane.update(cx, |pane, cx| { -// pane.activate_item(2, true, true, cx); -// }); - -// workspace -// .split_and_clone(left_pane.clone(), SplitDirection::Right, cx) -// .unwrap(); - -// left_pane -// }); - -// //Need to cause an effect flush in order to respect new focus -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item_3_4.clone()), cx); -// cx.focus(&left_pane); -// }); - -// // When closing all of the items in the left pane, we should be prompted twice: -// // once for project entry 0, and once for project entry 2. After those two -// // prompts, the task should complete. - -// let close = left_pane.update(cx, |pane, cx| { -// pane.close_items(cx, SaveIntent::Close, move |_| true) -// }); -// cx.foreground().run_until_parked(); -// // Discard "Save all" prompt -// window.simulate_prompt_answer(2, cx); - -// cx.foreground().run_until_parked(); -// left_pane.read_with(cx, |pane, cx| { -// assert_eq!( -// pane.active_item().unwrap().project_entry_ids(cx).as_slice(), -// &[ProjectEntryId::from_proto(0)] -// ); -// }); -// window.simulate_prompt_answer(0, cx); - -// cx.foreground().run_until_parked(); -// left_pane.read_with(cx, |pane, cx| { -// assert_eq!( -// pane.active_item().unwrap().project_entry_ids(cx).as_slice(), -// &[ProjectEntryId::from_proto(2)] -// ); -// }); -// window.simulate_prompt_answer(0, cx); - -// cx.foreground().run_until_parked(); -// close.await.unwrap(); -// left_pane.read_with(cx, |pane, _| { -// assert_eq!(pane.items_len(), 0); -// }); -// } - -// #[gpui::test] -// async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - -// let item = window.build_view(cx, |cx| { -// TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) -// }); -// let item_id = item.id(); -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item.clone()), cx); -// }); - -// // Autosave on window change. -// item.update(cx, |item, cx| { -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.autosave = Some(AutosaveSetting::OnWindowChange); -// }) -// }); -// item.is_dirty = true; -// }); - -// // Deactivating the window saves the file. -// window.simulate_deactivation(cx); -// deterministic.run_until_parked(); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); - -// // Autosave on focus change. -// item.update(cx, |item, cx| { -// cx.focus_self(); -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.autosave = Some(AutosaveSetting::OnFocusChange); -// }) -// }); -// item.is_dirty = true; -// }); - -// // Blurring the item saves the file. -// item.update(cx, |_, cx| cx.blur()); -// deterministic.run_until_parked(); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); - -// // Deactivating the window still saves the file. -// window.simulate_activation(cx); -// item.update(cx, |item, cx| { -// cx.focus_self(); -// item.is_dirty = true; -// }); -// window.simulate_deactivation(cx); - -// deterministic.run_until_parked(); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); - -// // Autosave after delay. -// item.update(cx, |item, cx| { -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); -// }) -// }); -// item.is_dirty = true; -// cx.emit(TestItemEvent::Edit); -// }); - -// // Delay hasn't fully expired, so the file is still dirty and unsaved. -// deterministic.advance_clock(Duration::from_millis(250)); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); - -// // After delay expires, the file is saved. -// deterministic.advance_clock(Duration::from_millis(250)); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); - -// // Autosave on focus change, ensuring closing the tab counts as such. -// item.update(cx, |item, cx| { -// cx.update_global(|settings: &mut SettingsStore, cx| { -// settings.update_user_settings::(cx, |settings| { -// settings.autosave = Some(AutosaveSetting::OnFocusChange); -// }) -// }); -// item.is_dirty = true; -// }); - -// pane.update(cx, |pane, cx| { -// pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) -// }) -// .await -// .unwrap(); -// assert!(!window.has_pending_prompt(cx)); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - -// // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item.clone()), cx); -// }); -// item.update(cx, |item, cx| { -// item.project_items[0].update(cx, |item, _| { -// item.entry_id = None; -// }); -// item.is_dirty = true; -// cx.blur(); -// }); -// deterministic.run_until_parked(); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - -// // Ensure autosave is prevented for deleted files also when closing the buffer. -// let _close_items = pane.update(cx, |pane, cx| { -// pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) -// }); -// deterministic.run_until_parked(); -// assert!(window.has_pending_prompt(cx)); -// item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); -// } - -// #[gpui::test] -// async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let item = window.build_view(cx, |cx| { -// TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) -// }); -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone()); -// let toolbar_notify_count = Rc::new(RefCell::new(0)); - -// workspace.update(cx, |workspace, cx| { -// workspace.add_item(Box::new(item.clone()), cx); -// let toolbar_notification_count = toolbar_notify_count.clone(); -// cx.observe(&toolbar, move |_, _, _| { -// *toolbar_notification_count.borrow_mut() += 1 -// }) -// .detach(); -// }); - -// pane.read_with(cx, |pane, _| { -// assert!(!pane.can_navigate_backward()); -// assert!(!pane.can_navigate_forward()); -// }); - -// item.update(cx, |item, cx| { -// item.set_state("one".to_string(), cx); -// }); - -// // Toolbar must be notified to re-render the navigation buttons -// assert_eq!(*toolbar_notify_count.borrow(), 1); - -// pane.read_with(cx, |pane, _| { -// assert!(pane.can_navigate_backward()); -// assert!(!pane.can_navigate_forward()); -// }); - -// workspace -// .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) -// .await -// .unwrap(); - -// assert_eq!(*toolbar_notify_count.borrow(), 3); -// pane.read_with(cx, |pane, _| { -// assert!(!pane.can_navigate_backward()); -// assert!(pane.can_navigate_forward()); -// }); -// } - -// #[gpui::test] -// async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { -// init_test(cx); -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let panel = workspace.update(cx, |workspace, cx| { -// let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right)); -// workspace.add_panel(panel.clone(), cx); - -// workspace -// .right_dock() -// .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); - -// panel -// }); - -// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); -// pane.update(cx, |pane, cx| { -// let item = cx.build_view(|_| TestItem::new()); -// pane.add_item(Box::new(item), true, true, None, cx); -// }); - -// // Transfer focus from center to panel -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(!panel.is_zoomed(cx)); -// assert!(panel.has_focus(cx)); -// }); - -// // Transfer focus from panel to center -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(!panel.is_zoomed(cx)); -// assert!(!panel.has_focus(cx)); -// }); - -// // Close the dock -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(!workspace.right_dock().read(cx).is_open()); -// assert!(!panel.is_zoomed(cx)); -// assert!(!panel.has_focus(cx)); -// }); - -// // Open the dock -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(!panel.is_zoomed(cx)); -// assert!(panel.has_focus(cx)); -// }); - -// // Focus and zoom panel -// panel.update(cx, |panel, cx| { -// cx.focus_self(); -// panel.set_zoomed(true, cx) -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(panel.is_zoomed(cx)); -// assert!(panel.has_focus(cx)); -// }); - -// // Transfer focus to the center closes the dock -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(!workspace.right_dock().read(cx).is_open()); -// assert!(panel.is_zoomed(cx)); -// assert!(!panel.has_focus(cx)); -// }); - -// // Transferring focus back to the panel keeps it zoomed -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_panel_focus::(cx); -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(panel.is_zoomed(cx)); -// assert!(panel.has_focus(cx)); -// }); - -// // Close the dock while it is zoomed -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx) -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(!workspace.right_dock().read(cx).is_open()); -// assert!(panel.is_zoomed(cx)); -// assert!(workspace.zoomed.is_none()); -// assert!(!panel.has_focus(cx)); -// }); - -// // Opening the dock, when it's zoomed, retains focus -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx) -// }); - -// workspace.read_with(cx, |workspace, cx| { -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(panel.is_zoomed(cx)); -// assert!(workspace.zoomed.is_some()); -// assert!(panel.has_focus(cx)); -// }); - -// // Unzoom and close the panel, zoom the active pane. -// panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx) -// }); -// pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); - -// // Opening a dock unzooms the pane. -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_dock(DockPosition::Right, cx) -// }); -// workspace.read_with(cx, |workspace, cx| { -// let pane = pane.read(cx); -// assert!(!pane.is_zoomed()); -// assert!(!pane.has_focus()); -// assert!(workspace.right_dock().read(cx).is_open()); -// assert!(workspace.zoomed.is_none()); -// }); -// } - -// #[gpui::test] -// async fn test_panels(cx: &mut gpui::TestAppContext) { -// init_test(cx); -// let fs = FakeFs::new(cx.background()); - -// let project = Project::test(fs, [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); -// let workspace = window.root(cx); - -// let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { -// // Add panel_1 on the left, panel_2 on the right. -// let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left)); -// workspace.add_panel(panel_1.clone(), cx); -// workspace -// .left_dock() -// .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); -// let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right)); -// workspace.add_panel(panel_2.clone(), cx); -// workspace -// .right_dock() -// .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); - -// let left_dock = workspace.left_dock(); -// assert_eq!( -// left_dock.read(cx).visible_panel().unwrap().id(), -// panel_1.id() -// ); -// assert_eq!( -// left_dock.read(cx).active_panel_size(cx).unwrap(), -// panel_1.size(cx) -// ); - -// left_dock.update(cx, |left_dock, cx| { -// left_dock.resize_active_panel(Some(1337.), cx) -// }); -// assert_eq!( -// workspace -// .right_dock() -// .read(cx) -// .visible_panel() -// .unwrap() -// .id(), -// panel_2.id() -// ); - -// (panel_1, panel_2) -// }); - -// // Move panel_1 to the right -// panel_1.update(cx, |panel_1, cx| { -// panel_1.set_position(DockPosition::Right, cx) -// }); - -// workspace.update(cx, |workspace, cx| { -// // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. -// // Since it was the only panel on the left, the left dock should now be closed. -// assert!(!workspace.left_dock().read(cx).is_open()); -// assert!(workspace.left_dock().read(cx).visible_panel().is_none()); -// let right_dock = workspace.right_dock(); -// assert_eq!( -// right_dock.read(cx).visible_panel().unwrap().id(), -// panel_1.id() -// ); -// assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); - -// // Now we move panel_2 to the left -// panel_2.set_position(DockPosition::Left, cx); -// }); - -// workspace.update(cx, |workspace, cx| { -// // Since panel_2 was not visible on the right, we don't open the left dock. -// assert!(!workspace.left_dock().read(cx).is_open()); -// // And the right dock is unaffected in it's displaying of panel_1 -// assert!(workspace.right_dock().read(cx).is_open()); -// assert_eq!( -// workspace -// .right_dock() -// .read(cx) -// .visible_panel() -// .unwrap() -// .id(), -// panel_1.id() -// ); -// }); - -// // Move panel_1 back to the left -// panel_1.update(cx, |panel_1, cx| { -// panel_1.set_position(DockPosition::Left, cx) -// }); - -// workspace.update(cx, |workspace, cx| { -// // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. -// let left_dock = workspace.left_dock(); -// assert!(left_dock.read(cx).is_open()); -// assert_eq!( -// left_dock.read(cx).visible_panel().unwrap().id(), -// panel_1.id() -// ); -// assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); -// // And right the dock should be closed as it no longer has any panels. -// assert!(!workspace.right_dock().read(cx).is_open()); - -// // Now we move panel_1 to the bottom -// panel_1.set_position(DockPosition::Bottom, cx); -// }); - -// workspace.update(cx, |workspace, cx| { -// // Since panel_1 was visible on the left, we close the left dock. -// assert!(!workspace.left_dock().read(cx).is_open()); -// // The bottom dock is sized based on the panel's default size, -// // since the panel orientation changed from vertical to horizontal. -// let bottom_dock = workspace.bottom_dock(); -// assert_eq!( -// bottom_dock.read(cx).active_panel_size(cx).unwrap(), -// panel_1.size(cx), -// ); -// // Close bottom dock and move panel_1 back to the left. -// bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); -// panel_1.set_position(DockPosition::Left, cx); -// }); - -// // Emit activated event on panel 1 -// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); - -// // Now the left dock is open and panel_1 is active and focused. -// workspace.read_with(cx, |workspace, cx| { -// let left_dock = workspace.left_dock(); -// assert!(left_dock.read(cx).is_open()); -// assert_eq!( -// left_dock.read(cx).visible_panel().unwrap().id(), -// panel_1.id() -// ); -// assert!(panel_1.is_focused(cx)); -// }); - -// // Emit closed event on panel 2, which is not active -// panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); - -// // Wo don't close the left dock, because panel_2 wasn't the active panel -// workspace.read_with(cx, |workspace, cx| { -// let left_dock = workspace.left_dock(); -// assert!(left_dock.read(cx).is_open()); -// assert_eq!( -// left_dock.read(cx).visible_panel().unwrap().id(), -// panel_1.id() -// ); -// }); - -// // Emitting a ZoomIn event shows the panel as zoomed. -// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); -// assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); -// }); - -// // Move panel to another dock while it is zoomed -// panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); -// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); -// }); - -// // If focus is transferred to another view that's not a panel or another pane, we still show -// // the panel as zoomed. -// let focus_receiver = window.build_view(cx, |_| EmptyView); -// focus_receiver.update(cx, |_, cx| cx.focus_self()); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); -// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); -// }); - -// // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. -// workspace.update(cx, |_, cx| cx.focus_self()); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, None); -// assert_eq!(workspace.zoomed_position, None); -// }); - -// // If focus is transferred again to another view that's not a panel or a pane, we won't -// // show the panel as zoomed because it wasn't zoomed before. -// focus_receiver.update(cx, |_, cx| cx.focus_self()); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, None); -// assert_eq!(workspace.zoomed_position, None); -// }); - -// // When focus is transferred back to the panel, it is zoomed again. -// panel_1.update(cx, |_, cx| cx.focus_self()); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); -// assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); -// }); - -// // Emitting a ZoomOut event unzooms the panel. -// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); -// workspace.read_with(cx, |workspace, _| { -// assert_eq!(workspace.zoomed, None); -// assert_eq!(workspace.zoomed_position, None); -// }); - -// // Emit closed event on panel 1, which is active -// panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); - -// // Now the left dock is closed, because panel_1 was the active panel -// workspace.read_with(cx, |workspace, cx| { -// let right_dock = workspace.right_dock(); -// assert!(!right_dock.read(cx).is_open()); -// }); -// } - -// pub fn init_test(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// language::init(cx); -// crate::init_settings(cx); -// Project::init_settings(cx); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + dock::test::TestPanel, + item::{ + test::{TestItem, TestItemEvent, TestProjectItem}, + ItemEvent, + }, + }; + use fs::FakeFs; + use gpui::TestAppContext; + use project::{Project, ProjectEntryId}; + use serde_json::json; + use settings::SettingsStore; + use std::{cell::RefCell, rc::Rc}; + + #[gpui::test] + async fn test_tab_disambiguation(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // Adding an item with no ambiguity renders the tab without detail. + let item1 = cx.build_view(|cx| { + let mut item = TestItem::new(cx); + item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); + item + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item1.clone()), cx); + }); + item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0))); + + // Adding an item that creates ambiguity increases the level of detail on + // both tabs. + let item2 = cx.build_view(|cx| { + let mut item = TestItem::new(cx); + item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); + item + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item2.clone()), cx); + }); + item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); + item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); + + // Adding an item that creates ambiguity increases the level of detail only + // on the ambiguous tabs. In this case, the ambiguity can't be resolved so + // we stop at the highest detail available. + let item3 = cx.build_view(|cx| { + let mut item = TestItem::new(cx); + item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); + item + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item3.clone()), cx); + }); + item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); + item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); + item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); + } + + // #[gpui::test] + // async fn test_tracking_active_path(cx: &mut TestAppContext) { + // init_test(cx); + + // let fs = FakeFs::new(cx.background()); + // fs.insert_tree( + // "/root1", + // json!({ + // "one.txt": "", + // "two.txt": "", + // }), + // ) + // .await; + // fs.insert_tree( + // "/root2", + // json!({ + // "three.txt": "", + // }), + // ) + // .await; + + // let project = Project::test(fs, ["root1".as_ref()], cx).await; + // let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + // let workspace = window.root(cx); + // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + // let worktree_id = project.read_with(cx, |project, cx| { + // project.worktrees().next().unwrap().read(cx).id() + // }); + + // let item1 = window.build_view(cx, |cx| { + // TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) + // }); + // let item2 = window.build_view(cx, |cx| { + // TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) + // }); + + // // Add an item to an empty pane + // workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); + // project.read_with(cx, |project, cx| { + // assert_eq!( + // project.active_entry(), + // project + // .entry_for_path(&(worktree_id, "one.txt").into(), cx) + // .map(|e| e.id) + // ); + // }); + // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); + + // // Add a second item to a non-empty pane + // workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); + // assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1")); + // project.read_with(cx, |project, cx| { + // assert_eq!( + // project.active_entry(), + // project + // .entry_for_path(&(worktree_id, "two.txt").into(), cx) + // .map(|e| e.id) + // ); + // }); + + // // Close the active item + // pane.update(cx, |pane, cx| { + // pane.close_active_item(&Default::default(), cx).unwrap() + // }) + // .await + // .unwrap(); + // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); + // project.read_with(cx, |project, cx| { + // assert_eq!( + // project.active_entry(), + // project + // .entry_for_path(&(worktree_id, "one.txt").into(), cx) + // .map(|e| e.id) + // ); + // }); + + // // Add a project folder + // project + // .update(cx, |project, cx| { + // project.find_or_create_local_worktree("/root2", true, cx) + // }) + // .await + // .unwrap(); + // assert_eq!( + // window.current_title(cx).as_deref(), + // Some("one.txt — root1, root2") + // ); + + // // Remove a project folder + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2")); + // } + + // #[gpui::test] + // async fn test_close_window(cx: &mut TestAppContext) { + // init_test(cx); + + // let fs = FakeFs::new(cx.background()); + // fs.insert_tree("/root", json!({ "one": "" })).await; + + // let project = Project::test(fs, ["root".as_ref()], cx).await; + // let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + // let workspace = window.root(cx); + + // // When there are no dirty items, there's nothing to do. + // let item1 = window.build_view(cx, |_| TestItem::new()); + // workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); + // let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); + // assert!(task.await.unwrap()); + + // // When there are dirty untitled items, prompt to save each one. If the user + // // cancels any prompt, then abort. + // let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true)); + // let item3 = window.build_view(cx, |cx| { + // TestItem::new() + // .with_dirty(true) + // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + // }); + // workspace.update(cx, |w, cx| { + // w.add_item(Box::new(item2.clone()), cx); + // w.add_item(Box::new(item3.clone()), cx); + // }); + // let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); + // cx.foreground().run_until_parked(); + // window.simulate_prompt_answer(2, cx); // cancel save all + // cx.foreground().run_until_parked(); + // window.simulate_prompt_answer(2, cx); // cancel save all + // cx.foreground().run_until_parked(); + // assert!(!window.has_pending_prompt(cx)); + // assert!(!task.await.unwrap()); + // } + + // #[gpui::test] + // async fn test_close_pane_items(cx: &mut TestAppContext) { + // init_test(cx); + + // let fs = FakeFs::new(cx.background()); + + // let project = Project::test(fs, None, cx).await; + // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + // let workspace = window.root(cx); + + // let item1 = window.build_view(cx, |cx| { + // TestItem::new() + // .with_dirty(true) + // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + // }); + // let item2 = window.build_view(cx, |cx| { + // TestItem::new() + // .with_dirty(true) + // .with_conflict(true) + // .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) + // }); + // let item3 = window.build_view(cx, |cx| { + // TestItem::new() + // .with_dirty(true) + // .with_conflict(true) + // .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + // }); + // let item4 = window.build_view(cx, |cx| { + // TestItem::new() + // .with_dirty(true) + // .with_project_items(&[TestProjectItem::new_untitled(cx)]) + // }); + // let pane = workspace.update(cx, |workspace, cx| { + // workspace.add_item(Box::new(item1.clone()), cx); + // workspace.add_item(Box::new(item2.clone()), cx); + // workspace.add_item(Box::new(item3.clone()), cx); + // workspace.add_item(Box::new(item4.clone()), cx); + // workspace.active_pane().clone() + // }); + + // let close_items = pane.update(cx, |pane, cx| { + // pane.activate_item(1, true, true, cx); + // assert_eq!(pane.active_item().unwrap().id(), item2.id()); + // let item1_id = item1.id(); + // let item3_id = item3.id(); + // let item4_id = item4.id(); + // pane.close_items(cx, SaveIntent::Close, move |id| { + // [item1_id, item3_id, item4_id].contains(&id) + // }) + // }); + // cx.foreground().run_until_parked(); + + // assert!(window.has_pending_prompt(cx)); + // // Ignore "Save all" prompt + // window.simulate_prompt_answer(2, cx); + // cx.foreground().run_until_parked(); + // // There's a prompt to save item 1. + // pane.read_with(cx, |pane, _| { + // assert_eq!(pane.items_len(), 4); + // assert_eq!(pane.active_item().unwrap().id(), item1.id()); + // }); + // // Confirm saving item 1. + // window.simulate_prompt_answer(0, cx); + // cx.foreground().run_until_parked(); + + // // Item 1 is saved. There's a prompt to save item 3. + // pane.read_with(cx, |pane, cx| { + // assert_eq!(item1.read(cx).save_count, 1); + // assert_eq!(item1.read(cx).save_as_count, 0); + // assert_eq!(item1.read(cx).reload_count, 0); + // assert_eq!(pane.items_len(), 3); + // assert_eq!(pane.active_item().unwrap().id(), item3.id()); + // }); + // assert!(window.has_pending_prompt(cx)); + + // // Cancel saving item 3. + // window.simulate_prompt_answer(1, cx); + // cx.foreground().run_until_parked(); + + // // Item 3 is reloaded. There's a prompt to save item 4. + // pane.read_with(cx, |pane, cx| { + // assert_eq!(item3.read(cx).save_count, 0); + // assert_eq!(item3.read(cx).save_as_count, 0); + // assert_eq!(item3.read(cx).reload_count, 1); + // assert_eq!(pane.items_len(), 2); + // assert_eq!(pane.active_item().unwrap().id(), item4.id()); + // }); + // assert!(window.has_pending_prompt(cx)); + + // // Confirm saving item 4. + // window.simulate_prompt_answer(0, cx); + // cx.foreground().run_until_parked(); + + // // There's a prompt for a path for item 4. + // cx.simulate_new_path_selection(|_| Some(Default::default())); + // close_items.await.unwrap(); + + // // The requested items are closed. + // pane.read_with(cx, |pane, cx| { + // assert_eq!(item4.read(cx).save_count, 0); + // assert_eq!(item4.read(cx).save_as_count, 1); + // assert_eq!(item4.read(cx).reload_count, 0); + // assert_eq!(pane.items_len(), 1); + // assert_eq!(pane.active_item().unwrap().id(), item2.id()); + // }); + // } + + #[gpui::test] + async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + // Create several workspace items with single project entries, and two + // workspace items with multiple project entries. + let single_entry_items = (0..=4) + .map(|project_entry_id| { + cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_project_items(&[TestProjectItem::new( + project_entry_id, + &format!("{project_entry_id}.txt"), + cx, + )]) + }) + }) + .collect::>(); + let item_2_3 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_project_items(&[ + single_entry_items[2].read(cx).project_items[0].clone(), + single_entry_items[3].read(cx).project_items[0].clone(), + ]) + }); + let item_3_4 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_project_items(&[ + single_entry_items[3].read(cx).project_items[0].clone(), + single_entry_items[4].read(cx).project_items[0].clone(), + ]) + }); + + // Create two panes that contain the following project entries: + // left pane: + // multi-entry items: (2, 3) + // single-entry items: 0, 1, 2, 3, 4 + // right pane: + // single-entry items: 1 + // multi-entry items: (3, 4) + let left_pane = workspace.update(cx, |workspace, cx| { + let left_pane = workspace.active_pane().clone(); + workspace.add_item(Box::new(item_2_3.clone()), cx); + for item in single_entry_items { + workspace.add_item(Box::new(item), cx); + } + left_pane.update(cx, |pane, cx| { + pane.activate_item(2, true, true, cx); + }); + + let right_pane = workspace + .split_and_clone(left_pane.clone(), SplitDirection::Right, cx) + .unwrap(); + + right_pane.update(cx, |pane, cx| { + pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx); + }); + + left_pane + }); + + cx.focus_view(&left_pane); + + // When closing all of the items in the left pane, we should be prompted twice: + // once for project entry 0, and once for project entry 2. Project entries 1, + // 3, and 4 are all still open in the other paten. After those two + // prompts, the task should complete. + + let close = left_pane.update(cx, |pane, cx| { + pane.close_all_items(&CloseAllItems::default(), cx).unwrap() + }); + cx.executor().run_until_parked(); + + // Discard "Save all" prompt + cx.simulate_prompt_answer(2); + + cx.executor().run_until_parked(); + left_pane.update(cx, |pane, cx| { + assert_eq!( + pane.active_item().unwrap().project_entry_ids(cx).as_slice(), + &[ProjectEntryId::from_proto(0)] + ); + }); + cx.simulate_prompt_answer(0); + + cx.executor().run_until_parked(); + left_pane.update(cx, |pane, cx| { + assert_eq!( + pane.active_item().unwrap().project_entry_ids(cx).as_slice(), + &[ProjectEntryId::from_proto(2)] + ); + }); + cx.simulate_prompt_answer(0); + + cx.executor().run_until_parked(); + close.await.unwrap(); + left_pane.update(cx, |pane, _| { + assert_eq!(pane.items_len(), 0); + }); + } + + #[gpui::test] + async fn test_autosave(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let item = cx.build_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + }); + let item_id = item.entity_id(); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item.clone()), cx); + }); + + // Autosave on window change. + item.update(cx, |item, cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnWindowChange); + }) + }); + item.is_dirty = true; + }); + + // Deactivating the window saves the file. + cx.simulate_deactivation(); + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 1)); + + // Autosave on focus change. + item.update(cx, |item, cx| { + cx.focus_self(); + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnFocusChange); + }) + }); + item.is_dirty = true; + }); + + // Blurring the item saves the file. + item.update(cx, |_, cx| cx.blur()); + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 2)); + + // Deactivating the window still saves the file. + cx.simulate_activation(); + item.update(cx, |item, cx| { + cx.focus_self(); + item.is_dirty = true; + }); + cx.simulate_deactivation(); + + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 3)); + + // Autosave after delay. + item.update(cx, |item, cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); + }) + }); + item.is_dirty = true; + cx.emit(ItemEvent::Edit); + }); + + // Delay hasn't fully expired, so the file is still dirty and unsaved. + cx.executor().advance_clock(Duration::from_millis(250)); + item.update(cx, |item, _| assert_eq!(item.save_count, 3)); + + // After delay expires, the file is saved. + cx.executor().advance_clock(Duration::from_millis(250)); + item.update(cx, |item, _| assert_eq!(item.save_count, 4)); + + // Autosave on focus change, ensuring closing the tab counts as such. + item.update(cx, |item, cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.autosave = Some(AutosaveSetting::OnFocusChange); + }) + }); + item.is_dirty = true; + }); + + pane.update(cx, |pane, cx| { + pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) + }) + .await + .unwrap(); + assert!(!cx.has_pending_prompt()); + item.update(cx, |item, _| assert_eq!(item.save_count, 5)); + + // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item.clone()), cx); + }); + item.update(cx, |item, cx| { + item.project_items[0].update(cx, |item, _| { + item.entry_id = None; + }); + item.is_dirty = true; + cx.blur(); + }); + cx.executor().run_until_parked(); + item.update(cx, |item, _| assert_eq!(item.save_count, 5)); + + // Ensure autosave is prevented for deleted files also when closing the buffer. + let _close_items = pane.update(cx, |pane, cx| { + pane.close_items(cx, SaveIntent::Close, move |id| id == item_id) + }); + cx.executor().run_until_parked(); + assert!(cx.has_pending_prompt()); + item.update(cx, |item, _| assert_eq!(item.save_count, 5)); + } + + #[gpui::test] + async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + + let item = cx.build_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + }); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone()); + let toolbar_notify_count = Rc::new(RefCell::new(0)); + + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item.clone()), cx); + let toolbar_notification_count = toolbar_notify_count.clone(); + cx.observe(&toolbar, move |_, _, _| { + *toolbar_notification_count.borrow_mut() += 1 + }) + .detach(); + }); + + pane.update(cx, |pane, _| { + assert!(!pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + item.update(cx, |item, cx| { + item.set_state("one".to_string(), cx); + }); + + // Toolbar must be notified to re-render the navigation buttons + assert_eq!(*toolbar_notify_count.borrow(), 1); + + pane.update(cx, |pane, _| { + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + workspace + .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) + .await + .unwrap(); + + assert_eq!(*toolbar_notify_count.borrow(), 2); + pane.update(cx, |pane, _| { + assert!(!pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + } + + // #[gpui::test] + // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { + // init_test(cx); + // let fs = FakeFs::new(cx.background()); + + // let project = Project::test(fs, [], cx).await; + // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + // let workspace = window.root(cx); + + // let panel = workspace.update(cx, |workspace, cx| { + // let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right)); + // workspace.add_panel(panel.clone(), cx); + + // workspace + // .right_dock() + // .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + // panel + // }); + + // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + // pane.update(cx, |pane, cx| { + // let item = cx.build_view(|_| TestItem::new()); + // pane.add_item(Box::new(item), true, true, None, cx); + // }); + + // // Transfer focus from center to panel + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(!panel.is_zoomed(cx)); + // assert!(panel.has_focus(cx)); + // }); + + // // Transfer focus from panel to center + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(!panel.is_zoomed(cx)); + // assert!(!panel.has_focus(cx)); + // }); + + // // Close the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(!workspace.right_dock().read(cx).is_open()); + // assert!(!panel.is_zoomed(cx)); + // assert!(!panel.has_focus(cx)); + // }); + + // // Open the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(!panel.is_zoomed(cx)); + // assert!(panel.has_focus(cx)); + // }); + + // // Focus and zoom panel + // panel.update(cx, |panel, cx| { + // cx.focus_self(); + // panel.set_zoomed(true, cx) + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(panel.is_zoomed(cx)); + // assert!(panel.has_focus(cx)); + // }); + + // // Transfer focus to the center closes the dock + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(!workspace.right_dock().read(cx).is_open()); + // assert!(panel.is_zoomed(cx)); + // assert!(!panel.has_focus(cx)); + // }); + + // // Transferring focus back to the panel keeps it zoomed + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_panel_focus::(cx); + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(panel.is_zoomed(cx)); + // assert!(panel.has_focus(cx)); + // }); + + // // Close the dock while it is zoomed + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(!workspace.right_dock().read(cx).is_open()); + // assert!(panel.is_zoomed(cx)); + // assert!(workspace.zoomed.is_none()); + // assert!(!panel.has_focus(cx)); + // }); + + // // Opening the dock, when it's zoomed, retains focus + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); + + // workspace.read_with(cx, |workspace, cx| { + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(panel.is_zoomed(cx)); + // assert!(workspace.zoomed.is_some()); + // assert!(panel.has_focus(cx)); + // }); + + // // Unzoom and close the panel, zoom the active pane. + // panel.update(cx, |panel, cx| panel.set_zoomed(false, cx)); + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); + // pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx)); + + // // Opening a dock unzooms the pane. + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_dock(DockPosition::Right, cx) + // }); + // workspace.read_with(cx, |workspace, cx| { + // let pane = pane.read(cx); + // assert!(!pane.is_zoomed()); + // assert!(!pane.has_focus()); + // assert!(workspace.right_dock().read(cx).is_open()); + // assert!(workspace.zoomed.is_none()); + // }); + // } + + // #[gpui::test] + // async fn test_panels(cx: &mut gpui::TestAppContext) { + // init_test(cx); + // let fs = FakeFs::new(cx.background()); + + // let project = Project::test(fs, [], cx).await; + // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + // let workspace = window.root(cx); + + // let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { + // // Add panel_1 on the left, panel_2 on the right. + // let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left)); + // workspace.add_panel(panel_1.clone(), cx); + // workspace + // .left_dock() + // .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); + // let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right)); + // workspace.add_panel(panel_2.clone(), cx); + // workspace + // .right_dock() + // .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + // let left_dock = workspace.left_dock(); + // assert_eq!( + // left_dock.read(cx).visible_panel().unwrap().id(), + // panel_1.id() + // ); + // assert_eq!( + // left_dock.read(cx).active_panel_size(cx).unwrap(), + // panel_1.size(cx) + // ); + + // left_dock.update(cx, |left_dock, cx| { + // left_dock.resize_active_panel(Some(1337.), cx) + // }); + // assert_eq!( + // workspace + // .right_dock() + // .read(cx) + // .visible_panel() + // .unwrap() + // .id(), + // panel_2.id() + // ); + + // (panel_1, panel_2) + // }); + + // // Move panel_1 to the right + // panel_1.update(cx, |panel_1, cx| { + // panel_1.set_position(DockPosition::Right, cx) + // }); + + // workspace.update(cx, |workspace, cx| { + // // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. + // // Since it was the only panel on the left, the left dock should now be closed. + // assert!(!workspace.left_dock().read(cx).is_open()); + // assert!(workspace.left_dock().read(cx).visible_panel().is_none()); + // let right_dock = workspace.right_dock(); + // assert_eq!( + // right_dock.read(cx).visible_panel().unwrap().id(), + // panel_1.id() + // ); + // assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + + // // Now we move panel_2 to the left + // panel_2.set_position(DockPosition::Left, cx); + // }); + + // workspace.update(cx, |workspace, cx| { + // // Since panel_2 was not visible on the right, we don't open the left dock. + // assert!(!workspace.left_dock().read(cx).is_open()); + // // And the right dock is unaffected in it's displaying of panel_1 + // assert!(workspace.right_dock().read(cx).is_open()); + // assert_eq!( + // workspace + // .right_dock() + // .read(cx) + // .visible_panel() + // .unwrap() + // .id(), + // panel_1.id() + // ); + // }); + + // // Move panel_1 back to the left + // panel_1.update(cx, |panel_1, cx| { + // panel_1.set_position(DockPosition::Left, cx) + // }); + + // workspace.update(cx, |workspace, cx| { + // // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. + // let left_dock = workspace.left_dock(); + // assert!(left_dock.read(cx).is_open()); + // assert_eq!( + // left_dock.read(cx).visible_panel().unwrap().id(), + // panel_1.id() + // ); + // assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + // // And right the dock should be closed as it no longer has any panels. + // assert!(!workspace.right_dock().read(cx).is_open()); + + // // Now we move panel_1 to the bottom + // panel_1.set_position(DockPosition::Bottom, cx); + // }); + + // workspace.update(cx, |workspace, cx| { + // // Since panel_1 was visible on the left, we close the left dock. + // assert!(!workspace.left_dock().read(cx).is_open()); + // // The bottom dock is sized based on the panel's default size, + // // since the panel orientation changed from vertical to horizontal. + // let bottom_dock = workspace.bottom_dock(); + // assert_eq!( + // bottom_dock.read(cx).active_panel_size(cx).unwrap(), + // panel_1.size(cx), + // ); + // // Close bottom dock and move panel_1 back to the left. + // bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); + // panel_1.set_position(DockPosition::Left, cx); + // }); + + // // Emit activated event on panel 1 + // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); + + // // Now the left dock is open and panel_1 is active and focused. + // workspace.read_with(cx, |workspace, cx| { + // let left_dock = workspace.left_dock(); + // assert!(left_dock.read(cx).is_open()); + // assert_eq!( + // left_dock.read(cx).visible_panel().unwrap().id(), + // panel_1.id() + // ); + // assert!(panel_1.is_focused(cx)); + // }); + + // // Emit closed event on panel 2, which is not active + // panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // // Wo don't close the left dock, because panel_2 wasn't the active panel + // workspace.read_with(cx, |workspace, cx| { + // let left_dock = workspace.left_dock(); + // assert!(left_dock.read(cx).is_open()); + // assert_eq!( + // left_dock.read(cx).visible_panel().unwrap().id(), + // panel_1.id() + // ); + // }); + + // // Emitting a ZoomIn event shows the panel as zoomed. + // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); + // }); + + // // Move panel to another dock while it is zoomed + // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + // }); + + // // If focus is transferred to another view that's not a panel or another pane, we still show + // // the panel as zoomed. + // let focus_receiver = window.build_view(cx, |_| EmptyView); + // focus_receiver.update(cx, |_, cx| cx.focus_self()); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + // }); + + // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. + // workspace.update(cx, |_, cx| cx.focus_self()); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, None); + // assert_eq!(workspace.zoomed_position, None); + // }); + + // // If focus is transferred again to another view that's not a panel or a pane, we won't + // // show the panel as zoomed because it wasn't zoomed before. + // focus_receiver.update(cx, |_, cx| cx.focus_self()); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, None); + // assert_eq!(workspace.zoomed_position, None); + // }); + + // // When focus is transferred back to the panel, it is zoomed again. + // panel_1.update(cx, |_, cx| cx.focus_self()); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); + // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); + // }); + + // // Emitting a ZoomOut event unzooms the panel. + // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); + // workspace.read_with(cx, |workspace, _| { + // assert_eq!(workspace.zoomed, None); + // assert_eq!(workspace.zoomed_position, None); + // }); + + // // Emit closed event on panel 1, which is active + // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // // Now the left dock is closed, because panel_1 was the active panel + // workspace.read_with(cx, |workspace, cx| { + // let right_dock = workspace.right_dock(); + // assert!(!right_dock.read(cx).is_open()); + // }); + // } + + pub fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + crate::init_settings(cx); + Project::init_settings(cx); + }); + } +} diff --git a/crates/workspace2/src/workspace_settings.rs b/crates/workspace2/src/workspace_settings.rs index c4d1bb41cd..f3882a9dbd 100644 --- a/crates/workspace2/src/workspace_settings.rs +++ b/crates/workspace2/src/workspace_settings.rs @@ -1,6 +1,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings2::Settings; +use settings::Settings; #[derive(Deserialize)] pub struct WorkspaceSettings { From cb11c1282c4bc6f0cd765e1f2ad897172a0e8d04 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Nov 2023 14:58:14 -0800 Subject: [PATCH 16/28] Re-introduce active path tracking in workspace --- crates/gpui2/src/app/test_context.rs | 13 + crates/gpui2/src/platform/test/window.rs | 6 +- crates/gpui2/src/window.rs | 4 + crates/workspace2/src/workspace2.rs | 482 +++++++++++------------ 4 files changed, 257 insertions(+), 248 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 31917e9bf3..c915753749 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -502,6 +502,19 @@ impl<'a> VisualTestContext<'a> { self.cx.dispatch_action(self.window, action) } + pub fn window_title(&mut self) -> Option { + self.cx + .update_window(self.window, |_, cx| { + cx.window + .platform_window + .as_test() + .unwrap() + .window_title + .clone() + }) + .unwrap() + } + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { self.cx.simulate_keystrokes(self.window, keystrokes) } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index e7ca7a1616..2ad54eff0d 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -22,6 +22,7 @@ pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, + pub(crate) window_title: Option, pub(crate) input_handler: Option>>>, pub(crate) handlers: Arc>, platform: Weak, @@ -42,6 +43,7 @@ impl TestWindow { input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), + window_title: Default::default(), } } } @@ -100,8 +102,8 @@ impl PlatformWindow for TestWindow { todo!() } - fn set_title(&mut self, _title: &str) { - todo!() + fn set_title(&mut self, title: &str) { + self.window_title = Some(title.to_owned()); } fn set_edited(&mut self, _edited: bool) { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 76932f28e4..228a95d054 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -678,6 +678,10 @@ impl<'a> WindowContext<'a> { self.window.platform_window.zoom(); } + pub fn set_window_title(&mut self, title: &str) { + self.window.platform_window.set_title(title); + } + pub fn display(&self) -> Option> { self.platform .displays() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 597b448267..a485eba516 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2591,8 +2591,7 @@ impl Workspace { title.push_str(" ↗"); } - // todo!() - // cx.set_window_title(&title); + cx.set_window_title(&title); } fn update_window_edited(&mut self, cx: &mut ViewContext) { @@ -4556,12 +4555,9 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { #[cfg(test)] mod tests { use super::*; - use crate::{ - dock::test::TestPanel, - item::{ - test::{TestItem, TestItemEvent, TestProjectItem}, - ItemEvent, - }, + use crate::item::{ + test::{TestItem, TestProjectItem}, + ItemEvent, }; use fs::FakeFs; use gpui::TestAppContext; @@ -4618,244 +4614,238 @@ mod tests { item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); } - // #[gpui::test] - // async fn test_tracking_active_path(cx: &mut TestAppContext) { - // init_test(cx); + #[gpui::test] + async fn test_tracking_active_path(cx: &mut TestAppContext) { + init_test(cx); - // let fs = FakeFs::new(cx.background()); - // fs.insert_tree( - // "/root1", - // json!({ - // "one.txt": "", - // "two.txt": "", - // }), - // ) - // .await; - // fs.insert_tree( - // "/root2", - // json!({ - // "three.txt": "", - // }), - // ) - // .await; + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root1", + json!({ + "one.txt": "", + "two.txt": "", + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "three.txt": "", + }), + ) + .await; - // let project = Project::test(fs, ["root1".as_ref()], cx).await; - // let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - // let workspace = window.root(cx); - // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - // let worktree_id = project.read_with(cx, |project, cx| { - // project.worktrees().next().unwrap().read(cx).id() - // }); + let project = Project::test(fs, ["root1".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); - // let item1 = window.build_view(cx, |cx| { - // TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) - // }); - // let item2 = window.build_view(cx, |cx| { - // TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) - // }); + let item1 = cx.build_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) + }); + let item2 = cx.build_view(|cx| { + TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) + }); - // // Add an item to an empty pane - // workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); - // project.read_with(cx, |project, cx| { - // assert_eq!( - // project.active_entry(), - // project - // .entry_for_path(&(worktree_id, "one.txt").into(), cx) - // .map(|e| e.id) - // ); - // }); - // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); + // Add an item to an empty pane + workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); + project.read_with(cx, |project, cx| { + assert_eq!( + project.active_entry(), + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) + ); + }); + assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1")); - // // Add a second item to a non-empty pane - // workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); - // assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1")); - // project.read_with(cx, |project, cx| { - // assert_eq!( - // project.active_entry(), - // project - // .entry_for_path(&(worktree_id, "two.txt").into(), cx) - // .map(|e| e.id) - // ); - // }); + // Add a second item to a non-empty pane + workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); + assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1")); + project.read_with(cx, |project, cx| { + assert_eq!( + project.active_entry(), + project + .entry_for_path(&(worktree_id, "two.txt").into(), cx) + .map(|e| e.id) + ); + }); - // // Close the active item - // pane.update(cx, |pane, cx| { - // pane.close_active_item(&Default::default(), cx).unwrap() - // }) - // .await - // .unwrap(); - // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); - // project.read_with(cx, |project, cx| { - // assert_eq!( - // project.active_entry(), - // project - // .entry_for_path(&(worktree_id, "one.txt").into(), cx) - // .map(|e| e.id) - // ); - // }); + // Close the active item + pane.update(cx, |pane, cx| { + pane.close_active_item(&Default::default(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1")); + project.read_with(cx, |project, cx| { + assert_eq!( + project.active_entry(), + project + .entry_for_path(&(worktree_id, "one.txt").into(), cx) + .map(|e| e.id) + ); + }); - // // Add a project folder - // project - // .update(cx, |project, cx| { - // project.find_or_create_local_worktree("/root2", true, cx) - // }) - // .await - // .unwrap(); - // assert_eq!( - // window.current_title(cx).as_deref(), - // Some("one.txt — root1, root2") - // ); + // Add a project folder + project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root2", true, cx) + }) + .await + .unwrap(); + assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2")); - // // Remove a project folder - // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - // assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2")); - // } + // Remove a project folder + project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2")); + } - // #[gpui::test] - // async fn test_close_window(cx: &mut TestAppContext) { - // init_test(cx); + #[gpui::test] + async fn test_close_window(cx: &mut TestAppContext) { + init_test(cx); - // let fs = FakeFs::new(cx.background()); - // fs.insert_tree("/root", json!({ "one": "" })).await; + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({ "one": "" })).await; - // let project = Project::test(fs, ["root".as_ref()], cx).await; - // let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - // let workspace = window.root(cx); + let project = Project::test(fs, ["root".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); - // // When there are no dirty items, there's nothing to do. - // let item1 = window.build_view(cx, |_| TestItem::new()); - // workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); - // let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); - // assert!(task.await.unwrap()); + // When there are no dirty items, there's nothing to do. + let item1 = cx.build_view(|cx| TestItem::new(cx)); + workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); + let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); + assert!(task.await.unwrap()); - // // When there are dirty untitled items, prompt to save each one. If the user - // // cancels any prompt, then abort. - // let item2 = window.build_view(cx, |_| TestItem::new().with_dirty(true)); - // let item3 = window.build_view(cx, |cx| { - // TestItem::new() - // .with_dirty(true) - // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - // }); - // workspace.update(cx, |w, cx| { - // w.add_item(Box::new(item2.clone()), cx); - // w.add_item(Box::new(item3.clone()), cx); - // }); - // let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); - // cx.foreground().run_until_parked(); - // window.simulate_prompt_answer(2, cx); // cancel save all - // cx.foreground().run_until_parked(); - // window.simulate_prompt_answer(2, cx); // cancel save all - // cx.foreground().run_until_parked(); - // assert!(!window.has_pending_prompt(cx)); - // assert!(!task.await.unwrap()); - // } + // When there are dirty untitled items, prompt to save each one. If the user + // cancels any prompt, then abort. + let item2 = cx.build_view(|cx| TestItem::new(cx).with_dirty(true)); + let item3 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + }); + workspace.update(cx, |w, cx| { + w.add_item(Box::new(item2.clone()), cx); + w.add_item(Box::new(item3.clone()), cx); + }); + let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); + cx.executor().run_until_parked(); + cx.simulate_prompt_answer(2); // cancel save all + cx.executor().run_until_parked(); + cx.simulate_prompt_answer(2); // cancel save all + cx.executor().run_until_parked(); + assert!(!cx.has_pending_prompt()); + assert!(!task.await.unwrap()); + } - // #[gpui::test] - // async fn test_close_pane_items(cx: &mut TestAppContext) { - // init_test(cx); + #[gpui::test] + async fn test_close_pane_items(cx: &mut TestAppContext) { + init_test(cx); - // let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor()); - // let project = Project::test(fs, None, cx).await; - // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - // let workspace = window.root(cx); + let project = Project::test(fs, None, cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let item1 = window.build_view(cx, |cx| { - // TestItem::new() - // .with_dirty(true) - // .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - // }); - // let item2 = window.build_view(cx, |cx| { - // TestItem::new() - // .with_dirty(true) - // .with_conflict(true) - // .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) - // }); - // let item3 = window.build_view(cx, |cx| { - // TestItem::new() - // .with_dirty(true) - // .with_conflict(true) - // .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) - // }); - // let item4 = window.build_view(cx, |cx| { - // TestItem::new() - // .with_dirty(true) - // .with_project_items(&[TestProjectItem::new_untitled(cx)]) - // }); - // let pane = workspace.update(cx, |workspace, cx| { - // workspace.add_item(Box::new(item1.clone()), cx); - // workspace.add_item(Box::new(item2.clone()), cx); - // workspace.add_item(Box::new(item3.clone()), cx); - // workspace.add_item(Box::new(item4.clone()), cx); - // workspace.active_pane().clone() - // }); + let item1 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + }); + let item2 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_conflict(true) + .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) + }); + let item3 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_conflict(true) + .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + }); + let item4 = cx.build_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_project_items(&[TestProjectItem::new_untitled(cx)]) + }); + let pane = workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(item1.clone()), cx); + workspace.add_item(Box::new(item2.clone()), cx); + workspace.add_item(Box::new(item3.clone()), cx); + workspace.add_item(Box::new(item4.clone()), cx); + workspace.active_pane().clone() + }); - // let close_items = pane.update(cx, |pane, cx| { - // pane.activate_item(1, true, true, cx); - // assert_eq!(pane.active_item().unwrap().id(), item2.id()); - // let item1_id = item1.id(); - // let item3_id = item3.id(); - // let item4_id = item4.id(); - // pane.close_items(cx, SaveIntent::Close, move |id| { - // [item1_id, item3_id, item4_id].contains(&id) - // }) - // }); - // cx.foreground().run_until_parked(); + let close_items = pane.update(cx, |pane, cx| { + pane.activate_item(1, true, true, cx); + assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); + let item1_id = item1.item_id(); + let item3_id = item3.item_id(); + let item4_id = item4.item_id(); + pane.close_items(cx, SaveIntent::Close, move |id| { + [item1_id, item3_id, item4_id].contains(&id) + }) + }); + cx.executor().run_until_parked(); - // assert!(window.has_pending_prompt(cx)); - // // Ignore "Save all" prompt - // window.simulate_prompt_answer(2, cx); - // cx.foreground().run_until_parked(); - // // There's a prompt to save item 1. - // pane.read_with(cx, |pane, _| { - // assert_eq!(pane.items_len(), 4); - // assert_eq!(pane.active_item().unwrap().id(), item1.id()); - // }); - // // Confirm saving item 1. - // window.simulate_prompt_answer(0, cx); - // cx.foreground().run_until_parked(); + assert!(cx.has_pending_prompt()); + // Ignore "Save all" prompt + cx.simulate_prompt_answer(2); + cx.executor().run_until_parked(); + // There's a prompt to save item 1. + pane.update(cx, |pane, _| { + assert_eq!(pane.items_len(), 4); + assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id()); + }); + // Confirm saving item 1. + cx.simulate_prompt_answer(0); + cx.executor().run_until_parked(); - // // Item 1 is saved. There's a prompt to save item 3. - // pane.read_with(cx, |pane, cx| { - // assert_eq!(item1.read(cx).save_count, 1); - // assert_eq!(item1.read(cx).save_as_count, 0); - // assert_eq!(item1.read(cx).reload_count, 0); - // assert_eq!(pane.items_len(), 3); - // assert_eq!(pane.active_item().unwrap().id(), item3.id()); - // }); - // assert!(window.has_pending_prompt(cx)); + // Item 1 is saved. There's a prompt to save item 3. + pane.update(cx, |pane, cx| { + assert_eq!(item1.read(cx).save_count, 1); + assert_eq!(item1.read(cx).save_as_count, 0); + assert_eq!(item1.read(cx).reload_count, 0); + assert_eq!(pane.items_len(), 3); + assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id()); + }); + assert!(cx.has_pending_prompt()); - // // Cancel saving item 3. - // window.simulate_prompt_answer(1, cx); - // cx.foreground().run_until_parked(); + // Cancel saving item 3. + cx.simulate_prompt_answer(1); + cx.executor().run_until_parked(); - // // Item 3 is reloaded. There's a prompt to save item 4. - // pane.read_with(cx, |pane, cx| { - // assert_eq!(item3.read(cx).save_count, 0); - // assert_eq!(item3.read(cx).save_as_count, 0); - // assert_eq!(item3.read(cx).reload_count, 1); - // assert_eq!(pane.items_len(), 2); - // assert_eq!(pane.active_item().unwrap().id(), item4.id()); - // }); - // assert!(window.has_pending_prompt(cx)); + // Item 3 is reloaded. There's a prompt to save item 4. + pane.update(cx, |pane, cx| { + assert_eq!(item3.read(cx).save_count, 0); + assert_eq!(item3.read(cx).save_as_count, 0); + assert_eq!(item3.read(cx).reload_count, 1); + assert_eq!(pane.items_len(), 2); + assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id()); + }); + assert!(cx.has_pending_prompt()); - // // Confirm saving item 4. - // window.simulate_prompt_answer(0, cx); - // cx.foreground().run_until_parked(); + // Confirm saving item 4. + cx.simulate_prompt_answer(0); + cx.executor().run_until_parked(); - // // There's a prompt for a path for item 4. - // cx.simulate_new_path_selection(|_| Some(Default::default())); - // close_items.await.unwrap(); + // There's a prompt for a path for item 4. + cx.simulate_new_path_selection(|_| Some(Default::default())); + close_items.await.unwrap(); - // // The requested items are closed. - // pane.read_with(cx, |pane, cx| { - // assert_eq!(item4.read(cx).save_count, 0); - // assert_eq!(item4.read(cx).save_as_count, 1); - // assert_eq!(item4.read(cx).reload_count, 0); - // assert_eq!(pane.items_len(), 1); - // assert_eq!(pane.active_item().unwrap().id(), item2.id()); - // }); - // } + // The requested items are closed. + pane.update(cx, |pane, cx| { + assert_eq!(item4.read(cx).save_count, 0); + assert_eq!(item4.read(cx).save_as_count, 1); + assert_eq!(item4.read(cx).reload_count, 0); + assert_eq!(pane.items_len(), 1); + assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); + }); + } #[gpui::test] async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { @@ -5143,7 +5133,7 @@ mod tests { // #[gpui::test] // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { // init_test(cx); - // let fs = FakeFs::new(cx.background()); + // let fs = FakeFs::new(cx.executor()); // let project = Project::test(fs, [], cx).await; // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); @@ -5160,9 +5150,9 @@ mod tests { // panel // }); - // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + // let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); // pane.update(cx, |pane, cx| { - // let item = cx.build_view(|_| TestItem::new()); + // let item = cx.build_view(|_| TestItem::new(cx)); // pane.add_item(Box::new(item), true, true, None, cx); // }); @@ -5171,7 +5161,7 @@ mod tests { // workspace.toggle_panel_focus::(cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(!panel.is_zoomed(cx)); // assert!(panel.has_focus(cx)); @@ -5182,7 +5172,7 @@ mod tests { // workspace.toggle_panel_focus::(cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(!panel.is_zoomed(cx)); // assert!(!panel.has_focus(cx)); @@ -5193,7 +5183,7 @@ mod tests { // workspace.toggle_dock(DockPosition::Right, cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(!workspace.right_dock().read(cx).is_open()); // assert!(!panel.is_zoomed(cx)); // assert!(!panel.has_focus(cx)); @@ -5204,7 +5194,7 @@ mod tests { // workspace.toggle_dock(DockPosition::Right, cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(!panel.is_zoomed(cx)); // assert!(panel.has_focus(cx)); @@ -5216,7 +5206,7 @@ mod tests { // panel.set_zoomed(true, cx) // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(panel.is_zoomed(cx)); // assert!(panel.has_focus(cx)); @@ -5227,7 +5217,7 @@ mod tests { // workspace.toggle_panel_focus::(cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(!workspace.right_dock().read(cx).is_open()); // assert!(panel.is_zoomed(cx)); // assert!(!panel.has_focus(cx)); @@ -5238,7 +5228,7 @@ mod tests { // workspace.toggle_panel_focus::(cx); // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(panel.is_zoomed(cx)); // assert!(panel.has_focus(cx)); @@ -5249,7 +5239,7 @@ mod tests { // workspace.toggle_dock(DockPosition::Right, cx) // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(!workspace.right_dock().read(cx).is_open()); // assert!(panel.is_zoomed(cx)); // assert!(workspace.zoomed.is_none()); @@ -5261,7 +5251,7 @@ mod tests { // workspace.toggle_dock(DockPosition::Right, cx) // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // assert!(workspace.right_dock().read(cx).is_open()); // assert!(panel.is_zoomed(cx)); // assert!(workspace.zoomed.is_some()); @@ -5279,7 +5269,7 @@ mod tests { // workspace.update(cx, |workspace, cx| { // workspace.toggle_dock(DockPosition::Right, cx) // }); - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // let pane = pane.read(cx); // assert!(!pane.is_zoomed()); // assert!(!pane.has_focus()); @@ -5291,7 +5281,7 @@ mod tests { // #[gpui::test] // async fn test_panels(cx: &mut gpui::TestAppContext) { // init_test(cx); - // let fs = FakeFs::new(cx.background()); + // let fs = FakeFs::new(cx.executor()); // let project = Project::test(fs, [], cx).await; // let window = cx.add_window(|cx| Workspace::test_new(project, cx)); @@ -5413,7 +5403,7 @@ mod tests { // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); // // Now the left dock is open and panel_1 is active and focused. - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // let left_dock = workspace.left_dock(); // assert!(left_dock.read(cx).is_open()); // assert_eq!( @@ -5427,7 +5417,7 @@ mod tests { // panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); // // Wo don't close the left dock, because panel_2 wasn't the active panel - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // let left_dock = workspace.left_dock(); // assert!(left_dock.read(cx).is_open()); // assert_eq!( @@ -5438,30 +5428,30 @@ mod tests { // // Emitting a ZoomIn event shows the panel as zoomed. // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); // assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); // }); // // Move panel to another dock while it is zoomed // panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx)); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); // }); // // If focus is transferred to another view that's not a panel or another pane, we still show // // the panel as zoomed. - // let focus_receiver = window.build_view(cx, |_| EmptyView); + // let focus_receiver = cx.build_view(|_| EmptyView); // focus_receiver.update(cx, |_, cx| cx.focus_self()); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); // }); // // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. // workspace.update(cx, |_, cx| cx.focus_self()); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, None); // assert_eq!(workspace.zoomed_position, None); // }); @@ -5469,21 +5459,21 @@ mod tests { // // If focus is transferred again to another view that's not a panel or a pane, we won't // // show the panel as zoomed because it wasn't zoomed before. // focus_receiver.update(cx, |_, cx| cx.focus_self()); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, None); // assert_eq!(workspace.zoomed_position, None); // }); // // When focus is transferred back to the panel, it is zoomed again. // panel_1.update(cx, |_, cx| cx.focus_self()); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); // assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); // }); // // Emitting a ZoomOut event unzooms the panel. // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); - // workspace.read_with(cx, |workspace, _| { + // workspace.update(cx, |workspace, _| { // assert_eq!(workspace.zoomed, None); // assert_eq!(workspace.zoomed_position, None); // }); @@ -5492,7 +5482,7 @@ mod tests { // panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); // // Now the left dock is closed, because panel_1 was the active panel - // workspace.read_with(cx, |workspace, cx| { + // workspace.update(cx, |workspace, cx| { // let right_dock = workspace.right_dock(); // assert!(!right_dock.read(cx).is_open()); // }); From 6d62e6c562412a9bc38856d72892cd5ff9a5682e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 30 Nov 2023 20:36:59 -0500 Subject: [PATCH 17/28] Respect `label_color` for `Button`s (#3469) This PR makes `Button`s respect the `label_color` that is specified, provided they are not disabled or selected. Release Notes: - N/A --- crates/ui2/src/components/button/button.rs | 2 +- crates/ui2/src/components/stories/button.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ui2/src/components/button/button.rs b/crates/ui2/src/components/button/button.rs index 4bfa71d092..ce26ee76a5 100644 --- a/crates/ui2/src/components/button/button.rs +++ b/crates/ui2/src/components/button/button.rs @@ -79,7 +79,7 @@ impl RenderOnce for Button { } else if self.base.selected { Color::Selected } else { - Color::Default + self.label_color.unwrap_or_default() }; self.base.child( diff --git a/crates/ui2/src/components/stories/button.rs b/crates/ui2/src/components/stories/button.rs index db8aa40cf7..17bcd8b268 100644 --- a/crates/ui2/src/components/stories/button.rs +++ b/crates/ui2/src/components/stories/button.rs @@ -14,6 +14,10 @@ impl Render for ButtonStory { .child(Story::title_for::