TinyGo + Vim で gopls するための設定

2022/09/06 追記

Vim / Neovim を使っている場合は `sago35/tinyg.vim` を使うと TinyGo の LSP 環境が立ち上がります。
今の所サポートしているのは以下の 4 パターンです。

* Vim8 + vim-lsp
* Vim8 + coc.nvim
* Neovim (native LSP client)
* Neovim + coc.nvim

インストール後は、 `:TinygoTarget wioterminal` のようにターゲットを指定することで vim-lsp が立ち上げ直します。
`:TinygoTarget -` で元の状態に戻ります。

![tinygo-target-vim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63649/12be7559-eb5b-e9d7-f6fa-e2591ef48cbf.gif)


2021/06/14 追記

`sago35/tinygo.vim` という Vim script を作成しました。
TinyGo + Vim (+ vim-lsp) の環境を作るなら、 `sago35/tinygo.vim` が良いはず。

インストール後は、 `:TinygoTarget wioterminal` のようにターゲットを指定することで vim-lsp が立ち上げ直します。

インストール方法は以下のような形でインストールしてください。

```
Plug 'sago35/tinygo.vim'
```

* https://github.com/sago35/tinygo.vim

----

2020/09/23 追記

TinyGo 0.15 以降は以下のやり方を使うのが簡単です。

