gpui-component/crates/ui/src/input/mode.rs
Jason Lee 344e4407ff
input: Use Rope for InputState. (#1208)
This PR to change the Input to use
[ropey](https://github.com/cessen/ropey) to store the text.

## Break Changes

- The `value` method of `InputState` now returns a `SharedString` type.

```diff
- pub fn value(&self) -> &SharedString
+ pub fn value(&self) -> SharedString
```

Ref links:

- https://github.com/cessen/ropey
- https://zed.dev/blog/zed-decoded-rope-sumtree
- https://github.com/helix-editor/helix/blob/master/docs/architecture.md
2025-09-05 18:20:47 +08:00

287 lines
7.8 KiB
Rust

use std::rc::Rc;
use std::{cell::RefCell, ops::Range};
use gpui::{App, SharedString};
use ropey::Rope;
use tree_sitter::{InputEdit, Point};
use crate::input::RopeExt as _;
use crate::{highlighter::SyntaxHighlighter, input::marker::Marker};
use super::text_wrapper::TextWrapper;
#[derive(Debug, Copy, Clone)]
pub struct TabSize {
/// Default is 2
pub tab_size: usize,
/// Set true to use `\t` as tab indent, default is false
pub hard_tabs: bool,
}
impl Default for TabSize {
fn default() -> Self {
Self {
tab_size: 2,
hard_tabs: false,
}
}
}
impl TabSize {
pub(super) fn to_string(&self) -> SharedString {
if self.hard_tabs {
"\t".into()
} else {
" ".repeat(self.tab_size).into()
}
}
}
#[derive(Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
tab: TabSize,
rows: usize,
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
CodeEditor {
tab: TabSize,
rows: usize,
/// Show line number
line_number: bool,
language: SharedString,
highlighter: Rc<RefCell<Option<SyntaxHighlighter>>>,
markers: Rc<Vec<Marker>>,
},
}
#[allow(unused)]
impl InputMode {
#[inline]
pub(super) fn is_single_line(&self) -> bool {
matches!(self, InputMode::SingleLine)
}
#[inline]
pub(super) fn is_code_editor(&self) -> bool {
matches!(self, InputMode::CodeEditor { .. })
}
#[inline]
pub(super) fn is_auto_grow(&self) -> bool {
matches!(self, InputMode::AutoGrow { .. })
}
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
matches!(
self,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
)
}
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::CodeEditor { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
let wrapped_lines = text_wrapper.wrapped_lines.len();
self.set_rows(wrapped_lines);
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::CodeEditor { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
/// Return false if the mode is not [`InputMode::CodeEditor`].
#[allow(unused)]
#[inline]
pub(super) fn line_number(&self) -> bool {
match self {
InputMode::CodeEditor { line_number, .. } => *line_number,
_ => false,
}
}
#[inline]
pub(super) fn tab_size(&self) -> Option<&TabSize> {
match self {
InputMode::MultiLine { tab, .. } => Some(tab),
InputMode::CodeEditor { tab, .. } => Some(tab),
_ => None,
}
}
pub(super) fn update_highlighter(
&mut self,
selected_range: &Range<usize>,
text: &Rope,
new_text: &str,
force: bool,
cx: &mut App,
) {
match &self {
InputMode::CodeEditor {
language,
highlighter,
..
} => {
if !force && highlighter.borrow().is_some() {
return;
}
let mut highlighter = highlighter.borrow_mut();
if highlighter.is_none() {
let new_highlighter = SyntaxHighlighter::new(language, cx);
highlighter.replace(new_highlighter);
}
let Some(highlighter) = highlighter.as_mut() else {
return;
};
// When full text changed, the selected_range may be out of bound (The before version).
let mut selected_range = selected_range.clone();
selected_range.end = selected_range.end.min(text.len_bytes());
// If insert a chart, this is 1.
// If backspace or delete, this is -1.
// If selected to delete, this is the length of the selected text.
// let changed_len = new_text.len() as isize - selected_range.len() as isize;
let changed_len = new_text.len() as isize - selected_range.len() as isize;
let new_end = (selected_range.end as isize + changed_len) as usize;
let start_pos = text.line_column(selected_range.start);
let old_end_pos = text.line_column(selected_range.end);
let new_end_pos = text.line_column(new_end);
let edit = InputEdit {
start_byte: selected_range.start,
old_end_byte: selected_range.end,
new_end_byte: new_end,
start_position: Point::new(start_pos.0, start_pos.1),
old_end_position: Point::new(old_end_pos.0, old_end_pos.1),
new_end_position: Point::new(new_end_pos.0, new_end_pos.1),
};
highlighter.update(Some(edit), text, cx);
}
_ => {}
}
}
pub(super) fn clear_markers(&mut self) {
match self {
InputMode::CodeEditor { markers, .. } => *markers = Rc::new(vec![]),
_ => {}
}
}
#[allow(unused)]
pub(super) fn markers(&self) -> Option<&Rc<Vec<Marker>>> {
match self {
InputMode::CodeEditor { markers, .. } => Some(markers),
_ => None,
}
}
pub(super) fn set_markers(&mut self, new_markers: Vec<Marker>) {
match self {
InputMode::CodeEditor { markers, .. } => *markers = Rc::new(new_markers),
_ => {}
}
}
pub(super) fn marker_for_offset(&self, offset: usize) -> Option<&Marker> {
let Some(markers) = self.markers() else {
return None;
};
for marker in markers.iter() {
if let Some(range) = marker.range.as_ref() {
if range.contains(&offset) {
return Some(marker);
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::TabSize;
#[test]
fn test_tab_size() {
let tab = TabSize {
tab_size: 2,
hard_tabs: false,
};
assert_eq!(tab.to_string(), " ");
let tab = TabSize {
tab_size: 4,
hard_tabs: false,
};
assert_eq!(tab.to_string(), " ");
let tab = TabSize {
tab_size: 2,
hard_tabs: true,
};
assert_eq!(tab.to_string(), "\t");
let tab = TabSize {
tab_size: 4,
hard_tabs: true,
};
assert_eq!(tab.to_string(), "\t");
}
}