Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17735] ファイルの概要

misc/vim/autoload/go/complete.vim ファイルは、VimエディタにおけるGo言語開発のための補完機能を提供するVimscriptファイルです。具体的には、Go言語のパッケージ名や、本コミットで追加されたパッケージメンバー(関数、変数、型など)の補完を担っています。Vimのautoloadディレクトリに配置されているため、必要に応じて遅延ロードされ、Vimの起動時間を短縮する役割も果たしています。

コミット

misc/vim: Autocompletion for :Godoc command

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/45b830ed319ee1d82ff9d41c35d6145ab7ecbb82

元コミット内容

misc/vim: Autocompletion for :Godoc command

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/14259043

変更の背景

このコミットが行われる以前は、Vimの:Godocコマンド(Go言語のドキュメントを表示するコマンド)において、パッケージ名の補完は可能でしたが、そのパッケージ内のメンバー(関数、変数、型など)の補完は提供されていませんでした。例えば、:Godoc fmt. と入力した際に、PrintfErrorfといったfmtパッケージのメンバーが補完候補として表示されないため、ユーザーは手動で完全な名前を入力する必要がありました。

この変更の背景には、開発者の生産性向上という明確な目的があります。Vimのような強力なエディタにおいて、コマンドの引数補完は非常に重要な機能であり、特にドキュメント参照のような頻繁に行われる操作においては、その利便性が大きく影響します。本コミットは、:Godocコマンドの使い勝手を向上させ、Go言語開発者がよりスムーズにドキュメントを参照できるようにするために導入されました。これにより、ユーザーはタイプミスを減らし、より迅速に目的のドキュメントにアクセスできるようになります。

前提知識の解説

Vim (Vi IMproved)

Vimは、プログラマーに広く利用されている高機能なテキストエディタです。コマンドラインインターフェースで動作し、キーボード操作のみでほとんどの編集作業を完結できる点が特徴です。モードの概念(ノーマルモード、挿入モード、ビジュアルモードなど)を持ち、効率的なテキスト編集を可能にします。Vimは非常にカスタマイズ性が高く、Vimscriptと呼ばれる独自のスクリプト言語を用いて機能を拡張できます。

Vimscript

Vimscriptは、Vimエディタの機能を拡張するために使用されるスクリプト言語です。Vimの設定ファイル(.vimrc)やプラグインの記述に用いられます。関数定義、変数、条件分岐、ループなどの基本的なプログラミング構造をサポートしており、Vimの内部機能や外部コマンドとの連携も可能です。本コミットでも、Vimscriptで記述された関数が補完ロジックを実装しています。

Autocompletion (Vimにおける補完機能)

Vimにおける補完機能は、ユーザーが入力している内容に基づいて、関連する候補を提示する機能です。これにより、入力の手間を省き、タイプミスを減らすことができます。Vimには、キーワード補完、ファイル名補完、辞書補完など、様々な種類の補完機能が組み込まれています。また、プラグインによってさらに高度な補完機能を追加することも可能です。コマンドライン補完は、Vimのコマンドモードでコマンドやその引数を入力する際に機能する補完です。

godocコマンド

godocは、Go言語に標準で付属するドキュメンテーションツールです。Goのソースコードからコメントを解析し、HTML形式やプレーンテキスト形式でドキュメントを生成・表示します。Goの標準ライブラリだけでなく、ユーザーが作成したパッケージのドキュメントも表示できます。例えば、godoc fmtと実行するとfmtパッケージのドキュメントが表示され、godoc fmt Printfと実行するとfmt.Printf関数のドキュメントが表示されます。このツールは、Go言語のコードベースを理解し、APIの利用方法を調べる上で不可欠なものです。

:Godocコマンド (Vimプラグインにおけるコマンド)

Go言語のVimプラグイン(go.vimなど)によって提供される:Godocコマンドは、Vimエディタ内からgodocツールを実行するためのラッパーコマンドです。これにより、Vimを離れることなく、Goのドキュメントを参照できるようになります。通常、:Godoc <package_name>:Godoc <package_name> <member_name>といった形式で使用されます。

autoloadディレクトリ

