Skip to content

Commit

Permalink
Initial test setup using gopls integration test fixtures
Browse files Browse the repository at this point in the history
  • Loading branch information
kralicky committed Feb 21, 2024
1 parent c793122 commit 85ebe43
Show file tree
Hide file tree
Showing 9 changed files with 529 additions and 195 deletions.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/bufbuild/protovalidate-go v0.5.2
github.com/google/cel-go v0.20.0
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f
github.com/kralicky/protocompile v0.0.0-20240221032829-e40f4c19d142
github.com/kralicky/tools-lite v0.0.0-20240209234032-93b7eedbea2e
github.com/kralicky/protocompile v0.0.0-20240221212304-b5a12d32e33d
github.com/kralicky/tools-lite v0.0.0-20240221184119-4cba2183fdda
github.com/mattn/go-tty v0.0.5
github.com/spf13/cobra v1.8.0
golang.org/x/mod v0.15.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f h1:MsNe8A51V+7Fu5OMXSl8SK02erPJ40vFs2zDHn89w1g=
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f/go.mod h1:vOkwMjs49XmP/7Xfo9ZL6eg2ei51lmtD/4U/Az5GTq8=
github.com/kralicky/protocompile v0.0.0-20240221032829-e40f4c19d142 h1:l30nktO2UguhdYw6ylVePYVHLFYnzgSBmoJYDywTosk=
github.com/kralicky/protocompile v0.0.0-20240221032829-e40f4c19d142/go.mod h1:eIoBteRQ90jYYcBBAL8RNOaVSMmyWFDqAH4t3i1elUc=
github.com/kralicky/tools-lite v0.0.0-20240209234032-93b7eedbea2e h1:Mic4oZbKrGxJ6l3FmVLax0+RNOxE/J6KnnlN4BH2d58=
github.com/kralicky/tools-lite v0.0.0-20240209234032-93b7eedbea2e/go.mod h1:NKsdxFI6awifvNvxDwtCU1YCaKRoSSPpbHXkKOMuq24=
github.com/kralicky/protocompile v0.0.0-20240221212304-b5a12d32e33d h1:gV7uD5ltHzixn1AzbfEs2rQqApmSz6o5yq/dmVZNrGM=
github.com/kralicky/protocompile v0.0.0-20240221212304-b5a12d32e33d/go.mod h1:eIoBteRQ90jYYcBBAL8RNOaVSMmyWFDqAH4t3i1elUc=
github.com/kralicky/tools-lite v0.0.0-20240221184119-4cba2183fdda h1:5zLw2UdV/QT50HmgsCBaJIVhrD2D+3AFr+EstHRtZFI=
github.com/kralicky/tools-lite v0.0.0-20240221184119-4cba2183fdda/go.mod h1:bDe7Unrh7kKhnkf5+csdbu0hSd8RRo1EnnnViKGUewo=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
Expand Down
4 changes: 0 additions & 4 deletions pkg/lsp/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package lsp
import (
"context"
"fmt"
"log/slog"
"runtime"
"strings"
"sync"
Expand Down Expand Up @@ -97,9 +96,6 @@ func NewCache(workspace protocol.WorkspaceFolder, opts ...CacheOption) *Cache {
}

func (c *Cache) LoadFiles(files []string) {
slog.Debug("initializing")
defer slog.Debug("done initializing")

created := make([]file.Modification, len(files))
for i, f := range files {
created[i] = file.Modification{
Expand Down
7 changes: 5 additions & 2 deletions pkg/lsp/semantic.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func semanticTokensRange(cache *Cache, doc protocol.TextDocumentIdentifier, rng
return ret, err
}

const debugCheckOverlappingTokens = false
var DebugCheckOverlappingTokens = false

func computeSemanticTokens(cache *Cache, e *semanticItems, walkOptions ...ast.WalkOption) {
e.inspect(cache, e.AST(), walkOptions...)
Expand All @@ -197,7 +197,7 @@ func computeSemanticTokens(cache *Cache, e *semanticItems, walkOptions ...ast.Wa
}
return e.items[i].start < e.items[j].start
})
if !debugCheckOverlappingTokens {
if !DebugCheckOverlappingTokens {
return
}

Expand Down Expand Up @@ -469,6 +469,9 @@ func (s *semanticItems) inspect(cache *Cache, node ast.Node, walkOptions ...ast.
s.mktokens(node, tracker.Path(), semanticTypeKeyword, 0)
}
case *ast.RuneNode:
if node.Virtual || node.Rune == 0 {
return true
}
switch node.Rune {
case '}', '{', '.', ',', '<', '>', '(', ')', '[', ']', ';', ':':
s.mkcomments(node)
Expand Down
46 changes: 35 additions & 11 deletions pkg/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ type Server struct {

client protocol.Client

trackerMu sync.Mutex
tracker *progress.Tracker

diagnosticStreamMu sync.RWMutex
diagnosticStreamCancel func()
trackerMu sync.Mutex
tracker *progress.Tracker
shutdownOnce sync.Once
}

type ServerOptions struct {
unknownCommandHandlers map[string]UnknownCommandHandler
shutdownHooks []func(context.Context)
}

type ServerOption func(*ServerOptions)
Expand All @@ -56,6 +55,12 @@ func WithUnknownCommandHandler(handler UnknownCommandHandler, cmds ...string) Se
}
}

func WithShutdownHook(hook func(context.Context)) ServerOption {
return func(o *ServerOptions) {
o.shutdownHooks = append(o.shutdownHooks, hook)
}
}

func NewServer(client protocol.Client, opts ...ServerOption) *Server {
var options ServerOptions
options.apply(opts...)
Expand Down Expand Up @@ -136,6 +141,10 @@ func (s *Server) Initialize(ctx context.Context, params *protocol.ParamInitializ
},
}
slog.Debug("Initialize", "folders", folders)
defer s.client.LogMessage(ctx, &protocol.LogMessageParams{
Type: protocol.Info,
Message: fmt.Sprintf("initialized workspace folders: %v", folders),
})
return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
TextDocumentSync: protocol.TextDocumentSyncOptions{
Expand Down Expand Up @@ -736,10 +745,30 @@ func (s *Server) References(ctx context.Context, params *protocol.ReferenceParam
}

// Shutdown implements protocol.Server.
func (*Server) Shutdown(context.Context) error {
func (s *Server) Shutdown(ctx context.Context) error {
s.shutdownOnce.Do(func() { s.shutdown(ctx) })
return nil
}

// Exit implements protocol.Server.
func (s *Server) Exit(ctx context.Context) error {
s.shutdownOnce.Do(func() { s.shutdown(ctx) })
return nil
}

func (s *Server) shutdown(ctx context.Context) {
slog.Info("server is shutting down")
for path := range s.caches {
s.cacheDestroyLocked(path, fmt.Errorf("server is shutting down"))
}
clear(s.caches)
for _, hook := range s.shutdownHooks {
// these must be run in a separate goroutine, since closing a jsonrpc conn
// from within the hook can trigger a deadlock.
go hook(ctx)
}
}

// DidChangeWorkspaceFolders implements protocol.Server.
func (s *Server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
added := params.Event.Added
Expand Down Expand Up @@ -1022,11 +1051,6 @@ func (*Server) DidSaveNotebookDocument(context.Context, *protocol.DidSaveNoteboo
return notImplemented("DidSaveNotebookDocument")
}

// Exit implements protocol.Server.
func (*Server) Exit(context.Context) error {
return notImplemented("Exit")
}

// FoldingRange implements protocol.Server.
func (*Server) FoldingRange(context.Context, *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
return nil, notImplemented("FoldingRange")
Expand Down
185 changes: 185 additions & 0 deletions pkg/lsprpc/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package lsprpc

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"strings"

"github.com/kralicky/protocompile/linker"
"github.com/kralicky/protols/pkg/lsp"
"github.com/kralicky/protols/pkg/util"
"github.com/kralicky/protols/sdk/codegen"
"github.com/kralicky/protols/sdk/plugin"
"github.com/kralicky/tools-lite/gopls/pkg/lsp/protocol"
"github.com/kralicky/tools-lite/pkg/event"
"github.com/kralicky/tools-lite/pkg/jsonrpc2"
"google.golang.org/protobuf/types/descriptorpb"
)

func NewStreamServer() jsonrpc2.StreamServer {
return &streamServer{}
}

type streamServer struct{}

func (s *streamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
client := protocol.ClientDispatcher(conn)

server := lsp.NewServer(client,
lsp.WithUnknownCommandHandler(
&unknownHandler{Generators: codegen.DefaultGenerators()},
"protols/generate",
"protols/generateWorkspace",
),
lsp.WithShutdownHook(func(ctx context.Context) {
conn.Close()
}),
)
handler := protocol.CancelHandler(
AsyncHandler(
jsonrpc2.MustReplyHandler(
protocol.ServerHandler(server, jsonrpc2.MethodNotFound))))
conn.Go(ctx, handler)
<-conn.Done()
if err := conn.Err(); err != nil {
return fmt.Errorf("server exited with error: %w", err)
}
return nil
}

// methods that are intended to be long-lived, and should not hold up the queue
var streamingRequestMethods = map[string]bool{
"workspace/diagnostic": true,
"workspace/executeCommand": true,
}

func AsyncHandler(handler jsonrpc2.Handler) jsonrpc2.Handler {
nextRequest := make(chan struct{})
close(nextRequest)
return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error {
waitForPrevious := nextRequest
nextRequest = make(chan struct{})
unlockNext := nextRequest
if streamingRequestMethods[req.Method()] {
close(unlockNext)
} else {
innerReply := reply
reply = func(ctx context.Context, result interface{}, err error) error {
close(unlockNext)
return innerReply(ctx, result, err)
}
}
_, queueDone := event.Start(ctx, "queued")
go func() {
<-waitForPrevious
queueDone()
if err := handler(ctx, reply, req); err != nil {
event.Error(ctx, "jsonrpc2 async message delivery failed", err)
}
}()
return nil
}
}

type unknownHandler struct {
Generators []codegen.Generator
}

// Execute implements lsp.UnknownCommandHandler.
func (h *unknownHandler) Execute(ctx context.Context, uc lsp.UnknownCommand) (any, error) {
switch uc.Command {
case "protols/generate":
var req lsp.GenerateCodeRequest
if err := json.Unmarshal(uc.Arguments[0], &req); err != nil {
return nil, err
}
if uc.Cache == nil {
return nil, errors.New("no cache available")
}
return nil, h.doGenerate(ctx, uc.Cache, req.URIs)
case "protols/generateWorkspace":
if uc.Cache == nil {
return nil, errors.New("no cache available")
}
return nil, h.doGenerate(ctx, uc.Cache, uc.Cache.XListWorkspaceLocalURIs())
default:
panic("unknown command: " + uc.Command)
}
}

var _ lsp.UnknownCommandHandler = (*unknownHandler)(nil)

func (h *unknownHandler) doGenerate(ctx context.Context, cache *lsp.Cache, uris []protocol.DocumentURI) error {
pathMappings := cache.XGetURIPathMappings()
roots := make(linker.Files, 0, len(uris))
outputDirs := map[string]string{}
for _, uri := range uris {
res, err := cache.FindResultByURI(uri)
if err != nil {
return err
}
if res.Package() == "" {
continue
}
if _, ok := res.AST().Pragma(lsp.PragmaNoGenerate); ok {
continue
}
roots = append(roots, res)
p := pathMappings.FilePathsByURI[uri]
outputDirs[path.Dir(p)] = path.Dir(uri.Path())
if opts := res.Options(); opts.ProtoReflect().IsValid() {
if goPkg := opts.(*descriptorpb.FileOptions).GoPackage; goPkg != nil {
// if the file has a different go_package than the implicit one, add
// it to the output dirs map as well
outputDirs[strings.Split(*goPkg, ";")[0]] = path.Dir(uri.Path())
}
}
}
closure := linker.ComputeReflexiveTransitiveClosure(roots)
closureResults := make([]linker.Result, len(closure))
for i, res := range closure {
closureResults[i] = res.(linker.Result)
}

plugin, err := plugin.New(roots, closureResults, pathMappings)
if err != nil {
return err
}
for _, g := range h.Generators {
if err := g.Generate(plugin); err != nil {
return err
}
}
response := plugin.Response()
if response.Error != nil {
return errors.New(response.GetError())
}
var errs error
for _, rf := range response.GetFile() {
dir, ok := outputDirs[path.Dir(rf.GetName())]
if !ok {
errs = errors.Join(errs, fmt.Errorf("cannot write outside of workspace module: %s", rf.GetName()))
continue
}
absPath := path.Join(dir, path.Base(rf.GetName()))
if info, err := os.Stat(absPath); err == nil {
original, err := os.ReadFile(absPath)
if err != nil {
return err
}
updated := rf.GetContent()
if err := util.OverwriteFile(absPath, original, []byte(updated), info.Mode().Perm(), info.Size()); err != nil {
return err
}
} else {
if err := os.WriteFile(absPath, []byte(rf.GetContent()), 0o644); err != nil {
return err
}
}
}
return errs
}
Loading

0 comments on commit 85ebe43

Please sign in to comment.