VimConf 2023 Tiny に行ってきた

昨日(11/18)はVimConfに行ってきました。
前回が2019年だったので実に4年ぶりの開催でした。

vimconf.org

2017年から2019年は3回ともスピーカーでしたが、今回は久しぶりの一聴衆でした。
なので当日は全く緊張する必要がなく、とても穏やかな気持ちで参加することができました。

また、休憩時間や懇親会などで久しぶりに会えた人と話をすることができ、とても懐かしい気持ちになりました。
そして古巣の皆さんのVim愛は変わらないどころか、さらに強まっていて感慨深かったです。

ただ後になってみると、新しい所属でのVim活なんかをLTしても良かったかなと思ったりもしました。
この感覚はVimConf 2016に参加した時に似ていて、あの時も「自分も登壇したい!」という気持ちになったことを覚えています。

最後に、発表者とスタッフの方に改めて大きな感謝を伝えたいです。
特にスタッフの方は限られたリソースの中でとても大変だったと思いますが、今までのVimConfと変わらない体験を得ることができました。
来年も楽しみにしています。

gnosticでOpenAPIをProtocol Buffersに変換する

gnosticgnostic-grpcを使えばOpenAPIの.yaml(.json)を.protoに変換できる。

github.com

github.com

1. gnosticをインストールする

go install github.com/google/gnostic@latest

2. gnostic-grpcをインストールする

git clone https://github.com/google/gnostic-grpc
cd gnostic-grpc
./COMPILE-PROTOS.sh
./plugin-creation.sh

※ protocコマンドがないとエラーになるので Protocol Buffer Compiler Installation | gRPC あたりを参考にインストールしておく必要がある。

3. 変換する

gnostic --grpc-out=proto openapi.yaml

前回の記事で生成した以下のyamlは、

openapi: 3.0.3
info:
  description: Foo API Document.
  title: Foo API
  version: "1.0"
paths:
  /foo:
    post:
      operationId: PostFoo
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostFooRequest'
      responses:
        "201":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Foo'
          description: Created
        "400":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          description: Bad Request
  /foo/{id}:
    get:
      operationId: GetFoo
      parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Foo'
          description: OK
        "404":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
          description: Not Found
components:
  schemas:
    ErrorResponse:
      properties:
        code:
          type: string
        message:
          type: string
      type: object
    Foo:
      properties:
        data:
          type: string
        id:
          type: string
      type: object
    PostFooRequest:
      properties:
        data:
          type: string
      type: object

次のようなprotoになる。

  • proto/openapi.proto
syntax = "proto3";

package openapi;

import "google/api/annotations.proto";

import "google/protobuf/descriptor.proto";

import "google/protobuf/empty.proto";

option go_package = ".;openapi";

message PostFooRequest {
  string data = 1;
}

//PostFooParameters holds parameters to PostFoo
message PostFooRequest {
  PostFooRequest post_foo_request = 1;
}

//GetFooParameters holds parameters to GetFoo
message GetFooRequest {
  string id = 1;
}

service Openapi {
  rpc PostFoo ( PostFooRequest ) returns ( google.protobuf.Empty ) {
    option (google.api.http) = { post:"/foo" body:"post_foo_request"  };
  }

  rpc GetFoo ( GetFooRequest ) returns ( google.protobuf.Empty ) {
    option (google.api.http) = { get:"/foo/{id}"  };
  }
}

※ OpenAPIの構成によっては変換できない場合がある。
 また、OpenAPI 3.1には対応していない。

github.com/swaggest/openapi-goを使ってOpenAPIを生成する

github.com/swaggest/openapi-goを使うとGoのコードからOpenAPIの定義(json/yaml)を生成できる。

pkg.go.dev

例:

package main

import (
    "encoding/json"
    "fmt"
    "log"

    openapi "github.com/swaggest/openapi-go"
    "github.com/swaggest/openapi-go/openapi3"
)

type endpoint struct {
    id        string
    method    string
    path      string
    request   any
    responses []response
}

type response struct {
    status int
    body   any
}

