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