[ty_test] Fix column units: use character offset not byte offset

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 <noreply@anthropic.com>
This commit is contained in:
Douglas Creager
2025-10-08 14:33:29 -04:00
parent 8c12fcb927
commit c574dff6b0
2 changed files with 14 additions and 11 deletions

View File

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

View File

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