Vimのプラグイン開発において、autoloadディレクトリは特別な意味を持ちます。このディレクトリに配置されたVimscriptファイル内の関数は、Vimの起動時に自動的にロードされるのではなく、その関数が初めて呼び出されたときにロードされます。これにより、Vimの起動時間を短縮し、必要な機能だけをオンデマンドでロードすることができます。本コミットで変更されたgo/complete.vimファイルもこのautoloadディレクトリ内にあり、補完機能が実際に必要になったときにロードされるようになっています。

技術的詳細

このコミットの主要な目的は、Vimの:Godocコマンドにおいて、Goパッケージのメンバー(関数、変数、型など)の自動補完機能を追加することです。この機能は、go#complete#PackageMembersという新しいVimscript関数によって実現されています。

go#complete#PackageMembers関数の役割

この関数は、指定されたGoパッケージのドキュメントをgodocコマンドから取得し、その内容を解析してパッケージメンバーの候補を抽出します。

  1. godocコマンドの実行: silent! let content = system('godoc ' . a:package) この行では、Vimscriptのsystem()関数を使用して、シェルコマンドgodoc <package_name>を実行しています。例えば、a:packagefmtであれば、godoc fmtが実行されます。silent!は、コマンドの出力をVimのメッセージ領域に表示しないようにするためのものです。content変数には、godocコマンドの標準出力が文字列として格納されます。

  2. エラーハンドリングと空コンテンツのチェック: if v:shell_error || !len(content) v:shell_errorは、直前のsystem()コマンドがエラーを返したかどうかを示すVimの組み込み変数です。!len(content)は、godocコマンドが何も出力を返さなかった場合をチェックします。これらの条件のいずれかが真であれば、補完候補は空のリスト[]を返します。

  3. godoc出力の解析: let lines = filter(split(content, "\n"),"v:val !~ '^\\s\\+$'") godocの出力は複数行のテキストなので、まずsplit(content, "\n")で各行に分割します。次に、filter()関数と正規表現'^\\s\\+$'を使って、空白文字のみで構成される空行を削除しています。これにより、意味のある行のみが残ります。

  4. パッケージメンバーの抽出: この部分が最も重要なロジックです。godocの出力形式に基づいて、パッケージメンバーを識別するための2つの正規表現が定義されています。

    • let mx1 = '^\\s\\+\\(\\S+\\)\\s\\+=\\s\\+.*' この正規表現は、constvarで定義された定数や変数を捕捉することを意図しています。例えば、const Error = errors.New("...")のような行からErrorを抽出します。\s\+は1つ以上の空白文字、\S\+は1つ以上の非空白文字、\(...\)はキャプチャグループです。
    • let mx2 = '^\\%(const\\|var\\|type\\|func\\) \\([A-Z][^ (]\\+\\).*' この正規表現は、constvartypefuncキーワードで始まる行から、その後に続く識別子(メンバー名)を抽出します。例えば、func Printf(...)からPrintfを、type MyType struct { ... }からMyTypeを抽出します。\%(...)は非キャプチャグループ、[A-Z]は大文字で始まる識別子を想定しています。

    これらの正規表現を使って、filter()map()関数を組み合わせ、linesから該当する行を抽出し、substitute()関数でメンバー名のみを抽出しています。抽出されたメンバー名はcandidatesリストに結合されます。

  5. 部分一致フィルタリング: return filter(candidates, '!stridx(v:val, a:member)') 最後に、抽出されたcandidatesリストを、ユーザーが入力した部分文字列a:memberでフィルタリングします。stridx(v:val, a:member)は、v:val(候補文字列)の中にa:member(ユーザー入力)が最初に現れるインデックスを返します。もしa:memberv:valの先頭に存在すれば0を返します。!stridx(...)は、a:memberv:valの先頭に一致する場合に真となります。これにより、ユーザーが入力した部分文字列で始まるメンバーのみが補完候補として返されます。

go#complete#Package関数の変更

既存のgo#complete#Package関数は、:Godocコマンドの引数を解析し、補完のコンテキストを判断する役割を担っています。このコミットでは、この関数が以下のように変更されました。

   let words = split(a:CmdLine, '\s\+', 1)
   if len(words) > 2
-    " TODO Complete package members
-    return []
+    " Complete package members
+    return go#complete#PackageMembers(words[1], words[2])
   endif

