goplsでworkspace/symbolが使いたい

現状の最新版(gopls/v0.2.2-pre2)では未実装。

daisuzu.hatenablog.com

を書いていたら欲しくなったので第1回 ゴリラ合宿で実装したのがコチラ。

github.com

symbolの探索はServersessionViews()から Snapshotを取ってきて、
KnownPackages()CompiledGoFiles()Parse()して*ast.Fileを引っ張り出して、
あとは地道にWorkspaceSymbolParamsにマッチするものを探していくようにした。

ただ、WorkspaceSymbolParamsの仕様*1がよくわかっておらず、実装の仕方も上記のやり方で良かったのか全然わかっていない。
また、最初は素直にスライスを作るように実装してみたところ、Vimから:tselectなどを実行すると

  • タグが重複したり、
  • 実行するたびに順番が変わったり、

するので、
重複を排除した上で、Vimで指定した文字列に近く、現在のパッケージに近いものを優先するようにしてみた。

とりあえず自分用としては使えそうなので色々と試してみることにする。

*1:特にQuery

VimのtagfuncでLSPを使う

この記事はVim Advent Calendar 2019の6日目の記事です。

今年の4月にv8.1.1228tagfuncという機能が追加されました。
こちらは:tag:tselectなどのタグ系コマンド*1を実行した時、tagsファイルを検索する代わりに呼ばれる関数を設定するためのオプションです。

設定する関数の形式としては次のようなものです。

" pattern: タグ検索中に使用されたタグ識別子
" flags: 関数の挙動を制御するためのフラグのリスト
"   'c' -> ノーマルモードのコマンドで呼び出された
"   'i' -> インサートモードのタグ補完で呼び出された
" info: 以下の情報を持つ辞書
"   {
"     'buf_ffname': 'フルファイル名',
"     'user_data': 'カスタムデータ文字列',
"   }
function! MyTagFunc(pattern, flags, info)
  " タグ情報のリストを返す
  " もしくはv:nullを返すとtagsファイルが使われる
  return [
        \  {
        \    'name': 'タグ名',
        \    'filename': 'タグが定義されているファイル名',
        \    'cmd': 'ファイル内のタグを見つけるためのExコマンド',
        \    'kind': 'タグの種類(Optional)',
        \    'user_data': 'カスタムデータ文字列(Optional)',
        \  },
        \]
endfunction

ヘルプにはtaglist()の結果を並べ替える関数が例として記載されていますが、LSPを使ってタグ情報のリストを生成できればタグジャンプがすごく便利になりそうです。

VimConf 2019ではsettagstack()を使ってタグスタックを直接操作する方法を紹介しましたが、こちらは、

  • タグ系のコマンドを使うために、
    • キーマップの変更が必須
    • Exコマンドはユーザ定義コマンドを定義する必要がある
  • Vim scriptでタグスタックの管理をしなければならない

といったものでした。

これに対し、tagfuncはオプションを設定するだけで、

  • 標準のキーマップ、Exコマンドがそのまま使える
    • タグ補完の時にも使える
  • タグスタックの管理をVim本体に任せられる

と、すごく良さそうです。

試しに任意の文字列で検索するworkspace/symbolvim-lspから呼び出してみます。

function! MyTagFunc(pattern, flags, info) abort
  let l:servers = filter(lsp#get_whitelisted_servers(),
      \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)')
  if len(l:servers) == 0
    echoerr 'not supported: workspace/symbol'
    return []
  endif

  " タグ補完の場合はa:patternの先頭に \< がつくので削除する
  " (サーバ側が正規表現に対応していないかもしれないので...)
  let l:query = a:flags =~# 'i' ?
      \ substitute(a:pattern, '^\\<', '', '') : a:pattern

  let l:ctx = {'result': []}
  for l:server in l:servers
    call lsp#send_request(l:server, {
        \ 'method': 'workspace/symbol',
        \ 'params': {
        \   'query': l:query,
        \ },
        \ 'sync': 1,
        \ 'on_notification': function('s:make_taglist', [l:ctx]),
        \ })
  endfor
  return l:ctx.result
endfunction

