diff --git a/cmd/wireplus/main.go b/cmd/wireplus/main.go index 6ffdcbf5..effe6b27 100644 --- a/cmd/wireplus/main.go +++ b/cmd/wireplus/main.go @@ -633,7 +633,8 @@ func (*detailCmd) Synopsis() string { func (*detailCmd) Usage() string { return `detail [package] [name] - detail is essentially equivalent to show but you can specify the name + detail is equivalent to show but only shows a provider set with the given name + and does not describe injectors. ` } func (cmd *detailCmd) SetFlags(f *flag.FlagSet) { @@ -667,7 +668,7 @@ func (cmd *detailCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...inte sb.WriteString(fmt.Sprintf("\t%s\n", imp)) } for i := range outGroups { - sb.WriteString(fmt.Sprintf("\tOutputs given %s:\n", outGroups[i].name)) + sb.WriteString(fmt.Sprintf("\n\tOutputs given %s:\n", outGroups[i].name)) out := make(map[string]token.Pos, outGroups[i].outputs.Len()) outGroups[i].outputs.Iterate(func(t types.Type, v interface{}) { switch v := v.(type) { @@ -801,12 +802,13 @@ func (cmd *lspCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa } if _, ok := msg["id"]; !ok { // Notification received + // TODO: Sending as error for debugging purposes. lsp.SendError("received notification: %v\n", string(buf)) switch method { case "initialized": case "exit": return subcommands.ExitFailure - // TODO + // TODO: Support client with autosave disabled. case "textDocument/didOpen", "textDocument/didSave", "textDocument/didChange": event := &lsp.DidSaveTextDocumentNotification{} if ok := lsp.ParseRequest(buf, event); !ok { @@ -819,6 +821,8 @@ func (cmd *lspCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa lsp.SendError("invalid notification: %v\n", string(buf)) } } else { + // TODO: Sending as error for debugging purposes. + lsp.SendError("received request: %v\n", string(buf)) switch method { case "initialize": req := &lsp.InitializeRequest{} @@ -904,58 +908,79 @@ func (cmd *lspCmd) processDefinitionRequest(ctx context.Context, req *lsp.Defini return res } if len(pkgs) != 1 { - lsp.SendError("") + lsp.SendError("expected exactly one package") return res } pkg := pkgs[0] - pos := lsp.CalculatePos(pkg.Fset, url.Path, req.Params.Position.Line, req.Params.Position.Character) - file := pkg.Fset.Position(pos).Filename + line := req.Params.Position.Line + char := req.Params.Position.Character + pos := lsp.CalculatePos(pkg.Fset, url.Path, line, char) for _, f := range pkg.Syntax { - if file != url.Path { - continue - } - path, ok := astutil.PathEnclosingInterval(f, pos, pos) - if !ok { - lsp.SendError("") - return res - } - node := path[0] - if ident, ok := node.(*ast.Ident); ok { - obj := pkg.TypesInfo.ObjectOf(ident) - wd := pkg.Module.Path - pattern := []string{obj.Pkg().Path()} - pkgs, errs := wire.LoadPackages(ctx, wd, os.Environ(), cmd.tags, pattern) - if len(errs) > 0 { - lsp.SendError("") - return res - } - if len(pkgs) != 1 { - lsp.SendError("") + file := pkg.Fset.File(f.Pos()) + if base := file.Base(); base <= int(pos) && int(pos) < base+file.Size() { + path, ok := astutil.PathEnclosingInterval(f, pos, pos) + if !ok { + lsp.SendError("invalid position within file") return res } - pkg := pkgs[0] - position := pkg.Fset.Position(obj.Pos()) - line := position.Line - char := position.Column - res.Result = &lsp.Location{ - Uri: obj.Pkg().Path(), - Range: lsp.Range{ - Start: lsp.Position{ - Line: line, - Character: char, - }, - End: lsp.Position{ - Line: line, - Character: char, + node := path[0] + if ident, ok := node.(*ast.Ident); ok { + // TODO: Check this works for packages above the wd + tarObj := pkg.TypesInfo.ObjectOf(ident) + tarWd, ok := absolutePath(wd, tarObj.Pkg().Path()) + if !ok { + lsp.SendError("unknown import path") + } + tarPattern := []string{"."} + tarPkgs, errs := wire.LoadPackages(ctx, tarWd, os.Environ(), cmd.tags, tarPattern) + if len(errs) > 0 { + lsp.SendErrors(errs) + return res + } + if len(tarPkgs) != 1 { + lsp.SendError("expected exactly one package") + return res + } + tarPkg := tarPkgs[0] + // TODO: Somehow jumps to a random position + tarPosition := tarPkg.Fset.Position(tarObj.Pos()) + tarFilename := tarPosition.Filename + tarLine := tarPosition.Line + tarChar := tarPosition.Column + res.Result = &lsp.Location{ + Uri: tarFilename, + Range: lsp.Range{ + Start: lsp.Position{ + Line: tarLine, + Character: tarChar, + }, + End: lsp.Position{ + Line: tarLine, + Character: tarChar, + }, }, - }, + } + return res } - return res } } return res } +func absolutePath(wd string, importPath string) (string, bool) { + tmp := "go list -f '{{.ImportPath}}:{{.Dir}}' all | grep " + cmd := exec.Command("sh", "-c", tmp+importPath) + cmd.Dir = wd + stdout, err := cmd.CombinedOutput() + if err != nil { + return "", false + } + line := string(stdout) + parts := strings.Split(line, ":") + absPath := strings.TrimSpace(parts[1]) + return absPath, true +} + func (cmd *lspCmd) processCodeLensRequest(ctx context.Context, req *lsp.CodeLensRequest) *lsp.CodeLensResponse { res := &lsp.CodeLensResponse{ Jsonrpc: "2.0", @@ -978,27 +1003,35 @@ func (cmd *lspCmd) processCodeLensRequest(ctx context.Context, req *lsp.CodeLens } var codeLenses []lsp.CodeLens for _, inj := range info.Injectors { + file := info.Fset.File(inj.Pos) + if file.Name() != url.Path { + continue + } codeLenses = append(codeLenses, makeCodeLens( info, inj.Pos, "Show Graph", - "exampleExtension.showGraph", + "wireplus.showGraph", []interface{}{wd, inj.FuncName}), ) } for _, set := range info.Sets { + file := info.Fset.File(set.Pos) + if file.Name() != url.Path { + continue + } codeLenses = append(codeLenses, makeCodeLens( info, set.Pos, "Show Graph", - "exampleExtension.showGraph", + "wireplus.showGraph", []interface{}{wd, set.VarName}), ) codeLenses = append(codeLenses, makeCodeLens( info, set.Pos, - "Show Details", - "exampleExtension.showDetail", + "Show Detail", + "wireplus.showDetail", []interface{}{wd, set.VarName}), ) } @@ -1022,8 +1055,8 @@ func makeCodeLens(info *wire.Info, pos token.Pos, title string, cmd string, args }, }, Command: lsp.Command{ - Title: "Show Graph", - Command: "exampleExtension.showGraph", + Title: title, + Command: cmd, Arguments: args, }, } @@ -1041,13 +1074,13 @@ func (cmd *lspCmd) createPublishDiagnosticsNotification(ctx context.Context, eve // to clear existing diagnostics diags := make([]lsp.Diagnostic, 0) for _, err := range errs { - werr := err.(*wire.WireErr) - file := werr.Position().Filename - if file != url.Path { + wireErr := err.(*wire.WireErr) + position := wireErr.Position() + if position.Filename != url.Path { continue } - line := werr.Position().Line - 1 - char := werr.Position().Column - 1 + line := wireErr.Position().Line - 1 + char := wireErr.Position().Column - 1 diags = append(diags, lsp.Diagnostic{ Range: lsp.Range{ Start: lsp.Position{ @@ -1059,7 +1092,7 @@ func (cmd *lspCmd) createPublishDiagnosticsNotification(ctx context.Context, eve Character: 0, }, }, - Message: werr.Message(), + Message: wireErr.Message(), }) } notif := &lsp.PublishDiagnosticsNotification{ diff --git a/internal/wire/analyze.go b/internal/wire/analyze.go index 1182fc72..7377a0eb 100644 --- a/internal/wire/analyze.go +++ b/internal/wire/analyze.go @@ -637,7 +637,7 @@ type buildSolution struct { } func solveForBuild(pkg *packages.Package, name string) (*buildSolution, []error) { - fn := findFuncDeclByName(pkg, name) + fn := findFuncDecl(pkg, name) if fn == nil { return nil, []error{fmt.Errorf("no function named %s found", name)} } @@ -695,7 +695,7 @@ type newSetSolution struct { } func solveForNewSet(pkg *packages.Package, name string) (*newSetSolution, []error) { - set := findVarExprByName(pkg, name) + set := findVarExpr(pkg, name) if set == nil { return nil, []error{fmt.Errorf("no value named %s found", name)} } diff --git a/internal/wire/graph.go b/internal/wire/graph.go index 790eeb8f..45824511 100644 --- a/internal/wire/graph.go +++ b/internal/wire/graph.go @@ -30,30 +30,19 @@ func Graph(ctx context.Context, wd string, env []string, pattern []string, name return nil, []error{fmt.Errorf("expected exactly one package")} } pkg := pkgs[0] - fn := findFuncDeclByName(pkg, name) - set := findVarExprByName(pkg, name) - - switch { - case set != nil: - sol, errs := solveForNewSet(pkg, name) - if len(errs) > 0 { - return nil, errs - } + if sol, errs := solveForNewSet(pkg, name); len(errs) == 0 { addInputsForNewSet(gviz, sol.missing) addOutputs(gviz, sol.calls, sol.pset, pkg.Fset) addDepsForNewSet(gviz, sol.calls, sol.missing, pkg.Fset) - case fn != nil: - sol, errs := solveForBuild(pkg, name) - if len(errs) > 0 { - return nil, errs - } + return gviz, nil + } + if sol, errs := solveForBuild(pkg, name); len(errs) == 0 { addInputsForBuild(gviz, sol.ins) addOutputs(gviz, sol.calls, sol.pset, pkg.Fset) addDepsForBuild(gviz, sol.calls, sol.ins, pkg.Fset) - default: - return nil, []error{fmt.Errorf("no function or variable named %q found", name)} + return gviz, nil } - return gviz, nil + return nil, errs } func addInputsForNewSet(gviz *Graphviz, missing []*types.Type) { diff --git a/internal/wire/lsp/lsp.go b/internal/wire/lsp/lsp.go index 739f9eb6..7cf18e40 100644 --- a/internal/wire/lsp/lsp.go +++ b/internal/wire/lsp/lsp.go @@ -20,11 +20,11 @@ func ReadBuffer(reader *bufio.Reader) ([]byte, bool) { fmt.Fprintf(os.Stderr, "error reading header: %v\n", err) return nil, false } - if header == "\r\n" { - break - } // TODO: trim the remaining \r also header = strings.TrimSpace(header) + if header == "" { + break + } switch { case strings.HasPrefix(header, "Content-Length: "): value := strings.TrimPrefix(header, "Content-Length: ") @@ -106,6 +106,7 @@ func ParseDocumentUri(uri string) *url.URL { return url } +// line and char must be zero-based func CalculatePos(fset *token.FileSet, path string, line int, char int) token.Pos { var file *token.File fset.Iterate(func(f *token.File) bool { @@ -115,7 +116,7 @@ func CalculatePos(fset *token.FileSet, path string, line int, char int) token.Po } return true }) - offset := int(file.LineStart(line)) - int(file.Base()) - offset += char - return file.Pos(offset) + // LineStart accepts one-based line number + start := file.LineStart(line + 1) + return token.Pos(int(start) + char) } diff --git a/internal/wire/lsp/types.go b/internal/wire/lsp/types.go index 6e6ab27c..4885ef7d 100644 --- a/internal/wire/lsp/types.go +++ b/internal/wire/lsp/types.go @@ -62,7 +62,7 @@ type InitializeResult struct { type ServerCapabilities struct { TextDocumentSync int `json:"textDocumentSync"` CodeLensProvider bool `json:"codeLensProvider"` - DefinitionProvider bool `json:"definition"` + DefinitionProvider bool `json:"definitionProvider"` Workspace WorkspaceServerCapabilities `json:"workspace"` } diff --git a/internal/wire/parse.go b/internal/wire/parse.go index cdefafea..f1bc8ee2 100644 --- a/internal/wire/parse.go +++ b/internal/wire/parse.go @@ -1147,6 +1147,40 @@ func findInjectorNewSet(info *types.Info, expr *ast.Expr) (*ast.CallExpr, error) return call, nil } +func findFuncDecl(pkg *packages.Package, name string) *ast.FuncDecl { + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + if fn.Name.Name == name { + return fn + } + } + } + } + return nil +} + +// TODO: can be replaced by types.Scope +func findVarExpr(pkg *packages.Package, name string) *ast.Expr { + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + // Matching against top-level variable declaration with the given name. + if gDecl, ok := decl.(*ast.GenDecl); ok && gDecl.Tok == token.VAR { + for _, spec := range gDecl.Specs { + if vSpec, ok := spec.(*ast.ValueSpec); ok { + for i, ident := range vSpec.Names { + if ident.Name == name { + return &vSpec.Values[i] + } + } + } + } + } + } + } + return nil +} + func isWireImport(path string) bool { // TODO(light): This is depending on details of the current loader. const vendorPart = "vendor/" @@ -1259,37 +1293,3 @@ func bindShouldUsePointer(info *types.Info, call *ast.CallExpr) bool { wireName := info.ObjectOf(pkgName).(*types.PkgName) // wire package return wireName.Imported().Scope().Lookup("bindToUsePointer") != nil } - -func findFuncDeclByName(pkg *packages.Package, name string) *ast.FuncDecl { - for _, f := range pkg.Syntax { - for _, decl := range f.Decls { - if fn, ok := decl.(*ast.FuncDecl); ok { - if fn.Name.Name == name { - return fn - } - } - } - } - return nil -} - -// TODO: can be replaced by types.Scope -func findVarExprByName(pkg *packages.Package, name string) *ast.Expr { - for _, f := range pkg.Syntax { - for _, decl := range f.Decls { - // Matching against top-level variable declaration with the given name. - if gDecl, ok := decl.(*ast.GenDecl); ok && gDecl.Tok == token.VAR { - for _, spec := range gDecl.Specs { - if vSpec, ok := spec.(*ast.ValueSpec); ok { - for i, ident := range vSpec.Names { - if ident.Name == name { - return &vSpec.Values[i] - } - } - } - } - } - } - } - return nil -}