From 91b54b352b19cecd9d5ab3b7901bf5de34599a50 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 23:21:32 -0700 Subject: [PATCH 01/36] Add command palette tests and simulate_keystrokes --- .../command_palette2/src/command_palette.rs | 206 +++++++++--------- crates/gpui2/src/action.rs | 11 + crates/gpui2/src/app/test_context.rs | 60 ++++- crates/gpui2/src/elements/div.rs | 14 ++ crates/gpui2/src/platform/test/window.rs | 6 +- crates/gpui2/src/window.rs | 2 +- 6 files changed, 180 insertions(+), 119 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 435a644669..a3594c0818 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -354,129 +354,117 @@ impl std::fmt::Debug for Command { } } -// #[cfg(test)] -// mod tests { -// use std::sync::Arc; +#[cfg(test)] +mod tests { + use std::sync::Arc; -// use super::*; -// use editor::Editor; -// use gpui::{executor::Deterministic, TestAppContext}; -// use project::Project; -// use workspace::{AppState, Workspace}; + use super::*; + use editor::Editor; + use gpui::TestAppContext; + use project::Project; + use workspace::{AppState, Workspace}; -// #[test] -// fn test_humanize_action_name() { -// assert_eq!( -// humanize_action_name("editor::GoToDefinition"), -// "editor: go to definition" -// ); -// assert_eq!( -// humanize_action_name("editor::Backspace"), -// "editor: backspace" -// ); -// assert_eq!( -// humanize_action_name("go_to_line::Deploy"), -// "go to line: deploy" -// ); -// } + #[test] + fn test_humanize_action_name() { + assert_eq!( + humanize_action_name("editor::GoToDefinition"), + "editor: go to definition" + ); + assert_eq!( + humanize_action_name("editor::Backspace"), + "editor: backspace" + ); + assert_eq!( + humanize_action_name("go_to_line::Deploy"), + "go to line: deploy" + ); + } -// #[gpui::test] -// async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { -// let app_state = init_test(cx); + #[gpui::test] + async fn test_command_palette(cx: &mut TestAppContext) { + let app_state = init_test(cx); -// let project = Project::test(app_state.fs.clone(), [], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let editor = window.add_view(cx, |cx| { -// let mut editor = Editor::single_line(None, cx); -// editor.set_text("abc", cx); -// editor -// }); + let project = Project::test(app_state.fs.clone(), [], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut cx; -// workspace.update(cx, |workspace, cx| { -// cx.focus(&editor); -// workspace.add_item(Box::new(editor.clone()), cx) -// }); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_text("abc", cx); + editor + }); -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| editor.focus(cx)) + }); -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); + cx.simulate_keystrokes("cmd-shift-p"); -// palette -// .update(cx, |palette, cx| { -// // Fill up palette's command list by running an empty query; -// // we only need it to subsequently assert that the palette is initially -// // sorted by command's name. -// palette.delegate_mut().update_matches("".to_string(), cx) -// }) -// .await; + let palette = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); -// palette.update(cx, |palette, _| { -// let is_sorted = -// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); -// assert!(is_sorted(&palette.delegate().actions)); -// }); + palette.update(cx, |palette, _| { + assert!(palette.delegate.commands.len() > 5); + let is_sorted = + |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); + assert!(is_sorted(&palette.delegate.commands)); + }); -// palette -// .update(cx, |palette, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; + cx.simulate_keystrokes("b c k s p"); -// palette.update(cx, |palette, cx| { -// assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); -// palette.confirm(&Default::default(), cx); -// }); -// deterministic.run_until_parked(); -// editor.read_with(cx, |editor, cx| { -// assert_eq!(editor.text(cx), "ab"); -// }); + palette.update(cx, |palette, _| { + assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); + }); -// // Add namespace filter, and redeploy the palette -// cx.update(|cx| { -// cx.update_default_global::(|filter, _| { -// filter.filtered_namespaces.insert("editor"); -// }) -// }); + cx.simulate_keystrokes("enter"); -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); + workspace.update(cx, |workspace, cx| { + assert!(workspace.current_modal::(cx).is_none()); + assert_eq!(editor.read(cx).text(cx), "ab") + }); -// // Assert editor command not present -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); + // Add namespace filter, and redeploy the palette + cx.update(|cx| { + cx.set_global(CommandPaletteFilter::default()); + cx.update_global::(|filter, _| { + filter.filtered_namespaces.insert("editor"); + }) + }); -// palette -// .update(cx, |palette, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; + cx.simulate_keystrokes("cmd-shift-p"); + cx.simulate_keystrokes("b c k s p"); -// palette.update(cx, |palette, _| { -// assert!(palette.delegate().matches.is_empty()) -// }); -// } + let palette = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + palette.update(cx, |palette, _| { + assert!(palette.delegate.matches.is_empty()) + }); + } -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.update(|cx| { -// let app_state = AppState::test(cx); -// theme::init(cx); -// language::init(cx); -// editor::init(cx); -// workspace::init(app_state.clone(), cx); -// init(cx); -// Project::init_settings(cx); -// app_state -// }) -// } -// } + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init(cx); + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + init(cx); + Project::init_settings(cx); + settings::load_default_keymap(cx); + app_state + }) + } +} diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 16487cf18a..dbb510b1c8 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static { where Self: Sized; fn build(value: Option) -> Result> + where + Self: Sized; + fn is_registered() -> bool where Self: Sized; @@ -88,6 +91,14 @@ where Ok(Box::new(action)) } + fn is_registered() -> bool { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&TypeId::of::()) + .is_some() + } + fn partial_eq(&self, action: &dyn Action) -> bool { action .as_any() diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index cc59b7a16a..5397a2214d 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, - ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, + View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -220,7 +220,21 @@ impl TestAppContext { { window .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) - .unwrap() + .unwrap(); + + self.background_executor.run_until_parked() + } + + pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { + for keystroke in keystrokes + .split(" ") + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke.into(), false); + } + + self.background_executor.run_until_parked() } pub fn dispatch_keystroke( @@ -229,15 +243,41 @@ impl TestAppContext { keystroke: Keystroke, is_held: bool, ) { + let keystroke2 = keystroke.clone(); let handled = window .update(self, |_, cx| { cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held })) }) .is_ok_and(|handled| handled); - - if !handled { - // todo!() simluate input here + if handled { + return; } + + let input_handler = self.update_test_window(window, |window| window.input_handler.clone()); + let Some(input_handler) = input_handler else { + panic!( + "dispatch_keystroke {:?} failed to dispatch action or input", + &keystroke2 + ); + }; + let text = keystroke2.ime_key.unwrap_or(keystroke2.key); + input_handler.lock().replace_text_in_range(None, &text); + } + + pub fn update_test_window( + &mut self, + window: AnyWindowHandle, + f: impl FnOnce(&mut TestWindow) -> R, + ) -> R { + window + .update(self, |_, cx| { + f(cx.window + .platform_window + .as_any_mut() + .downcast_mut::() + .unwrap()) + }) + .unwrap() } pub fn notifications(&mut self, entity: &Model) -> impl Stream { @@ -401,12 +441,20 @@ impl<'a> VisualTestContext<'a> { Self { cx, window } } + pub fn run_until_parked(&self) { + self.cx.background_executor.run_until_parked(); + } + pub fn dispatch_action(&mut self, action: A) where A: Action, { self.cx.dispatch_action(self.window, action) } + + pub fn simulate_keystrokes(&mut self, keystrokes: &str) { + self.cx.simulate_keystrokes(self.window, keystrokes) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a08d1619ae..e76d82f36f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -229,6 +229,20 @@ pub trait InteractiveComponent: Sized + Element { mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self { + // NOTE: this debug assert has the side-effect of working around + // a bug where a crate consisting only of action definitions does + // not register the actions in debug builds: + // + // https://github.com/rust-lang/rust/issues/47384 + // https://github.com/mmastrac/rust-ctor/issues/280 + // + // if we are relying on this side-effect still, removing the debug_assert! + // likely breaks the command_palette tests. + debug_assert!( + A::is_registered(), + "{:?} is not registered as an action", + A::qualified_name() + ); self.interactivity().action_listeners.push(( TypeId::of::(), Box::new(move |view, action, phase, cx| { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index adb15c4266..e355c3aa4b 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -22,7 +22,7 @@ pub struct TestWindow { bounds: WindowBounds, current_scene: Mutex>, display: Rc, - input_handler: Option>, + pub(crate) input_handler: Option>>>, handlers: Mutex, platform: Weak, sprite_atlas: Arc, @@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow { } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - todo!() + self } fn set_input_handler(&mut self, input_handler: Box) { - self.input_handler = Some(input_handler); + self.input_handler = Some(Arc::new(Mutex::new(input_handler))); } fn prompt( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c177ffc8c2..808f903bd0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -189,7 +189,7 @@ impl Drop for FocusHandle { pub struct Window { pub(crate) handle: AnyWindowHandle, pub(crate) removed: bool, - platform_window: Box, + pub(crate) platform_window: Box, display_id: DisplayId, sprite_atlas: Arc, rem_size: Pixels, From c81bd288d46a9d250cd733d6aa0b79b1fab58129 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 23:47:08 -0700 Subject: [PATCH 02/36] Fix test --- crates/gpui2/src/interactive.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 013ed2ea48..80a89ef625 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -337,7 +337,6 @@ mod test { .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle)) .unwrap(); - cx.dispatch_keystroke(*window, Keystroke::parse("space").unwrap(), false); cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false); window From 851a60a68e48e4e63e2d0de08f8a7524f2cc435e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 14:07:37 +0100 Subject: [PATCH 03/36] Render fold indicators in the gutter --- crates/editor2/src/editor.rs | 99 +++++++++-------------- crates/editor2/src/element.rs | 145 +++++++++++++++------------------- 2 files changed, 98 insertions(+), 146 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index e3d9440933..d2ba9567f0 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -4372,69 +4372,42 @@ impl Editor { } } - // pub fn render_fold_indicators( - // &self, - // fold_data: Vec>, - // style: &EditorStyle, - // gutter_hovered: bool, - // line_height: f32, - // gutter_margin: f32, - // cx: &mut ViewContext, - // ) -> Vec>> { - // enum FoldIndicators {} - - // let style = style.folds.clone(); - - // fold_data - // .iter() - // .enumerate() - // .map(|(ix, fold_data)| { - // fold_data - // .map(|(fold_status, buffer_row, active)| { - // (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - // MouseEventHandler::new::( - // ix as usize, - // cx, - // |mouse_state, _| { - // Svg::new(match fold_status { - // FoldStatus::Folded => style.folded_icon.clone(), - // FoldStatus::Foldable => style.foldable_icon.clone(), - // }) - // .with_color( - // style - // .indicator - // .in_state(fold_status == FoldStatus::Folded) - // .style_for(mouse_state) - // .color, - // ) - // .constrained() - // .with_width(gutter_margin * style.icon_margin_scale) - // .aligned() - // .constrained() - // .with_height(line_height) - // .with_width(gutter_margin) - // .aligned() - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_padding(Padding::uniform(3.)) - // .on_click(MouseButton::Left, { - // move |_, editor, cx| match fold_status { - // FoldStatus::Folded => { - // editor.unfold_at(&UnfoldAt { buffer_row }, cx); - // } - // FoldStatus::Foldable => { - // editor.fold_at(&FoldAt { buffer_row }, cx); - // } - // } - // }) - // .into_any() - // }) - // }) - // .flatten() - // }) - // .collect() - // } + pub fn render_fold_indicators( + &self, + fold_data: Vec>, + style: &EditorStyle, + gutter_hovered: bool, + line_height: Pixels, + gutter_margin: Pixels, + cx: &mut ViewContext, + ) -> Vec>> { + fold_data + .iter() + .enumerate() + .map(|(ix, fold_data)| { + fold_data + .map(|(fold_status, buffer_row, active)| { + (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { + let icon = match fold_status { + FoldStatus::Folded => ui::Icon::ChevronRight, + FoldStatus::Foldable => ui::Icon::ChevronDown, + }; + IconButton::new(ix as usize, icon) + .on_click(move |editor: &mut Editor, cx| match fold_status { + FoldStatus::Folded => { + editor.unfold_at(&UnfoldAt { buffer_row }, cx); + } + FoldStatus::Foldable => { + editor.fold_at(&FoldAt { buffer_row }, cx); + } + }) + .render() + }) + }) + .flatten() + }) + .collect() + } pub fn context_menu_visible(&self) -> bool { self.context_menu diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d11408c0ce..06f25c88b6 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -487,23 +487,26 @@ impl EditorElement { } } - // todo!("fold indicators") - // for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { - // if let Some(indicator) = fold_indicator.as_mut() { - // let position = point( - // bounds.width() - layout.gutter_padding, - // ix as f32 * line_height - (scroll_top % line_height), - // ); - // let centering_offset = point( - // (layout.gutter_padding + layout.gutter_margin - indicator.size().x) / 2., - // (line_height - indicator.size().y) / 2., - // ); + for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { + if let Some(fold_indicator) = fold_indicator.as_mut() { + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height * 0.55), + ); + let fold_indicator_size = fold_indicator.measure(available_space, editor, cx); - // let indicator_origin = bounds.origin + position + centering_offset; - - // indicator.paint(indicator_origin, visible_bounds, editor, cx); - // } - // } + let position = point( + bounds.size.width - layout.gutter_padding, + ix as f32 * line_height - (scroll_top % line_height), + ); + let centering_offset = point( + (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) / 2., + (line_height - fold_indicator_size.height) / 2., + ); + let origin = bounds.origin + position + centering_offset; + fold_indicator.draw(origin, available_space, editor, cx); + } + } if let Some(indicator) = layout.code_actions_indicator.as_mut() { let available_space = size( @@ -1684,35 +1687,25 @@ impl EditorElement { ShowScrollbar::Auto => { // Git (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) - || - // Selections - (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) - // Scrollmanager - || editor.scroll_manager.scrollbars_visible() + || + // Selections + (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) + // Scrollmanager + || editor.scroll_manager.scrollbars_visible() } ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), ShowScrollbar::Always => true, ShowScrollbar::Never => false, }; - let fold_ranges: Vec<(BufferRow, Range, Hsla)> = Vec::new(); - // todo!() - - // fold_ranges - // .into_iter() - // .map(|(id, fold)| { - // // todo!("folds!") - // // let color = self - // // .style - // // .folds - // // .ellipses - // // .background - // // .style_for(&mut cx.mouse_state::(id as usize)) - // // .color; - - // // (id, fold, color) - // }) - // .collect(); + let fold_ranges: Vec<(BufferRow, Range, Hsla)> = fold_ranges + .into_iter() + .map(|(id, fold)| { + // todo!("change color based on mouse state") + let color = gpui::red(); + (id, fold, color) + }) + .collect(); let head_for_relative = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); @@ -1754,21 +1747,23 @@ impl EditorElement { .width; let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - let (scroll_width, blocks) = self.layout_blocks( - start_row..end_row, - &snapshot, - bounds.size.width, - scroll_width, - gutter_padding, - gutter_width, - em_width, - gutter_width + gutter_margin, - line_height, - &style, - &line_layouts, - editor, - cx, - ); + let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { + self.layout_blocks( + start_row..end_row, + &snapshot, + bounds.size.width, + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ) + }); let scroll_max = point( f32::from((scroll_width - text_size.width) / em_width).max(0.0), @@ -1828,15 +1823,16 @@ impl EditorElement { // ); // let mode = editor.mode; - // todo!("fold_indicators") - // let mut fold_indicators = editor.render_fold_indicators( - // fold_statuses, - // &style, - // editor.gutter_hovered, - // line_height, - // gutter_margin, - // cx, - // ); + let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_margin, + cx, + ) + }); // todo!("context_menu") // if let Some((_, context_menu)) = context_menu.as_mut() { @@ -1853,20 +1849,6 @@ impl EditorElement { // ); // } - // todo!("fold indicators") - // for fold_indicator in fold_indicators.iter_mut() { - // if let Some(indicator) = fold_indicator.as_mut() { - // indicator.layout( - // SizeConstraint::strict_along( - // Axis::Vertical, - // line_height * style.code_actions.vertical_scale, - // ), - // editor, - // cx, - // ); - // } - // } - // todo!("hover popovers") // if let Some((_, hover_popovers)) = hover.as_mut() { // for hover_popover in hover_popovers.iter_mut() { @@ -1953,7 +1935,7 @@ impl EditorElement { selections, context_menu, code_actions_indicator, - // fold_indicators, + fold_indicators, tab_invisible, space_invisible, // hover_popovers: hover, @@ -2019,7 +2001,6 @@ impl EditorElement { }) } TransformBlock::ExcerptHeader { - id, buffer, range, starts_new_buffer, @@ -2041,9 +2022,7 @@ impl EditorElement { .map_or(range.context.start, |primary| primary.start); let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - // todo!("avoid ElementId collision risk here") - let icon_button_id: usize = id.clone().into(); - IconButton::new(icon_button_id, ui::Icon::ArrowUpRight) + IconButton::new(block_id, ui::Icon::ArrowUpRight) .on_click(move |editor: &mut Editor, cx| { editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); }) @@ -3117,7 +3096,7 @@ pub struct LayoutState { context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, - // fold_indicators: Vec>>, + fold_indicators: Vec>>, tab_invisible: Line, space_invisible: Line, } From 888098bad2fca07f23d9d4482fc0e59950246e2c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 15 Nov 2023 16:20:00 +0200 Subject: [PATCH 04/36] More detailed errors when opening files --- crates/project/src/project.rs | 3 ++- crates/project/src/worktree.rs | 2 +- crates/project2/src/project2.rs | 3 ++- crates/project2/src/worktree.rs | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 468659e5b9..322b2ae894 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1709,6 +1709,7 @@ impl Project { self.open_remote_buffer_internal(&project_path.path, &worktree, cx) }; + let project_path = project_path.clone(); cx.spawn(move |this, mut cx| async move { let load_result = load_buffer.await; *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| { @@ -1726,7 +1727,7 @@ impl Project { cx.foreground().spawn(async move { wait_for_loading_buffer(loading_watch) .await - .map_err(|error| anyhow!("{}", error)) + .map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}")) }) } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 80fd44761c..785ce58bb8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3694,7 +3694,7 @@ impl BackgroundScanner { } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("error reading file on event {:?}", err); + log::error!("error reading file {abs_path:?} on event: {err:#}"); } } } diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index efe407f847..61ad500a73 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -1741,6 +1741,7 @@ impl Project { self.open_remote_buffer_internal(&project_path.path, &worktree, cx) }; + let project_path = project_path.clone(); cx.spawn(move |this, mut cx| async move { let load_result = load_buffer.await; *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| { @@ -1759,7 +1760,7 @@ impl Project { cx.background_executor().spawn(async move { wait_for_loading_buffer(loading_watch) .await - .map_err(|error| anyhow!("{}", error)) + .map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}")) }) } diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 65959d3f31..9444dd9185 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -3684,7 +3684,7 @@ impl BackgroundScanner { } Err(err) => { // TODO - create a special 'error' entry in the entries tree to mark this - log::error!("error reading file on event {:?}", err); + log::error!("error reading file {abs_path:?} on event: {err:#}"); } } } From 3ff8c78b5867fd99e0f151a514400f6c6252ef93 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 17:09:59 +0100 Subject: [PATCH 05/36] Return a `Fold` struct when querying the `FoldMap` This contains a new `id` field that lets us distinguish among folds. Co-Authored-By: Nathan Sobo --- crates/editor2/src/display_map.rs | 6 +- crates/editor2/src/display_map/fold_map.rs | 105 ++++++++++++++------- crates/editor2/src/editor.rs | 8 +- crates/editor2/src/element.rs | 4 +- crates/editor2/src/git.rs | 6 +- crates/gpui2/src/window.rs | 6 +- 6 files changed, 87 insertions(+), 48 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index d88daaccc1..e64d5e301c 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -31,7 +31,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::fold_map::FoldPoint; +pub use self::fold_map::{Fold, FoldPoint}; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -124,7 +124,7 @@ impl DisplayMap { self.fold( other .folds_in_range(0..other.buffer_snapshot.len()) - .map(|fold| fold.to_offset(&other.buffer_snapshot)), + .map(|fold| fold.range.to_offset(&other.buffer_snapshot)), cx, ); } @@ -723,7 +723,7 @@ impl DisplaySnapshot { DisplayPoint(point) } - pub fn folds_in_range(&self, range: Range) -> impl Iterator> + pub fn folds_in_range(&self, range: Range) -> impl Iterator where T: ToOffset, { diff --git a/crates/editor2/src/display_map/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs index 88cd202b08..4dad2d52ae 100644 --- a/crates/editor2/src/display_map/fold_map.rs +++ b/crates/editor2/src/display_map/fold_map.rs @@ -3,15 +3,16 @@ use super::{ Highlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; -use gpui::{HighlightStyle, Hsla}; +use gpui::{ElementId, HighlightStyle, Hsla}; use language::{Chunk, Edit, Point, TextSummary}; use std::{ any::TypeId, cmp::{self, Ordering}, iter, - ops::{Add, AddAssign, Range, Sub}, + ops::{Add, AddAssign, Deref, DerefMut, Range, Sub}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; +use util::post_inc; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct FoldPoint(pub Point); @@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> { } // For now, ignore any ranges that span an excerpt boundary. - let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); - if fold.0.start.excerpt_id != fold.0.end.excerpt_id { + let fold_range = + FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)); + if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id { continue; } - folds.push(fold); + folds.push(Fold { + id: FoldId(post_inc(&mut self.0.next_fold_id.0)), + range: fold_range, + }); let inlay_range = snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); @@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> { } let buffer = &snapshot.buffer; - folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer)); + folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer)); self.0.snapshot.folds = { let mut new_tree = SumTree::new(); - let mut cursor = self.0.snapshot.folds.cursor::(); + let mut cursor = self.0.snapshot.folds.cursor::(); for fold in folds { - new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer); + new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer); new_tree.push(fold, buffer); } new_tree.append(cursor.suffix(buffer), buffer); @@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> { let mut folds_cursor = intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive); while let Some(fold) = folds_cursor.item() { - let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer); + let offset_range = + fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer); if offset_range.end > offset_range.start { let inlay_range = snapshot.to_inlay_offset(offset_range.start) ..snapshot.to_inlay_offset(offset_range.end); @@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> { pub struct FoldMap { snapshot: FoldSnapshot, ellipses_color: Option, + next_fold_id: FoldId, } impl FoldMap { @@ -197,6 +204,7 @@ impl FoldMap { ellipses_color: None, }, ellipses_color: None, + next_fold_id: FoldId::default(), }; let snapshot = this.snapshot.clone(); (this, snapshot) @@ -242,8 +250,8 @@ impl FoldMap { while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { let comparison = fold - .0 - .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer); + .range + .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer); assert!(comparison.is_le()); } } @@ -304,9 +312,9 @@ impl FoldMap { let anchor = inlay_snapshot .buffer .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); - let mut folds_cursor = self.snapshot.folds.cursor::(); + let mut folds_cursor = self.snapshot.folds.cursor::(); folds_cursor.seek( - &Fold(anchor..Anchor::max()), + &FoldRange(anchor..Anchor::max()), Bias::Left, &inlay_snapshot.buffer, ); @@ -315,8 +323,8 @@ impl FoldMap { let inlay_snapshot = &inlay_snapshot; move || { let item = folds_cursor.item().map(|f| { - let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer); - let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer); + let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer); + let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer); inlay_snapshot.to_inlay_offset(buffer_start) ..inlay_snapshot.to_inlay_offset(buffer_end) }); @@ -596,13 +604,13 @@ impl FoldSnapshot { self.transforms.summary().output.longest_row } - pub fn folds_in_range(&self, range: Range) -> impl Iterator> + pub fn folds_in_range(&self, range: Range) -> impl Iterator where T: ToOffset, { let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { - let item = folds.item().map(|f| &f.0); + let item = folds.item(); folds.next(&self.inlay_snapshot.buffer); item }) @@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary { } } -#[derive(Clone, Debug)] -struct Fold(Range); +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub struct FoldId(usize); -impl Default for Fold { +impl Into for FoldId { + fn into(self) -> ElementId { + ElementId::Integer(self.0) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Fold { + pub id: FoldId, + pub range: FoldRange, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FoldRange(Range); + +impl Deref for FoldRange { + type Target = Range; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FoldRange { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Default for FoldRange { fn default() -> Self { Self(Anchor::min()..Anchor::max()) } @@ -844,17 +881,17 @@ impl sum_tree::Item for Fold { fn summary(&self) -> Self::Summary { FoldSummary { - start: self.0.start.clone(), - end: self.0.end.clone(), - min_start: self.0.start.clone(), - max_end: self.0.end.clone(), + start: self.range.start.clone(), + end: self.range.end.clone(), + min_start: self.range.start.clone(), + max_end: self.range.end.clone(), count: 1, } } } #[derive(Clone, Debug)] -struct FoldSummary { +pub struct FoldSummary { start: Anchor, end: Anchor, min_start: Anchor, @@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary { } } -impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold { +impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange { fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) { self.0.start = summary.start.clone(); self.0.end = summary.end.clone(); } } -impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold { +impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { self.0.cmp(&other.0, buffer) } @@ -1321,7 +1358,10 @@ mod tests { let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) - .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot)) + .map(|fold| { + fold.range.start.to_point(&buffer_snapshot) + ..fold.range.end.to_point(&buffer_snapshot) + }) .collect::>(); assert_eq!( fold_ranges, @@ -1553,10 +1593,9 @@ mod tests { .filter(|fold| { let start = buffer_snapshot.anchor_before(start); let end = buffer_snapshot.anchor_after(end); - start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less - && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater + start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less + && end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater }) - .map(|fold| fold.0) .collect::>(); assert_eq!( @@ -1639,10 +1678,10 @@ mod tests { let buffer = &inlay_snapshot.buffer; let mut folds = self.snapshot.folds.items(buffer); // Ensure sorting doesn't change how folds get merged and displayed. - folds.sort_by(|a, b| a.0.cmp(&b.0, buffer)); + folds.sort_by(|a, b| a.range.cmp(&b.range, buffer)); let mut fold_ranges = folds .iter() - .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer)) + .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer)) .peekable(); let mut merged_ranges = Vec::new(); diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index d2ba9567f0..41757b4dc2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -5303,8 +5303,8 @@ impl Editor { buffer.anchor_before(range_to_move.start) ..buffer.anchor_after(range_to_move.end), ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); start.row -= row_delta; end.row -= row_delta; refold_ranges.push(start..end); @@ -5394,8 +5394,8 @@ impl Editor { buffer.anchor_before(range_to_move.start) ..buffer.anchor_after(range_to_move.end), ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); start.row += row_delta; end.row += row_delta; refold_ranges.push(start..end); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 06f25c88b6..0aee6c301b 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1581,11 +1581,11 @@ impl EditorElement { snapshot .folds_in_range(start_anchor..end_anchor) .map(|anchor| { - let start = anchor.start.to_point(&snapshot.buffer_snapshot); + let start = anchor.range.start.to_point(&snapshot.buffer_snapshot); ( start.row, start.to_display_point(&snapshot.display_snapshot) - ..anchor.end.to_display_point(&snapshot), + ..anchor.range.end.to_display_point(&snapshot), ) }), ); diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index e04372f0a7..6e408cd3a0 100644 --- a/crates/editor2/src/git.rs +++ b/crates/editor2/src/git.rs @@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> let folds_end = Point::new(hunk.buffer_range.end + 2, 0); let folds_range = folds_start..folds_end; - let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| { - let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot); + let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| { + let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot); let fold_point_range = fold_point_range.start..=fold_point_range.end; let folded_start = fold_point_range.contains(&hunk_start_point); @@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk, snapshot: &DisplaySnapshot) -> }); if let Some(fold) = containing_fold { - let row = fold.start.to_display_point(snapshot).row(); + let row = fold.range.start.to_display_point(snapshot).row(); DisplayDiffHunk::Folded { display_row: row } } else { let start = hunk_start_point.to_display_point(snapshot).row(); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c177ffc8c2..9fb9f51314 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2471,7 +2471,7 @@ impl From> for StackingOrder { #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum ElementId { View(EntityId), - Number(usize), + Integer(usize), Name(SharedString), FocusHandle(FocusId), } @@ -2484,13 +2484,13 @@ impl From for ElementId { impl From for ElementId { fn from(id: usize) -> Self { - ElementId::Number(id) + ElementId::Integer(id) } } impl From for ElementId { fn from(id: i32) -> Self { - Self::Number(id as usize) + Self::Integer(id as usize) } } From 17b8e4a68459b907f8ff8a96ff1eb22c566127e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 18:19:26 +0100 Subject: [PATCH 06/36] Handle clicking folded ranges Co-Authored-By: Nathan Sobo --- crates/editor2/src/element.rs | 695 +++++++++++++++---------------- crates/gpui2/src/element.rs | 46 +- crates/gpui2/src/elements/div.rs | 6 + crates/gpui2/src/window.rs | 2 +- 4 files changed, 374 insertions(+), 375 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 0aee6c301b..f5ea324050 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -18,11 +18,12 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, - Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, - TextStyle, View, ViewContext, WindowContext, + div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, + BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, + ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, + ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View, + ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -615,311 +616,341 @@ impl EditorElement { fn paint_text( &mut self, - bounds: Bounds, + text_bounds: Bounds, layout: &mut LayoutState, editor: &mut Editor, cx: &mut ViewContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; - let scroll_top = scroll_position.y * layout.position_map.line_height; - let max_glyph_width = layout.position_map.em_width; - let scroll_left = scroll_position.x * max_glyph_width; - let content_origin = bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); let line_end_overshoot = 0.15 * layout.position_map.line_height; let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // todo!("cursor region") - // cx.scene().push_cursor_region(CursorRegion { - // bounds, - // style: if !editor.link_go_to_definition_state.definitions.is_empty { - // CursorStyle::PointingHand - // } else { - // CursorStyle::IBeam - // }, - // }); + cx.with_content_mask( + Some(ContentMask { + bounds: text_bounds, + }), + |cx| { + // todo!("cursor region") + // cx.scene().push_cursor_region(CursorRegion { + // bounds, + // style: if !editor.link_go_to_definition_state.definitions.is_empty { + // CursorStyle::PointingHand + // } else { + // CursorStyle::IBeam + // }, + // }); - // todo!("fold ranges") - // let fold_corner_radius = - // self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height; - // for (id, range, color) in layout.fold_ranges.iter() { - // self.paint_highlighted_range( - // range.clone(), - // *color, - // fold_corner_radius, - // fold_corner_radius * 2., - // layout, - // content_origin, - // scroll_top, - // scroll_left, - // bounds, - // cx, - // ); + let fold_corner_radius = 0.15 * layout.position_map.line_height; + cx.with_element_id(Some("folds"), |cx| { + let snapshot = &layout.position_map.snapshot; + for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) { + let fold_range = fold.range.clone(); + let display_range = fold.range.start.to_display_point(&snapshot) + ..fold.range.end.to_display_point(&snapshot); + debug_assert_eq!(display_range.start.row(), display_range.end.row()); + let row = display_range.start.row(); - // for bound in range_to_bounds( - // &range, - // content_origin, - // scroll_left, - // scroll_top, - // &layout.visible_display_row_range, - // line_end_overshoot, - // &layout.position_map, - // ) { - // cx.scene().push_cursor_region(CursorRegion { - // bounds: bound, - // style: CursorStyle::PointingHand, - // }); + let line_layout = &layout.position_map.line_layouts + [(row - layout.visible_display_row_range.start) as usize] + .line; + let start_x = content_origin.x + + line_layout.x_for_index(display_range.start.column() as usize) + - layout.position_map.scroll_position.x; + let start_y = content_origin.y + + row as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let end_x = content_origin.x + + line_layout.x_for_index(display_range.end.column() as usize) + - layout.position_map.scroll_position.x; - // let display_row = range.start.row(); + let fold_bounds = Bounds { + origin: point(start_x, start_y), + size: size(end_x - start_x, layout.position_map.line_height), + }; - // let buffer_row = DisplayPoint::new(display_row, 0) - // .to_point(&layout.position_map.snapshot.display_snapshot) - // .row; + let fold_background = cx.with_z_index(1, |cx| { + div() + .id(fold.id) + .size_full() + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |editor: &mut Editor, _, cx| { + editor.unfold_ranges( + [fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }) + .draw( + fold_bounds.origin, + fold_bounds.size, + editor, + cx, + |fold_element_state, cx| { + if fold_element_state.is_active() { + gpui::blue() + } else if fold_bounds.contains_point(&cx.mouse_position()) { + gpui::black() + } else { + gpui::red() + } + }, + ) + }); - // let view_id = cx.view_id(); - // cx.scene().push_mouse_region( - // MouseRegion::new::(view_id, *id as usize, bound) - // .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| { - // editor.unfold_at(&UnfoldAt { buffer_row }, cx) - // }) - // .with_notify_on_hover(true) - // .with_notify_on_click(true), - // ) - // } - // } + self.paint_highlighted_range( + display_range.clone(), + fold_background, + fold_corner_radius, + fold_corner_radius * 2., + layout, + content_origin, + text_bounds, + cx, + ); + } + }); - for (range, color) in &layout.highlighted_ranges { - self.paint_highlighted_range( - range.clone(), - *color, - Pixels::ZERO, - line_end_overshoot, - layout, - content_origin, - scroll_top, - scroll_left, - bounds, - cx, - ); - } - - let mut cursors = SmallVec::<[Cursor; 32]>::new(); - let corner_radius = 0.15 * layout.position_map.line_height; - let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - - for (selection_style, selections) in &layout.selections { - for selection in selections { + for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( - selection.range.clone(), - selection_style.selection, - corner_radius, - corner_radius * 2., + range.clone(), + *color, + Pixels::ZERO, + line_end_overshoot, layout, content_origin, - scroll_top, - scroll_left, - bounds, + text_bounds, cx, ); + } - if selection.is_local && !selection.range.is_empty() { - invisible_display_ranges.push(selection.range.clone()); - } + let mut cursors = SmallVec::<[Cursor; 32]>::new(); + let corner_radius = 0.15 * layout.position_map.line_height; + let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - if !selection.is_local || editor.show_local_cursors(cx) { - let cursor_position = selection.head; - if layout - .visible_display_row_range - .contains(&cursor_position.row()) - { - let cursor_row_layout = &layout.position_map.line_layouts - [(cursor_position.row() - start_row) as usize] - .line; - let cursor_column = cursor_position.column() as usize; + for (selection_style, selections) in &layout.selections { + for selection in selections { + self.paint_highlighted_range( + selection.range.clone(), + selection_style.selection, + corner_radius, + corner_radius * 2., + layout, + content_origin, + text_bounds, + cx, + ); - let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); - let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - - cursor_character_x; - if block_width == Pixels::ZERO { - block_width = layout.position_map.em_width; + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + + if !selection.is_local || editor.show_local_cursors(cx) { + let cursor_position = selection.head; + if layout + .visible_display_row_range + .contains(&cursor_position.row()) + { + let cursor_row_layout = &layout.position_map.line_layouts + [(cursor_position.row() - start_row) as usize] + .line; + let cursor_column = cursor_position.column() as usize; + + let cursor_character_x = + cursor_row_layout.x_for_index(cursor_column); + let mut block_width = cursor_row_layout + .x_for_index(cursor_column + 1) + - cursor_character_x; + if block_width == Pixels::ZERO { + block_width = layout.position_map.em_width; + } + let block_text = if let CursorShape::Block = selection.cursor_shape + { + layout + .position_map + .snapshot + .chars_at(cursor_position) + .next() + .and_then(|(character, _)| { + let text = character.to_string(); + cx.text_system() + .layout_text( + &text, + cursor_row_layout.font_size, + &[TextRun { + len: text.len(), + font: self.style.text.font(), + color: self.style.background, + underline: None, + }], + None, + ) + .unwrap() + .pop() + }) + } else { + None + }; + + let x = cursor_character_x - layout.position_map.scroll_position.x; + let y = cursor_position.row() as f32 + * layout.position_map.line_height + - layout.position_map.scroll_position.y; + if selection.is_newest { + editor.pixel_position_of_newest_cursor = Some(point( + text_bounds.origin.x + x + block_width / 2., + text_bounds.origin.y + + y + + layout.position_map.line_height / 2., + )); + } + cursors.push(Cursor { + color: selection_style.cursor, + block_width, + origin: point(x, y), + line_height: layout.position_map.line_height, + shape: selection.cursor_shape, + block_text, + }); } - let block_text = if let CursorShape::Block = selection.cursor_shape { - layout - .position_map - .snapshot - .chars_at(cursor_position) - .next() - .and_then(|(character, _)| { - let text = character.to_string(); - cx.text_system() - .layout_text( - &text, - cursor_row_layout.font_size, - &[TextRun { - len: text.len(), - font: self.style.text.font(), - color: self.style.background, - underline: None, - }], - None, - ) - .unwrap() - .pop() - }) - } else { - None - }; - - let x = cursor_character_x - scroll_left; - let y = cursor_position.row() as f32 * layout.position_map.line_height - - scroll_top; - if selection.is_newest { - editor.pixel_position_of_newest_cursor = Some(point( - bounds.origin.x + x + block_width / 2., - bounds.origin.y + y + layout.position_map.line_height / 2., - )); - } - cursors.push(Cursor { - color: selection_style.cursor, - block_width, - origin: point(x, y), - line_height: layout.position_map.line_height, - shape: selection.cursor_shape, - block_text, - }); } } } - } - for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() { - let row = start_row + ix as u32; - line_with_invisibles.draw( - layout, - row, - scroll_top, - content_origin, - scroll_left, - whitespace_setting, - &invisible_display_ranges, - cx, - ) - } - - cx.with_z_index(0, |cx| { - for cursor in cursors { - cursor.paint(content_origin, cx); + for (ix, line_with_invisibles) in + layout.position_map.line_layouts.iter().enumerate() + { + let row = start_row + ix as u32; + line_with_invisibles.draw( + layout, + row, + content_origin, + whitespace_setting, + &invisible_display_ranges, + cx, + ) } - }); - if let Some((position, context_menu)) = layout.context_menu.as_mut() { - cx.with_z_index(1, |cx| { - let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite( - (12. * line_height).min((bounds.size.height - line_height) / 2.), - ), - ); - let context_menu_size = context_menu.measure(available_space, editor, cx); - - let cursor_row_layout = &layout.position_map.line_layouts - [(position.row() - start_row) as usize] - .line; - let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; - let y = - (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; - let mut list_origin = content_origin + point(x, y); - let list_width = context_menu_size.width; - let list_height = context_menu_size.height; - - // Snap the right edge of the list to the right edge of the window if - // its horizontal bounds overflow. - if list_origin.x + list_width > cx.viewport_size().width { - list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); + cx.with_z_index(0, |cx| { + for cursor in cursors { + cursor.paint(content_origin, cx); } + }); - if list_origin.y + list_height > bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height - list_height; - } + if let Some((position, context_menu)) = layout.context_menu.as_mut() { + cx.with_z_index(1, |cx| { + let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite( + (12. * line_height) + .min((text_bounds.size.height - line_height) / 2.), + ), + ); + let context_menu_size = context_menu.measure(available_space, editor, cx); - context_menu.draw(list_origin, available_space, editor, cx); - }) - } + let cursor_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; + let x = cursor_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = (position.row() + 1) as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let mut list_origin = content_origin + point(x, y); + let list_width = context_menu_size.width; + let list_height = context_menu_size.height; - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); + // Snap the right edge of the list to the right edge of the window if + // its horizontal bounds overflow. + if list_origin.x + list_width > cx.viewport_size().width { + list_origin.x = + (cx.viewport_size().width - list_width).max(Pixels::ZERO); + } - // // This is safe because we check on layout whether the required row is available - // let hovered_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; + if list_origin.y + list_height > text_bounds.lower_right().y { + list_origin.y -= layout.position_map.line_height - list_height; + } - // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover - // // height. This is the size we will use to decide whether to render popovers above or below - // // the hovered line. - // let first_size = hover_popovers[0].size(); - // let height_to_reserve = first_size.y - // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; + context_menu.draw(list_origin, available_space, editor, cx); + }) + } - // // Compute Hovered Point - // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; - // let hovered_point = content_origin + point(x, y); + // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { + // cx.scene().push_stacking_context(None, None); - // if hovered_point.y - height_to_reserve > 0.0 { - // // There is enough space above. Render popovers above the hovered point - // let mut current_y = hovered_point.y; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y - size.y); + // // This is safe because we check on layout whether the required row is available + // let hovered_row_layout = + // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } + // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // // height. This is the size we will use to decide whether to render popovers above or below + // // the hovered line. + // let first_size = hover_popovers[0].size(); + // let height_to_reserve = first_size.y + // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + // // Compute Hovered Point + // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; + // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; + // let hovered_point = content_origin + point(x, y); - // current_y = popover_origin.y - HOVER_POPOVER_GAP; - // } - // } else { - // // There is not enough space above. Render popovers below the hovered point - // let mut current_y = hovered_point.y + layout.position_map.line_height; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y); + // if hovered_point.y - height_to_reserve > 0.0 { + // // There is enough space above. Render popovers above the hovered point + // let mut current_y = hovered_point.y; + // for hover_popover in hover_popovers { + // let size = hover_popover.size(); + // let mut popover_origin = point(hovered_point.x, current_y - size.y); - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } + // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); + // if x_out_of_bounds < 0.0 { + // popover_origin.set_x(popover_origin.x + x_out_of_bounds); + // } - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + // hover_popover.paint( + // popover_origin, + // Bounds::::from_points( + // gpui::Point::::zero(), + // point(f32::MAX, f32::MAX), + // ), // Let content bleed outside of editor + // editor, + // cx, + // ); - // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; - // } - // } + // current_y = popover_origin.y - HOVER_POPOVER_GAP; + // } + // } else { + // // There is not enough space above. Render popovers below the hovered point + // let mut current_y = hovered_point.y + layout.position_map.line_height; + // for hover_popover in hover_popovers { + // let size = hover_popover.size(); + // let mut popover_origin = point(hovered_point.x, current_y); - // cx.scene().pop_stacking_context(); - // } - }) + // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); + // if x_out_of_bounds < 0.0 { + // popover_origin.set_x(popover_origin.x + x_out_of_bounds); + // } + + // hover_popover.paint( + // popover_origin, + // Bounds::::from_points( + // gpui::Point::::zero(), + // point(f32::MAX, f32::MAX), + // ), // Let content bleed outside of editor + // editor, + // cx, + // ); + + // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; + // } + // } + + // cx.scene().pop_stacking_context(); + // } + }, + ) } fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { @@ -1133,8 +1164,6 @@ impl EditorElement { line_end_overshoot: Pixels, layout: &LayoutState, content_origin: gpui::Point, - scroll_top: Pixels, - scroll_left: Pixels, bounds: Bounds, cx: &mut ViewContext, ) { @@ -1153,7 +1182,7 @@ impl EditorElement { corner_radius, start_y: content_origin.y + row_range.start as f32 * layout.position_map.line_height - - scroll_top, + - layout.position_map.scroll_position.y, lines: row_range .into_iter() .map(|row| { @@ -1163,17 +1192,17 @@ impl EditorElement { start_x: if row == range.start.row() { content_origin.x + line_layout.x_for_index(range.start.column() as usize) - - scroll_left + - layout.position_map.scroll_position.x } else { - content_origin.x - scroll_left + content_origin.x - layout.position_map.scroll_position.x }, end_x: if row == range.end.row() { content_origin.x + line_layout.x_for_index(range.end.column() as usize) - - scroll_left + - layout.position_map.scroll_position.x } else { content_origin.x + line_layout.width + line_end_overshoot - - scroll_left + - layout.position_map.scroll_position.x }, } }) @@ -1567,7 +1596,6 @@ impl EditorElement { let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); - let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); let highlighted_rows = editor.highlighted_rows(); @@ -1577,19 +1605,6 @@ impl EditorElement { cx.theme().colors(), ); - fold_ranges.extend( - snapshot - .folds_in_range(start_anchor..end_anchor) - .map(|anchor| { - let start = anchor.range.start.to_point(&snapshot.buffer_snapshot); - ( - start.row, - start.to_display_point(&snapshot.display_snapshot) - ..anchor.range.end.to_display_point(&snapshot), - ) - }), - ); - let mut newest_selection_head = None; if editor.show_local_selections { @@ -1698,15 +1713,6 @@ impl EditorElement { ShowScrollbar::Never => false, }; - let fold_ranges: Vec<(BufferRow, Range, Hsla)> = fold_ranges - .into_iter() - .map(|(id, fold)| { - // todo!("change color based on mouse state") - let color = gpui::red(); - (id, fold, color) - }) - .collect(); - let head_for_relative = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); SelectionLayout::new( @@ -1908,6 +1914,10 @@ impl EditorElement { mode: editor_mode, position_map: Arc::new(PositionMap { size: bounds.size, + scroll_position: point( + scroll_position.x * em_width, + scroll_position.y * line_height, + ), scroll_max, line_layouts, line_height, @@ -1915,6 +1925,7 @@ impl EditorElement { em_advance, snapshot, }), + visible_anchor_range: start_anchor..end_anchor, visible_display_row_range: start_row..end_row, wrap_guides, gutter_size, @@ -1928,7 +1939,6 @@ impl EditorElement { active_rows, highlighted_rows, highlighted_ranges, - fold_ranges, line_number_layouts, display_hunks, blocks, @@ -2116,11 +2126,13 @@ impl EditorElement { bounds: Bounds, gutter_bounds: Bounds, text_bounds: Bounds, - position_map: &Arc, + layout: &LayoutState, cx: &mut ViewContext, ) { + let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + cx.on_mouse_event({ - let position_map = position_map.clone(); + let position_map = layout.position_map.clone(); move |editor, event: &ScrollWheelEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -2132,7 +2144,7 @@ impl EditorElement { } }); cx.on_mouse_event({ - let position_map = position_map.clone(); + let position_map = layout.position_map.clone(); move |editor, event: &MouseDownEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -2144,7 +2156,7 @@ impl EditorElement { } }); cx.on_mouse_event({ - let position_map = position_map.clone(); + let position_map = layout.position_map.clone(); move |editor, event: &MouseUpEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -2157,7 +2169,7 @@ impl EditorElement { }); // todo!() // on_down(MouseButton::Right, { - // let position_map = position_map.clone(); + // let position_map = layout.position_map.clone(); // move |event, editor, cx| { // if !Self::mouse_right_down( // editor, @@ -2171,7 +2183,7 @@ impl EditorElement { // } // }); cx.on_mouse_event({ - let position_map = position_map.clone(); + let position_map = layout.position_map.clone(); move |editor, event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; @@ -2301,18 +2313,16 @@ impl LineWithInvisibles { &self, layout: &LayoutState, row: u32, - scroll_top: Pixels, content_origin: gpui::Point, - scroll_left: Pixels, whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], cx: &mut ViewContext, ) { let line_height = layout.position_map.line_height; - let line_y = line_height * row as f32 - scroll_top; + let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; self.line.paint( - content_origin + gpui::point(-scroll_left, line_y), + content_origin + gpui::point(-layout.position_map.scroll_position.x, line_y), line_height, cx, ); @@ -2321,7 +2331,6 @@ impl LineWithInvisibles { &selection_ranges, layout, content_origin, - scroll_left, line_y, row, line_height, @@ -2335,7 +2344,6 @@ impl LineWithInvisibles { selection_ranges: &[Range], layout: &LayoutState, content_origin: gpui::Point, - scroll_left: Pixels, line_y: Pixels, row: u32, line_height: Pixels, @@ -2357,8 +2365,11 @@ impl LineWithInvisibles { let x_offset = self.line.x_for_index(token_offset); let invisible_offset = (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0; - let origin = - content_origin + gpui::point(-scroll_left + x_offset + invisible_offset, line_y); + let origin = content_origin + + gpui::point( + x_offset + invisible_offset - layout.position_map.scroll_position.x, + line_y, + ); if let Some(allowed_regions) = allowed_invisibles_regions { let invisible_point = DisplayPoint::new(row, token_offset as u32); @@ -2440,13 +2451,6 @@ impl Element for EditorElement { // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); self.paint_background(gutter_bounds, text_bounds, &layout, cx); if layout.gutter_size.width > Pixels::ZERO { self.paint_gutter(gutter_bounds, &mut layout, editor, cx); @@ -2457,6 +2461,8 @@ impl Element for EditorElement { self.paint_blocks(bounds, &mut layout, editor, cx); } + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + let input_handler = ElementInputHandler::new(bounds, cx); cx.handle_input(&editor.focus_handle, input_handler); }); @@ -3080,6 +3086,7 @@ pub struct LayoutState { text_size: gpui::Size, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>, + visible_anchor_range: Range, visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, @@ -3087,7 +3094,6 @@ pub struct LayoutState { display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, - fold_ranges: Vec<(BufferRow, Range, Hsla)>, selections: Vec<(PlayerColor, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, @@ -3109,6 +3115,7 @@ struct CodeActionsIndicator { struct PositionMap { size: Size, line_height: Pixels, + scroll_position: gpui::Point, scroll_max: gpui::Point, em_width: Pixels, em_advance: Pixels, @@ -3445,58 +3452,6 @@ impl HighlightedRange { } } -// fn range_to_bounds( -// range: &Range, -// content_origin: gpui::Point, -// scroll_left: f32, -// scroll_top: f32, -// visible_row_range: &Range, -// line_end_overshoot: f32, -// position_map: &PositionMap, -// ) -> impl Iterator> { -// let mut bounds: SmallVec<[Bounds; 1]> = SmallVec::new(); - -// if range.start == range.end { -// return bounds.into_iter(); -// } - -// let start_row = visible_row_range.start; -// let end_row = visible_row_range.end; - -// let row_range = if range.end.column() == 0 { -// cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row) -// } else { -// cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row) -// }; - -// let first_y = -// content_origin.y + row_range.start as f32 * position_map.line_height - scroll_top; - -// for (idx, row) in row_range.enumerate() { -// let line_layout = &position_map.line_layouts[(row - start_row) as usize].line; - -// let start_x = if row == range.start.row() { -// content_origin.x + line_layout.x_for_index(range.start.column() as usize) -// - scroll_left -// } else { -// content_origin.x - scroll_left -// }; - -// let end_x = if row == range.end.row() { -// content_origin.x + line_layout.x_for_index(range.end.column() as usize) - scroll_left -// } else { -// content_origin.x + line_layout.width() + line_end_overshoot - scroll_left -// }; - -// bounds.push(Bounds::::from_points( -// point(start_x, first_y + position_map.line_height * idx as f32), -// point(end_x, first_y + position_map.line_height * (idx + 1) as f32), -// )) -// } - -// bounds.into_iter() -// } - pub fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 { (delta.pow(1.5) / 100.0).into() } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 46ea5c6cd2..9db256a572 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -3,7 +3,7 @@ use crate::{ }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, mem}; +use std::{any::Any, fmt::Debug, mem}; pub trait Element { type ElementState: 'static; @@ -33,6 +33,42 @@ pub trait Element { element_state: &mut Self::ElementState, cx: &mut ViewContext, ); + + fn draw( + self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + f: impl FnOnce(&Self::ElementState, &mut ViewContext) -> R, + ) -> R + where + Self: Sized, + T: Clone + Default + Debug + Into, + { + let mut element = RenderedElement { + element: self, + phase: ElementRenderPhase::Start, + }; + element.draw(origin, available_space.map(Into::into), view_state, cx); + if let ElementRenderPhase::Painted { frame_state } = &element.phase { + if let Some(frame_state) = frame_state.as_ref() { + f(&frame_state, cx) + } else { + let element_id = element + .element + .element_id() + .expect("we either have some frame_state or some element_id"); + cx.with_element_state(element_id, |element_state, cx| { + let element_state = element_state.unwrap(); + let result = f(&element_state, cx); + (result, element_state) + }) + } + } else { + unreachable!() + } + } } #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] @@ -99,7 +135,9 @@ enum ElementRenderPhase { available_space: Size, frame_state: Option, }, - Painted, + Painted { + frame_state: Option, + }, } /// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. @@ -157,7 +195,7 @@ where ElementRenderPhase::Start => panic!("must call initialize before layout"), ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::LayoutComputed { .. } - | ElementRenderPhase::Painted => { + | ElementRenderPhase::Painted { .. } => { panic!("element rendered twice") } }; @@ -192,7 +230,7 @@ where self.element .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx); } - ElementRenderPhase::Painted + ElementRenderPhase::Painted { frame_state } } _ => panic!("must call layout before paint"), diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a08d1619ae..447a15b542 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -711,6 +711,12 @@ pub struct DivState { interactive_state: InteractiveElementState, } +impl DivState { + pub fn is_active(&self) -> bool { + self.interactive_state.pending_mouse_down.lock().is_some() + } +} + pub struct Interactivity { pub element_id: Option, pub key_context: KeyContext, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 9fb9f51314..f209fd4bba 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -216,7 +216,7 @@ pub struct Window { // #[derive(Default)] pub(crate) struct Frame { - element_states: HashMap, + pub(crate) element_states: HashMap, mouse_listeners: HashMap>, pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, From 9ff238921fa5fe6dde8b9d5fbb88cdb63ceeb6cd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 18:29:53 +0100 Subject: [PATCH 07/36] Fix clicking on code action and fold indicators Co-Authored-By: Nathan Sobo --- crates/editor2/src/element.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index f5ea324050..eaaa2d2a2f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2451,6 +2451,12 @@ impl Element for EditorElement { // We call with_z_index to establish a new stacking context. cx.with_z_index(0, |cx| { cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + // Paint mouse listeners first, so any elements we paint on top of the editor + // take precedence. + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); if layout.gutter_size.width > Pixels::ZERO { self.paint_gutter(gutter_bounds, &mut layout, editor, cx); @@ -2460,11 +2466,6 @@ impl Element for EditorElement { if !layout.blocks.is_empty() { self.paint_blocks(bounds, &mut layout, editor, cx); } - - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); - - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); }); }); }, From d791fc707a50a4c30b147f8af771c2d2a2455718 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 15 Nov 2023 12:47:40 -0500 Subject: [PATCH 08/36] v0.114.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7eb358ddb..69f402f2e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11288,7 +11288,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.113.0" +version = "0.114.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 941898cdc3..028653696a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.113.0" +version = "0.114.0" publish = false [lib] From c7b7f7dfd5c34c68de5c7834c7f424f0713f23ef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 18:45:51 +0100 Subject: [PATCH 09/36] Move `render_view` into `View::render_with` Co-Authored-By: Nathan Sobo --- crates/editor2/src/editor.rs | 41 +++++++++++++++--------------------- crates/gpui2/src/view.rs | 27 ++++++++++++------------ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 41757b4dc2..af7da8e837 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,12 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list, - AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, - Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, - Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, - VisualContext, WeakView, WindowContext, + action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, + InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, + Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -7777,25 +7777,18 @@ impl Editor { } div() .pl(cx.anchor_x) - .child(render_view( + .child(rename_editor.render_with(EditorElement::new( &rename_editor, - EditorElement::new( - &rename_editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.editor_style.local_player, - text: text_style, - scrollbar_width: cx - .editor_style - .scrollbar_width, - syntax: cx.editor_style.syntax.clone(), - diagnostic_style: cx - .editor_style - .diagnostic_style - .clone(), - }, - ), - )) + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx.editor_style.scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + diagnostic_style: + cx.editor_style.diagnostic_style.clone(), + }, + ))) .render() } }), diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index c0bbe00db6..5a2cc0f103 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -63,6 +63,16 @@ impl View { pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V { self.model.read(cx) } + + pub fn render_with(&self, component: C) -> RenderViewWith + where + C: 'static + Component, + { + RenderViewWith { + view: self.clone(), + component: Some(component), + } + } } impl Clone for View { @@ -281,12 +291,12 @@ where } } -pub struct RenderView { +pub struct RenderViewWith { view: View, component: Option, } -impl Component for RenderView +impl Component for RenderViewWith where C: 'static + Component, ParentViewState: 'static, @@ -297,7 +307,7 @@ where } } -impl Element for RenderView +impl Element for RenderViewWith where C: 'static + Component, ParentViewState: 'static, @@ -348,17 +358,6 @@ where } } -pub fn render_view(view: &View, component: C) -> RenderView -where - C: 'static + Component, - V: 'static, -{ - RenderView { - view: view.clone(), - component: Some(component), - } -} - mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; From c225a3e5af84c1176558a315051eb1e332f10604 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 18:50:34 +0100 Subject: [PATCH 10/36] Don't use `Mutex` or `Arc` now that app state is not Send Co-Authored-By: Nathan Sobo --- crates/gpui2/src/elements/div.rs | 58 +++++++++++------------ crates/gpui2/src/elements/uniform_list.rs | 17 ++++--- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 447a15b542..5761af8b4d 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -6,15 +6,15 @@ use crate::{ SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, }; use collections::HashMap; -use parking_lot::Mutex; use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + cell::RefCell, fmt::Debug, marker::PhantomData, mem, - sync::Arc, + rc::Rc, time::Duration, }; use taffy::style::Overflow; @@ -406,7 +406,7 @@ pub trait StatefulInteractiveComponent>: InteractiveCo self.interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { + self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| { build_tooltip(view_state, cx).into() })); @@ -555,7 +555,7 @@ type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub type HoverListener = Box) + 'static>; -pub type TooltipBuilder = Arc) -> AnyView + 'static>; +pub type TooltipBuilder = Rc) -> AnyView + 'static>; pub type KeyDownListener = Box) + 'static>; @@ -713,7 +713,7 @@ pub struct DivState { impl DivState { pub fn is_active(&self) -> bool { - self.interactive_state.pending_mouse_down.lock().is_some() + self.interactive_state.pending_mouse_down.borrow().is_some() } } @@ -882,7 +882,7 @@ where if !click_listeners.is_empty() || drag_listener.is_some() { let pending_mouse_down = element_state.pending_mouse_down.clone(); - let mouse_down = pending_mouse_down.lock().clone(); + let mouse_down = pending_mouse_down.borrow().clone(); if let Some(mouse_down) = mouse_down { if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); @@ -896,7 +896,7 @@ where && bounds.contains_point(&event.position) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { - *active_state.lock() = ElementClickedState::default(); + *active_state.borrow_mut() = ElementClickedState::default(); let cursor_offset = event.position - bounds.origin; let drag = drag_listener(view_state, cursor_offset, cx); cx.active_drag = Some(drag); @@ -916,13 +916,13 @@ where listener(view_state, &mouse_click, cx); } } - *pending_mouse_down.lock() = None; + *pending_mouse_down.borrow_mut() = None; cx.notify(); }); } else { cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - *pending_mouse_down.lock() = Some(event.clone()); + *pending_mouse_down.borrow_mut() = Some(event.clone()); cx.notify(); } }); @@ -938,8 +938,8 @@ where return; } let is_hovered = - bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); - let mut was_hovered = was_hovered.lock(); + bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none(); + let mut was_hovered = was_hovered.borrow_mut(); if is_hovered != was_hovered.clone() { *was_hovered = is_hovered; @@ -960,13 +960,13 @@ where } let is_hovered = - bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none(); + bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none(); if !is_hovered { - active_tooltip.lock().take(); + active_tooltip.borrow_mut().take(); return; } - if active_tooltip.lock().is_none() { + if active_tooltip.borrow().is_none() { let task = cx.spawn({ let active_tooltip = active_tooltip.clone(); let tooltip_builder = tooltip_builder.clone(); @@ -974,7 +974,7 @@ where move |view, mut cx| async move { cx.background_executor().timer(TOOLTIP_DELAY).await; view.update(&mut cx, move |view_state, cx| { - active_tooltip.lock().replace(ActiveTooltip { + active_tooltip.borrow_mut().replace(ActiveTooltip { waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), @@ -986,14 +986,14 @@ where .ok(); } }); - active_tooltip.lock().replace(ActiveTooltip { + active_tooltip.borrow_mut().replace(ActiveTooltip { waiting: Some(task), tooltip: None, }); } }); - if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() { if active_tooltip.tooltip.is_some() { cx.active_tooltip = active_tooltip.tooltip.clone() } @@ -1001,10 +1001,10 @@ where } let active_state = element_state.clicked_state.clone(); - if !active_state.lock().is_clicked() { + if !active_state.borrow().is_clicked() { cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { - *active_state.lock() = ElementClickedState::default(); + *active_state.borrow_mut() = ElementClickedState::default(); cx.notify(); } }); @@ -1019,7 +1019,7 @@ where .map_or(false, |bounds| bounds.contains_point(&down.position)); let element = bounds.contains_point(&down.position); if group || element { - *active_state.lock() = ElementClickedState { group, element }; + *active_state.borrow_mut() = ElementClickedState { group, element }; cx.notify(); } } @@ -1030,14 +1030,14 @@ where if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { let scroll_offset = element_state .scroll_offset - .get_or_insert_with(Arc::default) + .get_or_insert_with(Rc::default) .clone(); let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - let mut scroll_offset = scroll_offset.lock(); + let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; let delta = event.delta.pixel_delta(line_height); @@ -1066,7 +1066,7 @@ where let scroll_offset = element_state .scroll_offset .as_ref() - .map(|scroll_offset| *scroll_offset.lock()); + .map(|scroll_offset| *scroll_offset.borrow()); cx.with_key_dispatch( self.key_context.clone(), @@ -1165,7 +1165,7 @@ where } } - let clicked_state = element_state.clicked_state.lock(); + let clicked_state = element_state.clicked_state.borrow(); if clicked_state.group { if let Some(group) = self.group_active_style.as_ref() { style.refine(&group.style) @@ -1219,11 +1219,11 @@ impl Default for Interactivity { #[derive(Default)] pub struct InteractiveElementState { pub focus_handle: Option, - pub clicked_state: Arc>, - pub hover_state: Arc>, - pub pending_mouse_down: Arc>>, - pub scroll_offset: Option>>>, - pub active_tooltip: Arc>>, + pub clicked_state: Rc>, + pub hover_state: Rc>, + pub pending_mouse_down: Rc>>, + pub scroll_offset: Option>>>, + pub active_tooltip: Rc>>, } pub struct ActiveTooltip { diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 84cd216275..340a2cbf87 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -3,9 +3,8 @@ use crate::{ ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Size, StyleRefinement, Styled, ViewContext, }; -use parking_lot::Mutex; use smallvec::SmallVec; -use std::{cmp, mem, ops::Range, sync::Arc}; +use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. @@ -61,23 +60,23 @@ pub struct UniformList { } #[derive(Clone, Default)] -pub struct UniformListScrollHandle(Arc>>); +pub struct UniformListScrollHandle(Rc>>); #[derive(Clone, Debug)] struct ScrollHandleState { item_height: Pixels, list_height: Pixels, - scroll_offset: Arc>>, + scroll_offset: Rc>>, } impl UniformListScrollHandle { pub fn new() -> Self { - Self(Arc::new(Mutex::new(None))) + Self(Rc::new(RefCell::new(None))) } pub fn scroll_to_item(&self, ix: usize) { - if let Some(state) = &*self.0.lock() { - let mut scroll_offset = state.scroll_offset.lock(); + if let Some(state) = &*self.0.borrow() { + let mut scroll_offset = state.scroll_offset.borrow_mut(); let item_top = state.item_height * ix; let item_bottom = item_top + state.item_height; let scroll_top = -scroll_offset.y; @@ -196,7 +195,7 @@ impl Element for UniformList { let shared_scroll_offset = element_state .interactive .scroll_offset - .get_or_insert_with(Arc::default) + .get_or_insert_with(Rc::default) .clone(); interactivity.paint( @@ -222,7 +221,7 @@ impl Element for UniformList { .measure_item(view_state, Some(padded_bounds.size.width), cx) .height; if let Some(scroll_handle) = self.scroll_handle.clone() { - scroll_handle.0.lock().replace(ScrollHandleState { + scroll_handle.0.borrow_mut().replace(ScrollHandleState { item_height, list_height: padded_bounds.size.height, scroll_offset: shared_scroll_offset, From 08dfcba68a73b9ce337bec7950f7a43236832c2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 20:02:35 +0100 Subject: [PATCH 11/36] Fix panic when painting blocks --- crates/editor2/src/element.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index eaaa2d2a2f..4d9a516f2b 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2464,7 +2464,9 @@ impl Element for EditorElement { self.paint_text(text_bounds, &mut layout, editor, cx); if !layout.blocks.is_empty() { - self.paint_blocks(bounds, &mut layout, editor, cx); + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, editor, cx); + }) } }); }); From 1d04dc5dbfcb78c868ab4d43e7c57b23f753e862 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 20:11:07 +0100 Subject: [PATCH 12/36] Clear the state of keystroke matchers when focus changes Co-Authored-By: Nathan Sobo --- crates/gpui2/src/key_dispatch.rs | 35 +++++++++++++++++++++++++------- crates/gpui2/src/window.rs | 18 +++++++++++++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index f737c6e30b..962a030844 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -60,7 +60,7 @@ impl DispatchTree { self.keystroke_matchers.clear(); } - pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) { + pub fn push_node(&mut self, context: KeyContext) { let parent = self.node_stack.last().copied(); let node_id = DispatchNodeId(self.nodes.len()); self.nodes.push(DispatchNode { @@ -71,12 +71,6 @@ impl DispatchTree { if !context.is_empty() { self.active_node().context = context.clone(); self.context_stack.push(context); - if let Some((context_stack, matcher)) = old_dispatcher - .keystroke_matchers - .remove_entry(self.context_stack.as_slice()) - { - self.keystroke_matchers.insert(context_stack, matcher); - } } } @@ -87,6 +81,33 @@ impl DispatchTree { } } + pub fn clear_keystroke_matchers(&mut self) { + self.keystroke_matchers.clear(); + } + + /// Preserve keystroke matchers from previous frames to support multi-stroke + /// bindings across multiple frames. + pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option) { + if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { + let dispatch_path = self.dispatch_path(node_id); + + self.context_stack.clear(); + for node_id in dispatch_path { + let node = self.node(node_id); + if !node.context.is_empty() { + self.context_stack.push(node.context.clone()); + } + + if let Some((context_stack, matcher)) = old_tree + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + } + pub fn on_key_event(&mut self, listener: KeyListener) { self.active_node().key_listeners.push(listener); } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c6361c8c6e..0563c107c0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -393,6 +393,10 @@ impl<'a> WindowContext<'a> { /// Move focus to the element associated with the given `FocusHandle`. pub fn focus(&mut self, handle: &FocusHandle) { + if self.window.focus == Some(handle.id) { + return; + } + let focus_id = handle.id; if self.window.last_blur.is_none() { @@ -400,6 +404,10 @@ impl<'a> WindowContext<'a> { } self.window.focus = Some(focus_id); + self.window + .current_frame + .dispatch_tree + .clear_keystroke_matchers(); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(focus_id), @@ -1091,6 +1099,14 @@ impl<'a> WindowContext<'a> { }); } + self.window + .current_frame + .dispatch_tree + .preserve_keystroke_matchers( + &mut self.window.previous_frame.dispatch_tree, + self.window.focus, + ); + self.window.root_view = Some(root_view); let scene = self.window.current_frame.scene_builder.build(); @@ -2093,7 +2109,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { window .current_frame .dispatch_tree - .push_node(context.clone(), &mut window.previous_frame.dispatch_tree); + .push_node(context.clone()); if let Some(focus_handle) = focus_handle.as_ref() { window .current_frame From 759ce7440cdd5c52c9cd0df8ea72f235f3a1279c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 20:12:30 +0100 Subject: [PATCH 13/36] Avoid unnecessary call to `with_element_id` in `RenderViewWith` Co-Authored-By: Nathan Sobo --- crates/gpui2/src/view.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 5a2cc0f103..3edaa900b0 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, - Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext, + Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, + Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -325,12 +325,10 @@ where _: Option, cx: &mut ViewContext, ) -> Self::ElementState { - cx.with_element_id(Some(self.view.entity_id()), |cx| { - self.view.update(cx, |view, cx| { - let mut element = self.component.take().unwrap().render(); - element.initialize(view, cx); - element - }) + self.view.update(cx, |view, cx| { + let mut element = self.component.take().unwrap().render(); + element.initialize(view, cx); + element }) } @@ -340,9 +338,7 @@ where element: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - cx.with_element_id(Some(self.view.entity_id()), |cx| { - self.view.update(cx, |view, cx| element.layout(view, cx)) - }) + self.view.update(cx, |view, cx| element.layout(view, cx)) } fn paint( @@ -352,9 +348,7 @@ where element: &mut Self::ElementState, cx: &mut ViewContext, ) { - cx.with_element_id(Some(self.view.entity_id()), |cx| { - self.view.update(cx, |view, cx| element.paint(view, cx)) - }) + self.view.update(cx, |view, cx| element.paint(view, cx)) } } From 33a808a49b61f2efe6e221b305b3f6028ab420c2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Nov 2023 20:41:09 +0100 Subject: [PATCH 14/36] WIP --- crates/gpui2/src/element.rs | 88 ++++------------------ crates/gpui2/src/elements/div.rs | 91 +++++++---------------- crates/gpui2/src/elements/img.rs | 13 +--- crates/gpui2/src/elements/svg.rs | 13 +--- crates/gpui2/src/elements/text.rs | 16 +--- crates/gpui2/src/elements/uniform_list.rs | 4 +- crates/gpui2/src/view.rs | 87 ++++++++-------------- crates/gpui2/src/window.rs | 16 +++- 8 files changed, 96 insertions(+), 232 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 9db256a572..3ee829df52 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -10,21 +10,12 @@ pub trait Element { fn element_id(&self) -> Option; - /// Called to initialize this element for the current frame. If this - /// element had state in a previous frame, it will be passed in for the 3rd argument. - fn initialize( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> Self::ElementState; - fn layout( &mut self, view_state: &mut V, - element_state: &mut Self::ElementState, + previous_element_state: Option, cx: &mut ViewContext, - ) -> LayoutId; + ) -> (LayoutId, Self::ElementState); fn paint( &mut self, @@ -96,7 +87,6 @@ pub trait ParentComponent { } trait ElementObject { - fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( @@ -123,9 +113,6 @@ struct RenderedElement> { enum ElementRenderPhase { #[default] Start, - Initialized { - frame_state: Option, - }, LayoutRequested { layout_id: LayoutId, frame_state: Option, @@ -157,42 +144,19 @@ where E: Element, E::ElementState: 'static, { - fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - let frame_state = if let Some(id) = self.element.element_id() { - cx.with_element_state(id, |element_state, cx| { - let element_state = self.element.initialize(view_state, element_state, cx); - ((), element_state) - }); - None - } else { - let frame_state = self.element.initialize(view_state, None, cx); - Some(frame_state) - }; - - self.phase = ElementRenderPhase::Initialized { frame_state }; - } - fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { - let layout_id; - let mut frame_state; - match mem::take(&mut self.phase) { - ElementRenderPhase::Initialized { - frame_state: initial_frame_state, - } => { - frame_state = initial_frame_state; + let (layout_id, frame_state) = match mem::take(&mut self.phase) { + ElementRenderPhase::Start => { if let Some(id) = self.element.element_id() { - layout_id = cx.with_element_state(id, |element_state, cx| { - let mut element_state = element_state.unwrap(); - let layout_id = self.element.layout(state, &mut element_state, cx); - (layout_id, element_state) + let layout_id = cx.with_element_state(id, |element_state, cx| { + self.element.layout(state, element_state, cx) }); + (layout_id, None) } else { - layout_id = self - .element - .layout(state, frame_state.as_mut().unwrap(), cx); + let (layout_id, frame_state) = self.element.layout(state, None, cx); + (layout_id, Some(frame_state)) } } - ElementRenderPhase::Start => panic!("must call initialize before layout"), ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::LayoutComputed { .. } | ElementRenderPhase::Painted { .. } => { @@ -244,10 +208,6 @@ where cx: &mut ViewContext, ) -> Size { if matches!(&self.phase, ElementRenderPhase::Start) { - self.initialize(view_state, cx); - } - - if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) { self.layout(view_state, cx); } @@ -290,10 +250,7 @@ where cx: &mut ViewContext, ) { self.measure(available_space, view_state, cx); - // Ignore the element offset when drawing this element, as the origin is already specified - // in absolute terms. - origin -= cx.element_offset(); - cx.with_element_offset(origin, |cx| self.paint(view_state, cx)) + cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) } } @@ -309,10 +266,6 @@ impl AnyElement { AnyElement(Box::new(RenderedElement::new(element))) } - pub fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.initialize(view_state, cx); - } - pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { self.0.layout(view_state, cx) } @@ -393,25 +346,16 @@ where None } - fn initialize( - &mut self, - view_state: &mut V, - _rendered_element: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - let render = self.take().unwrap(); - let mut rendered_element = (render)(view_state, cx).render(); - rendered_element.initialize(view_state, cx); - rendered_element - } - fn layout( &mut self, view_state: &mut V, - rendered_element: &mut Self::ElementState, + _: Option, cx: &mut ViewContext, - ) -> LayoutId { - rendered_element.layout(view_state, cx) + ) -> (LayoutId, Self::ElementState) { + let render = self.take().unwrap(); + let mut rendered_element = (render)(view_state, cx).render(); + let layout_id = rendered_element.layout(view_state, cx); + (layout_id, rendered_element) } fn paint( diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 1f9b2b020a..3a3ad5936e 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -617,46 +617,36 @@ impl Element for Div { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - let interactive_state = self - .interactivity - .initialize(element_state.map(|s| s.interactive_state), cx); - - for child in &mut self.children { - child.initialize(view_state, cx); - } - - DivState { - interactive_state, - child_layout_ids: SmallVec::new(), - } - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> crate::LayoutId { + ) -> (LayoutId, Self::ElementState) { + let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); - let layout_id = - interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| { + let (layout_id, interactive_state) = interactivity.layout( + element_state.map(|s| s.interactive_state), + cx, + |style, cx| { cx.with_text_style(style.text_style().cloned(), |cx| { - element_state.child_layout_ids = self + child_layout_ids = self .children .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + cx.request_layout(&style, child_layout_ids.iter().copied()) }) - }); + }, + ); self.interactivity = interactivity; - layout_id + ( + layout_id, + DivState { + interactive_state, + child_layout_ids, + }, + ) } fn paint( @@ -766,11 +756,12 @@ impl Interactivity where V: 'static, { - pub fn initialize( + pub fn layout( &mut self, element_state: Option, cx: &mut ViewContext, - ) -> InteractiveElementState { + f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + ) -> (LayoutId, InteractiveElementState) { let mut element_state = element_state.unwrap_or_default(); // Ensure we store a focus handle in our element state if we're focusable. @@ -785,17 +776,9 @@ where }); } - element_state - } - - pub fn layout( - &mut self, - element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, - ) -> LayoutId { - let style = self.compute_style(None, element_state, cx); - f(style, cx) + let style = self.compute_style(None, &mut element_state, cx); + let layout_id = f(style, cx); + (layout_id, element_state) } pub fn paint( @@ -1327,21 +1310,12 @@ where self.element.element_id() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.element.initialize(view_state, element_state, cx) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.element.layout(view_state, element_state, cx) } @@ -1422,21 +1396,12 @@ where self.element.element_id() } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.element.initialize(view_state, element_state, cx) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.element.layout(view_state, element_state, cx) } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index c5c5fb628e..1080135fe1 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -48,21 +48,12 @@ impl Element for Img { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, _view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.interactivity.initialize(element_state, cx) - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 4b441ad425..c1c7691fbf 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -37,21 +37,12 @@ impl Element for Svg { self.interactivity.element_id.clone() } - fn initialize( + fn layout( &mut self, _view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - self.interactivity.initialize(element_state, cx) - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 93d087833a..1081154e7d 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -76,21 +76,13 @@ impl Element for Text { None } - fn initialize( - &mut self, - _view_state: &mut V, - element_state: Option, - _cx: &mut ViewContext, - ) -> Self::ElementState { - element_state.unwrap_or_default() - } - fn layout( &mut self, _view: &mut V, - element_state: &mut Self::ElementState, + element_state: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { + let element_state = element_state.unwrap_or_default(); 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()); @@ -148,7 +140,7 @@ impl Element for Text { } }); - layout_id + (layout_id, element_state) } fn paint( diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 340a2cbf87..9736139619 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -131,9 +131,9 @@ impl Element for UniformList { fn layout( &mut self, _view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { let max_items = self.item_count; let item_size = element_state.item_size; let rem_size = cx.rem_size(); diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 3edaa900b0..1ce1c4d349 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, - Size, ViewContext, VisualContext, WeakModel, WindowContext, + BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, + Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -155,8 +155,7 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, - layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -164,7 +163,6 @@ impl AnyView { pub fn downgrade(&self) -> AnyWeakView { AnyWeakView { model: self.model.downgrade(), - initialize: self.initialize, layout: self.layout, paint: self.paint, } @@ -175,7 +173,6 @@ impl AnyView { Ok(model) => Ok(View { model }), Err(model) => Err(Self { model, - initialize: self.initialize, layout: self.layout, paint: self.paint, }), @@ -186,13 +183,19 @@ impl AnyView { self.model.entity_type } - pub(crate) fn draw(&self, available_space: Size, cx: &mut WindowContext) { - let mut rendered_element = (self.initialize)(self, cx); - let layout_id = (self.layout)(self, &mut rendered_element, cx); - cx.window - .layout_engine - .compute_layout(layout_id, available_space); - (self.paint)(self, &mut rendered_element, cx); + pub(crate) fn draw( + &self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) { + cx.with_absolute_element_offset(origin, |cx| { + let (layout_id, mut rendered_element) = (self.layout)(self, cx); + cx.window + .layout_engine + .compute_layout(layout_id, available_space); + (self.paint)(self, &mut rendered_element, cx); + }) } } @@ -206,7 +209,6 @@ impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), - initialize: any_view::initialize::, layout: any_view::layout::, paint: any_view::paint::, } @@ -220,21 +222,12 @@ impl Element for AnyView { Some(self.model.entity_id.into()) } - fn initialize( - &mut self, - _view_state: &mut ParentViewState, - _element_state: Option, - cx: &mut ViewContext, - ) -> Self::ElementState { - (self.initialize)(self, cx) - } - fn layout( &mut self, _view_state: &mut ParentViewState, - rendered_element: &mut Self::ElementState, + rendered_element: Option, cx: &mut ViewContext, - ) -> LayoutId { + ) -> (LayoutId, Self::ElementState) { (self.layout)(self, rendered_element, cx) } @@ -251,8 +244,7 @@ impl Element for AnyView { pub struct AnyWeakView { model: AnyWeakModel, - initialize: fn(&AnyView, &mut WindowContext) -> AnyBox, - layout: fn(&AnyView, &mut AnyBox, &mut WindowContext) -> LayoutId, + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), } @@ -261,7 +253,6 @@ impl AnyWeakView { let model = self.model.upgrade()?; Some(AnyView { model, - initialize: self.initialize, layout: self.layout, paint: self.paint, }) @@ -272,7 +263,6 @@ impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), - initialize: any_view::initialize::, layout: any_view::layout::, paint: any_view::paint::, } @@ -319,28 +309,19 @@ where Some(self.view.entity_id().into()) } - fn initialize( + fn layout( &mut self, _: &mut ParentViewState, _: Option, cx: &mut ViewContext, - ) -> Self::ElementState { + ) -> (LayoutId, Self::ElementState) { self.view.update(cx, |view, cx| { let mut element = self.component.take().unwrap().render(); - element.initialize(view, cx); - element + let layout_id = element.layout(view, cx); + (layout_id, element) }) } - fn layout( - &mut self, - _: &mut ParentViewState, - element: &mut Self::ElementState, - cx: &mut ViewContext, - ) -> LayoutId { - self.view.update(cx, |view, cx| element.layout(view, cx)) - } - fn paint( &mut self, _: Bounds, @@ -356,27 +337,17 @@ mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; - pub(crate) fn initialize(view: &AnyView, cx: &mut WindowContext) -> Box { - cx.with_element_id(Some(view.model.entity_id), |cx| { - let view = view.clone().downcast::().unwrap(); - let element = view.update(cx, |view, cx| { - let mut element = AnyElement::new(view.render(cx)); - element.initialize(view, cx); - element - }); - Box::new(element) - }) - } - pub(crate) fn layout( view: &AnyView, - element: &mut Box, cx: &mut WindowContext, - ) -> LayoutId { + ) -> (LayoutId, Box) { cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.layout(view, cx)) + view.update(cx, |view, cx| { + let mut element = AnyElement::new(view.render(cx)); + let layout_id = element.layout(view, cx); + (layout_id, Box::new(element) as Box) + }) }) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0563c107c0..9c19512871 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1633,8 +1633,8 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } - /// Update the global element offset based on the given offset. This is used to implement - /// scrolling and position drag handles. + /// Update the global element offset relative to the current offset. This is used to implement + /// scrolling. fn with_element_offset( &mut self, offset: Point, @@ -1644,7 +1644,17 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { return f(self); }; - let offset = self.element_offset() + offset; + let abs_offset = self.element_offset() + offset; + self.with_absolute_element_offset(abs_offset, f) + } + + /// Update the global element offset based on the given offset. This is used to implement + /// drag handles and other manual painting of elements. + fn with_absolute_element_offset( + &mut self, + offset: Point, + f: impl FnOnce(&mut Self) -> R, + ) -> R { self.window_mut() .current_frame .element_offset_stack From cebc8428c88a7f4049d2ab463c549eb4b04a0f84 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 15 Nov 2023 13:07:18 -0700 Subject: [PATCH 15/36] FileFinder tests --- .../command_palette2/src/command_palette.rs | 6 +- crates/file_finder2/src/file_finder.rs | 2335 ++++++++--------- crates/gpui2/src/app/test_context.rs | 18 + crates/workspace2/src/modal_layer.rs | 2 +- crates/workspace2/src/pane.rs | 13 +- crates/workspace2/src/workspace2.rs | 21 +- 6 files changed, 1144 insertions(+), 1251 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index a3594c0818..05bee6bd78 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -403,7 +403,7 @@ mod tests { let palette = workspace.update(cx, |workspace, cx| { workspace - .current_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .picker @@ -426,7 +426,7 @@ mod tests { cx.simulate_keystrokes("enter"); workspace.update(cx, |workspace, cx| { - assert!(workspace.current_modal::(cx).is_none()); + assert!(workspace.active_modal::(cx).is_none()); assert_eq!(editor.read(cx).text(cx), "ab") }); @@ -443,7 +443,7 @@ mod tests { let palette = workspace.update(cx, |workspace, cx| { workspace - .current_modal::(cx) + .active_modal::(cx) .unwrap() .read(cx) .picker diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index aae3bca160..57fc60a025 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -34,7 +34,7 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &Toggle, cx| { - let Some(file_finder) = workspace.current_modal::(cx) else { + let Some(file_finder) = workspace.active_modal::(cx) else { Self::open(workspace, cx); return; }; @@ -738,1236 +738,1103 @@ impl PickerDelegate for FileFinderDelegate { } } -// #[cfg(test)] -// mod tests { -// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - -// use super::*; -// use editor::Editor; -// use gpui::{Entity, TestAppContext, VisualTestContext}; -// use menu::{Confirm, SelectNext}; -// use serde_json::json; -// use workspace::{AppState, Workspace}; - -// #[ctor::ctor] -// fn init_logger() { -// if std::env::var("RUST_LOG").is_ok() { -// env_logger::init(); -// } -// } - -// #[gpui::test] -// async fn test_matching_paths(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "a": { -// "banana": "", -// "bandana": "", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - -// let (picker, workspace, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// picker -// .update(cx, |picker, cx| { -// picker.delegate.update_matches("bna".to_string(), cx) -// }) -// .await; - -// picker.update(cx, |picker, _| { -// assert_eq!(picker.delegate.matches.len(), 2); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(SelectNext); -// cx.dispatch_action(Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// assert_eq!( -// active_item -// .to_any() -// .downcast::() -// .unwrap() -// .read(cx) -// .title(cx), -// "bandana" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - -// let (picker, workspace, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// let file_query = &first_file_name[..3]; -// let file_row = 1; -// let file_column = 3; -// assert!(file_column <= first_file_contents.len()); -// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); -// picker -// .update(cx, |finder, cx| { -// finder -// .delegate -// .update_matches(query_inside_file.to_string(), cx) -// }) -// .await; -// picker.update(cx, |finder, _| { -// let finder = &finder.delegate; -// assert_eq!(finder.matches.len(), 1); -// let latest_search_query = finder -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(SelectNext); -// cx.dispatch_action(Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.executor().advance_clock(Duration::from_secs(2)); - -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(file_row, caret_selection.start.row + 1, -// "Query inside file should get caret with the same focus row"); -// assert_eq!(file_column, caret_selection.start.column as usize + 1, -// "Query inside file should get caret with the same focus column"); -// }); -// } - -// #[gpui::test] -// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { -// let app_state = init_test(cx); - -// let first_file_name = "first.rs"; -// let first_file_contents = "// First Rust file"; -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// first_file_name: first_file_contents, -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - -// let (picker, workspace, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// let file_query = &first_file_name[..3]; -// let file_row = 200; -// let file_column = 300; -// assert!(file_column > first_file_contents.len()); -// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); -// picker -// .update(cx, |picker, cx| { -// picker -// .delegate -// .update_matches(query_outside_file.to_string(), cx) -// }) -// .await; -// picker.update(cx, |finder, _| { -// let delegate = &finder.delegate; -// assert_eq!(delegate.matches.len(), 1); -// let latest_search_query = delegate -// .latest_search_query -// .as_ref() -// .expect("Finder should have a query after the update_matches call"); -// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); -// assert_eq!( -// latest_search_query.path_like.file_query_end, -// Some(file_query.len()) -// ); -// assert_eq!(latest_search_query.row, Some(file_row)); -// assert_eq!(latest_search_query.column, Some(file_column as u32)); -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(SelectNext); -// cx.dispatch_action(Confirm); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// let editor = cx.update(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// active_item.downcast::().unwrap() -// }); -// cx.executor().advance_clock(Duration::from_secs(2)); - -// editor.update(cx, |editor, cx| { -// let all_selections = editor.selections.all_adjusted(cx); -// assert_eq!( -// all_selections.len(), -// 1, -// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" -// ); -// let caret_selection = all_selections.into_iter().next().unwrap(); -// assert_eq!(caret_selection.start, caret_selection.end, -// "Caret selection should have its start and end at the same position"); -// assert_eq!(0, caret_selection.start.row, -// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); -// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, -// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); -// }); -// } - -// #[gpui::test] -// async fn test_matching_cancellation(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/dir", -// json!({ -// "hello": "", -// "goodbye": "", -// "halogen-light": "", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - -// let (picker, _, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// let query = test_path_like("hi"); -// picker -// .update(cx, |picker, cx| { -// picker.delegate.spawn_search(query.clone(), cx) -// }) -// .await; - -// picker.update(cx, |picker, _cx| { -// assert_eq!(picker.delegate.matches.len(), 5) -// }); - -// picker.update(cx, |picker, cx| { -// let delegate = &mut picker.delegate; -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); - -// // Simulate a search being cancelled after the time limit, -// // returning only a subset of the matches that would have been found. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[1].clone(), matches[3].clone()], -// cx, -// ); - -// // Simulate another cancellation. -// drop(delegate.spawn_search(query.clone(), cx)); -// delegate.set_search_matches( -// delegate.latest_search_id, -// true, // did-cancel -// query.clone(), -// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], -// cx, -// ); - -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); -// }); -// } - -// #[gpui::test] -// async fn test_ignored_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/ancestor", -// json!({ -// ".gitignore": "ignored-root", -// "ignored-root": { -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// "tracked-root": { -// ".gitignore": "height", -// "happiness": "", -// "height": "", -// "hi": "", -// "hiccup": "", -// }, -// }), -// ) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// [ -// "/ancestor/tracked-root".as_ref(), -// "/ancestor/ignored-root".as_ref(), -// ], -// cx, -// ) -// .await; - -// let (picker, _, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// picker -// .update(cx, |picker, cx| { -// picker.delegate.spawn_search(test_path_like("hi"), cx) -// }) -// .await; -// picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); -// } - -// #[gpui::test] -// async fn test_single_file_worktrees(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) -// .await; - -// let project = Project::test( -// app_state.fs.clone(), -// ["/root/the-parent-dir/the-file".as_ref()], -// cx, -// ) -// .await; - -// let (picker, _, mut cx) = build_find_picker(project, cx); -// let cx = &mut cx; - -// // Even though there is only one worktree, that worktree's filename -// // is included in the matching, because the worktree is a single file. -// picker -// .update(cx, |picker, cx| { -// picker.delegate.spawn_search(test_path_like("thf"), cx) -// }) -// .await; -// cx.read(|cx| { -// let picker = picker.read(cx); -// let delegate = &picker.delegate; -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches.len(), 1); - -// let (file_name, file_name_positions, full_path, full_path_positions) = -// delegate.labels_for_path_match(&matches[0]); -// assert_eq!(file_name, "the-file"); -// assert_eq!(file_name_positions, &[0, 1, 4]); -// assert_eq!(full_path, "the-file"); -// assert_eq!(full_path_positions, &[0, 1, 4]); -// }); - -// // Since the worktree root is a file, searching for its name followed by a slash does -// // not match anything. -// picker -// .update(cx, |f, cx| { -// f.delegate.spawn_search(test_path_like("thf/"), cx) -// }) -// .await; -// picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); -// } - -// #[gpui::test] -// async fn test_path_distance_ordering(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": { "a.txt": "" }, -// "dir2": { -// "a.txt": "", -// "b.txt": "" -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; - -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // When workspace has an active item, sort items which are closer to that item -// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt -// // so that one should be sorted earlier -// let b_path = Some(dummy_found_path(ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("/root/dir2/b.txt")), -// })); -// cx.dispatch_action(Toggle); - -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// b_path, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); - -// finder -// .update(cx, |f, cx| { -// f.delegate.spawn_search(test_path_like("a.txt"), cx) -// }) -// .await; - -// finder.read_with(cx, |f, _| { -// let delegate = &f.delegate; -// assert!( -// delegate.matches.history.is_empty(), -// "Search matches expected" -// ); -// let matches = delegate.matches.search.clone(); -// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); -// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); -// }); -// } - -// #[gpui::test] -// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { -// let app_state = init_test(cx); -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/root", -// json!({ -// "dir1": {}, -// "dir2": { -// "dir3": {} -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project, cx)) -// .root(cx); -// let finder = cx -// .add_window(|cx| { -// Picker::new( -// FileFinderDelegate::new( -// workspace.downgrade(), -// workspace.read(cx).project().clone(), -// None, -// Vec::new(), -// cx, -// ), -// cx, -// ) -// }) -// .root(cx); -// finder -// .update(cx, |f, cx| { -// f.delegate.spawn_search(test_path_like("dir"), cx) -// }) -// .await; -// cx.read(|cx| { -// let finder = finder.read(cx); -// assert_eq!(finder.delegate.matches.len(), 0); -// }); -// } - -// #[gpui::test] -// async fn test_query_history(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// WorktreeId::from_usize(worktrees[0].id()) -// }); - -// // Open and close panels, getting their history items afterwards. -// // Ensure history items get populated with opened items, and items are kept in a certain order. -// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. -// // -// // TODO: without closing, the opened items do not propagate their history changes for some reason -// // it does work in real app though, only tests do not propagate. - -// let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// assert!( -// initial_history.is_empty(), -// "Should have no history before opening any files" -// ); - -// let history_after_first = -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// assert_eq!( -// history_after_first, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )], -// "Should show 1st opened item in the history when opening the 2nd item" -// ); - -// let history_after_second = -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// assert_eq!( -// history_after_second, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ -// 2nd item should be the first in the history, as the last opened." -// ); - -// let history_after_third = -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// assert_eq!( -// history_after_third, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ -// 3rd item should be the first in the history, as the last opened." -// ); - -// let history_after_second_again = -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// assert_eq!( -// history_after_second_again, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/third.rs")), -// }, -// Some(PathBuf::from("/src/test/third.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// ), -// ], -// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ -// 2nd item, as the last opened, 3rd item should go next as it was opened right before." -// ); -// } - -// #[gpui::test] -// async fn test_external_files_history(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// } -// }), -// ) -// .await; - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/external-src", -// json!({ -// "test": { -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// cx.update(|cx| { -// project.update(cx, |project, cx| { -// project.find_or_create_local_worktree("/external-src", false, cx) -// }) -// }) -// .detach(); -// cx.background_executor.run_until_parked(); - -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].id()) -// }); -// workspace -// .update(cx, |workspace, cx| { -// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) -// }) -// .detach(); -// cx.background_executor.run_until_parked(); -// let external_worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!( -// worktrees.len(), -// 2, -// "External file should get opened in a new worktree" -// ); - -// WorktreeId::from_usize( -// worktrees -// .into_iter() -// .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) -// .expect("New worktree should have a different id") -// .id(), -// ) -// }); -// close_active_item(&workspace, cx).await; - -// let initial_history_items = -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// assert_eq!( -// initial_history_items, -// vec![FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// )], -// "Should show external file with its full path in the history after it was open" -// ); - -// let updated_history_items = -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// assert_eq!( -// updated_history_items, -// vec![ -// FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/second.rs")), -// }, -// Some(PathBuf::from("/src/test/second.rs")) -// ), -// FoundPath::new( -// ProjectPath { -// worktree_id: external_worktree_id, -// path: Arc::from(Path::new("")), -// }, -// Some(PathBuf::from("/external-src/test/third.rs")) -// ), -// ], -// "Should keep external file with history updates", -// ); -// } - -// #[gpui::test] -// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; - -// // generate some history to select from -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// cx.executor().run_until_parked(); -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - -// for expected_selected_index in 0..current_history.len() { -// cx.dispatch_action(Toggle); -// let selected_index = workspace.update(cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// .read(cx) -// .delegate -// .selected_index() -// }); -// assert_eq!( -// selected_index, expected_selected_index, -// "Should select the next item in the history" -// ); -// } - -// cx.dispatch_action(Toggle); -// let selected_index = workspace.update(cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// .read(cx) -// .delegate -// .selected_index() -// }); -// assert_eq!( -// selected_index, 0, -// "Should wrap around the history and start all over" -// ); -// } - -// #[gpui::test] -// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "fourth.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; -// let worktree_id = cx.read(|cx| { -// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1,); - -// WorktreeId::from_usize(worktrees[0].entity_id()) -// }); - -// // generate some history to select from -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - -// cx.dispatch_action(Toggle); -// let first_query = "f"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate.update_matches(first_query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = &finder.delegate; -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); - -// let second_query = "fsdasdsa"; -// let finder = workspace.update(cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// }); -// finder -// .update(cx, |finder, cx| { -// finder.delegate.update_matches(second_query.to_string(), cx) -// }) -// .await; -// finder.update(cx, |finder, _| { -// let delegate = &finder.delegate; -// assert!( -// delegate.matches.history.is_empty(), -// "No history entries should match {second_query}" -// ); -// assert!( -// delegate.matches.search.is_empty(), -// "No search entries should match {second_query}" -// ); -// }); - -// let first_query_again = first_query; - -// let finder = workspace.update(cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// }); -// finder -// .update(cx, |finder, cx| { -// finder -// .delegate -// .update_matches(first_query_again.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = &finder.delegate; -// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); -// let history_match = delegate.matches.history.first().unwrap(); -// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); -// assert_eq!(history_match.0, FoundPath::new( -// ProjectPath { -// worktree_id, -// path: Arc::from(Path::new("test/first.rs")), -// }, -// Some(PathBuf::from("/src/test/first.rs")) -// )); -// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); -// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); -// }); -// } - -// #[gpui::test] -// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "collab_ui": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// "collab_ui.rs": "// Fourth Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; -// // generate some history to select from -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - -// cx.dispatch_action(Toggle); -// let query = "collab_ui"; -// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.delegate.update_matches(query.to_string(), cx) -// }) -// .await; -// finder.read_with(cx, |finder, _| { -// let delegate = &finder.delegate; -// assert!( -// delegate.matches.history.is_empty(), -// "History items should not math query {query}, they should be matched by name only" -// ); - -// let search_entries = delegate -// .matches -// .search -// .iter() -// .map(|path_match| path_match.path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// search_entries, -// vec![ -// PathBuf::from("collab_ui/collab_ui.rs"), -// PathBuf::from("collab_ui/third.rs"), -// PathBuf::from("collab_ui/first.rs"), -// PathBuf::from("collab_ui/second.rs"), -// ], -// "Despite all search results having the same directory name, the most matching one should be on top" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { -// let app_state = init_test(cx); - -// app_state -// .fs -// .as_fake() -// .insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "nonexistent.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// let cx = &mut cx; -// // generate some history to select from -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; -// open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; -// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; -// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - -// cx.dispatch_action(Toggle); -// let query = "rs"; -// let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); -// finder -// .update(cx, |finder, cx| { -// finder.picker.update(cx, |picker, cx| { -// picker.delegate.update_matches(query.to_string(), cx) -// }) -// }) -// .await; -// finder.update(cx, |finder, _| { -// let history_entries = finder.delegate -// .matches -// .history -// .iter() -// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) -// .collect::>(); -// assert_eq!( -// history_entries, -// vec![ -// PathBuf::from("test/first.rs"), -// PathBuf::from("test/third.rs"), -// ], -// "Should have all opened files in the history, except the ones that do not exist on disk" -// ); -// }); -// } - -// async fn open_close_queried_buffer( -// input: &str, -// expected_matches: usize, -// expected_editor_title: &str, -// workspace: &View, -// cx: &mut gpui::VisualTestContext<'_>, -// ) -> Vec { -// cx.dispatch_action(Toggle); -// let picker = workspace.update(cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// .clone() -// }); -// picker -// .update(cx, |finder, cx| { -// finder.delegate.update_matches(input.to_string(), cx) -// }) -// .await; -// let history_items = picker.update(cx, |finder, _| { -// assert_eq!( -// finder.delegate.matches.len(), -// expected_matches, -// "Unexpected number of matches found for query {input}" -// ); -// finder.delegate.history_items.clone() -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// cx.dispatch_action(SelectNext); -// cx.dispatch_action(Confirm); -// cx.background_executor.run_until_parked(); -// active_pane -// .condition(cx, |pane, _| pane.active_item().is_some()) -// .await; -// cx.read(|cx| { -// let active_item = active_pane.read(cx).active_item().unwrap(); -// let active_editor_title = active_item -// .to_any() -// .downcast::() -// .unwrap() -// .read(cx) -// .title(cx); -// assert_eq!( -// expected_editor_title, active_editor_title, -// "Unexpected editor title for query {input}" -// ); -// }); - -// close_active_item(workspace, cx).await; - -// history_items -// } - -// async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { -// let mut original_items = HashMap::new(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.entity_id(); -// let pane = pane.read(cx); -// let insertion_result = original_items.insert(pane_id, pane.items().count()); -// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); -// } -// }); - -// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); -// active_pane -// .update(cx, |pane, cx| { -// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) -// .unwrap() -// }) -// .await -// .unwrap(); -// cx.background_executor.run_until_parked(); -// cx.read(|cx| { -// for pane in workspace.read(cx).panes() { -// let pane_id = pane.entity_id(); -// let pane = pane.read(cx); -// match original_items.remove(&pane_id) { -// Some(original_items) => { -// assert_eq!( -// pane.items().count(), -// original_items.saturating_sub(1), -// "Pane id {pane_id} should have item closed" -// ); -// } -// None => panic!("Pane id {pane_id} not found in original items"), -// } -// } -// }); -// assert!( -// original_items.len() <= 1, -// "At most one panel should got closed" -// ); -// } - -// fn init_test(cx: &mut TestAppContext) -> Arc { -// cx.update(|cx| { -// let state = AppState::test(cx); -// theme::init(cx); -// language::init(cx); -// super::init(cx); -// editor::init(cx); -// workspace::init_settings(cx); -// Project::init_settings(cx); -// state -// }) -// } - -// fn test_path_like(test_str: &str) -> PathLikeWithPosition { -// PathLikeWithPosition::parse_str(test_str, |path_like_str| { -// Ok::<_, std::convert::Infallible>(FileSearchQuery { -// raw_query: test_str.to_owned(), -// file_query_end: if path_like_str == test_str { -// None -// } else { -// Some(path_like_str.len()) -// }, -// }) -// }) -// .unwrap() -// } - -// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { -// FoundPath { -// project: project_path, -// absolute: None, -// } -// } - -// fn build_find_picker( -// project: Model, -// cx: &mut TestAppContext, -// ) -> ( -// View>, -// View, -// VisualTestContext, -// ) { -// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); -// cx.dispatch_action(Toggle); -// let picker = workspace.update(&mut cx, |workspace, cx| { -// workspace -// .current_modal::(cx) -// .unwrap() -// .read(cx) -// .picker -// .clone() -// }); -// (picker, workspace, cx) -// } -// } +#[cfg(test)] +mod tests { + use std::{assert_eq, path::Path, time::Duration}; + + use super::*; + use editor::Editor; + use gpui::{Entity, TestAppContext, VisualTestContext}; + use menu::{Confirm, SelectNext}; + use serde_json::json; + use workspace::{AppState, Workspace}; + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + + #[gpui::test] + async fn test_matching_paths(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "banana": "", + "bandana": "", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + cx.simulate_input("bna"); + + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!(active_editor.read(cx).title(cx), "bandana"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 1; + let file_column = 3; + assert!(file_column <= first_file_contents.len()); + let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let finder = &finder.delegate; + assert_eq!(finder.matches.len(), 1); + let latest_search_query = finder + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + let editor = cx.update(|cx| workspace.read(cx).active_item_as::(cx).unwrap()); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(file_row, caret_selection.start.row + 1, + "Query inside file should get caret with the same focus row"); + assert_eq!(file_column, caret_selection.start.column as usize + 1, + "Query inside file should get caret with the same focus column"); + }); + } + + #[gpui::test] + async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let first_file_name = "first.rs"; + let first_file_contents = "// First Rust file"; + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + first_file_name: first_file_contents, + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + + let (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let file_query = &first_file_name[..3]; + let file_row = 200; + let file_column = 300; + assert!(file_column > first_file_contents.len()); + let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.len(), 1); + let latest_search_query = delegate + .latest_search_query + .as_ref() + .expect("Finder should have a query after the update_matches call"); + assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); + assert_eq!( + latest_search_query.path_like.file_query_end, + Some(file_query.len()) + ); + assert_eq!(latest_search_query.row, Some(file_row)); + assert_eq!(latest_search_query.column, Some(file_column as u32)); + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + let editor = cx.update(|cx| workspace.read(cx).active_item_as::(cx).unwrap()); + cx.executor().advance_clock(Duration::from_secs(2)); + + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all_adjusted(cx); + assert_eq!( + all_selections.len(), + 1, + "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" + ); + let caret_selection = all_selections.into_iter().next().unwrap(); + assert_eq!(caret_selection.start, caret_selection.end, + "Caret selection should have its start and end at the same position"); + assert_eq!(0, caret_selection.start.row, + "Excessive rows (as in query outside file borders) should get trimmed to last file row"); + assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, + "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); + }); + } + + #[gpui::test] + async fn test_matching_cancellation(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/dir", + json!({ + "hello": "", + "goodbye": "", + "halogen-light": "", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let query = test_path_like("hi"); + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(query.clone(), cx) + }) + .await; + + picker.update(cx, |picker, _cx| { + assert_eq!(picker.delegate.matches.len(), 5) + }); + + picker.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + + // Simulate a search being cancelled after the time limit, + // returning only a subset of the matches that would have been found. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[1].clone(), matches[3].clone()], + cx, + ); + + // Simulate another cancellation. + drop(delegate.spawn_search(query.clone(), cx)); + delegate.set_search_matches( + delegate.latest_search_id, + true, // did-cancel + query.clone(), + vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], + cx, + ); + + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); + }); + } + + #[gpui::test] + async fn test_ignored_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/ancestor", + json!({ + ".gitignore": "ignored-root", + "ignored-root": { + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + "tracked-root": { + ".gitignore": "height", + "happiness": "", + "height": "", + "hi": "", + "hiccup": "", + }, + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + "/ancestor/tracked-root".as_ref(), + "/ancestor/ignored-root".as_ref(), + ], + cx, + ) + .await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(test_path_like("hi"), cx) + }) + .await; + picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); + } + + #[gpui::test] + async fn test_single_file_worktrees(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) + .await; + + let project = Project::test( + app_state.fs.clone(), + ["/root/the-parent-dir/the-file".as_ref()], + cx, + ) + .await; + + let (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + // Even though there is only one worktree, that worktree's filename + // is included in the matching, because the worktree is a single file. + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(test_path_like("thf"), cx) + }) + .await; + cx.read(|cx| { + let picker = picker.read(cx); + let delegate = &picker.delegate; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + assert_eq!(matches.len(), 1); + + let (file_name, file_name_positions, full_path, full_path_positions) = + delegate.labels_for_path_match(&matches[0]); + assert_eq!(file_name, "the-file"); + assert_eq!(file_name_positions, &[0, 1, 4]); + assert_eq!(full_path, "the-file"); + assert_eq!(full_path_positions, &[0, 1, 4]); + }); + + // Since the worktree root is a file, searching for its name followed by a slash does + // not match anything. + picker + .update(cx, |f, cx| { + f.delegate.spawn_search(test_path_like("thf/"), cx) + }) + .await; + picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); + } + + #[gpui::test] + async fn test_path_distance_ordering(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "dir1": { "a.txt": "" }, + "dir2": { + "a.txt": "", + "b.txt": "" + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize) + }); + + // When workspace has an active item, sort items which are closer to that item + // first when they have the same name. In this case, b.txt is closer to dir2's a.txt + // so that one should be sorted earlier + let b_path = ProjectPath { + worktree_id, + path: Arc::from(Path::new("/root/dir2/b.txt")), + }; + workspace + .update(cx, |workspace, cx| { + workspace.open_path(b_path, None, true, cx) + }) + .await + .unwrap(); + let finder = open_file_picker(&workspace, cx); + finder + .update(cx, |f, cx| { + f.delegate.spawn_search(test_path_like("a.txt"), cx) + }) + .await; + + finder.update(cx, |f, _| { + let delegate = &f.delegate; + assert!( + delegate.matches.history.is_empty(), + "Search matches expected" + ); + let matches = delegate.matches.search.clone(); + assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); + assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); + }); + } + + #[gpui::test] + async fn test_search_worktree_without_files(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "dir1": {}, + "dir2": { + "dir3": {} + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + let (picker, _workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + picker + .update(cx, |f, cx| { + f.delegate.spawn_search(test_path_like("dir"), cx) + }) + .await; + cx.read(|cx| { + let finder = picker.read(cx); + assert_eq!(finder.delegate.matches.len(), 0); + }); + } + + #[gpui::test] + async fn test_query_history(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize) + }); + + // Open and close panels, getting their history items afterwards. + // Ensure history items get populated with opened items, and items are kept in a certain order. + // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // + // TODO: without closing, the opened items do not propagate their history changes for some reason + // it does work in real app though, only tests do not propagate. + workspace.update(cx, |_, cx| dbg!(cx.focused())); + + let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + assert!( + initial_history.is_empty(), + "Should have no history before opening any files" + ); + + let history_after_first = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + assert_eq!( + history_after_first, + vec![FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )], + "Should show 1st opened item in the history when opening the 2nd item" + ); + + let history_after_second = + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + assert_eq!( + history_after_second, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ + 2nd item should be the first in the history, as the last opened." + ); + + let history_after_third = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + assert_eq!( + history_after_third, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + Some(PathBuf::from("/src/test/third.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ + 3rd item should be the first in the history, as the last opened." + ); + + let history_after_second_again = + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + assert_eq!( + history_after_second_again, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + Some(PathBuf::from("/src/test/third.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + ), + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ + 2nd item, as the last opened, 3rd item should go next as it was opened right before." + ); + } + + #[gpui::test] + async fn test_external_files_history(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + app_state + .fs + .as_fake() + .insert_tree( + "/external-src", + json!({ + "test": { + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + cx.update(|cx| { + project.update(cx, |project, cx| { + project.find_or_create_local_worktree("/external-src", false, cx) + }) + }) + .detach(); + cx.background_executor.run_until_parked(); + + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1,); + + WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize) + }); + workspace + .update(cx, |workspace, cx| { + workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) + }) + .detach(); + cx.background_executor.run_until_parked(); + let external_worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!( + worktrees.len(), + 2, + "External file should get opened in a new worktree" + ); + + WorktreeId::from_usize( + worktrees + .into_iter() + .find(|worktree| { + worktree.entity_id().as_u64() as usize != worktree_id.to_usize() + }) + .expect("New worktree should have a different id") + .entity_id() + .as_u64() as usize, + ) + }); + cx.dispatch_action(workspace::CloseActiveItem { save_intent: None }); + + let initial_history_items = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + assert_eq!( + initial_history_items, + vec![FoundPath::new( + ProjectPath { + worktree_id: external_worktree_id, + path: Arc::from(Path::new("")), + }, + Some(PathBuf::from("/external-src/test/third.rs")) + )], + "Should show external file with its full path in the history after it was open" + ); + + let updated_history_items = + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + assert_eq!( + updated_history_items, + vec![ + FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + Some(PathBuf::from("/src/test/second.rs")) + ), + FoundPath::new( + ProjectPath { + worktree_id: external_worktree_id, + path: Arc::from(Path::new("")), + }, + Some(PathBuf::from("/external-src/test/third.rs")) + ), + ], + "Should keep external file with history updates", + ); + } + + #[gpui::test] + async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + cx.executor().run_until_parked(); + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + let current_history = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(Toggle); + let picker = active_file_picker(&workspace, cx); + let selected_index = picker.update(cx, |picker, _| picker.delegate.selected_index()); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, 0, + "Should wrap around the history and start all over" + ); + } + + #[gpui::test] + async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1,); + + WorktreeId::from_usize(worktrees[0].entity_id().as_u64() as usize) + }); + + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + let finder = open_file_picker(&workspace, cx); + let first_query = "f"; + finder + .update(cx, |finder, cx| { + finder.delegate.update_matches(first_query.to_string(), cx) + }) + .await; + finder.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + + let second_query = "fsdasdsa"; + let finder = active_file_picker(&workspace, cx); + finder + .update(cx, |finder, cx| { + finder.delegate.update_matches(second_query.to_string(), cx) + }) + .await; + finder.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert!( + delegate.matches.history.is_empty(), + "No history entries should match {second_query}" + ); + assert!( + delegate.matches.search.is_empty(), + "No search entries should match {second_query}" + ); + }); + + let first_query_again = first_query; + + let finder = active_file_picker(&workspace, cx); + finder + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(first_query_again.to_string(), cx) + }) + .await; + finder.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); + let history_match = delegate.matches.history.first().unwrap(); + assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); + assert_eq!(history_match.0, FoundPath::new( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + Some(PathBuf::from("/src/test/first.rs")) + )); + assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); + assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); + }); + } + + #[gpui::test] + async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "collab_ui": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + "collab_ui.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + let finder = open_file_picker(&workspace, cx); + let query = "collab_ui"; + cx.simulate_input(query); + finder.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert!( + delegate.matches.history.is_empty(), + "History items should not math query {query}, they should be matched by name only" + ); + + let search_entries = delegate + .matches + .search + .iter() + .map(|path_match| path_match.path.to_path_buf()) + .collect::>(); + assert_eq!( + search_entries, + vec![ + PathBuf::from("collab_ui/collab_ui.rs"), + PathBuf::from("collab_ui/third.rs"), + PathBuf::from("collab_ui/first.rs"), + PathBuf::from("collab_ui/second.rs"), + ], + "Despite all search results having the same directory name, the most matching one should be on top" + ); + }); + } + + #[gpui::test] + async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "nonexistent.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + + picker.update(cx, |finder, _| { + let history_entries = finder.delegate + .matches + .history + .iter() + .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) + .collect::>(); + assert_eq!( + history_entries, + vec![ + PathBuf::from("test/first.rs"), + PathBuf::from("test/third.rs"), + ], + "Should have all opened files in the history, except the ones that do not exist on disk" + ); + }); + } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + workspace: &View, + cx: &mut gpui::VisualTestContext<'_>, + ) -> Vec { + let picker = open_file_picker(&workspace, cx); + cx.simulate_input(input); + + let history_items = picker.update(cx, |finder, _| { + assert_eq!( + finder.delegate.matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate.history_items.clone() + }); + + cx.dispatch_action(SelectNext); + cx.dispatch_action(Confirm); + + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + let active_editor_title = active_editor.read(cx).title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + cx.dispatch_action(workspace::CloseActiveItem { save_intent: None }); + + history_items + } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let state = AppState::test(cx); + theme::init(cx); + language::init(cx); + super::init(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + state + }) + } + + fn test_path_like(test_str: &str) -> PathLikeWithPosition { + PathLikeWithPosition::parse_str(test_str, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: test_str.to_owned(), + file_query_end: if path_like_str == test_str { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .unwrap() + } + + fn build_find_picker( + project: Model, + cx: &mut TestAppContext, + ) -> ( + View>, + View, + VisualTestContext, + ) { + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let picker = open_file_picker(&workspace, &mut cx); + (picker, workspace, cx) + } + + #[track_caller] + fn open_file_picker( + workspace: &View, + cx: &mut VisualTestContext, + ) -> View> { + cx.dispatch_action(Toggle); + active_file_picker(workspace, cx) + } + + #[track_caller] + fn active_file_picker( + workspace: &View, + cx: &mut VisualTestContext, + ) -> View> { + workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }) + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 5397a2214d..7eb10c675f 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -225,6 +225,9 @@ impl TestAppContext { self.background_executor.run_until_parked() } + /// simulate_keystrokes takes a space-separated list of keys to type. + /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") + /// will run backspace on the current editor through the command palette. pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { for keystroke in keystrokes .split(" ") @@ -237,6 +240,17 @@ impl TestAppContext { self.background_executor.run_until_parked() } + /// simulate_input takes a string of text to type. + /// cx.simulate_input("abc") + /// will type abc into your current editor. + pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { + for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { + self.dispatch_keystroke(window, keystroke.into(), false); + } + + self.background_executor.run_until_parked() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -455,6 +469,10 @@ impl<'a> VisualTestContext<'a> { pub fn simulate_keystrokes(&mut self, keystrokes: &str) { self.cx.simulate_keystrokes(self.window, keystrokes) } + + pub fn simulate_input(&mut self, input: &str) { + self.cx.simulate_input(self.window, input) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index c91df732c7..cd5995d65e 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -72,7 +72,7 @@ impl ModalLayer { cx.notify(); } - pub fn current_modal(&self) -> Option> + pub fn active_modal(&self) -> Option> where V: 'static, { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 67ecc16165..668ce2f207 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -8,8 +8,8 @@ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, - WeakView, WindowContext, + EventEmitter, FocusHandle, Focusable, Model, PromptLevel, Render, Task, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -1017,7 +1017,11 @@ impl Pane { .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); let should_activate = activate_pane || self.has_focus(cx); - self.activate_item(index_to_activate, should_activate, should_activate, cx); + if self.items.len() == 1 && should_activate { + self.focus_handle.focus(cx); + } else { + self.activate_item(index_to_activate, should_activate, should_activate, cx); + } } let item = self.items.remove(item_index); @@ -1913,11 +1917,12 @@ impl Pane { // } impl Render for Pane { - type Element = Div; + type Element = Focusable>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() .key_context("Pane") + .track_focus(&self.focus_handle) .size_full() .on_action(|pane: &mut Self, action, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 13997e7588..6c2d0c0ede 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,9 +38,9 @@ use futures::{ use gpui::{ actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, - Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render, + Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -433,7 +433,6 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, - focus_handle: FocusHandle, workspace_actions: Vec) -> Div>>, zoomed: Option, zoomed_position: Option, @@ -651,7 +650,6 @@ impl Workspace { cx.defer(|this, cx| this.update_window_title(cx)); Workspace { weak_self: weak_handle.clone(), - focus_handle: cx.focus_handle(), zoomed: None, zoomed_position: None, center: PaneGroup::new(center_pane.clone()), @@ -1450,6 +1448,11 @@ impl Workspace { self.active_pane().read(cx).active_item() } + pub fn active_item_as(&self, cx: &AppContext) -> Option> { + let item = self.active_item(cx)?; + item.to_any().downcast::().ok() + } + fn active_project_path(&self, cx: &ViewContext) -> Option { self.active_item(cx).and_then(|item| item.project_path(cx)) } @@ -1570,7 +1573,7 @@ impl Workspace { } if focus_center { - cx.focus(&self.focus_handle); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } cx.notify(); @@ -1704,7 +1707,7 @@ impl Workspace { } if focus_center { - cx.focus(&self.focus_handle); + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) } if self.zoomed_position != dock_to_reveal { @@ -3475,8 +3478,8 @@ impl Workspace { div } - pub fn current_modal(&mut self, cx: &ViewContext) -> Option> { - self.modal_layer.read(cx).current_modal() + pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + self.modal_layer.read(cx).active_modal() } pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) From 0a51784dd05c94ef4f702db47ab29f05c2f07055 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 15 Nov 2023 14:00:46 -0700 Subject: [PATCH 16/36] Leaky, but better, test abstraction --- .../command_palette2/src/command_palette.rs | 7 +- crates/editor2/src/editor_tests.rs | 68 ++++++++----------- crates/file_finder2/src/file_finder.rs | 50 +++++--------- crates/gpui2/src/app/test_context.rs | 6 +- 4 files changed, 54 insertions(+), 77 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 05bee6bd78..8138f025d3 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -385,8 +385,7 @@ mod tests { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); let editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); @@ -417,7 +416,7 @@ mod tests { assert!(is_sorted(&palette.delegate.commands)); }); - cx.simulate_keystrokes("b c k s p"); + cx.simulate_input("bcksp"); palette.update(cx, |palette, _| { assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); @@ -439,7 +438,7 @@ mod tests { }); cx.simulate_keystrokes("cmd-shift-p"); - cx.simulate_keystrokes("b c k s p"); + cx.simulate_input("bcksp"); let palette = workspace.update(cx, |workspace, cx| { workspace diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 9798735bf6..63bc6179c2 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), @@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }), + view.update(cx, |view, cx| { view.selections.display_ranges(cx) }), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), ] ); - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); // Trying to expand the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] ); - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), ] ); - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ] ); - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { ); // Trying to shrink the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), @@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { // Ensure that we keep expanding the selection if the larger selection starts or ends within // a fold. - view.update(&mut cx, |view, cx| { + view.update(cx, |view, cx| { view.fold_ranges( vec![ Point::new(0, 21)..Point::new(0, 24), @@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); }); assert_eq!( - view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)), + view.update(cx, |view, cx| view.selections.display_ranges(cx)), &[ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), @@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; @@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); let format = editor @@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); - let cx = &mut cx; + let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { @@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { multibuffer }); - let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); - let cx = &mut cx; + let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" @@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx) }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { let buffer_text = "one\ntwo\nthree\n"; let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - let cx = &mut cx; + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); editor diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 57fc60a025..236bc15244 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -775,8 +775,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, workspace, cx) = build_find_picker(project, cx); cx.simulate_input("bna"); @@ -815,8 +814,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, workspace, cx) = build_find_picker(project, cx); let file_query = &first_file_name[..3]; let file_row = 1; @@ -891,8 +889,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, workspace, cx) = build_find_picker(project, cx); let file_query = &first_file_name[..3]; let file_row = 200; @@ -967,8 +964,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - let (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, _, cx) = build_find_picker(project, cx); let query = test_path_like("hi"); picker @@ -1055,8 +1051,7 @@ mod tests { ) .await; - let (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, _, cx) = build_find_picker(project, cx); picker .update(cx, |picker, cx| { @@ -1082,8 +1077,7 @@ mod tests { ) .await; - let (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, _, cx) = build_find_picker(project, cx); // Even though there is only one worktree, that worktree's filename // is included in the matching, because the worktree is a single file. @@ -1139,8 +1133,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); @@ -1198,8 +1191,8 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (picker, _workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; + let (picker, _workspace, cx) = build_find_picker(project, cx); + picker .update(cx, |f, cx| { f.delegate.spawn_search(test_path_like("dir"), cx) @@ -1231,8 +1224,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1395,8 +1387,7 @@ mod tests { .detach(); cx.background_executor.run_until_parked(); - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); @@ -1488,8 +1479,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; @@ -1547,8 +1537,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); @@ -1652,8 +1641,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; @@ -1709,9 +1697,7 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; - // generate some history to select from + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); // generate some history to select from open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; @@ -1807,10 +1793,10 @@ mod tests { ) -> ( View>, View, - VisualTestContext, + &mut VisualTestContext, ) { - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let picker = open_file_picker(&workspace, &mut cx); + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let picker = open_file_picker(&workspace, cx); (picker, workspace, cx) } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 7eb10c675f..50c409c0f2 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -140,7 +140,7 @@ impl TestAppContext { .any_handle } - pub fn add_window_view(&mut self, build_window: F) -> (View, VisualTestContext) + pub fn add_window_view(&mut self, build_window: F) -> (View, &mut VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, V: Render, @@ -149,7 +149,9 @@ impl TestAppContext { let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); drop(cx); let view = window.root_view(self).unwrap(); - (view, VisualTestContext::from_window(*window.deref(), self)) + let cx = Box::new(VisualTestContext::from_window(*window.deref(), self)); + // it might be nice to try and cleanup these at the end of each test. + (view, Box::leak(cx)) } pub fn simulate_new_path_selection( From c6b374ebc9ba9efe852bd2111da2cb0aaca9bbaf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Nov 2023 14:11:19 -0700 Subject: [PATCH 17/36] Remove initialize method from Element trait --- crates/editor2/src/element.rs | 14 ++-- crates/gpui2/src/element.rs | 2 +- crates/gpui2/src/elements/uniform_list.rs | 84 ++++++++++------------- crates/gpui2/src/view.rs | 4 +- crates/gpui2/src/window.rs | 20 +++--- 5 files changed, 53 insertions(+), 71 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 4d9a516f2b..de1b6f0622 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2398,21 +2398,14 @@ impl Element for EditorElement { Some(self.editor_id.into()) } - fn initialize( + fn layout( &mut self, editor: &mut Editor, element_state: Option, cx: &mut gpui::ViewContext, - ) -> Self::ElementState { + ) -> (gpui::LayoutId, Self::ElementState) { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. - } - fn layout( - &mut self, - editor: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut gpui::ViewContext, - ) -> gpui::LayoutId { let rem_size = cx.rem_size(); let mut style = Style::default(); style.size.width = relative(1.).into(); @@ -2421,7 +2414,8 @@ impl Element for EditorElement { EditorMode::AutoHeight { .. } => todo!(), EditorMode::Full => relative(1.).into(), }; - cx.request_layout(&style, None) + let layout_id = cx.request_layout(&style, None); + (layout_id, ()) } fn paint( diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 3ee829df52..221eb903fd 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -244,7 +244,7 @@ where fn draw( &mut self, - mut origin: Point, + origin: Point, available_space: Size, view_state: &mut V, cx: &mut ViewContext, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 9736139619..773f9ec8aa 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -108,62 +108,54 @@ impl Element for UniformList { Some(self.id.clone()) } - fn initialize( + fn layout( &mut self, view_state: &mut V, element_state: Option, cx: &mut ViewContext, - ) -> Self::ElementState { - if let Some(mut element_state) = element_state { - element_state.interactive = self - .interactivity - .initialize(Some(element_state.interactive), cx); - element_state - } else { - let item_size = self.measure_item(view_state, None, cx); - UniformListState { - interactive: self.interactivity.initialize(None, cx), - item_size, - } - } - } - - fn layout( - &mut self, - _view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState) { let max_items = self.item_count; - let item_size = element_state.item_size; let rem_size = cx.rem_size(); + let item_size = element_state + .as_ref() + .map(|s| s.item_size) + .unwrap_or_else(|| self.measure_item(view_state, None, cx)); - self.interactivity - .layout(&mut element_state.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; - let width = known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, + let (layout_id, interactive) = + self.interactivity + .layout(element_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; + let width = + known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), AvailableSpace::MinContent | AvailableSpace::MaxContent => { - item_size.width + desired_height } - }); - let height = match available_space.height { - AvailableSpace::Definite(x) => desired_height.min(x), - AvailableSpace::MinContent | AvailableSpace::MaxContent => { - desired_height - } - }; - size(width, height) - }, - ) - }) + }; + size(width, height) + }, + ) + }); + + let element_state = UniformListState { + interactive, + item_size, + }; + + (layout_id, element_state) } fn paint( diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 1ce1c4d349..c6ae9240ab 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -225,10 +225,10 @@ impl Element for AnyView { fn layout( &mut self, _view_state: &mut ParentViewState, - rendered_element: Option, + _element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState) { - (self.layout)(self, rendered_element, cx) + (self.layout)(self, cx) } fn paint( diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 9c19512871..2f223e8314 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1076,26 +1076,22 @@ impl<'a> WindowContext<'a> { self.with_z_index(0, |cx| { let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(available_space, cx); + root_view.draw(Point::zero(), available_space, cx); }); if let Some(active_drag) = self.app.active_drag.take() { self.with_z_index(1, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; - cx.with_element_offset(offset, |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(available_space, cx); - cx.active_drag = Some(active_drag); - }); + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_drag.view.draw(offset, available_space, cx); + cx.active_drag = Some(active_drag); }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { - cx.with_element_offset(active_tooltip.cursor_offset, |cx| { - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_tooltip.view.draw(available_space, cx); - }); + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_tooltip + .view + .draw(active_tooltip.cursor_offset, available_space, cx); }); } From 4f096333793ad83577769b15a9203560aac67cfd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 15 Nov 2023 14:17:49 -0700 Subject: [PATCH 18/36] Remove focus_in styling helper --- crates/gpui2/src/elements/div.rs | 14 -------------- crates/storybook2/src/stories/focus.rs | 1 - 2 files changed, 15 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 3a3ad5936e..e098e8ef1a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -437,14 +437,6 @@ pub trait FocusableComponent: InteractiveComponent { self } - fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.interactivity().focus_in_style = f(StyleRefinement::default()); - self - } - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -730,7 +722,6 @@ pub struct Interactivity { pub group: Option, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, pub in_focus_style: StyleRefinement, pub hover_style: StyleRefinement, pub group_hover_style: Option, @@ -1113,10 +1104,6 @@ where style.refine(&self.base_style); if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - if focus_handle.contains_focused(cx) { - style.refine(&self.focus_in_style); - } - if focus_handle.within_focused(cx) { style.refine(&self.in_focus_style); } @@ -1189,7 +1176,6 @@ impl Default for Interactivity { group: None, base_style: StyleRefinement::default(), focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), in_focus_style: StyleRefinement::default(), hover_style: StyleRefinement::default(), group_hover_style: None, diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index a8794afdb8..571882f1f2 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -57,7 +57,6 @@ impl Render for FocusStory { .size_full() .bg(color_1) .focus(|style| style.bg(color_2)) - .focus_in(|style| style.bg(color_3)) .child( div() .track_focus(&self.child_1_focus) From faf93aed4e8a464017fabd0633d9950e13d26a8c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 15 Nov 2023 14:17:04 -0800 Subject: [PATCH 19/36] checkpoint --- crates/gpui2/src/action.rs | 2 +- crates/settings2/src/keymap_file.rs | 6 ++--- crates/workspace2/src/pane.rs | 41 +++++++++++------------------ crates/workspace2/src/workspace2.rs | 10 +++---- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index dbb510b1c8..a81bcfcdbc 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -68,7 +68,7 @@ pub trait Action: std::fmt::Debug + 'static { // Types become actions by satisfying a list of trait bounds. impl Action for A where - A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, + A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static, { fn qualified_name() -> SharedString { let name = type_name::(); diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 2b57af0fdb..9f279864ee 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -9,7 +9,7 @@ use schemars::{ }; use serde::Deserialize; use serde_json::Value; -use util::asset_str; +use util::{asset_str, ResultExt}; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] @@ -86,9 +86,7 @@ impl KeymapFile { "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) - // todo!() - .ok() - // .log_err() + .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 668ce2f207..7f3658260c 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1398,6 +1398,7 @@ impl Pane { .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone()))) }) + .on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) // .on_drop(|_view, state: View, cx| { @@ -1430,32 +1431,22 @@ impl Pane { .items_center() .gap_1() .text_color(text_color) - .children(if item.has_conflict(cx) { - Some( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(TextColor::Warning), - ) - } else if item.is_dirty(cx) { - Some( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(TextColor::Info), - ) - } else { - None - }) - .children(if !close_right { - Some(close_icon()) - } else { - None - }) + .children( + item.has_conflict(cx) + .then(|| { + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(TextColor::Warning) + }) + .or(item.is_dirty(cx).then(|| { + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(TextColor::Info) + })), + ) + .children((!close_right).then(|| close_icon())) .child(label) - .children(if close_right { - Some(close_icon()) - } else { - None - }), + .children(close_right.then(|| close_icon())), ) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6c2d0c0ede..6a26fbdb5c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,11 +36,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, - AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, - EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render, - Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + actions, div, point, prelude::*, register_action, rems, size, Action, AnyModel, AnyView, + AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, + EntityId, EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, + Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; From 84bcbf11284dce95b14f23795d029f069153b940 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 15 Nov 2023 14:24:01 -0700 Subject: [PATCH 20/36] Add collab_ui2 --- Cargo.lock | 41 + crates/collab_ui2/Cargo.toml | 80 + crates/collab_ui2/src/channel_view.rs | 454 +++ crates/collab_ui2/src/chat_panel.rs | 983 +++++ .../src/chat_panel/message_editor.rs | 313 ++ crates/collab_ui2/src/collab_panel.rs | 3548 +++++++++++++++++ .../src/collab_panel/channel_modal.rs | 717 ++++ .../src/collab_panel/contact_finder.rs | 261 ++ crates/collab_ui2/src/collab_titlebar_item.rs | 1278 ++++++ crates/collab_ui2/src/collab_ui.rs | 165 + crates/collab_ui2/src/face_pile.rs | 113 + crates/collab_ui2/src/notification_panel.rs | 884 ++++ crates/collab_ui2/src/notifications.rs | 11 + .../incoming_call_notification.rs | 213 + .../project_shared_notification.rs | 217 + crates/collab_ui2/src/panel_settings.rs | 69 + crates/zed2/Cargo.toml | 2 +- 17 files changed, 9348 insertions(+), 1 deletion(-) create mode 100644 crates/collab_ui2/Cargo.toml create mode 100644 crates/collab_ui2/src/channel_view.rs create mode 100644 crates/collab_ui2/src/chat_panel.rs create mode 100644 crates/collab_ui2/src/chat_panel/message_editor.rs create mode 100644 crates/collab_ui2/src/collab_panel.rs create mode 100644 crates/collab_ui2/src/collab_panel/channel_modal.rs create mode 100644 crates/collab_ui2/src/collab_panel/contact_finder.rs create mode 100644 crates/collab_ui2/src/collab_titlebar_item.rs create mode 100644 crates/collab_ui2/src/collab_ui.rs create mode 100644 crates/collab_ui2/src/face_pile.rs create mode 100644 crates/collab_ui2/src/notification_panel.rs create mode 100644 crates/collab_ui2/src/notifications.rs create mode 100644 crates/collab_ui2/src/notifications/incoming_call_notification.rs create mode 100644 crates/collab_ui2/src/notifications/project_shared_notification.rs create mode 100644 crates/collab_ui2/src/panel_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 69f402f2e7..de80a9a7b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1829,6 +1829,46 @@ dependencies = [ "zed-actions", ] +[[package]] +name = "collab_ui2" +version = "0.1.0" +dependencies = [ + "anyhow", + "call2", + "channel2", + "client2", + "clock", + "collections", + "db2", + "editor2", + "feature_flags2", + "futures 0.3.28", + "fuzzy", + "gpui2", + "language2", + "lazy_static", + "log", + "menu2", + "notifications2", + "picker2", + "postage", + "pretty_assertions", + "project2", + "rich_text2", + "rpc2", + "schemars", + "serde", + "serde_derive", + "settings2", + "smallvec", + "theme2", + "time", + "tree-sitter-markdown", + "util", + "workspace2", + "zed_actions2", +] + [[package]] name = "collections" version = "0.1.0" @@ -11441,6 +11481,7 @@ dependencies = [ "chrono", "cli", "client2", + "collab_ui2", "collections", "command_palette2", "copilot2", diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml new file mode 100644 index 0000000000..8c48e09846 --- /dev/null +++ b/crates/collab_ui2/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "collab_ui2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/collab_ui.rs" +doctest = false + +[features] +test-support = [ + "call/test-support", + "client/test-support", + "collections/test-support", + "editor/test-support", + "gpui/test-support", + "project/test-support", + "settings/test-support", + "util/test-support", + "workspace/test-support", +] + +[dependencies] +# auto_update = { path = "../auto_update" } +db = { package = "db2", path = "../db2" } +call = { package = "call2", path = "../call2" } +client = { package = "client2", path = "../client2" } +channel = { package = "channel2", path = "../channel2" } +clock = { path = "../clock" } +collections = { path = "../collections" } +# context_menu = { path = "../context_menu" } +# drag_and_drop = { path = "../drag_and_drop" } +editor = { package="editor2", path = "../editor2" } +#feedback = { path = "../feedback" } +fuzzy = { path = "../fuzzy" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +notifications = { package = "notifications2", path = "../notifications2" } +rich_text = { package = "rich_text2", path = "../rich_text2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +# recent_projects = { path = "../recent_projects" } +rpc = { package ="rpc2", path = "../rpc2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2"} +theme = { package = "theme2", path = "../theme2" } +# theme_selector = { path = "../theme_selector" } +# vcs_menu = { path = "../vcs_menu" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } +zed-actions = { package="zed_actions2", path = "../zed_actions2"} + +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +log.workspace = true +schemars.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +time.workspace = true +smallvec.workspace = true + +[dev-dependencies] +call = { package = "call2", path = "../call2", features = ["test-support"] } +client = { package = "client2", path = "../client2", features = ["test-support"] } +collections = { path = "../collections", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } + +pretty_assertions.workspace = true +tree-sitter-markdown.workspace = true diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs new file mode 100644 index 0000000000..fe46f3bb3e --- /dev/null +++ b/crates/collab_ui2/src/channel_view.rs @@ -0,0 +1,454 @@ +use anyhow::{anyhow, Result}; +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, +}; +use collections::HashMap; +use editor::{CollaborationHub, Editor}; +use gpui::{ + actions, + elements::{ChildView, Label}, + geometry::vector::Vector2F, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, +}; +use project::Project; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use util::ResultExt; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, +}; + +actions!(channel_view, [Deploy]); + +pub fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: ViewHandle, + project: ModelHandle, + channel_store: ModelHandle, + channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + }); + anyhow::Ok(channel_view) + }) + } + + pub fn open_in_pane( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = ChannelStore::global(cx); + let language_registry = workspace.app_state().languages.clone(); + let markdown = language_registry.language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); + + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { + buffer.set_language(Some(markdown), cx); + } + }) + }); + + pane.update(&mut cx, |pane, cx| { + let buffer_id = channel_buffer.read(cx).remote_id(cx); + + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + + // If this channel buffer is already open in this pane, just return it. + if let Some(existing_view) = existing_view.clone() { + if existing_view.read(cx).channel_buffer == channel_buffer { + return existing_view; + } + } + + let view = cx.add_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }); + + // If the pane contained a disconnected view for this channel buffer, + // replace that. + if let Some(existing_item) = existing_view { + if let Some(ix) = pane.index_for_item(&existing_item) { + pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) + .detach(); + pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); + } + } + + view + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + + pub fn new( + project: ModelHandle, + channel_store: ModelHandle, + channel_buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); + editor + }); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + Self { + editor, + project, + channel_store, + channel_buffer, + remote_id: None, + _editor_event_subscription, + } + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + event: &ChannelBufferEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::Event::TitleChanged); + cx.notify() + }); + } + ChannelBufferEvent::BufferEdited => { + if cx.is_self_focused() || self.editor.is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + ChannelBufferEvent::CollaboratorsChanged => {} + } + } + + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + self.acknowledge_buffer_version(cx); + cx.focus(self.editor.as_any()) + } + } +} + +impl Item for ChannelView { + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a ViewHandle, + _: &'a AppContext, + ) -> Option<&'a AnyViewHandle> { + if type_id == TypeId::of::() { + Some(self_handle) + } else if type_id == TypeId::of::() { + Some(&self.editor) + } else { + None + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } + } else { + format!("channel notes (disconnected)") + }; + Label::new(label, style.label.to_owned()).into_any() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + )) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + editor::Editor::to_item_events(event) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } + + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel_buffer.channel_id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; + + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + }) + .ok_or_else(|| anyhow!("window was closed"))?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + false + } +} + +struct ChannelBufferCollaborationHub(ModelHandle); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } +} diff --git a/crates/collab_ui2/src/chat_panel.rs b/crates/collab_ui2/src/chat_panel.rs new file mode 100644 index 0000000000..5a4dafb6d4 --- /dev/null +++ b/crates/collab_ui2/src/chat_panel.rs @@ -0,0 +1,983 @@ +use crate::{ + channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings, +}; +use anyhow::Result; +use call::ActiveCall; +use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; +use client::Client; +use collections::HashMap; +use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use gpui::{ + actions, + elements::*, + platform::{CursorStyle, MouseButton}, + serde_json, + views::{ItemType, Select, SelectStyle}, + AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, + ViewContext, ViewHandle, WeakViewHandle, +}; +use language::LanguageRegistry; +use menu::Confirm; +use message_editor::MessageEditor; +use project::Fs; +use rich_text::RichText; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use std::sync::Arc; +use theme::{IconButton, Theme}; +use time::{OffsetDateTime, UtcOffset}; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; + +mod message_editor; + +const MESSAGE_LOADING_THRESHOLD: usize = 50; +const CHAT_PANEL_KEY: &'static str = "ChatPanel"; + +pub struct ChatPanel { + client: Arc, + channel_store: ModelHandle, + languages: Arc, + active_chat: Option<(ModelHandle, Subscription)>, + message_list: ListState, + input_editor: ViewHandle, + channel_select: ViewHandle, + ) -> AnyElement, - local_timezone: UtcOffset, - fs: Arc, - width: Option, - active: bool, - pending_serialization: Task>, - subscriptions: Vec, - workspace: WeakViewHandle, - is_scrolled_to_bottom: bool, - has_focus: bool, - markdown_data: HashMap, -} - -#[derive(Serialize, Deserialize)] -struct SerializedChatPanel { - width: Option, -} - -#[derive(Debug)] -pub enum Event { - DockPositionChanged, - Focus, - Dismissed, -} - -actions!( - chat_panel, - [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall] -); - -pub fn init(cx: &mut AppContext) { - cx.add_action(ChatPanel::send); - cx.add_action(ChatPanel::load_more_messages); - cx.add_action(ChatPanel::open_notes); - cx.add_action(ChatPanel::join_call); -} - -impl ChatPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { - let fs = workspace.app_state().fs.clone(); - let client = workspace.app_state().client.clone(); - let channel_store = ChannelStore::global(cx); - let languages = workspace.app_state().languages.clone(); - - let input_editor = cx.add_view(|cx| { - MessageEditor::new( - languages.clone(), - channel_store.clone(), - cx.add_view(|cx| { - Editor::auto_height( - 4, - Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())), - cx, - ) - }), - cx, - ) - }); - - let workspace_handle = workspace.weak_handle(); - - let channel_select = cx.add_view(|cx| { - let channel_store = channel_store.clone(); - let workspace = workspace_handle.clone(); - Select::new(0, cx, { - move |ix, item_type, is_hovered, cx| { - Self::render_channel_name( - &channel_store, - ix, - item_type, - is_hovered, - workspace, - cx, - ) - } - }) - .with_style(move |cx| { - let style = &theme::current(cx).chat_panel.channel_select; - SelectStyle { - header: Default::default(), - menu: style.menu, - } - }) - }); - - let mut message_list = - ListState::::new(0, Orientation::Bottom, 10., move |this, ix, cx| { - this.render_message(ix, cx) - }); - message_list.set_scroll_handler(|visible_range, count, this, cx| { - if visible_range.start < MESSAGE_LOADING_THRESHOLD { - this.load_more_messages(&LoadMoreMessages, cx); - } - this.is_scrolled_to_bottom = visible_range.end == count; - }); - - cx.add_view(|cx| { - let mut this = Self { - fs, - client, - channel_store, - languages, - active_chat: Default::default(), - pending_serialization: Task::ready(None), - message_list, - input_editor, - channel_select, - local_timezone: cx.platform().local_timezone(), - has_focus: false, - subscriptions: Vec::new(), - workspace: workspace_handle, - is_scrolled_to_bottom: true, - active: false, - width: None, - markdown_data: Default::default(), - }; - - let mut old_dock_position = this.position(cx); - this.subscriptions - .push( - cx.observe_global::(move |this: &mut Self, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); - } - cx.notify(); - }), - ); - - this.update_channel_count(cx); - cx.observe(&this.channel_store, |this, _, cx| { - this.update_channel_count(cx) - }) - .detach(); - - cx.observe(&this.channel_select, |this, channel_select, cx| { - let selected_ix = channel_select.read(cx).selected_index(); - - let selected_channel_id = this - .channel_store - .read(cx) - .channel_at(selected_ix) - .map(|e| e.id); - if let Some(selected_channel_id) = selected_channel_id { - this.select_channel(selected_channel_id, None, cx) - .detach_and_log_err(cx); - } - }) - .detach(); - - this - }) - } - - pub fn is_scrolled_to_bottom(&self) -> bool { - self.is_scrolled_to_bottom - } - - pub fn active_chat(&self) -> Option> { - self.active_chat.as_ref().map(|(chat, _)| chat.clone()) - } - - pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let serialized_panel = if let Some(panel) = cx - .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) }) - .await - .log_err() - .flatten() - { - Some(serde_json::from_str::(&panel)?) - } else { - None - }; - - workspace.update(&mut cx, |workspace, cx| { - let panel = Self::new(workspace, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; - cx.notify(); - }); - } - panel - }) - }) - } - - fn serialize(&mut self, cx: &mut ViewContext) { - let width = self.width; - self.pending_serialization = cx.background().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - CHAT_PANEL_KEY.into(), - serde_json::to_string(&SerializedChatPanel { width })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } - - fn update_channel_count(&mut self, cx: &mut ViewContext) { - let channel_count = self.channel_store.read(cx).channel_count(); - self.channel_select.update(cx, |select, cx| { - select.set_item_count(channel_count, cx); - }); - } - - fn set_active_chat(&mut self, chat: ModelHandle, cx: &mut ViewContext) { - if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) { - let channel_id = chat.read(cx).channel_id; - { - self.markdown_data.clear(); - let chat = chat.read(cx); - self.message_list.reset(chat.message_count()); - - let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); - self.input_editor.update(cx, |editor, cx| { - editor.set_channel(channel_id, channel_name, cx); - }); - }; - let subscription = cx.subscribe(&chat, Self::channel_did_change); - self.active_chat = Some((chat, subscription)); - self.acknowledge_last_message(cx); - self.channel_select.update(cx, |select, cx| { - if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) { - select.set_selected_index(ix, cx); - } - }); - cx.notify(); - } - } - - fn channel_did_change( - &mut self, - _: ModelHandle, - event: &ChannelChatEvent, - cx: &mut ViewContext, - ) { - match event { - ChannelChatEvent::MessagesUpdated { - old_range, - new_count, - } => { - self.message_list.splice(old_range.clone(), *new_count); - if self.active { - self.acknowledge_last_message(cx); - } - } - ChannelChatEvent::NewMessage { - channel_id, - message_id, - } => { - if !self.active { - self.channel_store.update(cx, |store, cx| { - store.new_message(*channel_id, *message_id, cx) - }) - } - } - } - cx.notify(); - } - - fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) { - if self.active && self.is_scrolled_to_bottom { - if let Some((chat, _)) = &self.active_chat { - chat.update(cx, |chat, cx| { - chat.acknowledge_last_message(cx); - }); - } - } - } - - fn render_channel(&self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - Flex::column() - .with_child( - ChildView::new(&self.channel_select, cx) - .contained() - .with_style(theme.chat_panel.channel_select.container), - ) - .with_child(self.render_active_channel_messages(&theme)) - .with_child(self.render_input_box(&theme, cx)) - .into_any() - } - - fn render_active_channel_messages(&self, theme: &Arc) -> AnyElement { - let messages = if self.active_chat.is_some() { - List::new(self.message_list.clone()) - .contained() - .with_style(theme.chat_panel.list) - .into_any() - } else { - Empty::new().into_any() - }; - - messages.flex(1., true).into_any() - } - - fn render_message(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let (message, is_continuation, is_last, is_admin) = self - .active_chat - .as_ref() - .unwrap() - .0 - .update(cx, |active_chat, cx| { - let is_admin = self - .channel_store - .read(cx) - .is_channel_admin(active_chat.channel_id); - - let last_message = active_chat.message(ix.saturating_sub(1)); - let this_message = active_chat.message(ix).clone(); - let is_continuation = last_message.id != this_message.id - && this_message.sender.id == last_message.sender.id; - - if let ChannelMessageId::Saved(id) = this_message.id { - if this_message - .mentions - .iter() - .any(|(_, user_id)| Some(*user_id) == self.client.user_id()) - { - active_chat.acknowledge_message(id); - } - } - - ( - this_message, - is_continuation, - active_chat.message_count() == ix + 1, - is_admin, - ) - }); - - let is_pending = message.is_pending(); - let theme = theme::current(cx); - let text = self.markdown_data.entry(message.id).or_insert_with(|| { - Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message) - }); - - let now = OffsetDateTime::now_utc(); - - let style = if is_pending { - &theme.chat_panel.pending_message - } else if is_continuation { - &theme.chat_panel.continuation_message - } else { - &theme.chat_panel.message - }; - - let belongs_to_user = Some(message.sender.id) == self.client.user_id(); - let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) = - (message.id, belongs_to_user || is_admin) - { - Some(id) - } else { - None - }; - - enum MessageBackgroundHighlight {} - MouseEventHandler::new::(ix, cx, |state, cx| { - let container = style.style_for(state); - if is_continuation { - Flex::row() - .with_child( - text.element( - theme.editor.syntax.clone(), - theme.chat_panel.rich_text.clone(), - cx, - ) - .flex(1., true), - ) - .with_child(render_remove(message_id_to_remove, cx, &theme)) - .contained() - .with_style(*container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } else { - Flex::column() - .with_child( - Flex::row() - .with_child( - Flex::row() - .with_child(render_avatar( - message.sender.avatar.clone(), - &theme.chat_panel.avatar, - theme.chat_panel.avatar_container, - )) - .with_child( - Label::new( - message.sender.github_login.clone(), - theme.chat_panel.message_sender.text.clone(), - ) - .contained() - .with_style(theme.chat_panel.message_sender.container), - ) - .with_child( - Label::new( - format_timestamp( - message.timestamp, - now, - self.local_timezone, - ), - theme.chat_panel.message_timestamp.text.clone(), - ) - .contained() - .with_style(theme.chat_panel.message_timestamp.container), - ) - .align_children_center() - .flex(1., true), - ) - .with_child(render_remove(message_id_to_remove, cx, &theme)) - .align_children_center(), - ) - .with_child( - Flex::row() - .with_child( - text.element( - theme.editor.syntax.clone(), - theme.chat_panel.rich_text.clone(), - cx, - ) - .flex(1., true), - ) - // Add a spacer to make everything line up - .with_child(render_remove(None, cx, &theme)), - ) - .contained() - .with_style(*container) - .with_margin_bottom(if is_last { - theme.chat_panel.last_message_bottom_spacing - } else { - 0. - }) - .into_any() - } - }) - .into_any() - } - - fn render_markdown_with_mentions( - language_registry: &Arc, - current_user_id: u64, - message: &channel::ChannelMessage, - ) -> RichText { - let mentions = message - .mentions - .iter() - .map(|(range, user_id)| rich_text::Mention { - range: range.clone(), - is_self_mention: *user_id == current_user_id, - }) - .collect::>(); - - rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) - } - - fn render_input_box(&self, theme: &Arc, cx: &AppContext) -> AnyElement { - ChildView::new(&self.input_editor, cx) - .contained() - .with_style(theme.chat_panel.input_editor.container) - .into_any() - } - - fn render_channel_name( - channel_store: &ModelHandle, - ix: usize, - item_type: ItemType, - is_hovered: bool, - workspace: WeakViewHandle, - cx: &mut ViewContext { - let theme = theme::current(cx); - let tooltip_style = &theme.tooltip; - let theme = &theme.chat_panel; - let style = match (&item_type, is_hovered) { - (ItemType::Header, _) => &theme.channel_select.header, - (ItemType::Selected, _) => &theme.channel_select.active_item, - (ItemType::Unselected, false) => &theme.channel_select.item, - (ItemType::Unselected, true) => &theme.channel_select.hovered_item, - }; - - let channel = &channel_store.read(cx).channel_at(ix).unwrap(); - let channel_id = channel.id; - - let mut row = Flex::row() - .with_child( - Label::new("#".to_string(), style.hash.text.clone()) - .contained() - .with_style(style.hash.container), - ) - .with_child(Label::new(channel.name.clone(), style.name.clone())); - - if matches!(item_type, ItemType::Header) { - row.add_children([ - MouseEventHandler::new::(0, cx, |mouse_state, _| { - render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg") - }) - .on_click(MouseButton::Left, move |_, _, cx| { - if let Some(workspace) = workspace.upgrade(cx) { - ChannelView::open(channel_id, workspace, cx).detach(); - } - }) - .with_tooltip::( - channel_id as usize, - "Open Notes", - Some(Box::new(OpenChannelNotes)), - tooltip_style.clone(), - cx, - ) - .flex_float(), - MouseEventHandler::new::(0, cx, |mouse_state, _| { - render_icon_button( - theme.icon_button.style_for(mouse_state), - "icons/speaker-loud.svg", - ) - }) - .on_click(MouseButton::Left, move |_, _, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel_id, cx)) - .detach_and_log_err(cx); - }) - .with_tooltip::( - channel_id as usize, - "Join Call", - Some(Box::new(JoinCall)), - tooltip_style.clone(), - cx, - ) - .flex_float(), - ]); - } - - row.align_children_center() - .contained() - .with_style(style.container) - .into_any() - } - - fn render_sign_in_prompt( - &self, - theme: &Arc, - cx: &mut ViewContext, - ) -> AnyElement { - enum SignInPromptLabel {} - - MouseEventHandler::new::(0, cx, |mouse_state, _| { - Label::new( - "Sign in to use chat".to_string(), - theme - .chat_panel - .sign_in_prompt - .style_for(mouse_state) - .clone(), - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - let client = this.client.clone(); - cx.spawn(|this, mut cx| async move { - if client - .authenticate_and_connect(true, &cx) - .log_err() - .await - .is_some() - { - this.update(&mut cx, |this, cx| { - if cx.handle().is_focused(cx) { - cx.focus(&this.input_editor); - } - }) - .ok(); - } - }) - .detach(); - }) - .aligned() - .into_any() - } - - fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - let message = self - .input_editor - .update(cx, |editor, cx| editor.take_message(cx)); - - if let Some(task) = chat - .update(cx, |chat, cx| chat.send_message(message, cx)) - .log_err() - { - task.detach(); - } - } - } - - fn remove_message(&mut self, id: u64, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach()) - } - } - - fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext) { - if let Some((chat, _)) = self.active_chat.as_ref() { - chat.update(cx, |channel, cx| { - if let Some(task) = channel.load_more_messages(cx) { - task.detach(); - } - }) - } - } - - pub fn select_channel( - &mut self, - selected_channel_id: u64, - scroll_to_message_id: Option, - cx: &mut ViewContext, - ) -> Task> { - let open_chat = self - .active_chat - .as_ref() - .and_then(|(chat, _)| { - (chat.read(cx).channel_id == selected_channel_id) - .then(|| Task::ready(anyhow::Ok(chat.clone()))) - }) - .unwrap_or_else(|| { - self.channel_store.update(cx, |store, cx| { - store.open_channel_chat(selected_channel_id, cx) - }) - }); - - cx.spawn(|this, mut cx| async move { - let chat = open_chat.await?; - this.update(&mut cx, |this, cx| { - this.set_active_chat(chat.clone(), cx); - })?; - - if let Some(message_id) = scroll_to_message_id { - if let Some(item_ix) = - ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone()) - .await - { - this.update(&mut cx, |this, cx| { - if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) { - this.message_list.scroll_to(ListOffset { - item_ix, - offset_in_item: 0., - }); - cx.notify(); - } - })?; - } - } - - Ok(()) - }) - } - - fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - if let Some(workspace) = self.workspace.upgrade(cx) { - ChannelView::open(channel_id, workspace, cx).detach(); - } - } - } - - fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext) { - if let Some((chat, _)) = &self.active_chat { - let channel_id = chat.read(cx).channel_id; - ActiveCall::global(cx) - .update(cx, |call, cx| call.join_channel(channel_id, cx)) - .detach_and_log_err(cx); - } - } -} - -fn render_remove( - message_id_to_remove: Option, - cx: &mut ViewContext<'_, '_, ChatPanel>, - theme: &Arc, -) -> AnyElement { - enum DeleteMessage {} - - message_id_to_remove - .map(|id| { - MouseEventHandler::new::(id as usize, cx, |mouse_state, _| { - let button_style = theme.chat_panel.icon_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x.svg") - .aligned() - .into_any() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_message(id, cx); - }) - .flex_float() - .into_any() - }) - .unwrap_or_else(|| { - let style = theme.chat_panel.icon_button.default; - - Empty::new() - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_uniform_padding(2.) - .flex_float() - .into_any() - }) -} - -impl Entity for ChatPanel { - type Event = Event; -} - -impl View for ChatPanel { - fn ui_name() -> &'static str { - "ChatPanel" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - let element = if self.client.user_id().is_some() { - self.render_channel(cx) - } else { - self.render_sign_in_prompt(&theme, cx) - }; - element - .contained() - .with_style(theme.chat_panel.container) - .constrained() - .with_min_width(150.) - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - if matches!( - *self.client.status().borrow(), - client::Status::Connected { .. } - ) { - let editor = self.input_editor.read(cx).editor.clone(); - cx.focus(&editor); - } - } - - fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } -} - -impl Panel for ChatPanel { - fn position(&self, cx: &gpui::WindowContext) -> DockPosition { - settings::get::(cx).dock - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings| { - settings.dock = Some(position) - }); - } - - fn size(&self, cx: &gpui::WindowContext) -> f32 { - self.width - .unwrap_or_else(|| settings::get::(cx).default_width) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - self.active = active; - if active { - self.acknowledge_last_message(cx); - if !is_channels_feature_enabled(cx) { - cx.emit(Event::Dismissed); - } - } - } - - fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { - (settings::get::(cx).button && is_channels_feature_enabled(cx)) - .then(|| "icons/conversations.svg") - } - - fn icon_tooltip(&self) -> (String, Option>) { - ("Chat Panel".to_string(), Some(Box::new(ToggleFocus))) - } - - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, Event::DockPositionChanged) - } - - fn should_close_on_event(event: &Self::Event) -> bool { - matches!(event, Event::Dismissed) - } - - fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { - self.has_focus - } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, Event::Focus) - } -} - -fn format_timestamp( - mut timestamp: OffsetDateTime, - mut now: OffsetDateTime, - local_timezone: UtcOffset, -) -> String { - timestamp = timestamp.to_offset(local_timezone); - now = now.to_offset(local_timezone); - - let today = now.date(); - let date = timestamp.date(); - let mut hour = timestamp.hour(); - let mut part = "am"; - if hour > 12 { - hour -= 12; - part = "pm"; - } - if date == today { - format!("{:02}:{:02}{}", hour, timestamp.minute(), part) - } else if date.next_day() == Some(today) { - format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part) - } else { - format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year()) - } -} - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - .contained() - .with_style(style.container) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::fonts::HighlightStyle; - use pretty_assertions::assert_eq; - use rich_text::{BackgroundKind, Highlight, RenderedRegion}; - use util::test::marked_text_ranges; - - #[gpui::test] - fn test_render_markdown_with_mentions() { - let language_registry = Arc::new(LanguageRegistry::test()); - let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false); - let message = channel::ChannelMessage { - id: ChannelMessageId::Saved(0), - body, - timestamp: OffsetDateTime::now_utc(), - sender: Arc::new(client::User { - github_login: "fgh".into(), - avatar: None, - id: 103, - }), - nonce: 5, - mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)], - }; - - let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message); - - // Note that the "'" was replaced with ’ due to smart punctuation. - let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false); - assert_eq!(message.text, body); - assert_eq!( - message.highlights, - vec![ - ( - ranges[0].clone(), - HighlightStyle { - italic: Some(true), - ..Default::default() - } - .into() - ), - (ranges[1].clone(), Highlight::Mention), - ( - ranges[2].clone(), - HighlightStyle { - weight: Some(gpui::fonts::Weight::BOLD), - ..Default::default() - } - .into() - ), - (ranges[3].clone(), Highlight::SelfMention) - ] - ); - assert_eq!( - message.regions, - vec![ - RenderedRegion { - background_kind: Some(BackgroundKind::Mention), - link_url: None - }, - RenderedRegion { - background_kind: Some(BackgroundKind::SelfMention), - link_url: None - }, - ] - ); - } -} +// use crate::{ +// channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings, +// }; +// use anyhow::Result; +// use call::ActiveCall; +// use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore}; +// use client::Client; +// use collections::HashMap; +// use db::kvp::KEY_VALUE_STORE; +// use editor::Editor; +// use gpui::{ +// actions, +// elements::*, +// platform::{CursorStyle, MouseButton}, +// serde_json, +// views::{ItemType, Select, SelectStyle}, +// AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, +// ViewContext, ViewHandle, WeakViewHandle, +// }; +// use language::LanguageRegistry; +// use menu::Confirm; +// use message_editor::MessageEditor; +// use project::Fs; +// use rich_text::RichText; +// use serde::{Deserialize, Serialize}; +// use settings::SettingsStore; +// use std::sync::Arc; +// use theme::{IconButton, Theme}; +// use time::{OffsetDateTime, UtcOffset}; +// use util::{ResultExt, TryFutureExt}; +// use workspace::{ +// dock::{DockPosition, Panel}, +// Workspace, +// }; + +// mod message_editor; + +// const MESSAGE_LOADING_THRESHOLD: usize = 50; +// const CHAT_PANEL_KEY: &'static str = "ChatPanel"; + +// pub struct ChatPanel { +// client: Arc, +// channel_store: ModelHandle, +// languages: Arc, +// active_chat: Option<(ModelHandle, Subscription)>, +// message_list: ListState, +// input_editor: ViewHandle, +// channel_select: ViewHandle, +// ) -> AnyElement