func main() {
    r := openapi3.Reflector{
        Spec: &openapi3.Spec{
            Openapi:    "3.0.3",
            Components: &openapi3.Components{},
        },
    }

    r.Spec.Info.
        WithTitle("Foo API").
        WithDescription("Foo API Document.").
        WithVersion("1.0")

    list := []endpoint{
        {
            id:      "PostFoo",
            method:  "POST",
            path:    "/foo",
            request: new(PostFooRequest),
            responses: []response{
                {status: 201, body: new(Foo)},
                {status: 400, body: new(ErrorResponse)},
            },
        },
        {
            id:      "GetFoo",
            method:  "GET",
            path:    "/foo/{id}",
            request: new(GetFooRequest),
            responses: []response{
                {status: 200, body: new(Foo)},
                {status: 404, body: new(ErrorResponse)},
            },
        },
    }

    for _, v := range list {
        oc, err := r.NewOperationContext(v.method, v.path)
        if err != nil {
            log.Println(err)
            continue
        }

        oc.SetID(v.id)
        oc.AddReqStructure(v.request)
        for _, resp := range v.responses {
            oc.AddRespStructure(resp.body, openapi.WithHTTPStatus(resp.status))
        }

        if err := r.AddOperation(oc); err != nil {
            log.Println(err)
            continue
        }
    }

    // b, err := r.Spec.MarshalYAML()
    b, err := json.MarshalIndent(r.Spec, "", " ")
    if err != nil {
        log.Println(err)
        return
    }

    fmt.Println(string(b))
}

type PostFooRequest struct {
    Data string `json:"data"`
}

type GetFooRequest struct {
    ID string `json:"id" path:"id"`
}

type Foo struct {
    ID   string `json:"id"`
    Data string `json:"data"`
}

type ErrorResponse struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

list の中身を既存コードから生成すれば途中からでも比較的簡単にOpenAPIを始められる。

macvimをビルドする(2023/08)

だいぶ前にも書いていた。

daisuzu.hatenablog.com

ここ最近は毎朝UpdateMacVimを実行している。*1

UpdateMacVim() {
    cd $HOME/go/src/github.com/macvim-dev/macvim
    git fetch origin master
    if [ -z "`git diff FETCH_HEAD --shortstat`" ]; then
        cd -
        return 0
    fi
    git merge FETCH_HEAD
    make distclean && ConfigureMacVim && make
    rm -rf $HOME/.local/MacVim.app
    cp -R src/MacVim/build/Release/MacVim.app $HOME/.local/
    cd -
}
ConfigureMacVim() {
    ./configure \
        --enable-fail-if-missing \
        --with-features=huge \
        --enable-terminal \
        --enable-multibyte \
        --enable-python3interp \
        --enable-luainterp \
        --with-lua-prefix="$(brew --prefix lua)" \
        --enable-cscope \
        --with-tlib=ncurses \
        --with-compiledby="daisuzu <daisuzu@gmail.com>" \
        CFLAGS="-I$(brew --prefix)/include" \
        LDFLAGS="-L$(brew --prefix)/lib" \
        --prefix=$HOME/.local "$*"
}

あとは$HOME/.local/MacVim.app/Contents/binにパスを通しておけばOK。

*1:ghq getしたのでGOPATHにmacvimのリポジトリがある

Goの外部パッケージに独自の変更を加える

外部パッケージを使っていて、ちょっとした修正を試したい時は以下のような方法があります。

1. 外部パッケージをForkしてgo.modで置き換える

最も基本的なやり方なので特に理由がなければこちらの方法にするのが良いでしょう。
対象のパッケージのForkに変更を加え、以下のようにしてgo.modで置き換えます。

go mod edit -replace github.com/EXTERNAL_/PACKAGE@v1.0.0=github.com/daisuzu/PACKAGE@development
go mod tidy

なお、Forkにコミットを追加する場合はその度にgo mod tidyで擬似バージョンを更新していく必要があります。

2. 外部パッケージのコピーをリポジトリに追加してgo.modで置き換える

Forkを作りたくなかったり、試行錯誤したい場合などはリポジトリの中に対象の外部パッケージをコピーする方がやりやすいかもしれません。
その場合も同様にgo.modで置き換えます。

go mod edit -replace github.com/EXTERNAL_/PACKAGE@v1.0.0=./PATH_TO_COPY

