From c574dff6b09c976960ec6ec3ef59138f12054c62 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Wed, 8 Oct 2025 14:33:29 -0400 Subject: [PATCH] [ty_test] Fix column units: use character offset not byte offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The column field was being treated inconsistently - calculated as a character offset but used as a byte offset. This would break on any line with multi-byte UTF-8 characters before the hover position. Fixes: 1. Use chars().position() instead of find() to get character offset 2. Use LineIndex::offset() with PositionEncoding::Utf32 to properly convert character offset to byte offset (TextSize) 3. Document that column is a UTF-32 character offset This ensures hover assertions work correctly with Unicode text. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- crates/ty_test/src/assertion.rs | 13 +++++++------ crates/ty_test/src/hover.rs | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/ty_test/src/assertion.rs b/crates/ty_test/src/assertion.rs index 7727594191..5164624513 100644 --- a/crates/ty_test/src/assertion.rs +++ b/crates/ty_test/src/assertion.rs @@ -377,7 +377,7 @@ impl std::fmt::Display for ErrorAssertion<'_> { /// A parsed and validated `# hover:` assertion comment. #[derive(Debug)] pub(crate) struct HoverAssertion<'a> { - /// The zero-based column in the line where the down arrow appears. + /// The zero-based character offset (UTF-32) in the line where the down arrow appears. /// This indicates the character position in the target line where we should hover. pub(crate) column: usize, @@ -397,17 +397,18 @@ impl<'a> HoverAssertion<'a> { return Err(HoverAssertionParseError::EmptyType); } - // Find the down arrow position within the comment text - let arrow_offset_in_comment = full_comment - .find('↓') + // Find the down arrow position within the comment text (as character offset) + let arrow_char_offset_in_comment = full_comment + .chars() + .position(|c| c == '↓') .ok_or(HoverAssertionParseError::MissingDownArrow)?; // Calculate the column within the comment's line // First, get the line and column of the comment's start let comment_line_col = line_index.line_column(comment_range.start(), source); - // The hover column is the comment's column plus the arrow offset within the comment - let column = comment_line_col.column.to_zero_indexed() + arrow_offset_in_comment; + // The hover column is the comment's column plus the arrow's character offset within the comment + let column = comment_line_col.column.to_zero_indexed() + arrow_char_offset_in_comment; Ok(Self { column, diff --git a/crates/ty_test/src/hover.rs b/crates/ty_test/src/hover.rs index 8ba71b39a0..68704488a1 100644 --- a/crates/ty_test/src/hover.rs +++ b/crates/ty_test/src/hover.rs @@ -8,6 +8,7 @@ use ruff_db::parsed::parsed_module; use ruff_db::source::{line_index, source_text}; use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, TraversalSignal}; use ruff_python_ast::AnyNodeRef; +use ruff_source_file::{OneIndexed, PositionEncoding, SourceLocation}; use ruff_text_size::{Ranged, TextSize}; use ty_python_semantic::{HasType, SemanticModel}; @@ -106,11 +107,12 @@ pub(crate) fn generate_hover_outputs( continue; }; - // Get the start offset of the target line - let target_line_start = lines.line_start(target_line, &source); - - // Calculate the hover position: start of target line + column - let hover_offset = target_line_start + TextSize::try_from(hover.column).unwrap(); + // Convert the character column to a byte offset using LineIndex::offset + let hover_location = SourceLocation { + line: target_line, + character_offset: OneIndexed::from_zero_indexed(hover.column), + }; + let hover_offset = lines.offset(hover_location, &source, PositionEncoding::Utf32); // Get the inferred type at that position let Some(inferred_type) = infer_type_at_position(db, file, hover_offset) else {