editor: Revert to use ropey. (#1284)

For the License reason.

- The performance is slow down, before version can handle 200K lines,
now 150K lines is good.
This commit is contained in:
Jason Lee 2025-09-24 20:52:18 +08:00 committed by GitHub
parent ca8d595936
commit 1c3eca93b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 369 additions and 192 deletions

32
Cargo.lock generated
View file

@ -757,7 +757,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools 0.11.0",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"log",
@ -780,7 +780,7 @@ dependencies = [
"bitflags 2.9.1",
"cexpr",
"clang-sys",
"itertools 0.11.0",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
@ -3225,7 +3225,7 @@ dependencies = [
"once_cell",
"paste",
"regex",
"rope",
"ropey",
"rust-i18n",
"rust_decimal",
"schemars",
@ -3690,7 +3690,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
"windows-core 0.61.2",
]
[[package]]
@ -4980,7 +4980,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [
"proc-macro-crate 1.3.1",
"proc-macro-crate 3.3.0",
"proc-macro2",
"quote",
"syn 2.0.105",
@ -6754,18 +6754,12 @@ dependencies = [
]
[[package]]
name = "rope"
version = "0.1.0"
source = "git+https://github.com/zed-industries/zed.git#4532765ae845b8c98c73c88cc916f7d771b429d5"
name = "ropey"
version = "2.0.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74"
dependencies = [
"arrayvec",
"log",
"rayon",
"smallvec",
"sum_tree",
"unicode-segmentation",
"util",
"workspace-hack",
"str_indices",
]
[[package]]
@ -7802,6 +7796,12 @@ dependencies = [
"unindent",
]
[[package]]
name = "str_indices"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6"
[[package]]
name = "streaming-iterator"
version = "0.1.9"

View file

@ -17,13 +17,13 @@ resolver = "2"
[workspace.dependencies]
gpui = { git = "https://github.com/zed-industries/zed.git" }
gpui_macros = { git = "https://github.com/zed-industries/zed.git" }
rope = { git = "https://github.com/zed-industries/zed.git" }
reqwest_client = { git = "https://github.com/zed-industries/zed.git" }
sum_tree = { git = "https://github.com/zed-industries/zed.git" }
gpui-component = { path = "crates/ui" }
gpui-component-macros = { path = "crates/macros" }
story = { path = "crates/story" }
wef = { path = "crates/wef" }
ropey = { version = "=2.0.0-beta.1", features = ["metric_utf16"] }
anyhow = "1"
log = "0.4"

View file

@ -912,7 +912,9 @@ impl Render for Example {
.xsmall()
.label(format!(
"{}:{} ({} byte)",
position.line, position.character, cursor
position.line + 1,
position.character + 1,
cursor
))
.on_click(cx.listener(Self::go_to_line))
}),

View file

@ -53,7 +53,7 @@ gpui = { workspace = true }
gpui-component-macros.workspace = true
gpui_macros.workspace = true
notify.workspace = true
rope.workspace = true
ropey.workspace = true
rust-i18n.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -5,7 +5,7 @@ use std::{
};
use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
use rope::Rope;
use ropey::Rope;
use sum_tree::{Bias, SeekTarget, SumTree};
use crate::{
@ -343,7 +343,7 @@ mod tests {
#[test]
fn test_diagnostic() {
use rope::Rope;
use ropey::Rope;
use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity};

View file

@ -1,9 +1,9 @@
use crate::{highlighter::LanguageRegistry, input::RopeExt as _, ActiveTheme};
use crate::{highlighter::LanguageRegistry, ActiveTheme};
use anyhow::{anyhow, Context, Result};
use gpui::{App, HighlightStyle, SharedString};
use rope::Rope;
use ropey::{ChunkCursor, Rope};
use std::{
collections::{BTreeSet, HashMap},
ops::Range,
@ -40,12 +40,21 @@ pub struct SyntaxHighlighter {
}
struct TextProvider<'a>(&'a Rope);
struct ByteChunks<'a>(rope::Chunks<'a>);
struct ByteChunks<'a> {
cursor: ChunkCursor<'a>,
end: usize,
}
impl<'a> tree_sitter::TextProvider<&'a [u8]> for TextProvider<'a> {
type I = ByteChunks<'a>;
fn text(&mut self, node: tree_sitter::Node) -> Self::I {
ByteChunks(self.0.chunks_in_range(node.byte_range()))
let range = node.byte_range();
let cursor = self.0.chunk_cursor_at(range.start);
ByteChunks {
cursor,
end: range.end,
}
}
}
@ -53,7 +62,14 @@ impl<'a> Iterator for ByteChunks<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(str::as_bytes)
let cursor = &mut self.cursor;
let end = self.end;
if cursor.next() && cursor.byte_offset() < end {
Some(cursor.chunk().as_bytes())
} else {
None
}
}
}
@ -315,11 +331,14 @@ impl SyntaxHighlighter {
.unwrap_or(self.parser.parse("", None).unwrap());
old_tree.edit(&edit);
let mut chunks = text.chunks();
let new_tree = self.parser.parse_with_options(
&mut move |offset, _| {
chunks.seek(offset);
chunks.next().unwrap_or("").as_bytes()
if offset >= text.len() {
""
} else {
let (chunk, chunk_byte_ix) = text.chunk(offset);
&chunk[offset - chunk_byte_ix..]
}
},
Some(&old_tree),
None,

View file

@ -1,4 +1,4 @@
use std::ops::Range;
use std::ops::{Range, RangeBounds};
/// A selection in the text, represented by start and end byte indices.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
@ -42,6 +42,15 @@ impl From<Selection> for Range<usize> {
value.start..value.end
}
}
impl RangeBounds<usize> for Selection {
fn start_bound(&self) -> std::ops::Bound<&usize> {
std::ops::Bound::Included(&self.start)
}
fn end_bound(&self) -> std::ops::Bound<&usize> {
std::ops::Bound::Excluded(&self.end)
}
}
pub type Position = lsp_types::Position;

View file

@ -6,7 +6,7 @@ use gpui::{
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun,
UnderlineStyle, Window,
};
use rope::Rope;
use ropey::Rope;
use smallvec::SmallVec;
use crate::{
@ -508,7 +508,7 @@ impl TextElement {
let mut styles = vec![];
for line in text
.lines()
.rows()
.skip(visible_range.start)
.take(visible_range.len())
{
@ -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 as u32..visible_range.end as u32)
.slice_rows(visible_range.start..visible_range.end)
.to_string();
let lines = window
@ -792,11 +792,8 @@ 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 longtest_line: SharedString = state
.text
.line(state.text.summary().longest_row as usize)
.to_string()
.into();
let longest_row = state.text_wrapper.longest_row.row;
let longtest_line: SharedString = state.text.slice_row(longest_row).to_string().into();
longest_line_width = window
.text_system()
.shape_line(

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use gpui::{Context, EntityInputHandler, Task, Window};
use lsp_types::{request::Completion, CompletionContext, CompletionItem, CompletionResponse};
use rope::Rope;
use ropey::Rope;
use std::{cell::RefCell, ops::Range, rc::Rc};
use crate::input::{

View file

@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::{
px, App, Context, HighlightStyle, Hitbox, MouseDownEvent, Task, UnderlineStyle, Window,
};
use rope::Rope;
use ropey::Rope;
use std::{ops::Range, rc::Rc};
use crate::{

View file

@ -1,6 +1,6 @@
use anyhow::Result;
use gpui::{App, Context, Task, Window};
use rope::Rope;
use ropey::Rope;
use crate::input::{popovers::HoverPopover, InputState, RopeExt};

View file

@ -22,7 +22,7 @@ pub use mask_pattern::MaskPattern;
pub use mode::TabSize;
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
pub use otp_input::*;
pub use rope::Rope;
pub use rope_ext::*;
pub use ropey::Rope;
pub use state::*;
pub use text_input::*;

View file

@ -2,12 +2,13 @@ use std::rc::Rc;
use std::{cell::RefCell, ops::Range};
use gpui::{App, SharedString};
use rope::Rope;
use tree_sitter::{InputEdit, Point};
use ropey::Rope;
use tree_sitter::InputEdit;
use super::text_wrapper::TextWrapper;
use crate::highlighter::DiagnosticSet;
use crate::highlighter::SyntaxHighlighter;
use crate::input::RopeExt as _;
#[derive(Debug, Copy, Clone)]
pub struct TabSize {
@ -210,15 +211,9 @@ impl InputMode {
start_byte: selected_range.start,
old_end_byte: selected_range.end,
new_end_byte: new_end,
start_position: Point::new(start_pos.row as usize, start_pos.column as usize),
old_end_position: Point::new(
old_end_pos.row as usize,
old_end_pos.column as usize,
),
new_end_position: Point::new(
new_end_pos.row as usize,
new_end_pos.column as usize,
),
start_position: start_pos,
old_end_position: old_end_pos,
new_end_position: new_end_pos,
};
highlighter.update(Some(edit), text);

View file

@ -1,19 +1,25 @@
use std::ops::Range;
use rope::{Point, Rope};
use ropey::{LineType, Rope, RopeSlice};
use sum_tree::Bias;
use tree_sitter::Point;
use crate::input::Position;
/// An extension trait for `Rope` to provide additional utility methods.
pub trait RopeExt {
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
///
/// Return empty rope if the row (0-based) is out of bounds.
fn line(&self, row: usize) -> Rope;
/// Start offset of the line at the given row (0-based) index.
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<usize>) -> RopeSlice<'_>;
fn rows(&self) -> impl Iterator<Item = RopeSlice<'_>> + '_ {
(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.
@ -22,22 +28,19 @@ pub trait RopeExt {
/// Return the number of lines in the rope.
fn lines_len(&self) -> usize;
/// Return the lines iterator.
///
/// Each line is including the `\r` at the end, but not `\n`.
fn lines(&self) -> RopeLines;
/// Check is equal to another rope.
fn eq(&self, other: &Rope) -> bool;
/// Return the length of the row (0-based) in characters, including `\r` if present, but not `\n`.
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.
fn replace(&mut self, range: Range<usize>, new_text: &str);
/// Get char at the given offset (byte).
///
/// If the offset is in the middle of a multi-byte character will panic.
///
/// If the offset is out of bounds, return None.
/// - If the offset is in the middle of a multi-byte character will panic.
/// - If the offset is out of bounds, return None.
fn char_at(&self, offset: usize) -> Option<char>;
/// Get the byte offset from the given line, column [`Position`] (0-based).
@ -46,76 +49,76 @@ pub trait RopeExt {
/// Get the line, column [`Position`] (0-based) from the given byte offset.
fn offset_to_position(&self, offset: usize) -> Position;
fn offset_to_point(&self, offset: usize) -> Point;
fn point_to_offset(&self, point: Point) -> usize;
/// Get the word byte range at the given offset (byte).
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
/// Get word at the given offset (byte).
fn word_at(&self, offset: usize) -> String;
/// Convert offset_utf16 to offset (byte).
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize;
/// Convert offset (byte) to offset_utf16.
fn offset_to_offset_utf16(&self, offset: usize) -> usize;
/// Get a clipped offset (avoid in a char boundary).
///
/// - 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.
fn clip_offset(&self, offset: usize, bias: Bias) -> usize;
}
/// An iterator over the lines of a `Rope`.
pub struct RopeLines {
row: usize,
end_row: usize,
rope: Rope,
}
impl RopeLines {
/// Create a new `RopeLines` iterator.
pub fn new(rope: Rope) -> Self {
let end_row = rope.lines_len();
Self {
row: 0,
end_row,
rope,
}
}
}
impl Iterator for RopeLines {
type Item = Rope;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.row >= self.end_row {
return None;
}
let line = self.rope.line(self.row);
self.row += 1;
Some(line)
}
#[inline]
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.row = self.row.saturating_add(n);
self.next()
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.end_row - self.row;
(len, Some(len))
}
}
impl std::iter::ExactSizeIterator for RopeLines {}
impl std::iter::FusedIterator for RopeLines {}
impl RopeExt for Rope {
fn line(&self, row: usize) -> Rope {
let start = self.line_start_offset(row);
let end = start + self.line_len(row as u32) as usize;
fn slice_row(&self, row: usize) -> RopeSlice<'_> {
if row >= self.lines_len() {
return self.slice(0..0);
}
let line = self.line(row, LineType::LF_CR);
if line.len() > 0 && line.chars().last() == Some('\n') {
line.slice(..line.len().saturating_sub(1))
} else {
line
}
}
fn slice_rows(&self, rows_range: Range<usize>) -> 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 line_len(&self, row: usize) -> usize {
self.slice_row(row).len()
}
fn line_start_offset(&self, row: usize) -> usize {
let row = row as u32;
self.point_to_offset(Point::new(row, 0))
}
fn offset_to_point(&self, offset: usize) -> Point {
let offset = self.clip_offset(offset, Bias::Left);
let row = self.byte_to_line_idx(offset, LineType::LF_CR);
let line_start = self.line_to_byte_idx(row, LineType::LF_CR);
let column = offset.saturating_sub(line_start);
Point::new(row, column)
}
fn point_to_offset(&self, point: Point) -> usize {
if point.row >= self.lines_len() {
return self.len();
}
let line_start = self.line_to_byte_idx(point.row, LineType::LF_CR);
line_start + point.column
}
fn position_to_offset(&self, pos: &Position) -> usize {
let line = self.line(pos.line as usize);
let line = self.slice_row(pos.line as usize);
self.line_start_offset(pos.line as usize)
+ line
.chars()
@ -126,30 +129,22 @@ impl RopeExt for Rope {
fn offset_to_position(&self, offset: usize) -> Position {
let point = self.offset_to_point(offset);
let line = self.line(point.row as usize);
let column = line.clip_offset(point.column as usize, sum_tree::Bias::Left);
let character = line.slice(0..column).chars().count();
Position::new(point.row, character as u32)
let line = self.slice_row(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)
}
fn line_end_offset(&self, row: usize) -> usize {
if row > self.max_point().row as usize {
if row > self.lines_len() {
return self.len();
}
self.line_start_offset(row) + self.line_len(row as u32) as usize
self.line_start_offset(row) + self.line_len(row)
}
fn lines_len(&self) -> usize {
self.max_point().row as usize + 1
}
fn lines(&self) -> RopeLines {
RopeLines::new(self.clone())
}
fn eq(&self, other: &Rope) -> bool {
self.summary() == other.summary()
self.len_lines(LineType::LF_CR)
}
fn chars_count(&self) -> usize {
@ -161,8 +156,7 @@ impl RopeExt for Rope {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
self.slice(offset..self.len()).chars().next()
self.get_char(offset).ok()
}
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
@ -170,10 +164,9 @@ impl RopeExt for Rope {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
let mut left = String::new();
for c in self.reversed_chars_at(offset) {
let offset = self.clip_offset(offset, Bias::Left);
for c in self.chars_at(offset).reversed() {
if c.is_alphanumeric() || c == '_' {
left.insert(0, c);
} else {
@ -203,24 +196,65 @@ impl RopeExt for Rope {
String::new()
}
}
#[inline]
fn offset_utf16_to_offset(&self, offset_utf16: usize) -> usize {
if offset_utf16 > self.len_utf16() {
return self.len();
}
self.utf16_to_byte_idx(offset_utf16)
}
#[inline]
fn offset_to_offset_utf16(&self, offset: usize) -> usize {
if offset > self.len() {
return self.len_utf16();
}
self.byte_to_utf16_idx(offset)
}
fn replace(&mut self, range: Range<usize>, new_text: &str) {
self.remove(range.clone());
self.insert(range.start, new_text);
}
fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
if offset > self.len() {
return self.len();
}
if self.is_char_boundary(offset) {
return offset;
}
if bias == Bias::Left {
self.floor_char_boundary(offset)
} else {
self.ceil_char_boundary(offset)
}
}
}
#[cfg(test)]
mod tests {
use rope::Rope;
use ropey::Rope;
use sum_tree::Bias;
use tree_sitter::Point;
use crate::input::{Position, RopeExt as _};
use crate::input::{Position, RopeExt};
#[test]
fn test_line() {
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
assert_eq!(rope.line(0).to_string(), "Hello");
assert_eq!(rope.line(1).to_string(), "World\r");
assert_eq!(rope.line(2).to_string(), "This is a test 中文");
assert_eq!(rope.line(3).to_string(), "Rope");
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");
// over bounds
assert_eq!(rope.line(6).to_string(), "");
assert_eq!(rope.slice_row(6).to_string(), "");
}
#[test]
@ -244,9 +278,9 @@ mod tests {
}
#[test]
fn test_lines() {
fn test_rows() {
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
let lines: Vec<_> = rope.lines().map(|r| r.to_string()).collect();
let lines: Vec<_> = rope.rows().map(|r| r.to_string()).collect();
assert_eq!(
lines,
vec!["Hello", "World\r", "This is a test 中文", "Rope"]
@ -305,6 +339,32 @@ mod tests {
);
}
#[test]
fn test_offset_to_point() {
let rope = Rope::from("a 中文🎉 test\nRope");
assert_eq!(rope.offset_to_point(0), Point::new(0, 0));
assert_eq!(rope.offset_to_point(1), Point::new(0, 1));
assert_eq!(rope.offset_to_point("a 中".len()), Point::new(0, 5));
assert_eq!(rope.offset_to_point("a 中文🎉".len()), Point::new(0, 12));
assert_eq!(
rope.offset_to_point("a 中文🎉 test\nR".len()),
Point::new(1, 1)
);
}
#[test]
fn test_point_to_offset() {
let rope = Rope::from("a 中文🎉 test\nRope");
assert_eq!(rope.point_to_offset(Point::new(0, 0)), 0);
assert_eq!(rope.point_to_offset(Point::new(0, 1)), 1);
assert_eq!(rope.point_to_offset(Point::new(0, 5)), "a 中".len());
assert_eq!(rope.point_to_offset(Point::new(0, 12)), "a 中文🎉".len());
assert_eq!(
rope.point_to_offset(Point::new(1, 1)),
"a 中文🎉 test\nR".len()
);
}
#[test]
fn test_char_at() {
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
@ -336,4 +396,65 @@ mod tests {
assert_eq!(rope.word_range(44), Some(42..46));
assert_eq!(rope.word_at(45), "Rope");
}
#[test]
fn test_offset_utf16_conversion() {
let rope = Rope::from("hello 中文🎉 test\nRope");
assert_eq!(rope.offset_to_offset_utf16("hello".len()), 5);
assert_eq!(rope.offset_to_offset_utf16("hello 中".len()), 7);
assert_eq!(rope.offset_to_offset_utf16("hello 中文".len()), 8);
assert_eq!(rope.offset_to_offset_utf16("hello 中文🎉".len()), 10);
assert_eq!(rope.offset_to_offset_utf16(100), 20);
assert_eq!(rope.offset_utf16_to_offset(5), "hello".len());
assert_eq!(rope.offset_utf16_to_offset(7), "hello 中".len());
assert_eq!(rope.offset_utf16_to_offset(8), "hello 中文".len());
assert_eq!(rope.offset_utf16_to_offset(10), "hello 中文🎉".len());
assert_eq!(rope.offset_utf16_to_offset(100), rope.len());
}
#[test]
fn test_replace() {
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"
);
rope.replace(0..5, "Hi");
assert_eq!(
rope.to_string(),
"Hi\nUniverse\r\nThis is a test 中文\nRope"
);
rope.replace(rope.len() - 4..rope.len(), "String");
assert_eq!(
rope.to_string(),
"Hi\nUniverse\r\nThis is a test 中文\nString"
);
}
#[test]
fn test_clip_offset() {
let rope = Rope::from("Hello 中文🎉 test\nRope");
// Inside multi-byte character '中' (3 bytes)
assert_eq!(rope.clip_offset(5, Bias::Left), 5);
assert_eq!(rope.clip_offset(7, Bias::Left), 6);
assert_eq!(rope.clip_offset(7, Bias::Right), 9);
assert_eq!(rope.clip_offset(9, Bias::Left), 9);
// Inside multi-byte character '🎉' (4 bytes)
assert_eq!(rope.clip_offset(13, Bias::Left), 12);
assert_eq!(rope.clip_offset(13, Bias::Right), 16);
assert_eq!(rope.clip_offset(16, Bias::Left), 16);
// At character boundary
assert_eq!(rope.clip_offset(5, Bias::Left), 5);
assert_eq!(rope.clip_offset(5, Bias::Right), 5);
// Out of bounds
assert_eq!(rope.clip_offset(26, Bias::Left), 26);
assert_eq!(rope.clip_offset(100, Bias::Left), 26);
}
}

View file

@ -7,13 +7,13 @@ use gpui::{
Entity, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
ParentElement as _, Pixels, Render, Styled, Subscription, Window,
};
use rope::Rope;
use ropey::Rope;
use crate::{
actions::SelectPrev,
button::{Button, ButtonVariants},
h_flex,
input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt, Search, TextInput},
input::{Enter, Escape, IndentInline, InputEvent, InputState, RopeExt as _, Search, TextInput},
label::Label,
v_flex, ActiveTheme, Disableable, IconName, Selectable, Sizable,
};
@ -65,7 +65,9 @@ impl SearchMatcher {
fn update_matches(&mut self) {
let mut new_ranges = Vec::new();
if let Some(query) = &self.query {
let matches = query.stream_find_iter(self.text.bytes_in_range(0..self.text.len()));
let text = self.text.to_string();
// FIXME: Use stream find
let matches = query.stream_find_iter(text.as_bytes());
for query_match in matches.into_iter() {
let query_match = query_match.expect("query match for select all action");
@ -201,7 +203,7 @@ impl InputState {
let text = self.text.clone();
let editor = cx.entity();
let selected_text = self.selected_text();
let selected_text = Rope::from(self.selected_text());
search_panel.update(cx, |this, cx| {
this.editor = editor;
this.matcher.update(&text);

View file

@ -11,7 +11,7 @@ use gpui::{
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
WrappedLine,
};
use rope::{OffsetUtf16, Rope};
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
use smallvec::SmallVec;
use std::cell::RefCell;
@ -581,19 +581,18 @@ impl InputState {
};
let point = self.text.offset_to_point(self.cursor());
let row = (point.row as usize).saturating_sub(last_layout.visible_range.start);
let row = point.row.saturating_sub(last_layout.visible_range.start);
let Some(line) = last_layout.lines.get(row) else {
self.preferred_column = None;
return;
};
let Some(pos) = line.position_for_index(point.column as usize, last_layout.line_height)
else {
let Some(pos) = line.position_for_index(point.column, last_layout.line_height) else {
self.preferred_column = None;
return;
};
self.preferred_column = Some((pos.x, point.column as usize));
self.preferred_column = Some((pos.x, point.column));
}
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
@ -644,17 +643,18 @@ impl InputState {
let was_preferred_column = self.preferred_column;
let row = self.text.offset_to_point(offset).row;
let new_row = row.saturating_add_signed(move_lines as i32);
let line_start_offset = self.text.point_to_offset(rope::Point::new(new_row, 0));
let new_row = row.saturating_add_signed(move_lines);
let line_start_offset = self
.text
.point_to_offset(tree_sitter::Point::new(new_row, 0));
let mut new_offset = line_start_offset;
if let Some((preferred_x, column)) = was_preferred_column {
let new_column = column.min(self.text.line(new_row as usize).len());
let new_column = column.min(self.text.slice_row(new_row).len());
new_offset = line_start_offset + new_column;
// If in visible range, prefer to use position to get column.
let new_row = new_row as usize;
if new_row >= last_layout.visible_range.start {
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
if let Some(line) = last_layout.lines.get(visible_row) {
@ -889,13 +889,7 @@ impl InputState {
cx: &mut Context<Self>,
) {
let position: Position = position.into();
let max_point = self.text.max_point();
let row = position.line.min(max_point.row);
let col = position.character.min(self.text.line_len(row));
let offset = self
.text
.point_to_offset(rope::Point::new(row as u32, col as u32));
let offset = self.text.position_to_offset(&position);
self.move_to(offset, cx);
self.update_preferred_column();
@ -1164,7 +1158,7 @@ impl InputState {
}
let row = self.text.offset_to_point(self.cursor()).row;
self.text.line_start_offset(row as usize)
self.text.line_start_offset(row)
}
/// Get end of line byte offset of cursor
@ -1174,7 +1168,7 @@ impl InputState {
}
let row = self.text.offset_to_point(self.cursor()).row;
self.text.line_end_offset(row as usize)
self.text.line_end_offset(row)
}
/// Get start line of selection start or end (The min value).
@ -1668,7 +1662,7 @@ impl InputState {
let line_height = last_layout.line_height;
let point = self.text.offset_to_point(offset);
let row = point.row as usize;
let row = point.row;
let mut row_offset_y = px(0.);
for (ix, wrap_line) in self.text_wrapper.lines.iter().enumerate() {
@ -1710,7 +1704,7 @@ impl InputState {
return;
}
let selected_text = self.text.slice(self.selected_range.into()).to_string();
let selected_text = self.text.slice(self.selected_range).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
}
@ -1719,7 +1713,7 @@ impl InputState {
return;
}
let selected_text = self.text.slice(self.selected_range.into()).to_string();
let selected_text = self.text.slice(self.selected_range).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
self.replace_text_in_range_silent(None, "", window, cx);
@ -2014,12 +2008,12 @@ impl InputState {
#[inline]
pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
self.text.offset_utf16_to_offset(OffsetUtf16(offset))
self.text.offset_utf16_to_offset(offset)
}
#[inline]
pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
self.text.offset_to_offset_utf16(offset).0
self.text.offset_to_offset_utf16(offset)
}
#[inline]
@ -2191,7 +2185,7 @@ impl InputState {
}
}
pub(super) fn selected_text(&self) -> Rope {
pub(super) fn selected_text(&self) -> RopeSlice<'_> {
let range_utf16 = self.range_to_utf16(&self.selected_range.into());
let range = self.range_from_utf16(&range_utf16);
self.text.slice(range)

View file

@ -1,7 +1,7 @@
use std::ops::Range;
use gpui::{App, Font, LineFragment, Pixels};
use rope::Rope;
use ropey::{LineType, Rope};
use crate::input::RopeExt;
@ -37,6 +37,14 @@ impl LineItem {
}
}
#[derive(Debug, Default)]
pub(super) struct LongestRow {
/// The 0-based row index.
pub row: usize,
/// The bytes length of the longest line.
pub len: usize,
}
/// Used to prepare the text with soft wrap to be get lines to displayed in the Editor.
///
/// After use lines to calculate the scroll size of the Editor.
@ -48,6 +56,8 @@ pub(super) struct TextWrapper {
font_size: Pixels,
/// If is none, it means the text is not wrapped
wrap_width: Option<Pixels>,
/// The longest (row, bytes len) in characters, used to calculate the horizontal scroll width.
pub(super) longest_row: LongestRow,
/// The lines by split \n
pub(super) lines: Vec<LineItem>,
}
@ -61,6 +71,7 @@ impl TextWrapper {
font_size,
wrap_width,
soft_lines: 0,
longest_row: LongestRow::default(),
lines: Vec::new(),
}
}
@ -149,18 +160,25 @@ impl TextWrapper {
}
// Remove the old changed lines.
let start_row = self.text.offset_to_point(range.start).row as usize;
let start_row = self.text.offset_to_point(range.start).row;
let start_row = start_row.min(self.lines.len().saturating_sub(1));
let end_row = self.text.offset_to_point(range.end).row as usize;
let end_row = self.text.offset_to_point(range.end).row;
let end_row = end_row.min(self.lines.len().saturating_sub(1));
let rows_range = start_row..=end_row;
if rows_range.contains(&self.longest_row.row) {
self.longest_row = LongestRow::default();
}
let mut longest_row_ix = self.longest_row.row;
let mut longest_row_len = self.longest_row.len;
// To add the new lines.
let new_start_row = changed_text.offset_to_point(range.start).row as usize;
let new_start_row = changed_text.offset_to_point(range.start).row;
let new_start_offset = changed_text.line_start_offset(new_start_row);
let new_end_row = changed_text
.offset_to_point(range.start + new_text.len())
.row as usize;
.row;
let new_end_offset = changed_text.line_end_offset(new_end_row);
let new_range = new_start_offset..new_end_offset;
@ -168,11 +186,27 @@ impl TextWrapper {
let wrap_width = self.wrap_width;
for line in changed_text.slice(new_range).lines() {
for (ix, line) in changed_text
.slice(new_range)
.lines(LineType::LF_CR)
.enumerate()
{
// Remove the last `\n`
let line = if line.len() > 0 && line.chars().last() == Some('\n') {
line.slice(..line.len().saturating_sub(1))
} else {
line
};
let line_str = line.to_string();
let mut wrapped_lines = vec![];
let mut prev_boundary_ix = 0;
if line_str.len() > longest_row_len {
longest_row_ix = new_start_row + ix;
longest_row_len = line_str.len();
}
// If wrap_width is Pixels::MAX, skip wrapping to disable word wrap
if let Some(wrap_width) = wrap_width {
// Here only have wrapped line, if there is no wrap meet, the `line_wraps` result will empty.
@ -188,7 +222,7 @@ impl TextWrapper {
}
new_lines.push(LineItem {
line: line.clone(),
line: Rope::from(line),
wrapped_lines,
});
}
@ -204,6 +238,10 @@ impl TextWrapper {
// dbg!(self.lines.len());
self.text = changed_text.clone();
self.soft_lines = self.lines.iter().map(|l| l.lines_len()).sum();
self.longest_row = LongestRow {
row: longest_row_ix,
len: longest_row_len,
}
}
/// Update the text wrapper and recalculate the wrapped lines.

View file

@ -11,7 +11,7 @@ use lsp_types::{
CompletionItem, CompletionItemKind, CompletionResponse, CompletionTextEdit, Diagnostic,
DiagnosticSeverity, Position, TextEdit,
};
use rope::Rope;
use ropey::Rope;
use crate::{
alert::Alert,
@ -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.lines() {
for line in rope.rows() {
if line.to_string().trim().starts_with("//") {
offset += line.len() + 1;
continue;
@ -564,7 +564,7 @@ struct LspProvider {}
impl CompletionProvider for LspProvider {
fn completions(
&self,
rope: &rope::Rope,
rope: &ropey::Rope,
offset: usize,
_: lsp_types::CompletionContext,
_: &mut Window,

View file

@ -7,7 +7,7 @@ use gpui::{
StatefulInteractiveElement, Styled, StyledImage as _, Window,
};
use markdown::mdast;
use rope::Rope;
use ropey::Rope;
use crate::{
h_flex,
@ -291,7 +291,7 @@ impl CodeBlock {
let mut styles = vec![];
if let Some(lang) = &lang {
let mut highlighter = SyntaxHighlighter::new(&lang, cx);
highlighter.update(None, &Rope::from(code.as_str()));
highlighter.update(None, &Rope::from_str(code.as_str()));
styles = highlighter.styles(&(0..code.len()), cx);
};