docs(comments): trim paste-fix commentary to the crucial constraints

This commit is contained in:
2026-07-24 13:09:35 -04:00
parent 7380576e50
commit 867b3e110b
2 changed files with 11 additions and 28 deletions
@@ -148,7 +148,6 @@ mod linux {
} }
let err = std::io::Error::last_os_error(); let err = std::io::Error::last_os_error();
match err.kind() { match err.kind() {
// The fd is non-blocking: the queue is empty.
std::io::ErrorKind::WouldBlock => break, std::io::ErrorKind::WouldBlock => break,
std::io::ErrorKind::Interrupted => continue, std::io::ErrorKind::Interrupted => continue,
_ => return Err(err), _ => return Err(err),
+11 -27
View File
@@ -2333,11 +2333,7 @@ async fn drain_and_process(
} }
// On terminals without bracketed paste, try to capture more events // On terminals without bracketed paste, try to capture more events
// that may still be in transit from the input reader thread. A batch // that may still be in transit from the input reader thread.
// that already holds a key burst skips the short detection window:
// its 2 ms budget is below one Windows scheduler quantum, so a
// mid-paste gap would end collection before it starts and the paste
// tail would arrive as a separate fragment.
if should_extend_for_paste(&raw_events) if should_extend_for_paste(&raw_events)
&& (is_paste_burst(&raw_events) || detect_paste(&mut raw_events, input_rx).await) && (is_paste_burst(&raw_events) || detect_paste(&mut raw_events, input_rx).await)
{ {
@@ -2542,18 +2538,13 @@ async fn drain_and_process(
const PASTE_DETECT_TIMEOUT: Duration = Duration::from_millis(2); const PASTE_DETECT_TIMEOUT: Duration = Duration::from_millis(2);
/// Timeout for subsequent rounds once paste has been detected. Must exceed /// Timeout for subsequent rounds once paste has been detected. Must exceed
/// one Windows scheduler quantum (~15.6 ms): ConPTY delivers a paste as a /// one Windows scheduler quantum (~15.6 ms), or a routine mid-burst gap
/// per-character key burst, and a mid-burst gap of one quantum is routine. /// splits the paste and repaste-to-expand duplicates instead of expanding.
/// A smaller window ends collection mid-paste; the tail then arrives as a
/// second synthetic paste, whose content can never match the paste chip, so
/// repaste-to-expand inserts a duplicate instead of expanding.
const PASTE_CONTINUE_TIMEOUT: Duration = Duration::from_millis(25); const PASTE_CONTINUE_TIMEOUT: Duration = Duration::from_millis(25);
/// Safety cap on events accumulated in one extension pass. On the /// Safety cap on events accumulated in one extension pass. Pastes arrive
/// no-bracketed-paste path a paste is one key event per character, so the /// one key event per character here, so the cap must exceed any real paste;
/// cap must exceed any real paste (~200 KB of text); hitting it splits the /// hitting it splits the paste like a timeout miss would.
/// paste with the same duplicate-on-repaste effect as a timeout split. The
/// idle timeout above, not this cap, is the normal terminator.
const PASTE_EXTEND_MAX_EVENTS: usize = 200_000; const PASTE_EXTEND_MAX_EVENTS: usize = 200_000;
/// Returns `true` when the batch contains pasteable key events but no /// Returns `true` when the batch contains pasteable key events but no
@@ -2564,12 +2555,11 @@ fn should_extend_for_paste(events: &[Event]) -> bool {
} }
/// Minimum pasteable key events already in one batch to classify it as a /// Minimum pasteable key events already in one batch to classify it as a
/// paste burst in progress. Human typing and key auto-repeat deliver one /// paste burst; typing and auto-repeat deliver only a couple per batch.
/// or two events per batch; only a paste chunk lands more at once.
const PASTE_BURST_BATCH_EVENTS: usize = 8; const PASTE_BURST_BATCH_EVENTS: usize = 8;
/// True when the batch alone proves a paste is in progress, without /// True when the batch alone proves a paste is in progress, so collection
/// waiting on [`PASTE_DETECT_TIMEOUT`]. /// can start without the [`PASTE_DETECT_TIMEOUT`] round-trip.
fn is_paste_burst(events: &[Event]) -> bool { fn is_paste_burst(events: &[Event]) -> bool {
events.iter().filter(|e| is_pasteable_key_event(e)).count() >= PASTE_BURST_BATCH_EVENTS events.iter().filter(|e| is_pasteable_key_event(e)).count() >= PASTE_BURST_BATCH_EVENTS
} }
@@ -3188,11 +3178,9 @@ mod tests {
#[test] #[test]
fn typing_and_release_storms_are_not_a_paste_burst() { fn typing_and_release_storms_are_not_a_paste_burst() {
// A short typed run stays below the burst threshold.
let typed = vec![press(KeyCode::Char('h')), press(KeyCode::Char('i'))]; let typed = vec![press(KeyCode::Char('h')), press(KeyCode::Char('i'))];
assert!(!is_paste_burst(&typed)); assert!(!is_paste_burst(&typed));
// Release events carry no content and must not count toward it.
let releases: Vec<Event> = "releases only!" let releases: Vec<Event> = "releases only!"
.chars() .chars()
.map(|c| release(KeyCode::Char(c))) .map(|c| release(KeyCode::Char(c)))
@@ -3200,10 +3188,8 @@ mod tests {
assert!(!is_paste_burst(&releases)); assert!(!is_paste_burst(&releases));
} }
/// A key-burst paste whose tail trickles in with sub-window gaps must /// A burst tail trickling in with one-quantum gaps must reassemble into
/// reassemble into ONE synthetic paste. A split here is what made /// ONE paste — a split is what duplicated chips on Windows.
/// repaste-to-expand insert a duplicate chip on Windows: the fragment
/// could never byte-equal the chip content.
#[tokio::test(start_paused = true)] #[tokio::test(start_paused = true)]
async fn burst_tail_with_scheduler_gaps_reassembles_into_one_paste() { async fn burst_tail_with_scheduler_gaps_reassembles_into_one_paste() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
@@ -3213,8 +3199,6 @@ mod tests {
let mut batch: Vec<Event> = head.chars().map(char_or_enter).collect(); let mut batch: Vec<Event> = head.chars().map(char_or_enter).collect();
assert!(is_paste_burst(&batch), "head chunk must classify as burst"); assert!(is_paste_burst(&batch), "head chunk must classify as burst");
// Trickle the tail with 15 ms gaps — one Windows scheduler quantum,
// larger than the old 10 ms window, smaller than the current one.
tokio::spawn(async move { tokio::spawn(async move {
for c in tail.chars() { for c in tail.chars() {
tokio::time::sleep(Duration::from_millis(15)).await; tokio::time::sleep(Duration::from_millis(15)).await;