" ctx.resultにタグ情報を追加する
func s:make_taglist(ctx, data) abort
  for result in a:data.response.result
    call add(a:ctx.result, {
        \ 'name': result.name,
        \ 'filename': lsp#utils#uri_to_path(result.location.uri),
        \ 'cmd': string(result.location.range.start.line + 1),
        \ })
  endfor
endfunction

上記関数をset tagfunc=MyTagFuncで設定すると、ctagsとほぼ同じ使い勝手のタグジャンプができるようになります。

ただ、workspace/symboltextDocument/definitionなどのようなカーソル位置を起点に対象を探すメソッドとは異なり、100%目当ての場所にジャンプできるとは限りません。

確実にジャンプしたい時はtextDocument/definitionを呼べるような実装にできれば良いのですが、MyTagFuncの引数からだと、それがExコマンドの引数に指定したタグ名なのか、カーソル位置のキーワードなのかを判別できないので、メソッドの呼び分けをするのは非常に困難です。

そのため、tagfuncに設定する関数で全部をやるのではなく、

  • CTRL-]CTRL-W ]textDocument/definition*2を呼ぶ
    • 定義元にジャンプしたい
  • それ以外はtagfuncからworkspace/symbolを呼ぶ
    • 同名の別メソッド/関数やインタフェースの実体などを探したい

というように、用途に応じて使い分けるのがオススメです!

結局settagstack()tagfuncを両方使うことに。。。

[2019/12/06 17:00追記]
flagsで判別可能でした。
@presukuさん、ありがとうございます!

tagfuncだけで完結させるにはMyTagFuncを次のようにします。

function! MyTagFunc(pattern, flags, info) abort
  let l:ctx = {'result': []}

  if a:flags ==# 'c'
    " ノーマルモード(CTRL-]など)の場合はカーソル位置の定義に飛ぶ
    let l:method = 'textDocument/definition'
    let l:servers = filter(lsp#get_whitelisted_servers(),
        \ 'lsp#capabilities#has_definition_provider(v:val)')
    let l:ctx.pattern = a:pattern
    let l:params = {
        \   'textDocument': lsp#get_text_document_identifier(),
        \   'position': lsp#get_position(),
        \ }
  else
    " Exコマンド(:tag {name}など)やタグ補完の場合はa:patternで探す
    let l:method= 'workspace/symbol'
    let l:servers = filter(lsp#get_whitelisted_servers(),
        \ 'lsp#capabilities#has_workspace_symbol_provider(v:val)')

    " タグ補完の場合はa:patternの先頭に \< がつくので削除する
    " (サーバ側が正規表現に対応していないかもしれないので...)
    let l:query = a:flags =~# 'i' ?
        \ substitute(a:pattern, '^\\<', '', '') : a:pattern
    let l:params = {
        \   'query': l:query,
        \ }
  endif

  if len(l:servers) == 0
    echoerr 'not supported: ' . l:method
    return []
  endif

  for l:server in l:servers
    call lsp#send_request(l:server, {
        \ 'method': l:method,
        \ 'params': l:params,
        \ 'sync': 1,
        \ 'on_notification': function('s:make_taglist', [l:ctx, l:method]),
        \ })
  endfor
  return l:ctx.result
endfunction

" ctx.resultにタグ情報を追加する
func s:make_taglist(ctx, method, data) abort
  for result in a:data.response.result
    if a:method ==# 'workspace/symbol'
      let l:name = result.name
      let l:location = result.location
    else
      let l:name = a:ctx.pattern
      let l:location = result
    endif
    call add(a:ctx.result, {
        \ 'name': l:name,
        \ 'filename': lsp#utils#uri_to_path(l:location.uri),
        \ 'cmd': string(l:location.range.start.line + 1),
        \ })
  endfor
endfunction

ただ一番利用頻度の高いgoplsworkspace/symbolに対応していないのでまだ常用できていません。。。

VimConf 2019に行ってきた

今年もVimConf 2019に行ってきました。

今回はテーマが “how to be more productive with Vim?” ということもあったのか、比較的Vim暦が浅い人の登壇が多かったような気もしますが、その割には発表内容が素晴らしいものばかりだったので、やっぱりVimを使う人は(良い意味で)ヤバい人ばっかりだなと思いました。

毎年のことですが、運営のみなさま大変お疲れ様でした。本当にありがとうございます。
おかげでとても楽しく過ごすことができました。


