diff --git a/Cargo.lock b/Cargo.lock index b52108977aeeaa6c6611eb52f45aca44c6812b84..209560dcd3e39405b7ae1d1413a2ef904f4759c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11316,7 +11316,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.113.0" +version = "0.114.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 435a6446693e590f2af9c678df445c0f53b18428..8138f025d3da4af5cfb285c48be0ea8b88879f8f 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -354,129 +354,116 @@ impl std::fmt::Debug for Command { } } -// #[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}; - -// #[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); - -// 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 -// }); - -// workspace.update(cx, |workspace, cx| { -// cx.focus(&editor); -// workspace.add_item(Box::new(editor.clone()), cx) -// }); - -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); - -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); - -// 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; - -// 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, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; - -// 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"); -// }); - -// // Add namespace filter, and redeploy the palette -// cx.update(|cx| { -// cx.update_default_global::(|filter, _| { -// filter.filtered_namespaces.insert("editor"); -// }) -// }); - -// workspace.update(cx, |workspace, cx| { -// toggle_command_palette(workspace, &Toggle, cx); -// }); - -// // Assert editor command not present -// let palette = workspace.read_with(cx, |workspace, _| { -// workspace.modal::().unwrap() -// }); - -// palette -// .update(cx, |palette, cx| { -// palette -// .delegate_mut() -// .update_matches("bcksp".to_string(), cx) -// }) -// .await; - -// 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 -// }) -// } -// } +#[cfg(test)] +mod tests { + use std::sync::Arc; + + 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" + ); + } + + #[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 (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); + editor.set_text("abc", cx); + editor + }); + + workspace.update(cx, |workspace, cx| { + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| editor.focus(cx)) + }); + + cx.simulate_keystrokes("cmd-shift-p"); + + let palette = workspace.update(cx, |workspace, cx| { + workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + + 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)); + }); + + cx.simulate_input("bcksp"); + + palette.update(cx, |palette, _| { + assert_eq!(palette.delegate.matches[0].string, "editor: backspace"); + }); + + cx.simulate_keystrokes("enter"); + + workspace.update(cx, |workspace, cx| { + assert!(workspace.active_modal::(cx).is_none()); + assert_eq!(editor.read(cx).text(cx), "ab") + }); + + // Add namespace filter, and redeploy the palette + cx.update(|cx| { + cx.set_global(CommandPaletteFilter::default()); + cx.update_global::(|filter, _| { + filter.filtered_namespaces.insert("editor"); + }) + }); + + cx.simulate_keystrokes("cmd-shift-p"); + cx.simulate_input("bcksp"); + + let palette = workspace.update(cx, |workspace, cx| { + workspace + .active_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); + settings::load_default_keymap(cx); + app_state + }) + } +} diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index f808ffa7026f496faa0429b6fa7f8af1468e2bcc..e64d5e301caa0912d6309280e083ed3006225f19 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, ); } @@ -578,12 +578,7 @@ impl DisplaySnapshot { line.push_str(chunk.chunk); let text_style = if let Some(style) = chunk.style { - editor_style - .text - .clone() - .highlight(style) - .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text)) + Cow::Owned(editor_style.text.clone().highlight(style)) } else { Cow::Borrowed(&editor_style.text) }; @@ -728,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/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 2f65903f080a27d936397be1815abd9d1e133da0..05106dd2a1f1416529689750f77b2e264f4d5e83 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -2,7 +2,7 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; +use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, Pixels, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; @@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b> { pub em_width: Pixels, pub line_height: Pixels, pub block_id: usize, + pub editor_style: &'b EditorStyle, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/editor2/src/display_map/fold_map.rs b/crates/editor2/src/display_map/fold_map.rs index 88cd202b0801a4ca875122cece06c1c7ba0ab5a0..4dad2d52aeb5236ec2936b8bdcaf3b06f760cc84 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 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 Fold { +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 e37677b6fdb5a15ca255f4ae61affbd4fd1c3fad..65adada01e29f85ec40de5c0160703e0d807aedc 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -100,7 +100,9 @@ use theme::{ use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ - item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, + item::{ItemEvent, ItemHandle}, + searchable::SearchEvent, + ItemNavHistory, SplitDirection, ViewId, Workspace, }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -1878,10 +1880,8 @@ impl Editor { ); let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, Self::handle_focus_out) - .detach(); + cx.on_focus(&focus_handle, Self::handle_focus).detach(); + cx.on_blur(&focus_handle, Self::handle_blur).detach(); let mut this = Self { handle: cx.view().downgrade(), @@ -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 @@ -5330,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); @@ -5421,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); @@ -7690,183 +7663,203 @@ impl Editor { } } - // pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { - // use language::ToOffset as _; + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; - // let project = self.project.clone()?; - // let selection = self.selections.newest_anchor().clone(); - // let (cursor_buffer, cursor_buffer_position) = self - // .buffer - // .read(cx) - // .text_anchor_for_position(selection.head(), cx)?; - // let (tail_buffer, _) = self - // .buffer - // .read(cx) - // .text_anchor_for_position(selection.tail(), cx)?; - // if tail_buffer != cursor_buffer { - // return None; - // } + let project = self.project.clone()?; + let selection = self.selections.newest_anchor().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, _) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } - // let snapshot = cursor_buffer.read(cx).snapshot(); - // let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - // let prepare_rename = project.update(cx, |project, cx| { - // project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) - // }); + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); - // Some(cx.spawn(|this, mut cx| async move { - // let rename_range = if let Some(range) = prepare_rename.await? { - // Some(range) - // } else { - // this.update(&mut cx, |this, cx| { - // let buffer = this.buffer.read(cx).snapshot(cx); - // let mut buffer_highlights = this - // .document_highlights_for_position(selection.head(), &buffer) - // .filter(|highlight| { - // highlight.start.excerpt_id == selection.head().excerpt_id - // && highlight.end.excerpt_id == selection.head().excerpt_id - // }); - // buffer_highlights - // .next() - // .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) - // })? - // }; - // if let Some(rename_range) = rename_range { - // let rename_buffer_range = rename_range.to_offset(&snapshot); - // let cursor_offset_in_rename_range = - // cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - - // this.update(&mut cx, |this, cx| { - // this.take_rename(false, cx); - // let buffer = this.buffer.read(cx).read(cx); - // let cursor_offset = selection.head().to_offset(&buffer); - // let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - // let rename_end = rename_start + rename_buffer_range.len(); - // let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - // let mut old_highlight_id = None; - // let old_name: Arc = buffer - // .chunks(rename_start..rename_end, true) - // .map(|chunk| { - // if old_highlight_id.is_none() { - // old_highlight_id = chunk.syntax_highlight_id; - // } - // chunk.text - // }) - // .collect::() - // .into(); - - // drop(buffer); - - // // Position the selection in the rename editor so that it matches the current selection. - // this.show_local_selections = false; - // let rename_editor = cx.build_view(|cx| { - // let mut editor = Editor::single_line(cx); - // if let Some(old_highlight_id) = old_highlight_id { - // editor.override_text_style = - // Some(Box::new(move |style| old_highlight_id.style(&style.syntax))); - // } - // editor.buffer.update(cx, |buffer, cx| { - // buffer.edit([(0..0, old_name.clone())], None, cx) - // }); - // editor.select_all(&SelectAll, cx); - // editor - // }); + Some(cx.spawn(|this, mut cx| async move { + let rename_range = if let Some(range) = prepare_rename.await? { + Some(range) + } else { + this.update(&mut cx, |this, cx| { + let buffer = this.buffer.read(cx).snapshot(cx); + let mut buffer_highlights = this + .document_highlights_for_position(selection.head(), &buffer) + .filter(|highlight| { + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id + }); + buffer_highlights + .next() + .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) + })? + }; + if let Some(rename_range) = rename_range { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - // let ranges = this - // .clear_background_highlights::(cx) - // .into_iter() - // .flat_map(|(_, ranges)| ranges.into_iter()) - // .chain( - // this.clear_background_highlights::(cx) - // .into_iter() - // .flat_map(|(_, ranges)| ranges.into_iter()), - // ) - // .collect(); - - // this.highlight_text::( - // ranges, - // HighlightStyle { - // fade_out: Some(style.rename_fade), - // ..Default::default() - // }, - // cx, - // ); - // cx.focus(&rename_editor); - // let block_id = this.insert_blocks( - // [BlockProperties { - // style: BlockStyle::Flex, - // position: range.start.clone(), - // height: 1, - // render: Arc::new({ - // let editor = rename_editor.clone(); - // move |cx: &mut BlockContext| { - // ChildView::new(&editor, cx) - // .contained() - // .with_padding_left(cx.anchor_x) - // .into_any() - // } - // }), - // disposition: BlockDisposition::Below, - // }], - // Some(Autoscroll::fit()), - // cx, - // )[0]; - // this.pending_rename = Some(RenameState { - // range, - // old_name, - // editor: rename_editor, - // block_id, - // }); - // })?; - // } + this.update(&mut cx, |this, cx| { + this.take_rename(false, cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let mut old_highlight_id = None; + let old_name: Arc = buffer + .chunks(rename_start..rename_end, true) + .map(|chunk| { + if old_highlight_id.is_none() { + old_highlight_id = chunk.syntax_highlight_id; + } + chunk.text + }) + .collect::() + .into(); - // Ok(()) - // })) - // } + drop(buffer); - // pub fn confirm_rename( - // workspace: &mut Workspace, - // _: &ConfirmRename, - // cx: &mut ViewContext, - // ) -> Option>> { - // let editor = workspace.active_item(cx)?.act_as::(cx)?; - - // let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { - // let rename = editor.take_rename(false, cx)?; - // let buffer = editor.buffer.read(cx); - // let (start_buffer, start) = - // buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; - // let (end_buffer, end) = - // buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; - // if start_buffer == end_buffer { - // let new_name = rename.editor.read(cx).text(cx); - // Some((start_buffer, start..end, rename.old_name, new_name)) - // } else { - // None - // } - // })?; + // Position the selection in the rename editor so that it matches the current selection. + this.show_local_selections = false; + let rename_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); + editor.select_all(&SelectAll, cx); + editor + }); - // let rename = workspace.project().clone().update(cx, |project, cx| { - // project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) - // }); + let ranges = this + .clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()) + .chain( + this.clear_background_highlights::(cx) + .into_iter() + .flat_map(|(_, ranges)| ranges.into_iter()), + ) + .collect(); - // let editor = editor.downgrade(); - // Some(cx.spawn(|workspace, mut cx| async move { - // let project_transaction = rename.await?; - // Self::open_project_transaction( - // &editor, - // workspace, - // project_transaction, - // format!("Rename: {} → {}", old_name, new_name), - // cx.clone(), - // ) - // .await?; + this.highlight_text::( + ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + let rename_focus_handle = rename_editor.focus_handle(cx); + cx.focus(&rename_focus_handle); + let block_id = this.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: range.start.clone(), + height: 1, + render: Arc::new({ + let rename_editor = rename_editor.clone(); + move |cx: &mut BlockContext| { + let mut text_style = cx.editor_style.text.clone(); + if let Some(highlight_style) = old_highlight_id + .and_then(|h| h.style(&cx.editor_style.syntax)) + { + text_style = text_style.highlight(highlight_style); + } + div() + .pl(cx.anchor_x) + .child(rename_editor.render_with(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(), + }, + ))) + .render() + } + }), + disposition: BlockDisposition::Below, + }], + Some(Autoscroll::fit()), + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + })?; + } - // editor.update(&mut cx, |editor, cx| { - // editor.refresh_document_highlights(cx); - // })?; - // Ok(()) - // })) - // } + Ok(()) + })) + } + + pub fn confirm_rename( + &mut self, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let rename = self.take_rename(false, cx)?; + let workspace = self.workspace()?; + let (start_buffer, start) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer != end_buffer { + return None; + } + + let buffer = start_buffer; + let range = start..end; + let old_name = rename.old_name; + let new_name = rename.editor.read(cx).text(cx); + + let rename = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx) + }); + let workspace = workspace.downgrade(); + + Some(cx.spawn(|editor, mut cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx.clone(), + ) + .await?; + + editor.update(&mut cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) + })) + } fn take_rename( &mut self, @@ -7874,6 +7867,10 @@ impl Editor { cx: &mut ViewContext, ) -> Option { let rename = self.pending_rename.take()?; + if rename.editor.focus_handle(cx).is_focused(cx) { + cx.focus(&self.focus_handle); + } + self.remove_blocks( [rename.block_id].into_iter().collect(), Some(Autoscroll::fit()), @@ -9172,17 +9169,13 @@ impl Editor { self.focus_handle.is_focused(cx) } - fn handle_focus_in(&mut self, cx: &mut ViewContext) { - if self.focus_handle.is_focused(cx) { - // todo!() - // let focused_event = EditorFocused(cx.handle()); - // cx.emit_global(focused_event); - cx.emit(EditorEvent::Focused); - } + fn handle_focus(&mut self, cx: &mut ViewContext) { + cx.emit(EditorEvent::Focused); + if let Some(rename) = self.pending_rename.as_ref() { let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); cx.focus(&rename_editor_focus_handle); - } else if self.focus_handle.is_focused(cx) { + } else { self.blink_manager.update(cx, BlinkManager::enable); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); @@ -9198,7 +9191,7 @@ impl Editor { } } - fn handle_focus_out(&mut self, cx: &mut ViewContext) { + fn handle_blur(&mut self, cx: &mut ViewContext) { // todo!() // let blurred_event = EditorBlurred(cx.handle()); // cx.emit_global(blurred_event); diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 8504d152eb978d89d8377760b1ad1634a29de51a..84f9129a74f1c455a898de36f5de110e00093670 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/editor2/src/element.rs b/crates/editor2/src/element.rs index c9f16277984404b49b95a719057de4b64af2b71d..0e4fb35362d1a16f52dde276931895824c03341a 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, InteractiveComponent, 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; @@ -487,23 +488,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., - // ); - - // let indicator_origin = bounds.origin + position + centering_offset; + 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); - // 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( @@ -612,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 - // }, - // }); - - // 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, - // ); - - // 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 display_row = range.start.row(); - - // let buffer_row = DisplayPoint::new(display_row, 0) - // .to_point(&layout.position_map.snapshot.display_snapshot) - // .row; - - // 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), - // ) - // } - // } - - 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, - ); - } + 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 + // }, + // }); + + 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(); + + 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 fold_bounds = Bounds { + origin: point(start_x, start_y), + size: size(end_x - start_x, layout.position_map.line_height), + }; - 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(); + 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() + } + }, + ) + }); + + self.paint_highlighted_range( + display_range.clone(), + fold_background, + fold_corner_radius, + fold_corner_radius * 2., + layout, + content_origin, + text_bounds, + cx, + ); + } + }); - 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(); + + 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, + ); - 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 - 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., - )); + 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, + }); } - 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); + + 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; + + // 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); + } - context_menu.draw(list_origin, available_space, editor, cx); - }) - } + if list_origin.y + list_height > text_bounds.lower_right().y { + list_origin.y -= layout.position_map.line_height - list_height; + } - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); - - // // 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; - - // // 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; - - // // 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 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); - // } - - // 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 - 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); - - // 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(); - // } - }) + context_menu.draw(list_origin, available_space, editor, cx); + }) + } + + // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { + // cx.scene().push_stacking_context(None, None); + + // // 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; + + // // 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; + + // // 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 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); + // } + + // 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 - 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); + + // 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 { @@ -1130,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, ) { @@ -1150,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| { @@ -1160,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 }, } }) @@ -1564,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(); @@ -1574,19 +1605,6 @@ impl EditorElement { cx.theme().colors(), ); - fold_ranges.extend( - snapshot - .folds_in_range(start_anchor..end_anchor) - .map(|anchor| { - let start = anchor.start.to_point(&snapshot.buffer_snapshot); - ( - start.row, - start.to_display_point(&snapshot.display_snapshot) - ..anchor.end.to_display_point(&snapshot), - ) - }), - ); - let mut newest_selection_head = None; if editor.show_local_selections { @@ -1684,36 +1702,17 @@ 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 head_for_relative = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); SelectionLayout::new( @@ -1754,21 +1753,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 +1829,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 +1855,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() { @@ -1926,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, @@ -1933,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, @@ -1946,14 +1939,13 @@ impl EditorElement { active_rows, highlighted_rows, highlighted_ranges, - fold_ranges, line_number_layouts, display_hunks, blocks, selections, context_menu, code_actions_indicator, - // fold_indicators, + fold_indicators, tab_invisible, space_invisible, // hover_popovers: hover, @@ -2012,14 +2004,13 @@ impl EditorElement { anchor_x, gutter_padding, line_height, - // scroll_x, gutter_width, em_width, block_id, + editor_style: &self.style, }) } TransformBlock::ExcerptHeader { - id, buffer, range, starts_new_buffer, @@ -2041,9 +2032,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); }) @@ -2139,11 +2128,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; @@ -2155,7 +2146,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; @@ -2167,7 +2158,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; @@ -2180,7 +2171,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, @@ -2194,7 +2185,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; @@ -2260,11 +2251,7 @@ impl LineWithInvisibles { if !line_chunk.is_empty() && !line_exceeded_max_len { let text_style = if let Some(style) = highlighted_chunk.style { - text_style - .clone() - .highlight(style) - .map(Cow::Owned) - .unwrap_or_else(|_| Cow::Borrowed(text_style)) + Cow::Owned(text_style.clone().highlight(style)) } else { Cow::Borrowed(text_style) }; @@ -2328,18 +2315,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, ); @@ -2348,7 +2333,6 @@ impl LineWithInvisibles { &selection_ranges, layout, content_origin, - scroll_left, line_y, row, line_height, @@ -2362,7 +2346,6 @@ impl LineWithInvisibles { selection_ranges: &[Range], layout: &LayoutState, content_origin: gpui::Point, - scroll_left: Pixels, line_y: Pixels, row: u32, line_height: Pixels, @@ -2384,8 +2367,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); @@ -2462,179 +2448,17 @@ impl Element for EditorElement { dispatch_context, Some(editor.focus_handle.clone()), |_, cx| { - register_action(cx, Editor::move_left); - register_action(cx, Editor::move_right); - register_action(cx, Editor::move_down); - register_action(cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(cx, Editor::cancel); - register_action(cx, Editor::newline); - register_action(cx, Editor::newline_above); - register_action(cx, Editor::newline_below); - register_action(cx, Editor::backspace); - register_action(cx, Editor::delete); - register_action(cx, Editor::tab); - register_action(cx, Editor::tab_prev); - register_action(cx, Editor::indent); - register_action(cx, Editor::outdent); - register_action(cx, Editor::delete_line); - register_action(cx, Editor::join_lines); - register_action(cx, Editor::sort_lines_case_sensitive); - register_action(cx, Editor::sort_lines_case_insensitive); - register_action(cx, Editor::reverse_lines); - register_action(cx, Editor::shuffle_lines); - register_action(cx, Editor::convert_to_upper_case); - register_action(cx, Editor::convert_to_lower_case); - register_action(cx, Editor::convert_to_title_case); - register_action(cx, Editor::convert_to_snake_case); - register_action(cx, Editor::convert_to_kebab_case); - register_action(cx, Editor::convert_to_upper_camel_case); - register_action(cx, Editor::convert_to_lower_camel_case); - register_action(cx, Editor::delete_to_previous_word_start); - register_action(cx, Editor::delete_to_previous_subword_start); - register_action(cx, Editor::delete_to_next_word_end); - register_action(cx, Editor::delete_to_next_subword_end); - register_action(cx, Editor::delete_to_beginning_of_line); - register_action(cx, Editor::delete_to_end_of_line); - register_action(cx, Editor::cut_to_end_of_line); - register_action(cx, Editor::duplicate_line); - register_action(cx, Editor::move_line_up); - register_action(cx, Editor::move_line_down); - register_action(cx, Editor::transpose); - register_action(cx, Editor::cut); - register_action(cx, Editor::copy); - register_action(cx, Editor::paste); - register_action(cx, Editor::undo); - register_action(cx, Editor::redo); - register_action(cx, Editor::move_page_up); - register_action(cx, Editor::move_page_down); - register_action(cx, Editor::next_screen); - register_action(cx, Editor::scroll_cursor_top); - register_action(cx, Editor::scroll_cursor_center); - register_action(cx, Editor::scroll_cursor_bottom); - register_action(cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(cx, Editor::move_to_previous_word_start); - register_action(cx, Editor::move_to_previous_subword_start); - register_action(cx, Editor::move_to_next_word_end); - register_action(cx, Editor::move_to_next_subword_end); - register_action(cx, Editor::move_to_beginning_of_line); - register_action(cx, Editor::move_to_end_of_line); - register_action(cx, Editor::move_to_start_of_paragraph); - register_action(cx, Editor::move_to_end_of_paragraph); - register_action(cx, Editor::move_to_beginning); - register_action(cx, Editor::move_to_end); - register_action(cx, Editor::select_up); - register_action(cx, Editor::select_down); - register_action(cx, Editor::select_left); - register_action(cx, Editor::select_right); - register_action(cx, Editor::select_to_previous_word_start); - register_action(cx, Editor::select_to_previous_subword_start); - register_action(cx, Editor::select_to_next_word_end); - register_action(cx, Editor::select_to_next_subword_end); - register_action(cx, Editor::select_to_beginning_of_line); - register_action(cx, Editor::select_to_end_of_line); - register_action(cx, Editor::select_to_start_of_paragraph); - register_action(cx, Editor::select_to_end_of_paragraph); - register_action(cx, Editor::select_to_beginning); - register_action(cx, Editor::select_to_end); - register_action(cx, Editor::select_all); - register_action(cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(cx, Editor::select_line); - register_action(cx, Editor::split_selection_into_lines); - register_action(cx, Editor::add_selection_above); - register_action(cx, Editor::add_selection_below); - register_action(cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(cx, Editor::toggle_comments); - register_action(cx, Editor::select_larger_syntax_node); - register_action(cx, Editor::select_smaller_syntax_node); - register_action(cx, Editor::move_to_enclosing_bracket); - register_action(cx, Editor::undo_selection); - register_action(cx, Editor::redo_selection); - register_action(cx, Editor::go_to_diagnostic); - register_action(cx, Editor::go_to_prev_diagnostic); - register_action(cx, Editor::go_to_hunk); - register_action(cx, Editor::go_to_prev_hunk); - register_action(cx, Editor::go_to_definition); - register_action(cx, Editor::go_to_definition_split); - register_action(cx, Editor::go_to_type_definition); - register_action(cx, Editor::go_to_type_definition_split); - register_action(cx, Editor::fold); - register_action(cx, Editor::fold_at); - register_action(cx, Editor::unfold_lines); - register_action(cx, Editor::unfold_at); - register_action(cx, Editor::fold_selected_ranges); - register_action(cx, Editor::show_completions); - register_action(cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(cx, Editor::toggle_soft_wrap); - register_action(cx, Editor::toggle_inlay_hints); - register_action(cx, Editor::reveal_in_finder); - register_action(cx, Editor::copy_path); - register_action(cx, Editor::copy_relative_path); - register_action(cx, Editor::copy_highlight_json); - register_action(cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::restart_language_server); - register_action(cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - // on_action(cx, Editor::rename); todo!() - // on_action(cx, Editor::confirm_rename); todo!() - register_action(cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::next_copilot_suggestion); - register_action(cx, Editor::previous_copilot_suggestion); - register_action(cx, Editor::copilot_suggest); - register_action(cx, Editor::context_menu_first); - register_action(cx, Editor::context_menu_prev); - register_action(cx, Editor::context_menu_next); - register_action(cx, Editor::context_menu_last); + register_actions(cx); // 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, - ); + // 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); @@ -2642,11 +2466,10 @@ 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); + }) } - - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); }); }); }, @@ -2654,6 +2477,12 @@ impl Element for EditorElement { } } +impl Component for EditorElement { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + // impl EditorElement { // type LayoutState = LayoutState; // type PaintState = (); @@ -3262,6 +3091,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>, @@ -3269,7 +3099,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, @@ -3278,7 +3107,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, } @@ -3291,6 +3120,7 @@ struct CodeActionsIndicator { struct PositionMap { size: Size, line_height: Pixels, + scroll_position: gpui::Point, scroll_max: gpui::Point, em_width: Pixels, em_advance: Pixels, @@ -3627,58 +3457,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() } @@ -4130,6 +3908,179 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } +fn register_actions(cx: &mut ViewContext) { + register_action(cx, Editor::move_left); + register_action(cx, Editor::move_right); + register_action(cx, Editor::move_down); + register_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + register_action(cx, Editor::cancel); + register_action(cx, Editor::newline); + register_action(cx, Editor::newline_above); + register_action(cx, Editor::newline_below); + register_action(cx, Editor::backspace); + register_action(cx, Editor::delete); + register_action(cx, Editor::tab); + register_action(cx, Editor::tab_prev); + register_action(cx, Editor::indent); + register_action(cx, Editor::outdent); + register_action(cx, Editor::delete_line); + register_action(cx, Editor::join_lines); + register_action(cx, Editor::sort_lines_case_sensitive); + register_action(cx, Editor::sort_lines_case_insensitive); + register_action(cx, Editor::reverse_lines); + register_action(cx, Editor::shuffle_lines); + register_action(cx, Editor::convert_to_upper_case); + register_action(cx, Editor::convert_to_lower_case); + register_action(cx, Editor::convert_to_title_case); + register_action(cx, Editor::convert_to_snake_case); + register_action(cx, Editor::convert_to_kebab_case); + register_action(cx, Editor::convert_to_upper_camel_case); + register_action(cx, Editor::convert_to_lower_camel_case); + register_action(cx, Editor::delete_to_previous_word_start); + register_action(cx, Editor::delete_to_previous_subword_start); + register_action(cx, Editor::delete_to_next_word_end); + register_action(cx, Editor::delete_to_next_subword_end); + register_action(cx, Editor::delete_to_beginning_of_line); + register_action(cx, Editor::delete_to_end_of_line); + register_action(cx, Editor::cut_to_end_of_line); + register_action(cx, Editor::duplicate_line); + register_action(cx, Editor::move_line_up); + register_action(cx, Editor::move_line_down); + register_action(cx, Editor::transpose); + register_action(cx, Editor::cut); + register_action(cx, Editor::copy); + register_action(cx, Editor::paste); + register_action(cx, Editor::undo); + register_action(cx, Editor::redo); + register_action(cx, Editor::move_page_up); + register_action(cx, Editor::move_page_down); + register_action(cx, Editor::next_screen); + register_action(cx, Editor::scroll_cursor_top); + register_action(cx, Editor::scroll_cursor_center); + register_action(cx, Editor::scroll_cursor_bottom); + register_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(cx, Editor::move_to_previous_word_start); + register_action(cx, Editor::move_to_previous_subword_start); + register_action(cx, Editor::move_to_next_word_end); + register_action(cx, Editor::move_to_next_subword_end); + register_action(cx, Editor::move_to_beginning_of_line); + register_action(cx, Editor::move_to_end_of_line); + register_action(cx, Editor::move_to_start_of_paragraph); + register_action(cx, Editor::move_to_end_of_paragraph); + register_action(cx, Editor::move_to_beginning); + register_action(cx, Editor::move_to_end); + register_action(cx, Editor::select_up); + register_action(cx, Editor::select_down); + register_action(cx, Editor::select_left); + register_action(cx, Editor::select_right); + register_action(cx, Editor::select_to_previous_word_start); + register_action(cx, Editor::select_to_previous_subword_start); + register_action(cx, Editor::select_to_next_word_end); + register_action(cx, Editor::select_to_next_subword_end); + register_action(cx, Editor::select_to_beginning_of_line); + register_action(cx, Editor::select_to_end_of_line); + register_action(cx, Editor::select_to_start_of_paragraph); + register_action(cx, Editor::select_to_end_of_paragraph); + register_action(cx, Editor::select_to_beginning); + register_action(cx, Editor::select_to_end); + register_action(cx, Editor::select_all); + register_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(cx, Editor::select_line); + register_action(cx, Editor::split_selection_into_lines); + register_action(cx, Editor::add_selection_above); + register_action(cx, Editor::add_selection_below); + register_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(cx, Editor::toggle_comments); + register_action(cx, Editor::select_larger_syntax_node); + register_action(cx, Editor::select_smaller_syntax_node); + register_action(cx, Editor::move_to_enclosing_bracket); + register_action(cx, Editor::undo_selection); + register_action(cx, Editor::redo_selection); + register_action(cx, Editor::go_to_diagnostic); + register_action(cx, Editor::go_to_prev_diagnostic); + register_action(cx, Editor::go_to_hunk); + register_action(cx, Editor::go_to_prev_hunk); + register_action(cx, Editor::go_to_definition); + register_action(cx, Editor::go_to_definition_split); + register_action(cx, Editor::go_to_type_definition); + register_action(cx, Editor::go_to_type_definition_split); + register_action(cx, Editor::fold); + register_action(cx, Editor::fold_at); + register_action(cx, Editor::unfold_lines); + register_action(cx, Editor::unfold_at); + register_action(cx, Editor::fold_selected_ranges); + register_action(cx, Editor::show_completions); + register_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + register_action(cx, Editor::toggle_soft_wrap); + register_action(cx, Editor::toggle_inlay_hints); + register_action(cx, Editor::reveal_in_finder); + register_action(cx, Editor::copy_path); + register_action(cx, Editor::copy_relative_path); + register_action(cx, Editor::copy_highlight_json); + register_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + register_action(cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .confirm_rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::next_copilot_suggestion); + register_action(cx, Editor::previous_copilot_suggestion); + register_action(cx, Editor::copilot_suggest); + register_action(cx, Editor::context_menu_first); + register_action(cx, Editor::context_menu_prev); + register_action(cx, Editor::context_menu_next); + register_action(cx, Editor::context_menu_last); +} + fn register_action( cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, diff --git a/crates/editor2/src/git.rs b/crates/editor2/src/git.rs index e04372f0a7bfe79a97ba196ac853aad7b37c8119..6e408cd3a01f7603ccdf97660b8896fdee833d65 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/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index aae3bca160da8d9db514e2e566ff7d4a1dbeeb09..236bc152441e4eabb097616c7cc12e7c4da1236a 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,1089 @@ 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, cx) = build_find_picker(project, 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, cx) = build_find_picker(project, 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, cx) = build_find_picker(project, 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, _, cx) = build_find_picker(project, 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, _, cx) = build_find_picker(project, 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, _, 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. + 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 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, cx) = build_find_picker(project, 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, 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); + 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, 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,); + + 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, 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; + 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, 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,); + + 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, 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; + 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, 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; + 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, + &mut VisualTestContext, + ) { + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let picker = open_file_picker(&workspace, 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/docs/contexts.md b/crates/gpui2/docs/contexts.md index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..763a902517050da97757969977298180d5850742 100644 --- a/crates/gpui2/docs/contexts.md +++ b/crates/gpui2/docs/contexts.md @@ -0,0 +1,41 @@ +# Contexts + +GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services. + +There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead. + +``` + AppContext + / \ +ModelContext WindowContext + / + ViewContext +``` + +- The `AppContext` forms the root of the hierarchy +- `ModelContext` and `WindowContext` both dereference to `AppContext` +- `ViewContext` dereferences to `WindowContext` + +## `AppContext` + +Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`... + +## `WindowContext` + +Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`. + +## `ModelContext` + +Available when you create or update a `Model`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events. + +## `ViewContext` + +Available when you create or update a `View`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events. + +## `AsyncAppContext` and `AsyncWindowContext` + +Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself. + +## `TestAppContext` and `TestVisualContext` + +These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 16487cf18afc4ddf96bfb76598747cdfcce1568e..dbb510b1c8d6d680e265e16dc8502048dab4203f 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 cc59b7a16a52804c89b4b4b9489fe4ec7945a272..50c409c0f2b02fe8aa3994d9b650a7e8f18dd1f9 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}; @@ -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( @@ -220,7 +222,35 @@ impl TestAppContext { { window .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) - .unwrap() + .unwrap(); + + 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(" ") + .map(Keystroke::parse) + .map(Result::unwrap) + { + self.dispatch_keystroke(window, keystroke.into(), false); + } + + 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( @@ -229,15 +259,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 +457,24 @@ 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) + } + + 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/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 09392ebd3f6a3bfa29df8a731348936a0e1acef6..01b2d4da07e68e79c91ea0b58684a8b839dd4d57 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)] @@ -100,7 +136,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. @@ -162,7 +200,7 @@ where ElementRenderPhase::Start => panic!("must call initialize before layout"), ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::LayoutComputed { .. } - | ElementRenderPhase::Painted => { + | ElementRenderPhase::Painted { .. } => { panic!("element rendered twice") } }; @@ -197,7 +235,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 f3f6385503466387cbd992778203301eee40ad82..1f9b2b020abafd626b45453190dedbab7c406874 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; @@ -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| { @@ -406,7 +420,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 +569,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>; @@ -597,7 +611,7 @@ impl ParentComponent for Div { } impl Element for Div { - type ElementState = NodeState; + type ElementState = DivState; fn element_id(&self) -> Option { self.interactivity.element_id.clone() @@ -617,7 +631,7 @@ impl Element for Div { child.initialize(view_state, cx); } - NodeState { + DivState { interactive_state, child_layout_ids: SmallVec::new(), } @@ -706,11 +720,17 @@ impl Component for Div { } } -pub struct NodeState { +pub struct DivState { child_layout_ids: SmallVec<[LayoutId; 4]>, interactive_state: InteractiveElementState, } +impl DivState { + pub fn is_active(&self) -> bool { + self.interactive_state.pending_mouse_down.borrow().is_some() + } +} + pub struct Interactivity { pub element_id: Option, pub key_context: KeyContext, @@ -876,7 +896,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(); @@ -890,7 +910,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); @@ -910,12 +930,14 @@ 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 |_state, event: &MouseDownEvent, phase, _cx| { + 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(); } }); } @@ -930,8 +952,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; @@ -952,13 +974,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(); @@ -966,7 +988,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), @@ -978,14 +1000,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() } @@ -993,10 +1015,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(); } }); @@ -1011,7 +1033,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(); } } @@ -1022,14 +1044,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); @@ -1058,7 +1080,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(), @@ -1157,7 +1179,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) @@ -1211,11 +1233,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 84cd216275801bfa5ff35f08471f394402235931..340a2cbf87c79c93f54b8610cd44b26da9e41e4d 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, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 013ed2ea482a49cdf26f2411b09d8398d01dc506..80a89ef6257497459c848df60e68187a01e7142d 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 diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index f737c6e30b205d867133bd86561fbcd0dc10a3e6..962a0308445b48afc85dbc4492d19574917a2e9c 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/platform.rs b/crates/gpui2/src/platform.rs index 8b49addec9cc6caf4f7b06ca8d23223d34535ef5..00ce3340f82b6466990a0bc75cfdea175bad4edb 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync { fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)>; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; fn wrap_line( &self, diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index b87db09dc0ac16bee9e76bcf92e00b21761c1143..155f3097fec2e13b70355b78d9625fd74248bf82 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem { }, )?; - Ok(candidates[ix]) + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) } } @@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem { fn rasterize_glyph( &self, glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, ) -> Result<(Size, Vec)> { - self.0.read().rasterize_glyph(glyph_id) + self.0.read().rasterize_glyph(glyph_id, raster_bounds) } fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { @@ -247,8 +250,11 @@ impl MacTextSystemState { .into()) } - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)> { - let glyph_bounds = self.raster_bounds(params)?; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { @@ -260,6 +266,7 @@ impl MacTextSystemState { if params.subpixel_variant.y > 0 { bitmap_size.height += DevicePixels(1); } + let bitmap_size = bitmap_size; let mut bytes; let cx; diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index adb15c426629bf8f1292fbfd2eed044367408b21..e355c3aa4b5ec0681faad153f24c1f3ca5a4fceb 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/style.rs b/crates/gpui2/src/style.rs index 92959ef05795d441d395c6445f6ba0af0f745c9c..5d9dd5d804376e8f4840c2c4a56f1d1ef35274df 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,8 +1,8 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, - FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result, - Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, + FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, + SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -157,7 +157,7 @@ impl Default for TextStyle { } impl TextStyle { - pub fn highlight(mut self, style: HighlightStyle) -> Result { + pub fn highlight(mut self, style: HighlightStyle) -> Self { if let Some(weight) = style.font_weight { self.font_weight = weight; } @@ -177,7 +177,7 @@ impl TextStyle { self.underline = Some(underline); } - Ok(self) + self } pub fn font(&self) -> Font { diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index e8d6acc5a3e965818fd9198783c289297588f573..c7031fcb4d77c86ebe765c1021468f9751fcc96e 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -39,6 +39,7 @@ pub struct TextSystem { platform_text_system: Arc, font_ids_by_font: RwLock>, font_metrics: RwLock>, + raster_bounds: RwLock>>, wrapper_pool: Mutex>>, font_runs_pool: Mutex>>, } @@ -48,10 +49,11 @@ impl TextSystem { TextSystem { line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())), platform_text_system, - font_metrics: RwLock::new(HashMap::default()), - font_ids_by_font: RwLock::new(HashMap::default()), - wrapper_pool: Mutex::new(HashMap::default()), - font_runs_pool: Default::default(), + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), } } @@ -252,14 +254,24 @@ impl TextSystem { } pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - self.platform_text_system.glyph_raster_bounds(params) + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(bounds.clone()) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } } pub fn rasterize_glyph( &self, - glyph_id: &RenderGlyphParams, + params: &RenderGlyphParams, ) -> Result<(Size, Vec)> { - self.platform_text_system.rasterize_glyph(glyph_id) + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) } } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index c25b3068861b65bcf302437b3f1087a03162b605..8f070f22b5320179cbb838b7c4659f9603cf2d8c 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,6 +291,67 @@ impl From> for AnyWeakView { // } // } +pub struct RenderViewWith { + view: View, + component: Option, +} + +impl Component for RenderViewWith +where + C: 'static + Component, + ParentViewState: 'static, + ViewState: 'static, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl Element for RenderViewWith +where + C: 'static + Component, + ParentViewState: 'static, + ViewState: 'static, +{ + type ElementState = AnyElement; + + fn element_id(&self) -> Option { + Some(self.view.entity_id().into()) + } + + fn initialize( + &mut self, + _: &mut ParentViewState, + _: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + self.view.update(cx, |view, cx| { + let mut element = self.component.take().unwrap().render(); + element.initialize(view, cx); + 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, + _: &mut ParentViewState, + element: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.view.update(cx, |view, cx| element.paint(view, cx)) + } +} + mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0dbc0d8630e31ca0c9bbc09aeda56c8081da0c20..6d0d97029db81048bb4d4dc9c358ecd7f021c873 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, @@ -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, @@ -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 @@ -2471,7 +2487,7 @@ impl From> for StackingOrder { #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum ElementId { View(EntityId), - Number(usize), + Integer(usize), Name(SharedString), FocusHandle(FocusId), } @@ -2496,13 +2512,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) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 468659e5b9fbfcceee9b03f49ec663832a354e18..322b2ae894fbfa636643968a686eb572fddaee5b 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 80fd44761c7ca3d1afee4247cab5b019da5d111b..785ce58bb8d2b40266a25106deb15fe666d2c823 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 efe407f847baabda23811994e324ace3630a7756..61ad500a73413d795bc9fcf7bcc38c5355aba07c 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 65959d3f310598de3a6e6c4d5b59e215d262d351..9444dd9185440e7fbc953ac4a96dfe4e89e8c50f 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:#}"); } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index c91df732c75b442c43baf2e05df439c4d0474ac6..cd5995d65e438c98264d7d1bd44747d1ce72caf6 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 f9e6ba48de23b7047eeac2a2b3e5b71646f19d27..a25cb360566f470a09088641f0ead1f5cad28ed8 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 ec5d4d8afb8544e3e6978d74ee1fa625b41dd294..5e2e381e7e10014bb792a3af8fab604622ddebdd 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) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 941898cdc3e232b63ace2f08454a8db61ad93417..028653696ac7a13d979044a36277711413b1c874 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]