Vim上でGitHubのファイルツリーを表示する話
初めに
先日、gh.vimにファイルツリー機能を実装しました。
Vim上でリポジトリのファイルツリーを見ることができる機能です。
その周りの話をしていきます。
やり方
:30vnew gh://:owner/:repo/:branch|:tree_shaを実行すると、ファイルツリーを開けます。
:repoの後はブランチ名、もしくはコミットハッシュを指定できます。
また、gheでカーソル上にあるファイルの中身を見ることができます。

実装
GitHubにはTreesAPIが用意されていて、それをたたくとファイルとディレクトリの情報を取得できます。
しかし、このレスポンスのではデータ構造が入れ子になっておらず、pathにリポジトリのルートディレクトリからのパス情報などが配列になっています。
たとえば次のディレクトリ構成だった場合
.
├── README.md
├── doc
│ └── gh.txt
└── plugin
└── gh.vim
これが次のようなレスポンスになります。
{
"sha": "xxx",
"url": "xxx",
"tree": [
{
"path": "README.md",
...
},
{
"path": "doc",
...
},
{
"path": "doc/gh.txt",
...
},
{
"path": "plugin",
...
}
{
"path": "plugin/gh.vim",
...
}
]
}
ツリーを表現するにはフラットなデータ構造ではなく、ネストしたデータ構造のが都合よいので、これらのpathを元に次のようなデータ構造に変換する必要があります。
{
"name": "gh.vim",
"path": "gh.vim",
"children": [
{
"name": "README.md",
"path": "gh.vim/README.md"
},
{
"name": "doc",
"path": "gh.vim/doc",
"children": [
{
"name": "gh.txt",
"path": "gh.vim/doc/gh.txt"
}
]
},
{
"name": "plugin",
"path": "gh.vim/plugin",
"children": [
{
"name": "gh.vim",
"path": "gh.vim/plugin/gh.vim"
}
]
}
]
}
gh.vimではs:make_nodeという関数で、1ファイルずつ、上記のデータ構造を構築しています。
function! s:make_node(tree, file) abort
let paths = split(a:file.path, '/')
let parent_path = join(paths[:-2], "/")
let tree = a:tree
let item = {
\ 'name': a:file.type is# 'tree' ? paths[-1] .. '/' : paths[-1],
\ 'path': a:file.path,
\ 'info': a:file,
\ 'markable': 1,
\ }
if a:file.type is# 'tree'
let item['children'] = []
let item['state'] = 'close'
endif
if has_key(b:tree_node_cache, parent_path)
call add(b:tree_node_cache[parent_path], item)
return
endif
if exists('tree.children')
for node in tree.children
call s:make_node(node, a:file)
endfor
endif
if tree.path is# parent_path
call add(a:tree.children, item)
let b:tree_node_cache[parent_path] = a:tree.children
endif
endfunction
基本的な処理の流れは次です。
-
tree(親要素)と、file(GitHubから取得した1ファイルの情報)を受け取る -
fileのpathから親パスを取得 -
treeに追加するデータitemを作成 -
fileがディレクトリ(a:file.type is# 'tree'の部分)の場合はtreeにchildrenを追加 - すでに
treeにchildrenがある場合は、子要素の数だけ再帰処理 -
treeのpathがfileの親パスと一致した場合、tree.childrenにfileを追加 - これをAPIレスポンスの
treeの配列分繰り返す
ただ、このロジックではファイルの数とパスの深さと比例して再帰の回数がえげつない回数になるので、ファイル数が3000個くらいあるプロジェクトだとかなりと処理に時間がかかります。
そこですでに作成したノードをキャッシュして子要素の親パスがキャッシュにあったら、
キャッシュしたデータに追加することでパフォーマンスを改善しました。
それが次の部分のコードになります
" キャッシュがあればキャッシュに要素を追加
if has_key(b:tree_node_cache, parent_path)
call add(b:tree_node_cache[parent_path], item)
return
endif
...
if tree.path is# parent_path
call add(a:tree.children, item)
" 作成済みの要素をキャッシュ
let b:tree_node_cache[parent_path] = a:tree.children
endif
最後に
キャッシュ戦略でいくぶんパフォーマンスが改善されたとはいえ、
golang/goといった巨大なプロジェクトのファイルツリーを開くのは時間がかかってしまいます。
よりよいロジックがないか、年末あたりに模索してみたいと思います。
gh.vim自体はファイルツリー以外にも、プロジェクトやGitHub Actionsをツリーでみたりできますので、
興味ある方は一度触ってみてください。Vim/Neovimともに動きます。
Discussion