fix(vterm): harden terminal cell and input invariants
Reject C0/C1 controls before terminal text reaches screen cells, and preserve the released button code in SGR mouse reports. Remove dead screen branches, keep logical-line allocation saturating, and clear round-trip input state when pruning externally removed terminal buffers.
This commit is contained in:
parent
5d5055871f
commit
9797adaa0b
|
|
@ -529,8 +529,8 @@ impl EditorState {
|
|||
supervisor.tick();
|
||||
let mut manager = self.terminal_manager.borrow_mut();
|
||||
manager.tick(&mut supervisor);
|
||||
let core = self.core.borrow();
|
||||
manager.prune(&core, &mut supervisor);
|
||||
let mut core = self.core.borrow_mut();
|
||||
manager.prune(&mut core, &mut supervisor);
|
||||
}
|
||||
self.lua_host
|
||||
.run_hook("process.after-tick", mlua::MultiValue::new());
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ pub fn encode_mouse(
|
|||
}
|
||||
let (mut code, release) = match kind {
|
||||
MouseKind::Down(button) => (button_code(button), false),
|
||||
MouseKind::Up(_) => (3, true),
|
||||
MouseKind::Up(button) => (button_code(button), true),
|
||||
MouseKind::Drag(button) => (button_code(button) + 32, false),
|
||||
MouseKind::Move => (35, false),
|
||||
MouseKind::ScrollUp => (64, false),
|
||||
|
|
@ -317,12 +317,12 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
encode_mouse(
|
||||
MouseKind::Up(MouseButton::Left),
|
||||
MouseKind::Up(MouseButton::Right),
|
||||
CellCoord::new(4, 9),
|
||||
Modifiers::NONE,
|
||||
m
|
||||
),
|
||||
Some(b"\x1b[<3;10;5m".to_vec())
|
||||
Some(b"\x1b[<2;10;5m".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
encode_mouse(
|
||||
|
|
|
|||
|
|
@ -234,12 +234,12 @@ impl TerminalScreen {
|
|||
None
|
||||
}
|
||||
AnsiEvent::LineFeed | AnsiEvent::Index => {
|
||||
self.line_feed(false);
|
||||
self.line_feed();
|
||||
None
|
||||
}
|
||||
AnsiEvent::NextLine => {
|
||||
self.cursor.col = 0;
|
||||
self.line_feed(false);
|
||||
self.line_feed();
|
||||
None
|
||||
}
|
||||
AnsiEvent::ReverseIndex => {
|
||||
|
|
@ -455,7 +455,7 @@ impl TerminalScreen {
|
|||
if needs_newline {
|
||||
self.cursor.pending_wrap = false;
|
||||
self.cursor.col = 0;
|
||||
self.line_feed(false);
|
||||
self.line_feed();
|
||||
}
|
||||
self.write_text(annotation);
|
||||
self.cursor.pending_wrap = false;
|
||||
|
|
@ -561,6 +561,9 @@ impl TerminalScreen {
|
|||
fn write_text(&mut self, text: &str) {
|
||||
for ch in text.chars() {
|
||||
let ch = self.map_character(ch);
|
||||
if ch.is_control() {
|
||||
continue;
|
||||
}
|
||||
if self.try_extend_previous_grapheme(ch) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -624,9 +627,6 @@ impl TerminalScreen {
|
|||
}
|
||||
if width == 2 && lead + 1 >= cols {
|
||||
self.active_mut().rows[row].cells[lead] = blank(self.style);
|
||||
if old_width == 2 && lead + 1 < cols {
|
||||
self.active_mut().rows[row].cells[lead + 1] = blank(self.style);
|
||||
}
|
||||
self.cursor.row = row;
|
||||
self.cursor.col = cols - 1;
|
||||
self.cursor.pending_wrap = true;
|
||||
|
|
@ -751,13 +751,11 @@ impl TerminalScreen {
|
|||
self.changed();
|
||||
}
|
||||
|
||||
fn line_feed(&mut self, soft: bool) {
|
||||
fn line_feed(&mut self) {
|
||||
self.cursor.pending_wrap = false;
|
||||
let row = self.cursor.row;
|
||||
if !soft {
|
||||
self.active_mut().rows[row].soft_wrapped = false;
|
||||
self.break_chain_after(row);
|
||||
}
|
||||
self.active_mut().rows[row].soft_wrapped = false;
|
||||
self.break_chain_after(row);
|
||||
if row == self.scroll_bottom {
|
||||
self.scroll_up_internal(1, None);
|
||||
} else if row + 1 < self.size.rows as usize {
|
||||
|
|
@ -1330,7 +1328,7 @@ impl TerminalScreen {
|
|||
}
|
||||
while rows.len() < size.rows as usize {
|
||||
let id = self.next_line_id;
|
||||
self.next_line_id += 1;
|
||||
self.next_line_id = self.next_line_id.saturating_add(1);
|
||||
rows.push(TerminalRow::new(new_cols, id, self.style));
|
||||
}
|
||||
let split = rows.len().saturating_sub(size.rows as usize);
|
||||
|
|
@ -1396,7 +1394,7 @@ impl Grid {
|
|||
*next_id,
|
||||
Style::default(),
|
||||
));
|
||||
*next_id += 1;
|
||||
*next_id = next_id.saturating_add(1);
|
||||
}
|
||||
Self {
|
||||
rows,
|
||||
|
|
@ -1616,6 +1614,14 @@ mod tests {
|
|||
assert!(matches!(s.snapshot().cells[1].glyph, Glyph::Char('x')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_characters_never_enter_cells() {
|
||||
let mut s = screen(2, 4);
|
||||
let before = s.snapshot();
|
||||
s.apply_event(AnsiEvent::Text("\u{9b}\n\0".into()));
|
||||
assert_eq!(s.snapshot(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternate_screen_preserves_main_and_has_no_history() {
|
||||
let mut s = screen(2, 4);
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ impl TerminalManager {
|
|||
}
|
||||
|
||||
/// Tear down sessions whose identity buffers were removed by any path.
|
||||
pub fn prune(&mut self, core: &EditorCore, supervisor: &mut ProcessSupervisor) {
|
||||
pub fn prune(&mut self, core: &mut EditorCore, supervisor: &mut ProcessSupervisor) {
|
||||
let removed: Vec<BufferId> = {
|
||||
let registry = core.registry.borrow();
|
||||
self.sessions
|
||||
|
|
@ -483,6 +483,7 @@ impl TerminalManager {
|
|||
.collect()
|
||||
};
|
||||
for buffer_id in removed {
|
||||
core.set_round_trip_input(buffer_id, false);
|
||||
let Some(session) = self.sessions.remove(&buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use pmacs::ansi::AnsiEvent;
|
||||
use pmacs::buffer::{Buffer, BufferError, BufferId, EditOp};
|
||||
use pmacs::cell::Glyph;
|
||||
use pmacs::cell::{CellSize, Glyph};
|
||||
use pmacs::editor::EditorState;
|
||||
use pmacs::process::ProcessState;
|
||||
use pmacs::rope::Range;
|
||||
use pmacs::terminal::screen::TerminalScreen;
|
||||
use pmacs::terminal::{TerminalProcessState, TerminalSpec};
|
||||
|
||||
fn rope_bytes(buffer: &Buffer) -> Vec<u8> {
|
||||
|
|
@ -48,6 +50,14 @@ fn tick_until(
|
|||
panic!("terminal condition did not settle before {timeout:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_cells_reject_child_control_characters() {
|
||||
let mut screen = TerminalScreen::new(CellSize::new(2, 4), 0).expect("valid screen");
|
||||
let before = screen.snapshot();
|
||||
screen.apply_event(AnsiEvent::Text("\u{9b}\n\0".into()));
|
||||
assert_eq!(screen.snapshot(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_failure_is_transactional() {
|
||||
let mut state = EditorState::new();
|
||||
|
|
|
|||
Loading…
Reference in New Issue