Skip to content

Autogenerated documentation: using LSP make call hierarchy graphs #71

@joonas-fi

Description

@joonas-fi
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"log/slog"
	"os/exec"

	"github.com/sourcegraph/jsonrpc2"
	lsp "github.com/toitware/go-lsp"
	"github.com/toitware/go-lsp/lspext"
)

// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/

type CallHierarchyItem struct {
	Name           string          `json:"name"`
	Kind           int             `json:"kind"`
	Tags           []int           `json:"tags,omitempty"`
	Detail         string          `json:"detail,omitempty"`
	URI            lsp.DocumentURI `json:"uri"`
	Range          lsp.Range       `json:"range"`
	SelectionRange lsp.Range       `json:"selectionRange"`
	Data           interface{}     `json:"data,omitempty"`
}

type CallHierarchyPrepareParams struct {
	lsp.TextDocumentPositionParams

	WorkDoneToken interface{} `json:"workDoneToken,omitempty"`
}

type CallHierarchyIncomingCall struct {
	From       CallHierarchyItem `json:"from"`
	FromRanges []lsp.Range       `json:"fromRanges"`
}

type CallHierarchyOutgoingCall struct {
	To         CallHierarchyItem `json:"to"`
	FromRanges []lsp.Range       `json:"fromRanges"`
}

// Minimal stream wrapper for jsonrpc2
type stdioStream struct {
	io.Reader
	io.Writer
	io.Closer
}

var _ interface {
	io.Reader
	io.Writer
	io.Closer
} = (*stdioStream)(nil)

func hailait() {
	ctx := context.Background()

	// Start gopls in stdio mode
	cmd := exec.Command("bob", "tools", "langserver")
	stdin, _ := cmd.StdinPipe()
	stdout, _ := cmd.StdoutPipe()
	cmd.Stderr = cmd.Stdout

	if err := cmd.Start(); err != nil {
		log.Fatal("failed to start gopls:", err)
	}

	stream := jsonrpc2.NewBufferedStream(&stdioStream{stdout, stdin, stdin}, jsonrpc2.VSCodeObjectCodec{})
	conn := jsonrpc2.NewConn(ctx, stream, jsonrpc2.HandlerWithError(func(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) (result interface{}, err error) {
		switch r.Method {
		case "window/showMessage":
			logMessageParams := struct {
				Message string `json:"message"`
				Type    int    `json:"type"`
			}{}
			if err := json.Unmarshal(*r.Params, &logMessageParams); err != nil {
				panic(err)
			}
			slog.Info("LSP server", "kind", "show", "msg", logMessageParams.Message, "type", logMessageParams.Type)
			return nil, nil
		case "window/logMessage":
			logMessageParams := struct {
				Message string `json:"message"`
				Type    int    `json:"type"`
			}{}
			if err := json.Unmarshal(*r.Params, &logMessageParams); err != nil {
				panic(err)
			}
			slog.Info("LSP server", "kind", "log", "msg", logMessageParams.Message, "type", logMessageParams.Type)
			return nil, nil
		default:
			slog.Warn("returning error", "request", r.Method, "params", string(*r.Params))
			return nil, errors.New("requests not implemented")
		}
	}))
	defer stream.Close()

	// ---- Initialize ----
	initReq := lsp.InitializeParams{
		RootURI: "file:///workspace",
	}

	var initResp lsp.InitializeResult
	if err := conn.Call(ctx, "initialize", initReq, &initResp); err != nil {
		log.Fatal("initialize error:", err)
	}

	// Tell server we're ready
	_ = conn.Notify(ctx, "initialized", struct{}{})

	// --- Prepare call hierarchy ---
	params := CallHierarchyPrepareParams{
		TextDocumentPositionParams: lsp.TextDocumentPositionParams{
			TextDocument: lsp.TextDocumentIdentifier{
				URI: initReq.RootURI + "/pkg/mypkg/example.go",
			},
			Position: lsp.Position{ // -1 because positions are 0-based
				Line:      114 - 1,
				Character: 6 - 1,
			},
		},
	}

	var items []CallHierarchyItem
	if err := conn.Call(ctx, "textDocument/prepareCallHierarchy", params, &items); err != nil {
		log.Fatal("prepareCallHierarchy:", err)
	}

	if len(items) == 0 {
		log.Println("No call hierarchy items found at location")
		return
	}

	item := items[0]
	fmt.Println("Item:", item.Name)

	_ = lspext.CacheSetParams{}
	// --- Incoming calls ---
	var incoming []CallHierarchyIncomingCall
	if err := conn.Call(ctx, "callHierarchy/incomingCalls", struct {
		Item CallHierarchyItem `json:"item"`
	}{item}, &incoming); err != nil {
		log.Fatal("incomingCalls: ", err)
	}

	fmt.Println("\nIncoming calls:")
	for _, c := range incoming {
		fmt.Println("   from:", c.From.Name)
	}

	// --- Outgoing calls ---
	// var outgoing []CallHierarchyOutgoingCall
	// if err := conn.Call(ctx, "callHierarchy/outgoingCalls", item, &outgoing); err != nil {
	// 	log.Fatal("outgoingCalls:", err)
	// }

	// fmt.Println("\nOutgoing calls:")
	// for _, c := range outgoing {
	// 	fmt.Println("   to:", c.To.Name)
	// }
}

go.mod:

	github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect
	github.com/toitware/go-lsp v0.0.0-20240220075859-6b0ca01316f0 // indirect

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions