goplsのdaemonモードを使う

goplsgopls -listen=<addr>で実行するとdaemonモードで起動し、指定した<addr>TCP接続できるようになる。

github.com

クライアントはgoplsを使っても良いし、独自に実装することも可能。
その場合、TCP上で以下のような形式のJSON-RPCを送受信すれば良い。
(改行は\r\n)

Content-Length: <JSON部分のbyte数>

{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}
  • レスポンス
Content-Length: <JSON部分のbyte数>

{"jsonrpc":"2.0","result":{},"id":1}

接続ごとに 'initialize''initialized' を送信して初期化したら、あとは 'textDocument/references''callHierarchy/incomingCalls' など、呼びたいメソッドを呼べばOK。

毎回初期化しなくて済むのと、レスポンスをJSONとして扱えるので複雑なことがやりやすくなるはず。

クライアントのサンプル実装はこちら。

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net"
    "sync/atomic"
)

type Client struct {
    id   int64
    conn net.Conn
}

func Connect(addr string, initializedParams map[string]interface{}) (*Client, error) {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        return nil, err
    }
    client := &Client{conn: conn}

    if _, err := client.Call("initialize", initializedParams); err != nil {
        return nil, err
    }
    if _, err := client.Call("initialized", map[string]interface{}{}); err != nil {
        return nil, err
    }

    return client, nil
}

type response struct {
    ID     int64           `json:"id"`
    Result json.RawMessage `json:"result"`
}

func (c *Client) Call(method string, params interface{}) (*json.RawMessage, error) {
    id := atomic.AddInt64(&c.id, 1)

    data, err := json.Marshal(map[string]interface{}{
        "jsonrpc": "2.0",
        "method":  method,
        "params":  params,
        "id":      id,
    })
    if err != nil {
        return nil, err
    }

    if _, err := fmt.Fprintf(c.conn, "Content-Length: %v\r\n\r\n%s", len(data), data); err != nil {
        return nil, err
    }

    for {
        // Content-Lengthまで読む
        buf := make([]byte, 40)
        n, err := c.conn.Read(buf)
        if err != nil {
            return nil, err
        }

        r := bytes.NewBuffer(buf[:n])
        var length int
        if _, err := fmt.Fscanf(r, "Content-Length: %d\r\n\r\n", &length); err != nil {
            continue
        }

        // bufに入りきらなかったBodyを読む
        body := make([]byte, length)
        idx := copy(body, r.Bytes())
        if _, err = c.conn.Read(body[idx:]); err != nil {
            return nil, err
        }

        var res response
        if err := json.Unmarshal(body, &res); err != nil {
            return nil, err
        }
        if res.ID != id {
            // 送信したリクエストに対するレスポンス以外は無視
            // (goplsからの通知を含む)
            continue
        }
        return &res.Result, nil
    }
}

func (c *Client) Shutdown() error {
    _, err := c.Call("shutdown", nil)
    return err
}