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

https://qiita.com/hhatto/items/f1f6d1abe1cec7c0441c

https://qiita.com/hhatto/items/d04a317f0ea9313e2429

Rustでlessクローン実装の3日目です。
気分を変えて検索機能を入れてみます。

初めに考えていた戦略通り[ripgrep](https://github.com/BurntSushi/ripgrep)を使います。

## そのまえにclapバージョン3.0.0の対応

ripgrep関連の機能を入れようと`cargo update`するとclapのバージョン `3.0.0-rc.1` にアップデートされてしまいました。
各機能がfeaturesに分離されたようで、そのままではビルドできなくなりました。
というわけで、以下のようなパッチを当てました。

```diff
diff --git a/1207_less_clone/Cargo.toml b/1207_less_clone/Cargo.toml
index 142d615..b131d5f 100644
--- a/1207_less_clone/Cargo.toml
+++ b/1207_less_clone/Cargo.toml
@@ -6,6 +6,6 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

 [dependencies]
-clap = "3.0.0-beta.5"
+clap = { version = "3.0.0-rc.1", features = ["derive"] }
 crossterm = "0.22.1"
 ropey = "1.3.1"
```

これでビルドできるようになりました。

## ripgrep関連の機能を追加

まずはripgrepの[grep](https://docs.rs/grep/latest/grep/)crateを組み込みます。

```diff
diff --git a/1207_less_clone/Cargo.toml b/1207_less_clone/Cargo.toml
index b131d5f..d95294a 100644
--- a/1207_less_clone/Cargo.toml
+++ b/1207_less_clone/Cargo.toml
@@ -9,3 +9,4 @@ edition = "2021"
 clap = { version = "3.0.0-rc.1", features = ["derive"] }
 crossterm = "0.22.1"
 ropey = "1.3.1"
+grep = "0.2"
```
これで`grep-XXX`なcrateを呼び出せます。

検索機能を実装してみる。

```rust
use grep::searcher::SearcherBuilder;
use grep::regex::RegexMatcher;
use grep::searcher::sinks::UTF8;

fn search(filename: &str, search_word: &str) -> Result<Vec<(u64, String)>> {
    let matcher = RegexMatcher::new(search_word).unwrap();
    let mut matches: Vec<(u64, String)> = vec![];
    let mut searcher = SearcherBuilder::new().build();
    searcher.search_path(&matcher, filename, UTF8(|lnum, line| {
        matches.push((lnum, line.to_string()));
        Ok(true)
    }))?;
    Ok(matches)
}
```

ripgrep系の機能は、検索機能自体?を提供する`Searcher`、文字列(regexp)マッチ機能を提供する`Matcher`、マッチしたときにどのように処理するかを司る`Sink`の三点セットになっています。

`/WORD↩︎`で`WORD`文字列を取り出して、この実装した `fn search()`に入力します。
パッチはこんな感じ。

```diff
diff --git a/1207_less_clone/src/main.rs b/1207_less_clone/src/main.rs
index 174aefd..b975dd5 100644
--- a/1207_less_clone/src/main.rs
+++ b/1207_less_clone/src/main.rs
@@ -52,6 +52,7 @@ fn less_loop(filename: &str) -> Result<()> {
     let line_count = lines.len_lines();
     let mut is_search_mode = false;

+    let mut search_word_vec: Vec<char> = [].to_vec();
     let (_, window_rows) = terminal::size()?;
     let mut display_lines = DisplayLines { start: 0, end: 0 };

@@ -77,6 +78,40 @@ fn less_loop(filename: &str) -> Result<()> {
                 }) => {
                     is_search_mode = false;
                     execute!(stdout(), RestorePosition)?;
+                    search_word_vec = Vec::new();
+                },
+                Event::Key(KeyEvent {
+                    code: KeyCode::Enter,
+                    modifiers: _,
+                }) => {
+                    let search_word = String::from_iter(search_word_vec.clone());
+                    let result = search(filename, search_word.as_str())?;
+                    if result.len() > 0 {
+                        // jump to result line
+                        let (lnum, _) = result[0];
+                        execute!(stdout(), SavePosition, Clear(ClearType::All))?;
+
+                        for idx in lnum..(lnum+window_rows as u64) {
+                            println!("{}", lines.line(idx as usize));
+                            execute!(stdout(), MoveTo(0, idx as u16))?;
+                            if idx as usize >= line_count - 1 {
+                                break
+                            }
+                            *display_lines.end_mut() = idx;
+                        }
+                        execute!(stdout(), RestorePosition)?;
+                    }
+
+                    is_search_mode = false;
+                    execute!(stdout(), RestorePosition)?;
+                    search_word_vec = Vec::new();
+                },
+                Event::Key(KeyEvent {
+                    code: KeyCode::Char(c),
+                    modifiers: _,
+                }) => {
+                    search_word_vec.push(c);
+                    execute!(stdout(), Print(c))?;
                 },
                 _ => (),
             };
```

1文字ずつ入力を拾って、`search_word_vec`に検索ワードを入れる。
Enter入力されたら`search_word_vec`を文字列にして`fn search()`に渡す。
`fn search()`から結果が戻ってくるので、いい感じにジャンプする。（今は結果があるかチェックして0番の結果を無条件で使う感じ。）

という流れです。

ripgrepから返される行番号の型が`u64`なので、そのあたりは`u64`で保持するように変更しました。

## 動作確認

いちおここまでで検索してジャンプっぽい動きができました。

![input-search-word.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1775/9ec38641-f85d-f2d7-01b6-1447f852f4e7.png)

ただジャンプして表示するとバグりまくってましたorz
![search-jump.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1775/ee15cd3d-1372-2430-a263-ff77c537fb5f.png)

今日は時間切れでここまで。

今日の成果
https://github.com/hhatto/advent-2021/compare/3e90ddd468b1b737037cacdbe272d952b29c6d15..bdfa30aa516e6ec97ef2971486bc761923f4ded6

## おわりに

検索してみるとlessより断然検索が早くていい感じです!!
モチベーションがさらに上がってきました:v::v:

検索後の表示周りがバグりまくってるので、明日はそのあたりを整えたいと思います。

今日の感想としては、terminalさわる系のツールはprintデバッグ派の自分には動作確認が大変。
いい感じな方法ないのかな。。

まぁ悩みつつ、テンション上げつつ明日もやっていこうと思います。