Skip to content

Commit 68336c5

Browse files
committed
fix: handle utf16 panics #820
1 parent 6477cf7 commit 68336c5

File tree

2 files changed

+83
-8
lines changed

2 files changed

+83
-8
lines changed

crates/lsp/src/providers/text_document.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,16 +433,16 @@ pub(crate) fn did_change(
433433
// Convert UTF-16 column offsets to character offsets within the line
434434
// CRITICAL: range.start.character is UTF-16 offset *within the line*, not document-wide
435435
let start_line_utf16_cu = doc.content.char_to_utf16_cu(start_row_char_idx);
436-
let start_col_char_idx = doc
437-
.content
438-
.utf16_cu_to_char(start_line_utf16_cu + range.start.character as usize)
439-
- start_row_char_idx;
436+
let start_utf16_idx = start_line_utf16_cu + range.start.character as usize;
437+
// Clamp to document bounds to prevent panic if client sends invalid positions
438+
let start_utf16_idx = start_utf16_idx.min(doc.content.len_utf16_cu());
439+
let start_col_char_idx = doc.content.utf16_cu_to_char(start_utf16_idx) - start_row_char_idx;
440440

441441
let end_line_utf16_cu = doc.content.char_to_utf16_cu(end_row_char_idx);
442-
let end_col_char_idx = doc
443-
.content
444-
.utf16_cu_to_char(end_line_utf16_cu + range.end.character as usize)
445-
- end_row_char_idx;
442+
let end_utf16_idx = end_line_utf16_cu + range.end.character as usize;
443+
// Clamp to document bounds to prevent panic if client sends invalid positions
444+
let end_utf16_idx = end_utf16_idx.min(doc.content.len_utf16_cu());
445+
let end_col_char_idx = doc.content.utf16_cu_to_char(end_utf16_idx) - end_row_char_idx;
446446

447447
let start_char_idx = start_row_char_idx + start_col_char_idx;
448448
let end_char_idx = end_row_char_idx + end_col_char_idx;
@@ -904,4 +904,30 @@ mod tests {
904904
result
905905
);
906906
}
907+
908+
#[test]
909+
fn test_out_of_bounds_utf16_position() {
910+
// Test that out-of-bounds UTF-16 positions are clamped instead of panicking
911+
// This reproduces issue #820 where neovim sends positions beyond document bounds
912+
let content = "2023-01-01 * \"Test\"\n";
913+
let doc = create_test_document(content);
914+
let total_utf16_len = doc.content.len_utf16_cu();
915+
916+
// Simulate a change with end position beyond document bounds
917+
let start_row_char_idx = doc.content.line_to_char(0);
918+
let start_line_utf16_cu = doc.content.char_to_utf16_cu(start_row_char_idx);
919+
920+
// This should not panic - it should clamp to document end
921+
let out_of_bounds_utf16 = total_utf16_len + 100;
922+
let clamped_utf16 = out_of_bounds_utf16.min(doc.content.len_utf16_cu());
923+
let result = doc
924+
.content
925+
.utf16_cu_to_char(start_line_utf16_cu + clamped_utf16);
926+
927+
// Should succeed and clamp to valid position
928+
assert!(
929+
result <= doc.content.len_chars(),
930+
"Should clamp to valid char position"
931+
);
932+
}
907933
}

crates/lsp/src/treesitter_utils.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ fn lsp_position_to_core(
117117
let row_char_idx = source.line_to_char(row_idx);
118118
let row_utf16_cu_idx = source.char_to_utf16_cu(row_char_idx);
119119
let abs_utf16_cu_idx = row_utf16_cu_idx + col_utf16_cu_idx;
120+
// Clamp to document bounds to prevent panic if client sends invalid positions
121+
let abs_utf16_cu_idx = abs_utf16_cu_idx.min(source.len_utf16_cu());
120122

121123
// Convert absolute UTF-16 index -> absolute char index -> absolute byte index.
122124
let abs_char_idx = source.utf16_cu_to_char(abs_utf16_cu_idx);
@@ -397,4 +399,51 @@ mod tests {
397399
let text = text_for_tree_sitter_node(&source, &root);
398400
assert_eq!(text, "2024-01-01 * \"Coffee ☕\"");
399401
}
402+
403+
#[test]
404+
fn test_lsp_position_out_of_bounds() {
405+
// Test that out-of-bounds UTF-16 positions are clamped instead of panicking
406+
// This reproduces issue #820
407+
let source = Rope::from("2024-01-01 * \"Test\"\n");
408+
let total_utf16_len = source.len_utf16_cu();
409+
410+
// Position beyond document bounds - should be clamped
411+
let pos = Position::new(0, (total_utf16_len + 100) as u32);
412+
let result = lsp_position_to_core(&source, pos);
413+
414+
// Should not panic, should clamp to document end
415+
assert!(
416+
result.is_ok(),
417+
"Should handle out-of-bounds position gracefully"
418+
);
419+
let core_pos = result.unwrap();
420+
assert_eq!(
421+
core_pos.char as usize,
422+
source.len_chars(),
423+
"Should clamp to document end"
424+
);
425+
}
426+
427+
#[test]
428+
fn test_lsp_textdocchange_out_of_bounds_range() {
429+
// Test that text changes with out-of-bounds ranges don't panic
430+
let source = Rope::from("Short text");
431+
let total_utf16_len = source.len_utf16_cu();
432+
433+
// Change with end position beyond document - should be clamped
434+
let change = TextDocumentContentChangeEvent {
435+
range: Some(Range {
436+
start: Position::new(0, 0),
437+
end: Position::new(0, (total_utf16_len + 50) as u32),
438+
}),
439+
range_length: None,
440+
text: "Replacement".to_string(),
441+
};
442+
443+
let result = lsp_textdocchange_to_ts_inputedit(&source, &change);
444+
assert!(
445+
result.is_ok(),
446+
"Should handle out-of-bounds range gracefully"
447+
);
448+
}
400449
}

0 commit comments

Comments
 (0)