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
287 lines
7.8 KiB
Rust
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");
|
|
}
|
|
}
|