>  기사  >  백엔드 개발  >  손가락에 친숙한 타이핑을 위한 REPL

손가락에 친숙한 타이핑을 위한 REPL

Barbara Streisand
Barbara Streisand원래의
2024-10-22 06:12:30495검색

A REPL for Fat-Finger Friendly Typing

제가 사용하는 Python 인터프리터인 Memphis에는 REPL(읽기-평가-인쇄 루프)이 있습니다!

오래된 소식입니다. 현명한 부엉이 ?와 소통하면서 실수 없이 소통한다면 마음껏 해석할 수 있을 것입니다. 동일한 문을 두 번 평가하고 싶지 않다고 가정하거나, 그렇게 했다면 다시 입력해도 괜찮습니다. 역시 실수 제로.

저는 이 REPL에 완벽하게 만족했습니다. 심지어 감격스럽기도 했다. 나는 이 REPL에 관해 집에 편지를 썼습니다. 하지만 내 상사의 상사의 상사는 사람들의 이익을 위해 REPL을 개선할 것을 요구했습니다. 그들은 저를 사무실로 부르더니 마호가니 책상 맞은편 의자에 앉으라고 고개를 끄덕였습니다. "일부 사용자는 백스페이스 키가 작동할 것으로 기대합니다." 사용자들과 지옥으로! "위쪽 화살표를 누르면 마지막 명령이 표시됩니다." 화살표 키 지원은 90년대에 이르렀습니다! “5분기 말까지 이 작업을 완료할 수 있나요?” 그만뒀어요!

그래서 다시 자리로 돌아가 REPL을 개선했습니다.

모든 키가 작동할 정도로 많이 개선했습니다. 백스페이스 키, 위쪽 화살표, 아래쪽 화살표, 왼쪽 화살표, 그리고 마지막으로 중요한 것은 백스페이스 화살표입니다. 회계사는 새로운 REPL을 사용하여 현장 학습을 할 수 있습니다. 틱 틱 틱 틱 틱 틱 틱 틱. 그건 회계사가 숫자를 입력하는 것이지 천천히 퍼지는 폭탄이 아닙니다.

저는 REPL을 연구실로 보내고 담당 기계공에게 이 주문을 서둘러 처리하라고 말했습니다. 내가 말한 것은 REPL이고 그들의 눈빛을 보면 그들이 이해했다는 것을 알 수 있었습니다. 750ms 후에 빌드가 완료되었고 화살표 키가 지원되었습니다. 나는 제품을 큰 가발에게 가져가서 다시 일을 해달라고 간청하고 어떻게 생각하는지 물었습니다. 몇 가지 명령을 실행하고 일부 인쇄물을 인쇄하고 일부 추가 기능을 추가했습니다. 그들은 실수를 해서 백스페이스 키를 눌렀습니다. 누가 실수를 해도 진심으로 만족하는 것 같아서 눈을 굴렸습니다. 그들은 이미 입력한 긴 명령을 실행하고 싶지 않다는 것을 깨달았고 이것이 내 인생이 손바구니에 담긴 지옥이 된 곳이었습니다. 그들. 때리다. 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");
}

역시! 예상치 못한 패닉이 발생했을 때 원시 모드를 비활성화하지 않으면 터미널 세션을 사용할 수 없게 됩니다. 이것을 잡아서 잘 플레이하기 위해 커스텀 패닉 핸들러를 설치했습니다.

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에 전달하여 통합적으로 테스트할 수 있었습니다. 계약 분쟁으로 인해 크로스텀을 사용할 때 작동이 중단된 것 같습니다. 솔직히 완전히 설명할 수는 없지만 stdin 입력과 함께 제공되는 통합 테스트에서 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) 서버 실행/포트 열기/테스트 내부 소켓 수신 대기 중 하나를 수행하는 경우 이를 통합 테스트라고 부릅니다. 댓글에 남기고 싶은 다른 정의가 있다면, 짜증나는 TBH처럼 들리니 그러지 마세요.

시작하기 위해 두 가지 유틸리티 함수를 만들었습니다.

/// 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은 관리에 복귀하는 방법으로 기능 플래그 뒤에 있습니다. 저는 제3자 상자의 도움으로 Python 코드를 해석하는 능력을 유지하고 있습니다. 이는 crossterm이 예외가 되어야 하거나 기능 플래그를 도입해야 함을 의미합니다. 이제 REPL을 활성화하지 않고 컴파일하고 "memphis"를 실행하면 "잘못된 빌드, 멍청아"라고 정중하게 알려줄 것입니다.

안녕히 가세요

REPL이 왔습니다. 이렇게 실행하시면 됩니다. 사고싶다면 사기라고 봅니다. 빨리 잘 지내고 얘기하세요.


이런 게시물을 받은편지함으로 직접 받아보려면 여기에서 구독하세요!

다른 곳

저는 소프트웨어 엔지니어를 멘토링하는 것 외에도 성인 자폐증 진단을 받은 경험에 대해서도 글을 씁니다. 코드는 적고 농담 수는 동일합니다.

  • 영국의 적당히 - Scratch dot org
  • 에서

위 내용은 손가락에 친숙한 타이핑을 위한 REPL의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.