発表メモ

Vim Renaissance by Prabir Shrestha

愛用しているvim-lspの作者によるキーノート。
色々なエディタ・IDEプログラミング言語があった状況からLSPが登場し、そして未来への革命へ、的な。
乱立していると辛いとか、選択肢を広げるようにしているところとか、わかる部分もあれば自分の英語力だとちゃんと理解できているのか不安になるところもあったり...
多くの人が使えるようにするのは大事なことですけど、そこにもトレードオフがあったりするので難しいところですよね。

We can have nice things by Justin M. Keyes

2つ目のセッションはneovim作者によるキーノート。
neovimはあんまり使っていないんですが、その思想とか実装を直接聞くことができてすごく興味深かったです。
UI周りに関する作り込みとかもそうですけど、特に 「Legacy paradox」 の「表面化にある価値のあるモノ」についてはなるほどなーと思いました。

Your Vim is Only for You by mopp

午後の最初のセッション。
自分のVimを改善していくフローは今までそうしてきたことも多々あるし、今後もそうありたいと思っていることなのでとても共感できました。

Grown up from Vim User to Vim plugin developer side by IK

Vimを使い始めてからvim-jpに参加するまでの流れ。
課題を自分が直すしかない!みたいなところは思い当たることがあったり...
最後の「The next OSS contributer is YOU!!!」はとてもやる気が湧き起こる言葉ですね。

Usage and manipulation of the tag stack by daisuzu

こちらを発表してきました。 speakerdeck.com

make test by m-nishi

Vim本体のテストについて。
ターミナルが小さくてfailするのはやったことありますw
自分が以前blogに書いたことを見てくれていたりして、そういったことがこの発表に繋がっているのかなと思うとなんだか嬉しいです。

My Vim life by gorilla0513

1年前にVimを使い初めてやってきたことの軌跡。
ゴリラ.vimとか本とか、近くで関わっていて良く知ってる事もありますけど、ゴリラさんの情熱と行動力は本当にスゴいし尊敬しています!

Using Vim at Work! by Danish Prakash

心理学 + Vimといったとても興味深い発表でした。
UIについては(自分が思うように使えれば良いや、とか思ったりして)あんまり深く考えなかったりもしたけど、

  • キーボードよりマウスの方が効率的な事がある
  • テキストよりもビジュアル情報の方が6万倍速い

と言われると確かになーとなりました。

Let's Play with Vanilla Vim by Hezby Muhammad

プラグインを使わないVanilla Vimの話。
Vimを使いこなすと基本機能だけでもなんとかなってしまう事も多々あるんですよね。 :find とか補完とかタグジャンプとか。
1つ前の発表でも少し出てきましたが、まさか他の発表でタグジャンプがあるとは思っていませんでした。
そして :normalI とか A とか使うやり方はやった事なかったです。

13 Vim plugins I use every day by Tatsuhiro Ujihisa

毎日使っているプラグインの紹介。
ライブコーディングでサーバをパパッと作っていましたが、流れるようなVim捌きが本当に凄かったです!

My dark plugins development history ~ over 10 years ~ by Shougo

shougo wareの開発の歴史。
最近は使わなくなってしまいましたが...第二世代くらいまではすごくお世話になっていました!
今回で最後の登壇になるかもしれないというのがちょっと信じられません。
来年でなくてもまたいつか発表していただければなと思います。

素のVimでGoを書く時のテクニック

gorillavim.connpass.com

にて、

外部ネットワークに繋がらない環境でvimgoしか使えない

という縛りでライブコーディングをしてきました。

実際にそういう環境があるのか?という話はさておき、そのような状況でも以下を駆使してそこそこコードが書けます。

  1. :%!gofmtでコードを整形できる
  2. :r! go docの結果を適当なバッファに出力する
    • Vim内でドキュメントが読める
    • ドキュメントの内容をCTRL-NCTRL-Pで補完できる
  3. :set path+=$GOROOT/srcで標準パッケージを検索できるようにする
    • :findなどで標準パッケージのディレクトリやファイルを開ける
    • :grep*1 で色々と探せる
    • 開いたファイルはただ見るだけではなく、補完の対象にもできる

作ったものはGET /fizzbuzz/:numberにアクセスするとFizzBuzzを返すというHTTPサーバです。

