refactor(tui): extract paste-burst handlers into tui/paste.rs (P1.2)
Lifts `handle_paste_burst_key`, `handle_paste_burst_decision`, `apply_paste_burst_retro_capture`, and the local `in_command_context` helper out of `tui/ui.rs` into a sibling module. The state machine (`PasteBurst`) and its tests stay in `paste_burst.rs`; only the keymap- side wiring moves. Drops the now-unused `CharDecision` import from `ui.rs`. Workspace tests: 1011/1011 still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod markdown_render;
|
||||
pub mod model_picker;
|
||||
pub mod onboarding;
|
||||
pub mod pager;
|
||||
pub mod paste;
|
||||
pub mod paste_burst;
|
||||
pub mod plan_prompt;
|
||||
pub mod scrolling;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Paste-burst handling — turn rapid keystrokes (terminals without bracketed
|
||||
//! paste) into a single committed buffer instead of N individual chars.
|
||||
//!
|
||||
//! Extracted from `tui/ui.rs` (P1.2). The owning state machine lives on
|
||||
//! `App.paste_burst` (`tui::paste_burst`); these helpers wire it to the key
|
||||
//! event loop and the composer's text buffer.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use super::app::App;
|
||||
use super::paste_burst::CharDecision;
|
||||
|
||||
/// Process a key in the context of paste-burst detection. Returns `true`
|
||||
/// when the key was fully handled by the paste machinery (caller skips
|
||||
/// further input handling); `false` when the key still needs the normal
|
||||
/// composer path.
|
||||
pub fn handle_paste_burst_key(app: &mut App, key: &KeyEvent, now: Instant) -> bool {
|
||||
let has_ctrl_alt_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
|| key.modifiers.contains(KeyModifiers::ALT)
|
||||
|| key.modifiers.contains(KeyModifiers::SUPER);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if !in_command_context(app) && app.paste_burst.append_newline_if_active(now) {
|
||||
return true;
|
||||
}
|
||||
if !in_command_context(app)
|
||||
&& app.paste_burst.newline_should_insert_instead_of_submit(now)
|
||||
{
|
||||
app.insert_char('\n');
|
||||
app.paste_burst.extend_window(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if !has_ctrl_alt_or_super => {
|
||||
if !c.is_ascii() {
|
||||
if let Some(pending) = app.paste_burst.flush_before_modified_input() {
|
||||
app.insert_str(&pending);
|
||||
}
|
||||
if app.paste_burst.try_append_char_if_active(c, now) {
|
||||
return true;
|
||||
}
|
||||
if let Some(decision) = app.paste_burst.on_plain_char_no_hold(now) {
|
||||
return handle_paste_burst_decision(app, decision, c, now);
|
||||
}
|
||||
app.insert_char(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
let decision = app.paste_burst.on_plain_char(c, now);
|
||||
return handle_paste_burst_decision(app, decision, c, now);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Apply a paste-burst decision to the composer buffer. Some decisions
|
||||
/// retroactively grab the last few chars from the input back into the
|
||||
/// pending paste buffer (when the heuristic decides the recent typing was
|
||||
/// actually a paste).
|
||||
pub fn handle_paste_burst_decision(
|
||||
app: &mut App,
|
||||
decision: CharDecision,
|
||||
c: char,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
match decision {
|
||||
CharDecision::RetainFirstChar => true,
|
||||
CharDecision::BeginBufferFromPending | CharDecision::BufferAppend => {
|
||||
app.paste_burst.append_char_to_buffer(c, now);
|
||||
true
|
||||
}
|
||||
CharDecision::BeginBuffer { retro_chars } => {
|
||||
if apply_paste_burst_retro_capture(app, retro_chars as usize, c, now) {
|
||||
return true;
|
||||
}
|
||||
app.insert_char(c);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_paste_burst_retro_capture(
|
||||
app: &mut App,
|
||||
retro_chars: usize,
|
||||
c: char,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
let cursor_byte = app.cursor_byte_index();
|
||||
let before = &app.input[..cursor_byte];
|
||||
let Some(grab) = app
|
||||
.paste_burst
|
||||
.decide_begin_buffer(now, before, retro_chars)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !grab.grabbed.is_empty() {
|
||||
app.input.replace_range(grab.start_byte..cursor_byte, "");
|
||||
let removed = grab.grabbed.chars().count();
|
||||
app.cursor_position = app.cursor_position.saturating_sub(removed);
|
||||
}
|
||||
app.paste_burst.append_char_to_buffer(c, now);
|
||||
true
|
||||
}
|
||||
|
||||
fn in_command_context(app: &App) -> bool {
|
||||
app.input.starts_with('/')
|
||||
}
|
||||
@@ -55,7 +55,6 @@ use crate::tui::command_palette::{
|
||||
use crate::tui::event_broker::EventBroker;
|
||||
use crate::tui::onboarding;
|
||||
use crate::tui::pager::PagerView;
|
||||
use crate::tui::paste_burst::CharDecision;
|
||||
use crate::tui::plan_prompt::PlanPromptView;
|
||||
use crate::tui::scrolling::{ScrollDirection, TranscriptScroll};
|
||||
use crate::tui::selection::TranscriptSelectionPoint;
|
||||
@@ -1119,7 +1118,7 @@ async fn run_event_loop(
|
||||
app.insert_str(&pending);
|
||||
}
|
||||
|
||||
if (is_plain_char || is_enter) && handle_paste_burst_key(app, &key, now) {
|
||||
if (is_plain_char || is_enter) && super::paste::handle_paste_burst_key(app, &key, now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1523,48 +1522,6 @@ async fn run_event_loop(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_paste_burst_key(app: &mut App, key: &KeyEvent, now: Instant) -> bool {
|
||||
let has_ctrl_alt_or_super = key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
|| key.modifiers.contains(KeyModifiers::ALT)
|
||||
|| key.modifiers.contains(KeyModifiers::SUPER);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if !in_command_context(app) && app.paste_burst.append_newline_if_active(now) {
|
||||
return true;
|
||||
}
|
||||
if !in_command_context(app)
|
||||
&& app.paste_burst.newline_should_insert_instead_of_submit(now)
|
||||
{
|
||||
app.insert_char('\n');
|
||||
app.paste_burst.extend_window(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if !has_ctrl_alt_or_super => {
|
||||
if !c.is_ascii() {
|
||||
if let Some(pending) = app.paste_burst.flush_before_modified_input() {
|
||||
app.insert_str(&pending);
|
||||
}
|
||||
if app.paste_burst.try_append_char_if_active(c, now) {
|
||||
return true;
|
||||
}
|
||||
if let Some(decision) = app.paste_burst.on_plain_char_no_hold(now) {
|
||||
return handle_paste_burst_decision(app, decision, c, now);
|
||||
}
|
||||
app.insert_char(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
let decision = app.paste_burst.on_plain_char(c, now);
|
||||
return handle_paste_burst_decision(app, decision, c, now);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn apply_alt_4_shortcut(app: &mut App, modifiers: KeyModifiers) {
|
||||
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||
app.set_sidebar_focus(SidebarFocus::Agents);
|
||||
@@ -1574,55 +1531,6 @@ fn apply_alt_4_shortcut(app: &mut App, modifiers: KeyModifiers) {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_paste_burst_decision(
|
||||
app: &mut App,
|
||||
decision: CharDecision,
|
||||
c: char,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
match decision {
|
||||
CharDecision::RetainFirstChar => true,
|
||||
CharDecision::BeginBufferFromPending | CharDecision::BufferAppend => {
|
||||
app.paste_burst.append_char_to_buffer(c, now);
|
||||
true
|
||||
}
|
||||
CharDecision::BeginBuffer { retro_chars } => {
|
||||
if apply_paste_burst_retro_capture(app, retro_chars as usize, c, now) {
|
||||
return true;
|
||||
}
|
||||
app.insert_char(c);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_paste_burst_retro_capture(
|
||||
app: &mut App,
|
||||
retro_chars: usize,
|
||||
c: char,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
let cursor_byte = app.cursor_byte_index();
|
||||
let before = &app.input[..cursor_byte];
|
||||
let Some(grab) = app
|
||||
.paste_burst
|
||||
.decide_begin_buffer(now, before, retro_chars)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !grab.grabbed.is_empty() {
|
||||
app.input.replace_range(grab.start_byte..cursor_byte, "");
|
||||
let removed = grab.grabbed.chars().count();
|
||||
app.cursor_position = app.cursor_position.saturating_sub(removed);
|
||||
}
|
||||
app.paste_burst.append_char_to_buffer(c, now);
|
||||
true
|
||||
}
|
||||
|
||||
fn in_command_context(app: &App) -> bool {
|
||||
app.input.starts_with('/')
|
||||
}
|
||||
|
||||
fn visible_slash_menu_entries(app: &App, limit: usize) -> Vec<String> {
|
||||
if app.slash_menu_hidden {
|
||||
return Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user