ホームページ  >  記事  >  バックエンド開発  >  太い指に優しいタイピングのための REPL

太い指に優しいタイピングのための REPL

Barbara Streisand
Barbara Streisandオリジナル
2024-10-22 06:12:30407ブラウズ

A REPL for Fat-Finger Friendly Typing

私の Python インタープリタである Memphis には REPL (read-eval-print ループ) があります!

これは古いニュースです。賢い老フクロウと対話する際に間違いがなければ、思う存分解釈することができます。同じステートメントを 2 回評価したくなかった場合、またはそうする場合は、再入力する必要はありません。しかもミスゼロです。

私はこの REPL に完全に満足しました。ワクワクさえする。私はこの REPL について家に手紙を書きました。しかし、私の上司の上司の上司は、従業員の利益のために REPL を改善することを要求しました。彼らは私をオフィスに呼び、マホガニーの机の向かい側の椅子にうなずいて座らせました。 「一部のユーザーはバックスペースキーが機能することを期待しています。」ユーザーは地獄へ! 「上矢印を押すと最後のコマンドが表示されます。」矢印キーのサポートは 90 年代のものです! 「第 5 四半期の終わりまでにこれを完了してもらえますか?」やめます!

そこで私はデスクに戻り、REPL を改善しました。

すべてのキーが機能するように改良しました。バックスペース キー、上矢印、下矢印、左矢印、そして最後にバックスペース矢印です。会計士は新しい REPL を実際に使用することができます。カチカチカチカチカチカチカチカチカチカチ。それは会計士が数字を入力しているだけであり、ゆっくりと拡散する爆弾ではありません。

私は REPL を研究室に送り、主力の機械工にこの注文について急ぎの仕事をするように言いました。それは私が言ったREPLであり、彼らの目の表情から、彼らが理解していることがわかりました。 750 ミリ秒後にビルドが完了し、矢印キーがサポートされるようになりました。私はその製品を大手かつらに持ち帰り、仕事に戻ってほしいと懇願し、彼らの意見を尋ねました。彼らはいくつかのコマンドを実行し、いくつかのプリントを印刷し、いくつかの追加を追加しました。彼らは間違いを犯してバックスペースキーを押しました。マジで誰が間違えるのかと目を丸くしましたが、彼らは満足しているようでした。彼らは、すでに入力した長いコマンドを実行したくないことに気づきました。これが、私の人生が地獄に落ちた場所です。彼らは。打つ。 Ctrl. C. マジで、誰がそんなことするの?これで現在のプロセスが終了することはご存知ですか?そうでしょう?

「来年末までに Ctrl-C のサポートが必要です。」これらの人々と彼らの要求。 Ctrl-C サポートを追加します。しかし、今後 2 年以内には絶対に実現しないでしょう。

そこで私はデスクに戻り、Ctrl-C のサポートを追加しました。

この REPL が指が太い傾向のある人々にとって価値がある理由は何でしょうか?

私は道具になれるでしょうか?

私は「ゼロから」物事を構築することに職業的および経済的な将来のすべてを賭けてきたため、このプロジェクトの初日に困難に直面しました。私がキー検出に Crossterm を使用することを選択したのは、主にクロスプラットフォームのサポートのためです。正直に言うと、クロスタームはとてもとても良かったです。 API は直感的で、特に KeyModifiers に満足しました (Ctrl-C を処理するために必要でしたが、不要だと思いました。上記を参照)。

RAWモードは面倒だ

端末が特別なキーを処理しないようにするために、これが必要でした。しかし、くそー、それが私たちの画面を故障したタイプライターに変えるとは知りませんでした。とにかく、すべての文字列を正規化して、改行文字の前に復帰を追加する必要がありました。それはうまくいきました、そして私はそれについて興奮しています。

/// 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");
}

も!予期しないパニックが発生した場合に raw モードを無効にしないと、ターミナル セッションが使用できなくなります。これをキャッチして適切に動作させるために、カスタム パニック ハンドラーをインストールしました。

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 コードを標準入力に渡すだけで、統合的にテストできました。契約上の紛争のため、クロスタームを使用すると機能しなくなりました。正直、完全に説明することはできませんが、標準入力入力を使用した統合テストでは、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) テスト内でサーバーを起動 / ポートを開いて / ソケットでリッスンする場合、それを統合テストと呼びます。コメントに残したい別の定義がある場合は、迷惑に聞こえるのでやめてください。

まずは 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 を追加する動機の 1 つは、2 番目のエントリポイントを追加するとコードが改善されると信じているからです。あなたは本質的にあなたのライブラリの 2 番目のユーザーになることになります。これは、私たち全員が探し求めてキーボードを叩いている 1 つの完璧な抽象化の理解にさらに近づくのに役立ちます。この点は本気で言いたいのです。

「依存関係ゼロ」?

REPL は現在、管理を取り戻す方法として機能フラグを立てています。私はサードパーティのクレートを一切使用せずに Python コードを解釈する機能を維持しています。つまり、crossterm を例外にするか、機能フラグを導入する必要があります。ここで、REPL を有効にせずにコンパイルして「memphis」を実行すると、「ビルドが間違っています、愚か者」と丁寧に通知されます。

さようなら

REPL が登場しました。このように実行できます。購入したい場合は詐欺のようです。お元気で、すぐに話しましょう。


このような投稿をさらに直接受信トレイに受け取りたい場合は、ここから購読できます!

他の場所

私はソフトウェア エンジニアの指導に加えて、成人と診断された自閉症者としての経験についても書いています。コードは減り、ジョークの数は同じです。

  • イギリスはほどほどに - From Scratch dot org

以上が太い指に優しいタイピングのための REPLの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
前の記事:Python のデータ型次の記事:Python のデータ型