diff --git a/go.mod b/go.mod index 683e5ba..168cedb 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,21 @@ go 1.21.1 require ( github.com/google/go-github v17.0.0+incompatible + github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/spf13/cobra v1.5.0 go.lsp.dev/jsonrpc2 v0.10.0 go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 go.lsp.dev/protocol v0.12.0 + golang.org/x/mod v0.12.0 + mvdan.cc/gofumpt v0.4.0 ) require ( + github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.3.4 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -23,4 +28,5 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/sys v0.12.0 // indirect + golang.org/x/tools v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 37abcf4..e4acfd1 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -16,8 +20,14 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= +github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= @@ -52,8 +62,12 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= diff --git a/internal/lsp/general.go b/internal/lsp/general.go new file mode 100644 index 0000000..0dfda04 --- /dev/null +++ b/internal/lsp/general.go @@ -0,0 +1,75 @@ +package lsp + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + + "go.lsp.dev/jsonrpc2" + "go.lsp.dev/protocol" +) + +func (s *server) DidOpen(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { + var params protocol.DidOpenTextDocumentParams + if err := json.Unmarshal(req.Params(), ¶ms); err != nil { + return sendParseError(ctx, reply, err) + } + + uri := params.TextDocument.URI + file := &GnoFile{ + URI: uri, + Src: []byte(params.TextDocument.Text), + } + s.snapshot.file.Set(uri.Filename(), file) + + slog.Info("open " + string(params.TextDocument.URI.Filename())) + return reply(ctx, nil, nil) +} + +func (s *server) DidClose(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { + var params protocol.DidChangeTextDocumentParams + if err := json.Unmarshal(req.Params(), ¶ms); err != nil { + return sendParseError(ctx, reply, err) + } + + slog.Info("close" + string(params.TextDocument.URI.Filename())) + return reply(ctx, s.conn.Notify(ctx, protocol.MethodTextDocumentDidClose, nil), nil) +} + +func (s *server) DidChange(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { + var params protocol.DidChangeTextDocumentParams + if err := json.Unmarshal(req.Params(), ¶ms); err != nil { + return sendParseError(ctx, reply, err) + } + + uri := params.TextDocument.URI + _, ok := s.snapshot.Get(uri.Filename()) + if !ok { + return reply(ctx, nil, errors.New("snapshot not found")) + } + + file := &GnoFile{ + URI: uri, + Src: []byte(params.ContentChanges[0].Text), + } + s.snapshot.file.Set(uri.Filename(), file) + + slog.Info("change " + string(params.TextDocument.URI.Filename())) + return reply(ctx, nil, nil) +} + +func (s *server) DidSave(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { + var params protocol.DidSaveTextDocumentParams + if err := json.Unmarshal(req.Params(), ¶ms); err != nil { + return sendParseError(ctx, reply, err) + } + + uri := params.TextDocument.URI + file, ok := s.snapshot.Get(uri.Filename()) + if !ok { + return reply(ctx, nil, errors.New("snapshot not found")) + } + + slog.Info("save " + string(uri.Filename())) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 5c2c6fe..881c5c3 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -18,6 +18,9 @@ type server struct { conn jsonrpc2.Conn env *env.Env + snapshot *Snapshot + + formatOpt tools.FormattingOption } func BuildServerHandler(conn jsonrpc2.Conn, env *env.Env) jsonrpc2.Handler { @@ -31,6 +34,9 @@ func BuildServerHandler(conn jsonrpc2.Conn, env *env.Env) jsonrpc2.Handler { env: env, + snapshot: NewSnapshot(), + + formatOpt: tools.Gofumpt, } return jsonrpc2.ReplyHandler(server.ServerHandler) @@ -46,6 +52,14 @@ func (s *server) ServerHandler(ctx context.Context, reply jsonrpc2.Replier, req return s.Initialized(ctx, reply, req) case "shutdown": return s.Shutdown(ctx, reply, req) + case "textDocument/didChange": + return s.DidChange(ctx, reply, req) + case "textDocument/didClose": + return s.DidClose(ctx, reply, req) + case "textDocument/didOpen": + return s.DidOpen(ctx, reply, req) + case "textDocument/didSave": + return s.DidSave(ctx, reply, req) default: return jsonrpc2.MethodNotFoundHandler(ctx, reply, req) } diff --git a/internal/lsp/snapshot.go b/internal/lsp/snapshot.go new file mode 100644 index 0000000..5387845 --- /dev/null +++ b/internal/lsp/snapshot.go @@ -0,0 +1,124 @@ +package lsp + +import ( + "context" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "log/slog" + "strings" + "unicode/utf8" + + "go.lsp.dev/protocol" + "golang.org/x/mod/modfile" + + cmap "github.com/orcaman/concurrent-map/v2" +) + +type Snapshot struct { + file cmap.ConcurrentMap[string, *GnoFile] +} + +func NewSnapshot() *Snapshot { + return &Snapshot{ + file: cmap.New[*GnoFile](), + } +} + +func (s *Snapshot) Get(filePath string) (*GnoFile, bool) { + return s.file.Get(filePath) +} + +// contains gno file. +type GnoFile struct { + URI protocol.DocumentURI + Src []byte +} + +// contains parsed gno file. +type ParsedGnoFile struct { + URI protocol.DocumentURI + File *ast.File + + Src []byte +} + +func (f *GnoFile) ParseGno(ctx context.Context) (*ParsedGnoFile, error) { + fset := token.NewFileSet() + ast, err := parser.ParseFile(fset, f.URI.Filename(), nil, parser.ParseComments) + if err != nil { + return nil, err + } + + pgf := &ParsedGnoFile{ + URI: f.URI, + + File: ast, + Src: f.Src, + } + + return pgf, nil +} + +// contains parsed gno.mod file. +type ParsedGnoMod struct { + URI string + File *modfile.File +} + +func (f *GnoFile) TokenAt(pos protocol.Position) (*HoveredToken, error) { + lines := strings.SplitAfter(string(f.Src), "\n") + + size := uint32(len(lines)) + if pos.Line >= size { + return nil, errors.New("line out of range") + } + + line := lines[pos.Line] + lineLen := uint32(len(line)) + + // TODO: fix it. should not happen? + if len(line) == 0 { + return nil, errors.New("no token found") + } + + index := pos.Character + start := index + // TODO: fix it. should not happen? + if lineLen < start { + return nil, errors.New("start is greater than len") + } + for start > 0 && line[start-1] != ' ' { + start-- + } + + end := index + slog.Info(fmt.Sprintf("end: %d", end)) + for end < lineLen && line[end] != ' ' { + end++ + } + + if start == end { + return nil, errors.New("no token found") + } + + return &HoveredToken{ + Text: line[start:end], + Start: int(start), + End: int(end), + }, nil +} + +func (f *GnoFile) PositionToOffset(pos protocol.Position) int { + lines := strings.SplitAfter(string(f.Src), "\n") + offset := 0 + for i, l := range lines { + if i == int(pos.Line) { + break + } + offset += utf8.RuneCountInString(l) + } + return offset + int(pos.Character) +} diff --git a/internal/tools/format.go b/internal/tools/format.go new file mode 100644 index 0000000..5249842 --- /dev/null +++ b/internal/tools/format.go @@ -0,0 +1,9 @@ +package tools + +type FormattingOption int + +const ( + Gofmt FormattingOption = iota + Gofumpt +) +