@@ -72,6 +72,26 @@ struct FilePos {
7272 col : usize ,
7373}
7474
75+ /// Describes how much of the editor view must be repainted.
76+ ///
77+ /// Order is important! Later entries in this enum mean that "more" of the viewport must
78+ /// be refreshed.
79+ #[ derive( Clone , Copy , Eq , Ord , PartialEq , PartialOrd ) ]
80+ enum RefreshKind {
81+ None ,
82+ CurrentLine ,
83+ Full ,
84+ }
85+
86+ impl RefreshKind {
87+ /// Upgrades `self` to at least `new_value`.
88+ fn upgrade ( & mut self , new_value : Self ) {
89+ if new_value > * self {
90+ * self = new_value;
91+ }
92+ }
93+ }
94+
7595/// An interactive console-based text editor.
7696///
7797/// The text editor owns the textual contents it is editing.
@@ -192,6 +212,38 @@ impl Editor {
192212 Ok ( ( ) )
193213 }
194214
215+ /// Refreshes only the line under the cursor, using the previously queried `console_size`.
216+ fn refresh_current_line (
217+ & self ,
218+ console : & mut dyn Console ,
219+ console_size : CharsXY ,
220+ ) -> io:: Result < ( ) > {
221+ debug_assert ! ( self . file_pos. line >= self . viewport_pos. line) ;
222+ debug_assert ! (
223+ self . file_pos. line < self . viewport_pos. line + usize :: from( console_size. y - 1 )
224+ ) ;
225+
226+ let row = self . file_pos . line - self . viewport_pos . line ;
227+ let row = if cfg ! ( debug_assertions) {
228+ u16:: try_from ( row) . expect ( "Computed y must have fit on screen" )
229+ } else {
230+ row as u16
231+ } ;
232+
233+ console. set_color ( TEXT_COLOR . 0 , TEXT_COLOR . 1 ) ?;
234+ console. locate ( CharsXY :: new ( 0 , row) ) ?;
235+ console. clear ( ClearType :: CurrentLine ) ?;
236+
237+ let line = & self . content [ self . file_pos . line ] ;
238+ if line. len ( ) > self . viewport_pos . col {
239+ console. write ( & line. range (
240+ self . viewport_pos . col ,
241+ self . viewport_pos . col + usize:: from ( console_size. x ) ,
242+ ) ) ?;
243+ }
244+ Ok ( ( ) )
245+ }
246+
195247 /// Moves the cursor down by the given number of lines in `nlines` or to the last line if there
196248 /// are insufficient lines to perform the move.
197249 fn move_down ( & mut self , nlines : usize ) {
@@ -226,44 +278,49 @@ impl Editor {
226278 self . content . push ( LineBuffer :: default ( ) ) ;
227279 }
228280
229- let mut need_refresh = true ;
281+ let mut refresh = RefreshKind :: Full ;
230282 loop {
231283 // The key handling below only deals with moving the insertion position within the file
232284 // but does not bother to update the viewport. Adjust it now, if necessary.
233285 let width = usize:: from ( console_size. x ) ;
234286 let height = usize:: from ( console_size. y ) ;
235287 if self . file_pos . line < self . viewport_pos . line {
236288 self . viewport_pos . line = self . file_pos . line ;
237- need_refresh = true ;
289+ refresh . upgrade ( RefreshKind :: Full ) ;
238290 } else if self . file_pos . line > self . viewport_pos . line + height - 2 {
239291 if self . file_pos . line > height - 2 {
240292 self . viewport_pos . line = self . file_pos . line - ( height - 2 ) ;
241293 } else {
242294 self . viewport_pos . line = 0 ;
243295 }
244- need_refresh = true ;
296+ refresh . upgrade ( RefreshKind :: Full ) ;
245297 }
246298
247299 if self . file_pos . col < self . viewport_pos . col {
248300 self . viewport_pos . col = self . file_pos . col ;
249- need_refresh = true ;
301+ refresh . upgrade ( RefreshKind :: Full ) ;
250302 } else if self . file_pos . col >= self . viewport_pos . col + width {
251303 self . viewport_pos . col = self . file_pos . col - width + 1 ;
252- need_refresh = true ;
304+ refresh . upgrade ( RefreshKind :: Full ) ;
253305 }
254306
255307 // TODO(jmmv): We must handle the cursor visibility outside of the non-sync block
256308 // because the current console implementation forces the cursor to be invisible
257309 // when syncing is disabled. This is suboptimal and should be fixed by decoupling
258310 // the two properties...
259311 console. hide_cursor ( ) ?;
260- if need_refresh {
261- self . refresh ( console, console_size) ?;
262- need_refresh = false ;
263- } else {
264- self . refresh_status ( console, console_size) ?;
265- console. set_color ( TEXT_COLOR . 0 , TEXT_COLOR . 1 ) ?;
312+ match refresh {
313+ RefreshKind :: Full => self . refresh ( console, console_size) ?,
314+ RefreshKind :: CurrentLine => {
315+ self . refresh_status ( console, console_size) ?;
316+ self . refresh_current_line ( console, console_size) ?;
317+ }
318+ RefreshKind :: None => {
319+ self . refresh_status ( console, console_size) ?;
320+ console. set_color ( TEXT_COLOR . 0 , TEXT_COLOR . 1 ) ?;
321+ }
266322 }
323+ refresh = RefreshKind :: None ;
267324 let cursor_pos = {
268325 let x = self . file_pos . col - self . viewport_pos . col ;
269326 let y = self . file_pos . line - self . viewport_pos . line ;
@@ -329,8 +386,7 @@ impl Editor {
329386 console. show_cursor ( ) ?;
330387 }
331388 } else {
332- // TODO(jmmv): Refresh only the affected line.
333- need_refresh = true ;
389+ refresh. upgrade ( RefreshKind :: CurrentLine ) ;
334390 }
335391 for _ in 0 ..nremove {
336392 line. remove ( self . file_pos . col - 1 ) ;
@@ -343,7 +399,7 @@ impl Editor {
343399 self . file_pos . col = self . content [ self . file_pos . line - 1 ] . len ( ) ;
344400 self . join_next_line_into ( self . file_pos . line - 1 ) ;
345401 self . file_pos . line -= 1 ;
346- need_refresh = true ;
402+ refresh . upgrade ( RefreshKind :: Full ) ;
347403 }
348404 self . insert_col = self . file_pos . col ;
349405 }
@@ -354,15 +410,15 @@ impl Editor {
354410 line. remove ( self . file_pos . col ) ;
355411 self . dirty = true ;
356412 if self . file_pos . col < line. len ( ) {
357- need_refresh = true ;
413+ refresh . upgrade ( RefreshKind :: Full ) ;
358414 } else {
359415 console. hide_cursor ( ) ?;
360416 console. clear ( ClearType :: UntilNewLine ) ?;
361417 console. show_cursor ( ) ?;
362418 }
363419 } else if self . file_pos . line + 1 < self . content . len ( ) {
364420 self . join_next_line_into ( self . file_pos . line ) ;
365- need_refresh = true ;
421+ refresh . upgrade ( RefreshKind :: Full ) ;
366422 }
367423 }
368424
@@ -371,15 +427,14 @@ impl Editor {
371427
372428 let line = & mut self . content [ self . file_pos . line ] ;
373429 if self . file_pos . col < line. len ( ) {
374- // TODO(jmmv): Refresh only the affected line.
375- need_refresh = true ;
430+ refresh. upgrade ( RefreshKind :: CurrentLine ) ;
376431 }
377432
378433 line. insert ( self . file_pos . col , ch) ;
379434 self . file_pos . col += 1 ;
380435 self . insert_col = self . file_pos . col ;
381436
382- if cursor_pos. x < console_size. x - 1 && !need_refresh {
437+ if cursor_pos. x < console_size. x - 1 && refresh == RefreshKind :: None {
383438 console. write ( ch. encode_utf8 ( & mut buf) ) ?;
384439 }
385440
@@ -413,7 +468,9 @@ impl Editor {
413468 self . file_pos . line + 1 ,
414469 LineBuffer :: from ( indent + & new. into_inner ( ) ) ,
415470 ) ;
416- need_refresh = !appending;
471+ if !appending {
472+ refresh. upgrade ( RefreshKind :: Full ) ;
473+ }
417474
418475 self . file_pos . col = indent_len;
419476 self . file_pos . line += 1 ;
@@ -428,8 +485,7 @@ impl Editor {
428485 Key :: Tab => {
429486 let line = & mut self . content [ self . file_pos . line ] ;
430487 if self . file_pos . col < line. len ( ) {
431- // TODO(jmmv): Refresh only the affected line.
432- need_refresh = true ;
488+ refresh. upgrade ( RefreshKind :: CurrentLine ) ;
433489 }
434490
435491 let new_pos = ( self . file_pos . col + INDENT_WIDTH ) / INDENT_WIDTH * INDENT_WIDTH ;
@@ -440,7 +496,7 @@ impl Editor {
440496 line. insert_str ( self . file_pos . col , & new_text) ;
441497 self . file_pos . col = new_pos;
442498 self . insert_col = self . file_pos . col ;
443- if !need_refresh {
499+ if refresh == RefreshKind :: None {
444500 console. write ( & new_text) ?;
445501 }
446502 self . dirty = true ;
@@ -588,6 +644,22 @@ mod tests {
588644 self
589645 }
590646
647+ /// Records the console changes needed to refresh only the current line and the status bar.
648+ fn refresh_line ( mut self , file_pos : FilePos , line : & str , cursor : CharsXY ) -> Self {
649+ self . output . push ( CapturedOut :: HideCursor ) ;
650+ self = self . refresh_status ( file_pos) ;
651+ self . output . push ( CapturedOut :: SetColor ( TEXT_COLOR . 0 , TEXT_COLOR . 1 ) ) ;
652+ self . output . push ( CapturedOut :: Locate ( yx ( cursor. y , 0 ) ) ) ;
653+ self . output . push ( CapturedOut :: Clear ( ClearType :: CurrentLine ) ) ;
654+ if !line. is_empty ( ) {
655+ self . output . push ( CapturedOut :: Write ( line. to_string ( ) ) ) ;
656+ }
657+ self . output . push ( CapturedOut :: Locate ( cursor) ) ;
658+ self . output . push ( CapturedOut :: ShowCursor ) ;
659+ self . output . push ( CapturedOut :: SyncNow ) ;
660+ self
661+ }
662+
591663 /// Records the console changes needed to refresh the whole console view. The status line
592664 /// is updated to reflect `file_pos`; the editor is pre-populated with the lines specified
593665 /// in `previous`; and the `cursor` is placed at the given location.
@@ -738,16 +810,16 @@ mod tests {
738810
739811 cb. add_input_chars ( "a" ) ;
740812 ob = ob. set_dirty ( ) ;
741- ob = ob. refresh ( linecol ( 0 , 1 ) , & [ "aprevious content" ] , yx ( 0 , 1 ) ) ;
813+ ob = ob. refresh_line ( linecol ( 0 , 1 ) , "aprevious content" , yx ( 0 , 1 ) ) ;
742814
743815 cb. add_input_chars ( "b" ) ;
744- ob = ob. refresh ( linecol ( 0 , 2 ) , & [ "abprevious content" ] , yx ( 0 , 2 ) ) ;
816+ ob = ob. refresh_line ( linecol ( 0 , 2 ) , "abprevious content" , yx ( 0 , 2 ) ) ;
745817
746818 cb. add_input_chars ( "c" ) ;
747- ob = ob. refresh ( linecol ( 0 , 3 ) , & [ "abcprevious content" ] , yx ( 0 , 3 ) ) ;
819+ ob = ob. refresh_line ( linecol ( 0 , 3 ) , "abcprevious content" , yx ( 0 , 3 ) ) ;
748820
749821 cb. add_input_chars ( " " ) ;
750- ob = ob. refresh ( linecol ( 0 , 4 ) , & [ "abc previous content" ] , yx ( 0 , 4 ) ) ;
822+ ob = ob. refresh_line ( linecol ( 0 , 4 ) , "abc previous content" , yx ( 0 , 4 ) ) ;
751823
752824 run_editor ( "previous content" , "abc previous content\n " , cb, ob) ;
753825 }
@@ -772,7 +844,7 @@ mod tests {
772844 ob = ob. quick_refresh ( linecol ( 0 , 2 ) , yx ( 0 , 2 ) ) ;
773845
774846 cb. add_input_chars ( "d" ) ;
775- ob = ob. refresh ( linecol ( 0 , 3 ) , & [ "abdc" ] , yx ( 0 , 3 ) ) ;
847+ ob = ob. refresh_line ( linecol ( 0 , 3 ) , "abdc" , yx ( 0 , 3 ) ) ;
776848
777849 run_editor ( "" , "abdc\n " , cb, ob) ;
778850 }
@@ -903,13 +975,13 @@ mod tests {
903975
904976 cb. add_input_chars ( "." ) ;
905977 ob = ob. set_dirty ( ) ;
906- ob = ob. refresh ( linecol ( 0 , 1 ) , & [ ".text" ] , yx ( 0 , 1 ) ) ;
978+ ob = ob. refresh_line ( linecol ( 0 , 1 ) , ".text" , yx ( 0 , 1 ) ) ;
907979
908980 cb. add_input_keys ( & [ Key :: Home ] ) ;
909981 ob = ob. quick_refresh ( linecol ( 0 , 0 ) , yx ( 0 , 0 ) ) ;
910982
911983 cb. add_input_chars ( "," ) ;
912- ob = ob. refresh ( linecol ( 0 , 1 ) , & [ ",.text" ] , yx ( 0 , 1 ) ) ;
984+ ob = ob. refresh_line ( linecol ( 0 , 1 ) , ",.text" , yx ( 0 , 1 ) ) ;
913985
914986 run_editor ( "text" , ",.text\n " , cb, ob) ;
915987 }
@@ -941,7 +1013,7 @@ mod tests {
9411013
9421014 cb. add_input_chars ( "." ) ;
9431015 ob = ob. set_dirty ( ) ;
944- ob = ob. refresh ( linecol ( 0 , 3 ) , & [ " .text" ] , yx ( 0 , 3 ) ) ;
1016+ ob = ob. refresh_line ( linecol ( 0 , 3 ) , " .text" , yx ( 0 , 3 ) ) ;
9451017
9461018 run_editor ( " text" , " .text\n " , cb, ob) ;
9471019 }
@@ -1025,10 +1097,10 @@ mod tests {
10251097
10261098 cb. add_input_keys ( & [ Key :: Tab ] ) ;
10271099 ob = ob. set_dirty ( ) ;
1028- ob = ob. refresh ( linecol ( 0 , 4 ) , & [ " ." ] , yx ( 0 , 4 ) ) ;
1100+ ob = ob. refresh_line ( linecol ( 0 , 4 ) , " ." , yx ( 0 , 4 ) ) ;
10291101
10301102 cb. add_input_keys ( & [ Key :: Tab ] ) ;
1031- ob = ob. refresh ( linecol ( 0 , 8 ) , & [ " ." ] , yx ( 0 , 8 ) ) ;
1103+ ob = ob. refresh_line ( linecol ( 0 , 8 ) , " ." , yx ( 0 , 8 ) ) ;
10321104
10331105 run_editor ( "." , " .\n " , cb, ob) ;
10341106 }
@@ -1089,13 +1161,13 @@ mod tests {
10891161
10901162 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
10911163 ob = ob. set_dirty ( ) ;
1092- ob = ob. refresh ( linecol ( 0 , 8 ) , & [ " aligned" ] , yx ( 0 , 8 ) ) ;
1164+ ob = ob. refresh_line ( linecol ( 0 , 8 ) , " aligned" , yx ( 0 , 8 ) ) ;
10931165
10941166 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
1095- ob = ob. refresh ( linecol ( 0 , 4 ) , & [ " aligned" ] , yx ( 0 , 4 ) ) ;
1167+ ob = ob. refresh_line ( linecol ( 0 , 4 ) , " aligned" , yx ( 0 , 4 ) ) ;
10961168
10971169 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
1098- ob = ob. refresh ( linecol ( 0 , 0 ) , & [ "aligned" ] , yx ( 0 , 0 ) ) ;
1170+ ob = ob. refresh_line ( linecol ( 0 , 0 ) , "aligned" , yx ( 0 , 0 ) ) ;
10991171
11001172 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
11011173 ob = ob. quick_refresh ( linecol ( 0 , 0 ) , yx ( 0 , 0 ) ) ;
@@ -1226,7 +1298,7 @@ mod tests {
12261298
12271299 cb. add_input_keys ( & [ Key :: Char ( 'X' ) ] ) ;
12281300 ob = ob. set_dirty ( ) ;
1229- ob = ob. refresh ( linecol ( 2 , 5 ) , & [ "longer" , "a" , " longXer", "b" ] , yx ( 2 , 5 ) ) ;
1301+ ob = ob. refresh_line ( linecol ( 2 , 5 ) , " longXer", yx ( 2 , 5 ) ) ;
12301302
12311303 cb. add_input_keys ( & [ Key :: ArrowDown ] ) ;
12321304 ob = ob. quick_refresh ( linecol ( 3 , 1 ) , yx ( 3 , 1 ) ) ;
@@ -1503,37 +1575,17 @@ mod tests {
15031575 ob = ob. quick_refresh ( linecol ( 1 , * file_col) , yx ( 1 , * cursor_col) ) ;
15041576 }
15051577 cb. add_input_keys ( & [ Key :: Char ( 'D' ) ] ) ;
1506- ob = ob. refresh (
1507- linecol ( 1 , 40 ) ,
1508- & [ "" , "456789012345678901234567890123456789DABC" , "" ] ,
1509- yx ( 1 , 37 ) ,
1510- ) ;
1578+ ob = ob. refresh_line ( linecol ( 1 , 40 ) , "456789012345678901234567890123456789DABC" , yx ( 1 , 37 ) ) ;
15111579 cb. add_input_keys ( & [ Key :: Char ( 'E' ) ] ) ;
1512- ob = ob. refresh (
1513- linecol ( 1 , 41 ) ,
1514- & [ "" , "456789012345678901234567890123456789DEAB" , "" ] ,
1515- yx ( 1 , 38 ) ,
1516- ) ;
1580+ ob = ob. refresh_line ( linecol ( 1 , 41 ) , "456789012345678901234567890123456789DEAB" , yx ( 1 , 38 ) ) ;
15171581
15181582 // Delete a few characters to restore the overflow part of the insertion line.
15191583 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
1520- ob = ob. refresh (
1521- linecol ( 1 , 40 ) ,
1522- & [ "" , "456789012345678901234567890123456789DABC" , "" ] ,
1523- yx ( 1 , 37 ) ,
1524- ) ;
1584+ ob = ob. refresh_line ( linecol ( 1 , 40 ) , "456789012345678901234567890123456789DABC" , yx ( 1 , 37 ) ) ;
15251585 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
1526- ob = ob. refresh (
1527- linecol ( 1 , 39 ) ,
1528- & [ "" , "456789012345678901234567890123456789ABC" , "" ] ,
1529- yx ( 1 , 36 ) ,
1530- ) ;
1586+ ob = ob. refresh_line ( linecol ( 1 , 39 ) , "456789012345678901234567890123456789ABC" , yx ( 1 , 36 ) ) ;
15311587 cb. add_input_keys ( & [ Key :: Backspace ] ) ;
1532- ob = ob. refresh (
1533- linecol ( 1 , 38 ) ,
1534- & [ "" , "45678901234567890123456789012345678ABC" , "" ] ,
1535- yx ( 1 , 35 ) ,
1536- ) ;
1588+ ob = ob. refresh_line ( linecol ( 1 , 38 ) , "45678901234567890123456789012345678ABC" , yx ( 1 , 35 ) ) ;
15371589
15381590 // Move back to the beginning of the line to see surrounding lines reappear.
15391591 for col in 0u16 ..35u16 {
0 commit comments