Rustでlessクローンを作りたい

## はじめに

最近よくログ確認をします。
300万行(1.5GB)くらいのファイルを開いて、文字列を検索しつつエラー箇所を見つけたり、意図通りの動作をしているか確認したりします。

[ripgrep](https://github.com/BurntSushi/ripgrep)であたりをつけつつ、lessコマンドでファイルを開いて該当箇所に飛んだりします。
lessコマンドはファイルを開く動作は軽いのですが、検索が重く、検索してもなかなか結果が返ってきません。
かといって、vim(view)コマンドを使うと検索はlessより速いですが、それでも待たされるのと、ファイルを開いた直後から内容を見れるまでがかなり重いです。（Ctrl+Cとかで途中で読み込みを止めたりします。）

なんかless並みに軽くファイルを開くことができて、viewまたはそれ以上に速く検索できるようなコマンドが欲しいなぁと思い立ちました。

というわけで、完成しないかもしれませんが、検索が速いlessクローンを実装してみたいと思います。

## 今考えている戦略

* Rustを使う
* 検索にripgrepを使う

以上。今考えてるのはこれくらい。

あとはターミナルアプリケーションということで、以下の記事を参考にRustでターミナル扱うcrateを選定しました。
速度と程よい抽象化がされているということで、[crossterm](https://github.com/crossterm-rs/crossterm)を使うことにしました。

https://meganehouser.github.io/2019-12-11_rust-terminal-crates.html

2019年の記事なので少し事情は変わってるかもですが、大枠変わらんだろうという感じで記事は読みました。

## 最低限必要な機能

* コマンドラインからファイルを指定してファイルを閲覧できる
* hjklキーでカーソル移動
* `/` で検索モードに入って `/{WORD}↩︎` で検索できる
    * できれば検索ワードをハイライトしたい
    * できればインクリメンタル検索したい
* 検索ワードが存在する状態でnキーで次の検索候補にジャンプ、n+shiftキーで前の検索候補にジャンプ
* ESCキーで終了


## less

ちなみにlessコマンドはどんな実装か見てみたいなーと思って調べてみると、GitHub上にソースはあるようでした。
まだソースコードは読んでいませんが、実装にどん詰まったら気分転換に読んでみようかと思います。

https://github.com/gwsw/less

古くからあると思うのですが、今なおアクティブにメンテナンスされてるのがすごいです。
見習わねば。

## 実装

### コマンドラインオプション

定番の[clap](https://github.com/clap-rs/clap)を使います。

```rust
#[derive(Parser)]
#[clap(version = "0.0.1", author = "hhatto")]
struct Opts {
    input: String,
}

fn main() -> Result<()> {
    let opts: Opts = Opts::parse();

    opts.input  // これで入力ファイル名取れます
}
```

### ファイル読み込み

ナイーブな実装でBufReaderとか使うと大変そうということがわかってしまい、いろいろ調べているうちに以下の記事で[ropey](https://github.com/cessen/ropey)というライブラリがあることがわかりました。

https://qiita.com/hatoo@github/items/905a19a98876e7446edf#%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%AE%E4%BF%9D%E6%8C%81%E6%96%B9%E6%B3%95

ファイル読み込ませてみると、体感ですがlessよりは遅いがviewよりは速い感じだったので、一旦これで実装を進めていきます。

以下のような感じで使えます。

```rust
    let f = File::open(filename)?;
    let lines = ropey::Rope::from_reader(f)?;

    // ターミナルの高さ(行数)取得して、最初の行数分だけ出力
    let (_, rows) = terminal::size()?;

    for idx in 0..rows {
        println!("{}", lines.line(idx as usize));
        execute!(stdout(), MoveTo(0, idx as u16))?;
        if idx as usize >= line_count - 1 {
            break
        }
    }
```

### main

```rust
fn main() -> Result<()> {
    let opts: Opts = Opts::parse();
    let mut stdout = stdout();

    // rawモード使う
    enable_raw_mode()?;
    // ターミナルを一旦クリア
    execute!(stdout, Clear(ClearType::All))?;
    // カーソル位置を左上に持っていく
    execute!(
        stdout,
        SavePosition,
        MoveTo(0, 0),
        DisableBlinking,
    )?;

    // コア部分 イベントループ
    if let Err(e) = less_loop(opts.input.as_str()) {
        println!("error={:?}\r", e);
    }

    disable_raw_mode()
}
```

### less_loop

入力イベントループ。
検索機能はまだないけどざっくり入れた。

```rust
fn less_loop(filename: &str) -> Result<()> {
    let mut is_search_mode = false;
        :
        :
    execute!(stdout(), MoveTo(0, 0))?;

    loop {
        let event = read()?;

        if is_search_mode {
            match event {
                Event::Key(KeyEvent {
                    code: KeyCode::Esc,
                    modifiers: _,
                }) => {
                    is_search_mode = false;
                    execute!(stdout(), RestorePosition)?;
                },
                _ => (),
            };
        } else {
            execute!(stdout(), SavePosition)?;
            match event {
                Event::Key(KeyEvent {
                    code: KeyCode::Char('h'),
                    modifiers: _,
                }) => execute!(stdout(), MoveLeft(1))?,
                Event::Key(KeyEvent {
                    code: KeyCode::Char('j'),
                    modifiers: _,
                }) => execute!(stdout(), MoveDown(1))?,
                Event::Key(KeyEvent {
                    code: KeyCode::Char('k'),
                    modifiers: _,
                }) => execute!(stdout(), MoveUp(1))?,
                Event::Key(KeyEvent {
                    code: KeyCode::Char('l'),
                    modifiers: _,
                }) => execute!(stdout(), MoveRight(1))?,
                Event::Key(KeyEvent {
                    code: KeyCode::Char('/'),
                    modifiers: _,
                }) => {
                    is_search_mode = true;
                    execute!(
                        stdout(),
                        MoveTo(0, rows+1),
                        Print("/"),
                    )?;
                },
                Event::Key(KeyEvent {
                    code: KeyCode::Esc,
                    modifiers: _,
                }) => break,
                _ => (),
            };
        }
    }
```

## おわりに

今日はここまで。

今日できたこと

* 入力したファイルを開いて画面に表示
* hjklキー入力でカーソル移動（スクロールはできない）
* `/`入力でサーチモードに移行。サーチモード状態でESCキー入力で元のカーソル位置に戻る
* ESCキーでコマンド終了

ここまでのソースは以下にあります。明日つづきやろう。

https://github.com/hhatto/advent-2021/tree/30a514cbf3ea7ce409af0a248ff11fc7221b12c5/1207_less_clone