适合胖手指打字的 REPL

Barbara Streisand
发布: 2024-10-22 06:12:30
原创
412 人浏览过

A REPL for Fat-Finger Friendly Typing

我的 Python 解释器 Memphis 有一个 REPL(读取-评估-打印循环)!

这是旧闻了。只要你在与聪明的老猫头鹰互动时零失误,就可以随心所欲地解读。假设您不想对同一个语句求值两次,或者如果您这样做,也不介意重新输入它。而且零错误。

我对这个 REPL 非常满意。甚至激动不已。我已经写信回家谈论这个 REPL。但我老板的老板的老板要求我们为了人民的底线而改进 REPL。他们把我叫进办公室,点点头让我坐在红木办公桌对面的椅子上。 “一些用户希望退格键起作用。”让用户见鬼去吧! “向上箭头应该会显示他们的最后一个命令。”箭头键支持是如此九十年代! “你能在第五季度结束前完成这项工作吗?”我放弃了!

所以我回到办公桌前改进了 REPL。

我对其进行了很大改进,所有按键都可以使用。退格键、向上箭头、向下箭头、向左箭头,以及最后但并非最不重要的退格箭头。会计师可以使用新的 REPL 进行实地考察。勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾勾。那是会计师在敲数字,而不是慢慢扩散的炸弹。

我将 REPL 发送到实验室,并告诉我的主要机械师对该订单进行加急工作。这是我说的 REPL,从他们的眼神我可以看出他们明白了。 750 毫秒后,构建完成,我们获得了箭头键支持。我把产品带回给大佬们,请求让我恢复工作,并询问他们的想法。他们运行了一些命令,打印了一些打印内容,并添加了一些添加内容。他们犯了一个错误,按下了退格键。我翻了个白眼,因为说真的,谁犯了错误,但他们似乎很满意。他们意识到他们不想运行已经输入的长命令,这就是我的生活陷入困境的地方。他们。打。 Ctrl。 C. 说真的,这是谁干的?!您知道这会结束当前进程,对吗?对吗???

“到明年年底,我们需要 Ctrl-C 支持。”这些人和他们的要求。我会添加 Ctrl-C 支持。但两年之内绝对不会。

所以我回到办公桌并添加了 Ctrl-C 支持。

是什么让这个 REPL 值得那些有胖手指倾向的人使用呢?

我会成为一个工具吗?

我把我的整个职业和财务未来都押在“从头开始”构建东西上,所以我在这个项目的第一天就面临着困境。我选择使用 crossterm 进行关键检测主要是因为跨平台支持。老实说,crossterm 非常非常好。 API 很直观,我对 KeyModifiers 特别满意(我们需要它来处理 Ctrl-C,我认为这是不必要的,见上文)。

原始模式很痛苦

我们需要它,这样终端就不会为我们处理特殊的密钥。但该死的,我没有意识到它会把我们的屏幕变成一台有故障的打字机。无论如何,我必须规范所有字符串以在任何换行符之前添加回车符。效果很好,我对此感到非常兴奋。

/// When the terminal is in raw mode, we must emit a carriage return in addition to a newline,
/// because that does not happen automatically.
fn normalize<T: Display>(err: T) -> String {
    let formatted = format!("{}", err);
    if terminal::is_raw_mode_enabled().expect("Failed to query terminal raw mode") {
        formatted.replace("\n", "\n\r")
    } else {
        formatted.to_string()
    }
}

/// Print command which will normalize newlines + carriage returns before printing.
fn print_raw<T: Display>(val: T) {
    print!("{}", normalize(val));
    io::stdout().flush().expect("Failed to flush stdout");
}
登录后复制

还有!如果您没有在意外恐慌时禁用原始模式,您的终端会话将变得不可用。我安装了自定义恐慌处理程序来捕捉这个并玩得很好。

panic::set_hook(Box::new(|info| {
    // This line is critical!! The rest of this function is just debug info, but without this
    // line, your shell will become unusable on an unexpected panic.
    let _ = terminal::disable_raw_mode();

    if let Some(s) = info.payload().downcast_ref::<&str>() {
        eprintln!("\nPanic: {s:?}");
    } else if let Some(s) = info.payload().downcast_ref::<String>() {
        eprintln!("\nPanic: {s:?}");
    } else {
        eprintln!("\nPanic occurred!");
    }

    if let Some(location) = info.location() {
        eprintln!(
            "  in file '{}' at line {}",
            location.file(),
            location.line()
        );
    } else {
        eprintln!("  in an unknown location.");
    }

    process::exit(1);
}));
登录后复制

集成测试很有趣