3. 変更したファイルをリポジトリに追加してoverlayで書き換える

変更が数行程度だと上記の方法が面倒だと感じることがあるかもしれません。
その場合は変更したファイルのみをリポジトリに追加し、goコマンドのoverlayフラグで書き換えることも可能です。

以下のようなoverlay.jsonを用意してgo build -overlay=overlay.jsonのように指定します。

{
  "Replace": {
    "/GO_MOD_CACHE/PATH_TO_EXTERNAL_PACKAGE/TARGET.go": "/PATH_TO_COPY/TARGET.go"
  }
}

いずれの方法もうまくいったら本家に還元しましょう。

Vimの極意

この記事はVim Advent Calendar 2022の1日目の記事です。

今年でVimをメインエディタにして15年になります。
最近どうすれば思考する速度でテキストを編集できるようになる*1のか考えたりすることがあったので、この機会に軽くまとめてみます。

簡単な操作であれば「○○をしたい」と思った瞬間にそうなっていることもありますが、実際はそうならないことがの方が多いです。
それが何故なのかというと、複雑な編集をする際には自分のやりたいことをVimの操作に変換する必要があり、そこに時間がかかっているからだと考えました。

そこで思いついたのが、やろうとしていること自体をVimのコマンド群として捉えられるようになればさらに高速にテキストを編集できるのではないか、ということです。

具体例をあげてみると、以下のようなGoのコードでカーソルがfuncのfにある時に、戻り値の型をClientInterfaceから*Clientに変更したいと思ったら、

func NewClient() ClientInterface {
    return newDefaultClient()
}

戻り値の位置にカーソルを移動して、カーソル下の単語を*Clientに書き換えよう、と考えてVimを操作するのではなく、
最初から$bcw*Clientと考えてVimを操作します。

つまり、「あらゆる編集操作をVimのコマンドで表現できるようになる」ことがVimの極意ということになります。

実際に文章に書いて読んでみるとかなり難しそうな気がしてきましたが、
これを会得する方法としては以下の7つが考えられます。

  1. 極意のことを意識しながらVimを使い続ける
  2. 定期的にVimのヘルプを読み返してコマンドとしての語彙を増やす
  3. 指に負担を感じたら少し立ち止まってより良い方法がないか調べる
  4. VimGolfをやる
  5. 覚えにくい処理をユーザー定義コマンドやマッピングにする
    • vimrcを育てる
  6. 4で汎用性が高いものをプラグインにする
    • 既に似たようなプラグインがあればそれを使っても良い
  7. 5で有用なものをVimの本体に組み込む
    • Vim本体の実装に対する知識が必要

ある程度身に付いてくれば先のサンプルコードで戻り値の型がわからなくてもすぐに
/newD<CR><C-]>Wyiw<C-T>j%bcw<C-R>0<ESC>が出てくるようになることでしょう。

ぜひ試してみてください。

*1:実践Vim 第21章の続き

goplsに独自Analyzerを組み込む

internal/lsp/source/options.godefaultAnalyzers()が返すmapに自作のAnalyzerを追加してgo installすれば使えるようになる*1

用途としてはチームのコーディング規約をtextDocument/diagnosticでチェックしたり、チェックに引っかかったコードの修正や一部だけ実装したコードの続きを生成するtextDocument/codeActionを実行したりなど。

今まではCIで独自Analyzerとreviewdog + action-suggesterを用いてチェックやコード修正を行っていたものを、goplsに組み込むことによってより早いコーディングのタイミングで行えるようになった。

もちろん補完やコードジャンプ、コード生成(textDocument/codeLens)などをカスタマイズしてさらに便利にすることもできるけど、本体の変更に追従するのが大変になりそうなので今のところはやっていない。
Analyzerの追加だけであればコンフリクトのことはほとんど考えなくて良いので定期的にupstreamを取り込むように設定するくらいでメンテナンスに関しては特に問題なさそう。
それにgoplsのCLI版で使えない機能を増やしすぎてしまうと、GoLandなどLSPに対応していないツールと差が開きすぎてしまうのも理由の一つ*2

*1:gopls/v0.9.4の場合

*2:今のチームは半分近くのメンバーがGoLandを使っているので