@@ -41,12 +41,13 @@ impl Widget for State {
4141 . config
4242 . lines
4343 . map_or ( height as usize , |lines| lines. min ( height as usize ) ) ;
44- let rows = self . document . extract_rows_from_current ( height) ;
45- let line_numbers = self
46- . config
47- . show_line_numbers
48- . then ( || self . document . line_numbers_from_current ( height) ) ;
49- self . create_graphemes_from_rows ( & rows, 0 , line_numbers)
44+ let ( viewport_start, active_row) = self . document . project_viewport ( height) ;
45+ let rows = self . document . extract_rows_from ( viewport_start, height) ;
46+ let line_numbers = self . config . show_line_numbers . then ( || {
47+ self . document
48+ . line_numbers_from_viewport ( viewport_start, height)
49+ } ) ;
50+ self . create_graphemes_from_rows ( & rows, active_row, line_numbers)
5051 }
5152}
5253
@@ -97,7 +98,7 @@ impl State {
9798 /// Interprets a content position in a viewport projection as a semantic target.
9899 pub fn hit_at_viewport ( & self , position : ContentPosition ) -> Option < JsonHit > {
99100 self . document
100- . row_index_at_visible_offset_from_current ( position. row )
101+ . row_index_at_viewport_position ( position. row )
101102 . map ( |row_index| JsonHit :: Toggle { row_index } )
102103 }
103104}
@@ -113,7 +114,7 @@ mod tests {
113114 use super :: * ;
114115
115116 #[ test]
116- fn viewport_projection_is_bounded_and_resolves_hits_from_cursor ( ) {
117+ fn viewport_projection_is_bounded_and_resolves_hits ( ) {
117118 let value = serde_json:: json!( {
118119 "first" : "one" ,
119120 "second" : {
@@ -126,6 +127,9 @@ mod tests {
126127 config : Config :: default ( ) ,
127128 } ;
128129
130+ state. create_graphemes_in_viewport ( 80 , 2 ) ;
131+ state. document . down ( ) ;
132+ state. create_graphemes_in_viewport ( 80 , 2 ) ;
129133 state. document . down ( ) ;
130134 let projected = state. create_graphemes_in_viewport ( 80 , 2 ) ;
131135 let rendered = projected. graphemes . to_string ( ) ;
@@ -134,7 +138,7 @@ mod tests {
134138 assert ! ( rendered. contains( "\" second\" : {" ) ) ;
135139 assert ! ( !rendered. contains( "\" nested\" : \" two\" " ) ) ;
136140 assert ! ( !rendered. contains( "\" third\" : \" three\" " ) ) ;
137- assert_eq ! ( projected. cursor. unwrap( ) . row, 0 ) ;
141+ assert_eq ! ( projected. cursor. unwrap( ) . row, 1 ) ;
138142
139143 let JsonHit :: Toggle { row_index } = state
140144 . hit_at_viewport ( ContentPosition { row : 1 , column : 4 } )
@@ -145,6 +149,61 @@ mod tests {
145149 assert ! ( collapsed. graphemes. to_string( ) . contains( "\" second\" : {…}" ) ) ;
146150 }
147151
152+ #[ test]
153+ fn viewport_projection_stays_stable_until_cursor_leaves_it ( ) {
154+ let value = serde_json:: json!( {
155+ "first" : 1 ,
156+ "second" : 2 ,
157+ "third" : 3 ,
158+ "fourth" : 4
159+ } ) ;
160+ let mut state = State {
161+ document : Document :: new ( [ & value] ) ,
162+ config : Config :: default ( ) ,
163+ } ;
164+
165+ let initial = state. create_graphemes_in_viewport ( 80 , 3 ) ;
166+ assert ! ( initial. graphemes. to_string( ) . starts_with( "{" ) ) ;
167+ assert_eq ! ( initial. cursor. unwrap( ) . row, 0 ) ;
168+
169+ state. document . down ( ) ;
170+ let moved_inside = state. create_graphemes_in_viewport ( 80 , 3 ) ;
171+ assert ! ( moved_inside. graphemes. to_string( ) . starts_with( "{" ) ) ;
172+ assert_eq ! ( moved_inside. cursor. unwrap( ) . row, 1 ) ;
173+
174+ state. document . down ( ) ;
175+ let moved_to_edge = state. create_graphemes_in_viewport ( 80 , 3 ) ;
176+ assert ! ( moved_to_edge. graphemes. to_string( ) . starts_with( "{" ) ) ;
177+ assert_eq ! ( moved_to_edge. cursor. unwrap( ) . row, 2 ) ;
178+
179+ state. document . down ( ) ;
180+ let moved_outside = state. create_graphemes_in_viewport ( 80 , 3 ) ;
181+ assert ! (
182+ moved_outside
183+ . graphemes
184+ . to_string( )
185+ . starts_with( "\" first\" : 1" )
186+ ) ;
187+ assert_eq ! ( moved_outside. cursor. unwrap( ) . row, 2 ) ;
188+
189+ state. document . up ( ) ;
190+ let moved_back_inside = state. create_graphemes_in_viewport ( 80 , 3 ) ;
191+ assert ! (
192+ moved_back_inside
193+ . graphemes
194+ . to_string( )
195+ . starts_with( "\" first\" : 1" )
196+ ) ;
197+ assert_eq ! ( moved_back_inside. cursor. unwrap( ) . row, 1 ) ;
198+
199+ state. document . up ( ) ;
200+ state. create_graphemes_in_viewport ( 80 , 3 ) ;
201+ state. document . up ( ) ;
202+ let moved_above = state. create_graphemes_in_viewport ( 80 , 3 ) ;
203+ assert ! ( moved_above. graphemes. to_string( ) . starts_with( "{" ) ) ;
204+ assert_eq ! ( moved_above. cursor. unwrap( ) . row, 0 ) ;
205+ }
206+
148207 #[ test]
149208 fn configured_line_limit_bounds_viewport_projection ( ) {
150209 let value = serde_json:: json!( { "first" : 1 , "second" : 2 } ) ;
@@ -193,7 +252,7 @@ mod tests {
193252 }
194253
195254 #[ test]
196- fn viewport_projection_uses_stable_line_numbers_from_cursor ( ) {
255+ fn viewport_projection_uses_stable_line_numbers ( ) {
197256 let value = serde_json:: json!( { "first" : 1 , "second" : 2 , "third" : 3 } ) ;
198257 let mut state = State {
199258 document : Document :: new ( [ & value] ) ,
@@ -203,6 +262,8 @@ mod tests {
203262 } ,
204263 } ;
205264
265+ state. create_graphemes_in_viewport ( 80 , 2 ) ;
266+ state. document . down ( ) ;
206267 state. document . down ( ) ;
207268 state. document . down ( ) ;
208269
0 commit comments