package main

import (
    "net/http"
    "strconv"
    "strings"
)

func fizzbuzz(w http.ResponseWriter, r *http.Request) {
    p := strings.TrimPrefix(r.URL.Path, "/fizzbuzz/")
    if p == "" {
        http.NotFound(w, r)
        return
    }

    n, _ := strconv.Atoi(p)
    if n < 1 {
        http.NotFound(w, r)
        return
    }

    switch {
    case n%5 == 0 && n%3 == 0:
        w.Write([]byte("FizzBuzz"))
    case n%5 == 0:
        w.Write([]byte("Buzz"))
    case n%3 == 0:
        w.Write([]byte("Fizz"))
    default:
        w.Write([]byte(p))
    }
    w.Write([]byte("\n"))
}

func main() {
    http.HandleFunc("/fizzbuzz/", fizzbuzz)
    http.ListenAndServe(":8080", nil)
}

ただ当日は1.のgofmtしか使いませんでした。
というのもスパルタンなVimmerに憧れており、元から自動補完は使っておらず、オムニ補完にもそこまで依存しないようにしていたので、このくらいのものであれば無くても特に困らなかったからです。

まあ常に役立つものではありませんが、覚えておくと何かの時に便利かもしれません。

*1:grepコマンドがない場合は:vimgrep

第1回 ゴリラ.vimが開催されました

2/18(月)にゴリラ.vim #1が開催され、スタッフ兼発表者として参加してきました。

gorillavim.connpass.com

スタッフになったのはvim-jpのslackでゴリラさんこと@gorilla0513さんがVimの勉強会をやろうとしているのを見かけ、これは前々から思っていた渋谷でVim勉強会をするチャンスなのでは!?と会場提供を申し出たのがきっかけです。

そして当初は発表するネタが思い浮かばなかったのでスタッフに専念するつもりでいたのですが、少し経っても発表者枠が空いており、かつメンバーがVimConf 2018のスピーカーばかりという状態だったため、これは自分も何か話した方が良いんじゃないかと考え、ここ最近使ったgotypego-treeを基にVimと外部コマンドという発表をしてきました。

ちなみに発表・デモは@thincaさんのshowtimequickrunを使いました。

gorillavim.connpass.com

全体の発表内容としては、短い時間ながらも初心者向けから上級者向けまであり、とても充実したものとなっていました。
特に前半の発表は自分にもそういう時期があったんだよなぁ、と非常に懐かしい気持ちになりました。

その後、一通り発表が終わった後は懇親会となり、各々がVimトークでとても盛り上がっていました。

開催前からこのイベントは盛り上がる...という予感はありましたが、予想以上に盛り上がり、スタッフとしても発表者としても参加者としても、嬉しい限りです。

