From a65c1776d8226fa4483272d2e1b8e56d543041fa Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 3 Nov 2023 12:15:23 -0400 Subject: [PATCH 01/27] port rope2 to zed2 --- Cargo.lock | 19 +- crates/fs2/Cargo.toml | 2 +- crates/rope2/Cargo.toml | 21 + crates/rope2/src/offset_utf16.rs | 50 ++ crates/rope2/src/point.rs | 128 +++ crates/rope2/src/point_utf16.rs | 119 +++ crates/rope2/src/rope2.rs | 1433 ++++++++++++++++++++++++++++++ crates/rope2/src/unclipped.rs | 57 ++ crates/text2/Cargo.toml | 2 +- crates/zed2/Cargo.toml | 1 + 10 files changed, 1828 insertions(+), 4 deletions(-) create mode 100644 crates/rope2/Cargo.toml create mode 100644 crates/rope2/src/offset_utf16.rs create mode 100644 crates/rope2/src/point.rs create mode 100644 crates/rope2/src/point_utf16.rs create mode 100644 crates/rope2/src/rope2.rs create mode 100644 crates/rope2/src/unclipped.rs diff --git a/Cargo.lock b/Cargo.lock index d5d0493936..27126326a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3125,7 +3125,7 @@ dependencies = [ "log", "parking_lot 0.11.2", "regex", - "rope", + "rope2", "serde", "serde_derive", "serde_json", @@ -7106,6 +7106,20 @@ dependencies = [ "util", ] +[[package]] +name = "rope2" +version = "0.1.0" +dependencies = [ + "arrayvec 0.7.4", + "bromberg_sl2", + "gpui2", + "log", + "rand 0.8.5", + "smallvec", + "sum_tree", + "util", +] + [[package]] name = "roxmltree" version = "0.14.1" @@ -8911,7 +8925,7 @@ dependencies = [ "postage", "rand 0.8.5", "regex", - "rope", + "rope2", "smallvec", "sum_tree", "util", @@ -11189,6 +11203,7 @@ dependencies = [ "project2", "rand 0.8.5", "regex", + "rope2", "rpc2", "rsa 0.4.0", "rust-embed", diff --git a/crates/fs2/Cargo.toml b/crates/fs2/Cargo.toml index ca525afe5f..46ff7a274e 100644 --- a/crates/fs2/Cargo.toml +++ b/crates/fs2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/fs2.rs" [dependencies] collections = { path = "../collections" } -rope = { path = "../rope" } +rope = { package = "rope2", path = "../rope2" } text = { package = "text2", path = "../text2" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } diff --git a/crates/rope2/Cargo.toml b/crates/rope2/Cargo.toml new file mode 100644 index 0000000000..2ef247f418 --- /dev/null +++ b/crates/rope2/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "rope2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/rope2.rs" + +[dependencies] +bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" } +smallvec.workspace = true +sum_tree = { path = "../sum_tree" } +arrayvec = "0.7.1" +log.workspace = true +util = { path = "../util" } + +[dev-dependencies] +rand.workspace = true +util = { path = "../util", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/rope2/src/offset_utf16.rs b/crates/rope2/src/offset_utf16.rs new file mode 100644 index 0000000000..9a52b3c3f9 --- /dev/null +++ b/crates/rope2/src/offset_utf16.rs @@ -0,0 +1,50 @@ +use std::ops::{Add, AddAssign, Sub}; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct OffsetUtf16(pub usize); + +impl<'a> Add<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn add(self, other: &'a Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl Add for OffsetUtf16 { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + Self(self.0 + other.0) + } +} + +impl<'a> Sub<&'a Self> for OffsetUtf16 { + type Output = Self; + + fn sub(self, other: &'a Self) -> Self::Output { + debug_assert!(*other <= self); + Self(self.0 - other.0) + } +} + +impl Sub for OffsetUtf16 { + type Output = OffsetUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + Self(self.0 - other.0) + } +} + +impl<'a> AddAssign<&'a Self> for OffsetUtf16 { + fn add_assign(&mut self, other: &'a Self) { + self.0 += other.0; + } +} + +impl AddAssign for OffsetUtf16 { + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} diff --git a/crates/rope2/src/point.rs b/crates/rope2/src/point.rs new file mode 100644 index 0000000000..4be01dfdac --- /dev/null +++ b/crates/rope2/src/point.rs @@ -0,0 +1,128 @@ +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Sub}, +}; + +#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] +pub struct Point { + pub row: u32, + pub column: u32, +} + +impl Point { + pub const MAX: Self = Self { + row: u32::MAX, + column: u32::MAX, + }; + + pub fn new(row: u32, column: u32) -> Self { + Point { row, column } + } + + pub fn zero() -> Self { + Point::new(0, 0) + } + + pub fn parse_str(s: &str) -> Self { + let mut point = Self::zero(); + for (row, line) in s.split('\n').enumerate() { + point.row = row as u32; + point.column = line.len() as u32; + } + point + } + + pub fn is_zero(&self) -> bool { + self.row == 0 && self.column == 0 + } + + pub fn saturating_sub(self, other: Self) -> Self { + if self < other { + Self::zero() + } else { + self - other + } + } +} + +impl<'a> Add<&'a Self> for Point { + type Output = Point; + + fn add(self, other: &'a Self) -> Self::Output { + self + *other + } +} + +impl Add for Point { + type Output = Point; + + fn add(self, other: Self) -> Self::Output { + if other.row == 0 { + Point::new(self.row, self.column + other.column) + } else { + Point::new(self.row + other.row, other.column) + } + } +} + +impl<'a> Sub<&'a Self> for Point { + type Output = Point; + + fn sub(self, other: &'a Self) -> Self::Output { + self - *other + } +} + +impl Sub for Point { + type Output = Point; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + + if self.row == other.row { + Point::new(0, self.column - other.column) + } else { + Point::new(self.row - other.row, self.column) + } + } +} + +impl<'a> AddAssign<&'a Self> for Point { + fn add_assign(&mut self, other: &'a Self) { + *self += *other; + } +} + +impl AddAssign for Point { + fn add_assign(&mut self, other: Self) { + if other.row == 0 { + self.column += other.column; + } else { + self.row += other.row; + self.column = other.column; + } + } +} + +impl PartialOrd for Point { + fn partial_cmp(&self, other: &Point) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Point { + #[cfg(target_pointer_width = "64")] + fn cmp(&self, other: &Point) -> Ordering { + let a = (self.row as usize) << 32 | self.column as usize; + let b = (other.row as usize) << 32 | other.column as usize; + a.cmp(&b) + } + + #[cfg(target_pointer_width = "32")] + fn cmp(&self, other: &Point) -> Ordering { + match self.row.cmp(&other.row) { + Ordering::Equal => self.column.cmp(&other.column), + comparison @ _ => comparison, + } + } +} diff --git a/crates/rope2/src/point_utf16.rs b/crates/rope2/src/point_utf16.rs new file mode 100644 index 0000000000..7b76ee3776 --- /dev/null +++ b/crates/rope2/src/point_utf16.rs @@ -0,0 +1,119 @@ +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Sub}, +}; + +#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] +pub struct PointUtf16 { + pub row: u32, + pub column: u32, +} + +impl PointUtf16 { + pub const MAX: Self = Self { + row: u32::MAX, + column: u32::MAX, + }; + + pub fn new(row: u32, column: u32) -> Self { + PointUtf16 { row, column } + } + + pub fn zero() -> Self { + PointUtf16::new(0, 0) + } + + pub fn is_zero(&self) -> bool { + self.row == 0 && self.column == 0 + } + + pub fn saturating_sub(self, other: Self) -> Self { + if self < other { + Self::zero() + } else { + self - other + } + } +} + +impl<'a> Add<&'a Self> for PointUtf16 { + type Output = PointUtf16; + + fn add(self, other: &'a Self) -> Self::Output { + self + *other + } +} + +impl Add for PointUtf16 { + type Output = PointUtf16; + + fn add(self, other: Self) -> Self::Output { + if other.row == 0 { + PointUtf16::new(self.row, self.column + other.column) + } else { + PointUtf16::new(self.row + other.row, other.column) + } + } +} + +impl<'a> Sub<&'a Self> for PointUtf16 { + type Output = PointUtf16; + + fn sub(self, other: &'a Self) -> Self::Output { + self - *other + } +} + +impl Sub for PointUtf16 { + type Output = PointUtf16; + + fn sub(self, other: Self) -> Self::Output { + debug_assert!(other <= self); + + if self.row == other.row { + PointUtf16::new(0, self.column - other.column) + } else { + PointUtf16::new(self.row - other.row, self.column) + } + } +} + +impl<'a> AddAssign<&'a Self> for PointUtf16 { + fn add_assign(&mut self, other: &'a Self) { + *self += *other; + } +} + +impl AddAssign for PointUtf16 { + fn add_assign(&mut self, other: Self) { + if other.row == 0 { + self.column += other.column; + } else { + self.row += other.row; + self.column = other.column; + } + } +} + +impl PartialOrd for PointUtf16 { + fn partial_cmp(&self, other: &PointUtf16) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PointUtf16 { + #[cfg(target_pointer_width = "64")] + fn cmp(&self, other: &PointUtf16) -> Ordering { + let a = (self.row as usize) << 32 | self.column as usize; + let b = (other.row as usize) << 32 | other.column as usize; + a.cmp(&b) + } + + #[cfg(target_pointer_width = "32")] + fn cmp(&self, other: &PointUtf16) -> Ordering { + match self.row.cmp(&other.row) { + Ordering::Equal => self.column.cmp(&other.column), + comparison @ _ => comparison, + } + } +} diff --git a/crates/rope2/src/rope2.rs b/crates/rope2/src/rope2.rs new file mode 100644 index 0000000000..9c764c468e --- /dev/null +++ b/crates/rope2/src/rope2.rs @@ -0,0 +1,1433 @@ +mod offset_utf16; +mod point; +mod point_utf16; +mod unclipped; + +use arrayvec::ArrayString; +use bromberg_sl2::HashMatrix; +use smallvec::SmallVec; +use std::{ + cmp, fmt, io, mem, + ops::{AddAssign, Range}, + str, +}; +use sum_tree::{Bias, Dimension, SumTree}; +use util::debug_panic; + +pub use offset_utf16::OffsetUtf16; +pub use point::Point; +pub use point_utf16::PointUtf16; +pub use unclipped::Unclipped; + +#[cfg(test)] +const CHUNK_BASE: usize = 6; + +#[cfg(not(test))] +const CHUNK_BASE: usize = 16; + +/// Type alias to [HashMatrix], an implementation of a homomorphic hash function. Two [Rope] instances +/// containing the same text will produce the same fingerprint. This hash function is special in that +/// it allows us to hash individual chunks and aggregate them up the [Rope]'s tree, with the resulting +/// hash being equivalent to hashing all the text contained in the [Rope] at once. +pub type RopeFingerprint = HashMatrix; + +#[derive(Clone, Default)] +pub struct Rope { + chunks: SumTree, +} + +impl Rope { + pub fn new() -> Self { + Self::default() + } + + pub fn append(&mut self, rope: Rope) { + let mut chunks = rope.chunks.cursor::<()>(); + chunks.next(&()); + if let Some(chunk) = chunks.item() { + if self.chunks.last().map_or(false, |c| c.0.len() < CHUNK_BASE) + || chunk.0.len() < CHUNK_BASE + { + self.push(&chunk.0); + chunks.next(&()); + } + } + + self.chunks.append(chunks.suffix(&()), &()); + self.check_invariants(); + } + + pub fn replace(&mut self, range: Range, text: &str) { + let mut new_rope = Rope::new(); + let mut cursor = self.cursor(0); + new_rope.append(cursor.slice(range.start)); + cursor.seek_forward(range.end); + new_rope.push(text); + new_rope.append(cursor.suffix()); + *self = new_rope; + } + + pub fn slice(&self, range: Range) -> Rope { + let mut cursor = self.cursor(0); + cursor.seek_forward(range.start); + cursor.slice(range.end) + } + + pub fn slice_rows(&self, range: Range) -> Rope { + //This would be more efficient with a forward advance after the first, but it's fine + let start = self.point_to_offset(Point::new(range.start, 0)); + let end = self.point_to_offset(Point::new(range.end, 0)); + self.slice(start..end) + } + + pub fn push(&mut self, text: &str) { + let mut new_chunks = SmallVec::<[_; 16]>::new(); + let mut new_chunk = ArrayString::new(); + for ch in text.chars() { + if new_chunk.len() + ch.len_utf8() > 2 * CHUNK_BASE { + new_chunks.push(Chunk(new_chunk)); + new_chunk = ArrayString::new(); + } + + new_chunk.push(ch); + } + if !new_chunk.is_empty() { + new_chunks.push(Chunk(new_chunk)); + } + + let mut new_chunks = new_chunks.into_iter(); + let mut first_new_chunk = new_chunks.next(); + self.chunks.update_last( + |last_chunk| { + if let Some(first_new_chunk_ref) = first_new_chunk.as_mut() { + if last_chunk.0.len() + first_new_chunk_ref.0.len() <= 2 * CHUNK_BASE { + last_chunk.0.push_str(&first_new_chunk.take().unwrap().0); + } else { + let mut text = ArrayString::<{ 4 * CHUNK_BASE }>::new(); + text.push_str(&last_chunk.0); + text.push_str(&first_new_chunk_ref.0); + let (left, right) = text.split_at(find_split_ix(&text)); + last_chunk.0.clear(); + last_chunk.0.push_str(left); + first_new_chunk_ref.0.clear(); + first_new_chunk_ref.0.push_str(right); + } + } + }, + &(), + ); + + self.chunks + .extend(first_new_chunk.into_iter().chain(new_chunks), &()); + self.check_invariants(); + } + + pub fn push_front(&mut self, text: &str) { + let suffix = mem::replace(self, Rope::from(text)); + self.append(suffix); + } + + fn check_invariants(&self) { + #[cfg(test)] + { + // Ensure all chunks except maybe the last one are not underflowing. + // Allow some wiggle room for multibyte characters at chunk boundaries. + let mut chunks = self.chunks.cursor::<()>().peekable(); + while let Some(chunk) = chunks.next() { + if chunks.peek().is_some() { + assert!(chunk.0.len() + 3 >= CHUNK_BASE); + } + } + } + } + + pub fn summary(&self) -> TextSummary { + self.chunks.summary().text.clone() + } + + pub fn len(&self) -> usize { + self.chunks.extent(&()) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn max_point(&self) -> Point { + self.chunks.extent(&()) + } + + pub fn max_point_utf16(&self) -> PointUtf16 { + self.chunks.extent(&()) + } + + pub fn cursor(&self, offset: usize) -> Cursor { + Cursor::new(self, offset) + } + + pub fn chars(&self) -> impl Iterator + '_ { + self.chars_at(0) + } + + pub fn chars_at(&self, start: usize) -> impl Iterator + '_ { + self.chunks_in_range(start..self.len()).flat_map(str::chars) + } + + pub fn reversed_chars_at(&self, start: usize) -> impl Iterator + '_ { + self.reversed_chunks_in_range(0..start) + .flat_map(|chunk| chunk.chars().rev()) + } + + pub fn bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, false) + } + + pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes { + Bytes::new(self, range, true) + } + + pub fn chunks(&self) -> Chunks { + self.chunks_in_range(0..self.len()) + } + + pub fn chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, false) + } + + pub fn reversed_chunks_in_range(&self, range: Range) -> Chunks { + Chunks::new(self, range, true) + } + + pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { + if offset >= self.summary().len { + return self.summary().len_utf16; + } + let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_to_offset_utf16(overshoot) + }) + } + + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { + if offset >= self.summary().len_utf16 { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Default::default(), |chunk| { + chunk.offset_utf16_to_offset(overshoot) + }) + } + + pub fn offset_to_point(&self, offset: usize) -> Point { + if offset >= self.summary().len { + return self.summary().lines; + } + let mut cursor = self.chunks.cursor::<(usize, Point)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(Point::zero(), |chunk| chunk.offset_to_point(overshoot)) + } + + pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { + if offset >= self.summary().len { + return self.summary().lines_utf16(); + } + let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); + cursor.seek(&offset, Bias::Left, &()); + let overshoot = offset - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(PointUtf16::zero(), |chunk| { + chunk.offset_to_point_utf16(overshoot) + }) + } + + pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { + if point >= self.summary().lines { + return self.summary().lines_utf16(); + } + let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(PointUtf16::zero(), |chunk| { + chunk.point_to_point_utf16(overshoot) + }) + } + + pub fn point_to_offset(&self, point: Point) -> usize { + if point >= self.summary().lines { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(Point, usize)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(0, |chunk| chunk.point_to_offset(overshoot)) + } + + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + self.point_utf16_to_offset_impl(point, false) + } + + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { + self.point_utf16_to_offset_impl(point.0, true) + } + + fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize { + if point >= self.summary().lines_utf16() { + return self.summary().len; + } + let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); + cursor.seek(&point, Bias::Left, &()); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor + .item() + .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip)) + } + + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { + if point.0 >= self.summary().lines_utf16() { + return self.summary().lines; + } + let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(); + cursor.seek(&point.0, Bias::Left, &()); + let overshoot = Unclipped(point.0 - cursor.start().0); + cursor.start().1 + + cursor.item().map_or(Point::zero(), |chunk| { + chunk.unclipped_point_utf16_to_point(overshoot) + }) + } + + pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Left, &()); + if let Some(chunk) = cursor.item() { + let mut ix = offset - cursor.start(); + while !chunk.0.is_char_boundary(ix) { + match bias { + Bias::Left => { + ix -= 1; + offset -= 1; + } + Bias::Right => { + ix += 1; + offset += 1; + } + } + } + offset + } else { + self.summary().len + } + } + + pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = offset - cursor.start(); + *cursor.start() + chunk.clip_offset_utf16(overshoot, bias) + } else { + self.summary().len_utf16 + } + } + + pub fn clip_point(&self, point: Point, bias: Bias) -> Point { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&point, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = point - cursor.start(); + *cursor.start() + chunk.clip_point(overshoot, bias) + } else { + self.summary().lines + } + } + + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { + let mut cursor = self.chunks.cursor::(); + cursor.seek(&point.0, Bias::Right, &()); + if let Some(chunk) = cursor.item() { + let overshoot = Unclipped(point.0 - cursor.start()); + *cursor.start() + chunk.clip_point_utf16(overshoot, bias) + } else { + self.summary().lines_utf16() + } + } + + pub fn line_len(&self, row: u32) -> u32 { + self.clip_point(Point::new(row, u32::MAX), Bias::Left) + .column + } + + pub fn fingerprint(&self) -> RopeFingerprint { + self.chunks.summary().fingerprint + } +} + +impl<'a> From<&'a str> for Rope { + fn from(text: &'a str) -> Self { + let mut rope = Self::new(); + rope.push(text); + rope + } +} + +impl<'a> FromIterator<&'a str> for Rope { + fn from_iter>(iter: T) -> Self { + let mut rope = Rope::new(); + for chunk in iter { + rope.push(chunk); + } + rope + } +} + +impl From for Rope { + fn from(text: String) -> Self { + Rope::from(text.as_str()) + } +} + +impl fmt::Display for Rope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for chunk in self.chunks() { + write!(f, "{}", chunk)?; + } + Ok(()) + } +} + +impl fmt::Debug for Rope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use std::fmt::Write as _; + + write!(f, "\"")?; + let mut format_string = String::new(); + for chunk in self.chunks() { + write!(&mut format_string, "{:?}", chunk)?; + write!(f, "{}", &format_string[1..format_string.len() - 1])?; + format_string.clear(); + } + write!(f, "\"")?; + Ok(()) + } +} + +pub struct Cursor<'a> { + rope: &'a Rope, + chunks: sum_tree::Cursor<'a, Chunk, usize>, + offset: usize, +} + +impl<'a> Cursor<'a> { + pub fn new(rope: &'a Rope, offset: usize) -> Self { + let mut chunks = rope.chunks.cursor(); + chunks.seek(&offset, Bias::Right, &()); + Self { + rope, + chunks, + offset, + } + } + + pub fn seek_forward(&mut self, end_offset: usize) { + debug_assert!(end_offset >= self.offset); + + self.chunks.seek_forward(&end_offset, Bias::Right, &()); + self.offset = end_offset; + } + + pub fn slice(&mut self, end_offset: usize) -> Rope { + debug_assert!( + end_offset >= self.offset, + "cannot slice backwards from {} to {}", + self.offset, + end_offset + ); + + let mut slice = Rope::new(); + if let Some(start_chunk) = self.chunks.item() { + let start_ix = self.offset - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + slice.push(&start_chunk.0[start_ix..end_ix]); + } + + if end_offset > self.chunks.end(&()) { + self.chunks.next(&()); + slice.append(Rope { + chunks: self.chunks.slice(&end_offset, Bias::Right, &()), + }); + if let Some(end_chunk) = self.chunks.item() { + let end_ix = end_offset - self.chunks.start(); + slice.push(&end_chunk.0[..end_ix]); + } + } + + self.offset = end_offset; + slice + } + + pub fn summary(&mut self, end_offset: usize) -> D { + debug_assert!(end_offset >= self.offset); + + let mut summary = D::default(); + if let Some(start_chunk) = self.chunks.item() { + let start_ix = self.offset - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + summary.add_assign(&D::from_text_summary(&TextSummary::from( + &start_chunk.0[start_ix..end_ix], + ))); + } + + if end_offset > self.chunks.end(&()) { + self.chunks.next(&()); + summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &())); + if let Some(end_chunk) = self.chunks.item() { + let end_ix = end_offset - self.chunks.start(); + summary.add_assign(&D::from_text_summary(&TextSummary::from( + &end_chunk.0[..end_ix], + ))); + } + } + + self.offset = end_offset; + summary + } + + pub fn suffix(mut self) -> Rope { + self.slice(self.rope.chunks.extent(&())) + } + + pub fn offset(&self) -> usize { + self.offset + } +} + +pub struct Chunks<'a> { + chunks: sum_tree::Cursor<'a, Chunk, usize>, + range: Range, + reversed: bool, +} + +impl<'a> Chunks<'a> { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { + let mut chunks = rope.chunks.cursor(); + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } + } + + pub fn offset(&self) -> usize { + if self.reversed { + self.range.end.min(self.chunks.end(&())) + } else { + self.range.start.max(*self.chunks.start()) + } + } + + pub fn seek(&mut self, offset: usize) { + let bias = if self.reversed { + Bias::Left + } else { + Bias::Right + }; + + if offset >= self.chunks.end(&()) { + self.chunks.seek_forward(&offset, bias, &()); + } else { + self.chunks.seek(&offset, bias, &()); + } + + if self.reversed { + self.range.end = offset; + } else { + self.range.start = offset; + } + } + + pub fn peek(&self) -> Option<&'a str> { + let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } + let chunk_start = *self.chunks.start(); + if self.range.end <= chunk_start { + return None; + } + + let start = self.range.start.saturating_sub(chunk_start); + let end = self.range.end - chunk_start; + Some(&chunk.0[start..chunk.0.len().min(end)]) + } +} + +impl<'a> Iterator for Chunks<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let result = self.peek(); + if result.is_some() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + result + } +} + +pub struct Bytes<'a> { + chunks: sum_tree::Cursor<'a, Chunk, usize>, + range: Range, + reversed: bool, +} + +impl<'a> Bytes<'a> { + pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { + let mut chunks = rope.chunks.cursor(); + if reversed { + chunks.seek(&range.end, Bias::Left, &()); + } else { + chunks.seek(&range.start, Bias::Right, &()); + } + Self { + chunks, + range, + reversed, + } + } + + pub fn peek(&self) -> Option<&'a [u8]> { + let chunk = self.chunks.item()?; + if self.reversed && self.range.start >= self.chunks.end(&()) { + return None; + } + let chunk_start = *self.chunks.start(); + if self.range.end <= chunk_start { + return None; + } + let start = self.range.start.saturating_sub(chunk_start); + let end = self.range.end - chunk_start; + Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)]) + } +} + +impl<'a> Iterator for Bytes<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + let result = self.peek(); + if result.is_some() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + result + } +} + +impl<'a> io::Read for Bytes<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if let Some(chunk) = self.peek() { + let len = cmp::min(buf.len(), chunk.len()); + if self.reversed { + buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); + buf[..len].reverse(); + self.range.end -= len; + } else { + buf[..len].copy_from_slice(&chunk[..len]); + self.range.start += len; + } + + if len == chunk.len() { + if self.reversed { + self.chunks.prev(&()); + } else { + self.chunks.next(&()); + } + } + Ok(len) + } else { + Ok(0) + } + } +} + +#[derive(Clone, Debug, Default)] +struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>); + +impl Chunk { + fn offset_to_offset_utf16(&self, target: usize) -> OffsetUtf16 { + let mut offset = 0; + let mut offset_utf16 = OffsetUtf16(0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset_utf16 + } + + fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { + let mut offset_utf16 = OffsetUtf16(0); + let mut offset = 0; + for ch in self.0.chars() { + if offset_utf16 >= target { + break; + } + + offset += ch.len_utf8(); + offset_utf16.0 += ch.len_utf16(); + } + offset + } + + fn offset_to_point(&self, target: usize) -> Point { + let mut offset = 0; + let mut point = Point::new(0, 0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + } else { + point.column += ch.len_utf8() as u32; + } + offset += ch.len_utf8(); + } + point + } + + fn offset_to_point_utf16(&self, target: usize) -> PointUtf16 { + let mut offset = 0; + let mut point = PointUtf16::new(0, 0); + for ch in self.0.chars() { + if offset >= target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + } else { + point.column += ch.len_utf16() as u32; + } + offset += ch.len_utf8(); + } + point + } + + fn point_to_offset(&self, target: Point) -> usize { + let mut offset = 0; + let mut point = Point::new(0, 0); + + for ch in self.0.chars() { + if point >= target { + if point > target { + debug_panic!("point {target:?} is inside of character {ch:?}"); + } + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + + if point.row > target.row { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + break; + } + } else { + point.column += ch.len_utf8() as u32; + } + + offset += ch.len_utf8(); + } + + offset + } + + fn point_to_point_utf16(&self, target: Point) -> PointUtf16 { + let mut point = Point::zero(); + let mut point_utf16 = PointUtf16::new(0, 0); + for ch in self.0.chars() { + if point >= target { + break; + } + + if ch == '\n' { + point_utf16.row += 1; + point_utf16.column = 0; + point.row += 1; + point.column = 0; + } else { + point_utf16.column += ch.len_utf16() as u32; + point.column += ch.len_utf8() as u32; + } + } + point_utf16 + } + + fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize { + let mut offset = 0; + let mut point = PointUtf16::new(0, 0); + + for ch in self.0.chars() { + if point == target { + break; + } + + if ch == '\n' { + point.row += 1; + point.column = 0; + + if point.row > target.row { + if !clip { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + } + // Return the offset of the newline + return offset; + } + } else { + point.column += ch.len_utf16() as u32; + } + + if point > target { + if !clip { + debug_panic!("point {target:?} is inside of codepoint {ch:?}"); + } + // Return the offset of the codepoint which we have landed within, bias left + return offset; + } + + offset += ch.len_utf8(); + } + + offset + } + + fn unclipped_point_utf16_to_point(&self, target: Unclipped) -> Point { + let mut point = Point::zero(); + let mut point_utf16 = PointUtf16::zero(); + + for ch in self.0.chars() { + if point_utf16 == target.0 { + break; + } + + if point_utf16 > target.0 { + // If the point is past the end of a line or inside of a code point, + // return the last valid point before the target. + return point; + } + + if ch == '\n' { + point_utf16 += PointUtf16::new(1, 0); + point += Point::new(1, 0); + } else { + point_utf16 += PointUtf16::new(0, ch.len_utf16() as u32); + point += Point::new(0, ch.len_utf8() as u32); + } + } + + point + } + + fn clip_point(&self, target: Point, bias: Bias) -> Point { + for (row, line) in self.0.split('\n').enumerate() { + if row == target.row as usize { + let mut column = target.column.min(line.len() as u32); + while !line.is_char_boundary(column as usize) { + match bias { + Bias::Left => column -= 1, + Bias::Right => column += 1, + } + } + return Point::new(row as u32, column); + } + } + unreachable!() + } + + fn clip_point_utf16(&self, target: Unclipped, bias: Bias) -> PointUtf16 { + for (row, line) in self.0.split('\n').enumerate() { + if row == target.0.row as usize { + let mut code_units = line.encode_utf16(); + let mut column = code_units.by_ref().take(target.0.column as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => column -= 1, + Bias::Right => column += 1, + } + } + return PointUtf16::new(row as u32, column as u32); + } + } + unreachable!() + } + + fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { + let mut code_units = self.0.encode_utf16(); + let mut offset = code_units.by_ref().take(target.0 as usize).count(); + if char::decode_utf16(code_units).next().transpose().is_err() { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + OffsetUtf16(offset) + } +} + +impl sum_tree::Item for Chunk { + type Summary = ChunkSummary; + + fn summary(&self) -> Self::Summary { + ChunkSummary::from(self.0.as_str()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ChunkSummary { + text: TextSummary, + fingerprint: RopeFingerprint, +} + +impl<'a> From<&'a str> for ChunkSummary { + fn from(text: &'a str) -> Self { + Self { + text: TextSummary::from(text), + fingerprint: bromberg_sl2::hash_strict(text.as_bytes()), + } + } +} + +impl sum_tree::Summary for ChunkSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + self.text += &summary.text; + self.fingerprint = self.fingerprint * summary.fingerprint; + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TextSummary { + pub len: usize, + pub len_utf16: OffsetUtf16, + pub lines: Point, + pub first_line_chars: u32, + pub last_line_chars: u32, + pub last_line_len_utf16: u32, + pub longest_row: u32, + pub longest_row_chars: u32, +} + +impl TextSummary { + pub fn lines_utf16(&self) -> PointUtf16 { + PointUtf16 { + row: self.lines.row, + column: self.last_line_len_utf16, + } + } +} + +impl<'a> From<&'a str> for TextSummary { + fn from(text: &'a str) -> Self { + let mut len_utf16 = OffsetUtf16(0); + let mut lines = Point::new(0, 0); + let mut first_line_chars = 0; + let mut last_line_chars = 0; + let mut last_line_len_utf16 = 0; + let mut longest_row = 0; + let mut longest_row_chars = 0; + for c in text.chars() { + len_utf16.0 += c.len_utf16(); + + if c == '\n' { + lines += Point::new(1, 0); + last_line_len_utf16 = 0; + last_line_chars = 0; + } else { + lines.column += c.len_utf8() as u32; + last_line_len_utf16 += c.len_utf16() as u32; + last_line_chars += 1; + } + + if lines.row == 0 { + first_line_chars = last_line_chars; + } + + if last_line_chars > longest_row_chars { + longest_row = lines.row; + longest_row_chars = last_line_chars; + } + } + + TextSummary { + len: text.len(), + len_utf16, + lines, + first_line_chars, + last_line_chars, + last_line_len_utf16, + longest_row, + longest_row_chars, + } + } +} + +impl sum_tree::Summary for TextSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &Self::Context) { + *self += summary; + } +} + +impl std::ops::Add for TextSummary { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + AddAssign::add_assign(&mut self, &rhs); + self + } +} + +impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { + fn add_assign(&mut self, other: &'a Self) { + let joined_chars = self.last_line_chars + other.first_line_chars; + if joined_chars > self.longest_row_chars { + self.longest_row = self.lines.row; + self.longest_row_chars = joined_chars; + } + if other.longest_row_chars > self.longest_row_chars { + self.longest_row = self.lines.row + other.longest_row; + self.longest_row_chars = other.longest_row_chars; + } + + if self.lines.row == 0 { + self.first_line_chars += other.first_line_chars; + } + + if other.lines.row == 0 { + self.last_line_chars += other.first_line_chars; + self.last_line_len_utf16 += other.last_line_len_utf16; + } else { + self.last_line_chars = other.last_line_chars; + self.last_line_len_utf16 = other.last_line_len_utf16; + } + + self.len += other.len; + self.len_utf16 += other.len_utf16; + self.lines += other.lines; + } +} + +impl std::ops::AddAssign for TextSummary { + fn add_assign(&mut self, other: Self) { + *self += &other; + } +} + +pub trait TextDimension: 'static + for<'a> Dimension<'a, ChunkSummary> { + fn from_text_summary(summary: &TextSummary) -> Self; + fn add_assign(&mut self, other: &Self); +} + +impl TextDimension for (D1, D2) { + fn from_text_summary(summary: &TextSummary) -> Self { + ( + D1::from_text_summary(summary), + D2::from_text_summary(summary), + ) + } + + fn add_assign(&mut self, other: &Self) { + self.0.add_assign(&other.0); + self.1.add_assign(&other.1); + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += &summary.text; + } +} + +impl TextDimension for TextSummary { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.clone() + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.len; + } +} + +impl TextDimension for usize { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.len + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.len_utf16; + } +} + +impl TextDimension for OffsetUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.len_utf16 + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for Point { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.lines; + } +} + +impl TextDimension for Point { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.lines + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + *self += summary.text.lines_utf16(); + } +} + +impl TextDimension for PointUtf16 { + fn from_text_summary(summary: &TextSummary) -> Self { + summary.lines_utf16() + } + + fn add_assign(&mut self, other: &Self) { + *self += other; + } +} + +fn find_split_ix(text: &str) -> usize { + let mut ix = text.len() / 2; + while !text.is_char_boundary(ix) { + if ix < 2 * CHUNK_BASE { + ix += 1; + } else { + ix = (text.len() / 2) - 1; + break; + } + } + while !text.is_char_boundary(ix) { + ix -= 1; + } + + debug_assert!(ix <= 2 * CHUNK_BASE); + debug_assert!(text.len() - ix <= 2 * CHUNK_BASE); + ix +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::prelude::*; + use std::{cmp::Ordering, env, io::Read}; + use util::RandomCharIter; + use Bias::{Left, Right}; + + #[test] + fn test_all_4_byte_chars() { + let mut rope = Rope::new(); + let text = "🏀".repeat(256); + rope.push(&text); + assert_eq!(rope.text(), text); + } + + #[test] + fn test_clip() { + let rope = Rope::from("🧘"); + + assert_eq!(rope.clip_offset(1, Bias::Left), 0); + assert_eq!(rope.clip_offset(1, Bias::Right), 4); + assert_eq!(rope.clip_offset(5, Bias::Right), 4); + + assert_eq!( + rope.clip_point(Point::new(0, 1), Bias::Left), + Point::new(0, 0) + ); + assert_eq!( + rope.clip_point(Point::new(0, 1), Bias::Right), + Point::new(0, 4) + ); + assert_eq!( + rope.clip_point(Point::new(0, 5), Bias::Right), + Point::new(0, 4) + ); + + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left), + PointUtf16::new(0, 0) + ); + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right), + PointUtf16::new(0, 2) + ); + assert_eq!( + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right), + PointUtf16::new(0, 2) + ); + + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), + OffsetUtf16(0) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), + OffsetUtf16(2) + ); + assert_eq!( + rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), + OffsetUtf16(2) + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_rope(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut expected = String::new(); + let mut actual = Rope::new(); + for _ in 0..operations { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + let len = rng.gen_range(0..=64); + let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); + + let mut new_actual = Rope::new(); + let mut cursor = actual.cursor(0); + new_actual.append(cursor.slice(start_ix)); + new_actual.push(&new_text); + cursor.seek_forward(end_ix); + new_actual.append(cursor.suffix()); + actual = new_actual; + + expected.replace_range(start_ix..end_ix, &new_text); + + assert_eq!(actual.text(), expected); + log::info!("text: {:?}", expected); + + for _ in 0..5 { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + + let actual_text = actual.chunks_in_range(start_ix..end_ix).collect::(); + assert_eq!(actual_text, &expected[start_ix..end_ix]); + + let mut actual_text = String::new(); + actual + .bytes_in_range(start_ix..end_ix) + .read_to_string(&mut actual_text) + .unwrap(); + assert_eq!(actual_text, &expected[start_ix..end_ix]); + + assert_eq!( + actual + .reversed_chunks_in_range(start_ix..end_ix) + .collect::>() + .into_iter() + .rev() + .collect::(), + &expected[start_ix..end_ix] + ); + } + + let mut offset_utf16 = OffsetUtf16(0); + let mut point = Point::new(0, 0); + let mut point_utf16 = PointUtf16::new(0, 0); + for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { + assert_eq!(actual.offset_to_point(ix), point, "offset_to_point({})", ix); + assert_eq!( + actual.offset_to_point_utf16(ix), + point_utf16, + "offset_to_point_utf16({})", + ix + ); + assert_eq!( + actual.point_to_offset(point), + ix, + "point_to_offset({:?})", + point + ); + assert_eq!( + actual.point_utf16_to_offset(point_utf16), + ix, + "point_utf16_to_offset({:?})", + point_utf16 + ); + assert_eq!( + actual.offset_to_offset_utf16(ix), + offset_utf16, + "offset_to_offset_utf16({:?})", + ix + ); + assert_eq!( + actual.offset_utf16_to_offset(offset_utf16), + ix, + "offset_utf16_to_offset({:?})", + offset_utf16 + ); + if ch == '\n' { + point += Point::new(1, 0); + point_utf16 += PointUtf16::new(1, 0); + } else { + point.column += ch.len_utf8() as u32; + point_utf16.column += ch.len_utf16() as u32; + } + offset_utf16.0 += ch.len_utf16(); + } + + let mut offset_utf16 = OffsetUtf16(0); + let mut point_utf16 = Unclipped(PointUtf16::zero()); + for unit in expected.encode_utf16() { + let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); + let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); + assert!(right_offset >= left_offset); + // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. + actual.offset_utf16_to_offset(left_offset); + actual.offset_utf16_to_offset(right_offset); + + let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); + let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); + assert!(right_point >= left_point); + // Ensure translating valid UTF-16 points to offsets doesn't panic. + actual.point_utf16_to_offset(left_point); + actual.point_utf16_to_offset(right_point); + + offset_utf16.0 += 1; + if unit == b'\n' as u16 { + point_utf16.0 += PointUtf16::new(1, 0); + } else { + point_utf16.0 += PointUtf16::new(0, 1); + } + } + + for _ in 0..5 { + let end_ix = clip_offset(&expected, rng.gen_range(0..=expected.len()), Right); + let start_ix = clip_offset(&expected, rng.gen_range(0..=end_ix), Left); + assert_eq!( + actual.cursor(start_ix).summary::(end_ix), + TextSummary::from(&expected[start_ix..end_ix]) + ); + } + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected.split('\n').enumerate() { + let row = row as u32; + assert_eq!( + actual.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + let longest_row = actual.summary().longest_row; + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); + } + } + + fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize { + while !text.is_char_boundary(offset) { + match bias { + Bias::Left => offset -= 1, + Bias::Right => offset += 1, + } + } + offset + } + + impl Rope { + fn text(&self) -> String { + let mut text = String::new(); + for chunk in self.chunks.cursor::<()>() { + text.push_str(&chunk.0); + } + text + } + } +} diff --git a/crates/rope2/src/unclipped.rs b/crates/rope2/src/unclipped.rs new file mode 100644 index 0000000000..937cbca053 --- /dev/null +++ b/crates/rope2/src/unclipped.rs @@ -0,0 +1,57 @@ +use crate::{ChunkSummary, TextDimension, TextSummary}; +use std::ops::{Add, AddAssign, Sub, SubAssign}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Unclipped(pub T); + +impl From for Unclipped { + fn from(value: T) -> Self { + Unclipped(value) + } +} + +impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary> + for Unclipped +{ + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + self.0.add_summary(summary, &()); + } +} + +impl TextDimension for Unclipped { + fn from_text_summary(summary: &TextSummary) -> Self { + Unclipped(T::from_text_summary(summary)) + } + + fn add_assign(&mut self, other: &Self) { + TextDimension::add_assign(&mut self.0, &other.0); + } +} + +impl> Add> for Unclipped { + type Output = Unclipped; + + fn add(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 + rhs.0) + } +} + +impl> Sub> for Unclipped { + type Output = Unclipped; + + fn sub(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 - rhs.0) + } +} + +impl> AddAssign> for Unclipped { + fn add_assign(&mut self, rhs: Unclipped) { + self.0 += rhs.0; + } +} + +impl> SubAssign> for Unclipped { + fn sub_assign(&mut self, rhs: Unclipped) { + self.0 -= rhs.0; + } +} diff --git a/crates/text2/Cargo.toml b/crates/text2/Cargo.toml index 7c12d22adf..679a2df8d6 100644 --- a/crates/text2/Cargo.toml +++ b/crates/text2/Cargo.toml @@ -14,7 +14,7 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -rope = { path = "../rope" } +rope = { package = "rope2", path = "../rope2" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index d510de24ee..8cc8cdcec9 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -58,6 +58,7 @@ project = { package = "project2", path = "../project2" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } +rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" } From acab2f900392a33d86916bf979f6d4052a309384 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 7 Nov 2023 13:23:08 -0700 Subject: [PATCH 02/27] MODAL --- crates/go_to_line2/src/go_to_line.rs | 11 +- crates/gpui2/src/styled.rs | 19 +-- crates/ui2/src/components.rs | 2 + crates/ui2/src/components/elevated_surface.rs | 28 +++++ crates/ui2/src/elevation.rs | 27 ++++- crates/workspace2/src/modal_layer.rs | 51 ++++++-- styles/src/style_tree/assistant.ts | 114 +++++++++++++++--- styles/src/style_tree/status_bar.ts | 18 +-- styles/src/themes/rose-pine/rose-pine-dawn.ts | 2 +- 9 files changed, 207 insertions(+), 65 deletions(-) create mode 100644 crates/ui2/src/components/elevated_surface.rs diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 13d283ecff..6ae58124d8 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,4 +1,8 @@ -use gpui::{actions, div, px, red, AppContext, Div, Render, Styled, ViewContext, VisualContext}; +use gpui::{ + actions, div, px, red, AppContext, Div, ParentElement, Render, Styled, ViewContext, + VisualContext, +}; +use ui::modal; use workspace::ModalRegistry; actions!(Toggle); @@ -27,9 +31,8 @@ pub struct GoToLine; impl Render for GoToLine { type Element = Div; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - dbg!("rendering GoToLine"); - div().bg(red()).w(px(100.0)).h(px(100.0)) + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + modal(cx).child(div().bg(red()).w(px(100.0)).h(px(100.0))) } } diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 06be0368c0..2bf3006d41 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -4,7 +4,7 @@ use crate::{ SharedString, StyleRefinement, Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; -use smallvec::smallvec; +use smallvec::{smallvec, SmallVec}; pub trait Styled { fn style(&mut self) -> &mut StyleRefinement; @@ -290,24 +290,11 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow(mut self) -> Self + fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self where Self: Sized, { - self.style().box_shadow = Some(smallvec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(2.), - spread_radius: px(-1.), - } - ]); + self.style().box_shadow = Some(shadows); self } diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 857d0f1042..706918c080 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -3,6 +3,7 @@ mod button; mod checkbox; mod context_menu; mod details; +mod elevated_surface; mod facepile; mod icon; mod icon_button; @@ -30,6 +31,7 @@ pub use button::*; pub use checkbox::*; pub use context_menu::*; pub use details::*; +pub use elevated_surface::*; pub use facepile::*; pub use icon::*; pub use icon_button::*; diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs new file mode 100644 index 0000000000..9070ac4428 --- /dev/null +++ b/crates/ui2/src/components/elevated_surface.rs @@ -0,0 +1,28 @@ +use gpui::{div, hsla, point, px, BoxShadow, Div}; + +use crate::{prelude::*, v_stack}; + +/// Create an elevated surface. +/// +/// Must be used inside of a relative parent element +pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext) -> Div { + let colors = cx.theme().colors(); + + // let shadow = BoxShadow { + // color: hsla(0., 0., 0., 0.1), + // offset: point(px(0.), px(1.)), + // blur_radius: px(3.), + // spread_radius: px(0.), + // }; + + v_stack() + .rounded_lg() + .bg(colors.elevated_surface_background) + .border() + .border_color(colors.border) + .shadow(level.shadow()) +} + +pub fn modal(cx: &mut ViewContext) -> Div { + elevated_surface(ElevationIndex::ModalSurfaces, cx) +} diff --git a/crates/ui2/src/elevation.rs b/crates/ui2/src/elevation.rs index 3218f3f5f1..0dd51e3314 100644 --- a/crates/ui2/src/elevation.rs +++ b/crates/ui2/src/elevation.rs @@ -1,3 +1,6 @@ +use gpui::{hsla, point, px, BoxShadow}; +use smallvec::{smallvec, SmallVec}; + #[doc = include_str!("elevation.md")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Elevation { @@ -17,8 +20,8 @@ pub enum ElevationIndex { } impl ElevationIndex { - pub fn usize(&self) -> usize { - match *self { + pub fn z_index(self) -> u32 { + match self { ElevationIndex::AppBackground => 0, ElevationIndex::UISurface => 100, ElevationIndex::ElevatedSurface => 200, @@ -27,6 +30,26 @@ impl ElevationIndex { ElevationIndex::DraggedElement => 900, } } + + pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { + match self { + ElevationIndex::AppBackground => smallvec![], + + ElevationIndex::UISurface => smallvec![BoxShadow { + color: hsla(0., 0., 0., 0.12), + offset: point(px(0.), px(1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }], + + _ => smallvec![BoxShadow { + color: hsla(0., 0., 0., 0.32), + offset: point(px(1.), px(3.)), + blur_radius: px(12.), + spread_radius: px(0.), + }], + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 01f940273a..5f8b9b0345 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,8 +1,8 @@ use std::{any::TypeId, sync::Arc}; use gpui::{ - div, AnyView, AppContext, DispatchPhase, Div, ParentElement, Render, StatelessInteractive, - View, ViewContext, + div, AnyView, AppContext, Component, DispatchPhase, Div, ParentElement, Render, + StatelessInteractive, Styled, View, ViewContext, }; use crate::Workspace; @@ -69,22 +69,47 @@ impl ModalLayer { Self { open_modal: None } } + // Workspace + // - ModalLayer parent + // - - container + // - - - modal + // - - - content of the modal + // - - content of the workspace + + // app + // workspace + // container some layer that contains all modals and is 100% wide and high + // modal (this has a shadow, some witdht) + // whatever + pub fn render(&self, workspace: &Workspace, cx: &ViewContext) -> Div { - let mut div = div(); - - // div, c workspace.toggle_modal()div.on_action()) { - // - // } - - // for (type_id, action) in cx.global::().registered_modals.iter() { - // div = div.useful_on_action(*type_id, action) - // } + let mut parent = div().relative(); for (_, action) in cx.global::().registered_modals.iter() { - div = (action)(div); + parent = (action)(parent); } - div.children(self.open_modal.clone()) + parent.when_some(self.open_modal.as_ref(), |parent, open_modal| { + let container1 = div() + .absolute() + .size_full() + .top_0() + .left_0() + .right_0() + .bottom_0(); + + // transparent layer + let container2 = div() + .flex() + .h_96() + .justify_center() + .size_full() + .relative() + .top_20() + .z_index(400); + + parent.child(container1.child(container2.child(open_modal.clone()))) + }) } } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 08297731bb..d261a23bcd 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -23,7 +23,7 @@ export default function assistant(): any { const theme = useTheme() const interactive_role = ( - color: StyleSets + color: StyleSets, ): Interactive => { return interactive({ base: { @@ -94,7 +94,7 @@ export default function assistant(): any { margin: { left: 8, right: 18 }, color: foreground(theme.highest, "positive"), width: 12, - } + }, }, retrieve_context: toggleable({ base: interactive({ @@ -106,7 +106,8 @@ export default function assistant(): any { background: background(theme.highest, "on"), corner_radius: 2, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, margin: { left: 2 }, padding: { @@ -118,17 +119,45 @@ export default function assistant(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered"), - background: background(theme.highest, "on", "hovered"), + ...text( + theme.highest, + "mono", + "variant", + "hovered", + ), + background: background( + theme.highest, + "on", + "hovered", + ), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background( + theme.highest, + "on", + "hovered", + ), }, }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed"), - background: background(theme.highest, "on", "pressed"), + ...text( + theme.highest, + "mono", + "variant", + "pressed", + ), + background: background( + theme.highest, + "on", + "pressed", + ), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background( + theme.highest, + "on", + "pressed", + ), }, }, }, @@ -143,11 +172,19 @@ export default function assistant(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered", + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed", + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -163,7 +200,8 @@ export default function assistant(): any { background: background(theme.highest, "on"), corner_radius: 2, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -174,17 +212,45 @@ export default function assistant(): any { }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered"), - background: background(theme.highest, "on", "hovered"), + ...text( + theme.highest, + "mono", + "variant", + "hovered", + ), + background: background( + theme.highest, + "on", + "hovered", + ), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background( + theme.highest, + "on", + "hovered", + ), }, }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed"), - background: background(theme.highest, "on", "pressed"), + ...text( + theme.highest, + "mono", + "variant", + "pressed", + ), + background: background( + theme.highest, + "on", + "pressed", + ), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background( + theme.highest, + "on", + "pressed", + ), }, }, }, @@ -199,11 +265,19 @@ export default function assistant(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered", + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed", + ), border: border(theme.highest, "accent", "pressed"), }, }, diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index b279bbac14..5ced6b0d7b 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -78,33 +78,33 @@ export default function status_bar(): any { padding: { top: 2, bottom: 2, left: 6, right: 6 }, }, container_warning: diagnostic_status_container, - container_error: diagnostic_status_container + container_error: diagnostic_status_container, }, state: { hovered: { icon_color_ok: foreground(layer, "on"), container_ok: { - background: background(layer, "hovered") + background: background(layer, "hovered"), }, container_warning: { - background: background(layer, "hovered") + background: background(layer, "hovered"), }, container_error: { - background: background(layer, "hovered") + background: background(layer, "hovered"), }, }, clicked: { icon_color_ok: foreground(layer, "on"), container_ok: { - background: background(layer, "pressed") + background: background(layer, "pressed"), }, container_warning: { - background: background(layer, "pressed") + background: background(layer, "pressed"), }, container_error: { - background: background(layer, "pressed") - } - } + background: background(layer, "pressed"), + }, + }, }, }), panel_buttons: { diff --git a/styles/src/themes/rose-pine/rose-pine-dawn.ts b/styles/src/themes/rose-pine/rose-pine-dawn.ts index c78f1132dd..9642107bdb 100644 --- a/styles/src/themes/rose-pine/rose-pine-dawn.ts +++ b/styles/src/themes/rose-pine/rose-pine-dawn.ts @@ -31,7 +31,7 @@ export const theme: ThemeConfig = { color.muted, color.subtle, color.text, - ].reverse() + ].reverse(), ) .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]), red: color_ramp(chroma(color.love)), From 3a85beeaa550b207b2ed2f6021e20cc3c117f0e7 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 7 Nov 2023 16:23:41 -0500 Subject: [PATCH 03/27] center a div --- crates/go_to_line2/src/go_to_line.rs | 2 +- crates/ui2/src/components/elevated_surface.rs | 2 +- crates/workspace2/src/modal_layer.rs | 22 +++++++++---------- crates/workspace2/src/workspace2.rs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 6ae58124d8..c2e2f28ab2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -32,7 +32,7 @@ impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx).child(div().bg(red()).w(px(100.0)).h(px(100.0))) + modal(cx).child(div().m_4().bg(red()).w(px(100.0)).h(px(100.0))) } } diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 9070ac4428..5d0bbe698c 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -1,4 +1,4 @@ -use gpui::{div, hsla, point, px, BoxShadow, Div}; +use gpui::Div; use crate::{prelude::*, v_stack}; diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 5f8b9b0345..a0c3515245 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,9 +1,10 @@ use std::{any::TypeId, sync::Arc}; use gpui::{ - div, AnyView, AppContext, Component, DispatchPhase, Div, ParentElement, Render, + div, hsla, px, red, AnyView, AppContext, Component, DispatchPhase, Div, ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, }; +use ui::v_stack; use crate::Workspace; @@ -83,7 +84,7 @@ impl ModalLayer { // whatever pub fn render(&self, workspace: &Workspace, cx: &ViewContext) -> Div { - let mut parent = div().relative(); + let mut parent = div().relative().bg(red()).size_full(); for (_, action) in cx.global::().registered_modals.iter() { parent = (action)(parent); @@ -92,21 +93,20 @@ impl ModalLayer { parent.when_some(self.open_modal.as_ref(), |parent, open_modal| { let container1 = div() .absolute() + .flex() + .flex_col() + .items_center() .size_full() .top_0() .left_0() - .right_0() - .bottom_0(); + .z_index(400); // transparent layer - let container2 = div() - .flex() - .h_96() - .justify_center() - .size_full() + let container2 = v_stack() + .bg(hsla(0.5, 0.5, 0.5, 0.5)) + .h(px(0.0)) .relative() - .top_20() - .z_index(400); + .top_20(); parent.child(container1.child(container2.child(open_modal.clone()))) }) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 90204f6038..ee6134e9e4 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3710,10 +3710,10 @@ impl Render for Workspace { self.modal_layer .read(cx) .render(self, cx) + .relative() .flex_1() .w_full() .flex() - .flex_row() .overflow_hidden() .border_t() .border_b() From 5751303ea4423ca4758561616474ff018331d293 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 7 Nov 2023 13:42:33 -0700 Subject: [PATCH 04/27] MOAR CODE --- crates/go_to_line2/src/go_to_line.rs | 163 ++++++++++++++++----------- crates/workspace2/src/modal_layer.rs | 8 +- 2 files changed, 105 insertions(+), 66 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c2e2f28ab2..d280f31de2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -3,22 +3,27 @@ use gpui::{ VisualContext, }; use ui::modal; +use editor::{scroll::autoscroll::Autoscroll, Editor}; +use gpui::{ + actions, div, px, red, AppContext, Div, EventEmitter, ParentElement, Render, Styled, View, + ViewContext, VisualContext, +}; +use text::{Bias, Point}; +use ui::modal; +use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalRegistry; -actions!(Toggle); +actions!(Toggle, Cancel, Confirm); pub fn init(cx: &mut AppContext) { cx.register_action_type::(); cx.global_mut::() - .register_modal(Toggle, |_, cx| { - // if let Some(editor) = workspace - // .active_item(cx) - // .and_then(|active_item| active_item.downcast::()) - // { - // cx.build_view(|cx| GoToLine::new(editor, cx)) - // } - let view = cx.build_view(|_| GoToLine); - view + .register_modal(Toggle, |workspace, cx| { + let editor = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::())?; + + Some(cx.build_view(|cx| GoToLine::new(editor, cx))) }); // cx.add_action(GoToLine::toggle); @@ -26,13 +31,96 @@ pub fn init(cx: &mut AppContext) { // cx.add_action(GoToLine::cancel); } -pub struct GoToLine; +pub struct GoToLine { + line_editor: View, + active_editor: View, +} + +pub enum Event { + Dismissed, +} + +impl EventEmitter for GoToLine { + type Event = Event; +} + +impl GoToLine { + pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { + let line_editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&line_editor, Self::on_line_editor_event) + .detach(); + + Self { + line_editor, + active_editor, + } + } + + fn on_line_editor_event( + &mut self, + _: View, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Blurred => cx.emit(Event::Dismissed), + editor::Event::BufferEdited { .. } => { + if let Some(point) = self.point_from_query(cx) { + // todo!() + // self.active_editor.update(cx, |active_editor, cx| { + // let snapshot = active_editor.snapshot(cx).display_snapshot; + // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + // let display_point = point.to_display_point(&snapshot); + // let row = display_point.row(); + // active_editor.highlight_rows(Some(row..row + 1)); + // active_editor.request_autoscroll(Autoscroll::center(), cx); + // }); + cx.notify(); + } + } + _ => {} + } + } + + fn point_from_query(&self, cx: &ViewContext) -> Option { + // todo!() + let line_editor = "2:2"; //self.line_editor.read(cx).text(cx); + let mut components = line_editor + .splitn(2, FILE_ROW_COLUMN_DELIMITER) + .map(str::trim) + .fuse(); + let row = components.next().and_then(|row| row.parse::().ok())?; + let column = components.next().and_then(|col| col.parse::().ok()); + Some(Point::new( + row.saturating_sub(1), + column.unwrap_or(0).saturating_sub(1), + )) + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + cx.emit(Event::Dismissed); + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + if let Some(point) = self.point_from_query(cx) { + self.active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }); + } + + cx.emit(Event::Dismissed); + } +} impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx).child(div().m_4().bg(red()).w(px(100.0)).h(px(100.0))) + modal(cx).child(self.line_editor.clone()).child("blah blah") } } @@ -51,14 +139,6 @@ impl Render for GoToLine { // impl GoToLine { // pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { -// // let line_editor = cx.build_view(|cx| { -// // Editor::single_line( -// // Some(Arc::new(|theme| theme.picker.input_editor.clone())), -// // cx, -// // ) -// // }); -// // cx.subscribe(&line_editor, Self::on_line_editor_event) -// // .detach(); // let (scroll_position, cursor_point, max_point) = active_editor.update(cx, |editor, cx| { // let scroll_position = editor.scroll_position(cx); @@ -101,49 +181,6 @@ impl Render for GoToLine { // cx.emit(Event::Dismissed); // } -// fn on_line_editor_event( -// &mut self, -// _: View, -// event: &editor::Event, -// cx: &mut ViewContext, -// ) { -// match event { -// editor::Event::Blurred => cx.emit(Event::Dismissed), -// editor::Event::BufferEdited { .. } => { -// if let Some(point) = self.point_from_query(cx) { -// // todo!() -// // self.active_editor.update(cx, |active_editor, cx| { -// // let snapshot = active_editor.snapshot(cx).display_snapshot; -// // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); -// // let display_point = point.to_display_point(&snapshot); -// // let row = display_point.row(); -// // active_editor.highlight_rows(Some(row..row + 1)); -// // active_editor.request_autoscroll(Autoscroll::center(), cx); -// // }); -// cx.notify(); -// } -// } -// _ => {} -// } -// } - -// fn point_from_query(&self, cx: &ViewContext) -> Option { -// return None; -// // todo!() -// // let line_editor = self.line_editor.read(cx).text(cx); -// // let mut components = line_editor -// // .splitn(2, FILE_ROW_COLUMN_DELIMITER) -// // .map(str::trim) -// // .fuse(); -// // let row = components.next().and_then(|row| row.parse::().ok())?; -// // let column = components.next().and_then(|col| col.parse::().ok()); -// // Some(Point::new( -// // row.saturating_sub(1), -// // column.unwrap_or(0).saturating_sub(1), -// // )) -// } -// } - // impl EventEmitter for GoToLine { // type Event = Event; // } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a0c3515245..298e5646b7 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -33,7 +33,7 @@ impl ModalRegistry { pub fn register_modal(&mut self, action: A, build_view: B) where V: Render, - B: Fn(&Workspace, &mut ViewContext) -> View + 'static, + B: Fn(&Workspace, &mut ViewContext) -> Option> + 'static, { let build_view = Arc::new(build_view); @@ -47,12 +47,14 @@ impl ModalRegistry { event: &A, phase: DispatchPhase, cx: &mut ViewContext| { - dbg!("GOT HERE"); if phase == DispatchPhase::Capture { return; } - let new_modal = (build_view)(workspace, cx); + let Some(new_modal) = (build_view)(workspace, cx) else { + return; + }; + workspace.modal_layer.update(cx, |modal_layer, _| { modal_layer.open_modal = Some(new_modal.into()); }); From 7cdece4857caee93f3dd6af3319a976165895c72 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 7 Nov 2023 15:27:08 -0700 Subject: [PATCH 05/27] Beautiful go to line modal Co-authored-by: Julia Co-authored-by: Marshall --- crates/editor2/src/editor.rs | 68 +++++++++++------ crates/go_to_line2/src/go_to_line.rs | 106 +++++++++++++++++---------- crates/theme2/src/theme2.rs | 8 +- crates/workspace2/src/modal_layer.rs | 23 +----- 4 files changed, 124 insertions(+), 81 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index c63c531ceb..10831995ff 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -36,10 +36,10 @@ pub use element::{ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, px, relative, AnyElement, AppContext, BackgroundExecutor, Context, - DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, FontStyle, FontWeight, Hsla, - Model, Pixels, Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, - WeakView, WindowContext, + actions, div, hsla, px, relative, rems, AnyElement, AppContext, BackgroundExecutor, Context, + DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, FontFeatures, FontStyle, + FontWeight, Hsla, Model, Pixels, Render, Styled, Subscription, Task, TextStyle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -2162,14 +2162,14 @@ impl Editor { // self.collaboration_hub = Some(hub); // } - // pub fn set_placeholder_text( - // &mut self, - // placeholder_text: impl Into>, - // cx: &mut ViewContext, - // ) { - // self.placeholder_text = Some(placeholder_text.into()); - // cx.notify(); - // } + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut ViewContext, + ) { + self.placeholder_text = Some(placeholder_text.into()); + cx.notify(); + } // pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { // self.cursor_shape = cursor_shape; @@ -9365,18 +9365,42 @@ impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, - font_size: settings.buffer_font_size.into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(settings.buffer_line_height.value()), - underline: None, + let text_style = match self.mode { + EditorMode::SingleLine => { + TextStyle { + color: cx.theme().colors().text, + font_family: "Zed Sans".into(), // todo!() + font_features: FontFeatures::default(), + font_size: rems(1.0).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()), + underline: None, + } + } + + EditorMode::AutoHeight { max_lines } => todo!(), + + EditorMode::Full => TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features, + font_size: settings.buffer_font_size.into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(settings.buffer_line_height.value()), + underline: None, + }, }; + + let background = match self.mode { + EditorMode::SingleLine => cx.theme().system().transparent, + EditorMode::AutoHeight { max_lines } => cx.theme().system().transparent, + EditorMode::Full => cx.theme().colors().editor_background, + }; + EditorElement::new(EditorStyle { - background: cx.theme().colors().editor_background, + background, local_player: cx.theme().players().local(), text: text_style, scrollbar_width: px(12.), diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index d280f31de2..fc0b7432c7 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,15 +1,11 @@ +use editor::Editor; use gpui::{ - actions, div, px, red, AppContext, Div, ParentElement, Render, Styled, ViewContext, - VisualContext, -}; -use ui::modal; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use gpui::{ - actions, div, px, red, AppContext, Div, EventEmitter, ParentElement, Render, Styled, View, + actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, }; -use text::{Bias, Point}; -use ui::modal; +use text::Point; +use theme::ActiveTheme; +use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalRegistry; @@ -33,6 +29,7 @@ pub fn init(cx: &mut AppContext) { pub struct GoToLine { line_editor: View, + #[allow(unused)] // todo!() active_editor: View, } @@ -46,7 +43,11 @@ impl EventEmitter for GoToLine { impl GoToLine { pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - let line_editor = cx.build_view(|cx| Editor::single_line(cx)); + let line_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Find something", cx); + editor + }); cx.subscribe(&line_editor, Self::on_line_editor_event) .detach(); @@ -65,26 +66,27 @@ impl GoToLine { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::BufferEdited { .. } => { - if let Some(point) = self.point_from_query(cx) { - // todo!() - // self.active_editor.update(cx, |active_editor, cx| { - // let snapshot = active_editor.snapshot(cx).display_snapshot; - // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - // let display_point = point.to_display_point(&snapshot); - // let row = display_point.row(); - // active_editor.highlight_rows(Some(row..row + 1)); - // active_editor.request_autoscroll(Autoscroll::center(), cx); - // }); - cx.notify(); - } + // if let Some(point) = self.point_from_query(cx) { + // todo!() + // self.active_editor.update(cx, |active_editor, cx| { + // let snapshot = active_editor.snapshot(cx).display_snapshot; + // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + // let display_point = point.to_display_point(&snapshot); + // let row = display_point.row(); + // active_editor.highlight_rows(Some(row..row + 1)); + // active_editor.request_autoscroll(Autoscroll::center(), cx); + // }); + // cx.notify(); + // } } _ => {} } } + #[allow(unused)] fn point_from_query(&self, cx: &ViewContext) -> Option { // todo!() - let line_editor = "2:2"; //self.line_editor.read(cx).text(cx); + let line_editor = self.line_editor.read(cx).text(cx); let mut components = line_editor .splitn(2, FILE_ROW_COLUMN_DELIMITER) .map(str::trim) @@ -97,22 +99,26 @@ impl GoToLine { )) } - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismissed); - } + // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + // cx.emit(Event::Dismissed); + // } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(point) = self.point_from_query(cx) { - self.active_editor.update(cx, |active_editor, cx| { - let snapshot = active_editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([point..point]) - }); - }); - } + // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + // if let Some(point) = self.point_from_query(cx) { + // self.active_editor.update(cx, |active_editor, cx| { + // let snapshot = active_editor.snapshot(cx).display_snapshot; + // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + // active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + // s.select_ranges([point..point]) + // }); + // }); + // } - cx.emit(Event::Dismissed); + // cx.emit(Event::Dismissed); + // } + + fn status_text(&self) -> SharedString { + "Default text".into() } } @@ -120,7 +126,31 @@ impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx).child(self.line_editor.clone()).child("blah blah") + modal(cx).w_96().child( + v_stack() + .px_1() + .pt_0p5() + .gap_px() + .child( + v_stack() + .py_0p5() + .px_1() + .child(div().px_1().py_0p5().child(self.line_editor.clone())), + ) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) + .child( + h_stack() + .justify_between() + .px_2() + .py_1() + .child(Label::new(self.status_text()).color(LabelColor::Muted)), + ), + ) } } diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 88db3c55f4..33a977f695 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -63,7 +63,13 @@ pub struct Theme { } impl Theme { - /// Returns the [`ThemeColors`] for the theme. + /// Returns the [`SystemColors`] for the theme. + #[inline(always)] + pub fn system(&self) -> &SystemColors { + &self.styles.system + } + + /// Returns the [`PlayerColors`] for the theme. #[inline(always)] pub fn players(&self) -> &PlayerColors { &self.styles.player diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 298e5646b7..c69354f87b 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,7 +1,7 @@ use std::{any::TypeId, sync::Arc}; use gpui::{ - div, hsla, px, red, AnyView, AppContext, Component, DispatchPhase, Div, ParentElement, Render, + div, px, AnyView, AppContext, Component, DispatchPhase, Div, ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, }; use ui::v_stack; @@ -72,21 +72,8 @@ impl ModalLayer { Self { open_modal: None } } - // Workspace - // - ModalLayer parent - // - - container - // - - - modal - // - - - content of the modal - // - - content of the workspace - - // app - // workspace - // container some layer that contains all modals and is 100% wide and high - // modal (this has a shadow, some witdht) - // whatever - pub fn render(&self, workspace: &Workspace, cx: &ViewContext) -> Div { - let mut parent = div().relative().bg(red()).size_full(); + let mut parent = div().relative().size_full(); for (_, action) in cx.global::().registered_modals.iter() { parent = (action)(parent); @@ -104,11 +91,7 @@ impl ModalLayer { .z_index(400); // transparent layer - let container2 = v_stack() - .bg(hsla(0.5, 0.5, 0.5, 0.5)) - .h(px(0.0)) - .relative() - .top_20(); + let container2 = v_stack().h(px(0.0)).relative().top_20(); parent.child(container1.child(container2.child(open_modal.clone()))) }) From 738b2ce6c50b3ecb65c715b24b41278f66eb2617 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 8 Nov 2023 18:10:46 +0100 Subject: [PATCH 06/27] Extract a `Frame` struct from `Window` Co-Authored-By: Marshall Co-Authored-By: Nathan Co-Authored-By: Piotr --- crates/gpui2/src/app.rs | 6 +- crates/gpui2/src/scene.rs | 10 +- crates/gpui2/src/window.rs | 238 +++++++++++++++++++------------------ 3 files changed, 136 insertions(+), 118 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index f2ac3a91cf..79f80f474d 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -519,7 +519,7 @@ impl AppContext { window_handle .update(self, |_, cx| { if cx.window.focus == focused { - let mut listeners = mem::take(&mut cx.window.focus_listeners); + let mut listeners = mem::take(&mut cx.window.current_frame.focus_listeners); let focused = focused .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); let blurred = cx @@ -535,8 +535,8 @@ impl AppContext { } } - listeners.extend(cx.window.focus_listeners.drain(..)); - cx.window.focus_listeners = listeners; + listeners.extend(cx.window.current_frame.focus_listeners.drain(..)); + cx.window.current_frame.focus_listeners = listeners; } }) .ok(); diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index c176479a67..918c3c8351 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -27,8 +27,8 @@ pub(crate) struct SceneBuilder { polychrome_sprites: Vec, } -impl SceneBuilder { - pub fn new() -> SceneBuilder { +impl Default for SceneBuilder { + fn default() -> Self { SceneBuilder { layers_by_order: BTreeMap::new(), splitter: BspSplitter::new(), @@ -40,6 +40,12 @@ impl SceneBuilder { polychrome_sprites: Vec::new(), } } +} + +impl SceneBuilder { + pub fn new() -> SceneBuilder { + SceneBuilder::default() + } pub fn build(&mut self) -> Scene { // Map each layer id to a float between 0. and 1., with 1. closer to the viewer. diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index e55f0152d8..0dae6171d9 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -132,7 +132,12 @@ impl FocusHandle { if self.id == ancestor_id { return true; } else { - ancestor = cx.window.focus_parents_by_child.get(&ancestor_id).copied(); + ancestor = cx + .window + .current_frame + .focus_parents_by_child + .get(&ancestor_id) + .copied(); } } false @@ -175,19 +180,8 @@ pub struct Window { pub(crate) layout_engine: TaffyLayoutEngine, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, - prev_frame_element_states: HashMap, - element_states: HashMap, - prev_frame_key_matchers: HashMap, - key_matchers: HashMap, - z_index_stack: StackingOrder, - content_mask_stack: Vec>, - element_offset_stack: Vec>, - mouse_listeners: HashMap>, - key_dispatch_stack: Vec, - freeze_key_dispatch_stack: bool, - focus_stack: Vec, - focus_parents_by_child: HashMap, - pub(crate) focus_listeners: Vec, + pub(crate) previous_frame: Frame, + pub(crate) current_frame: Frame, pub(crate) focus_handles: Arc>>, default_prevented: bool, mouse_position: Point, @@ -198,12 +192,27 @@ pub struct Window { bounds_observers: SubscriberSet<(), AnyObserver>, active: bool, activation_observers: SubscriberSet<(), AnyObserver>, - pub(crate) scene_builder: SceneBuilder, pub(crate) dirty: bool, pub(crate) last_blur: Option>, pub(crate) focus: Option, } +#[derive(Default)] +pub(crate) struct Frame { + element_states: HashMap, + key_matchers: HashMap, + mouse_listeners: HashMap>, + pub(crate) focus_listeners: Vec, + key_dispatch_stack: Vec, + freeze_key_dispatch_stack: bool, + focus_parents_by_child: HashMap, + pub(crate) scene_builder: SceneBuilder, + z_index_stack: StackingOrder, + content_mask_stack: Vec>, + element_offset_stack: Vec>, + focus_stack: Vec, +} + impl Window { pub(crate) fn new( handle: AnyWindowHandle, @@ -270,19 +279,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - prev_frame_element_states: HashMap::default(), - element_states: HashMap::default(), - prev_frame_key_matchers: HashMap::default(), - key_matchers: HashMap::default(), - z_index_stack: StackingOrder(SmallVec::new()), - content_mask_stack: Vec::new(), - element_offset_stack: Vec::new(), - mouse_listeners: HashMap::default(), - key_dispatch_stack: Vec::new(), - freeze_key_dispatch_stack: false, - focus_stack: Vec::new(), - focus_parents_by_child: HashMap::default(), - focus_listeners: Vec::new(), + previous_frame: Frame::default(), + current_frame: Frame::default(), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), default_prevented: true, mouse_position, @@ -293,7 +291,6 @@ impl Window { bounds_observers: SubscriberSet::new(), active: false, activation_observers: SubscriberSet::new(), - scene_builder: SceneBuilder::new(), dirty: true, last_blur: None, focus: None, @@ -667,8 +664,9 @@ impl<'a> WindowContext<'a> { &mut self, handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { - let order = self.window.z_index_stack.clone(); + let order = self.window.current_frame.z_index_stack.clone(); self.window + .current_frame .mouse_listeners .entry(TypeId::of::()) .or_default() @@ -692,9 +690,9 @@ impl<'a> WindowContext<'a> { /// Called during painting to invoke the given closure in a new stacking context. The given /// z-index is interpreted relative to the previous call to `stack`. pub fn stack(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { - self.window.z_index_stack.push(z_index); + self.window.current_frame.z_index_stack.push(z_index); let result = f(self); - self.window.z_index_stack.pop(); + self.window.current_frame.z_index_stack.pop(); result } @@ -712,8 +710,8 @@ impl<'a> WindowContext<'a> { let mut shadow_bounds = bounds; shadow_bounds.origin += shadow.offset; shadow_bounds.dilate(shadow.spread_radius); - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, Shadow { order: 0, bounds: shadow_bounds.scale(scale_factor), @@ -740,8 +738,8 @@ impl<'a> WindowContext<'a> { let content_mask = self.content_mask(); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, Quad { order: 0, bounds: bounds.scale(scale_factor), @@ -761,9 +759,10 @@ impl<'a> WindowContext<'a> { path.content_mask = content_mask; path.color = color.into(); let window = &mut *self.window; - window - .scene_builder - .insert(&window.z_index_stack, path.scale(scale_factor)); + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, + path.scale(scale_factor), + ); } /// Paint an underline into the scene for the current frame at the current z-index. @@ -785,8 +784,8 @@ impl<'a> WindowContext<'a> { }; let content_mask = self.content_mask(); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, Underline { order: 0, bounds: bounds.scale(scale_factor), @@ -839,8 +838,8 @@ impl<'a> WindowContext<'a> { }; let content_mask = self.content_mask().scale(scale_factor); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, MonochromeSprite { order: 0, bounds, @@ -890,8 +889,8 @@ impl<'a> WindowContext<'a> { let content_mask = self.content_mask().scale(scale_factor); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, PolychromeSprite { order: 0, bounds, @@ -932,8 +931,8 @@ impl<'a> WindowContext<'a> { let content_mask = self.content_mask().scale(scale_factor); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, MonochromeSprite { order: 0, bounds, @@ -968,8 +967,8 @@ impl<'a> WindowContext<'a> { let corner_radii = corner_radii.scale(scale_factor); let window = &mut *self.window; - window.scene_builder.insert( - &window.z_index_stack, + window.current_frame.scene_builder.insert( + &window.current_frame.z_index_stack, PolychromeSprite { order: 0, bounds, @@ -1014,7 +1013,7 @@ impl<'a> WindowContext<'a> { } self.window.root_view = Some(root_view); - let scene = self.window.scene_builder.build(); + let scene = self.window.current_frame.scene_builder.build(); self.window.platform_window.draw(scene); let cursor_style = self @@ -1030,39 +1029,21 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } + /// Rotate the current frame and the previous frame, then clear the current frame. + /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { self.text_system().start_frame(); let window = &mut *self.window; - - // Move the current frame element states to the previous frame. - // The new empty element states map will be populated for any element states we - // reference during the upcoming frame. - mem::swap( - &mut window.element_states, - &mut window.prev_frame_element_states, - ); - window.element_states.clear(); - - // Make the current key matchers the previous, and then clear the current. - // An empty key matcher map will be created for every identified element in the - // upcoming frame. - mem::swap( - &mut window.key_matchers, - &mut window.prev_frame_key_matchers, - ); - window.key_matchers.clear(); - - // Clear mouse event listeners, because elements add new element listeners - // when the upcoming frame is painted. - window.mouse_listeners.values_mut().for_each(Vec::clear); - - // Clear focus state, because we determine what is focused when the new elements - // in the upcoming frame are initialized. - window.focus_listeners.clear(); - window.key_dispatch_stack.clear(); - window.focus_parents_by_child.clear(); - window.freeze_key_dispatch_stack = false; + mem::swap(&mut window.previous_frame, &mut window.current_frame); + let frame = &mut window.current_frame; + frame.element_states.clear(); + frame.key_matchers.clear(); + frame.mouse_listeners.values_mut().for_each(Vec::clear); + frame.focus_listeners.clear(); + frame.key_dispatch_stack.clear(); + frame.focus_parents_by_child.clear(); + frame.freeze_key_dispatch_stack = false; } /// Dispatch a mouse or keyboard event on the window. @@ -1126,6 +1107,7 @@ impl<'a> WindowContext<'a> { if let Some(any_mouse_event) = event.mouse_event() { if let Some(mut handlers) = self .window + .current_frame .mouse_listeners .remove(&any_mouse_event.type_id()) { @@ -1160,18 +1142,20 @@ impl<'a> WindowContext<'a> { // Just in case any handlers added new handlers, which is weird, but possible. handlers.extend( self.window + .current_frame .mouse_listeners .get_mut(&any_mouse_event.type_id()) .into_iter() .flat_map(|handlers| handlers.drain(..)), ); self.window + .current_frame .mouse_listeners .insert(any_mouse_event.type_id(), handlers); } } else if let Some(any_key_event) = event.keyboard_event() { let mut did_handle_action = false; - let key_dispatch_stack = mem::take(&mut self.window.key_dispatch_stack); + let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack); let key_event_type = any_key_event.type_id(); let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new(); @@ -1233,7 +1217,7 @@ impl<'a> WindowContext<'a> { } drop(context_stack); - self.window.key_dispatch_stack = key_dispatch_stack; + self.window.current_frame.key_dispatch_stack = key_dispatch_stack; return did_handle_action; } @@ -1249,13 +1233,14 @@ impl<'a> WindowContext<'a> { ) -> KeyMatch { let key_match = self .window + .current_frame .key_matchers .get_mut(element_id) .unwrap() .match_keystroke(keystroke, context_stack); if key_match.is_some() { - for matcher in self.window.key_matchers.values_mut() { + for matcher in self.window.current_frame.key_matchers.values_mut() { matcher.clear_pending(); } } @@ -1515,11 +1500,12 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { window.element_id_stack.push(id.into()); let global_id = window.element_id_stack.clone(); - if window.key_matchers.get(&global_id).is_none() { - window.key_matchers.insert( + if window.current_frame.key_matchers.get(&global_id).is_none() { + window.current_frame.key_matchers.insert( global_id.clone(), window - .prev_frame_key_matchers + .previous_frame + .key_matchers .remove(&global_id) .unwrap_or_else(|| KeyMatcher::new(keymap)), ); @@ -1539,9 +1525,12 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { f: impl FnOnce(&mut Self) -> R, ) -> R { let mask = mask.intersect(&self.content_mask()); - self.window_mut().content_mask_stack.push(mask); + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); let result = f(self); - self.window_mut().content_mask_stack.pop(); + self.window_mut().current_frame.content_mask_stack.pop(); result } @@ -1557,15 +1546,19 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { }; let offset = self.element_offset() + offset; - self.window_mut().element_offset_stack.push(offset); + self.window_mut() + .current_frame + .element_offset_stack + .push(offset); let result = f(self); - self.window_mut().element_offset_stack.pop(); + self.window_mut().current_frame.element_offset_stack.pop(); result } /// Obtain the current element offset. fn element_offset(&self) -> Point { self.window() + .current_frame .element_offset_stack .last() .copied() @@ -1587,9 +1580,15 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { self.with_element_id(id, |global_id, cx| { if let Some(any) = cx .window_mut() + .current_frame .element_states .remove(&global_id) - .or_else(|| cx.window_mut().prev_frame_element_states.remove(&global_id)) + .or_else(|| { + cx.window_mut() + .previous_frame + .element_states + .remove(&global_id) + }) { // Using the extra inner option to avoid needing to reallocate a new box. let mut state_box = any @@ -1600,11 +1599,15 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .expect("element state is already on the stack"); let (result, state) = f(Some(state), cx); state_box.replace(state); - cx.window_mut().element_states.insert(global_id, state_box); + cx.window_mut() + .current_frame + .element_states + .insert(global_id, state_box); result } else { let (result, state) = f(None, cx); cx.window_mut() + .current_frame .element_states .insert(global_id, Box::new(Some(state))); result @@ -1632,6 +1635,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { /// Obtain the current content mask. fn content_mask(&self) -> ContentMask { self.window() + .current_frame .content_mask_stack .last() .cloned() @@ -1716,9 +1720,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { } pub fn with_z_index(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { - self.window.z_index_stack.push(z_index); + self.window.current_frame.z_index_stack.push(z_index); let result = f(self); - self.window.z_index_stack.pop(); + self.window.current_frame.z_index_stack.pop(); result } @@ -1873,11 +1877,14 @@ impl<'a, V: 'static> ViewContext<'a, V> { listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, ) { let handle = self.view().downgrade(); - self.window.focus_listeners.push(Box::new(move |event, cx| { - handle - .update(cx, |view, cx| listener(view, event, cx)) - .log_err(); - })); + self.window + .current_frame + .focus_listeners + .push(Box::new(move |event, cx| { + handle + .update(cx, |view, cx| listener(view, event, cx)) + .log_err(); + })); } pub fn with_key_listeners( @@ -1885,8 +1892,8 @@ impl<'a, V: 'static> ViewContext<'a, V> { key_listeners: impl IntoIterator)>, f: impl FnOnce(&mut Self) -> R, ) -> R { - let old_stack_len = self.window.key_dispatch_stack.len(); - if !self.window.freeze_key_dispatch_stack { + let old_stack_len = self.window.current_frame.key_dispatch_stack.len(); + if !self.window.current_frame.freeze_key_dispatch_stack { for (event_type, listener) in key_listeners { let handle = self.view().downgrade(); let listener = Box::new( @@ -1902,19 +1909,22 @@ impl<'a, V: 'static> ViewContext<'a, V> { .flatten() }, ); - self.window - .key_dispatch_stack - .push(KeyDispatchStackFrame::Listener { + self.window.current_frame.key_dispatch_stack.push( + KeyDispatchStackFrame::Listener { event_type, listener, - }); + }, + ); } } let result = f(self); - if !self.window.freeze_key_dispatch_stack { - self.window.key_dispatch_stack.truncate(old_stack_len); + if !self.window.current_frame.freeze_key_dispatch_stack { + self.window + .current_frame + .key_dispatch_stack + .truncate(old_stack_len); } result @@ -1929,16 +1939,17 @@ impl<'a, V: 'static> ViewContext<'a, V> { return f(self); } - if !self.window.freeze_key_dispatch_stack { + if !self.window.current_frame.freeze_key_dispatch_stack { self.window + .current_frame .key_dispatch_stack .push(KeyDispatchStackFrame::Context(context)); } let result = f(self); - if !self.window.freeze_key_dispatch_stack { - self.window.key_dispatch_stack.pop(); + if !self.window.previous_frame.freeze_key_dispatch_stack { + self.window.previous_frame.key_dispatch_stack.pop(); } result @@ -1949,20 +1960,21 @@ impl<'a, V: 'static> ViewContext<'a, V> { focus_handle: FocusHandle, f: impl FnOnce(&mut Self) -> R, ) -> R { - if let Some(parent_focus_id) = self.window.focus_stack.last().copied() { + if let Some(parent_focus_id) = self.window.current_frame.focus_stack.last().copied() { self.window + .current_frame .focus_parents_by_child .insert(focus_handle.id, parent_focus_id); } - self.window.focus_stack.push(focus_handle.id); + self.window.current_frame.focus_stack.push(focus_handle.id); if Some(focus_handle.id) == self.window.focus { - self.window.freeze_key_dispatch_stack = true; + self.window.current_frame.freeze_key_dispatch_stack = true; } let result = f(self); - self.window.focus_stack.pop(); + self.window.current_frame.focus_stack.pop(); result } From 2fd8b1f4893dee41ea167ab4ab612711bd0dc8fe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 8 Nov 2023 19:03:57 +0100 Subject: [PATCH 07/27] Fix blinking behavior in editor when receiving/losing focus Co-Authored-By: Marshall --- crates/editor2/src/blink_manager.rs | 4 + crates/editor2/src/editor.rs | 47 ++++++++- crates/gpui2/src/app.rs | 20 +++- crates/gpui2/src/window.rs | 152 ++++++++++++++++++++++++---- 4 files changed, 195 insertions(+), 28 deletions(-) diff --git a/crates/editor2/src/blink_manager.rs b/crates/editor2/src/blink_manager.rs index d25e30f649..0fc748f48a 100644 --- a/crates/editor2/src/blink_manager.rs +++ b/crates/editor2/src/blink_manager.rs @@ -85,6 +85,10 @@ impl BlinkManager { } pub fn enable(&mut self, cx: &mut ModelContext) { + if self.enabled { + return; + } + self.enabled = true; // Set cursors as invisible and start blinking: this causes cursors // to be visible during the next render. diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ea76a7b57d..049d304750 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1920,9 +1920,15 @@ impl Editor { cx, ); + 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(); + let mut this = Self { handle: cx.view().downgrade(), - focus_handle: cx.focus_handle(), + focus_handle, buffer: buffer.clone(), display_map: display_map.clone(), selections, @@ -9195,6 +9201,45 @@ impl Editor { pub fn focus(&self, cx: &mut WindowContext) { cx.focus(&self.focus_handle) } + + 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(Event::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) { + self.blink_manager.update(cx, BlinkManager::enable); + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + if self.leader_peer_id.is_none() { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ); + } + }); + } + } + + fn handle_focus_out(&mut self, cx: &mut ViewContext) { + // todo!() + // let blurred_event = EditorBlurred(cx.handle()); + // cx.emit_global(blurred_event); + self.blink_manager.update(cx, BlinkManager::disable); + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + self.hide_context_menu(cx); + hide_hover(self, cx); + cx.emit(Event::Blurred); + cx.notify(); + } } pub trait CollaborationHub { diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 79f80f474d..b38c403cbc 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -518,8 +518,9 @@ impl AppContext { ) { window_handle .update(self, |_, cx| { + // The window might change focus multiple times in an effect cycle. + // We only honor effects for the most recently focused handle. if cx.window.focus == focused { - let mut listeners = mem::take(&mut cx.window.current_frame.focus_listeners); let focused = focused .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); let blurred = cx @@ -528,15 +529,24 @@ impl AppContext { .take() .unwrap() .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); - if focused.is_some() || blurred.is_some() { - let event = FocusEvent { focused, blurred }; - for listener in &listeners { + let focus_changed = focused.is_some() || blurred.is_some(); + let event = FocusEvent { focused, blurred }; + + let mut listeners = mem::take(&mut cx.window.current_frame.focus_listeners); + if focus_changed { + for listener in &mut listeners { listener(&event, cx); } } - listeners.extend(cx.window.current_frame.focus_listeners.drain(..)); cx.window.current_frame.focus_listeners = listeners; + + if focus_changed { + cx.window + .focus_listeners + .clone() + .retain(&(), |listener| listener(&event, cx)); + } } }) .ok(); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0dae6171d9..f9a5d59acd 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -60,7 +60,7 @@ pub enum DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyListener = Box; +type AnyListener = Box; type AnyKeyListener = Box< dyn Fn( &dyn Any, @@ -71,9 +71,49 @@ type AnyKeyListener = Box< + 'static, >; type AnyFocusListener = Box; +type AnyWindowFocusListener = Box bool + 'static>; slotmap::new_key_type! { pub struct FocusId; } +impl FocusId { + /// Obtains whether the element associated with this handle is currently focused. + pub fn is_focused(&self, cx: &WindowContext) -> bool { + cx.window.focus == Some(*self) + } + + /// Obtains whether the element associated with this handle contains the focused + /// element or is itself focused. + pub fn contains_focused(&self, cx: &WindowContext) -> bool { + cx.focused() + .map_or(false, |focused| self.contains(focused.id, cx)) + } + + /// Obtains whether the element associated with this handle is contained within the + /// focused element or is itself focused. + pub fn within_focused(&self, cx: &WindowContext) -> bool { + let focused = cx.focused(); + focused.map_or(false, |focused| focused.id.contains(*self, cx)) + } + + /// Obtains whether this handle contains the given handle in the most recently rendered frame. + pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { + let mut ancestor = Some(other); + while let Some(ancestor_id) = ancestor { + if *self == ancestor_id { + return true; + } else { + ancestor = cx + .window + .current_frame + .focus_parents_by_child + .get(&ancestor_id) + .copied(); + } + } + false + } +} + /// A handle which can be used to track and manipulate the focused element in a window. pub struct FocusHandle { pub(crate) id: FocusId, @@ -108,39 +148,24 @@ impl FocusHandle { /// Obtains whether the element associated with this handle is currently focused. pub fn is_focused(&self, cx: &WindowContext) -> bool { - cx.window.focus == Some(self.id) + self.id.is_focused(cx) } /// Obtains whether the element associated with this handle contains the focused /// element or is itself focused. pub fn contains_focused(&self, cx: &WindowContext) -> bool { - cx.focused() - .map_or(false, |focused| self.contains(&focused, cx)) + self.id.contains_focused(cx) } /// Obtains whether the element associated with this handle is contained within the /// focused element or is itself focused. pub fn within_focused(&self, cx: &WindowContext) -> bool { - let focused = cx.focused(); - focused.map_or(false, |focused| focused.contains(self, cx)) + self.id.within_focused(cx) } /// Obtains whether this handle contains the given handle in the most recently rendered frame. pub(crate) fn contains(&self, other: &Self, cx: &WindowContext) -> bool { - let mut ancestor = Some(other.id); - while let Some(ancestor_id) = ancestor { - if self.id == ancestor_id { - return true; - } else { - ancestor = cx - .window - .current_frame - .focus_parents_by_child - .get(&ancestor_id) - .copied(); - } - } - false + self.id.contains(other.id, cx) } } @@ -183,6 +208,7 @@ pub struct Window { pub(crate) previous_frame: Frame, pub(crate) current_frame: Frame, pub(crate) focus_handles: Arc>>, + pub(crate) focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, default_prevented: bool, mouse_position: Point, requested_cursor_style: Option, @@ -282,6 +308,7 @@ impl Window { previous_frame: Frame::default(), current_frame: Frame::default(), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), + focus_listeners: SubscriberSet::new(), default_prevented: true, mouse_position, requested_cursor_style: None, @@ -1116,7 +1143,7 @@ impl<'a> WindowContext<'a> { // Capture phase, events bubble from back to front. Handlers for this phase are used for // special purposes, such as detecting events outside of a given Bounds. - for (_, handler) in &handlers { + for (_, handler) in &mut handlers { handler(any_mouse_event, DispatchPhase::Capture, self); if !self.app.propagate_event { break; @@ -1125,7 +1152,7 @@ impl<'a> WindowContext<'a> { // Bubble phase, where most normal handlers do their work. if self.app.propagate_event { - for (_, handler) in handlers.iter().rev() { + for (_, handler) in handlers.iter_mut().rev() { handler(any_mouse_event, DispatchPhase::Bubble, self); if !self.app.propagate_event { break; @@ -1872,6 +1899,87 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } + /// Register a listener to be called when the given focus handle receives focus. + /// Unlike [on_focus_changed], returns a subscription and persists until the subscription + /// is dropped. + pub fn on_focus( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + self.window.focus_listeners.insert( + (), + Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event.focused.as_ref().map(|focused| focused.id) == Some(focus_id) { + listener(view, cx) + } + }) + .is_ok() + }), + ) + } + + /// Register a listener to be called when the given focus handle or one of its descendants receives focus. + /// Unlike [on_focus_changed], returns a subscription and persists until the subscription + /// is dropped. + pub fn on_focus_in( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + self.window.focus_listeners.insert( + (), + Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event + .focused + .as_ref() + .map_or(false, |focused| focus_id.contains(focused.id, cx)) + { + listener(view, cx) + } + }) + .is_ok() + }), + ) + } + + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. + /// Unlike [on_focus_changed], returns a subscription and persists until the subscription + /// is dropped. + pub fn on_focus_out( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + self.window.focus_listeners.insert( + (), + Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event + .blurred + .as_ref() + .map_or(false, |focused| focus_id.contains(focused.id, cx)) + { + listener(view, cx) + } + }) + .is_ok() + }), + ) + } + + /// Register a focus listener for the current frame only. It will be cleared + /// on the next frame render. You should use this method only from within elements, + /// and we may want to enforce that better via a different context type. + // todo!() Move this to `FrameContext` to emphasize its individuality? pub fn on_focus_changed( &mut self, listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, From 14b41d657dd26a8842bea59da5d89d6f3ba19d7f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 8 Nov 2023 19:09:01 +0100 Subject: [PATCH 08/27] Introduce `ViewContext::on_blur` Co-Authored-By: Marshall --- crates/gpui2/src/scene.rs | 4 ---- crates/gpui2/src/window.rs | 25 ++++++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 918c3c8351..87e89adfa0 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -43,10 +43,6 @@ impl Default for SceneBuilder { } impl SceneBuilder { - pub fn new() -> SceneBuilder { - SceneBuilder::default() - } - pub fn build(&mut self) -> Scene { // Map each layer id to a float between 0. and 1., with 1. closer to the viewer. let mut layer_z_values = vec![0.; self.layers_by_order.len()]; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index f9a5d59acd..38e7c56eeb 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1949,6 +1949,29 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } + /// Register a listener to be called when the given focus handle loses focus. + /// Unlike [on_focus_changed], returns a subscription and persists until the subscription + /// is dropped. + pub fn on_blur( + &mut self, + handle: &FocusHandle, + mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, + ) -> Subscription { + let view = self.view.downgrade(); + let focus_id = handle.id; + self.window.focus_listeners.insert( + (), + Box::new(move |event, cx| { + view.update(cx, |view, cx| { + if event.blurred.as_ref().map(|blurred| blurred.id) == Some(focus_id) { + listener(view, cx) + } + }) + .is_ok() + }), + ) + } + /// Register a listener to be called when the given focus handle or one of its descendants loses focus. /// Unlike [on_focus_changed], returns a subscription and persists until the subscription /// is dropped. @@ -1966,7 +1989,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { if event .blurred .as_ref() - .map_or(false, |focused| focus_id.contains(focused.id, cx)) + .map_or(false, |blurred| focus_id.contains(blurred.id, cx)) { listener(view, cx) } From 097efdebc5f564ce8fa692e4958aa5fd4caa770f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 12:49:09 -0800 Subject: [PATCH 09/27] WIP --- Cargo.lock | 1 + crates/go_to_line2/src/go_to_line.rs | 98 ++++++++++++++-------------- crates/gpui2/src/action.rs | 1 + crates/menu2/src/menu2.rs | 7 +- crates/workspace2/src/modal_layer.rs | 19 +++++- crates/zed2/Cargo.toml | 1 + crates/zed2/src/main.rs | 4 ++ 7 files changed, 79 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcbf5a0f95..1e6d24df8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11279,6 +11279,7 @@ dependencies = [ "libc", "log", "lsp2", + "menu2", "node_runtime", "num_cpus", "parking_lot 0.11.2", diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index bb705427b1..f6481fdbd9 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, Styled, View, - ViewContext, VisualContext, + actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, + StatelessInteractive, Styled, View, ViewContext, VisualContext, }; use text::Point; use theme::ActiveTheme; @@ -9,7 +9,7 @@ use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalRegistry; -actions!(Toggle, Cancel, Confirm); +actions!(Toggle); pub fn init(cx: &mut AppContext) { cx.global_mut::() @@ -20,10 +20,6 @@ pub fn init(cx: &mut AppContext) { Some(cx.build_view(|cx| GoToLine::new(editor, cx))) }); - - // cx.add_action(GoToLine::toggle); - // cx.add_action(GoToLine::confirm); - // cx.add_action(GoToLine::cancel); } pub struct GoToLine { @@ -37,7 +33,7 @@ pub enum Event { } impl EventEmitter for GoToLine { - type Event = Event; + type Event = ModalEvent; } impl GoToLine { @@ -45,6 +41,7 @@ impl GoToLine { let line_editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); editor.set_placeholder_text("Find something", cx); + editor.focus(cx); editor }); cx.subscribe(&line_editor, Self::on_line_editor_event) @@ -98,23 +95,24 @@ impl GoToLine { )) } - // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - // cx.emit(Event::Dismissed); - // } + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + println!("CANCLE"); + cx.emit(Event::Dismissed); + } - // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - // if let Some(point) = self.point_from_query(cx) { - // self.active_editor.update(cx, |active_editor, cx| { - // let snapshot = active_editor.snapshot(cx).display_snapshot; - // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - // active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - // s.select_ranges([point..point]) - // }); - // }); - // } + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // // if let Some(point) = self.point_from_query(cx) { + // // self.active_editor.update(cx, |active_editor, cx| { + // // let snapshot = active_editor.snapshot(cx).display_snapshot; + // // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + // // active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + // // s.select_ranges([point..point]) + // // }); + // // }); + // // } - // cx.emit(Event::Dismissed); - // } + // cx.emit(Event::Dismissed); + } fn status_text(&self) -> SharedString { "Default text".into() @@ -125,31 +123,35 @@ impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - modal(cx).w_96().child( - v_stack() - .px_1() - .pt_0p5() - .gap_px() - .child( - v_stack() - .py_0p5() - .px_1() - .child(div().px_1().py_0p5().child(self.line_editor.clone())), - ) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().element_background), - ) - .child( - h_stack() - .justify_between() - .px_2() - .py_1() - .child(Label::new(self.status_text()).color(LabelColor::Muted)), - ), - ) + modal(cx) + .w_96() + .on_action(Self::cancel) + .on_action(Self::confirm) + .child( + v_stack() + .px_1() + .pt_0p5() + .gap_px() + .child( + v_stack() + .py_0p5() + .px_1() + .child(div().px_1().py_0p5().child(self.line_editor.clone())), + ) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) + .child( + h_stack() + .justify_between() + .px_2() + .py_1() + .child(Label::new(self.status_text()).color(LabelColor::Muted)), + ), + ) } } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 4d89ba1826..85149f5d55 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -123,6 +123,7 @@ pub fn register_action() { /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action(name: &str, params: Option) -> Result> { let lock = ACTION_REGISTRY.read(); + let build_action = lock .builders_by_name .get(name) diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index e5e8242f37..eafb1e295f 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,4 +1,9 @@ -use gpui::actions; +use gpui::{actions, ctor}; + +// todo!(remove this) +// https://github.com/rust-lang/rust/issues/47384 +// https://github.com/mmastrac/rust-ctor/issues/280 +pub fn unused() {} actions!( Cancel, diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 694b4e7ffe..55edab353b 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,7 +1,7 @@ use crate::Workspace; use gpui::{ div, px, AnyView, AppContext, Component, Div, ParentElement, Render, StatelessInteractive, - Styled, View, ViewContext, + Styled, View, ViewContext, EventEmitter, }; use std::{any::TypeId, sync::Arc}; use ui::v_stack; @@ -27,10 +27,18 @@ struct ToggleModal { name: String, } +pub enum ModalEvents { + Dismissed +} + +trait Modal: EventEmitter + Render { + fn to_modal_events(&Self::Event) -> Option; +} + impl ModalRegistry { pub fn register_modal(&mut self, action: A, build_view: B) where - V: Render, + V: Modal, B: Fn(&Workspace, &mut ViewContext) -> Option> + 'static, { let build_view = Arc::new(build_view); @@ -45,10 +53,15 @@ impl ModalRegistry { let Some(new_modal) = (build_view)(workspace, cx) else { return; }; - workspace.modal_layer.update(cx, |modal_layer, _| { modal_layer.open_modal = Some(new_modal.into()); }); + cx.subscribe(new_modal, |e, modal, cx| { + match modal.to_modal_events(e) { + Some(Dismissed) => + dismissed -> whatever + } + }) cx.notify(); }, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 2c93de734e..f7cb69c73a 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -48,6 +48,7 @@ journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } # language_selector = { path = "../language_selector" } lsp = { package = "lsp2", path = "../lsp2" } +menu = { package = "menu2", path = "../menu2" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } # assistant = { path = "../assistant" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 7f25a6a5d4..cd0f8e5fbf 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -56,6 +56,10 @@ use zed2::{ mod open_listener; fn main() { + //TODO!(figure out what the linker issues are here) + // https://github.com/rust-lang/rust/issues/47384 + // https://github.com/mmastrac/rust-ctor/issues/280 + menu::unused(); let http = http::client(); init_paths(); init_logger(); From 6a802e2fdab675512fc63fe6484f7f31583206fa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 8 Nov 2023 14:45:36 -0700 Subject: [PATCH 10/27] Make Modals dismissable in theory --- crates/go_to_line2/src/go_to_line.rs | 14 ++++-- crates/menu2/src/menu2.rs | 2 +- crates/workspace2/src/modal_layer.rs | 75 +++++++++++++++------------- crates/workspace2/src/workspace2.rs | 8 +-- 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index f6481fdbd9..9d14e2d2d6 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -7,7 +7,7 @@ use text::Point; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::ModalRegistry; +use workspace::{ModalRegistry, Modal, ModalEvent}; actions!(Toggle); @@ -33,7 +33,7 @@ pub enum Event { } impl EventEmitter for GoToLine { - type Event = ModalEvent; + type Event = Event; } impl GoToLine { @@ -100,7 +100,7 @@ impl GoToLine { cx.emit(Event::Dismissed); } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + fn confirm(&mut self, _: &menu::Confirm, _cx: &mut ViewContext) { // // if let Some(point) = self.point_from_query(cx) { // // self.active_editor.update(cx, |active_editor, cx| { // // let snapshot = active_editor.snapshot(cx).display_snapshot; @@ -119,6 +119,14 @@ impl GoToLine { } } +impl Modal for GoToLine { + fn to_modal_event(&self, e: &Self::Event) -> Option { + match e { + Event::Dismissed => Some(ModalEvent::Dismissed), + } + } +} + impl Render for GoToLine { type Element = Div; diff --git a/crates/menu2/src/menu2.rs b/crates/menu2/src/menu2.rs index eafb1e295f..44ebffcca2 100644 --- a/crates/menu2/src/menu2.rs +++ b/crates/menu2/src/menu2.rs @@ -1,4 +1,4 @@ -use gpui::{actions, ctor}; +use gpui::actions; // todo!(remove this) // https://github.com/rust-lang/rust/issues/47384 diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 55edab353b..c46e8f7acc 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,7 +1,7 @@ use crate::Workspace; use gpui::{ - div, px, AnyView, AppContext, Component, Div, ParentElement, Render, StatelessInteractive, - Styled, View, ViewContext, EventEmitter, + div, px, AnyView, AppContext, Component, Div, EventEmitter, ParentElement, Render, + StatelessInteractive, Styled, Subscription, View, ViewContext, WeakView, }; use std::{any::TypeId, sync::Arc}; use ui::v_stack; @@ -10,11 +10,10 @@ pub struct ModalRegistry { registered_modals: Vec<(TypeId, Box) -> Div>)>, } -pub trait Modal {} - -#[derive(Clone)] pub struct ModalLayer { + workspace: WeakView, open_modal: Option, + subscription: Option, } pub fn init_modal_registry(cx: &mut AppContext) { @@ -23,23 +22,19 @@ pub fn init_modal_registry(cx: &mut AppContext) { }); } -struct ToggleModal { - name: String, +pub enum ModalEvent { + Dismissed, } -pub enum ModalEvents { - Dismissed -} - -trait Modal: EventEmitter + Render { - fn to_modal_events(&Self::Event) -> Option; +pub trait Modal: EventEmitter + Render { + fn to_modal_event(&self, _: &Self::Event) -> Option; } impl ModalRegistry { pub fn register_modal(&mut self, action: A, build_view: B) where V: Modal, - B: Fn(&Workspace, &mut ViewContext) -> Option> + 'static, + B: Fn(&mut Workspace, &mut ViewContext) -> Option> + 'static, { let build_view = Arc::new(build_view); @@ -48,35 +43,47 @@ impl ModalRegistry { Box::new(move |mut div| { let build_view = build_view.clone(); - div.on_action( - move |workspace: &mut Workspace, event: &A, cx: &mut ViewContext| { - let Some(new_modal) = (build_view)(workspace, cx) else { - return; + div.on_action(move |workspace, event: &A, cx| { + let Some(new_modal) = + (build_view)(workspace, cx) else { + return }; - workspace.modal_layer.update(cx, |modal_layer, _| { - modal_layer.open_modal = Some(new_modal.into()); - }); - cx.subscribe(new_modal, |e, modal, cx| { - match modal.to_modal_events(e) { - Some(Dismissed) => - dismissed -> whatever - } - }) - - cx.notify(); - }, - ) + workspace.modal_layer.update(cx, |modal_layer, cx| { + modal_layer.show_modal(new_modal, cx); + }) + }) }), )); } } impl ModalLayer { - pub fn new() -> Self { - Self { open_modal: None } + pub fn new(workspace: WeakView) -> Self { + Self { + workspace, + open_modal: None, + subscription: None, + } } - pub fn render(&self, workspace: &Workspace, cx: &ViewContext) -> Div { + pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) { + self.subscription = Some(cx.subscribe(&new_modal, |this, modal, e, cx| { + match modal.read(cx).to_modal_event(e) { + Some(ModalEvent::Dismissed) => this.hide_modal(cx), + None => {} + } + })); + self.open_modal = Some(new_modal.into()); + cx.notify(); + } + + pub fn hide_modal(&mut self, cx: &mut ViewContext) { + self.open_modal.take(); + self.subscription.take(); + cx.notify(); + } + + pub fn render(&self, cx: &ViewContext) -> Div { let mut parent = div().relative().size_full(); for (_, action) in cx.global::().registered_modals.iter() { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 42751ed632..4eda0e2af6 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -46,8 +46,7 @@ use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, use itertools::Itertools; use language2::LanguageRegistry; use lazy_static::lazy_static; -pub use modal_layer::ModalRegistry; -use modal_layer::{init_modal_registry, ModalLayer}; +pub use modal_layer::*; use node_runtime::NodeRuntime; use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; pub use pane::*; @@ -697,7 +696,8 @@ impl Workspace { status_bar }); - let modal_layer = cx.build_view(|cx| ModalLayer::new()); + let workspace_handle = cx.view().downgrade(); + let modal_layer = cx.build_view(|cx| ModalLayer::new(workspace_handle)); // todo!() // cx.update_default_global::, _, _>(|drag_and_drop, _| { @@ -3709,7 +3709,7 @@ impl Render for Workspace { .child( self.modal_layer .read(cx) - .render(self, cx) + .render(cx) .relative() .flex_1() .w_full() From d273fa6dd0bf063765318582c61d4120a0b211d6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 13:52:42 -0800 Subject: [PATCH 11/27] Fix DisplaySnapshot::x_for_point always returning 0 Co-authored-by: Marshall --- crates/editor2/src/display_map.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 808f0d340a..f808ffa702 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -572,7 +572,6 @@ impl DisplaySnapshot { ) -> Line { let mut runs = Vec::new(); let mut line = String::new(); - let mut ended_in_newline = false; let range = display_row..display_row + 1; for chunk in self.highlighted_chunks(range, false, &editor_style) { @@ -588,17 +587,18 @@ impl DisplaySnapshot { } else { Cow::Borrowed(&editor_style.text) }; - ended_in_newline = chunk.chunk.ends_with("\n"); runs.push(text_style.to_run(chunk.chunk.len())) } - // our pixel positioning logic assumes each line ends in \n, - // this is almost always true except for the last line which - // may have no trailing newline. - if !ended_in_newline && display_row == self.max_point().row() { - line.push_str("\n"); - runs.push(editor_style.text.to_run("\n".len())); + if line.ends_with('\n') { + line.pop(); + if let Some(last_run) = runs.last_mut() { + last_run.len -= 1; + if last_run.len == 0 { + runs.pop(); + } + } } let font_size = editor_style.text.font_size.to_pixels(*rem_size); From 1b9f76c01dfd2d8462d7a1acb51f4697fcf4eef2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 8 Nov 2023 16:23:05 -0700 Subject: [PATCH 12/27] Refactor GoToLine to use cx.observe_new_views() --- crates/editor2/src/editor.rs | 3 +- crates/go_to_line2/src/go_to_line.rs | 53 +++++++++++++++++++------ crates/gpui2/src/app.rs | 25 +++++++++++- crates/gpui2/src/app/async_context.rs | 2 +- crates/gpui2/src/gpui2.rs | 2 +- crates/gpui2/src/interactive.rs | 4 ++ crates/gpui2/src/window.rs | 15 +++++-- crates/workspace2/src/dock.rs | 8 ++++ crates/workspace2/src/modal_layer.rs | 57 ++++++++++----------------- crates/workspace2/src/toolbar.rs | 11 +++++- crates/workspace2/src/workspace2.rs | 21 +++++----- 11 files changed, 136 insertions(+), 65 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 4361ed53d0..a34f7d734d 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -57,6 +57,7 @@ use language::{ Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; +use lazy_static::lazy_static; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; use lsp::{DiagnosticSeverity, Documentation, LanguageServerId}; use movement::TextLayoutDetails; @@ -66,7 +67,7 @@ pub use multi_buffer::{ ToPoint, }; use ordered_float::OrderedFloat; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use project::{FormatTrigger, Location, Project}; use rand::prelude::*; use rpc::proto::*; diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 9d14e2d2d6..c9a04b408f 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,25 +1,55 @@ use editor::Editor; use gpui::{ actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatelessInteractive, Styled, View, ViewContext, VisualContext, + StatefulInteractivity, StatelessInteractive, Styled, View, ViewContext, VisualContext, }; use text::Point; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{ModalRegistry, Modal, ModalEvent}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.global_mut::() - .register_modal(Toggle, |workspace, cx| { - let editor = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::())?; + cx.observe_new_views( + |workspace: &mut Workspace, _: &mut ViewContext| { + workspace + .modal_layer() + .register_modal(Toggle, |workspace, cx| { + let editor = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::())?; - Some(cx.build_view(|cx| GoToLine::new(editor, cx))) - }); + Some(cx.build_view(|cx| GoToLine::new(editor, cx))) + }); + dbg!("hey!"); + }, + ) + .detach(); + + // // cx.window_global() + // // cx.window_global:: + // // cx.window_global::() + // Workspace::on_init(|workspace, cx| { + // workspace.on_open_item() + // }); + + // Editor::on_init(|editor, cx|{ + + // }) + + // Editor::register_action(|_editor, _: &Toggle, cx| { + // dbg!("HEY!"); + // // let editor = cx.view(); + // // cx.update_window(cx.window().handle(), |cx, view| { + // // let workspace = view.downcast::(); + // // }) + // // workspace.show_modal(cx.build_view(|cx| GoToLine::new(editor, cx))) + // }) + // cx.global_mut::() + // .register_modal(Toggle, |workspace, cx| { + // }); } pub struct GoToLine { @@ -128,13 +158,14 @@ impl Modal for GoToLine { } impl Render for GoToLine { - type Element = Div; + type Element = Div>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { modal(cx) - .w_96() + .id("go to line") .on_action(Self::cancel) .on_action(Self::confirm) + .w_96() .child( v_stack() .px_1() diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index a3ab426321..1cb46e87c1 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -18,8 +18,8 @@ use crate::{ AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet, - Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, Window, - WindowContext, WindowHandle, WindowId, + Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, + Window, WindowContext, WindowHandle, WindowId, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; @@ -167,6 +167,7 @@ type Handler = Box bool + 'static>; type Listener = Box bool + 'static>; type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; type ReleaseListener = Box; +type NewViewListener = Box; // struct FrameConsumer { // next_frame_callbacks: Vec, @@ -193,6 +194,7 @@ pub struct AppContext { pub(crate) text_style_stack: Vec, pub(crate) globals_by_type: HashMap, pub(crate) entities: EntityMap, + pub(crate) new_view_observers: SubscriberSet, pub(crate) windows: SlotMap>, pub(crate) keymap: Arc>, pub(crate) global_action_listeners: @@ -251,6 +253,7 @@ impl AppContext { text_style_stack: Vec::new(), globals_by_type: HashMap::default(), entities, + new_view_observers: SubscriberSet::new(), windows: SlotMap::with_key(), keymap: Arc::new(Mutex::new(Keymap::default())), global_action_listeners: HashMap::default(), @@ -599,6 +602,7 @@ impl AppContext { fn apply_notify_effect(&mut self, emitter: EntityId) { self.pending_notifications.remove(&emitter); + self.observers .clone() .retain(&emitter, |handler| handler(self)); @@ -828,6 +832,23 @@ impl AppContext { self.globals_by_type.insert(global_type, lease.global); } + pub fn observe_new_views( + &mut self, + on_new: impl 'static + Fn(&mut V, &mut ViewContext), + ) -> Subscription { + self.new_view_observers.insert( + TypeId::of::(), + Box::new(move |any_view: AnyView, cx: &mut WindowContext| { + any_view + .downcast::() + .unwrap() + .update(cx, |view_state, cx| { + on_new(view_state, cx); + }) + }), + ) + } + pub fn observe_release( &mut self, handle: &E, diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index c05182444e..e191e7315f 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -258,7 +258,7 @@ impl VisualContext for AsyncWindowContext { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static, + V: 'static + Render, { self.window .update(self, |_, cx| cx.build_view(build_view_state)) diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index e253872ed4..91e4141735 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -112,7 +112,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static; + V: 'static + Render; fn update_view( &mut self, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b..40d247f32b 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -414,10 +414,14 @@ pub trait ElementInteractivity: 'static { Box::new(move |_, key_down, context, phase, cx| { if phase == DispatchPhase::Bubble { let key_down = key_down.downcast_ref::().unwrap(); + dbg!(key_down); if let KeyMatch::Some(action) = cx.match_keystroke(&global_id, &key_down.keystroke, context) { + dbg!(&action); return Some(action); + } else { + dbg!("none"); } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index cf138eb1ef..374e893037 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1411,7 +1411,7 @@ impl VisualContext for WindowContext<'_> { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -1419,7 +1419,16 @@ impl VisualContext for WindowContext<'_> { }; let mut cx = ViewContext::new(&mut *self.app, &mut *self.window, &view); let entity = build_view_state(&mut cx); - self.entities.insert(slot, entity); + cx.entities.insert(slot, entity); + + cx.new_view_observers + .clone() + .retain(&TypeId::of::(), |observer| { + let any_view = AnyView::from(view.clone()); + (observer)(any_view, self); + true + }); + view } @@ -2102,7 +2111,7 @@ impl Context for ViewContext<'_, V> { } impl VisualContext for ViewContext<'_, V> { - fn build_view( + fn build_view( &mut self, build_view_state: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index e6b6c7561d..635b48051a 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -407,6 +407,14 @@ impl Dock { // } } +impl Render for Dock { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } +} + // todo!() // impl View for Dock { // fn ui_name() -> &'static str { diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index c46e8f7acc..564db0f982 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,25 +1,15 @@ use crate::Workspace; use gpui::{ - div, px, AnyView, AppContext, Component, Div, EventEmitter, ParentElement, Render, - StatelessInteractive, Styled, Subscription, View, ViewContext, WeakView, + div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatelessInteractive, + Styled, Subscription, View, ViewContext, }; use std::{any::TypeId, sync::Arc}; use ui::v_stack; -pub struct ModalRegistry { - registered_modals: Vec<(TypeId, Box) -> Div>)>, -} - pub struct ModalLayer { - workspace: WeakView, open_modal: Option, subscription: Option, -} - -pub fn init_modal_registry(cx: &mut AppContext) { - cx.set_global(ModalRegistry { - registered_modals: Vec::new(), - }); + registered_modals: Vec<(TypeId, Box) -> Div>)>, } pub enum ModalEvent { @@ -30,7 +20,15 @@ pub trait Modal: EventEmitter + Render { fn to_modal_event(&self, _: &Self::Event) -> Option; } -impl ModalRegistry { +impl ModalLayer { + pub fn new() -> Self { + Self { + open_modal: None, + subscription: None, + registered_modals: Vec::new(), + } + } + pub fn register_modal(&mut self, action: A, build_view: B) where V: Modal, @@ -44,32 +42,19 @@ impl ModalRegistry { let build_view = build_view.clone(); div.on_action(move |workspace, event: &A, cx| { - let Some(new_modal) = - (build_view)(workspace, cx) else { - return - }; - workspace.modal_layer.update(cx, |modal_layer, cx| { - modal_layer.show_modal(new_modal, cx); - }) + let Some(new_modal) = (build_view)(workspace, cx) else { + return; + }; + workspace.modal_layer().show_modal(new_modal, cx); }) }), )); } -} -impl ModalLayer { - pub fn new(workspace: WeakView) -> Self { - Self { - workspace, - open_modal: None, - subscription: None, - } - } - - pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) { + pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) { self.subscription = Some(cx.subscribe(&new_modal, |this, modal, e, cx| { match modal.read(cx).to_modal_event(e) { - Some(ModalEvent::Dismissed) => this.hide_modal(cx), + Some(ModalEvent::Dismissed) => this.modal_layer().hide_modal(cx), None => {} } })); @@ -77,16 +62,16 @@ impl ModalLayer { cx.notify(); } - pub fn hide_modal(&mut self, cx: &mut ViewContext) { + pub fn hide_modal(&mut self, cx: &mut ViewContext) { self.open_modal.take(); self.subscription.take(); cx.notify(); } - pub fn render(&self, cx: &ViewContext) -> Div { + pub fn wrapper_element(&self, cx: &ViewContext) -> Div { let mut parent = div().relative().size_full(); - for (_, action) in cx.global::().registered_modals.iter() { + for (_, action) in self.registered_modals.iter() { parent = (action)(parent); } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 80503ad7bb..25054571da 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,6 +1,7 @@ use crate::ItemHandle; use gpui::{ - AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, + AnyView, AppContext, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, + WindowContext, }; pub trait ToolbarItemView: Render + EventEmitter { @@ -56,6 +57,14 @@ pub struct Toolbar { items: Vec<(Box, ToolbarItemLocation)>, } +impl Render for Toolbar { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } +} + // todo!() // impl View for Toolbar { // fn ui_name() -> &'static str { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 4eda0e2af6..e11bcdc163 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,10 +37,10 @@ use futures::{ }; use gpui::{ div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, - GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + AsyncWindowContext, Bounds, Component, Context, Div, Entity, EntityId, EventEmitter, + FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -225,7 +225,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); - init_modal_registry(cx); pane::init(cx); notifications::init(cx); @@ -545,7 +544,7 @@ pub struct Workspace { last_active_center_pane: Option>, last_active_view_id: Option, status_bar: View, - modal_layer: View, + modal_layer: ModalLayer, // titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, @@ -697,7 +696,7 @@ impl Workspace { }); let workspace_handle = cx.view().downgrade(); - let modal_layer = cx.build_view(|cx| ModalLayer::new(workspace_handle)); + let modal_layer = ModalLayer::new(); // todo!() // cx.update_default_global::, _, _>(|drag_and_drop, _| { @@ -781,6 +780,10 @@ impl Workspace { } } + pub fn modal_layer(&mut self) -> &mut ModalLayer { + &mut self.modal_layer + } + fn new_local( abs_paths: Vec, app_state: Arc, @@ -3707,9 +3710,9 @@ impl Render for Workspace { .bg(cx.theme().colors().background) .child(self.render_titlebar(cx)) .child( + // todo! should this be a component a view? self.modal_layer - .read(cx) - .render(cx) + .wrapper_element(cx) .relative() .flex_1() .w_full() From 1a37d9edc654b91ff36a59ed9a530eb0f55f8549 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 15:48:55 -0800 Subject: [PATCH 13/27] Register text input handlers via new element hook Provide element bounds to the input handler's `bounds_for_rect` method. Co-authored-by: Marshall --- crates/editor2/src/editor.rs | 3 +- crates/editor2/src/element.rs | 21 ++-- crates/gpui2/src/element.rs | 25 +++- crates/gpui2/src/platform.rs | 2 +- crates/gpui2/src/platform/mac/window.rs | 6 +- crates/gpui2/src/window.rs | 31 ++--- crates/gpui2/src/window_input_handler.rs | 138 ++++++++++++++++++----- 7 files changed, 160 insertions(+), 66 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 049d304750..9542eaddc6 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9752,8 +9752,9 @@ impl InputHandler for Editor { fn bounds_for_range( &self, range_utf16: Range, + element_bounds: gpui::Bounds, cx: &mut ViewContext, - ) -> Option> { + ) -> Option> { // todo!() // See how we did it before: `rect_for_range` None diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 04b8494a88..ab9aa2ccf3 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -17,10 +17,11 @@ use collections::{BTreeMap, HashMap}; use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, - Edges, Element, ElementId, Entity, GlobalElementId, Hsla, KeyDownEvent, KeyListener, KeyMatch, - Line, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, - WindowContext, + Edges, Element, ElementId, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, + InputHandlerView, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, + ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, + WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -2502,10 +2503,6 @@ impl Element for EditorElement { size: layout.text_size, }; - if editor.focus_handle.is_focused(cx) { - cx.handle_text_input(); - } - cx.with_content_mask(ContentMask { bounds }, |cx| { self.paint_mouse_listeners( bounds, @@ -2521,6 +2518,14 @@ impl Element for EditorElement { self.paint_text(text_bounds, &layout, editor, cx); }); } + + fn handle_text_input<'a>( + &self, + editor: &'a mut Editor, + cx: &mut ViewContext, + ) -> Option<(Box, &'a FocusHandle)> { + Some((Box::new(cx.view()), &editor.focus_handle)) + } } // impl EditorElement { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 8fdc17de07..a6067eb68d 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,4 +1,7 @@ -use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext}; +use crate::{ + BorrowWindow, Bounds, ElementId, FocusHandle, InputHandlerView, LayoutId, Pixels, ViewContext, + WindowInputHandler, +}; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; @@ -31,6 +34,14 @@ pub trait Element { element_state: &mut Self::ElementState, cx: &mut ViewContext, ); + + fn handle_text_input<'a>( + &self, + _view_state: &'a mut V, + _cx: &mut ViewContext, + ) -> Option<(Box, &'a FocusHandle)> { + None + } } #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] @@ -154,6 +165,18 @@ where mut frame_state, } => { let bounds = cx.layout_bounds(layout_id); + if let Some((input_handler, focus_handle)) = + self.element.handle_text_input(view_state, cx) + { + if focus_handle.is_focused(cx) { + cx.window.requested_input_handler = Some(Box::new(WindowInputHandler { + cx: cx.app.this.clone(), + window: cx.window_handle(), + input_handler, + element_bounds: bounds, + })); + } + } if let Some(id) = self.element.id() { cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index a4be21ddf3..5ebb12b64d 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -305,7 +305,7 @@ pub trait PlatformInputHandler { new_selected_range: Option>, ); fn unmark_text(&mut self); - fn bounds_for_range(&self, range_utf16: Range) -> Option>; + fn bounds_for_range(&self, range_utf16: Range) -> Option>; } #[derive(Debug)] diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index affeab57c7..bd45178e97 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1484,10 +1484,10 @@ extern "C" fn first_rect_for_character_range( |bounds| { NSRect::new( NSPoint::new( - frame.origin.x + bounds.origin.x as f64, - frame.origin.y + frame.size.height - bounds.origin.y as f64, + frame.origin.x + bounds.origin.x.0 as f64, + frame.origin.y + frame.size.height - bounds.origin.y.0 as f64, ), - NSSize::new(bounds.size.width as f64, bounds.size.height as f64), + NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), ) }, ) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fbaeae322b..b1e756fe6f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2,14 +2,14 @@ use crate::{ px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, - GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, InputHandler, IsZero, KeyListener, - KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, - PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, - Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowInputHandler, WindowOptions, SUBPIXEL_VARIANTS, + GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, + KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, + PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, + RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, + SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowInputHandler, WindowOptions, + SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -212,7 +212,7 @@ pub struct Window { default_prevented: bool, mouse_position: Point, requested_cursor_style: Option, - requested_input_handler: Option>, + pub(crate) requested_input_handler: Option>, scale_factor: f32, bounds: WindowBounds, bounds_observers: SubscriberSet<(), AnyObserver>, @@ -2168,19 +2168,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } -impl ViewContext<'_, V> -where - V: InputHandler + 'static, -{ - pub fn handle_text_input(&mut self) { - self.window.requested_input_handler = Some(Box::new(WindowInputHandler { - cx: self.app.this.clone(), - window: self.window_handle(), - handler: self.view().downgrade(), - })); - } -} - impl ViewContext<'_, V> where V: EventEmitter, diff --git a/crates/gpui2/src/window_input_handler.rs b/crates/gpui2/src/window_input_handler.rs index caae5838ce..3ce9f01dda 100644 --- a/crates/gpui2/src/window_input_handler.rs +++ b/crates/gpui2/src/window_input_handler.rs @@ -1,65 +1,142 @@ -use crate::{AnyWindowHandle, AppCell, Context, PlatformInputHandler, ViewContext, WeakView}; +use crate::{ + AnyWindowHandle, AppCell, Bounds, Context, Pixels, PlatformInputHandler, View, ViewContext, + WindowContext, +}; use std::{ops::Range, rc::Weak}; -pub struct WindowInputHandler -where - V: InputHandler, -{ +pub struct WindowInputHandler { pub cx: Weak, + pub input_handler: Box, pub window: AnyWindowHandle, - pub handler: WeakView, + pub element_bounds: Bounds, } -impl PlatformInputHandler for WindowInputHandler { - fn selected_text_range(&self) -> Option> { - self.update(|view, cx| view.selected_text_range(cx)) - .flatten() +pub trait InputHandlerView { + fn text_for_range(&self, range: Range, cx: &mut WindowContext) -> Option; + fn selected_text_range(&self, cx: &mut WindowContext) -> Option>; + fn marked_text_range(&self, cx: &mut WindowContext) -> Option>; + fn unmark_text(&self, cx: &mut WindowContext); + fn replace_text_in_range( + &self, + range: Option>, + text: &str, + cx: &mut WindowContext, + ); + fn replace_and_mark_text_in_range( + &self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut WindowContext, + ); + fn bounds_for_range( + &self, + range_utf16: std::ops::Range, + element_bounds: crate::Bounds, + cx: &mut WindowContext, + ) -> Option>; +} + +impl InputHandlerView for View { + fn text_for_range(&self, range: Range, cx: &mut WindowContext) -> Option { + self.update(cx, |this, cx| this.text_for_range(range, cx)) } - fn marked_text_range(&self) -> Option> { - self.update(|view, cx| view.marked_text_range(cx)).flatten() + fn selected_text_range(&self, cx: &mut WindowContext) -> Option> { + self.update(cx, |this, cx| this.selected_text_range(cx)) } - fn text_for_range(&self, range_utf16: std::ops::Range) -> Option { - self.update(|view, cx| view.text_for_range(range_utf16, cx)) - .flatten() + fn marked_text_range(&self, cx: &mut WindowContext) -> Option> { + self.update(cx, |this, cx| this.marked_text_range(cx)) + } + + fn unmark_text(&self, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.unmark_text(cx)) } fn replace_text_in_range( - &mut self, - replacement_range: Option>, + &self, + range: Option>, text: &str, + cx: &mut WindowContext, ) { - self.update(|view, cx| view.replace_text_in_range(replacement_range, text, cx)); + self.update(cx, |this, cx| this.replace_text_in_range(range, text, cx)) + } + + fn replace_and_mark_text_in_range( + &self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut WindowContext, + ) { + self.update(cx, |this, cx| { + this.replace_and_mark_text_in_range(range, new_text, new_selected_range, cx) + }) + } + + fn bounds_for_range( + &self, + range_utf16: std::ops::Range, + element_bounds: crate::Bounds, + cx: &mut WindowContext, + ) -> Option> { + self.update(cx, |this, cx| { + this.bounds_for_range(range_utf16, element_bounds, cx) + }) + } +} + +impl PlatformInputHandler for WindowInputHandler { + fn selected_text_range(&self) -> Option> { + self.update(|handler, cx| handler.selected_text_range(cx)) + .flatten() + } + + fn marked_text_range(&self) -> Option> { + self.update(|handler, cx| handler.marked_text_range(cx)) + .flatten() + } + + fn text_for_range(&self, range_utf16: Range) -> Option { + self.update(|handler, cx| handler.text_for_range(range_utf16, cx)) + .flatten() + } + + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.update(|handler, cx| handler.replace_text_in_range(replacement_range, text, cx)); } fn replace_and_mark_text_in_range( &mut self, - range_utf16: Option>, + range_utf16: Option>, new_text: &str, - new_selected_range: Option>, + new_selected_range: Option>, ) { - self.update(|view, cx| { - view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) + self.update(|handler, cx| { + handler.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) }); } fn unmark_text(&mut self) { - self.update(|view, cx| view.unmark_text(cx)); + self.update(|handler, cx| handler.unmark_text(cx)); } - fn bounds_for_range(&self, range_utf16: std::ops::Range) -> Option> { - self.update(|view, cx| view.bounds_for_range(range_utf16, cx)) + fn bounds_for_range(&self, range_utf16: Range) -> Option> { + self.update(|handler, cx| handler.bounds_for_range(range_utf16, self.element_bounds, cx)) .flatten() } } -impl WindowInputHandler { - fn update(&self, f: impl FnOnce(&mut V, &mut ViewContext) -> T) -> Option { +impl WindowInputHandler { + fn update( + &self, + f: impl FnOnce(&dyn InputHandlerView, &mut WindowContext) -> R, + ) -> Option { let cx = self.cx.upgrade()?; let mut cx = cx.borrow_mut(); - cx.update_window(self.window, |_, cx| self.handler.update(cx, f).ok()) - .ok()? + cx.update_window(self.window, |_, cx| f(&*self.input_handler, cx)) + .ok() } } @@ -84,6 +161,7 @@ pub trait InputHandler: Sized { fn bounds_for_range( &self, range_utf16: std::ops::Range, + element_bounds: crate::Bounds, cx: &mut ViewContext, - ) -> Option>; + ) -> Option>; } From cef8fa5570d3f32fe45a6d3dd7507b31fe420e63 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 8 Nov 2023 17:09:38 -0700 Subject: [PATCH 14/27] tidy tidy --- crates/go_to_line2/src/go_to_line.rs | 267 +++++++-------------------- crates/workspace2/src/workspace2.rs | 8 +- 2 files changed, 69 insertions(+), 206 deletions(-) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c9a04b408f..af69d42d41 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,9 +1,10 @@ -use editor::Editor; +use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatefulInteractivity, StatelessInteractive, Styled, View, ViewContext, VisualContext, + StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, + VisualContext, WindowContext, }; -use text::Point; +use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; @@ -23,39 +24,17 @@ pub fn init(cx: &mut AppContext) { Some(cx.build_view(|cx| GoToLine::new(editor, cx))) }); - dbg!("hey!"); }, ) .detach(); - - // // cx.window_global() - // // cx.window_global:: - // // cx.window_global::() - // Workspace::on_init(|workspace, cx| { - // workspace.on_open_item() - // }); - - // Editor::on_init(|editor, cx|{ - - // }) - - // Editor::register_action(|_editor, _: &Toggle, cx| { - // dbg!("HEY!"); - // // let editor = cx.view(); - // // cx.update_window(cx.window().handle(), |cx, view| { - // // let workspace = view.downcast::(); - // // }) - // // workspace.show_modal(cx.build_view(|cx| GoToLine::new(editor, cx))) - // }) - // cx.global_mut::() - // .register_modal(Toggle, |workspace, cx| { - // }); } pub struct GoToLine { line_editor: View, - #[allow(unused)] // todo!() active_editor: View, + current_text: SharedString, + prev_scroll_position: Option>, + _subscriptions: Vec, } pub enum Event { @@ -69,20 +48,45 @@ impl EventEmitter for GoToLine { impl GoToLine { pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { let line_editor = cx.build_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Find something", cx); + let editor = Editor::single_line(cx); editor.focus(cx); editor }); - cx.subscribe(&line_editor, Self::on_line_editor_event) - .detach(); + let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); + + let editor = active_editor.read(cx); + let cursor = editor.selections.last::(cx).head(); + let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; + let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); + + let current_text = format!( + "line {} of {} (column {})", + cursor.row + 1, + last_line + 1, + cursor.column + 1, + ); Self { line_editor, active_editor, + current_text: current_text.into(), + prev_scroll_position: Some(scroll_position), + _subscriptions: vec![line_editor_change, cx.on_release(Self::release)], } } + fn release(&mut self, cx: &mut WindowContext) { + let scroll_position = self.prev_scroll_position.take(); + self.active_editor.update(cx, |editor, cx| { + editor.focus(cx); + editor.highlight_rows(None); + if let Some(scroll_position) = scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + cx.notify(); + }) + } + fn on_line_editor_event( &mut self, _: View, @@ -90,28 +94,28 @@ impl GoToLine { cx: &mut ViewContext, ) { match event { + // todo!() this isn't working... editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::BufferEdited { .. } => { - // if let Some(point) = self.point_from_query(cx) { - // todo!() - // self.active_editor.update(cx, |active_editor, cx| { - // let snapshot = active_editor.snapshot(cx).display_snapshot; - // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - // let display_point = point.to_display_point(&snapshot); - // let row = display_point.row(); - // active_editor.highlight_rows(Some(row..row + 1)); - // active_editor.request_autoscroll(Autoscroll::center(), cx); - // }); - // cx.notify(); - // } - } + editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } } - #[allow(unused)] + fn highlight_current_line(&mut self, cx: &mut ViewContext) { + if let Some(point) = self.point_from_query(cx) { + self.active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + let display_point = point.to_display_point(&snapshot); + let row = display_point.row(); + active_editor.highlight_rows(Some(row..row + 1)); + active_editor.request_autoscroll(Autoscroll::center(), cx); + }); + cx.notify(); + } + } + fn point_from_query(&self, cx: &ViewContext) -> Option { - // todo!() let line_editor = self.line_editor.read(cx).text(cx); let mut components = line_editor .splitn(2, FILE_ROW_COLUMN_DELIMITER) @@ -126,26 +130,22 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - println!("CANCLE"); cx.emit(Event::Dismissed); } - fn confirm(&mut self, _: &menu::Confirm, _cx: &mut ViewContext) { - // // if let Some(point) = self.point_from_query(cx) { - // // self.active_editor.update(cx, |active_editor, cx| { - // // let snapshot = active_editor.snapshot(cx).display_snapshot; - // // let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - // // active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - // // s.select_ranges([point..point]) - // // }); - // // }); - // // } + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(point) = self.point_from_query(cx) { + self.active_editor.update(cx, |active_editor, cx| { + let snapshot = active_editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); + active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }); + self.prev_scroll_position.take(); + } - // cx.emit(Event::Dismissed); - } - - fn status_text(&self) -> SharedString { - "Default text".into() + cx.emit(Event::Dismissed); } } @@ -188,145 +188,8 @@ impl Render for GoToLine { .justify_between() .px_2() .py_1() - .child(Label::new(self.status_text()).color(LabelColor::Muted)), + .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)), ), ) } } - -// pub struct GoToLine { -// //line_editor: View, -// active_editor: View, -// prev_scroll_position: Option>, -// cursor_point: Point, -// max_point: Point, -// has_focus: bool, -// } - -// pub enum Event { -// Dismissed, -// } - -// impl GoToLine { -// pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - -// let (scroll_position, cursor_point, max_point) = active_editor.update(cx, |editor, cx| { -// let scroll_position = editor.scroll_position(cx); -// let buffer = editor.buffer().read(cx).snapshot(cx); -// ( -// Some(scroll_position), -// editor.selections.newest(cx).head(), -// buffer.max_point(), -// ) -// }); - -// cx.on_release(|_, on_release| {}).detach(); - -// Self { -// //line_editor, -// active_editor, -// prev_scroll_position: scroll_position, -// cursor_point, -// max_point, -// has_focus: false, -// } -// } - -// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { -// cx.emit(Event::Dismissed); -// } - -// fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { -// self.prev_scroll_position.take(); -// if let Some(point) = self.point_from_query(cx) { -// self.active_editor.update(cx, |active_editor, cx| { -// let snapshot = active_editor.snapshot(cx).display_snapshot; -// let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); -// active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { -// s.select_ranges([point..point]) -// }); -// }); -// } - -// cx.emit(Event::Dismissed); -// } - -// impl EventEmitter for GoToLine { -// type Event = Event; -// } - -// impl Entity for GoToLine { -// fn release(&mut self, cx: &mut AppContext) { -// let scroll_position = self.prev_scroll_position.take(); -// self.active_editor.window().update(cx, |cx| { -// self.active_editor.update(cx, |editor, cx| { -// editor.highlight_rows(None); -// if let Some(scroll_position) = scroll_position { -// editor.set_scroll_position(scroll_position, cx); -// } -// }) -// }); -// } -// } - -// impl Render for GoToLine { -// type Element = Div; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// // todo!() -// div() -// } -// } - -// impl View for GoToLine { -// fn ui_name() -> &'static str { -// "GoToLine" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let theme = &theme::current(cx).picker; - -// let label = format!( -// "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines", -// self.cursor_point.row + 1, -// self.cursor_point.column + 1, -// self.max_point.row + 1 -// ); - -// Flex::new(Axis::Vertical) -// .with_child( -// ChildView::new(&self.line_editor, cx) -// .contained() -// .with_style(theme.input_editor.container), -// ) -// .with_child( -// Label::new(label, theme.no_matches.label.clone()) -// .contained() -// .with_style(theme.no_matches.container), -// ) -// .contained() -// .with_style(theme.container) -// .constrained() -// .with_max_width(500.0) -// .into_any_named("go to line") -// } - -// fn focus_in(&mut self, _: AnyView, cx: &mut ViewContext) { -// self.has_focus = true; -// cx.focus(&self.line_editor); -// } - -// fn focus_out(&mut self, _: AnyView, _: &mut ViewContext) { -// self.has_focus = false; -// } -// } - -// impl Modal for GoToLine { -// fn has_focus(&self) -> bool { -// self.has_focus -// } - -// fn dismiss_on_event(event: &Self::Event) -> bool { -// matches!(event, Event::Dismissed) -// } -// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e11bcdc163..cfa0d6fa07 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,10 +37,10 @@ use futures::{ }; use gpui::{ div, point, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Component, Context, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, + GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, + Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; From 7a8f2192516a28e4ffc0d89449270d09d3e404d8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 16:02:10 -0800 Subject: [PATCH 15/27] Account for element's bounds in Editor::bounds_for_range Co-authored-by: Marshall --- crates/editor2/src/editor.rs | 40 +++++++++++++++++++----- crates/gpui2/src/window.rs | 3 +- crates/gpui2/src/window_input_handler.rs | 2 +- test.rs | 24 +++++++------- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 9542eaddc6..9d7c0b7fa3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,10 +39,10 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, px, relative, AnyElement, AppContext, BackgroundExecutor, ClipboardItem, - Context, DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, FontStyle, - FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, PlatformInputHandler, Render, - Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, + action, actions, div, point, px, relative, AnyElement, AppContext, BackgroundExecutor, Bounds, + ClipboardItem, Context, DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, + FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, PlatformInputHandler, + Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; @@ -9750,14 +9750,38 @@ impl InputHandler for Editor { } fn bounds_for_range( - &self, + &mut self, range_utf16: Range, element_bounds: gpui::Bounds, cx: &mut ViewContext, ) -> Option> { - // todo!() - // See how we did it before: `rect_for_range` - None + let text_layout_details = self.text_layout_details(cx); + let style = &text_layout_details.editor_style; + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + let snapshot = self.snapshot(cx); + let scroll_position = snapshot.scroll_position(); + let scroll_left = scroll_position.x * em_width; + + let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); + let end = OffsetUtf16(range_utf16.end).to_display_point(&snapshot); + let start_y = line_height * (start.row() as f32 - scroll_position.y); + let end_y = line_height * (end.row() as f32 - scroll_position.y); + let start_x = snapshot.x_for_point(start, &text_layout_details) - scroll_left; + let end_x = snapshot.x_for_point(end, &text_layout_details) - scroll_left; + + Some(Bounds::from_corners( + element_bounds.origin + point(start_x, start_y), + element_bounds.origin + point(end_x, end_y), + )) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b1e756fe6f..9a1e8916c4 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -8,8 +8,7 @@ use crate::{ PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, - UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowInputHandler, WindowOptions, - SUBPIXEL_VARIANTS, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; diff --git a/crates/gpui2/src/window_input_handler.rs b/crates/gpui2/src/window_input_handler.rs index 3ce9f01dda..d09a842a6b 100644 --- a/crates/gpui2/src/window_input_handler.rs +++ b/crates/gpui2/src/window_input_handler.rs @@ -159,7 +159,7 @@ pub trait InputHandler: Sized { cx: &mut ViewContext, ); fn bounds_for_range( - &self, + &mut self, range_utf16: std::ops::Range, element_bounds: crate::Bounds, cx: &mut ViewContext, diff --git a/test.rs b/test.rs index 4a7b9faa8f..36996263b0 100644 --- a/test.rs +++ b/test.rs @@ -10,49 +10,49 @@ use element::Element; use frame::frame; use gpui::{ geometry::{rect::RectF, vector::vec2f}, - platform::WindowOptions, + platform::WindowOptions,aa }; -use log::LevelFilter; +use log::LevelFilter;a use simplelog::SimpleLogger; use themes::{rose_pine, ThemeColors}; -use view::view; +use view::view;a mod adapter { use crate::element::AnyElement; use crate::element::{LayoutContext, PaintContext}; - use gpui::{geometry::rect::RectF, LayoutEngine}; + use gpui::{geometry::rect::RectF, LayoutEngine};aaaa use util::ResultExt; pub struct Adapter(pub(crate) AnyElement); - impl gpui::Element for Adapter { - type LayoutState = Option; + impl gpui::Element for Adapter {aa + type LayoutState = Option; type PaintState = (); fn layout( &mut self, constraint: gpui::SizeConstraint, view: &mut V, - cx: &mut LayoutContext, + cx: &mut LayoutContext,aa ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { cx.push_layout_engine(LayoutEngine::new()); - let node = self.0.layout(view, cx).log_err(); + let node = self.0.layout(view, cx).log_err();a if let Some(node) = node { let layout_engine = cx.layout_engine().unwrap(); layout_engine.compute_layout(node, constraint.max).log_err(); } let layout_engine = cx.pop_layout_engine(); - if true { + if true {a if !layout_engine.is_some() { ::core::panicking::panic("assertion failed: layout_engine.is_some()") } } - (constraint.max, layout_engine) + (constraint.max, layout_engine)a } - fn paint( + fn paint(a &mut self, scene: &mut gpui::SceneBuilder, bounds: RectF, visible_bounds: RectF, layout_engine: &mut Option, view: &mut V, - legacy_cx: &mut gpui::PaintContext, + legacy_cx: &mut gpui::PaintContext,aaa ) -> Self::PaintState { legacy_cx.push_layout_engine(layout_engine.take().unwrap()); let mut cx = PaintContext::new(legacy_cx, scene); From b77fab0fae3b5f799f1cfef52da9590a6cdd3031 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 16:24:11 -0800 Subject: [PATCH 16/27] :art: --- crates/gpui2/src/window_input_handler.rs | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/crates/gpui2/src/window_input_handler.rs b/crates/gpui2/src/window_input_handler.rs index d09a842a6b..f3ff33f3c0 100644 --- a/crates/gpui2/src/window_input_handler.rs +++ b/crates/gpui2/src/window_input_handler.rs @@ -37,6 +37,32 @@ pub trait InputHandlerView { ) -> Option>; } +pub trait InputHandler: Sized { + fn text_for_range(&self, range: Range, cx: &mut ViewContext) -> Option; + fn selected_text_range(&self, cx: &mut ViewContext) -> Option>; + fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; + fn unmark_text(&mut self, cx: &mut ViewContext); + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut ViewContext, + ); + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut ViewContext, + ); + fn bounds_for_range( + &mut self, + range_utf16: std::ops::Range, + element_bounds: crate::Bounds, + cx: &mut ViewContext, + ) -> Option>; +} + impl InputHandlerView for View { fn text_for_range(&self, range: Range, cx: &mut WindowContext) -> Option { self.update(cx, |this, cx| this.text_for_range(range, cx)) @@ -139,29 +165,3 @@ impl WindowInputHandler { .ok() } } - -pub trait InputHandler: Sized { - fn text_for_range(&self, range: Range, cx: &mut ViewContext) -> Option; - fn selected_text_range(&self, cx: &mut ViewContext) -> Option>; - fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; - fn unmark_text(&mut self, cx: &mut ViewContext); - fn replace_text_in_range( - &mut self, - range: Option>, - text: &str, - cx: &mut ViewContext, - ); - fn replace_and_mark_text_in_range( - &mut self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - cx: &mut ViewContext, - ); - fn bounds_for_range( - &mut self, - range_utf16: std::ops::Range, - element_bounds: crate::Bounds, - cx: &mut ViewContext, - ) -> Option>; -} From 9a41c587797327550ef986c61d61ea79f7b34c01 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 8 Nov 2023 19:25:12 -0500 Subject: [PATCH 17/27] Hard-code the gutter margin temporarily --- crates/editor2/src/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 9d7c0b7fa3..618157fb96 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9771,11 +9771,14 @@ impl InputHandler for Editor { let scroll_position = snapshot.scroll_position(); let scroll_left = scroll_position.x * em_width; + // todo!() How do we actually get the gutter margin here? + let gutter_margin = px(84.46154); + let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); let end = OffsetUtf16(range_utf16.end).to_display_point(&snapshot); let start_y = line_height * (start.row() as f32 - scroll_position.y); let end_y = line_height * (end.row() as f32 - scroll_position.y); - let start_x = snapshot.x_for_point(start, &text_layout_details) - scroll_left; + let start_x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + gutter_margin; let end_x = snapshot.x_for_point(end, &text_layout_details) - scroll_left; Some(Bounds::from_corners( From 26fc36ee0e47da3097ab9429bd58685f4d4afa91 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 16:34:38 -0800 Subject: [PATCH 18/27] First pass at allowing multiple event types to be emitted by an entity --- crates/gpui2/src/app.rs | 54 +++++++++----- crates/gpui2/src/app/model_context.rs | 19 +++-- crates/gpui2/src/app/test_context.rs | 13 ++-- crates/gpui2/src/gpui2.rs | 6 +- crates/gpui2/src/subscription.rs | 2 + crates/gpui2/src/window.rs | 103 ++++++++++++++++---------- 6 files changed, 123 insertions(+), 74 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 8fa70d9c9e..9673f946a8 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -201,7 +201,8 @@ pub struct AppContext { pub(crate) pending_notifications: HashSet, pub(crate) pending_global_notifications: HashSet, pub(crate) observers: SubscriberSet, - pub(crate) event_listeners: SubscriberSet, + // (Entity, Event Type) + pub(crate) event_listeners: SubscriberSet, pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, @@ -351,14 +352,15 @@ impl AppContext { ) } - pub fn subscribe( + pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(E, &T::Event, &mut AppContext) + 'static, + mut on_event: impl FnMut(E, &Evt, &mut AppContext) + 'static, ) -> Subscription where - T: 'static + EventEmitter, + T: 'static + EventEmitter, E: Entity, + Evt: 'static, { self.subscribe_internal(entity, move |entity, event, cx| { on_event(entity, event, cx); @@ -366,27 +368,32 @@ impl AppContext { }) } - pub(crate) fn subscribe_internal( + pub(crate) fn subscribe_internal( &mut self, entity: &E, - mut on_event: impl FnMut(E, &T::Event, &mut AppContext) -> bool + 'static, + mut on_event: impl FnMut(E, &Evt, &mut AppContext) -> bool + 'static, ) -> Subscription where - T: 'static + EventEmitter, + T: 'static + EventEmitter, E: Entity, + Evt: 'static, { let entity_id = entity.entity_id(); let entity = entity.downgrade(); + self.event_listeners.insert( entity_id, - Box::new(move |event, cx| { - let event: &T::Event = event.downcast_ref().expect("invalid event type"); - if let Some(handle) = E::upgrade_from(&entity) { - on_event(handle, event, cx) - } else { - false - } - }), + ( + TypeId::of::(), + Box::new(move |event, cx| { + let event: &Evt = event.downcast_ref().expect("invalid event type"); + if let Some(handle) = E::upgrade_from(&entity) { + on_event(handle, event, cx) + } else { + false + } + }), + ), ) } @@ -509,7 +516,11 @@ impl AppContext { Effect::Notify { emitter } => { self.apply_notify_effect(emitter); } - Effect::Emit { emitter, event } => self.apply_emit_effect(emitter, event), + Effect::Emit { + emitter, + event_type, + event, + } => self.apply_emit_effect(emitter, event_type, event), Effect::FocusChanged { window_handle, focused, @@ -604,10 +615,16 @@ impl AppContext { .retain(&emitter, |handler| handler(self)); } - fn apply_emit_effect(&mut self, emitter: EntityId, event: Box) { + fn apply_emit_effect(&mut self, emitter: EntityId, event_type: TypeId, event: Box) { self.event_listeners .clone() - .retain(&emitter, |handler| handler(event.as_ref(), self)); + .retain(&emitter, |(stored_type, handler)| { + if *stored_type == event_type { + handler(event.as_ref(), self) + } else { + true + } + }); } fn apply_focus_changed_effect( @@ -978,6 +995,7 @@ pub(crate) enum Effect { }, Emit { emitter: EntityId, + event_type: TypeId, event: Box, }, FocusChanged { diff --git a/crates/gpui2/src/app/model_context.rs b/crates/gpui2/src/app/model_context.rs index 35d41ab362..44a3337f03 100644 --- a/crates/gpui2/src/app/model_context.rs +++ b/crates/gpui2/src/app/model_context.rs @@ -59,15 +59,16 @@ impl<'a, T: 'static> ModelContext<'a, T> { }) } - pub fn subscribe( + pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(&mut T, E, &T2::Event, &mut ModelContext<'_, T>) + 'static, + mut on_event: impl FnMut(&mut T, E, &Evt, &mut ModelContext<'_, T>) + 'static, ) -> Subscription where T: 'static, - T2: 'static + EventEmitter, + T2: 'static + EventEmitter, E: Entity, + Evt: 'static, { let this = self.weak_model(); self.app.subscribe_internal(entity, move |e, event, cx| { @@ -189,13 +190,15 @@ impl<'a, T: 'static> ModelContext<'a, T> { } } -impl<'a, T> ModelContext<'a, T> -where - T: EventEmitter, -{ - pub fn emit(&mut self, event: T::Event) { +impl<'a, T> ModelContext<'a, T> { + pub fn emit(&mut self, event: Evt) + where + T: EventEmitter, + Evt: 'static, + { self.app.pending_effects.push_back(Effect::Emit { emitter: self.model_state.entity_id, + event_type: TypeId::of::(), event: Box::new(event), }); } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index eb5ce283a5..7b5ab5f7d7 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -197,12 +197,12 @@ impl TestAppContext { rx } - pub fn events( + pub fn events>( &mut self, entity: &Model, - ) -> futures::channel::mpsc::UnboundedReceiver + ) -> futures::channel::mpsc::UnboundedReceiver where - T::Event: 'static + Clone, + Evt: 'static + Clone, { let (tx, rx) = futures::channel::mpsc::unbounded(); entity @@ -240,10 +240,11 @@ impl TestAppContext { } } -impl Model { - pub fn next_event(&self, cx: &mut TestAppContext) -> T::Event +impl Model { + pub fn next_event(&self, cx: &mut TestAppContext) -> Evt where - T::Event: Send + Clone, + Evt: Send + Clone + 'static, + T: EventEmitter, { let (tx, mut rx) = futures::channel::mpsc::unbounded(); let _subscription = self.update(cx, |_, cx| { diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index e253872ed4..71ea77bd63 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -138,6 +138,8 @@ pub trait Entity: Sealed { Self: Sized; } +pub trait EventEmitter: 'static {} + pub enum GlobalKey { Numeric(usize), View(EntityId), @@ -171,10 +173,6 @@ where } } -pub trait EventEmitter: 'static { - type Event: Any; -} - pub trait Flatten { fn flatten(self) -> Result; } diff --git a/crates/gpui2/src/subscription.rs b/crates/gpui2/src/subscription.rs index 2f4ec0d2f1..7cb023a907 100644 --- a/crates/gpui2/src/subscription.rs +++ b/crates/gpui2/src/subscription.rs @@ -75,6 +75,8 @@ where .flatten() } + /// Call the given callback for each subscriber to the given emitter. + /// If the callback returns false, the subscriber is removed. pub fn retain(&self, emitter: &EmitterKey, mut f: F) where F: FnMut(&mut Callback) -> bool, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fbaeae322b..0392c0e83c 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -439,33 +439,37 @@ impl<'a> WindowContext<'a> { }); } - pub fn subscribe( + pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(E, &Emitter::Event, &mut WindowContext<'_>) + 'static, + mut on_event: impl FnMut(E, &Evt, &mut WindowContext<'_>) + 'static, ) -> Subscription where - Emitter: EventEmitter, + Emitter: EventEmitter, E: Entity, + Evt: 'static, { let entity_id = entity.entity_id(); let entity = entity.downgrade(); let window_handle = self.window.handle; self.app.event_listeners.insert( entity_id, - Box::new(move |event, cx| { - window_handle - .update(cx, |_, cx| { - if let Some(handle) = E::upgrade_from(&entity) { - let event = event.downcast_ref().expect("invalid event type"); - on_event(handle, event, cx); - true - } else { - false - } - }) - .unwrap_or(false) - }), + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&entity) { + let event = event.downcast_ref().expect("invalid event type"); + on_event(handle, event, cx); + true + } else { + false + } + }) + .unwrap_or(false) + }), + ), ) } @@ -1809,14 +1813,33 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } - pub fn subscribe( + // Options for simplifying this new event API: + // + // - Make a new stlye of API which does partial application of the arguments to capture + // the types involved e.g. + // `cx.for_entity(handle).subscribe::(..)` + // + // - Make it so there are less types: + // - Bail on this idea all together, go back to associated types. + // causes our event enums to be a blob of anything that could happen ever, and + // makes applications have some translation boilerplate + // + // - Move some of the types into the method names, + // `cx.subscribe_model::<_, ItemEvents>(handle)` + // + // - Do something drastic like removing views and models, or removing the multiple + // kind of contexts. (Not going to happen, we already tried this before.) + // + // - Accept it, and use `cx.subscribe::<_, _, ItemEvents>(handle, ...)` + pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(&mut V, E, &V2::Event, &mut ViewContext<'_, V>) + 'static, + mut on_event: impl FnMut(&mut V, E, &Evt, &mut ViewContext<'_, V>) + 'static, ) -> Subscription where - V2: EventEmitter, + V2: EventEmitter, E: Entity, + Evt: 'static, { let view = self.view().downgrade(); let entity_id = entity.entity_id(); @@ -1824,19 +1847,22 @@ impl<'a, V: 'static> ViewContext<'a, V> { let window_handle = self.window.handle; self.app.event_listeners.insert( entity_id, - Box::new(move |event, cx| { - window_handle - .update(cx, |_, cx| { - if let Some(handle) = E::upgrade_from(&handle) { - let event = event.downcast_ref().expect("invalid event type"); - view.update(cx, |this, cx| on_event(this, handle, event, cx)) - .is_ok() - } else { - false - } - }) - .unwrap_or(false) - }), + ( + TypeId::of::(), + Box::new(move |event, cx| { + window_handle + .update(cx, |_, cx| { + if let Some(handle) = E::upgrade_from(&handle) { + let event = event.downcast_ref().expect("invalid event type"); + view.update(cx, |this, cx| on_event(this, handle, event, cx)) + .is_ok() + } else { + false + } + }) + .unwrap_or(false) + }), + ), ) } @@ -2181,15 +2207,16 @@ where } } -impl ViewContext<'_, V> -where - V: EventEmitter, - V::Event: 'static, -{ - pub fn emit(&mut self, event: V::Event) { +impl ViewContext<'_, V> { + pub fn emit(&mut self, event: Evt) + where + Evt: 'static, + V: EventEmitter, + { let emitter = self.view.model.entity_id; self.app.push_effect(Effect::Emit { emitter, + event_type: TypeId::of::(), event: Box::new(event), }); } From 86865431b98b0cc62aef5b35185228b5953be0e1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 17:01:50 -0800 Subject: [PATCH 19/27] Assign gutter widht on editor view when painting element --- crates/editor2/src/editor.rs | 8 ++++---- crates/editor2/src/element.rs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 618157fb96..8cc58a87bf 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -674,6 +674,7 @@ pub struct Editor { next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, + gutter_width: Pixels, style: Option, } @@ -1984,6 +1985,7 @@ impl Editor { inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, pixel_position_of_newest_cursor: None, + gutter_width: Default::default(), style: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -9771,14 +9773,12 @@ impl InputHandler for Editor { let scroll_position = snapshot.scroll_position(); let scroll_left = scroll_position.x * em_width; - // todo!() How do we actually get the gutter margin here? - let gutter_margin = px(84.46154); - let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); let end = OffsetUtf16(range_utf16.end).to_display_point(&snapshot); let start_y = line_height * (start.row() as f32 - scroll_position.y); let end_y = line_height * (end.row() as f32 - scroll_position.y); - let start_x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + gutter_margin; + let start_x = + snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width; let end_x = snapshot.x_for_point(end, &text_layout_details) - scroll_left; Some(Bounds::from_corners( diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab9aa2ccf3..0e53aa449d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1468,6 +1468,7 @@ impl EditorElement { gutter_margin = Pixels::ZERO; }; + editor.gutter_width = gutter_width; let text_width = bounds.size.width - gutter_width; let overscroll = size(em_width, px(0.)); let snapshot = { From 277fbda35696802d4fb4ab6b2ce28009a1bec795 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2023 17:27:32 -0800 Subject: [PATCH 20/27] Fix vertical position in first_rect_for_character_range --- crates/editor2/src/editor.rs | 26 +++++++++++-------------- crates/gpui2/src/platform/mac/window.rs | 4 +++- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8cc58a87bf..2b64aa2e8b 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,11 +39,11 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, px, relative, AnyElement, AppContext, BackgroundExecutor, Bounds, - ClipboardItem, Context, DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, - FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, PlatformInputHandler, - Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, - WindowContext, + action, actions, div, point, px, relative, size, AnyElement, AppContext, BackgroundExecutor, + Bounds, ClipboardItem, Context, DispatchContext, Div, Element, Entity, EventEmitter, + FocusHandle, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, + PlatformInputHandler, Render, Styled, Subscription, Task, TextStyle, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -9774,17 +9774,13 @@ impl InputHandler for Editor { let scroll_left = scroll_position.x * em_width; let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let end = OffsetUtf16(range_utf16.end).to_display_point(&snapshot); - let start_y = line_height * (start.row() as f32 - scroll_position.y); - let end_y = line_height * (end.row() as f32 - scroll_position.y); - let start_x = - snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width; - let end_x = snapshot.x_for_point(end, &text_layout_details) - scroll_left; + let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width; + let y = line_height * (start.row() as f32 - scroll_position.y); - Some(Bounds::from_corners( - element_bounds.origin + point(start_x, start_y), - element_bounds.origin + point(end_x, end_y), - )) + Some(Bounds { + origin: element_bounds.origin + point(x, y), + size: size(em_width, line_height), + }) } } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index bd45178e97..d07df3d94b 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -1485,7 +1485,9 @@ extern "C" fn first_rect_for_character_range( NSRect::new( NSPoint::new( frame.origin.x + bounds.origin.x.0 as f64, - frame.origin.y + frame.size.height - bounds.origin.y.0 as f64, + frame.origin.y + frame.size.height + - bounds.origin.y.0 as f64 + - bounds.size.height.0 as f64, ), NSSize::new(bounds.size.width.0 as f64, bounds.size.height.0 as f64), ) From 47a63d5cb7a6aa4c539c78eaad9bde5f219ce0f2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 8 Nov 2023 18:36:12 -0700 Subject: [PATCH 21/27] de-dbg --- crates/gpui2/src/interactive.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 40d247f32b..a546c1b40b 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -414,14 +414,10 @@ pub trait ElementInteractivity: 'static { Box::new(move |_, key_down, context, phase, cx| { if phase == DispatchPhase::Bubble { let key_down = key_down.downcast_ref::().unwrap(); - dbg!(key_down); if let KeyMatch::Some(action) = cx.match_keystroke(&global_id, &key_down.keystroke, context) { - dbg!(&action); return Some(action); - } else { - dbg!("none"); } } From a97c8bf58f7d650b4f846681fb36313bab7c0a09 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 19:29:00 -0800 Subject: [PATCH 22/27] Get workspace compiling with new event emitters --- Cargo.lock | 1 - crates/call2/src/call2.rs | 5 +- crates/call2/src/room.rs | 4 +- crates/channel2/src/channel_buffer.rs | 4 +- crates/channel2/src/channel_chat.rs | 4 +- crates/channel2/src/channel_store.rs | 4 +- crates/client2/src/user.rs | 4 +- crates/copilot2/src/copilot2.rs | 4 +- crates/editor2/src/editor.rs | 4 +- crates/gpui2/src/app.rs | 2 +- crates/gpui2/src/window.rs | 18 ---- crates/language2/src/buffer.rs | 4 +- crates/multi_buffer2/src/multi_buffer2.rs | 4 +- crates/picker2/Cargo.toml | 2 - crates/project2/src/project2.rs | 4 +- crates/project2/src/worktree.rs | 4 +- crates/terminal2/src/terminal2.rs | 4 +- crates/workspace2/src/dock.rs | 115 ++++++++-------------- crates/workspace2/src/item.rs | 106 ++++++++++---------- crates/workspace2/src/notifications.rs | 38 +++---- crates/workspace2/src/pane.rs | 76 +++++++------- crates/workspace2/src/searchable.rs | 23 ++--- crates/workspace2/src/toolbar.rs | 32 +++--- crates/workspace2/src/workspace2.rs | 77 ++++++++------- 24 files changed, 224 insertions(+), 319 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2787475e83..6058497013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6109,7 +6109,6 @@ dependencies = [ "settings2", "theme2", "util", - "workspace2", ] [[package]] diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index b19720bcdc..2fab3d40ce 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -17,6 +17,7 @@ use gpui::{ }; use postage::watch; use project::Project; +use room::Event; use settings::Settings; use std::sync::Arc; @@ -85,9 +86,7 @@ pub struct ActiveCall { _subscriptions: Vec, } -impl EventEmitter for ActiveCall { - type Event = room::Event; -} +impl EventEmitter for ActiveCall {} impl ActiveCall { fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index 27bc51a277..87118764fd 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -79,9 +79,7 @@ pub struct Room { maintain_connection: Option>>, } -impl EventEmitter for Room { - type Event = Event; -} +impl EventEmitter for Room {} impl Room { pub fn channel_id(&self) -> Option { diff --git a/crates/channel2/src/channel_buffer.rs b/crates/channel2/src/channel_buffer.rs index 4c321a8fbc..764f5f7661 100644 --- a/crates/channel2/src/channel_buffer.rs +++ b/crates/channel2/src/channel_buffer.rs @@ -38,9 +38,7 @@ pub enum ChannelBufferEvent { ChannelChanged, } -impl EventEmitter for ChannelBuffer { - type Event = ChannelBufferEvent; -} +impl EventEmitter for ChannelBuffer {} impl ChannelBuffer { pub(crate) async fn new( diff --git a/crates/channel2/src/channel_chat.rs b/crates/channel2/src/channel_chat.rs index a5b5249853..eb92eb18e4 100644 --- a/crates/channel2/src/channel_chat.rs +++ b/crates/channel2/src/channel_chat.rs @@ -76,9 +76,7 @@ pub enum ChannelChatEvent { }, } -impl EventEmitter for ChannelChat { - type Event = ChannelChatEvent; -} +impl EventEmitter for ChannelChat {} pub fn init(client: &Arc) { client.add_model_message_handler(ChannelChat::handle_message_sent); client.add_model_message_handler(ChannelChat::handle_message_removed); diff --git a/crates/channel2/src/channel_store.rs b/crates/channel2/src/channel_store.rs index 3c9abd59e2..5fcc2b9566 100644 --- a/crates/channel2/src/channel_store.rs +++ b/crates/channel2/src/channel_store.rs @@ -114,9 +114,7 @@ pub enum ChannelEvent { ChannelRenamed(ChannelId), } -impl EventEmitter for ChannelStore { - type Event = ChannelEvent; -} +impl EventEmitter for ChannelStore {} enum OpenedModelHandle { Open(WeakModel), diff --git a/crates/client2/src/user.rs b/crates/client2/src/user.rs index 8ff134e6b7..a5dba03d2d 100644 --- a/crates/client2/src/user.rs +++ b/crates/client2/src/user.rs @@ -103,9 +103,7 @@ pub enum ContactEventKind { Cancelled, } -impl EventEmitter for UserStore { - type Event = Event; -} +impl EventEmitter for UserStore {} enum UpdateContacts { Update(proto::UpdateContacts), diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index 9e82823b9b..2daf2fec12 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -284,9 +284,7 @@ pub enum Event { CopilotLanguageServerStarted, } -impl EventEmitter for Copilot { - type Event = Event; -} +impl EventEmitter for Copilot {} impl Copilot { pub fn global(cx: &AppContext) -> Option> { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 049d304750..5bb765e1e0 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9407,9 +9407,7 @@ pub struct EditorReleased(pub WeakView); // } // } // -impl EventEmitter for Editor { - type Event = Event; -} +impl EventEmitter for Editor {} impl Render for Editor { type Element = EditorElement; diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 9673f946a8..c36fe02f30 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -201,7 +201,7 @@ pub struct AppContext { pub(crate) pending_notifications: HashSet, pub(crate) pending_global_notifications: HashSet, pub(crate) observers: SubscriberSet, - // (Entity, Event Type) + // TypeId is the type of the event that the listener callback expects pub(crate) event_listeners: SubscriberSet, pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 0392c0e83c..405d7c5bd1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1813,24 +1813,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } - // Options for simplifying this new event API: - // - // - Make a new stlye of API which does partial application of the arguments to capture - // the types involved e.g. - // `cx.for_entity(handle).subscribe::(..)` - // - // - Make it so there are less types: - // - Bail on this idea all together, go back to associated types. - // causes our event enums to be a blob of anything that could happen ever, and - // makes applications have some translation boilerplate - // - // - Move some of the types into the method names, - // `cx.subscribe_model::<_, ItemEvents>(handle)` - // - // - Do something drastic like removing views and models, or removing the multiple - // kind of contexts. (Not going to happen, we already tried this before.) - // - // - Accept it, and use `cx.subscribe::<_, _, ItemEvents>(handle, ...)` pub fn subscribe( &mut self, entity: &E, diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index b19cf97354..2c8c55d577 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -1815,9 +1815,7 @@ impl Buffer { } } -impl EventEmitter for Buffer { - type Event = Event; -} +impl EventEmitter for Buffer {} impl Deref for Buffer { type Target = TextBuffer; diff --git a/crates/multi_buffer2/src/multi_buffer2.rs b/crates/multi_buffer2/src/multi_buffer2.rs index df33f98b4b..a38d36f02f 100644 --- a/crates/multi_buffer2/src/multi_buffer2.rs +++ b/crates/multi_buffer2/src/multi_buffer2.rs @@ -1872,9 +1872,7 @@ impl MultiBuffer { } } -impl EventEmitter for MultiBuffer { - type Event = Event; -} +impl EventEmitter for MultiBuffer {} impl MultiBufferSnapshot { pub fn text(&self) -> String { diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 8d88c25366..90e1ae931c 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -15,7 +15,6 @@ menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } parking_lot.workspace = true @@ -23,6 +22,5 @@ parking_lot.workspace = true editor = { package = "editor2", path = "../editor2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } serde_json.workspace = true -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 5d7c976e77..95f04dfc82 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -9062,9 +9062,7 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> { } } -impl EventEmitter for Project { - type Event = Event; -} +impl EventEmitter for Project {} impl> From<(WorktreeId, P)> for ProjectPath { fn from((worktree_id, path): (WorktreeId, P)) -> Self { diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 937a549a31..65959d3f31 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -281,9 +281,7 @@ pub enum Event { UpdatedGitRepositories(UpdatedGitRepositoriesSet), } -impl EventEmitter for Worktree { - type Event = Event; -} +impl EventEmitter for Worktree {} impl Worktree { pub async fn local( diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index 3d06b48812..9f94339504 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -1351,9 +1351,7 @@ impl Drop for Terminal { } } -impl EventEmitter for Terminal { - type Event = Event; -} +impl EventEmitter for Terminal {} /// Based on alacritty/src/display/hint.rs > regex_match_at /// Retrieve the match, if the specified point is inside the content matching the regex. diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index e6b6c7561d..14646b55ac 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -7,7 +7,16 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; -pub trait Panel: Render + EventEmitter { +pub enum PanelEvent { + ChangePosition, + ZoomIn, + ZoomOut, + Activate, + Close, + Focus, +} + +pub trait Panel: Render + EventEmitter { fn persistent_name(&self) -> &'static str; fn position(&self, cx: &WindowContext) -> DockPosition; fn position_is_valid(&self, position: DockPosition) -> bool; @@ -19,26 +28,12 @@ pub trait Panel: Render + EventEmitter { fn icon_label(&self, _: &WindowContext) -> Option { None } - fn should_change_position_on_event(_: &Self::Event) -> bool; - fn should_zoom_in_on_event(_: &Self::Event) -> bool { - false - } - fn should_zoom_out_on_event(_: &Self::Event) -> bool { - false - } fn is_zoomed(&self, _cx: &WindowContext) -> bool { false } fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} - fn should_activate_on_event(_: &Self::Event) -> bool { - false - } - fn should_close_on_event(_: &Self::Event) -> bool { - false - } fn has_focus(&self, cx: &WindowContext) -> bool; - fn is_focus_event(_: &Self::Event) -> bool; } pub trait PanelHandle: Send + Sync { @@ -268,21 +263,37 @@ impl Dock { let subscriptions = [ cx.observe(&panel, |_, _, cx| cx.notify()), cx.subscribe(&panel, |this, panel, event, cx| { - if T::should_activate_on_event(event) { - if let Some(ix) = this - .panel_entries - .iter() - .position(|entry| entry.panel.id() == panel.id()) - { - this.set_open(true, cx); - this.activate_panel(ix, cx); - // todo!() - // cx.focus(&panel); + match event { + PanelEvent::ChangePosition => { + //todo!() + // see: Workspace::add_panel_with_extra_event_handler } - } else if T::should_close_on_event(event) - && this.visible_panel().map_or(false, |p| p.id() == panel.id()) - { - this.set_open(false, cx); + PanelEvent::ZoomIn => { + //todo!() + // see: Workspace::add_panel_with_extra_event_handler + } + PanelEvent::ZoomOut => { + // todo!() + // // see: Workspace::add_panel_with_extra_event_handler + } + PanelEvent::Activate => { + if let Some(ix) = this + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + this.set_open(true, cx); + this.activate_panel(ix, cx); + //` todo!() + // cx.focus(&panel); + } + } + PanelEvent::Close => { + if this.visible_panel().map_or(false, |p| p.id() == panel.id()) { + this.set_open(false, cx); + } + } + PanelEvent::Focus => todo!(), } }), ]; @@ -452,10 +463,6 @@ impl PanelButtons { } } -impl EventEmitter for PanelButtons { - type Event = (); -} - // impl Render for PanelButtons { // type Element = (); @@ -625,7 +632,7 @@ impl StatusItemView for PanelButtons { _active_pane_item: Option<&dyn crate::ItemHandle>, _cx: &mut ViewContext, ) { - // todo!(This is empty in the old `workspace::dock`) + // Nothing to do, panel buttons don't depend on the active center item } } @@ -634,16 +641,6 @@ pub mod test { use super::*; use gpui::{div, Div, ViewContext, WindowContext}; - #[derive(Debug)] - pub enum TestPanelEvent { - PositionChanged, - Activated, - Closed, - ZoomIn, - ZoomOut, - Focus, - } - pub struct TestPanel { pub position: DockPosition, pub zoomed: bool, @@ -652,9 +649,7 @@ pub mod test { pub size: f32, } - impl EventEmitter for TestPanel { - type Event = TestPanelEvent; - } + impl EventEmitter for TestPanel {} impl TestPanel { pub fn new(position: DockPosition) -> Self { @@ -691,7 +686,7 @@ pub mod test { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { self.position = position; - cx.emit(TestPanelEvent::PositionChanged); + cx.emit(PanelEvent::ChangePosition); } fn size(&self, _: &WindowContext) -> f32 { @@ -710,18 +705,6 @@ pub mod test { ("Test Panel".into(), None) } - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::PositionChanged) - } - - fn should_zoom_in_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::ZoomIn) - } - - fn should_zoom_out_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::ZoomOut) - } - fn is_zoomed(&self, _: &WindowContext) -> bool { self.zoomed } @@ -734,20 +717,8 @@ pub mod test { self.active = active; } - fn should_activate_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::Activated) - } - - fn should_close_on_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::Closed) - } - fn has_focus(&self, _cx: &WindowContext) -> bool { self.has_focus } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, TestPanelEvent::Focus) - } } } diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 1a7f30646d..2b750a9861 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -91,7 +91,7 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: Render + EventEmitter { +pub trait Item: Render + EventEmitter { fn focus_handle(&self) -> FocusHandle; fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} @@ -106,12 +106,13 @@ pub trait Item: Render + EventEmitter { } fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement; + /// (model id, Item) fn for_each_project_item( &self, _: &AppContext, _: &mut dyn FnMut(EntityId, &dyn project2::Item), ) { - } // (model id, Item) + } fn is_singleton(&self, _cx: &AppContext) -> bool { false } @@ -153,15 +154,6 @@ pub trait Item: Render + EventEmitter { ) -> Task> { unimplemented!("reload() must be implemented if can_save() returns true") } - fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - SmallVec::new() - } - fn should_close_item_on_event(_: &Self::Event) -> bool { - false - } - fn should_update_tab_on_event(_: &Self::Event) -> bool { - false - } fn act_as_type<'a>( &'a self, @@ -218,7 +210,7 @@ pub trait ItemHandle: 'static + Send { fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; @@ -300,12 +292,10 @@ impl ItemHandle for View { fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { - for item_event in T::to_item_events(event) { - handler(item_event, cx) - } + handler(event, cx); }) } @@ -433,7 +423,10 @@ impl ItemHandle for View { let is_project_item = item.is_project_item(cx); let leader_id = workspace.leader_for_pane(&pane); - if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { + let follow_event = item.to_follow_event(event); + if leader_id.is_some() + && matches!(follow_event, Some(FollowEvent::Unfollow)) + { workspace.unfollow(&pane, cx); } @@ -467,36 +460,34 @@ impl ItemHandle for View { } } - for item_event in T::to_item_events(event).into_iter() { - match item_event { - ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) - }) - .detach_and_log_err(cx); - return; - } + match event { + ItemEvent::CloseItem => { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx) + }) + .detach_and_log_err(cx); + return; + } - ItemEvent::UpdateTab => { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::ChangeItemTitle); - cx.notify(); + ItemEvent::UpdateTab => { + pane.update(cx, |_, cx| { + cx.emit(pane::Event::ChangeItemTitle); + cx.notify(); + }); + } + + ItemEvent::Edit => { + let autosave = WorkspaceSettings::get_global(cx).autosave; + if let AutosaveSetting::AfterDelay { milliseconds } = autosave { + let delay = Duration::from_millis(milliseconds); + let item = item.clone(); + pending_autosave.fire_new(delay, cx, move |workspace, cx| { + Pane::autosave_item(&item, workspace.project().clone(), cx) }); } - - ItemEvent::Edit => { - let autosave = WorkspaceSettings::get_global(cx).autosave; - if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds); - let item = item.clone(); - pending_autosave.fire_new(delay, cx, move |workspace, cx| { - Pane::autosave_item(&item, workspace.project().clone(), cx) - }); - } - } - - _ => {} } + + _ => {} } })); @@ -660,7 +651,16 @@ pub trait ProjectItem: Item { Self: Sized; } +pub enum FollowEvent { + Unfollow, +} + +pub trait FollowableEvents { + fn to_follow_event(&self) -> Option; +} + pub trait FollowableItem: Item { + type FollowableEvent: FollowableEvents; fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( @@ -672,7 +672,7 @@ pub trait FollowableItem: Item { ) -> Option>>>; fn add_event_to_update_proto( &self, - event: &Self::Event, + event: &Self::FollowableEvent, update: &mut Option, cx: &AppContext, ) -> bool; @@ -685,7 +685,6 @@ pub trait FollowableItem: Item { fn is_project_item(&self, cx: &AppContext) -> bool; fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); - fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { @@ -698,13 +697,13 @@ pub trait FollowableItemHandle: ItemHandle { update: &mut Option, cx: &AppContext, ) -> bool; + fn to_follow_event(&self, event: &dyn Any) -> Option; fn apply_update_proto( &self, project: &Model, message: proto::update_view::Variant, cx: &mut WindowContext, ) -> Task>; - fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; fn is_project_item(&self, cx: &AppContext) -> bool; } @@ -739,6 +738,13 @@ impl FollowableItemHandle for View { } } + fn to_follow_event(&self, event: &dyn Any) -> Option { + event + .downcast_ref() + .map(T::FollowableEvent::to_follow_event) + .flatten() + } + fn apply_update_proto( &self, project: &Model, @@ -748,14 +754,6 @@ impl FollowableItemHandle for View { self.update(cx, |this, cx| this.apply_update_proto(project, message, cx)) } - fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { - if let Some(event) = event.downcast_ref() { - T::should_unfollow_on_event(event, cx) - } else { - false - } - } - fn is_project_item(&self, cx: &AppContext) -> bool { self.read(cx).is_project_item(cx) } diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 5dd5b2c7ae..7277cc6fc4 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -9,10 +9,12 @@ pub fn init(cx: &mut AppContext) { // simple_message_notification::init(cx); } -pub trait Notification: EventEmitter + Render { - fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool; +pub enum NotificationEvent { + Dismiss, } +pub trait Notification: EventEmitter + Render {} + pub trait NotificationHandle: Send { fn id(&self) -> EntityId; fn to_any(&self) -> AnyView; @@ -101,11 +103,14 @@ impl Workspace { }) { let notification = build_notification(cx); - cx.subscribe(¬ification, move |this, handle, event, cx| { - if handle.read(cx).should_dismiss_notification_on_event(event) { - this.dismiss_notification_internal(type_id, id, cx); - } - }) + cx.subscribe( + ¬ification, + move |this, handle, event: &NotificationEvent, cx| match event { + NotificationEvent::Dismiss => { + this.dismiss_notification_internal(type_id, id, cx); + } + }, + ) .detach(); self.notifications .push((type_id, id, Box::new(notification))); @@ -159,7 +164,7 @@ impl Workspace { } pub mod simple_message_notification { - use super::Notification; + use super::{Notification, NotificationEvent}; use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext}; use serde::Deserialize; use std::{borrow::Cow, sync::Arc}; @@ -200,13 +205,7 @@ pub mod simple_message_notification { click_message: Option>, } - pub enum MessageNotificationEvent { - Dismiss, - } - - impl EventEmitter for MessageNotification { - type Event = MessageNotificationEvent; - } + impl EventEmitter for MessageNotification {} impl MessageNotification { pub fn new(message: S) -> MessageNotification @@ -359,13 +358,8 @@ pub mod simple_message_notification { // } // } - impl Notification for MessageNotification { - fn should_dismiss_notification_on_event(&self, event: &Self::Event) -> bool { - match event { - MessageNotificationEvent::Dismiss => true, - } - } - } + impl EventEmitter for MessageNotification {} + impl Notification for MessageNotification {} } pub trait NotifyResultExt { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 5af5514da4..2bba684d12 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -9,8 +9,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, - PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, + EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, + WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -48,8 +49,10 @@ pub enum SaveIntent { Skip, } -// #[derive(Clone, Deserialize, PartialEq)] -// pub struct ActivateItem(pub usize); +//todo!("Do we need the default bound on actions? Decide soon") +// #[register_action] +#[derive(Clone, Deserialize, PartialEq, Debug)] +pub struct ActivateItem(pub usize); // #[derive(Clone, PartialEq)] // pub struct CloseItemById { @@ -69,40 +72,37 @@ pub enum SaveIntent { // pub pane: WeakView, // } -// #[derive(Clone, PartialEq, Debug, Deserialize, Default)] -// #[serde(rename_all = "camelCase")] -// pub struct CloseActiveItem { -// pub save_intent: Option, -// } +#[register_action] +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseActiveItem { + pub save_intent: Option, +} -// #[derive(Clone, PartialEq, Debug, Deserialize)] -// #[serde(rename_all = "camelCase")] -// pub struct CloseAllItems { -// pub save_intent: Option, -// } +#[register_action] +#[derive(Clone, PartialEq, Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItems { + pub save_intent: Option, +} -// todo!() -// actions!( -// pane, -// [ -// ActivatePrevItem, -// ActivateNextItem, -// ActivateLastItem, -// CloseInactiveItems, -// CloseCleanItems, -// CloseItemsToTheLeft, -// CloseItemsToTheRight, -// GoBack, -// GoForward, -// ReopenClosedItem, -// SplitLeft, -// SplitUp, -// SplitRight, -// SplitDown, -// ] -// ); - -// impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]); +// todo!(These used to be under pane::{Action}. Are they now workspace::pane::{Action}?) +actions!( + ActivatePrevItem, + ActivateNextItem, + ActivateLastItem, + CloseInactiveItems, + CloseCleanItems, + CloseItemsToTheLeft, + CloseItemsToTheRight, + GoBack, + GoForward, + ReopenClosedItem, + SplitLeft, + SplitUp, + SplitRight, + SplitDown, +); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -310,9 +310,7 @@ pub struct NavigationEntry { // .into_any_named("nav button") // } -impl EventEmitter for Pane { - type Event = Event; -} +impl EventEmitter for Pane {} impl Pane { pub fn new( diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs index 2b870c2944..2a393a9f6d 100644 --- a/crates/workspace2/src/searchable.rs +++ b/crates/workspace2/src/searchable.rs @@ -1,6 +1,8 @@ use std::{any::Any, sync::Arc}; -use gpui::{AnyView, AppContext, Subscription, Task, View, ViewContext, WindowContext}; +use gpui::{ + AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WindowContext, +}; use project2::search::SearchQuery; use crate::{ @@ -29,7 +31,7 @@ pub struct SearchOptions { pub replacement: bool, } -pub trait SearchableItem: Item { +pub trait SearchableItem: Item + EventEmitter { type Match: Any + Sync + Send + Clone; fn supported_options() -> SearchOptions { @@ -40,11 +42,7 @@ pub trait SearchableItem: Item { replacement: true, } } - fn to_search_event( - &mut self, - event: &Self::Event, - cx: &mut ViewContext, - ) -> Option; + fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; @@ -95,7 +93,7 @@ pub trait SearchableItemHandle: ItemHandle { fn subscribe_to_search_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> Subscription; fn clear_matches(&self, cx: &mut WindowContext); fn update_matches(&self, matches: &Vec>, cx: &mut WindowContext); @@ -146,14 +144,9 @@ impl SearchableItemHandle for View { fn subscribe_to_search_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> Subscription { - cx.subscribe(self, move |handle, event, cx| { - let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx)); - if let Some(search_event) = search_event { - handler(search_event, cx) - } - }) + cx.subscribe(self, move |_, event: &SearchEvent, cx| handler(event, cx)) } fn clear_matches(&self, cx: &mut WindowContext) { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 80503ad7bb..1e45a17897 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,24 +1,17 @@ use crate::ItemHandle; -use gpui::{ - AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, -}; +use gpui::{AnyView, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext}; -pub trait ToolbarItemView: Render + EventEmitter { +pub enum ToolbarItemEvent { + ChangeLocation(ToolbarItemLocation), +} + +pub trait ToolbarItemView: Render + EventEmitter { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn crate::ItemHandle>, cx: &mut ViewContext, ) -> ToolbarItemLocation; - fn location_for_event( - &self, - _event: &Self::Event, - current_location: ToolbarItemLocation, - _cx: &AppContext, - ) -> ToolbarItemLocation { - current_location - } - fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext) {} /// Number of times toolbar's height will be repeated to get the effective height. @@ -211,12 +204,13 @@ impl Toolbar { if let Some((_, current_location)) = this.items.iter_mut().find(|(i, _)| i.id() == item.id()) { - let new_location = item - .read(cx) - .location_for_event(event, *current_location, cx); - if new_location != *current_location { - *current_location = new_location; - cx.notify(); + match event { + ToolbarItemEvent::ChangeLocation(new_location) => { + if new_location != current_location { + *current_location = *new_location; + cx.notify(); + } + } } } }) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index bc2c649637..cc40659354 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,7 +36,7 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, + actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, @@ -88,35 +88,32 @@ lazy_static! { // #[derive(Clone, PartialEq)] // pub struct RemoveWorktreeFromProject(pub WorktreeId); -// actions!( -// workspace, -// [ -// Open, -// NewFile, -// NewWindow, -// CloseWindow, -// CloseInactiveTabsAndPanes, -// AddFolderToProject, -// Unfollow, -// SaveAs, -// ReloadActiveItem, -// ActivatePreviousPane, -// ActivateNextPane, -// FollowNextCollaborator, -// NewTerminal, -// NewCenterTerminal, -// ToggleTerminalFocus, -// NewSearch, -// Feedback, -// Restart, -// Welcome, -// ToggleZoom, -// ToggleLeftDock, -// ToggleRightDock, -// ToggleBottomDock, -// CloseAllDocks, -// ] -// ); +actions!( + Open, + NewFile, + NewWindow, + CloseWindow, + CloseInactiveTabsAndPanes, + AddFolderToProject, + Unfollow, + SaveAs, + ReloadActiveItem, + ActivatePreviousPane, + ActivateNextPane, + FollowNextCollaborator, + NewTerminal, + NewCenterTerminal, + ToggleTerminalFocus, + NewSearch, + Feedback, + Restart, + Welcome, + ToggleZoom, + ToggleLeftDock, + ToggleRightDock, + ToggleBottomDock, + CloseAllDocks, +); // #[derive(Clone, PartialEq)] // pub struct OpenPaths { @@ -964,6 +961,9 @@ impl Workspace { // let mut prev_position = panel.position(cx); // move |this, panel, event, cx| { // if T::should_change_position_on_event(event) { + // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION + // See: Dock::add_panel + // // let new_position = panel.read(cx).position(cx); // let mut was_visible = false; // dock.update(cx, |dock, cx| { @@ -994,6 +994,9 @@ impl Workspace { // } // }); // } else if T::should_zoom_in_on_event(event) { + // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION + // See: Dock::add_panel + // // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); // if !panel.has_focus(cx) { // cx.focus(&panel); @@ -1001,6 +1004,9 @@ impl Workspace { // this.zoomed = Some(panel.downgrade().into_any()); // this.zoomed_position = Some(panel.read(cx).position(cx)); // } else if T::should_zoom_out_on_event(event) { + // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION + // See: Dock::add_panel + // // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); // if this.zoomed_position == Some(prev_position) { // this.zoomed = None; @@ -1008,6 +1014,9 @@ impl Workspace { // } // cx.notify(); // } else if T::is_focus_event(event) { + // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION + // See: Dock::add_panel + // // let position = panel.read(cx).position(cx); // this.dismiss_zoomed_items_to_reveal(Some(position), cx); // if panel.is_zoomed(cx) { @@ -3691,9 +3700,7 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA .log_err(); } -impl EventEmitter for Workspace { - type Event = Event; -} +impl EventEmitter for Workspace {} impl Render for Workspace { type Element = Div; @@ -4135,10 +4142,6 @@ impl WorkspaceStore { } } -impl EventEmitter for WorkspaceStore { - type Event = (); -} - impl ViewId { pub(crate) fn from_proto(message: proto::ViewId) -> Result { Ok(Self { From 3c57b912b9f59f1b709aa0218b0efbf12c0c941b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 8 Nov 2023 19:52:43 -0800 Subject: [PATCH 23/27] Update editor to use new event emitter --- crates/editor2/src/editor.rs | 51 ++++++++++++++-------- crates/editor2/src/items.rs | 84 +++++++++++------------------------- 2 files changed, 60 insertions(+), 75 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 5bb765e1e0..ede49f61f9 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,10 +39,10 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, px, relative, AnyElement, AppContext, BackgroundExecutor, ClipboardItem, - Context, DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, FontStyle, - FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, PlatformInputHandler, Render, - Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, + action, actions, div, px, relative, AnyElement, AppContext, BackgroundExecutor, BorrowWindow, + ClipboardItem, Context, DispatchContext, Div, Element, Entity, EventEmitter, FocusHandle, + FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, PlatformInputHandler, + Render, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; @@ -96,7 +96,9 @@ use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; -use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace}; +use workspace::{ + item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, +}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -1903,7 +1905,8 @@ impl Editor { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { project_subscriptions.push(cx.observe(project, |_, _, cx| { - cx.emit(Event::TitleChanged); + cx.emit(ItemEvent::UpdateTab); + cx.emit(ItemEvent::UpdateBreadcrumbs); })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { @@ -2361,6 +2364,15 @@ impl Editor { self.blink_manager.update(cx, BlinkManager::pause_blinking); cx.emit(Event::SelectionsChanged { local }); + + if self.selections.disjoint_anchors().len() == 1 { + cx.emit(SearchEvent::ActiveMatchChanged) + } + + if local { + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + cx.notify(); } @@ -8760,6 +8772,9 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(Event::BufferEdited); + cx.emit(ItemEvent::Edit); + cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(SearchEvent::MatchesInvalidated); if *sigleton_buffer_edited { if let Some(project) = &self.project { @@ -8806,13 +8821,20 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } - multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), - multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged), - multi_buffer::Event::Saved => cx.emit(Event::Saved), - multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged), - multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged), + multi_buffer::Event::Reparsed => { + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + multi_buffer::Event::DirtyChanged => { + cx.emit(ItemEvent::UpdateTab); + } + multi_buffer::Event::Saved + | multi_buffer::Event::FileHandleChanged + | multi_buffer::Event::Reloaded => { + cx.emit(ItemEvent::UpdateTab); + cx.emit(ItemEvent::UpdateBreadcrumbs); + } multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged), - multi_buffer::Event::Closed => cx.emit(Event::Closed), + multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } @@ -9378,12 +9400,8 @@ pub enum Event { }, BufferEdited, Edited, - Reparsed, Focused, Blurred, - DirtyChanged, - Saved, - TitleChanged, DiffBaseChanged, SelectionsChanged { local: bool, @@ -9392,7 +9410,6 @@ pub enum Event { local: bool, autoscroll: bool, }, - Closed, } pub struct EditorFocused(pub View); diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 9c49e5f143..1081a329c6 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -7,9 +7,9 @@ use anyhow::{anyhow, Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, FocusHandle, Model, - ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, + div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, + FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -29,7 +29,7 @@ use std::{ use text::Selection; use theme::{ActiveTheme, Theme}; use util::{paths::PathExt, ResultExt, TryFutureExt}; -use workspace::item::{BreadcrumbText, FollowableItemHandle}; +use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, @@ -38,7 +38,26 @@ use workspace::{ pub const MAX_TAB_TITLE_LEN: usize = 24; +impl FollowableEvents for Event { + fn to_follow_event(&self) -> Option { + match self { + Event::Edited => Some(FollowEvent::Unfollow), + Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => { + if *local { + Some(FollowEvent::Unfollow) + } else { + None + } + } + _ => None, + } + } +} + +impl EventEmitter for Editor {} + impl FollowableItem for Editor { + type FollowableEvent = Event; fn remote_id(&self) -> Option { self.remote_id } @@ -217,7 +236,7 @@ impl FollowableItem for Editor { fn add_event_to_update_proto( &self, - event: &Self::Event, + event: &Self::FollowableEvent, update: &mut Option, cx: &AppContext, ) -> bool { @@ -292,15 +311,6 @@ impl FollowableItem for Editor { }) } - fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { - match event { - Event::Edited => true, - Event::SelectionsChanged { local } => *local, - Event::ScrollPositionChanged { local, .. } => *local, - _ => false, - } - } - fn is_project_item(&self, _cx: &AppContext) -> bool { true } @@ -739,32 +749,6 @@ impl Item for Editor { }) } - fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - let mut result = SmallVec::new(); - match event { - Event::Closed => result.push(ItemEvent::CloseItem), - Event::Saved | Event::TitleChanged => { - result.push(ItemEvent::UpdateTab); - result.push(ItemEvent::UpdateBreadcrumbs); - } - Event::Reparsed => { - result.push(ItemEvent::UpdateBreadcrumbs); - } - Event::SelectionsChanged { local } if *local => { - result.push(ItemEvent::UpdateBreadcrumbs); - } - Event::DirtyChanged => { - result.push(ItemEvent::UpdateTab); - } - Event::BufferEdited => { - result.push(ItemEvent::Edit); - result.push(ItemEvent::UpdateBreadcrumbs); - } - _ => {} - } - result - } - fn as_searchable(&self, handle: &View) -> Option> { Some(Box::new(handle.clone())) } @@ -913,28 +897,12 @@ impl ProjectItem for Editor { } } +impl EventEmitter for Editor {} + pub(crate) enum BufferSearchHighlights {} impl SearchableItem for Editor { type Match = Range; - fn to_search_event( - &mut self, - event: &Self::Event, - _: &mut ViewContext, - ) -> Option { - match event { - Event::BufferEdited => Some(SearchEvent::MatchesInvalidated), - Event::SelectionsChanged { .. } => { - if self.selections.disjoint_anchors().len() == 1 { - Some(SearchEvent::ActiveMatchChanged) - } else { - None - } - } - _ => None, - } - } - fn clear_matches(&mut self, cx: &mut ViewContext) { todo!() // self.clear_background_highlights::(cx); From 9a022671a2ea92d773d3d1d7abf3b09c739a6ca9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 8 Nov 2023 21:06:00 -0700 Subject: [PATCH 24/27] Simplify IME support --- crates/editor2/src/editor.rs | 4 +- crates/editor2/src/element.rs | 19 +-- crates/gpui2/src/element.rs | 40 +++--- crates/gpui2/src/gpui2.rs | 4 +- crates/gpui2/src/input.rs | 106 ++++++++++++++ crates/gpui2/src/platform.rs | 10 +- crates/gpui2/src/window.rs | 16 ++- crates/gpui2/src/window_input_handler.rs | 167 ----------------------- 8 files changed, 148 insertions(+), 218 deletions(-) create mode 100644 crates/gpui2/src/input.rs delete mode 100644 crates/gpui2/src/window_input_handler.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 2fe35bb1f6..43af9466b0 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9565,7 +9565,7 @@ impl Render for Editor { impl InputHandler for Editor { fn text_for_range( - &self, + &mut self, range_utf16: Range, cx: &mut ViewContext, ) -> Option { @@ -9578,7 +9578,7 @@ impl InputHandler for Editor { ) } - fn selected_text_range(&self, cx: &mut ViewContext) -> Option> { + fn selected_text_range(&mut self, cx: &mut ViewContext) -> Option> { // Prevent the IME menu from appearing when holding down an alphabetic key // while input is disabled. if !self.input_enabled { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 0e53aa449d..3e77a66936 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -17,11 +17,10 @@ use collections::{BTreeMap, HashMap}; use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, - Edges, Element, ElementId, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - InputHandlerView, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, - ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, - WrappedLineLayout, + Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, + InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, + Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -2517,16 +2516,10 @@ impl Element for EditorElement { self.paint_gutter(gutter_bounds, &layout, editor, cx); } self.paint_text(text_bounds, &layout, editor, cx); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); }); } - - fn handle_text_input<'a>( - &self, - editor: &'a mut Editor, - cx: &mut ViewContext, - ) -> Option<(Box, &'a FocusHandle)> { - Some((Box::new(cx.view()), &editor.focus_handle)) - } } // impl EditorElement { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index a6067eb68d..de9788a9a0 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,7 +1,4 @@ -use crate::{ - BorrowWindow, Bounds, ElementId, FocusHandle, InputHandlerView, LayoutId, Pixels, ViewContext, - WindowInputHandler, -}; +use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext}; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; @@ -34,14 +31,6 @@ pub trait Element { element_state: &mut Self::ElementState, cx: &mut ViewContext, ); - - fn handle_text_input<'a>( - &self, - _view_state: &'a mut V, - _cx: &mut ViewContext, - ) -> Option<(Box, &'a FocusHandle)> { - None - } } #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] @@ -165,18 +154,21 @@ where mut frame_state, } => { let bounds = cx.layout_bounds(layout_id); - if let Some((input_handler, focus_handle)) = - self.element.handle_text_input(view_state, cx) - { - if focus_handle.is_focused(cx) { - cx.window.requested_input_handler = Some(Box::new(WindowInputHandler { - cx: cx.app.this.clone(), - window: cx.window_handle(), - input_handler, - element_bounds: bounds, - })); - } - } + // if let Some((input_handler, focus_handle)) = + // self.element.handle_text_input(view_state, cx) + // { + // todo!() + // // cx.handle_input(&focus_handle, Box::new()) + + // // if focus_handle.is_focused(cx) { + // // cx.window.requested_input_handler = Some(Box::new(WindowInputHandler { + // // cx: cx.app.this.clone(), + // // window: cx.window_handle(), + // // input_handler, + // // element_bounds: bounds, + // // })); + // // } + // } if let Some(id) = self.element.id() { cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 91e4141735..86c0528456 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -9,6 +9,7 @@ mod executor; mod focusable; mod geometry; mod image_cache; +mod input; mod interactive; mod keymap; mod platform; @@ -24,7 +25,6 @@ mod text_system; mod util; mod view; mod window; -mod window_input_handler; mod private { /// A mechanism for restricting implementations of a trait to only those in GPUI. @@ -45,6 +45,7 @@ pub use focusable::*; pub use geometry::*; pub use gpui2_macros::*; pub use image_cache::*; +pub use input::*; pub use interactive::*; pub use keymap::*; pub use platform::*; @@ -66,7 +67,6 @@ pub use text_system::*; pub use util::arc_cow::ArcCow; pub use view::*; pub use window::*; -pub use window_input_handler::*; use derive_more::{Deref, DerefMut}; use std::{ diff --git a/crates/gpui2/src/input.rs b/crates/gpui2/src/input.rs new file mode 100644 index 0000000000..8d9e9b01ad --- /dev/null +++ b/crates/gpui2/src/input.rs @@ -0,0 +1,106 @@ +use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext}; +use std::ops::Range; + +pub trait InputHandler: 'static + Sized { + fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) + -> Option; + fn selected_text_range(&mut self, cx: &mut ViewContext) -> Option>; + fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; + fn unmark_text(&mut self, cx: &mut ViewContext); + fn replace_text_in_range( + &mut self, + range: Option>, + text: &str, + cx: &mut ViewContext, + ); + fn replace_and_mark_text_in_range( + &mut self, + range: Option>, + new_text: &str, + new_selected_range: Option>, + cx: &mut ViewContext, + ); + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: Bounds, + cx: &mut ViewContext, + ) -> Option>; +} + +pub struct ElementInputHandler { + view: View, + element_bounds: Bounds, + cx: AsyncWindowContext, +} + +impl ElementInputHandler { + pub fn new(element_bounds: Bounds, cx: &mut ViewContext) -> Self { + ElementInputHandler { + view: cx.view(), + element_bounds, + cx: cx.to_async(), + } + } +} + +impl PlatformInputHandler for ElementInputHandler { + fn selected_text_range(&mut self) -> Option> { + self.view + .update(&mut self.cx, |view, cx| view.selected_text_range(cx)) + .ok() + .flatten() + } + + fn marked_text_range(&mut self) -> Option> { + self.view + .update(&mut self.cx, |view, cx| view.marked_text_range(cx)) + .ok() + .flatten() + } + + fn text_for_range(&mut self, range_utf16: Range) -> Option { + self.view + .update(&mut self.cx, |view, cx| { + view.text_for_range(range_utf16, cx) + }) + .ok() + .flatten() + } + + fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { + self.view + .update(&mut self.cx, |view, cx| { + view.replace_text_in_range(replacement_range, text, cx) + }) + .ok(); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range: Option>, + ) { + self.view + .update(&mut self.cx, |view, cx| { + view.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) + }) + .ok(); + } + + fn unmark_text(&mut self) { + self.view + .update(&mut self.cx, |view, cx| view.unmark_text(cx)) + .ok(); + } + + fn bounds_for_range(&mut self, range_utf16: Range) -> Option> { + self.view + .update(&mut self.cx, |view, cx| { + view.bounds_for_range(range_utf16, self.element_bounds, cx) + }) + .ok() + .flatten() + } +} diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 5ebb12b64d..8b49addec9 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -293,10 +293,10 @@ impl From for etagere::AllocId { } } -pub trait PlatformInputHandler { - fn selected_text_range(&self) -> Option>; - fn marked_text_range(&self) -> Option>; - fn text_for_range(&self, range_utf16: Range) -> Option; +pub trait PlatformInputHandler: 'static { + fn selected_text_range(&mut self) -> Option>; + fn marked_text_range(&mut self) -> Option>; + fn text_for_range(&mut self, range_utf16: Range) -> Option; fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str); fn replace_and_mark_text_in_range( &mut self, @@ -305,7 +305,7 @@ pub trait PlatformInputHandler { new_selected_range: Option>, ); fn unmark_text(&mut self); - fn bounds_for_range(&self, range_utf16: Range) -> Option>; + fn bounds_for_range(&mut self, range_utf16: Range) -> Option>; } #[derive(Debug)] diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 1daebf184c..354c98813f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -211,7 +211,6 @@ pub struct Window { default_prevented: bool, mouse_position: Point, requested_cursor_style: Option, - pub(crate) requested_input_handler: Option>, scale_factor: f32, bounds: WindowBounds, bounds_observers: SubscriberSet<(), AnyObserver>, @@ -236,6 +235,7 @@ pub(crate) struct Frame { content_mask_stack: Vec>, element_offset_stack: Vec>, focus_stack: Vec, + input_handler: Option>, } impl Window { @@ -311,7 +311,6 @@ impl Window { default_prevented: true, mouse_position, requested_cursor_style: None, - requested_input_handler: None, scale_factor, bounds, bounds_observers: SubscriberSet::new(), @@ -1048,9 +1047,6 @@ impl<'a> WindowContext<'a> { .take() .unwrap_or(CursorStyle::Arrow); self.platform.set_cursor_style(cursor_style); - if let Some(handler) = self.window.requested_input_handler.take() { - self.window.platform_window.set_input_handler(handler); - } self.window.dirty = false; } @@ -2174,6 +2170,16 @@ impl<'a, V: 'static> ViewContext<'a, V> { }) }); } + + pub fn handle_input( + &mut self, + focus_handle: &FocusHandle, + input_handler: impl PlatformInputHandler, + ) { + if focus_handle.is_focused(self) { + self.window.current_frame.input_handler = Some(Box::new(input_handler)); + } + } } impl ViewContext<'_, V> diff --git a/crates/gpui2/src/window_input_handler.rs b/crates/gpui2/src/window_input_handler.rs deleted file mode 100644 index f3ff33f3c0..0000000000 --- a/crates/gpui2/src/window_input_handler.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - AnyWindowHandle, AppCell, Bounds, Context, Pixels, PlatformInputHandler, View, ViewContext, - WindowContext, -}; -use std::{ops::Range, rc::Weak}; - -pub struct WindowInputHandler { - pub cx: Weak, - pub input_handler: Box, - pub window: AnyWindowHandle, - pub element_bounds: Bounds, -} - -pub trait InputHandlerView { - fn text_for_range(&self, range: Range, cx: &mut WindowContext) -> Option; - fn selected_text_range(&self, cx: &mut WindowContext) -> Option>; - fn marked_text_range(&self, cx: &mut WindowContext) -> Option>; - fn unmark_text(&self, cx: &mut WindowContext); - fn replace_text_in_range( - &self, - range: Option>, - text: &str, - cx: &mut WindowContext, - ); - fn replace_and_mark_text_in_range( - &self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - cx: &mut WindowContext, - ); - fn bounds_for_range( - &self, - range_utf16: std::ops::Range, - element_bounds: crate::Bounds, - cx: &mut WindowContext, - ) -> Option>; -} - -pub trait InputHandler: Sized { - fn text_for_range(&self, range: Range, cx: &mut ViewContext) -> Option; - fn selected_text_range(&self, cx: &mut ViewContext) -> Option>; - fn marked_text_range(&self, cx: &mut ViewContext) -> Option>; - fn unmark_text(&mut self, cx: &mut ViewContext); - fn replace_text_in_range( - &mut self, - range: Option>, - text: &str, - cx: &mut ViewContext, - ); - fn replace_and_mark_text_in_range( - &mut self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - cx: &mut ViewContext, - ); - fn bounds_for_range( - &mut self, - range_utf16: std::ops::Range, - element_bounds: crate::Bounds, - cx: &mut ViewContext, - ) -> Option>; -} - -impl InputHandlerView for View { - fn text_for_range(&self, range: Range, cx: &mut WindowContext) -> Option { - self.update(cx, |this, cx| this.text_for_range(range, cx)) - } - - fn selected_text_range(&self, cx: &mut WindowContext) -> Option> { - self.update(cx, |this, cx| this.selected_text_range(cx)) - } - - fn marked_text_range(&self, cx: &mut WindowContext) -> Option> { - self.update(cx, |this, cx| this.marked_text_range(cx)) - } - - fn unmark_text(&self, cx: &mut WindowContext) { - self.update(cx, |this, cx| this.unmark_text(cx)) - } - - fn replace_text_in_range( - &self, - range: Option>, - text: &str, - cx: &mut WindowContext, - ) { - self.update(cx, |this, cx| this.replace_text_in_range(range, text, cx)) - } - - fn replace_and_mark_text_in_range( - &self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - cx: &mut WindowContext, - ) { - self.update(cx, |this, cx| { - this.replace_and_mark_text_in_range(range, new_text, new_selected_range, cx) - }) - } - - fn bounds_for_range( - &self, - range_utf16: std::ops::Range, - element_bounds: crate::Bounds, - cx: &mut WindowContext, - ) -> Option> { - self.update(cx, |this, cx| { - this.bounds_for_range(range_utf16, element_bounds, cx) - }) - } -} - -impl PlatformInputHandler for WindowInputHandler { - fn selected_text_range(&self) -> Option> { - self.update(|handler, cx| handler.selected_text_range(cx)) - .flatten() - } - - fn marked_text_range(&self) -> Option> { - self.update(|handler, cx| handler.marked_text_range(cx)) - .flatten() - } - - fn text_for_range(&self, range_utf16: Range) -> Option { - self.update(|handler, cx| handler.text_for_range(range_utf16, cx)) - .flatten() - } - - fn replace_text_in_range(&mut self, replacement_range: Option>, text: &str) { - self.update(|handler, cx| handler.replace_text_in_range(replacement_range, text, cx)); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - new_text: &str, - new_selected_range: Option>, - ) { - self.update(|handler, cx| { - handler.replace_and_mark_text_in_range(range_utf16, new_text, new_selected_range, cx) - }); - } - - fn unmark_text(&mut self) { - self.update(|handler, cx| handler.unmark_text(cx)); - } - - fn bounds_for_range(&self, range_utf16: Range) -> Option> { - self.update(|handler, cx| handler.bounds_for_range(range_utf16, self.element_bounds, cx)) - .flatten() - } -} - -impl WindowInputHandler { - fn update( - &self, - f: impl FnOnce(&dyn InputHandlerView, &mut WindowContext) -> R, - ) -> Option { - let cx = self.cx.upgrade()?; - let mut cx = cx.borrow_mut(); - cx.update_window(self.window, |_, cx| f(&*self.input_handler, cx)) - .ok() - } -} From 8278a0735437cae7e5c152e0503e7fd1bf83cf81 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 8 Nov 2023 21:43:14 -0700 Subject: [PATCH 25/27] Actually set the input handler --- crates/gpui2/src/window.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 354c98813f..b72793b998 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -235,7 +235,6 @@ pub(crate) struct Frame { content_mask_stack: Vec>, element_offset_stack: Vec>, focus_stack: Vec, - input_handler: Option>, } impl Window { @@ -2177,7 +2176,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { input_handler: impl PlatformInputHandler, ) { if focus_handle.is_focused(self) { - self.window.current_frame.input_handler = Some(Box::new(input_handler)); + self.window + .platform_window + .set_input_handler(Box::new(input_handler)); } } } From 7c922ad6ee1c71200c324db9bf60dc1f23f52bbf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 8 Nov 2023 21:49:21 -0700 Subject: [PATCH 26/27] Remove comments --- crates/gpui2/src/element.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index de9788a9a0..8fdc17de07 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -154,21 +154,6 @@ where mut frame_state, } => { let bounds = cx.layout_bounds(layout_id); - // if let Some((input_handler, focus_handle)) = - // self.element.handle_text_input(view_state, cx) - // { - // todo!() - // // cx.handle_input(&focus_handle, Box::new()) - - // // if focus_handle.is_focused(cx) { - // // cx.window.requested_input_handler = Some(Box::new(WindowInputHandler { - // // cx: cx.app.this.clone(), - // // window: cx.window_handle(), - // // input_handler, - // // element_bounds: bounds, - // // })); - // // } - // } if let Some(id) = self.element.id() { cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); From d52c5646b451ae27475b3ebb3db0e3e5d772cc56 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 8 Nov 2023 22:03:26 -0700 Subject: [PATCH 27/27] Add docs --- crates/gpui2/src/input.rs | 8 ++++++++ crates/gpui2/src/window.rs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/crates/gpui2/src/input.rs b/crates/gpui2/src/input.rs index 8d9e9b01ad..d768ce946a 100644 --- a/crates/gpui2/src/input.rs +++ b/crates/gpui2/src/input.rs @@ -1,6 +1,10 @@ use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext}; use std::ops::Range; +/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. +/// +/// Once your view `V` implements this trait, you can use it to construct an [ElementInputHandler]. +/// This input handler can then be assigned during paint by calling [WindowContext::handle_input]. pub trait InputHandler: 'static + Sized { fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) -> Option; @@ -28,6 +32,8 @@ pub trait InputHandler: 'static + Sized { ) -> Option>; } +/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input` +/// with an instance during your element's paint. pub struct ElementInputHandler { view: View, element_bounds: Bounds, @@ -35,6 +41,8 @@ pub struct ElementInputHandler { } impl ElementInputHandler { + /// Used in [Element::paint] with the element's bounds and a view context for its + /// containing view. pub fn new(element_bounds: Bounds, cx: &mut ViewContext) -> Self { ElementInputHandler { view: cx.view(), diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b72793b998..29980a486b 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2170,6 +2170,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Set an input handler, such as [ElementInputHandler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. pub fn handle_input( &mut self, focus_handle: &FocusHandle,