Telescope.nvimの実装を読みながら自分の設定をカスタマイズする

October 1, 2023

ふと思い立ったので、自分が使っているプラグインのカスタマイズを雑に紹介したいと思います。 今回は Telescope.nvim のハイライトのカスタマイズを紹介します。

Telescope.nvim

Telescope.nvim はNeovimのファジーファインダープラグインです。 多くの組み込みソースを提供し幅広い操作をサポートしているので、このプラグインを導入するだけでも多くのことができるようになります。 この記事では、インストール手順や基本的な使い方は説明しませんが、実際に行ったカスタマイズや、そのためにコードリーディングした箇所を簡単に紹介します。 今回行ったカスタマイズはdotfilesの一部として公開しています。

用語の定義

カスタマイズを紹介する前に、用語の定義をしておきます。 基本的にはREADMEやWIKIなどのドキュメントおよびソースコード内のコメントにおける説明で用いられているものを使用します。 このセクションで明示していない場合でも、説明が難しい場合はソース内で用いられている変数および関数の名前をそのまま使用することもあります。

Finder

Fuzzy search の対象となるオブジェクトの集合を返すものです。 ファイルやdiagnosticsの一覧(を返すもの)などが挙げられます。

Picker

検索結果が表示される部分です。 Finderから表示内容を受け取り、ユーザからの入力に応じて表示内容の絞り込みを行います。

Entry

Pickerに表示される各アイテムです。 Finderから返されたものを絞り込み、Highlightなどを適用したものがEntryの集合になります。

Previewer

Picker内で選択されたEntryのプレビューを表示します。 設定でon/offが切り替えられるほか、ハイライトも適用できます。

Previewer

Telescope.nvimにはカーソルのある検索結果のプレビューを表示することができるのですが、このプレビューにはシンタックスハイライトを適用することもできます。 視認性が向上するため是非とも適用しておきたい一方、仮にサイズの大きいファイルが検索結果に出現した場合に動作が詰まってしまうことも考えられます。

これに対しては wiki に記載されているように、特定のサイズを超えるファイルを表示しないようにしたり、最初から特定の位置までファイルを読み込むことが可能です。

Picker

ファイル名が検索対象となるとき、検索結果に表示されるのはもちろんファイル名なのですが、全て同じ文字色だと見づらい場合があります。 特にディレクトリ構成が複雑であり、深い位置にファイルが位置している場合などは顕著だと思います。

そのため、ディレクトリ名とファイル名に別のハイライトを当てるようにしたいと思いました。 Telescope.nvimでは各pickerに渡すオプションにentry_makerを指定することができ、Entryの内容を自由にカスタマイズできます。

ソース上のコメントによるとEntryMaker には最低限以下の3つの要素が必要になります。

The Entry Maker at least defines

  • value: "raw" result of the finder
  • ordinal: string to be sorted derived from value
  • display: line representation of entry in results buffer

今回は見た目をカスタマイズしたいので、display をカスタマイズします。 displayの正体はentryを受け取り、実際に表示するテキストとハイライト情報を返す関数(もしくは文字列)です。

ここで返されるハイライト情報は{ { start_col, end_col }, hl_group } のような構造になっています。 したがって、各Entryには部分的に別々のハイライトを適用できます。 これを利用して、ディレクトリとファイル名に別のハイライトを適用するようにします。

Telescope.nvimではPicker内の各Entryにおけるdisplayを作成するための関数が提供されています。 これを用いてカスタムハイライトを適用できるdisplayを作成します。

local displayer = require("telescope.pickers.entry_display").create({
    separator = "",
    items = { ... },
})

Entryは見かけ上は文字列になります。 文字列のjoin関数をイメージしてもらえるとわかりやすいでしょうか。 itemsにEntryに表示する内容を順に指定します。 この指定を別々のitemとすることで、別のハイライトを指定することができるようになります。 separatorは各itemsを結合する際の区切り文字です。

itemは表示する文字の幅を指定することができます。 数値を指定することで、渡された内容がその文字数になるようにトリミングされます。 nilを指定することで、入力された文字列をそのまま表示することができます。 残りの部分すべてを使用するように設定することもでき、その場合はremaining=trueを指定します。

今回はパスのディレクトリ部分とファイル名部分に別々のハイライトを指定するのが目的です。 したがってディレクトリ名とファイル名部分は別々のitemsとして定義します。 その際に見た目が不自然にならないようにseparatorには空文字を指定します。

こうして生成されたdisplayを用いて各Entryを生成します。 Finderから渡された情報、実質的には素のEntryですが、これをよしなに変換し先程生成したdisplayを介することでカスタマイズしたEntryを出力します。

Telescope.nvimではFinderの結果からEntryを生成する関数も提供されています。 カスタマイズしたいPickerが使用しているFinderの種類に応じてこれらの関数を使い分けます。 ファイルの一覧をソースとしているならgen_from_fileを、vimgrepの結果をソースとしているならgen_from_vimgrepをといった具合です。 これらの関数から生成されたEntryのdisplayを上書きすることで、ハイライトをカスタマイズするのが簡単で良いと思います。 以下に一例を挙げますが、これ以外にもgitやLSPなど様々なソースに対応しています。

  • gen_from_string
  • gen_from_file
  • gen_from_vimgrep
  • gen_from_quickfix
  • etc...

これらの関数を使用することで、デフォルトの状態のEntryが作成されます。 ソースから渡された入力からファイルパスを受け取り、ディレクトリ名とファイル名に分け先程のdisplayに渡します。 参考までに、vimgrepの結果を表示するPickerをカスタマイズする際のコードを載せます。

return function(line)
    -- vimgrepの結果からデフォルト状態のEntryを生成する関数を取得
    local entry_maker = make_entry.gen_from_vimgrep(opts or {})
    -- Finderの出力1行分からデフォルト状態のEntryを生成
    local entry = entry_maker(line)

    -- entryのdisplayをカスタマイズしたハイライトを適用したもので上書きする
    entry.display = function(et)
        -- vimgrepの結果を分割
        local filepath, row, col, text = get_path_and_pos(et.value, ":")
        -- ファイル名とディレクトリ名を分割
        local filename, directory_path = get_path_and_tail(filepath)
        -- ファイルタイプからアイコンを取得
        local icon, iconhl = utils.get_devicons(filename)

        return displayer({
            { icon, iconhl },
            spacer,
            { directory_path .. "/", "Directory" },
            { filename, nil },
            separator,
            { row, "Number" },
            separator,
            { col, "Number" },
            spacer,
            { text, "Comment" },
        })
    end

    -- entryを返す
    return entry
end

Spacer や Separator は明示的にwidth=1としても良いですし、好みに応じてそれ以外の値を指定してもいいです。

スクリーンショット

このような見た目になります。

この記事で紹介したもの以外にも、ファイル名を切り出して表示することで目的のファイルを見つけやすくするカスタマイズなども適用しています。

特にindex or initファイルと細かく別れたモジュールが配置されているような場合には少し見やすくなると思います。 アイディアは issue から拝借しました。 詳細なコードも紹介されているので、気になった方は参考にしてみてください。