diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd089d3c..22e8b4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,3 +76,4 @@ jobs: if: ${{ matrix.run_on == 'macos-latest' }} run: | cargo test --all + cargo test -p gpui-component --doc diff --git a/crates/ui/src/button/button_group.rs b/crates/ui/src/button/button_group.rs index 575c0d72..ec520436 100644 --- a/crates/ui/src/button/button_group.rs +++ b/crates/ui/src/button/button_group.rs @@ -89,7 +89,7 @@ impl ButtonGroup { /// The `&Vec` is the indices of the clicked (selected in `multiple` mode) buttons. /// For example: `[0, 2, 3]` is means the first, third and fourth buttons are clicked. /// - /// ```rust + /// ```ignore /// ButtonGroup::new("size-button") /// .child(Button::new("large").label("Large").selected(self.size == Size::Large)) /// .child(Button::new("medium").label("Medium").selected(self.size == Size::Medium)) diff --git a/crates/ui/src/highlighter/diagnostics.rs b/crates/ui/src/highlighter/diagnostics.rs index 1ff93a55..56d0cabd 100644 --- a/crates/ui/src/highlighter/diagnostics.rs +++ b/crates/ui/src/highlighter/diagnostics.rs @@ -58,7 +58,7 @@ pub struct Diagnostic { impl From for Diagnostic { fn from(value: lsp_types::Diagnostic) -> Self { Self { - range: Position::from(value.range.start)..Position::from(value.range.end), + range: value.range.start..value.range.end, severity: value .severity .map(Into::into) diff --git a/crates/ui/src/input/cursor.rs b/crates/ui/src/input/cursor.rs index e4a6a887..375600f6 100644 --- a/crates/ui/src/input/cursor.rs +++ b/crates/ui/src/input/cursor.rs @@ -52,8 +52,6 @@ impl RangeBounds for Selection { } } -pub type Position = lsp_types::Position; - #[cfg(test)] mod tests { use crate::input::Position; diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index dabd03c7..435eb905 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -508,7 +508,7 @@ impl TextElement { let mut styles = vec![]; for line in text - .rows() + .iter_lines() .skip(visible_range.start) .take(visible_range.len()) { @@ -675,7 +675,7 @@ impl Element for TextElement { ) } else if state.masked { ( - Rope::from("*".repeat(text.chars_count())), + Rope::from("*".repeat(text.chars().count())), cx.theme().foreground, ) } else { @@ -781,7 +781,7 @@ impl Element for TextElement { // NOTE: Here 50 lines about 150µs // let measure = crate::Measure::new("shape_text"); let visible_text = display_text - .slice_rows(visible_range.start..visible_range.end) + .slice_lines(visible_range.start..visible_range.end) .to_string(); let lines = window @@ -793,7 +793,7 @@ impl Element for TextElement { let mut longest_line_width = wrap_width.unwrap_or(px(0.)); if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 { let longest_row = state.text_wrapper.longest_row.row; - let longtest_line: SharedString = state.text.slice_row(longest_row).to_string().into(); + let longtest_line: SharedString = state.text.slice_line(longest_row).to_string().into(); longest_line_width = window .text_system() .shape_line( diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index c0750bc3..1b3f983c 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -22,7 +22,9 @@ pub use mask_pattern::MaskPattern; pub use mode::TabSize; pub use number_input::{NumberInput, NumberInputEvent, StepAction}; pub use otp_input::*; -pub use rope_ext::*; -pub use ropey::Rope; pub use state::*; pub use text_input::*; + +pub use lsp_types::Position; +pub use rope_ext::*; +pub use ropey::Rope; diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index 01237e51..17d7cc77 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -6,35 +6,156 @@ use tree_sitter::Point; use crate::input::Position; -/// An extension trait for `Rope` to provide additional utility methods. +/// An iterator over the lines of a `Rope`. +pub struct RopeLines<'a> { + rope: &'a Rope, + row: usize, + end_row: usize, +} + +impl<'a> RopeLines<'a> { + /// Create a new `RopeLines` iterator. + pub fn new(rope: &'a Rope) -> Self { + let end_row = rope.lines_len(); + Self { + row: 0, + end_row, + rope, + } + } +} +impl<'a> Iterator for RopeLines<'a> { + type Item = RopeSlice<'a>; + + #[inline] + fn next(&mut self) -> Option { + if self.row >= self.end_row { + return None; + } + + let line = self.rope.slice_line(self.row); + self.row += 1; + Some(line) + } + + #[inline] + fn nth(&mut self, n: usize) -> Option { + self.row = self.row.saturating_add(n); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.end_row - self.row; + (len, Some(len)) + } +} + +impl std::iter::ExactSizeIterator for RopeLines<'_> {} +impl std::iter::FusedIterator for RopeLines<'_> {} + +/// An extension trait for [`Rope`] to provide additional utility methods. pub trait RopeExt { /// Start offset of the line at the given row (0-based) index. + /// + /// # Example + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.line_start_offset(0), 0); + /// assert_eq!(rope.line_start_offset(1), 6); + /// ``` fn line_start_offset(&self, row: usize) -> usize; - /// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`. - fn slice_row(&self, row: usize) -> RopeSlice<'_>; - - fn slice_rows(&self, rows_range: Range) -> RopeSlice<'_>; - - fn rows(&self) -> impl Iterator> + '_ { - (0..self.lines_len()).map(|row| self.slice_row(row)) - } - /// Line the end offset (including `\n`) of the line at the given row (0-based) index. /// /// Return the end of the rope if the row is out of bounds. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.line_end_offset(0), 5); // "Hello\n" + /// assert_eq!(rope.line_end_offset(1), 12); // "World\r\n" + /// ``` fn line_end_offset(&self, row: usize) -> usize; + /// Return a line slice at the given row (0-based) index. including `\r` if present, but not `\n`. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.slice_line(0).to_string(), "Hello"); + /// assert_eq!(rope.slice_line(1).to_string(), "World\r"); + /// assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文"); + /// assert_eq!(rope.slice_line(6).to_string(), ""); // out of bounds + /// ``` + fn slice_line(&self, row: usize) -> RopeSlice<'_>; + + /// Return a slice of rows in the given range (0-based, end exclusive). + /// + /// If the range is out of bounds, it will be clamped to the valid range. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.slice_lines(0..2).to_string(), "Hello\nWorld\r"); + /// assert_eq!(rope.slice_lines(1..3).to_string(), "World\r\nThis is a test 中文"); + /// assert_eq!(rope.slice_lines(2..5).to_string(), "This is a test 中文\nRope"); + /// assert_eq!(rope.slice_lines(3..10).to_string(), "Rope"); + /// assert_eq!(rope.slice_lines(5..10).to_string(), ""); // out of bounds + /// ``` + fn slice_lines(&self, rows_range: Range) -> RopeSlice<'_>; + + /// Return an iterator over all lines in the rope. + /// + /// Each line slice includes `\r` if present, but not `\n`. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect(); + /// assert_eq!(lines, vec!["Hello", "World\r", "This is a test 中文", "Rope"]); + /// ``` + fn iter_lines(&self) -> RopeLines<'_>; + /// Return the number of lines in the rope. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.lines_len(), 4); + /// ``` fn lines_len(&self) -> usize; /// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`. + /// + /// If the row is out of bounds, return 0. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// assert_eq!(rope.line_len(0), 5); // "Hello" + /// assert_eq!(rope.line_len(1), 6); // "World\r" + /// assert_eq!(rope.line_len(2), 21); // "This is a test 中文" + /// assert_eq!(rope.line_len(4), 0); // out of bounds + /// ``` fn line_len(&self, row: usize) -> usize; - /// Total number of characters in the rope. - fn chars_count(&self) -> usize; - /// Replace the text in the given byte range with new text. + /// + /// # Panics + /// + /// - If the range is not on char boundary. + /// - If the range is out of bounds. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// let mut rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + /// rope.replace(6..11, "Universe"); + /// assert_eq!(rope.to_string(), "Hello\nUniverse\r\nThis is a test 中文\nRope"); + /// ``` fn replace(&mut self, range: Range, new_text: &str); /// Get char at the given offset (byte). @@ -44,24 +165,39 @@ pub trait RopeExt { fn char_at(&self, offset: usize) -> Option; /// Get the byte offset from the given line, column [`Position`] (0-based). + /// + /// The column is in characters. fn position_to_offset(&self, line_col: &Position) -> usize; /// Get the line, column [`Position`] (0-based) from the given byte offset. + /// + /// The column is in characters. fn offset_to_position(&self, offset: usize) -> Position; + /// Get point (row, column) from the given byte offset. + /// + /// The column is in bytes. fn offset_to_point(&self, offset: usize) -> Point; + + /// Get byte offset from the given point (row, column). + /// + /// The column is 0-based in bytes. fn point_to_offset(&self, point: Point) -> usize; - /// Get the word byte range at the given offset (byte). + /// Get the word byte range at the given byte offset (0-based). fn word_range(&self, offset: usize) -> Option>; - /// Get word at the given offset (byte). + /// Get word at the given byte offset (0-based). fn word_at(&self, offset: usize) -> String; - /// Convert offset_utf16 to offset (byte). + /// Convert offset in UTF-16 to byte offset (0-based). + /// + /// Runs in O(log N) time. fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize; - /// Convert offset (byte) to offset_utf16. + /// Convert byte offset (0-based) to offset in UTF-16. + /// + /// Runs in O(log N) time. fn offset_to_offset_utf16(&self, offset: usize) -> usize; /// Get a clipped offset (avoid in a char boundary). @@ -69,11 +205,22 @@ pub trait RopeExt { /// - If Bias::Left and inside the char boundary, return the ix - 1; /// - If Bias::Right and in inside char boundary, return the ix + 1; /// - Otherwise return the ix. + /// + /// ``` + /// use gpui_component::input::{Rope, RopeExt}; + /// use sum_tree::Bias; + /// + /// let rope = Rope::from("Hello 中文🎉 test\nRope"); + /// assert_eq!(rope.clip_offset(5, Bias::Left), 5); + /// // Inside multi-byte character '中' (3 bytes) + /// assert_eq!(rope.clip_offset(7, Bias::Left), 6); + /// assert_eq!(rope.clip_offset(7, Bias::Right), 9); + /// ``` fn clip_offset(&self, offset: usize, bias: Bias) -> usize; } impl RopeExt for Rope { - fn slice_row(&self, row: usize) -> RopeSlice<'_> { + fn slice_line(&self, row: usize) -> RopeSlice<'_> { if row >= self.lines_len() { return self.slice(0..0); } @@ -86,14 +233,18 @@ impl RopeExt for Rope { } } - fn slice_rows(&self, rows_range: Range) -> RopeSlice<'_> { + fn slice_lines(&self, rows_range: Range) -> RopeSlice<'_> { let start = self.line_start_offset(rows_range.start); let end = self.line_end_offset(rows_range.end.saturating_sub(1)); self.slice(start..end) } + fn iter_lines(&self) -> RopeLines<'_> { + RopeLines::new(&self) + } + fn line_len(&self, row: usize) -> usize { - self.slice_row(row).len() + self.slice_line(row).len() } fn line_start_offset(&self, row: usize) -> usize { @@ -118,7 +269,7 @@ impl RopeExt for Rope { } fn position_to_offset(&self, pos: &Position) -> usize { - let line = self.slice_row(pos.line as usize); + let line = self.slice_line(pos.line as usize); self.line_start_offset(pos.line as usize) + line .chars() @@ -129,7 +280,7 @@ impl RopeExt for Rope { fn offset_to_position(&self, offset: usize) -> Position { let point = self.offset_to_point(offset); - let line = self.slice_row(point.row); + let line = self.slice_line(point.row); let offset = line.utf16_to_byte_idx(line.byte_to_utf16_idx(point.column)); let character = line.slice(..offset).chars().count(); Position::new(point.row as u32, character as u32) @@ -147,10 +298,6 @@ impl RopeExt for Rope { self.len_lines(LineType::LF_CR) } - fn chars_count(&self) -> usize { - self.chars().count() - } - fn char_at(&self, offset: usize) -> Option { if offset > self.len() { return None; @@ -248,13 +395,13 @@ mod tests { #[test] fn test_line() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); - assert_eq!(rope.slice_row(0).to_string(), "Hello"); - assert_eq!(rope.slice_row(1).to_string(), "World\r"); - assert_eq!(rope.slice_row(2).to_string(), "This is a test 中文"); - assert_eq!(rope.slice_row(3).to_string(), "Rope"); + assert_eq!(rope.slice_line(0).to_string(), "Hello"); + assert_eq!(rope.slice_line(1).to_string(), "World\r"); + assert_eq!(rope.slice_line(2).to_string(), "This is a test 中文"); + assert_eq!(rope.slice_line(3).to_string(), "Rope"); // over bounds - assert_eq!(rope.slice_row(6).to_string(), ""); + assert_eq!(rope.slice_line(6).to_string(), ""); } #[test] @@ -267,6 +414,16 @@ mod tests { assert_eq!(rope.lines_len(), 1); } + #[test] + fn test_lines() { + let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); + let lines: Vec<_> = rope.iter_lines().map(|r| r.to_string()).collect(); + assert_eq!( + lines, + vec!["Hello", "World\r", "This is a test 中文", "Rope"] + ); + } + #[test] fn test_eq() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); @@ -278,13 +435,15 @@ mod tests { } #[test] - fn test_rows() { + fn test_iter_lines() { let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope"); - let lines: Vec<_> = rope.rows().map(|r| r.to_string()).collect(); - assert_eq!( - lines, - vec!["Hello", "World\r", "This is a test 中文", "Rope"] - ); + let lines: Vec<_> = rope + .iter_lines() + .skip(1) + .take(2) + .map(|r| r.to_string()) + .collect(); + assert_eq!(lines, vec!["World\r", "This is a test 中文"]); } #[test] @@ -306,16 +465,6 @@ mod tests { assert_eq!(rope.line_end_offset(4), 39); } - #[test] - fn test_chars_count() { - let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope"); - assert_eq!(rope.chars_count(), 36); - let rope = Rope::from(""); - assert_eq!(rope.chars_count(), 0); - let rope = Rope::from("Single line"); - assert_eq!(rope.chars_count(), 11); - } - #[test] fn test_line_column() { let rope = Rope::from("a 中文🎉 test\nRope"); diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 34fb1d43..25c27d30 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -652,7 +652,7 @@ impl InputState { let mut new_offset = line_start_offset; if let Some((preferred_x, column)) = was_preferred_column { - let new_column = column.min(self.text.slice_row(new_row).len()); + let new_column = column.min(self.text.slice_line(new_row).len()); new_offset = line_start_offset + new_column; // If in visible range, prefer to use position to get column. diff --git a/crates/ui/src/inspector.rs b/crates/ui/src/inspector.rs index 1a77dff0..f155f5bd 100644 --- a/crates/ui/src/inspector.rs +++ b/crates/ui/src/inspector.rs @@ -348,7 +348,7 @@ fn rust_to_style(mut style: StyleRefinement, source: &str) -> (StyleRefinement, let mut offset = 0; let mut method_offset = 0; let mut method = String::new(); - for line in rope.rows() { + for line in rope.iter_lines() { if line.to_string().trim().starts_with("//") { offset += line.len() + 1; continue;