You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Rust与GTK的文本编辑器:共享数据下撤销/重做功能排障

Rust GTK文本编辑器撤销/重做功能实现问题解决建议

我在开发Rust GTK文本编辑器时,卡在了撤销/重做功能上:尝试Rc<RefCell>会触发panic,使用Arc<Mutex>导致程序冻结;直接用引用因需修改数据无法生效,GTK的上下文传递也增加了复杂度。目标是实现全局共享的状态栈与撤销栈系统。

核心问题分析

  1. 死锁(Arc冻结):undo/redo操作中调用text_buffer.set_text()会触发changed信号回调,而回调会尝试再次获取同一个Mutex锁,导致线程阻塞死锁。
  2. RefCell panic:回调嵌套中同时持有多个可变引用,违反了RefCell的借用规则。
  3. 撤销逻辑缺陷:当前UndoRedoHandler的undo逻辑在历史栈仅存一条数据时会返回空值,不符合预期;且保存完整文本的方式效率低下。

解决方案

一、修复死锁:分离锁持有与UI修改

在undo/redo操作中,先获取需要恢复的文本,释放锁后再修改TextBuffer,避免在持有锁时触发changed回调。

修改undo_redo.rs的处理函数:

pub fn undo_handler(undo_redo: &mut UndoRedoHandler) -> Option<String> {
    undo_redo.undo()
}

pub fn redo_handler(undo_redo: &mut UndoRedoHandler) -> Option<String> {
    undo_redo.redo()
}

调整main.rs的回调逻辑:

edit_menu.edit_undo.connect_activate(move |_| {
    // 锁仅在获取文本时持有,随后自动释放
    let text_opt = {
        let mut undo_data = undo_data.lock().unwrap();
        undo_handler(&mut undo_data)
    };
    if let Some(text) = text_opt {
        undo_text.set_text(text.as_str());
    } else {
        println!("Nothing to undo.");
    }
});

edit_menu.edit_redo.connect_activate(move |_| {
    let text_opt = {
        let mut redo_data = redo_data.lock().unwrap();
        redo_handler(&mut redo_data)
    };
    if let Some(text) = text_opt {
        redo_text.set_text(text.as_str());
    } else {
        println!("Nothing to redo.");
    }
});

二、优化UndoRedoHandler逻辑

修正状态栈初始化与undo/redo逻辑,避免空栈异常:

// unre_handler_impl.rs
impl UndoRedoHandler {
    pub fn new() -> Self {
        let mut history = Vec::new();
        history.push(String::new()); // 初始化空文本作为初始状态
        UndoRedoHandler {
            history,
            undo_stack: Vec::new(),
        }
    }

    pub fn push(&mut self, item: String) {
        self.history.push(item);
        self.undo_stack.clear();
    }

    pub fn undo(&mut self) -> Option<String> {
        // 仅当历史栈长度大于1时允许撤销(保留初始状态)
        if self.history.len() > 1 {
            let item = self.history.pop().unwrap();
            self.undo_stack.push(item);
            self.history.last().cloned()
        } else {
            None
        }
    }

    pub fn redo(&mut self) -> Option<String> {
        if let Some(item) = self.undo_stack.pop() {
            self.history.push(item.clone());
            Some(item)
        } else {
            None
        }
    }
}

同时优化状态触发条件,避免重复保存:

// undo_redo.rs
pub fn character_change_handler(text_buffer: &TextBuffer, undo_redo: &mut UndoRedoHandler) {
    let start_iter = text_buffer.start_iter();
    let end_iter = text_buffer.end_iter();
    let current_text = text_buffer.text(&start_iter, &end_iter, true).unwrap();

    // 仅当当前文本与历史最后一条不同时,且检测到空格/回车才保存状态
    if let Some(last_state) = undo_redo.history.last() {
        if last_state != &current_text {
            let last_char = current_text.chars().last();
            if last_char == Some(' ') || last_char == Some('\n') {
                undo_redo.push(current_text);
                println!("Saved undo state");
            }
        }
    }
}

三、切换为Rc(单线程场景)

GTK是单线程UI模型,无需跨线程同步,Rc<RefCell>比Arc<Mutex>更轻量,且能避免死锁风险:

// main.rs
use std::rc::Rc;
use std::cell::RefCell;

// ...

app.connect_activate(move |_| {
    let undo_redo_handler = Rc::new(RefCell::new(UndoRedoHandler::new()));

    // changed回调
    let undo_redo_text = text_buffer.clone();
    let undo_redo_data = Rc::clone(&undo_redo_handler);
    text_buffer.connect_changed(move |_| {
        let mut undo_redo_data = undo_redo_data.borrow_mut();
        character_change_handler(&undo_redo_text, &mut undo_redo_data);
    });

    // 撤销回调
    let undo_text = text_buffer.clone();
    let undo_data = Rc::clone(&undo_redo_handler);
    edit_menu.edit_undo.connect_activate(move |_| {
        let text_opt = {
            let mut undo_data = undo_data.borrow_mut();
            undo_handler(&mut undo_data)
        };
        if let Some(text) = text_opt {
            undo_text.set_text(text.as_str());
        }
    });

    // 重做回调(结构同撤销)
    let redo_text = text_buffer.clone();
    let redo_data = Rc::clone(&undo_redo_handler);
    edit_menu.edit_redo.connect_activate(move |_| {
        let text_opt = {
            let mut redo_data = redo_data.borrow_mut();
            redo_handler(&mut redo_data)
        };
        if let Some(text) = text_opt {
            redo_text.set_text(text.as_str());
        }
    });
});

四、进阶优化:命令模式

如果需要支持复杂操作(如局部修改、删除),建议使用命令模式,封装每个操作的正向/反向逻辑,替代保存完整文本的方式:

// 定义命令 trait
trait Command {
    fn execute(&mut self, buffer: &TextBuffer);
    fn undo(&mut self, buffer: &TextBuffer);
}

// 插入文本命令示例
struct InsertCommand {
    position: u32,
    text: String,
}

impl Command for InsertCommand {
    fn execute(&mut self, buffer: &TextBuffer) {
        let iter = buffer.iter_at_offset(self.position as i32);
        buffer.insert(&iter, &self.text);
    }

    fn undo(&mut self, buffer: &TextBuffer) {
        let start = buffer.iter_at_offset(self.position as i32);
        let end = buffer.iter_at_offset((self.position + self.text.len()) as i32);
        buffer.delete(&start, &end);
    }
}

// 调整UndoRedoHandler保存命令栈
pub struct UndoRedoHandler {
    undo_stack: Vec<Box<dyn Command>>,
    redo_stack: Vec<Box<dyn Command>>,
}

内容的提问来源于stack exchange,提问作者Daniel McLarty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 14:32:08