input: Fix line column by use chars offset. (#1239)

- Add autocorrect to markdown example for test `markers`.

<img width="1104" height="590" alt="image"
src="https://github.com/user-attachments/assets/03103088-e828-4039-82b0-c816d5f36141"
/>
This commit is contained in:
Jason Lee 2025-09-10 20:56:15 +08:00 committed by GitHub
parent 2a918bf4de
commit 3fa996c331
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 185 additions and 67 deletions

97
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -164,7 +164,7 @@ impl Example {
}
}
fn set_markers(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn set_markers(&mut self, _: &mut Window, cx: &mut Context<Self>) {
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,
);
});

View file

@ -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

View file

@ -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<InputState>,
resizable_state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>,
}
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,
}
}

View file

@ -61,33 +61,6 @@ impl From<(usize, usize)> for LineColumn {
}
}
impl From<rope::Point> for LineColumn {
fn from(value: rope::Point) -> Self {
Self {
line: value.row as usize + 1,
column: value.column as usize + 1,
}
}
}
impl From<LineColumn> 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<LineColumn> 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]

View file

@ -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);
}

View file

@ -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;

View file

@ -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<char>;
/// 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::<usize>()
}
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");

View file

@ -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<Marker>, _: &mut Window, _: &mut Context<Self>) {
pub fn set_markers(&mut self, markers: Vec<Marker>, _: &mut Context<Self>) {
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.