diff --git a/Cargo.lock b/Cargo.lock index 8f0fb834..f7c7df06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -615,6 +615,36 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "autocorrect" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3199cc73b9b6af61f5034dcdb28c21e6050ffdc1229a20abfc71c244083ab9dc" +dependencies = [ + "autocorrect-derive", + "diff", + "ignore", + "lazy_static", + "owo-colors", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "serde_repr", + "serde_yaml", +] + +[[package]] +name = "autocorrect-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80889323facc446add06be71b9c4c000acd083b12563d9c914b86424bf2f17a" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "av1-grain" version = "0.2.4" @@ -1884,6 +1914,12 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -3625,7 +3661,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -4902,7 +4938,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", @@ -5414,6 +5450,12 @@ dependencies = [ "ttf-parser 0.25.1", ] +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "pango" version = "0.18.3" @@ -5515,6 +5557,50 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.14", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.105", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.8.0" @@ -7630,6 +7716,7 @@ name = "story" version = "0.1.0" dependencies = [ "anyhow", + "autocorrect", "chrono", "fake", "gpui", @@ -8872,6 +8959,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 8b43324c..38cd3652 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -23,6 +23,7 @@ tracing.workspace = true tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tree-sitter-navi = "0.2.2" itertools = "0.14.0" +autocorrect = "2.14.2" [target.'cfg(target_os = "linux")'.dependencies] gtk = { version = "0.18" } diff --git a/crates/story/examples/code-editor.rs b/crates/story/examples/code-editor.rs index d44e2de8..a39c64c7 100644 --- a/crates/story/examples/code-editor.rs +++ b/crates/story/examples/code-editor.rs @@ -164,7 +164,7 @@ impl Example { } } - fn set_markers(&mut self, window: &mut Window, cx: &mut Context) { + fn set_markers(&mut self, _: &mut Window, cx: &mut Context) { if self.language.name() != "rust" { return; } @@ -177,7 +177,6 @@ impl Example { Marker::new("info", (25, 10), (25, 20), "This is a info message, this is a very long message, with **Markdown** support."), Marker::new("hint", (36, 9), (40, 10), "This is a hint message."), ], - window, cx, ); }); diff --git a/crates/story/examples/fixtures/test.rs b/crates/story/examples/fixtures/test.rs index 7609abc2..6b4ecae7 100644 --- a/crates/story/examples/fixtures/test.rs +++ b/crates/story/examples/fixtures/test.rs @@ -7,6 +7,7 @@ use tokio::time; const VERSION: &str = "1.0.0"; /// HelloWorld struct provides greeting functionality with configuration options +/// This is CJK δΈ­ζ–‡πŸŽŠ for test line, column. /// /// # Features /// - Async greetings with customizable names diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs index dfd56070..d785d27f 100644 --- a/crates/story/examples/markdown.rs +++ b/crates/story/examples/markdown.rs @@ -1,7 +1,7 @@ use gpui::*; use gpui_component::{ highlighter::{HighlightTheme, Language}, - input::{InputEvent, InputState, TabSize, TextInput}, + input::{InputEvent, InputState, Marker, MarkerSeverity, TabSize, TextInput}, resizable::{h_resizable, resizable_panel, ResizableState}, text::{TextView, TextViewStyle}, ActiveTheme as _, @@ -11,6 +11,7 @@ use story::Assets; pub struct Example { input_state: Entity, resizable_state: Entity, + _subscriptions: Vec, } const EXAMPLE: &str = include_str!("./fixtures/test.md"); @@ -30,13 +31,37 @@ impl Example { }); let resizable_state = ResizableState::new(cx); - let _subscribe = cx.subscribe(&input_state, |_, _, _: &InputEvent, cx| { + let _subscriptions = vec![cx.subscribe(&input_state, |_, input, _: &InputEvent, cx| { + // Subscribe to input changes and perform linting with AutoCorrect for markers example. + let value = input.read(cx).value().clone(); + let result = autocorrect::lint_for(value.as_str(), "md"); + + let mut markets = vec![]; + for item in result.lines.iter() { + let severity = match item.severity { + autocorrect::Severity::Error => MarkerSeverity::Warning, + autocorrect::Severity::Warning => MarkerSeverity::Hint, + autocorrect::Severity::Pass => MarkerSeverity::Info, + }; + + let start = (item.line, item.col); + let end = (item.line, item.col + item.old.chars().count()); + let message = format!("AutoCorrect: {}", item.new); + let market = Marker::new(severity, start, end, message); + markets.push(market); + } + + input.update(cx, |state, cx| { + state.set_markers(markets, cx); + }); + cx.notify(); - }); + })]; Self { resizable_state, input_state, + _subscriptions, } } diff --git a/crates/ui/src/input/cursor.rs b/crates/ui/src/input/cursor.rs index 89197583..6ff9596e 100644 --- a/crates/ui/src/input/cursor.rs +++ b/crates/ui/src/input/cursor.rs @@ -61,33 +61,6 @@ impl From<(usize, usize)> for LineColumn { } } -impl From for LineColumn { - fn from(value: rope::Point) -> Self { - Self { - line: value.row as usize + 1, - column: value.column as usize + 1, - } - } -} - -impl From for rope::Point { - fn from(value: LineColumn) -> Self { - Self { - row: value.line.saturating_sub(1) as u32, - column: value.column.saturating_sub(1) as u32, - } - } -} - -impl From for tree_sitter::Point { - fn from(value: LineColumn) -> Self { - Self { - row: value.line.saturating_sub(1), - column: value.column.saturating_sub(1), - } - } -} - impl fmt::Display for LineColumn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.line, self.column) @@ -111,18 +84,6 @@ mod tests { } ); assert_eq!(LineColumn::from((0, 0)), LineColumn { line: 1, column: 1 }); - - assert_eq!( - LineColumn::from(rope::Point::new(0, 1)), - LineColumn { line: 1, column: 2 } - ); - assert_eq!( - LineColumn::from(rope::Point::new(10, 9)), - LineColumn { - line: 11, - column: 10 - } - ); } #[test] diff --git a/crates/ui/src/input/marker.rs b/crates/ui/src/input/marker.rs index b9c98341..7e6c441f 100644 --- a/crates/ui/src/input/marker.rs +++ b/crates/ui/src/input/marker.rs @@ -1,6 +1,6 @@ use crate::{ highlighter::HighlightTheme, - input::{InputState, LineColumn}, + input::{InputState, LineColumn, RopeExt}, }; use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle}; use std::ops::Range; @@ -35,17 +35,8 @@ impl Marker { /// Prepare the marker to convert line, column to byte offsets. pub(super) fn prepare(&mut self, state: &InputState) { - let mut start_point: rope::Point = self.start.into(); - let mut end_point: rope::Point = self.end.into(); - - // limit column avoid overflow - let start_line_len = state.text.line_len(start_point.row); - start_point.column = start_point.column.min(start_line_len); - let end_line_len = state.text.line_len(end_point.row); - end_point.column = end_point.column.min(end_line_len); - - let start = state.text.point_to_offset(start_point); - let end = state.text.point_to_offset(end_point); + let start = state.text.line_column_to_offset(&self.start); + let end = state.text.line_column_to_offset(&self.end); self.range = Some(start..end); } diff --git a/crates/ui/src/input/mod.rs b/crates/ui/src/input/mod.rs index f9ea1c57..65e92fdf 100644 --- a/crates/ui/src/input/mod.rs +++ b/crates/ui/src/input/mod.rs @@ -15,7 +15,7 @@ mod text_input; mod text_wrapper; pub(crate) use clear_button::*; -pub(super) use cursor::*; +pub use cursor::*; pub use marker::*; pub use mask_pattern::MaskPattern; pub use mode::TabSize; diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index d7669861..202c4cb7 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -1,5 +1,7 @@ use rope::{Point, Rope}; +use crate::input::LineColumn; + /// 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`. @@ -35,6 +37,12 @@ pub trait RopeExt { /// /// If the offset is out of bounds, return None. fn char_at(&self, offset: usize) -> Option; + + /// Get the byte offset from the given `LineColumn` (1-based). + fn line_column_to_offset(&self, line_col: &LineColumn) -> usize; + + /// Get the `LineColumn` (1-based) from the given byte offset. + fn offset_to_line_column(&self, offset: usize) -> LineColumn; } /// An iterator over the lines of a `Rope`. @@ -98,6 +106,24 @@ impl RopeExt for Rope { self.point_to_offset(Point::new(row, 0)) } + fn line_column_to_offset(&self, line_col: &LineColumn) -> usize { + let row = line_col.line.saturating_sub(1); + let col = line_col.column.saturating_sub(1); + + let line = self.line(row); + self.line_start_offset(row) + line.chars().take(col).map(|c| c.len_utf8()).sum::() + } + + fn offset_to_line_column(&self, offset: usize) -> LineColumn { + let point = self.offset_to_point(offset); + let line = self.line(point.row as usize); + let column = line.slice(0..point.column as usize).chars().count(); + LineColumn { + line: point.row as usize + 1, + column: column + 1, + } + } + fn line_end_offset(&self, row: usize) -> usize { if row > self.max_point().row as usize { return self.len(); @@ -135,7 +161,7 @@ impl RopeExt for Rope { mod tests { use rope::Rope; - use crate::input::RopeExt as _; + use crate::input::{LineColumn, RopeExt as _}; #[test] fn test_line() { @@ -208,6 +234,32 @@ mod tests { assert_eq!(rope.chars_count(), 11); } + #[test] + fn test_line_column() { + let rope = Rope::from("a δΈ­ζ–‡πŸŽ‰ test\nRope"); + assert_eq!( + rope.line_column_to_offset(&LineColumn::new(1, 4)), + "a δΈ­".len() + ); + assert_eq!( + rope.line_column_to_offset(&LineColumn::new(1, 6)), + "a δΈ­ζ–‡πŸŽ‰".len() + ); + assert_eq!( + rope.line_column_to_offset(&LineColumn::new(2, 2)), + "a δΈ­ζ–‡πŸŽ‰ test\nR".len() + ); + + assert_eq!( + rope.offset_to_line_column("a δΈ­ζ–‡πŸŽ‰ test\nR".len()), + LineColumn::new(2, 2) + ); + assert_eq!( + rope.offset_to_line_column("a δΈ­ζ–‡πŸŽ‰".len()), + LineColumn::new(1, 6) + ); + } + #[test] fn test_char_at() { let rope = Rope::from("Hello\nWorld\r\nThis is a test δΈ­ζ–‡πŸŽ‰\nRope"); diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 3b53be17..728176d1 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -493,7 +493,7 @@ impl InputState { /// Set markers, only for [`InputMode::CodeEditor`] mode. /// /// For example to set the diagnostic markers in the code editor. - pub fn set_markers(&mut self, markers: Vec, _: &mut Window, _: &mut Context) { + pub fn set_markers(&mut self, markers: Vec, _: &mut Context) { let mut markers = markers; for marker in &mut markers { marker.prepare(self); @@ -747,12 +747,7 @@ impl InputState { /// Return the (1-based) line and column of the cursor. pub fn line_column(&self) -> LineColumn { let offset = self.cursor(); - let point = self.text.offset_to_point(offset); - - LineColumn { - line: point.row as usize + 1, - column: point.column as usize + 1, - } + self.text.offset_to_line_column(offset) } /// Set (1-based) line and column of the cursor.