mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
Merge feat/ctrl-bs-delete-line into main: add Ctrl+Backspace line delete, Ctrl+Z undo in TUI composer; treat benign finalize commits as success; keep v0.2.88 release metadata
This commit is contained in:
+13
-4
@@ -564,11 +564,20 @@ impl App<'_> {
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.suspend(terminal)?;
|
||||
// Prefer in-app undo in Chat (composer) over shell suspend.
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => {
|
||||
widget.handle_key_event(key_event);
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppState::Onboarding { .. } => {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.suspend(terminal)?;
|
||||
}
|
||||
// No-op on non-Unix platforms.
|
||||
}
|
||||
}
|
||||
// No-op on non-Unix platforms.
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('r') | KeyCode::Char('t'),
|
||||
|
||||
@@ -20,6 +20,9 @@ pub(crate) struct TextArea {
|
||||
cursor_pos: usize,
|
||||
wrap_cache: RefCell<Option<WrapCache>>,
|
||||
preferred_col: Option<usize>,
|
||||
// Simple undo stack capturing full snapshots of text and cursor before edits.
|
||||
// This is intentionally simple to reliably undo paste and bulk edits across terminals.
|
||||
undo_stack: Vec<UndoSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -28,6 +31,12 @@ struct WrapCache {
|
||||
lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UndoSnapshot {
|
||||
text: String,
|
||||
cursor_pos: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct TextAreaState {
|
||||
/// Index into wrapped lines of the first visible line.
|
||||
@@ -41,6 +50,7 @@ impl TextArea {
|
||||
cursor_pos: 0,
|
||||
wrap_cache: RefCell::new(None),
|
||||
preferred_col: None,
|
||||
undo_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +70,7 @@ impl TextArea {
|
||||
}
|
||||
|
||||
pub fn insert_str_at(&mut self, pos: usize, text: &str) {
|
||||
self.push_undo_snapshot();
|
||||
let pos = self.clamp_pos_for_insertion(pos);
|
||||
self.text.insert_str(pos, text);
|
||||
self.wrap_cache.replace(None);
|
||||
@@ -77,6 +88,7 @@ impl TextArea {
|
||||
|
||||
fn replace_range_raw(&mut self, range: std::ops::Range<usize>, text: &str) {
|
||||
assert!(range.start <= range.end);
|
||||
self.push_undo_snapshot();
|
||||
let start = range.start.clamp(0, self.text.len());
|
||||
let end = range.end.clamp(0, self.text.len());
|
||||
let removed_len = end - start;
|
||||
@@ -253,6 +265,16 @@ impl TextArea {
|
||||
} if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
||||
self.delete_backward_word()
|
||||
},
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
} => {
|
||||
// Map Ctrl+Backspace to delete the entire current line contents.
|
||||
// This provides a portable shortcut similar to macOS Cmd+Backspace
|
||||
// for terminals that do not forward the Command (SUPER) modifier.
|
||||
self.delete_entire_line();
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
modifiers: KeyModifiers::ALT,
|
||||
@@ -285,6 +307,14 @@ impl TextArea {
|
||||
} => {
|
||||
self.delete_backward_word();
|
||||
}
|
||||
// Undo: Ctrl+Z reverts the last edit snapshot (paste, insert, delete, etc.).
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('z'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
} => {
|
||||
self.undo();
|
||||
}
|
||||
// macOS-like shortcuts (when terminals report the Command key as SUPER):
|
||||
// Cmd+Left -> move to beginning of line
|
||||
// Cmd+Right -> move to end of line
|
||||
@@ -507,6 +537,16 @@ impl TextArea {
|
||||
self.set_cursor(bol);
|
||||
}
|
||||
|
||||
/// Revert the most recent edit (if any).
|
||||
pub fn undo(&mut self) {
|
||||
if let Some(prev) = self.undo_stack.pop() {
|
||||
self.text = prev.text;
|
||||
self.cursor_pos = prev.cursor_pos.min(self.text.len());
|
||||
self.wrap_cache.replace(None);
|
||||
self.preferred_col = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the cursor left by a single grapheme cluster.
|
||||
pub fn move_cursor_left(&mut self) {
|
||||
self.cursor_pos = self.prev_atomic_boundary(self.cursor_pos);
|
||||
@@ -719,6 +759,20 @@ impl TextArea {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_undo_snapshot(&mut self) {
|
||||
// Capture current state before mutation.
|
||||
self.undo_stack.push(UndoSnapshot {
|
||||
text: self.text.clone(),
|
||||
cursor_pos: self.cursor_pos,
|
||||
});
|
||||
// Keep a bounded history to avoid unbounded memory growth.
|
||||
const MAX_UNDO_SNAPSHOTS: usize = 128;
|
||||
if self.undo_stack.len() > MAX_UNDO_SNAPSHOTS {
|
||||
let excess = self.undo_stack.len() - MAX_UNDO_SNAPSHOTS;
|
||||
self.undo_stack.drain(0..excess);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn beginning_of_previous_word(&self) -> usize {
|
||||
if let Some(first_non_ws) = self.text[..self.cursor_pos].rfind(|c: char| !c.is_whitespace())
|
||||
{
|
||||
|
||||
@@ -6303,15 +6303,21 @@ impl ChatWidget<'_> {
|
||||
.output()
|
||||
.await;
|
||||
if let Ok(o) = &commit_out {
|
||||
if !o.status.success() {
|
||||
// Ignore "nothing to commit" (exit 1); surface others
|
||||
let msg = String::from_utf8_lossy(&o.stderr);
|
||||
if !msg.contains("nothing to commit") {
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch finalize` — commit failed: {}", msg.trim()) }), order: None }));
|
||||
}
|
||||
if !o.status.success() {
|
||||
// Git prints "nothing to commit, working tree clean" on stdout and exits 1.
|
||||
let stderr_s = String::from_utf8_lossy(&o.stderr);
|
||||
let stdout_s = String::from_utf8_lossy(&o.stdout);
|
||||
let benign = stdout_s.contains("nothing to commit")
|
||||
|| stdout_s.contains("working tree clean")
|
||||
|| stderr_s.contains("nothing to commit")
|
||||
|| stderr_s.contains("working tree clean");
|
||||
if !benign {
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let detail = if !stderr_s.trim().is_empty() { stderr_s.trim().to_string() } else { stdout_s.trim().to_string() };
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch finalize` — commit failed: {}", detail) }), order: None }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine default branch in main repo
|
||||
let default_branch = match codex_core::git_worktree::detect_default_branch(&git_root).await {
|
||||
|
||||
Reference in New Issue
Block a user