在我的旧 REPL(我更喜欢它,见上文)下,我可以通过运行二进制文件并将一些 Python 代码传递到 stdin 来测试它的集成。当使用 crossterm 时,它停止工作,我认为是由于合同纠纷。老实说,我无法完全解释它,但是 event::read() 在标准输入输入提供的集成测试中会超时并失败。所以我嘲笑了它。

pub trait TerminalIO {
    fn read_event(&mut self) -> Result<Event, io::Error>;
    fn write<T: Display>(&mut self, output: T) -> io::Result<()>;
    fn writeln<T: Display>(&mut self, output: T) -> io::Result<()>;
}

/// A mock for testing that doesn't use `crossterm`.
struct MockTerminalIO {                                                        
    /// Predefined events for testing
    events: Vec<Event>,

    /// Captured output for assertions
    output: Vec<String>,
}

impl TerminalIO for MockTerminalIO {
    fn read_event(&mut self) -> Result<Event, io::Error> {
        if self.events.is_empty() {
            Err(io::Error::new(io::ErrorKind::Other, "No more events"))
        } else {
            // remove from the front (semantically similar to VecDequeue::pop_front).
            Ok(self.events.remove(0))
        }
    }

    fn write<T: Display>(&mut self, output: T) -> io::Result<()> {
        self.output.push(format!("{}", output));
        Ok(())
    }

    fn writeln<T: Display>(&mut self, output: T) -> io::Result<()> {
        self.write(output)?;
        self.write("\n")?;
        Ok(())
    }
}
登录后复制

这导致整个事情变成了单元测试?老实说我不知道​​。此时,如果我 a) 在另一个二进制文件中调用一个二进制文件,或者 2) 在测试中启动服务器/打开端口/侦听套接字,我将其称为集成测试。如果您想在评论中留下其他定义,请不要留下,因为这听起来很烦人。

我创建了两个实用函数来开始。

/// Run the complete flow, from input code string to return value string. If you need any Ctrl
/// modifiers, do not use this!
fn run_and_return(input: &str) -> String {
    let mut terminal = MockTerminalIO::from_str(input);
    Repl::new().run(&mut terminal);
    terminal.return_val()
}

/// Turn an input string into a list of crossterm events so we don't have to
/// hand-compile our test.
fn string_to_events(input: &str) -> Vec<Event> {
    input
        .chars()
        .map(|c| {
            let key_code = match c {
                '\n' => KeyCode::Enter,
                _ => KeyCode::Char(c),
            };
            Event::Key(KeyEvent::new(key_code, KeyModifiers::NONE))
        })
        .collect()
}
登录后复制

使用这些,我们现在可以用相当少的样板来测试这些常见场景。

#[test]
fn test_repl_name_error() {
    let return_val = run_and_return("e\n");
    assert!(return_val.contains("NameError: name 'e' is not defined"));
}

#[test]
fn test_repl_expr() {
    let third_from_last = run_and_return("12345\n");
    assert_eq!(third_from_last, "12345");
}

#[test]
fn test_repl_statement() {
    let return_val = run_and_return("a = 5.5\n");

    // empty string because a statement does not have a return value
    assert_eq!(return_val, "");
}

#[test]
fn test_repl_function() {
    let code = r#"
def foo():
    a = 10
    return 2 * a

foo()
"#;
    let return_val = run_and_return(code);
    assert_eq!(return_val, "20");
}

#[test]
fn test_repl_ctrl_c() {
    let mut events = string_to_events("123456789\n");
    let ctrl_c = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
    events.insert(4, ctrl_c);
    let mut terminal = MockTerminalIO::new(events);

    Repl::new().run(&mut terminal);
    assert_eq!(terminal.return_val(), "56789");
}
登录后复制

代码入口点让我早上起床

我添加 REPL 的动机之一是因为我相信当您添加第二个入口点时,您的代码会变得更好。您实际上正在成为您的图书馆的第二个用户,这可以帮助您更接近地理解我们都在键盘上寻找的完美抽象。我是认真的说这一点。

“零依赖”?

REPL 现在位于功能标志后面,作为重新获得管理的一种方式。我在零第三方 crate 的帮助下保持解释 Python 代码的能力,这意味着 crossterm 要么需要成为例外,要么我会引入一个功能标志。现在,如果你在没有启用 REPL 的情况下编译并运行“memphis”,它会礼貌地告诉你“错误的构建,笨蛋。”

再见

REPL 就在这里。你可以像这样运行它。如果你想买的话,那听起来就像是一个骗局。祝你好起来,尽快说话。


如果您想将更多类似的帖子直接发送到您的收件箱,您可以在这里订阅!

别处

除了指导软件工程师之外,我还写了我作为一名成人诊断自闭症患者的经历。更少的代码和相同数量的笑话。

  • 英国的节制 - From Scratch dot org

以上是适合胖手指打字的 REPL的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!