Skip to content

Commit 00ad06b

Browse files
committed
Refresh only the edited editor line
Address various TODOs in the editor to make refreshes more optimal. Instead of always refreshing the whole viewport, try to refresh the current line wherever possible.
1 parent 74067e4 commit 00ad06b

1 file changed

Lines changed: 114 additions & 62 deletions

File tree

repl/src/editor.rs

Lines changed: 114 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)