diff --git a/.travis.yml b/.travis.yml index 011789e1..87f786e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,5 @@ language: go go: - 1.9.x - 1.10.x + - 1.11.x - master diff --git a/client.go b/client.go index cfcff9db..0e959807 100644 --- a/client.go +++ b/client.go @@ -128,6 +128,7 @@ func cmdAutoComplete(c *rpc.Client) { // For now, assume same environment for server and client. req.Context = &suggest.PackedContext{} req.Builtin = *g_builtin + req.IgnoreCase = *g_ignore_case var res AutoCompleteReply var err error diff --git a/gocode.go b/gocode.go index 87fbf3cd..7cdae64b 100644 --- a/gocode.go +++ b/gocode.go @@ -8,14 +8,15 @@ import ( ) var ( - g_is_server = flag.Bool("s", false, "run a server instead of a client") - g_format = flag.String("f", "nice", "output format (vim | emacs | nice | csv | json)") - g_input = flag.String("in", "", "use this file instead of stdin input") - g_sock = flag.String("sock", defaultSocketType, "socket type (unix | tcp | none)") - g_addr = flag.String("addr", "127.0.0.1:37373", "address for tcp socket") - g_debug = flag.Bool("debug", false, "enable server-side debug mode") - g_source = flag.Bool("source", false, "use source importer") - g_builtin = flag.Bool("builtin", false, "propose builtin objects") + g_is_server = flag.Bool("s", false, "run a server instead of a client") + g_format = flag.String("f", "nice", "output format (vim | emacs | nice | csv | json)") + g_input = flag.String("in", "", "use this file instead of stdin input") + g_sock = flag.String("sock", defaultSocketType, "socket type (unix | tcp | none)") + g_addr = flag.String("addr", "127.0.0.1:37373", "address for tcp socket") + g_debug = flag.Bool("debug", false, "enable server-side debug mode") + g_source = flag.Bool("source", false, "use source importer") + g_builtin = flag.Bool("builtin", false, "propose builtin objects") + g_ignore_case = flag.Bool("ignore-case", false, "do case-insensitive matching") ) func getSocketPath() string { diff --git a/internal/suggest/candidate.go b/internal/suggest/candidate.go index d83bda90..7bfd64be 100644 --- a/internal/suggest/candidate.go +++ b/internal/suggest/candidate.go @@ -2,6 +2,7 @@ package suggest import ( "fmt" + "go/ast" "go/types" "sort" "strings" @@ -77,10 +78,12 @@ func classifyObject(obj types.Object) string { type candidateCollector struct { exact []types.Object badcase []types.Object + imports []*ast.ImportSpec localpkg *types.Package partial string filter objectFilter builtin bool + ignoreCase bool } func (b *candidateCollector) getCandidates() []Candidate { @@ -162,6 +165,26 @@ func (b *candidateCollector) qualify(pkg *types.Package) string { if pkg == b.localpkg { return "" } + + // the *types.Package we are asked to qualify might _not_ be imported + // by the file in which we are asking for candidates. Hence... we retain + // the default of pkg.Name() as the qualifier + + for _, i := range b.imports { + // given the import spec has been correctly parsed (by virtue of + // its existence) we can safely byte-index the path value knowing + // that len("\"") == 1 + iPath := i.Path.Value[1 : len(i.Path.Value)-1] + + if iPath == pkg.Path() { + if i.Name != nil && i.Name.Name != "." { + return i.Name.Name + } else { + return pkg.Name() + } + } + } + return pkg.Name() } @@ -180,8 +203,7 @@ func (b *candidateCollector) appendObject(obj types.Object) { if b.filter != nil && !b.filter(obj) { return } - - if b.filter != nil || strings.HasPrefix(obj.Name(), b.partial) { + if !b.ignoreCase && (b.filter != nil || strings.HasPrefix(obj.Name(), b.partial)) { b.exact = append(b.exact, obj) } else if strings.HasPrefix(strings.ToLower(obj.Name()), strings.ToLower(b.partial)) { b.badcase = append(b.badcase, obj) diff --git a/internal/suggest/suggest.go b/internal/suggest/suggest.go index b3f8bf6e..38b0ceb2 100644 --- a/internal/suggest/suggest.go +++ b/internal/suggest/suggest.go @@ -8,6 +8,7 @@ import ( "go/token" "go/types" "os" + "strings" "sync" "github.com/stamblerre/gocode/internal/lookdot" @@ -15,9 +16,10 @@ import ( ) type Config struct { - Logf func(fmt string, args ...interface{}) - Context *PackedContext - Builtin bool + Logf func(fmt string, args ...interface{}) + Context *PackedContext + Builtin bool + IgnoreCase bool } // Copied from go/packages. @@ -49,7 +51,7 @@ func (c *Config) Suggest(filename string, data []byte, cursor int) ([]Candidate, return nil, 0 } - fset, pos, pkg := c.analyzePackage(filename, data, cursor) + fset, pos, pkg, imports := c.analyzePackage(filename, data, cursor) if pkg == nil { return nil, 0 } @@ -57,10 +59,12 @@ func (c *Config) Suggest(filename string, data []byte, cursor int) ([]Candidate, ctx, expr, partial := deduceCursorContext(data, cursor) b := candidateCollector{ - localpkg: pkg, - partial: partial, - filter: objectFilters[partial], - builtin: ctx != selectContext && c.Builtin, + localpkg: pkg, + imports: imports, + partial: partial, + filter: objectFilters[partial], + builtin: ctx != selectContext && c.Builtin, + ignoreCase: c.IgnoreCase, } switch ctx { @@ -99,19 +103,20 @@ func (c *Config) Suggest(filename string, data []byte, cursor int) ([]Candidate, return res, len(partial) } -func sameFile(filename1, filename2 string) bool { - finfo1, err := os.Stat(filename1) - if err != nil { - return false +func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*token.FileSet, token.Pos, *types.Package, []*ast.ImportSpec) { + var tags string + parsed, _ := parser.ParseFile(token.NewFileSet(), filename, data, parser.ParseComments) + if parsed != nil && len(parsed.Comments) > 0 { + buildTagText := parsed.Comments[0].Text() + if strings.HasPrefix(buildTagText, "+build ") { + tags = strings.TrimPrefix(buildTagText, "+build ") + } } - finfo2, err := os.Stat(filename2) - if err != nil { - return false + if suffix := buildConstraint(filename); suffix != "" { + tags = suffix } - return os.SameFile(finfo1, finfo2) -} -func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*token.FileSet, token.Pos, *types.Package) { + var fileAST *ast.File var pos token.Pos var posMu sync.Mutex // guards pos in ParseFile @@ -119,9 +124,9 @@ func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*toke Mode: packages.LoadSyntax, Env: c.Context.Env, Dir: c.Context.Dir, - BuildFlags: c.Context.BuildFlags, + BuildFlags: append(c.Context.BuildFlags, fmt.Sprintf("-tags=%s", tags)), Tests: true, - ParseFile: func(fset *token.FileSet, parseFilename string) (*ast.File, error) { + ParseFile: func(fset *token.FileSet, parseFilename string, _ []byte) (*ast.File, error) { var src interface{} var filePos token.Pos mode := parser.DeclarationErrors @@ -137,6 +142,7 @@ func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*toke return nil, err } if sameFile(filename, parseFilename) { + fileAST = file filePos = fset.File(file.Pos()).Pos(cursor) if filePos == token.NoPos { return nil, fmt.Errorf("no position for cursor in %s", parseFilename) @@ -159,11 +165,23 @@ func (c *Config) analyzePackage(filename string, data []byte, cursor int) (*toke } pkgs, _ := packages.Load(cfg, fmt.Sprintf("contains:%v", filename)) if len(pkgs) <= 0 { // ignore errors - return nil, token.NoPos, nil + return nil, token.NoPos, nil, nil } pkg := pkgs[0] - return pkg.Fset, pos, pkg.Types + return pkg.Fset, pos, pkg.Types, fileAST.Imports +} + +func sameFile(filename1, filename2 string) bool { + finfo1, err := os.Stat(filename1) + if err != nil { + return false + } + finfo2, err := os.Stat(filename2) + if err != nil { + return false + } + return os.SameFile(finfo1, finfo2) } func (c *Config) fieldNameCandidates(typ types.Type, b *candidateCollector) { diff --git a/internal/suggest/suggest_test.go b/internal/suggest/suggest_test.go index 938c5c96..5ca0c2e1 100644 --- a/internal/suggest/suggest_test.go +++ b/internal/suggest/suggest_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "strings" "testing" @@ -19,6 +20,11 @@ func TestRegress(t *testing.T) { } for _, testDir := range testDirs { + // Skip test.0011 for Go <= 1.11 because a method was added to reflect.Value. + // TODO(rstambler): Change this when Go 1.12 comes out. + if !strings.HasPrefix(runtime.Version(), "devel") && strings.HasSuffix(testDir, "test.0011") { + continue + } testDir := testDir // capture name := strings.TrimPrefix(testDir, "testdata/") t.Run(name, func(t *testing.T) { @@ -81,3 +87,12 @@ func testRegress(t *testing.T, testDir string) { return } } + +func contains(haystack []string, needle string) bool { + for _, x := range haystack { + if needle == x { + return true + } + } + return false +} \ No newline at end of file diff --git a/internal/suggest/testdata/test.0065/out.expected b/internal/suggest/testdata/test.0065/out.expected index 33630698..040ba3c3 100644 --- a/internal/suggest/testdata/test.0065/out.expected +++ b/internal/suggest/testdata/test.0065/out.expected @@ -1,2 +1,26 @@ -Found 1 candidates: - func New(text string) error +Found 25 candidates: + func Errorf(format string, a ...interface{}) error + func Fprint(w banana.Writer, a ...interface{}) (n int, err error) + func Fprintf(w banana.Writer, format string, a ...interface{}) (n int, err error) + func Fprintln(w banana.Writer, a ...interface{}) (n int, err error) + func Fscan(r banana.Reader, a ...interface{}) (n int, err error) + func Fscanf(r banana.Reader, format string, a ...interface{}) (n int, err error) + func Fscanln(r banana.Reader, a ...interface{}) (n int, err error) + func Print(a ...interface{}) (n int, err error) + func Printf(format string, a ...interface{}) (n int, err error) + func Println(a ...interface{}) (n int, err error) + func Scan(a ...interface{}) (n int, err error) + func Scanf(format string, a ...interface{}) (n int, err error) + func Scanln(a ...interface{}) (n int, err error) + func Sprint(a ...interface{}) string + func Sprintf(format string, a ...interface{}) string + func Sprintln(a ...interface{}) string + func Sscan(str string, a ...interface{}) (n int, err error) + func Sscanf(str string, format string, a ...interface{}) (n int, err error) + func Sscanln(str string, a ...interface{}) (n int, err error) + type Formatter interface + type GoStringer interface + type ScanState interface + type Scanner interface + type State interface + type Stringer interface diff --git a/internal/suggest/testdata/test.0065/test.go.in b/internal/suggest/testdata/test.0065/test.go.in index 74646e9c..2120ae51 100644 --- a/internal/suggest/testdata/test.0065/test.go.in +++ b/internal/suggest/testdata/test.0065/test.go.in @@ -1,7 +1,8 @@ package p -import "errors" +import ( + "fmt" + banana "io" +) -func f() { - errors.@ -} +var _ = fmt.@ diff --git a/internal/suggest/testdata/test.0066/config.json b/internal/suggest/testdata/test.0066/config.json new file mode 100644 index 00000000..e30fee59 --- /dev/null +++ b/internal/suggest/testdata/test.0066/config.json @@ -0,0 +1 @@ +{"IgnoreCase": false} diff --git a/internal/suggest/testdata/test.0066/out.expected b/internal/suggest/testdata/test.0066/out.expected new file mode 100644 index 00000000..4dbcfa95 --- /dev/null +++ b/internal/suggest/testdata/test.0066/out.expected @@ -0,0 +1,2 @@ +Found 1 candidates: + func FuncX() diff --git a/internal/suggest/testdata/test.0066/test.go.in b/internal/suggest/testdata/test.0066/test.go.in new file mode 100644 index 00000000..d4f2f733 --- /dev/null +++ b/internal/suggest/testdata/test.0066/test.go.in @@ -0,0 +1,8 @@ +package p + +func FuncX() {} +func funcY() {} + +func f() { + Fun@ +} diff --git a/internal/suggest/testdata/test.0067/config.json b/internal/suggest/testdata/test.0067/config.json new file mode 100644 index 00000000..e30fee59 --- /dev/null +++ b/internal/suggest/testdata/test.0067/config.json @@ -0,0 +1 @@ +{"IgnoreCase": false} diff --git a/internal/suggest/testdata/test.0067/out.expected b/internal/suggest/testdata/test.0067/out.expected new file mode 100644 index 00000000..eb986f88 --- /dev/null +++ b/internal/suggest/testdata/test.0067/out.expected @@ -0,0 +1,2 @@ +Found 1 candidates: + func funcY() diff --git a/internal/suggest/testdata/test.0067/test.go.in b/internal/suggest/testdata/test.0067/test.go.in new file mode 100644 index 00000000..0efabfee --- /dev/null +++ b/internal/suggest/testdata/test.0067/test.go.in @@ -0,0 +1,8 @@ +package p + +func FuncX() {} +func funcY() {} + +func f() { + fun@ +} diff --git a/internal/suggest/testdata/test.0068/config.json b/internal/suggest/testdata/test.0068/config.json new file mode 100644 index 00000000..30d49ee2 --- /dev/null +++ b/internal/suggest/testdata/test.0068/config.json @@ -0,0 +1 @@ +{"IgnoreCase": true} diff --git a/internal/suggest/testdata/test.0068/out.expected b/internal/suggest/testdata/test.0068/out.expected new file mode 100644 index 00000000..1b60778a --- /dev/null +++ b/internal/suggest/testdata/test.0068/out.expected @@ -0,0 +1,3 @@ +Found 2 candidates: + func FuncX() + func funcY() diff --git a/internal/suggest/testdata/test.0068/test.go.in b/internal/suggest/testdata/test.0068/test.go.in new file mode 100644 index 00000000..0efabfee --- /dev/null +++ b/internal/suggest/testdata/test.0068/test.go.in @@ -0,0 +1,8 @@ +package p + +func FuncX() {} +func funcY() {} + +func f() { + fun@ +} diff --git a/server.go b/server.go index 51f6b941..d1f2f86e 100644 --- a/server.go +++ b/server.go @@ -49,11 +49,13 @@ type Server struct { } type AutoCompleteRequest struct { - Context *suggest.PackedContext - Filename string - Data []byte - Cursor int - Builtin bool + Filename string + Data []byte + Cursor int + Context gbimporter.PackedContext + Source bool + Builtin bool + IgnoreCase bool } type AutoCompleteReply struct { @@ -85,8 +87,9 @@ func (s *Server) AutoComplete(req *AutoCompleteRequest, res *AutoCompleteReply) } now := time.Now() cfg := suggest.Config{ - Builtin: req.Builtin, - Context: req.Context, + Importer: gbimporter.New(&req.Context, req.Filename, underlying), + Builtin: req.Builtin, + IgnoreCase: req.IgnoreCase, } if *g_debug { cfg.Logf = log.Printf