* [TinyGo + `VSCode or Vim (もしくはそれ以外の LSP 対応エディタ)` で gopls 連携する方法](https://qiita.com/sago35/items/c30cbce4a0a3e12d899c)

----

2020/08/10 追記

以下に記載している go.mod で replace を書く方法でも動かすことはできますが、ページ下部に記載のコメント 1 および 2 の方法／情報で実施するのが簡単で良いです。

----

以下の GIF のように、 PyPortal (atsamd51j20a) の設定で gopls を動かせるようになりました。
正しく board_pyportal.go 等に飛べているのが確認できます。
もちろん、メソッド等の補完も動きます。

![tinygo-gopls-vim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63649/f6dee7b4-7d3d-bc13-272b-f82b1aebec6c.gif)

## はじめに

TinyGo は、以下の理由により gopls がほとんど動きませんでした。

* 独自のビルトインパッケージを持っている
    * 例: machine や device など
* buildtag による分岐

buildtag による分岐については、 gopls でも issue として上がっている内容になりますが、現時点でも gopls 全体で `ある単一の buildtag を持つこと` は可能なので何とかなりました。

ということで、 LSP を動かすために以下の 3 つを実施していきます。

1. 開発しているフォルダの go.mod に replace を書く
2. tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く
3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する


## 環境

この記事を作るにあたり、以下の環境で確認しました。

* Windows 10
* Go version go1.14.1 windows/amd64
* tinygo version 0.13.1 windows/amd64 (using go version go1.14.1 and LLVM version 10.0.1)
* gopls ead0a569305d87def8dc4ad3899a7d78432b12c6
* Vim 8.2.147
* vim-go 13af5df6a1b3bc4bdfd03e3c05fa600d1dd16de6

多分、 Windows じゃなくても Vim じゃなくても同じように設定可能だと思います。


# やり方

## 1. 開発しているフォルダの go.mod に replace を書く

### 背景

TinyGo では、以下のパッケージ以外のものは GOROOT にあるものを使っています。
以下のパッケージは tinygo をインストールしたディレクトリ直下のものが使われます。

* machine
* os
* reflect
* runtime
* runtime/interrupt
* runtime/volatile
* sync
* testing
* internal/reflectlite
* internal/task
* device/*
* examples/*

例えば fmt パッケージは GOROOT にあるものが使われるので、インストールしている Go の fmt パッケージが使われます。
`fmt.Printf()` はほとんどのマイコンボードにおいて、 USBCDC (USB の SerialPort) への書き込みに使われますが、 fmt パッケージ自体は Go (not TinyGo) の物が使われます。
Go の fmt パッケージを使っているのにどうやって USBCDC に出力するかというと、 TinyGo 内で `os.Stdout` を USBCDC への書き込みにマッピングして実現しています。

この `特別扱い` は以下のコードにより実現しています。
そして、この `特別扱い` が gopls がうまく動かない状態を作っています。

```go
// tinygo/compiler/compiler.go
func Compile(pkgName string, machine llvm.TargetMachine, config *compileopts.Config) (llvm.Module, []string, []error) {
    // ...
    lprogram := &loader.Program{
        // ...
        OverlayPath: func(path string) string {
            // Return the (overlay) import path when it should be overlaid, and
            // "" if it should not.
            if strings.HasPrefix(path, tinygoPath+"/src/") {
                // Avoid issues with packages that are imported twice, one from
                // GOPATH and one from TINYGOPATH.
                path = path[len(tinygoPath+"/src/"):]
            }
            switch path {
            case "machine", "os", "reflect", "runtime", "runtime/interrupt", "runtime/volatile", "sync", "testing", "internal/reflectlite", "internal/task":
                return path
            default:
                if strings.HasPrefix(path, "device/") || strings.HasPrefix(path, "examples/") {
                    return path
                } else if path == "syscall" {
                    for _, tag := range c.BuildTags() {
                        if tag == "baremetal" || tag == "darwin" {
                            return path
                        }
                    }
                }
            }
            return ""
        },
        // ...
    }
    // ...
}
```

### 対策

ということで、上記の特別扱いされたフォルダに対して go.mod で replace ディレクティブを書くことにより解決できます。

Windows での標準的なインストール場所に合わせた設定は以下になります。

```
module tinygo.org/x/drivers

go 1.14

replace (
	device/sam => C:\tinygo\src\device/sam
	internal/reflectlite => C:\tinygo\src\internal/reflectlite
	internal/task => C:\tinygo\src\internal/task
	machine => C:\tinygo\src\machine
	os => C:\tinygo\src\os
	reflect => C:\tinygo\src\reflect
	runtime => C:\tinygo\src\runtime
	runtime/interrupt => C:\tinygo\src\runtime/interrupt
	runtime/volatile => C:\tinygo\src\runtime/volatile
	sync => C:\tinygo\src\sync
	testing => C:\tinygo\src\testing
)
```

なお、 replace ディレクティブで指し示す先は、空でも良いので go.mod ファイルが必要になります。
後述の `各パッケージフォルダに go.mod を置く` を実行しないと、この時点ではうまく動作しません。


## 2. tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く


### 背景

以下に記載の通り、 replace される側については go.mod ファイルが必要となります。
空でも良いので作成しておく必要があります。

> Note: if the right-hand side of a replace directive is a filesystem path, then the target must have a go.mod file at that location. If the go.mod file is not present, you can create one with go mod init.
https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive

### 対策

touch 等を使ってからファイルを置いてください。
git-bash 等が使える場合は以下を実行することで go.mod を簡単に作成することができます。

```
touch C:/tinygo/src/device/sam/go.mod
touch C:/tinygo/src/internal/reflectlite/go.mod
touch C:/tinygo/src/internal/task/go.mod
touch C:/tinygo/src/machine/go.mod
touch C:/tinygo/src/os/go.mod
touch C:/tinygo/src/reflect/go.mod
touch C:/tinygo/src/runtime/go.mod
touch C:/tinygo/src/runtime/interrupt/go.mod
touch C:/tinygo/src/runtime/volatile/go.mod
touch C:/tinygo/src/sync/go.mod
touch C:/tinygo/src/testing/go.mod
```

## 3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する

### 背景

TinyGo では以下を使用しています。
これらを gopls に伝える必要があります。

* GOOS
* GOARCH
* buildtag (マイコンやボードなどの分岐)

### 対策

buildtag については、環境変数 GOFLAGS で設定できます。
それぞれの値をどう設定すべきかは `tinygo info` で調べることができます。
例えば、 PyPortal というターゲットに対しては `tinygo flash -target pyportal .` というようなコマンドでビルドしますが、その際の `-target pyportal` の部分を `tinygo info` に設定します。

```
$ C:\tinygo\bin\tinygo.exe info -target pyportal
LLVM triple:       armv7em-none-eabi
GOOS:              linux
GOARCH:            arm
build tags:        cortexm baremetal linux arm sam atsamd51 atsamd51j20 atsamd51j20a pyportal tinygo gc.conservative scheduler.tasks
garbage collector: conservative
scheduler:         tasks
```

上記を調べることができたので、後は環境変数を設定します。
GOFLAGS はカンマ区切りなので注意が必要です。

```
set GOOS=linux
set GOARCH=arm
set GOFLAGS=-tags=cortexm,baremetal,linux,arm,sam,atsamd51,atsamd51j20,atsamd51j20a,pyportal,tinygo,gc.conservative,scheduler.tasks
```

bash 等では以下のように設定します。

```
export GOOS=linux
export GOARCH=arm
export GOFLAGS=-tags=cortexm,baremetal,linux,arm,sam,atsamd51,atsamd51j20,atsamd51j20a,pyportal,tinygo,gc.conservative,scheduler.tasks
```

### おまけ

`3. 環境変数 GOOS + GOARCH + GOFLAGS を設定する` の部分だけをヘルプする小さな自分用 CLI ツールとして tinygo-edit を作りました。
GOOS + GOARCH + GOFLAGS を設定しつつ --editor で指定した editor で開きます。

https://github.com/sago35/tinygo-edit

以下でインストールできます。

```
$ go get github.com/sago35/tinygo-edit
```

以下のように使用できます。

```
$ cd ./examples/blinky1

# feather-m4 の設定で gvim を立ち上げ
$ tinygo-edit --editor gvim --target feather-m4

# pyportal の設定で vim を立ち上げ
$ tinygo-edit --editor vim --target pyportal
```

# まとめ

以下を実施することで、快適な TinyGo 環境になりました。
今後、 (主に gopls 側の version up により) 以下の設定は不要になるかと思いますが、しばらくはこの方法を使っていくことになりそうです。

* 開発しているフォルダの go.mod に replace を書く
* tinygo ディレクトリの各パッケージフォルダに go.mod (空で良い) を置く
* 環境変数 GOOS + GOARCH + GOFLAGS を設定する


# リンク

* sago35/tinygo-edit: Add an environment variable for tinygo and open the editor
    * https://github.com/sago35/tinygo-edit
* x/tools/gopls: improve handling for build tags · Issue #29202 · golang/go
    * https://github.com/golang/go/issues/29202