以前は、コマンドライン引数が2つ以上ある場合(例: :Godoc fmt P)、パッケージメンバーの補完は「TODO」として実装されておらず、空のリストが返されていました。この変更により、len(words) > 2の条件が満たされた場合、つまりユーザーがパッケージ名に加えてさらに何かを入力している場合に、新しく定義されたgo#complete#PackageMembers関数が呼び出されるようになりました。

  • words[1]はコマンドラインの2番目の単語、つまりパッケージ名(例: fmt)をgo#complete#PackageMembersa:package引数として渡します。
  • words[2]はコマンドラインの3番目の単語、つまりユーザーが入力している部分的なメンバー名(例: P)をgo#complete#PackageMembersa:member引数として渡します。

この連携により、ユーザーが:Godoc <package_name> <partial_member_name>と入力すると、go#complete#PackageMembersが呼び出され、適切なパッケージメンバーの補完候補が提供されるようになります。

コアとなるコードの変更箇所

--- a/misc/vim/autoload/go/complete.vim
+++ b/misc/vim/autoload/go/complete.vim
@@ -28,13 +28,31 @@ if len(s:goarch) == 0
   endif
 endif
 
+function! go#complete#PackageMembers(package, member)
+  silent! let content = system('godoc ' . a:package)
+  if v:shell_error || !len(content)
+    return []
+  endif
+  let lines = filter(split(content, "\n"),"v:val !~ '^\\s\\+$'")
+  try
+    let mx1 = '^\\s\\+\\(\\S+\\)\\s\\+=\\s\\+.*'
+    let mx2 = '^\\%(const\\|var\\|type\\|func\\) \\([A-Z][^ (]\\+\\).*'
+    let candidates =
+    \   map(filter(copy(lines), 'v:val =~ mx1'), 'substitute(v:val, mx1, "\\1", "")')
+    \ + map(filter(copy(lines), 'v:val =~ mx2'), 'substitute(v:val, mx2, "\\1", "")')
+    return filter(candidates, '!stridx(v:val, a:member)')
+  catch
+    return []
+  endtry
+endfunction
+\
 function! go#complete#Package(ArgLead, CmdLine, CursorPos)
   let dirs = []
 
   let words = split(a:CmdLine, '\s\+', 1)
   if len(words) > 2
-    " TODO Complete package members
-    return []
+    " Complete package members
+    return go#complete#PackageMembers(words[1], words[2])
   endif
 
   if executable('go')

コアとなるコードの解説

このコミットの核となる変更は、以下の2点です。

  1. go#complete#PackageMembers(package, member) 関数の新規追加: この関数は、指定されたGoパッケージ(package引数)のドキュメントをgodocコマンドで取得し、その出力からパッケージメンバー(定数、変数、型、関数など)を抽出します。抽出された候補は、ユーザーが入力した部分文字列(member引数)でフィルタリングされ、補完候補のリストとして返されます。 内部では、system()関数でgodocコマンドを実行し、その標準出力をVimscriptの文字列として取得します。取得した文字列は、正規表現を用いて解析され、Goのメンバー名に合致するパターンが抽出されます。特に、mx1mx2という2つの正規表現が定義されており、それぞれ異なる形式で宣言されたメンバーを捕捉するように設計されています。最終的に、filter()関数とstridx()関数を組み合わせて、ユーザーの入力に前方一致する候補のみを絞り込んでいます。

  2. go#complete#Package 関数内の呼び出し箇所の変更: 既存のgo#complete#Package関数は、:Godocコマンドの引数を解析し、補完のコンテキストを判断する役割を担っています。この変更により、コマンドラインの単語数が2つを超える場合(例: :Godoc fmt Pのように、パッケージ名に加えてさらに何か入力されている場合)、以前は「TODO」として実装されていなかったパッケージメンバーの補完が、新しく追加されたgo#complete#PackageMembers関数を呼び出すように修正されました。 具体的には、words[1](パッケージ名)とwords[2](入力中のメンバー名)を引数としてgo#complete#PackageMembersに渡し、その戻り値を補完候補として利用します。

これらの変更により、Vimの:Godocコマンドは、パッケージ名だけでなく、そのパッケージ内のメンバーについても自動補完を提供できるようになり、Go開発者の利便性が大幅に向上しました。

関連リンク

参考にした情報源リンク