diff --git a/Cargo.lock b/Cargo.lock index 9417057bbae7d648420c6d543318318eaaaccd4c..a8e555b42362bfa89a8cc308363fcd4df5d448bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,6 +1719,23 @@ dependencies = [ "workspace", ] +[[package]] +name = "find" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "collections", + "editor", + "gpui", + "postage", + "regex", + "smol", + "theme", + "unindent", + "workspace", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -5725,6 +5742,7 @@ dependencies = [ "editor", "env_logger", "file_finder", + "find", "fsevent", "futures", "fuzzy", diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index 993a0cec0e6e6b923d2141195f47674267f7d3a6..b155d9fc3260225cdddae2529a90ce34b16d5f67 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -217,7 +217,7 @@ impl ChatPanel { let theme = &self.settings.borrow().theme; Flex::column() .with_child( - Container::new(ChildView::new(self.channel_select.id()).boxed()) + Container::new(ChildView::new(&self.channel_select).boxed()) .with_style(theme.chat_panel.channel_select.container) .boxed(), ) @@ -282,7 +282,7 @@ impl ChatPanel { fn render_input_box(&self) -> ElementBox { let theme = &self.settings.borrow().theme; - Container::new(ChildView::new(self.input_editor.id()).boxed()) + Container::new(ChildView::new(&self.input_editor).boxed()) .with_style(theme.chat_panel.input_editor.container) .boxed() } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d99713308ad298421d772e8c2bce1db0540a555b..ac197a7456eb6ae94f27cbcab1ea9847a14de547 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -111,7 +111,7 @@ impl View for ProjectDiagnosticsEditor { .with_style(theme.container) .boxed() } else { - ChildView::new(self.editor.id()).boxed() + ChildView::new(&self.editor).boxed() } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 27a8c79ba3cf4b1b1fc7ea8dca764e66a3de35f1..db0c099c4b8e228eea8c2349e945770c836bda82 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9,12 +9,13 @@ mod test; use aho_corasick::AhoCorasick; use clock::ReplicaId; -use collections::{HashMap, HashSet}; +use collections::{BTreeMap, HashMap, HashSet}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ action, + color::Color, elements::*, fonts::TextStyle, geometry::vector::{vec2f, Vector2F}, @@ -28,23 +29,25 @@ use language::{ AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; +use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot, + ToOffset, ToPoint, }; -use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot}; use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; use std::{ - cmp, + any::TypeId, + cmp::{self, Ordering}, iter::{self, FromIterator}, mem, ops::{Deref, Range, RangeInclusive, Sub}, sync::Arc, time::{Duration, Instant}, }; -use sum_tree::Bias; +pub use sum_tree::Bias; use text::rope::TextDimension; use theme::{DiagnosticStyle, EditorStyle}; use util::post_inc; @@ -382,6 +385,7 @@ pub struct Editor { vertical_scroll_margin: f32, placeholder_text: Option>, highlighted_rows: Option>, + highlighted_ranges: BTreeMap>)>, nav_history: Option, } @@ -435,6 +439,14 @@ pub struct NavigationData { offset: usize, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + impl Editor { pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext) -> Self { let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); @@ -522,6 +534,7 @@ impl Editor { vertical_scroll_margin: 3.0, placeholder_text: None, highlighted_rows: None, + highlighted_ranges: Default::default(), nav_history: None, }; let selection = Selection { @@ -848,7 +861,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; - let newest_selection = self.newest_selection_internal().unwrap().clone(); + let newest_selection = self.newest_anchor_selection().unwrap().clone(); let start; let end; @@ -1078,6 +1091,11 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.mode != EditorMode::Full { + cx.propagate_action(); + return; + } + if self.active_diagnostics.is_some() { self.dismiss_diagnostics(cx); } else if let Some(PendingSelection { selection, .. }) = self.pending_selection.take() { @@ -3345,10 +3363,10 @@ impl Editor { &self, snapshot: &MultiBufferSnapshot, ) -> Selection { - self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot) + self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot) } - pub fn newest_selection_internal(&self) -> Option<&Selection> { + pub fn newest_anchor_selection(&self) -> Option<&Selection> { self.pending_selection .as_ref() .map(|s| &s.selection) @@ -3364,7 +3382,7 @@ impl Editor { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(cx).snapshot(cx); - let old_cursor_position = self.newest_selection_internal().map(|s| s.head()); + let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. @@ -3498,6 +3516,7 @@ impl Editor { buffer.set_active_selections(&self.selections, cx) }); } + cx.emit(Event::SelectionsChanged); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { @@ -3721,6 +3740,76 @@ impl Editor { self.highlighted_rows.clone() } + pub fn highlight_ranges( + &mut self, + ranges: Vec>, + color: Color, + cx: &mut ViewContext, + ) { + self.highlighted_ranges + .insert(TypeId::of::(), (color, ranges)); + cx.notify(); + } + + pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { + self.highlighted_ranges.remove(&TypeId::of::()); + cx.notify(); + } + + #[cfg(feature = "test-support")] + pub fn all_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Vec<(Range, Color)> { + let snapshot = self.snapshot(cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + self.highlighted_ranges_in_range(start..end, &snapshot) + } + + pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { + self.highlighted_ranges + .get(&TypeId::of::()) + .map(|(color, ranges)| (*color, ranges.as_slice())) + } + + pub fn highlighted_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Range, Color)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (color, ranges) in self.highlighted_ranges.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, *color)) + } + } + results + } + fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch @@ -3934,6 +4023,7 @@ pub enum Event { Dirtied, Saved, TitleChanged, + SelectionsChanged, Closed, } @@ -4159,6 +4249,18 @@ pub fn settings_builder( }) } +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} + #[cfg(test)] mod tests { use super::*; @@ -6555,6 +6657,83 @@ mod tests { }); } + #[gpui::test] + fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + let settings = EditorSettings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = buffer.read(cx).snapshot(cx); + + let anchor_range = |range: Range| { + buffer.anchor_after(range.start)..buffer.anchor_after(range.end) + }; + + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + Color::red(), + cx, + ); + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ), + &[ + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Color::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + ), + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Color::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Color::green(), + ), + ] + ); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + )] + ); + }); + } + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f1af1b4ce8beac39d4afc37abb9270c581782f0c..8ef66568970ac5037711d258a8b502352054f62c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -312,64 +312,47 @@ impl EditorElement { let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; + let content_origin = bounds.origin() + layout.text_offset; cx.scene.push_layer(Some(bounds)); - // Draw selections - let corner_radius = 2.5; - let mut cursors = SmallVec::<[Cursor; 32]>::new(); - - let content_origin = bounds.origin() + layout.text_offset; + for (range, color) in &layout.highlighted_ranges { + self.paint_highlighted_range( + range.clone(), + start_row, + end_row, + *color, + 0., + 0.15 * layout.line_height, + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); + } + let mut cursors = SmallVec::<[Cursor; 32]>::new(); for (replica_id, selections) in &layout.selections { let style = style.replica_selection_style(*replica_id); + let corner_radius = 0.15 * layout.line_height; for selection in selections { - if selection.start != selection.end { - let row_range = if selection.end.column() == 0 { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row(), end_row) - } else { - cmp::max(selection.start.row(), start_row) - ..cmp::min(selection.end.row() + 1, end_row) - }; - - let selection = Selection { - color: style.selection, - line_height: layout.line_height, - start_y: content_origin.y() + row_range.start as f32 * layout.line_height - - scroll_top, - lines: row_range - .into_iter() - .map(|row| { - let line_layout = &layout.line_layouts[(row - start_row) as usize]; - SelectionLine { - start_x: if row == selection.start.row() { - content_origin.x() - + line_layout - .x_for_index(selection.start.column() as usize) - - scroll_left - } else { - content_origin.x() - scroll_left - }, - end_x: if row == selection.end.row() { - content_origin.x() - + line_layout - .x_for_index(selection.end.column() as usize) - - scroll_left - } else { - content_origin.x() - + line_layout.width() - + corner_radius * 2.0 - - scroll_left - }, - } - }) - .collect(), - }; - - selection.paint(bounds, cx.scene); - } + self.paint_highlighted_range( + selection.start..selection.end, + start_row, + end_row, + style.selection, + corner_radius, + corner_radius * 2., + layout, + content_origin, + scroll_top, + scroll_left, + bounds, + cx, + ); if view.show_local_cursors() || *replica_id != local_replica_id { let cursor_position = selection.head(); @@ -412,6 +395,63 @@ impl EditorElement { cx.scene.pop_layer(); } + fn paint_highlighted_range( + &self, + range: Range, + start_row: u32, + end_row: u32, + color: Color, + corner_radius: f32, + line_end_overshoot: f32, + layout: &LayoutState, + content_origin: Vector2F, + scroll_top: f32, + scroll_left: f32, + bounds: RectF, + cx: &mut PaintContext, + ) { + if range.start != 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 highlighted_range = HighlightedRange { + color, + line_height: layout.line_height, + corner_radius, + start_y: content_origin.y() + row_range.start as f32 * layout.line_height + - scroll_top, + lines: row_range + .into_iter() + .map(|row| { + let line_layout = &layout.line_layouts[(row - start_row) as usize]; + HighlightedRangeLine { + 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 + }, + 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 + }, + } + }) + .collect(), + }; + + highlighted_range.paint(bounds, cx.scene); + } + } + fn paint_blocks( &mut self, bounds: RectF, @@ -715,10 +755,16 @@ impl Element for EditorElement { let mut selections = HashMap::default(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; + let mut highlighted_ranges = Vec::new(); self.update_view(cx.app, |view, cx| { - highlighted_rows = view.highlighted_rows(); let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx)); + highlighted_rows = view.highlighted_rows(); + highlighted_ranges = view.highlighted_ranges_in_range( + start_anchor.clone()..end_anchor.clone(), + &display_map, + ); + let local_selections = view .local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map); for selection in &local_selections { @@ -837,6 +883,7 @@ impl Element for EditorElement { snapshot, active_rows, highlighted_rows, + highlighted_ranges, line_layouts, line_number_layouts, blocks, @@ -950,6 +997,7 @@ pub struct LayoutState { line_height: f32, em_width: f32, em_advance: f32, + highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, text_offset: Vector2F, } @@ -1036,20 +1084,21 @@ impl Cursor { } #[derive(Debug)] -struct Selection { +struct HighlightedRange { start_y: f32, line_height: f32, - lines: Vec, + lines: Vec, color: Color, + corner_radius: f32, } #[derive(Debug)] -struct SelectionLine { +struct HighlightedRangeLine { start_x: f32, end_x: f32, } -impl Selection { +impl HighlightedRange { fn paint(&self, bounds: RectF, scene: &mut Scene) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene); @@ -1064,26 +1113,31 @@ impl Selection { } } - fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) { + fn paint_lines( + &self, + start_y: f32, + lines: &[HighlightedRangeLine], + bounds: RectF, + scene: &mut Scene, + ) { if lines.is_empty() { return; } let mut path = PathBuilder::new(); - let corner_radius = 0.15 * self.line_height; let first_line = lines.first().unwrap(); let last_line = lines.last().unwrap(); let first_top_left = vec2f(first_line.start_x, start_y); let first_top_right = vec2f(first_line.end_x, start_y); - let curve_height = vec2f(0., corner_radius); + let curve_height = vec2f(0., self.corner_radius); let curve_width = |start_x: f32, end_x: f32| { let max = (end_x - start_x) / 2.; - let width = if max < corner_radius { + let width = if max < self.corner_radius { max } else { - corner_radius + self.corner_radius }; vec2f(width, 0.) @@ -1107,26 +1161,38 @@ impl Selection { Ordering::Less => { let curve_width = curve_width(next_top_right.x(), bottom_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } path.line_to(next_top_right + curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } Ordering::Greater => { let curve_width = curve_width(bottom_right.x(), next_top_right.x()); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right + curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right + curve_width, bottom_right); + } path.line_to(next_top_right - curve_width); - path.curve_to(next_top_right + curve_height, next_top_right); + if self.corner_radius > 0. { + path.curve_to(next_top_right + curve_height, next_top_right); + } } } } else { let curve_width = curve_width(line.start_x, line.end_x); path.line_to(bottom_right - curve_height); - path.curve_to(bottom_right - curve_width, bottom_right); + if self.corner_radius > 0. { + path.curve_to(bottom_right - curve_width, bottom_right); + } let bottom_left = vec2f(line.start_x, bottom_right.y()); path.line_to(bottom_left + curve_width); - path.curve_to(bottom_left - curve_height, bottom_left); + if self.corner_radius > 0. { + path.curve_to(bottom_left - curve_height, bottom_left); + } } } @@ -1134,14 +1200,20 @@ impl Selection { let curve_width = curve_width(last_line.start_x, first_line.start_x); let second_top_left = vec2f(last_line.start_x, start_y + self.line_height); path.line_to(second_top_left + curve_height); - path.curve_to(second_top_left + curve_width, second_top_left); + if self.corner_radius > 0. { + path.curve_to(second_top_left + curve_width, second_top_left); + } let first_bottom_left = vec2f(first_line.start_x, second_top_left.y()); path.line_to(first_bottom_left - curve_width); - path.curve_to(first_bottom_left - curve_height, first_bottom_left); + if self.corner_radius > 0. { + path.curve_to(first_bottom_left - curve_height, first_bottom_left); + } } path.line_to(first_top_left + curve_height); - path.curve_to(first_top_left + top_curve_width, first_top_left); + if self.corner_radius > 0. { + path.curve_to(first_top_left + top_curve_width, first_top_left); + } path.line_to(first_top_right - top_curve_width); scene.push_path(path.build(self.color, Some(bounds))); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1dafec32c6f080dfe2799513840c14ad26eff28e..97ce05615246cdd236e4a32cf580296bd142d119 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -141,7 +141,7 @@ impl ItemView for Editor { } fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(selection) = self.newest_selection_internal() { + if let Some(selection) = self.newest_anchor_selection() { self.push_to_nav_history(selection.head(), None, cx); } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 466b6e932390fc7b0ae1e1da286fb455f50be634..9a800f9abba9bc63dfa53db2f3a2aae6192ea486 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,5 +1,5 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::ToPoint; +use crate::{char_kind, CharKind, ToPoint}; use anyhow::Result; use std::{cmp, ops::Range}; @@ -215,26 +215,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4cedfa80d2703f3987bf92aac080db2d98733f48..3d9b7ceb3985f75901bf0de8781cc1117467aa31 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -76,6 +76,7 @@ struct BufferState { #[derive(Clone, Default)] pub struct MultiBufferSnapshot { + singleton: bool, excerpts: SumTree, parse_count: usize, diagnostics_update_count: usize, @@ -163,6 +164,7 @@ impl MultiBuffer { }, cx, ); + this.snapshot.borrow_mut().singleton = true; this } @@ -1079,11 +1081,9 @@ impl MultiBufferSnapshot { .eq(needle.bytes()) } - fn as_singleton(&self) -> Option<&BufferSnapshot> { - let mut excerpts = self.excerpts.iter(); - let buffer = excerpts.next().map(|excerpt| &excerpt.buffer); - if excerpts.next().is_none() { - buffer + fn as_singleton(&self) -> Option<&Excerpt> { + if self.singleton { + self.excerpts.iter().next() } else { None } @@ -1098,6 +1098,10 @@ impl MultiBufferSnapshot { } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_offset(offset, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&offset, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1113,6 +1117,10 @@ impl MultiBufferSnapshot { } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_point(point, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&point, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1128,6 +1136,10 @@ impl MultiBufferSnapshot { } pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.clip_point_utf16(point, bias); + } + let mut cursor = self.excerpts.cursor::(); cursor.seek(&point, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { @@ -1193,6 +1205,10 @@ impl MultiBufferSnapshot { } pub fn offset_to_point(&self, offset: usize) -> Point { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.offset_to_point(offset); + } + let mut cursor = self.excerpts.cursor::<(usize, Point)>(); cursor.seek(&offset, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1210,6 +1226,10 @@ impl MultiBufferSnapshot { } pub fn point_to_offset(&self, point: Point) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.point_to_offset(point); + } + let mut cursor = self.excerpts.cursor::<(Point, usize)>(); cursor.seek(&point, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1227,6 +1247,10 @@ impl MultiBufferSnapshot { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + if let Some(excerpt) = self.as_singleton() { + return excerpt.buffer.point_utf16_to_offset(point); + } + let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); cursor.seek(&point, Bias::Right, &()); if let Some(excerpt) = cursor.item() { @@ -1520,6 +1544,14 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); + if let Some(excerpt) = self.as_singleton() { + return Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt.buffer.anchor_at(offset, bias), + }; + } + let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>(); cursor.seek(&offset, Bias::Right, &()); if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { @@ -1675,7 +1707,7 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |buffer| buffer.diagnostic_group(group_id)) + .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( @@ -1686,8 +1718,10 @@ impl MultiBufferSnapshot { T: 'a + ToOffset, O: 'a + text::FromAnchor, { - self.as_singleton().into_iter().flat_map(move |buffer| { - buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) + self.as_singleton().into_iter().flat_map(move |excerpt| { + excerpt + .buffer + .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) }) } @@ -1730,17 +1764,16 @@ impl MultiBufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let buffer = self.as_singleton()?; - let outline = buffer.outline(theme)?; - let excerpt_id = &self.excerpts.iter().next().unwrap().id; + let excerpt = self.as_singleton()?; + let outline = excerpt.buffer.outline(theme)?; Some(Outline::new( outline .items .into_iter() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), + range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start) + ..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 3bf489df9194ea84de0f2fb3aed40da6250acfc7..e950dc0fcb74cc001cc7123008906b24010761b0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -76,7 +76,7 @@ impl View for FileFinder { Container::new( Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.query_editor.id()).boxed()) + Container::new(ChildView::new(&self.query_editor).boxed()) .with_style(settings.theme.selector.input_editor.container) .boxed(), ) diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..acab695d12c17c3bd6501ac64adf9a4f2f718daf --- /dev/null +++ b/crates/find/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "find" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/find.rs" + +[dependencies] +collections = { path = "../collections" } +editor = { path = "../editor" } +gpui = { path = "../gpui" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +aho-corasick = "0.7" +anyhow = "1.0" +postage = { version = "0.4.1", features = ["futures-traits"] } +regex = "1.5" +smol = { version = "1.2" } + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } +unindent = "0.1" diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs new file mode 100644 index 0000000000000000000000000000000000000000..859e9f4923f1804daf5de5202b59d278de90e61b --- /dev/null +++ b/crates/find/src/find.rs @@ -0,0 +1,945 @@ +use aho_corasick::AhoCorasickBuilder; +use anyhow::Result; +use collections::HashSet; +use editor::{ + char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, + MultiBufferSnapshot, +}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use postage::watch; +use regex::RegexBuilder; +use smol::future::yield_now; +use std::{ + cmp::{self, Ordering}, + ops::Range, + sync::Arc, +}; +use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; + +action!(Deploy, bool); +action!(Dismiss); +action!(FocusEditor); +action!(ToggleMode, SearchMode); +action!(GoToMatch, Direction); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + +#[derive(Clone, Copy)] +pub enum SearchMode { + WholeWord, + CaseSensitive, + Regex, +} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), + Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), + Binding::new("escape", Dismiss, Some("FindBar")), + Binding::new("cmd-f", FocusEditor, Some("FindBar")), + Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), + Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), + Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), + ]); + cx.add_action(FindBar::deploy); + cx.add_action(FindBar::dismiss); + cx.add_action(FindBar::focus_editor); + cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::go_to_match); + cx.add_action(FindBar::go_to_match_on_pane); +} + +struct FindBar { + settings: watch::Receiver, + query_editor: ViewHandle, + active_editor: Option>, + active_match_index: Option, + active_editor_subscription: Option, + highlighted_editors: HashSet>, + pending_search: Option>, + case_sensitive_mode: bool, + whole_word_mode: bool, + regex_mode: bool, + query_contains_error: bool, +} + +impl Entity for FindBar { + type Event = (); +} + +impl View for FindBar { + fn ui_name() -> &'static str { + "FindBar" + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx)) + .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx)) + .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx)) + .contained() + .with_style(theme.find.mode_button_group) + .aligned() + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) + .with_children(self.active_editor.as_ref().and_then(|editor| { + let (_, highlighted_ranges) = + editor.read(cx).highlighted_ranges_for_type::()?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, highlighted_ranges.len()) + } else { + "No matches".to_string() + }; + + Some( + Label::new(message, theme.find.match_index.text.clone()) + .contained() + .with_style(theme.find.match_index.container) + .aligned() + .boxed(), + ) + })) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } +} + +impl Toolbar for FindBar { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool { + self.active_editor_subscription.take(); + self.active_editor.take(); + self.pending_search.take(); + + if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(cx); + true + } else { + false + } + } + + fn on_dismiss(&mut self, cx: &mut ViewContext) { + self.active_editor.take(); + self.active_editor_subscription.take(); + self.active_match_index.take(); + self.pending_search.take(); + self.clear_matches(cx); + } +} + +impl FindBar { + fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.find.editor.input.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + + Self { + query_editor, + active_editor: None, + active_editor_subscription: None, + active_match_index: None, + highlighted_editors: Default::default(), + case_sensitive_mode: false, + whole_word_mode: false, + regex_mode: false, + settings, + pending_search: None, + query_contains_error: false, + } + } + + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.read(cx).len(); + query_buffer.edit([0..len], query, cx); + }); + }); + } + + fn render_mode_button( + &self, + icon: &str, + mode: SearchMode, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_mode_enabled(mode); + MouseEventHandler::new::((cx.view_id(), mode as usize), cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.mode_button, + (false, true) => &theme.hovered_mode_button, + (true, false) => &theme.active_mode_button, + (true, true) => &theme.active_hovered_mode_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + MouseEventHandler::new::( + (cx.view_id(), 10 + direction as usize), + cx, + |state, _| { + let style = if state.hovered { + &theme.hovered_mode_button + } else { + &theme.mode_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }, + ) + .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { + let settings = workspace.settings(); + workspace.active_pane().update(cx, |pane, cx| { + let findbar_was_visible = pane + .active_toolbar() + .map_or(false, |toolbar| toolbar.downcast::().is_some()); + + pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + + if let Some(find_bar) = pane + .active_toolbar() + .and_then(|toolbar| toolbar.downcast::()) + { + if !findbar_was_visible { + let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); + let display_map = editor + .update(cx, |editor, cx| editor.snapshot(cx)) + .display_snapshot; + let selection = editor + .read(cx) + .newest_selection::(&display_map.buffer_snapshot); + + let mut text: String; + if selection.start == selection.end { + let point = selection.start.to_display_point(&display_map); + let range = editor::movement::surrounding_word(&display_map, point); + let range = range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Right); + text = display_map.buffer_snapshot.text_for_range(range).collect(); + if text.trim().is_empty() { + text = String::new(); + } + } else { + text = display_map + .buffer_snapshot + .text_for_range(selection.start..selection.end) + .collect(); + } + + if !text.is_empty() { + find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + } + } + + if *focus { + if !findbar_was_visible { + let query_editor = find_bar.read(cx).query_editor.clone(); + query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&editor::SelectAll, cx); + }); + } + cx.focus(&find_bar); + } + } + }); + } + + fn dismiss(workspace: &mut Workspace, _: &Dismiss, cx: &mut ViewContext) { + workspace + .active_pane() + .update(cx, |pane, cx| pane.dismiss_toolbar(cx)); + } + + fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_editor.as_ref() { + cx.focus(active_editor); + } + } + + fn is_mode_enabled(&self, mode: SearchMode) -> bool { + match mode { + SearchMode::WholeWord => self.whole_word_mode, + SearchMode::CaseSensitive => self.case_sensitive_mode, + SearchMode::Regex => self.regex_mode, + } + } + + fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { + let value = match mode { + SearchMode::WholeWord => &mut self.whole_word_mode, + SearchMode::CaseSensitive => &mut self.case_sensitive_mode, + SearchMode::Regex => &mut self.regex_mode, + }; + *value = !*value; + self.update_matches(cx); + cx.notify(); + } + + fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + if let Some(editor) = self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + let newest_selection = editor.newest_anchor_selection().cloned(); + if let Some(((_, ranges), newest_selection)) = editor + .highlighted_ranges_for_type::() + .zip(newest_selection) + { + let position = newest_selection.head(); + let buffer = editor.buffer().read(cx).read(cx); + if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { + if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { + if *direction == Direction::Next { + index = 0; + } + } else if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if *direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + } + + let range_to_select = ranges[index].clone(); + drop(buffer); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + } + }); + } + } + } + + fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { + if let Some(find_bar) = pane.toolbar::() { + find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => { + self.query_contains_error = false; + self.clear_matches(cx); + self.update_matches(cx); + cx.notify(); + } + _ => {} + } + } + + fn on_active_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(cx), + editor::Event::SelectionsChanged => self.update_match_index(cx), + _ => {} + } + } + + fn clear_matches(&mut self, cx: &mut ViewContext) { + for editor in self.highlighted_editors.drain() { + if let Some(editor) = editor.upgrade(cx) { + if Some(&editor) != self.active_editor.as_ref() { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } + } + + fn update_matches(&mut self, cx: &mut ViewContext) { + let query = self.query_editor.read(cx).text(cx); + self.pending_search.take(); + if let Some(editor) = self.active_editor.as_ref() { + if query.is_empty() { + self.active_match_index.take(); + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } else { + let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); + let case_sensitive = self.case_sensitive_mode; + let whole_word = self.whole_word_mode; + let ranges = if self.regex_mode { + cx.background() + .spawn(regex_search(buffer, query, case_sensitive, whole_word)) + } else { + cx.background().spawn(async move { + Ok(search(buffer, query, case_sensitive, whole_word).await) + }) + }; + + let editor = editor.downgrade(); + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + match ranges.await { + Ok(ranges) => { + if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) { + this.update(&mut cx, |this, cx| { + this.highlighted_editors.insert(editor.downgrade()); + editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ) + }); + this.update_match_index(cx); + }); + } + } + Err(_) => { + this.update(&mut cx, |this, cx| { + this.query_contains_error = true; + cx.notify(); + }); + } + } + })); + } + } + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + self.active_match_index = self.active_match_index(cx); + cx.notify(); + } + + fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { + let editor = self.active_editor.as_ref()?; + let editor = editor.read(cx); + let position = editor.newest_anchor_selection()?.head(); + let ranges = editor.highlighted_ranges_for_type::()?.1; + if ranges.is_empty() { + None + } else { + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } + } +} + +const YIELD_INTERVAL: usize = 20000; + +async fn search( + buffer: MultiBufferSnapshot, + query: String, + case_sensitive: bool, + whole_word: bool, +) -> Vec> { + let mut ranges = Vec::new(); + + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + for (ix, mat) in search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + + if whole_word { + let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + + ranges +} + +async fn regex_search( + buffer: MultiBufferSnapshot, + mut query: String, + case_sensitive: bool, + whole_word: bool, +) -> Result>> { + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query; + } + + let mut ranges = Vec::new(); + + if query.contains("\n") || query.contains("\\n") { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(true) + .build()?; + for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + } else { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .build()?; + + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in buffer + .chunks(0..buffer.len(), None) + .map(|c| c.text) + .chain(["\n"]) + .enumerate() + { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + + Ok(ranges) +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; + use gpui::{color::Color, TestAppContext}; + use std::sync::Arc; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_find_simple(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.find.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + + let buffer = cx.update(|cx| { + MultiBuffer::build_simple( + &r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + cx, + ) + }); + let editor = cx.add_view(Default::default(), |cx| { + Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx) + }); + + let find_bar = cx.add_view(Default::default(), |cx| { + let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); + find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + find_bar + }); + + // Search for a string that appears with different casing. + // By default, search is case-insensitive. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("us", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ), + ] + ); + }); + + // Switch to a case sensitive search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("or", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + Color::red(), + ), + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + Color::red(), + ), + ( + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ( + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + Color::red(), + ), + ] + ); + }); + + // Switch to a whole word search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ] + ); + }); + + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + } +} diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 68d7424ae4b10b80782234710bc6b3a0bb7c4206..5a87aa6bc13d72eb82c61c1c8a16cbd0c8c3d33c 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -181,7 +181,7 @@ impl View for GoToLine { Container::new( Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.line_editor.id()).boxed()) + Container::new(ChildView::new(&self.line_editor).boxed()) .with_style(theme.input_editor.container) .boxed(), ) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 31efb4991ea73b1b83cc03fbdd6546acb4d126e2..3a2124de95e3a04e1e360dda59fe3e37c62b253d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3246,6 +3246,7 @@ impl Drop for AnyModelHandle { self.ref_counts.lock().dec_model(self.model_id); } } + pub struct WeakViewHandle { window_id: usize, view_id: usize, @@ -3288,6 +3289,21 @@ impl Clone for WeakViewHandle { } } +impl PartialEq for WeakViewHandle { + fn eq(&self, other: &Self) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + +impl Eq for WeakViewHandle {} + +impl Hash for WeakViewHandle { + fn hash(&self, state: &mut H) { + self.window_id.hash(state); + self.view_id.hash(state); + } +} + #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct ElementStateId(usize, usize); diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 354f0a0f821af81040581a7afedb2eb4652acbc2..2666a329f0613d26711589d3c05b94412845ea90 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -6,8 +6,8 @@ use crate::{ json::{self, ToJson}, platform::Event, text_layout::TextLayoutCache, - Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel, - ReadView, Scene, View, ViewHandle, + Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, + ReadModel, ReadView, Scene, View, ViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -462,8 +462,10 @@ pub struct ChildView { } impl ChildView { - pub fn new(view_id: usize) -> Self { - Self { view_id } + pub fn new(view: impl Into) -> Self { + Self { + view_id: view.into().id(), + } } } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 151e3ec0bb3187caea98d0fe0b8f8464baaae80f..b6921ce69f63c18d9cb92df28bbd61af97701de8 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -80,7 +80,7 @@ impl View for OutlineView { Flex::new(Axis::Vertical) .with_child( - Container::new(ChildView::new(self.query_editor.id()).boxed()) + Container::new(ChildView::new(&self.query_editor).boxed()) .with_style(settings.theme.selector.input_editor.container) .boxed(), ) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c3d37b950c86e3dad9e21b92a1e0a538ffe9e3eb..fec42853dcb146360c3211352260d66dd7035c0c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -24,6 +24,7 @@ pub struct Theme { pub project_panel: ProjectPanel, pub selector: Selector, pub editor: EditorStyle, + pub find: Find, pub project_diagnostics: ProjectDiagnostics, } @@ -37,6 +38,7 @@ pub struct Workspace { pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, + pub toolbar: Toolbar, } #[derive(Clone, Deserialize, Default)] @@ -87,6 +89,33 @@ pub struct Tab { pub icon_conflict: Color, } +#[derive(Clone, Deserialize, Default)] +pub struct Toolbar { + pub height: f32, +} + +#[derive(Clone, Deserialize, Default)] +pub struct Find { + #[serde(flatten)] + pub container: ContainerStyle, + pub editor: FindEditor, + pub invalid_editor: ContainerStyle, + pub mode_button_group: ContainerStyle, + pub mode_button: ContainedText, + pub active_mode_button: ContainedText, + pub hovered_mode_button: ContainedText, + pub active_hovered_mode_button: ContainedText, + pub match_background: Color, + pub match_index: ContainedText, +} + +#[derive(Clone, Deserialize, Default)] +pub struct FindEditor { + #[serde(flatten)] + pub input: InputEditorStyle, + pub max_width: f32, +} + #[derive(Deserialize, Default)] pub struct Sidebar { #[serde(flatten)] diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f359bd85ddbbe1f961df2b8b669d2a1ea3c1a9a1..5e536394e0b61b72dba1bcd470027cc00d0c5187 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -299,7 +299,7 @@ impl View for ThemeSelector { ConstrainedBox::new( Container::new( Flex::new(Axis::Vertical) - .with_child(ChildView::new(self.query_editor.id()).boxed()) + .with_child(ChildView::new(&self.query_editor).boxed()) .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed()) .boxed(), ) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 739d07aab11baa2fbdd51310256e696d80042e27..05b7b7e19b433b2db654607a85e53de97b29badb 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -7,11 +7,17 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle, - Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, + AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, + ViewHandle, }; use postage::watch; use project::ProjectPath; -use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + cmp, mem, + rc::Rc, +}; use util::ResultExt; action!(Split, SplitDirection); @@ -75,12 +81,29 @@ pub struct Pane { active_item_index: usize, settings: watch::Receiver, nav_history: Rc>, + toolbars: HashMap>, + active_toolbar_type: Option, + active_toolbar_visible: bool, +} + +pub trait Toolbar: View { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool; + fn on_dismiss(&mut self, cx: &mut ViewContext); } -// #[derive(Debug, Eq, PartialEq)] -// pub struct State { -// pub tabs: Vec, -// } +trait ToolbarHandle { + fn active_item_changed( + &self, + item: Option>, + cx: &mut MutableAppContext, + ) -> bool; + fn on_dismiss(&self, cx: &mut MutableAppContext); + fn to_any(&self) -> AnyViewHandle; +} pub struct ItemNavHistory { history: Rc>, @@ -120,6 +143,9 @@ impl Pane { active_item_index: 0, settings, nav_history: Default::default(), + toolbars: Default::default(), + active_toolbar_type: Default::default(), + active_toolbar_visible: false, } } @@ -288,9 +314,12 @@ impl Pane { pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { if index < self.item_views.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if prev_active_item_ix != self.active_item_index { + if prev_active_item_ix != self.active_item_index + && prev_active_item_ix < self.item_views.len() + { self.item_views[prev_active_item_ix].1.deactivated(cx); } + self.update_active_toolbar(cx); self.focus_active_item(cx); cx.notify(); } @@ -344,14 +373,19 @@ impl Pane { true } }); - self.active_item_index = cmp::min( - self.active_item_index, - self.item_views.len().saturating_sub(1), + self.activate_item( + cmp::min( + self.active_item_index, + self.item_views.len().saturating_sub(1), + ), + cx, ); if self.item_views.is_empty() { + self.update_active_toolbar(cx); cx.emit(Event::Remove); } + cx.notify(); } @@ -365,6 +399,68 @@ impl Pane { cx.emit(Event::Split(direction)); } + pub fn show_toolbar(&mut self, cx: &mut ViewContext, build_toolbar: F) + where + F: FnOnce(&mut ViewContext) -> V, + V: Toolbar, + { + let type_id = TypeId::of::(); + if self.active_toolbar_type != Some(type_id) { + self.dismiss_toolbar(cx); + + let active_item = self.active_item(); + self.toolbars + .entry(type_id) + .or_insert_with(|| Box::new(cx.add_view(build_toolbar))); + + self.active_toolbar_type = Some(type_id); + self.active_toolbar_visible = + self.toolbars[&type_id].active_item_changed(active_item, cx); + cx.notify(); + } + } + + pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext) { + if let Some(active_toolbar_type) = self.active_toolbar_type.take() { + self.toolbars + .get_mut(&active_toolbar_type) + .unwrap() + .on_dismiss(cx); + self.active_toolbar_visible = false; + self.focus_active_item(cx); + cx.notify(); + } + } + + pub fn toolbar(&self) -> Option> { + self.toolbars + .get(&TypeId::of::()) + .and_then(|toolbar| toolbar.to_any().downcast()) + } + + pub fn active_toolbar(&self) -> Option { + let type_id = self.active_toolbar_type?; + let toolbar = self.toolbars.get(&type_id)?; + if self.active_toolbar_visible { + Some(toolbar.to_any()) + } else { + None + } + } + + fn update_active_toolbar(&mut self, cx: &mut ViewContext) { + if let Some(type_id) = self.active_toolbar_type { + if let Some(toolbar) = self.toolbars.get(&type_id) { + self.active_toolbar_visible = toolbar.active_item_changed( + self.item_views + .get(self.active_item_index) + .map(|i| i.1.clone()), + cx, + ); + } + } + } + fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; @@ -516,7 +612,12 @@ impl View for Pane { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) - .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed()) + .with_children( + self.active_toolbar() + .as_ref() + .map(|view| ChildView::new(view).boxed()), + ) + .with_child(ChildView::new(active_item).flexible(1., true).boxed()) .named("pane") } else { Empty::new().named("pane") @@ -528,6 +629,24 @@ impl View for Pane { } } +impl ToolbarHandle for ViewHandle { + fn active_item_changed( + &self, + item: Option>, + cx: &mut MutableAppContext, + ) -> bool { + self.update(cx, |this, cx| this.active_item_changed(item, cx)) + } + + fn on_dismiss(&self, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| this.on_dismiss(cx)); + } + + fn to_any(&self) -> AnyViewHandle { + self.into() + } +} + impl ItemNavHistory { pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { Self { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index a2b3803b85b5354d6e4266a368f42db936b0c5eb..2b56a023fc2a7dbc423b177503c7117e6c02b044 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,43 +1,45 @@ use anyhow::{anyhow, Result}; -use gpui::{elements::*, Axis}; +use gpui::{elements::*, Axis, ViewHandle}; use theme::Theme; +use crate::Pane; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, } impl PaneGroup { - pub fn new(pane_id: usize) -> Self { + pub fn new(pane: ViewHandle) -> Self { Self { - root: Member::Pane(pane_id), + root: Member::Pane(pane), } } pub fn split( &mut self, - old_pane_id: usize, - new_pane_id: usize, + old_pane: &ViewHandle, + new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { match &mut self.root { - Member::Pane(pane_id) => { - if *pane_id == old_pane_id { - self.root = Member::new_axis(old_pane_id, new_pane_id, direction); + Member::Pane(pane) => { + if pane == old_pane { + self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); Ok(()) } else { Err(anyhow!("Pane not found")) } } - Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction), + Member::Axis(axis) => axis.split(old_pane, new_pane, direction), } } - pub fn remove(&mut self, pane_id: usize) -> Result { + pub fn remove(&mut self, pane: &ViewHandle) -> Result { match &mut self.root { Member::Pane(_) => Ok(false), Member::Axis(axis) => { - if let Some(last_pane) = axis.remove(pane_id)? { + if let Some(last_pane) = axis.remove(pane)? { self.root = last_pane; } Ok(true) @@ -53,11 +55,15 @@ impl PaneGroup { #[derive(Clone, Debug, Eq, PartialEq)] enum Member { Axis(PaneAxis), - Pane(usize), + Pane(ViewHandle), } impl Member { - fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self { + fn new_axis( + old_pane: ViewHandle, + new_pane: ViewHandle, + direction: SplitDirection, + ) -> Self { use Axis::*; use SplitDirection::*; @@ -67,16 +73,16 @@ impl Member { }; let members = match direction { - Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)], - Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)], + Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], + Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], }; Member::Axis(PaneAxis { axis, members }) } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { + pub fn render(&self, theme: &Theme) -> ElementBox { match self { - Member::Pane(view_id) => ChildView::new(*view_id).boxed(), + Member::Pane(pane) => ChildView::new(pane).boxed(), Member::Axis(axis) => axis.render(theme), } } @@ -91,8 +97,8 @@ struct PaneAxis { impl PaneAxis { fn split( &mut self, - old_pane_id: usize, - new_pane_id: usize, + old_pane: &ViewHandle, + new_pane: &ViewHandle, direction: SplitDirection, ) -> Result<()> { use SplitDirection::*; @@ -100,23 +106,24 @@ impl PaneAxis { for (idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { - if axis.split(old_pane_id, new_pane_id, direction).is_ok() { + if axis.split(old_pane, new_pane, direction).is_ok() { return Ok(()); } } - Member::Pane(pane_id) => { - if *pane_id == old_pane_id { + Member::Pane(pane) => { + if pane == old_pane { if direction.matches_axis(self.axis) { match direction { Up | Left => { - self.members.insert(idx, Member::Pane(new_pane_id)); + self.members.insert(idx, Member::Pane(new_pane.clone())); } Down | Right => { - self.members.insert(idx + 1, Member::Pane(new_pane_id)); + self.members.insert(idx + 1, Member::Pane(new_pane.clone())); } } } else { - *member = Member::new_axis(old_pane_id, new_pane_id, direction); + *member = + Member::new_axis(old_pane.clone(), new_pane.clone(), direction); } return Ok(()); } @@ -126,13 +133,13 @@ impl PaneAxis { Err(anyhow!("Pane not found")) } - fn remove(&mut self, pane_id_to_remove: usize) -> Result> { + fn remove(&mut self, pane_to_remove: &ViewHandle) -> Result> { let mut found_pane = false; let mut remove_member = None; for (idx, member) in self.members.iter_mut().enumerate() { match member { Member::Axis(axis) => { - if let Ok(last_pane) = axis.remove(pane_id_to_remove) { + if let Ok(last_pane) = axis.remove(pane_to_remove) { if let Some(last_pane) = last_pane { *member = last_pane; } @@ -140,8 +147,8 @@ impl PaneAxis { break; } } - Member::Pane(pane_id) => { - if *pane_id == pane_id_to_remove { + Member::Pane(pane) => { + if pane == pane_to_remove { found_pane = true; remove_member = Some(idx); break; diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs index 8d3d0f63709b895d720db31a921602bae01a95ca..7c0cb5c91184f0c892a926b5b359fe404b1aaeba 100644 --- a/crates/workspace/src/sidebar.rs +++ b/crates/workspace/src/sidebar.rs @@ -136,7 +136,7 @@ impl Sidebar { container.add_child( Hook::new( - ConstrainedBox::new(ChildView::new(active_item.id()).boxed()) + ConstrainedBox::new(ChildView::new(active_item).boxed()) .with_max_width(*self.width.borrow()) .boxed(), ) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 162394ed00edd9740b90641bb060ecb471c33491..2d26c33a8aa70e4ec44b358367751a69f6c86f4a 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,7 +1,7 @@ use crate::{ItemViewHandle, Pane, Settings}; use gpui::{ - elements::*, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, - ViewContext, ViewHandle, + elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, + View, ViewContext, ViewHandle, }; use postage::watch; @@ -14,7 +14,7 @@ pub trait StatusItemView: View { } trait StatusItemViewHandle { - fn id(&self) -> usize; + fn to_any(&self) -> AnyViewHandle; fn set_active_pane_item( &self, active_pane_item: Option<&dyn ItemViewHandle>, @@ -45,13 +45,13 @@ impl View for StatusBar { .with_children( self.left_items .iter() - .map(|i| ChildView::new(i.id()).aligned().boxed()), + .map(|i| ChildView::new(i.as_ref()).aligned().boxed()), ) .with_child(Empty::new().flexible(1., true).boxed()) .with_children( self.right_items .iter() - .map(|i| ChildView::new(i.id()).aligned().boxed()), + .map(|i| ChildView::new(i.as_ref()).aligned().boxed()), ) .contained() .with_style(theme.container) @@ -111,8 +111,8 @@ impl StatusBar { } impl StatusItemViewHandle for ViewHandle { - fn id(&self) -> usize { - self.id() + fn to_any(&self) -> AnyViewHandle { + self.into() } fn set_active_pane_item( @@ -125,3 +125,9 @@ impl StatusItemViewHandle for ViewHandle { }); } } + +impl Into for &dyn StatusItemViewHandle { + fn into(self) -> AnyViewHandle { + self.to_any() + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 48dcb4907bb6f3bce747034252ff8f2a612086a7..86d5271224d80e114eb4885f5f886d597e6c5133 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -599,7 +599,7 @@ impl Workspace { Workspace { modal: None, weak_self: cx.weak_handle(), - center: PaneGroup::new(pane.id()), + center: PaneGroup::new(pane.clone()), panes: vec![pane.clone()], active_pane: pane.clone(), status_bar, @@ -1048,15 +1048,13 @@ impl Workspace { new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } - self.center - .split(pane.id(), new_pane.id(), direction) - .unwrap(); + self.center.split(&pane, &new_pane, direction).unwrap(); cx.notify(); new_pane } fn remove_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { - if self.center.remove(pane.id()).unwrap() { + if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); } @@ -1287,7 +1285,7 @@ impl View for Workspace { Flexible::new(1., true, self.center.render(&settings.theme)) .boxed(), ) - .with_child(ChildView::new(self.status_bar.id()).boxed()) + .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) .boxed(), ); @@ -1298,7 +1296,7 @@ impl View for Workspace { content.add_child(self.right_sidebar.render(&settings, cx)); content.boxed() }) - .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) + .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed())) .flexible(1.0, true) .boxed(), ) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 21ad632bd2895ab0600a879b1d6834dc17b288d0..5ab0262288e27dc40e055e8790e56addeb43442a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,6 +36,7 @@ contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } +find = { path = "../find" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index b818b514a778d2365b93497073fdaa92dbe3aa89..93019db6e41218ad611e5b0bdb3f4817816537d9 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -81,6 +81,9 @@ item_spacing = 24 cursor_position = "$text.2" diagnostic_message = "$text.2" +[workspace.toolbar] +height = 44 + [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -318,3 +321,50 @@ status_bar_item = { extends = "$text.2", margin.right = 10 } tab_icon_width = 13 tab_icon_spacing = 4 tab_summary_spacing = 10 + +[find] +match_background = "$state.highlighted_line" +background = "$surface.1" + +[find.mode_button] +extends = "$text.1" +padding = { left = 6, right = 6, top = 1, bottom = 1 } +corner_radius = 6 +background = "$surface.1" +border = { width = 1, color = "$border.0" } +margin.left = 1 +margin.right = 1 + +[find.mode_button_group] +padding = { left = 2, right = 2 } + +[find.active_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.hovered_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.active_hovered_mode_button] +extends = "$find.mode_button" +background = "$surface.2" + +[find.match_index] +extends = "$text.1" +padding = 6 + +[find.editor] +max_width = 400 +background = "$surface.0" +corner_radius = 6 +padding = { left = 13, right = 13, top = 3, bottom = 3 } +margin = { top = 5, bottom = 5, left = 5, right = 5 } +text = "$text.0" +placeholder_text = "$text.2" +selection = "$selection.host" +border = { width = 1, color = "$border.0" } + +[find.invalid_editor] +extends = "$find.editor" +border = { width = 1, color = "$status.bad" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 59804cf87c1bc8e8a6a8f2ea12c44faabdc10a02..dd658255550ee6295733d9ff47899a2fd6cb2915 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -62,6 +62,7 @@ fn main() { outline::init(cx); project_panel::init(cx); diagnostics::init(cx); + find::init(cx); cx.spawn({ let client = client.clone(); |cx| async move {