ということで、今回ご参加いただいた皆様、ありがとうございました。
次回も是非よろしくお願いします。
(#2は少し枠が増えています!)

gorillavim.connpass.com

Vimの折畳に対応したtreeコマンド

golang.tokyo #21DevQuizはGoでtreeコマンドを作成するというものでした。

treeコマンドといえば、以前Vimでファイル一覧をツリー表示するためのtree.vimというプラグインを作る時に使いました。

github.com

このプラグインディレクトリにマーカーをつけることでサブディレクトリを階層ごとに折り畳んで表示します。
当時、マーカーを(Go製の)自作コマンドでつけることも検討しましたが、実装が非常に面倒くさそうだったのでVim scriptでtreeコマンドの結果をパースすることにしました。

そんな事情があったため、せっかくなのでDevQuizをやりつつ、追加機能としてVimの折畳に対応した出力形式も実装してしまおう!と思って作ったのがgo-treeです。*1

こちらはコマンドラインフラグに-Vをつけると以下のような出力になります。

$ go-tree -V github.com/daisuzu/tree.vim
github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
github.com/daisuzu/tree.vim/autoload/{{{
  github.com/daisuzu/tree.vim/autoload/tree.vim}}}
github.com/daisuzu/tree.vim/ftplugin/{{{
  github.com/daisuzu/tree.vim/ftplugin/tree.vim}}}
github.com/daisuzu/tree.vim/plugin/{{{
  github.com/daisuzu/tree.vim/plugin/tree.vim}}}
github.com/daisuzu/tree.vim/syntax/{{{
  github.com/daisuzu/tree.vim/syntax/tree.vim}}}

コマンド自体はVimから実行しても良いし、

:r! go-tree -V [DIR]

シェルで実行した結果をVimに流し込んでもOKです。

# -cでfoldmethod(fdm)をmarkerにする
go-tree -V [DIR] | vim - -R -c 'setl fdm=marker'

# モードラインでfoldmethod(fdm)をmarkerにする
echo -e "vim: fdm=marker\n$(go-tree -V [DIR])" | vim - -R

そうするとgithub.com/daisuzu/tree.vimVimで以下のように表示されます。

github.com/daisuzu/tree.vim/../
github.com/daisuzu/tree.vim/./
github.com/daisuzu/tree.vim/README.md
+--  2 lines: github.com/daisuzu/tree.vim/autoload/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/ftplugin/-----------------------------
+--  2 lines: github.com/daisuzu/tree.vim/plugin/-------------------------------
+--  2 lines: github.com/daisuzu/tree.vim/syntax/-------------------------------
~
~
~
~

さらに、コマンド単体で動作するのでjob機能を使った非同期実行も簡単です。

:vnew | setl fdm=marker | call job_start('go-tree -V', {'out_io': 'buffer', 'out_buf': bufnr('%')})

f:id:daisuzu:20190128202818g:plain

このように、github.com配下に大量のファイルがあるような場合でもVimをブロックすることなく操作を継続することができます。

現在はvimrcにAtreeコマンドを定義してgo-treeを常用しています。
filetypeをtree.vimのものにすることで、そのまま表示するよりも少し見栄えが良くなっています。

command! -nargs=? -complete=dir -count -bang -bar ATree
      \ call s:async_tree(<q-args>, <count>, <bang>0, <q-mods>)

function! s:async_tree(dir, depth, bang, mods) abort
  let cmd = 'go-tree -V ' . g:tree_options
  if a:bang
    " コマンドに!をつけたら隠しファイルも表示する
    let cmd .= ' -a'
  endif
  if a:depth > 0
    let cmd .= ' -L ' . a:depth
  endif
  " cd後もgfできるようにディレクトリ名を絶対パスにする
  let cmd .= ' ' . fnamemodify(a:dir != '' ? a:dir : '.', ':p:h')

  execute a:mods . ' new'
  setfiletype tree

  let bufnr = bufnr('%')
  call job_start(cmd, {
        \   'out_io': 'buffer',
        \   'out_buf': bufnr,
        \   'exit_cb': {channel, msg -> s:goto_first(bufnr)},
        \ })
endfunction

" オリジナル版はカーソル位置が先頭行になるので同じ挙動にする
function! s:goto_first(bufnr)
  if bufnr('%') != a:bufnr
    " バッファを移動していたら何もしない
    return
  endif
  if line('.') != line('$')
    " 行を移動していたら何もしない
    return
  endif
  normal gg
endfunction

予想通り実装は面倒でしたが、Vimをより快適に使えるようになりました!

*1:人が見ることはあまり考えてませんでした...

Vimのexecute()と組み合わせて便利なコマンド

この記事はVim Advent Calendar 2018の13日目の記事です。

先月に開催されたVimConf 2018で、Migrating plugins to standard featuresというタイトルで発表してコマンドの結果をバッファに表示する方法を紹介しました。
その中で解説しきれなかったテクニックとして、以下のようなコマンドを定義しておくとL <コマンド>*1のようにして簡単に任意のコマンドの結果をバッファに表示することが出来るようになります。

command! -bar ToScratch
      \ setlocal buftype=nofile bufhidden=hide noswapfile

command! -nargs=1 -complete=command L
      \ <mods> new | ToScratch |
      \ call setline(1, split(execute(<q-args>), '\n'))

当日はプラグイン一覧やMRUを表示する用途として、

  • :scriptnames
  • v:oldfiles(:oldfilesでも良い)

を紹介しましたが、簡単に使えるなら他にも便利なコマンドがあるんじゃないかと思って調べてみました。
なお、対象は:exusageから

  • 引数不要
  • ファイルタイプやカーソル位置、バッファの状態などに依存しない
  • 結果が複数行になる

コマンドのみとしています。(収拾ががつかなくなりそうなので...)

定義系

各種定義を一覧表示するのは何かと便利そうです。
年末にvimrcを掃除する際などにも大活躍するんじゃないでしょうか?

  • :augroup ... 自動コマンドのグループ
  • :autocmd ... 自動コマンド
  • :abbreviate ... 短縮入力
    • :noreabbrevや、先頭にc, iを付けても良い
  • :command ... コマンド
  • :function ... 関数
  • :highlight ... ハイライト
    • :runtime syntax/hitest.vimだと実際にハイライトされる
  • :let ... 変数
  • :map ... マップ
    • :noremapや、先頭にc, i, l, n, o, s, t, v, xを付けても良い
  • :menu ... メニュー
    • :noremenuや、先頭にa, c, i, n, o, s, tl, v, xを付けても良い

開いているもの

gfで開く用途には向きませんが、どちらも大量に開いている時はバッファに表示されると便利そうです。

  • :buffers ... バッファ
    • または:files, :ls
  • :tabs ... タブ

各種操作結果

こちらも大量に表示されるような状況では便利そうです。

  • :registers ... レジスタ
    • または:display
  • :marks ... マーク
  • :messages ... メッセージ
  • :history ... コマンド履歴

Vimの情報

Vim関連の様々な情報はバッファに表示して見たいというケースはあまりないかもしれません。
個人的には代替の手段の方が使い勝手が良いと思っています。

  • :compiler ... コンパイラ用設定
  • :digraphs ... ダイグラフ
    • :h digraph-tableの方が見やすいかもしれない
  • :set ... 既定値と異なるオプション
    • :setglobal:setlocalもある
    • :optionsでも良いかもしれない
  • :version ... バージョン番号やその他の情報

デバッグ向け

Vim script開発者向け。

QuickFix

切り替える際に表示するだけならわざわざバッファに表示する必要はないかもしれません。

  • :chistory ... grepやmakeなど、quickfixリストの履歴
    • locationリストの場合は:lhistory
  • :clist ... quickfixリストの内容
    • locationリストの場合は:llist

移動系

こちらも一時的に表示するだけならバッファに表示する必要はなさそうです。
※実際に移動できるマッピングなどがあれば...*2

  • :jumps ... ジャンプリスト
  • :tags ... タグスタック
  • :tselect ... タグ

その他

ということで最後はこちら。

  • :smile ... 「ついに見つけたんだね、セイバー!」
                            oooo$$$$$$$$$$$$oooo
                        oo$$$$$$$$$$$$$$$$$$$$$$$$o
                     oo$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o         o$   $$ o$
     o $ oo        o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$o       $$ $$ $$o$
  oo $ $ "$      o$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$o       $$$o$$o$
  "$$$$$$o$     o$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$o    $$$$$$$$
    $$$$$$$    $$$$$$$$$$$      $$$$$$$$$$$      $$$$$$$$$$$$$$$$$$$$$$$
    $$$$$$$$$$$$$$$$$$$$$$$    $$$$$$$$$$$$$    $$$$$$$$$$$$$$  """$$$
     "$$$""""$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$
      $$$   o$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     "$$$o
     o$$"   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$       $$$o
     $$$    $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" "$$$$$$ooooo$$$$o
    o$$$oooo$$$$$  $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$   o$$$$$$$$$$$$$$$$$
    $$$$$$$$"$$$$   $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$     $$$$""""""""
   """"       $$$$    "$$$$$$$$$$$$$$$$$$$$$$$$$$$$"      o$$$
              "$$$o     """$$$$$$$$$$$$$$$$$$"$$"         $$$
                $$$o          "$$""$$$$$$""""           o$$$
                 $$$$o                                o$$$"
                  "$$$$o      o$$$$$$o"$$$$o        o$$$$
                    "$$$$$oo     ""$$$$o$$$$$o   o$$$$""
                       ""$$$$$oooo  "$$$o$$$$$$$$$"""
                          ""$$$$$$$oo $$$$$$$$$$
                                  """"$$$$$$$$$$$
                                      $$$$$$$$$$$$
                                       $$$$$$$$$$"
                                        "$$$""""

*1:Listの略

*2:i.e. Unite