From b3e96b912dcbda07e8166570744dfab97f90c349 Mon Sep 17 00:00:00 2001 From: Tayfun Bocek Date: Thu, 14 May 2026 00:17:38 +0300 Subject: [PATCH 1/2] fix!: return error when the normalized row is off by one --- src/change.rs | 10 ++++------ src/core/text.rs | 28 ++++++++++++++++++++++------ src/error.rs | 14 ++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/change.rs b/src/change.rs index bdf065d..73c082e 100644 --- a/src/change.rs +++ b/src/change.rs @@ -172,13 +172,11 @@ impl GridIndex { /// /// If the row value of the [`GridIndex`] is same as the number of rows, this will insert a /// line break. - pub fn normalize(&mut self, text: &mut Text) -> Result<()> { - let br_indexes = &mut text.br_indexes; - let mut row_count = br_indexes.row_count(); + pub fn normalize(&mut self, text: &Text) -> Result<()> { + let br_indexes = &text.br_indexes; + let row_count = br_indexes.row_count(); if self.row == row_count.get() { - br_indexes.insert_index(self.row, br_indexes.last_row_start()); - text.text.push('\n'); - row_count = row_count.saturating_add(1); + return Err(Error::oob_row(row_count, self.row)); } let row_start = br_indexes diff --git a/src/core/text.rs b/src/core/text.rs index f2106cc..c693b89 100644 --- a/src/core/text.rs +++ b/src/core/text.rs @@ -7,7 +7,6 @@ use std::{ ops::Range, }; - use super::{ encodings::{EncodingFns, UTF16, UTF32, UTF8}, eol_indexes::EolIndexes, @@ -888,6 +887,10 @@ mod tests { } mod replace { + use std::{borrow::Cow, num::NonZeroUsize}; + + use crate::error::Error; + use super::*; #[test] @@ -1107,6 +1110,18 @@ mod tests { Text::new("SomeText\nSome Other Text\nSome somsoemesome\n wowoas \n\n".into()); assert_eq!(t.br_indexes, [0, 8, 24, 42, 51, 52]); + let err = t + .replace( + "Hello, World!\nBye World!", + GridIndex { row: 0, col: 0 }, + GridIndex { row: 6, col: 0 }, + &mut (), + ) + .unwrap_err(); + assert_eq!(err, Error::oob_row(NonZeroUsize::new(6).unwrap(), 6)); + t.text.push('\n'); + t.br_indexes + .insert_index(6, t.br_indexes.last_row_start()); t.replace( "Hello, World!\nBye World!", GridIndex { row: 0, col: 0 }, @@ -1114,7 +1129,6 @@ mod tests { &mut (), ) .unwrap(); - assert_eq!(t.text, "Hello, World!\nBye World!"); assert_eq!(t.br_indexes, [0, 13]); } @@ -1149,14 +1163,16 @@ mod tests { assert_eq!(t.br_indexes, [0, 17]); // Second call - this previously panicked due to missing update_prep() - t.replace_full(Cow::Borrowed("Second replacement\nAnother line\nThird"), &mut ()) - .unwrap(); + t.replace_full( + Cow::Borrowed("Second replacement\nAnother line\nThird"), + &mut (), + ) + .unwrap(); assert_eq!(t.text, "Second replacement\nAnother line\nThird"); assert_eq!(t.br_indexes, [0, 18, 31]); // Third call to ensure stability - t.replace_full(Cow::Borrowed("Final"), &mut ()) - .unwrap(); + t.replace_full(Cow::Borrowed("Final"), &mut ()).unwrap(); assert_eq!(t.text, "Final"); assert_eq!(t.br_indexes, [0]); } diff --git a/src/error.rs b/src/error.rs index 010da85..9d08fe7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,6 +42,20 @@ impl Error { current, } } + + /// Check if the error is caused by a single missing newline + /// + /// Some LSP servers and clients may assume an extra newline exists at the end. + /// In the majority of these cases the correct fix is to append an empty line to the string. + /// This function returns [`true`] if the error can be fixed/ignored by appending a newline. + #[inline] + pub fn is_missing_newline(self) -> bool { + if let Self::OutOfBoundsRow { max, current } = self { + max + 1 == current + } else { + false + } + } } impl std::error::Error for Error {} From 3aa13a7e6169f4cfb94a18b2c5296d4eb38a697d Mon Sep 17 00:00:00 2001 From: Tayfun Bocek Date: Thu, 14 May 2026 00:20:47 +0300 Subject: [PATCH 2/2] refactor: reuse pure line logic in normalize and denormalize Co-Authored-By: Adrien Clauzel clauzeladrien@gmail.com --- src/change.rs | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/change.rs b/src/change.rs index 73c082e..3a18393 100644 --- a/src/change.rs +++ b/src/change.rs @@ -178,19 +178,7 @@ impl GridIndex { if self.row == row_count.get() { return Err(Error::oob_row(row_count, self.row)); } - - let row_start = br_indexes - .row_start(self.row) - .ok_or(Error::oob_row(row_count, self.row))?; - let pure_line = if !br_indexes.is_last_row(self.row) && row_count.get() > 1 { - let row_end = br_indexes - .row_start(self.row + 1) - .ok_or(Error::oob_row(row_count, self.row))?; - let base_line = &text.text[row_start..row_end]; - trim_eol_from_end(base_line) - } else { - &text.text[row_start..] - }; + let pure_line = resolve_pure_line(text, self.row)?; self.col = (text.encoding[0])(pure_line, self.col)?; @@ -199,20 +187,7 @@ impl GridIndex { /// Transform the positions to the [`Text`]'s expected encoding, from UTF-8 positions. pub fn denormalize(&mut self, text: &Text) -> Result<()> { - let br_indexes = &text.br_indexes; - let row_count = br_indexes.row_count(); - let row_start = br_indexes - .row_start(self.row) - .ok_or(Error::oob_row(row_count, self.row))?; - let pure_line = if !br_indexes.is_last_row(self.row) && row_count.get() > 1 { - let row_end = br_indexes - .row_start(self.row + 1) - .ok_or(Error::oob_row(row_count, self.row))?; - let base_line = &text.text[row_start..row_end]; - trim_eol_from_end(base_line) - } else { - &text.text[row_start..] - }; + let pure_line = resolve_pure_line(text, self.row)?; self.col = (text.encoding[1])(pure_line, self.col)?; @@ -227,3 +202,20 @@ pub(crate) fn correct_positions(start: &mut GridIndex, end: &mut GridIndex) { std::mem::swap(start, end); } } + +/// Resolve the line content for the given row, trimming EOL for non-last rows. +pub(crate) fn resolve_pure_line(text: &Text, row: usize) -> Result<&str> { + let br_indexes = &text.br_indexes; + let row_count = br_indexes.row_count(); + let row_start = br_indexes + .row_start(row) + .ok_or(Error::oob_row(row_count, row))?; + if !br_indexes.is_last_row(row) && row_count.get() > 1 { + let row_end = br_indexes + .row_start(row + 1) + .ok_or(Error::oob_row(row_count, row))?; + Ok(trim_eol_from_end(&text.text[row_start..row_end])) + } else { + Ok(&text.text[row_start..]) + } +}