From 5b32042675aade5f05df995ae5c7a53da6faa14c Mon Sep 17 00:00:00 2001 From: lighttiger2505 Date: Fri, 19 Mar 2021 21:59:47 +0900 Subject: [PATCH] feat: Add definition --- internal/handler/definition.go | 85 ++++++++++++++++++++ internal/handler/definition_test.go | 117 ++++++++++++++++++++++++++++ internal/handler/handler.go | 6 +- internal/handler/handler_test.go | 2 +- internal/lsp/lsp.go | 14 ++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 internal/handler/definition.go create mode 100644 internal/handler/definition_test.go diff --git a/internal/handler/definition.go b/internal/handler/definition.go new file mode 100644 index 0000000..99a0724 --- /dev/null +++ b/internal/handler/definition.go @@ -0,0 +1,85 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/lighttiger2505/sqls/ast" + "github.com/lighttiger2505/sqls/ast/astutil" + "github.com/lighttiger2505/sqls/internal/database" + "github.com/lighttiger2505/sqls/internal/lsp" + "github.com/lighttiger2505/sqls/parser" + "github.com/lighttiger2505/sqls/parser/parseutil" + "github.com/lighttiger2505/sqls/token" + "github.com/sourcegraph/jsonrpc2" +) + +func (s *Server) handleDefinition(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { + if req.Params == nil { + return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} + } + + var params lsp.DefinitionParams + if err := json.Unmarshal(*req.Params, ¶ms); err != nil { + return nil, err + } + + f, ok := s.files[params.TextDocument.URI] + if !ok { + return nil, fmt.Errorf("document not found: %s", params.TextDocument.URI) + } + + return definition(params.TextDocument.URI, f.Text, params, s.worker.Cache()) +} + +func definition(url, text string, params lsp.DefinitionParams, dbCache *database.DBCache) (lsp.Definition, error) { + pos := token.Pos{ + Line: params.Position.Line, + Col: params.Position.Character + 1, + } + parsed, err := parser.Parse(text) + if err != nil { + return nil, err + } + + nodeWalker := parseutil.NewNodeWalker(parsed, pos) + m := astutil.NodeMatcher{ + NodeTypes: []ast.NodeType{ast.TypeIdentifer}, + } + currentVariable := nodeWalker.CurNodeButtomMatched(m) + if currentVariable == nil { + return nil, nil + } + + aliases := parseutil.ExtractAliasedIdentifer(parsed) + if len(aliases) == 0 { + return nil, nil + } + + var define ast.Node + for _, v := range aliases { + alias, _ := v.(*ast.Aliased) + if alias.AliasedName.String() == currentVariable.String() { + define = alias.AliasedName + } + } + + res := []lsp.Location{ + { + URI: url, + Range: lsp.Range{ + Start: lsp.Position{ + Line: define.Pos().Line, + Character: define.Pos().Col, + }, + End: lsp.Position{ + Line: define.End().Line, + Character: define.End().Col, + }, + }, + }, + } + + return res, nil +} diff --git a/internal/handler/definition_test.go b/internal/handler/definition_test.go new file mode 100644 index 0000000..d1ebd4e --- /dev/null +++ b/internal/handler/definition_test.go @@ -0,0 +1,117 @@ +package handler + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/lighttiger2505/sqls/internal/config" + "github.com/lighttiger2505/sqls/internal/database" + "github.com/lighttiger2505/sqls/internal/lsp" +) + +var definitionTestCases = []struct { + name string + input string + output lsp.Definition + pos lsp.Position +}{ + { + name: "", + input: "SELECT ci.ID, ci.Name FROM city as ci", + output: []lsp.Location{ + { + URI: testFileURI, + Range: lsp.Range{ + Start: lsp.Position{ + Line: 0, + Character: 35, + }, + End: lsp.Position{ + Line: 0, + Character: 37, + }, + }, + }, + }, + pos: lsp.Position{ + Line: 0, + Character: 8, + }, + }, +} + +func TestDefinition(t *testing.T) { + tx := newTestContext() + tx.setup(t) + defer tx.tearDown() + + cfg := &config.Config{ + Connections: []*database.DBConfig{ + {Driver: "mock"}, + }, + } + tx.addWorkspaceConfig(t, cfg) + + for _, tt := range definitionTestCases { + t.Run(tt.name, func(t *testing.T) { + tx.textDocumentDidOpen(t, testFileURI, tt.input) + + params := lsp.DefinitionParams{ + TextDocumentPositionParams: lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{ + URI: testFileURI, + }, + Position: tt.pos, + }, + } + var got lsp.Definition + err := tx.conn.Call(tx.ctx, "textDocument/definition", params, &got) + if err != nil { + t.Errorf("conn.Call textDocument/definition: %+v", err) + return + } + + if diff := cmp.Diff(tt.output, got); diff != "" { + t.Errorf("unmatch hover contents (- want, + got):\n%s", diff) + } + }) + } +} + +func TestTypeDefinition(t *testing.T) { + tx := newTestContext() + tx.setup(t) + defer tx.tearDown() + + cfg := &config.Config{ + Connections: []*database.DBConfig{ + {Driver: "mock"}, + }, + } + tx.addWorkspaceConfig(t, cfg) + + for _, tt := range definitionTestCases { + t.Run(tt.name, func(t *testing.T) { + tx.textDocumentDidOpen(t, testFileURI, tt.input) + + params := lsp.DefinitionParams{ + TextDocumentPositionParams: lsp.TextDocumentPositionParams{ + TextDocument: lsp.TextDocumentIdentifier{ + URI: testFileURI, + }, + Position: tt.pos, + }, + } + var got lsp.Definition + err := tx.conn.Call(tx.ctx, "textDocument/typeDefinition", params, &got) + if err != nil { + t.Errorf("conn.Call textDocument/definition: %+v", err) + return + } + + if diff := cmp.Diff(tt.output, got); diff != "" { + t.Errorf("unmatch hover contents (- want, + got):\n%s", diff) + } + }) + } +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 2ec4d02..0a153be 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -120,6 +120,10 @@ func (s *Server) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2. return s.handleTextDocumentSignatureHelp(ctx, conn, req) case "textDocument/rename": return s.handleTextDocumentRename(ctx, conn, req) + case "textDocument/definition": + return s.handleDefinition(ctx, conn, req) + case "textDocument/typeDefinition": + return s.handleDefinition(ctx, conn, req) } return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeMethodNotFound, Message: fmt.Sprintf("method not supported: %s", req.Method)} } @@ -149,7 +153,7 @@ func (s *Server) handleInitialize(ctx context.Context, conn *jsonrpc2.Conn, req WorkDoneProgress: false, }, }, - DefinitionProvider: false, + DefinitionProvider: true, DocumentFormattingProvider: true, DocumentRangeFormattingProvider: true, RenameProvider: true, diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index fa2ea5c..028c677 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -118,7 +118,7 @@ func TestInitialized(t *testing.T) { }, }, CodeActionProvider: true, - DefinitionProvider: false, + DefinitionProvider: true, DocumentFormattingProvider: true, DocumentRangeFormattingProvider: true, RenameProvider: true, diff --git a/internal/lsp/lsp.go b/internal/lsp/lsp.go index 633fea4..da29bac 100644 --- a/internal/lsp/lsp.go +++ b/internal/lsp/lsp.go @@ -486,3 +486,17 @@ type OptionalVersionedTextDocumentIdentifier struct { Version int32 `json:"version"` TextDocumentIdentifier } + +type DefinitionParams struct { + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +type TypeDefinitionParams struct { + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +type Definition = []Location