diff --git a/go.mod b/go.mod index f29e6914..20dac058 100644 --- a/go.mod +++ b/go.mod @@ -26,3 +26,8 @@ require ( golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace ( + github.com/goplus/gogen => github.com/visualfc/gogen v0.1.2-0.20260306080659-7aab9b483d98 + github.com/goplus/xgo => github.com/visualfc/gop v1.5.1-0.20260306075634-55fbc122a22d +) diff --git a/go.sum b/go.sum index 18c2c91d..b3913818 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,12 @@ 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/goplus/gogen v1.21.2 h1:xbXPgZOZiQx/WBM0nZxVSxFbtdCMZCRB+lguDnh8pfs= -github.com/goplus/gogen v1.21.2/go.mod h1:Y7ulYW3wonQ3d9er00b0uGFEV/IUZa6okWJZh892ACQ= github.com/goplus/mod v0.19.6 h1:o00M6mN/rQ06cGDdH8xSHdL3xiZJXom6AUmwpOTBZ84= github.com/goplus/mod v0.19.6/go.mod h1:UnoI3xX5LbUu5TFwOhVRpwOBHSH//s7yqWNIbXpzpRA= github.com/goplus/spbase v0.1.0 h1:JNZ0D/65DerYyv9/9IfrXHZZbd0WNK0jHiVvgCtZhwY= github.com/goplus/spbase v0.1.0/go.mod h1:brnD3OJnHtipqob2IsJ3/QzGBf+eOnqXNnHGKpv1irQ= github.com/goplus/spx/v2 v2.0.0-pre.47.0.20260317084052-07546096faba h1:zMrw0i0jSaoLmvaFQT/DMGRSB32Q+yBYsGt99gqwkVU= github.com/goplus/spx/v2 v2.0.0-pre.47.0.20260317084052-07546096faba/go.mod h1:oP0YVzWNrczCyyO8qjYyni5sV4wJ3sqVkjCmc+P6jBo= -github.com/goplus/xgo v1.6.7 h1:vPKx5ddOmx93oSan0rXTKWk6RO2/gJowBew9GFrUNcU= -github.com/goplus/xgo v1.6.7/go.mod h1:8wsvmW48dGeZ+rCMqlk/MsNecp/snz/tUqNkXaLoe6U= github.com/petermattis/goid v0.0.0-20250721140440-ea1c0173183e h1:D0bJD+4O3G4izvrQUmzCL80zazlN7EwJ0PPDhpJWC/I= github.com/petermattis/goid v0.0.0-20250721140440-ea1c0173183e/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -21,6 +17,10 @@ github.com/qiniu/x v1.16.5 h1:+/cSm9m8F8sx6zJ4ylmsuhux8xVpxMHP/pzL8xv1Y9w= github.com/qiniu/x v1.16.5/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/visualfc/gogen v0.1.2-0.20260306080659-7aab9b483d98 h1:3Zc5lgPxKho/jPMa9l14/dRS7aeoUsy2uzitY2DKW40= +github.com/visualfc/gogen v0.1.2-0.20260306080659-7aab9b483d98/go.mod h1:Y7ulYW3wonQ3d9er00b0uGFEV/IUZa6okWJZh892ACQ= +github.com/visualfc/gop v1.5.1-0.20260306075634-55fbc122a22d h1:Abw2IeUNh1DnNuiPVBUxvAjC+kbM9zQkvCJvmodLCQ4= +github.com/visualfc/gop v1.5.1-0.20260306075634-55fbc122a22d/go.mod h1:Azdt4ZMMSOOR3jqx7fmv1CKQi4+OHFgfqOErbW6M8e8= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/internal/server/command_test.go b/internal/server/command_test.go index 8a28383c..5018fa01 100644 --- a/internal/server/command_test.go +++ b/internal/server/command_test.go @@ -2738,7 +2738,7 @@ func onStart() { // Create a field with nil package fieldWithNilPkg := types.NewField(0, nil, "fieldWithNilPkg", types.Typ[types.Int], false) - assert.True(t, fieldWithNilPkg.IsField(), "Should be a field") + assert.Equal(t, types.FieldVar, fieldWithNilPkg.Kind(), "Should be a field var") assert.Nil(t, fieldWithNilPkg.Pkg(), "Package should be nil") // Try to find enclosing type for field with nil package diff --git a/internal/server/hover_test.go b/internal/server/hover_test.go index a6a9f5ee..66258391 100644 --- a/internal/server/hover_test.go +++ b/internal/server/hover_test.go @@ -758,4 +758,49 @@ this = 1 } } }) + + t.Run("VarKindOverviews", func(t *testing.T) { + m := map[string][]byte{ + "main.spx": []byte(` +type T struct{} + +func (recv T) M(x int) (r int) { + return x +} + +var _ = T{}.M(1) +`), + } + s := New(newProjectWithoutModTime(m), nil, fileMapGetter(m), &MockScheduler{}) + + recvHover, err := s.textDocumentHover(&HoverParams{ + TextDocumentPositionParams: TextDocumentPositionParams{ + TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, + Position: Position{Line: 3, Character: 6}, + }, + }) + require.NoError(t, err) + require.NotNil(t, recvHover) + assert.Contains(t, recvHover.Contents.Value, `overview="recv recv main.T"`) + + paramHover, err := s.textDocumentHover(&HoverParams{ + TextDocumentPositionParams: TextDocumentPositionParams{ + TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, + Position: Position{Line: 3, Character: 16}, + }, + }) + require.NoError(t, err) + require.NotNil(t, paramHover) + assert.Contains(t, paramHover.Contents.Value, `overview="param x int"`) + + resultHover, err := s.textDocumentHover(&HoverParams{ + TextDocumentPositionParams: TextDocumentPositionParams{ + TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, + Position: Position{Line: 3, Character: 24}, + }, + }) + require.NoError(t, err) + require.NotNil(t, resultHover) + assert.Contains(t, resultHover.Contents.Value, `overview="result r int"`) + }) } diff --git a/internal/server/semantic_token.go b/internal/server/semantic_token.go index 1e9c14b7..a985d293 100644 --- a/internal/server/semantic_token.go +++ b/internal/server/semantic_token.go @@ -70,6 +70,18 @@ type semanticTokenInfo struct { tokenModifiers []SemanticTokenModifiers } +// semanticTokenTypeForVarKind returns the semantic token type for kind. +func semanticTokenTypeForVarKind(kind types.VarKind) SemanticTokenTypes { + switch { + case isParameterLikeVarKind(kind): + return ParameterType + case kind == types.PackageVar, kind == types.LocalVar, kind == types.ResultVar: + return VariableType + default: + return VariableType + } +} + // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_semanticTokens func (s *Server) textDocumentSemanticTokensFull(params *SemanticTokensParams) (*SemanticTokens, error) { result, _, astFile, err := s.compileAndGetASTFileForDocumentURI(params.TextDocument.URI) @@ -85,6 +97,7 @@ func (s *Server) textDocumentSemanticTokensFull(params *SemanticTokensParams) (* } fset := result.proj.Fset + astPkg, _ := result.proj.ASTPackage() var tokenInfos []semanticTokenInfo addToken := func(startPos, endPos xgotoken.Pos, tokenType SemanticTokenTypes, tokenModifiers []SemanticTokenModifiers) { if !startPos.IsValid() || !endPos.IsValid() { @@ -152,24 +165,15 @@ func (s *Server) textDocumentSemanticTokensFull(params *SemanticTokensParams) (* tokenType = TypeType } case *types.Var: - switch obj.Kind() { - case types.FieldVar: - typeInfo, _ := result.proj.TypeInfo() - astPkg, _ := result.proj.ASTPackage() + switch { + case obj.IsField(): if xgoutil.IsInMainPkg(obj) && xgoutil.IsDefinedInClassFieldsDecl(result.proj.Fset, typeInfo, astPkg, obj) { tokenType = VariableType } else { tokenType = PropertyType } - case types.PackageVar: - defIdent := typeInfo.ObjToDef[obj] - if defIdent == node { - tokenType = ParameterType - } else { - tokenType = VariableType - } default: - tokenType = VariableType + tokenType = semanticTokenTypeForVarKind(obj.Kind()) } case *types.Const: tokenType = VariableType diff --git a/internal/server/semantic_token_test.go b/internal/server/semantic_token_test.go index 360d6522..7a393298 100644 --- a/internal/server/semantic_token_test.go +++ b/internal/server/semantic_token_test.go @@ -1,8 +1,11 @@ package server import ( + "go/types" + "strings" "testing" + "github.com/goplus/gogen" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -63,4 +66,97 @@ onStart => { 0, 1, 1, 13, 0, // } }, mySpriteTokens.Data) }) + + t.Run("LocalVariableKind", func(t *testing.T) { + const mySpriteSrc = `onStart => { + z := 1 + echo z +} +` + m := map[string][]byte{ + "main.spx": []byte(`run "assets", {Title: "My Game"}`), + "MySprite.spx": []byte(mySpriteSrc), + "assets/index.json": []byte(`{}`), + "assets/sprites/MySprite/index.json": []byte(`{}`), + } + s := New(newProjectWithoutModTime(m), nil, fileMapGetter(m), &MockScheduler{}) + + tokens, err := s.textDocumentSemanticTokensFull(&SemanticTokensParams{ + TextDocument: TextDocumentIdentifier{URI: "file:///MySprite.spx"}, + }) + require.NoError(t, err) + require.NotNil(t, tokens) + + var zTokens []decodedSemanticToken + for _, tok := range decodeSemanticTokens(mySpriteSrc, tokens.Data) { + if tok.text == "z" { + zTokens = append(zTokens, tok) + } + } + require.Len(t, zTokens, 2) + assert.Equal(t, getSemanticTokenTypeIndex(VariableType), zTokens[0].tokenType) + assert.Equal(t, getSemanticTokenTypeIndex(VariableType), zTokens[1].tokenType) + }) +} + +func TestSemanticTokenTypeForVarKind(t *testing.T) { + for _, tt := range []struct { + name string + kind types.VarKind + want SemanticTokenTypes + }{ + {name: "ParamVar", kind: types.ParamVar, want: ParameterType}, + {name: "RecvVar", kind: types.RecvVar, want: ParameterType}, + {name: "ParamOptionalVar", kind: types.VarKind(gogen.ParamOptionalVar), want: ParameterType}, + {name: "PackageVar", kind: types.PackageVar, want: VariableType}, + {name: "LocalVar", kind: types.LocalVar, want: VariableType}, + {name: "ResultVar", kind: types.ResultVar, want: VariableType}, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, semanticTokenTypeForVarKind(tt.kind)) + }) + } +} + +type decodedSemanticToken struct { + line uint32 + char uint32 + length uint32 + tokenType uint32 + text string +} + +func decodeSemanticTokens(src string, data []uint32) []decodedSemanticToken { + lines := strings.Split(src, "\n") + var ( + line uint32 + char uint32 + out []decodedSemanticToken + ) + for i := 0; i+4 < len(data); i += 5 { + line += data[i] + if data[i] == 0 { + char += data[i+1] + } else { + char = data[i+1] + } + length := data[i+2] + text := "" + if int(line) < len(lines) { + row := lines[line] + start := int(char) + end := start + int(length) + if start >= 0 && end <= len(row) { + text = row[start:end] + } + } + out = append(out, decodedSemanticToken{ + line: line, + char: char, + length: length, + tokenType: data[i+3], + text: text, + }) + } + return out } diff --git a/internal/server/spx_definition.go b/internal/server/spx_definition.go index f072ec76..4af144b2 100644 --- a/internal/server/spx_definition.go +++ b/internal/server/spx_definition.go @@ -631,21 +631,21 @@ func GetSpxDefinitionForVar(v *types.Var, selectorTypeName string, forceVar bool } var overview strings.Builder - if !v.IsField() || forceVar { - overview.WriteString("var ") - } else { - overview.WriteString("field ") - } + overview.WriteString(varOverviewPrefix(v, forceVar)) + overview.WriteString(" ") overview.WriteString(v.Name()) overview.WriteString(" ") overview.WriteString(GetSimplifiedTypeString(v.Type())) var detail string if pkgDoc != nil { - if selectorTypeName == "" { + switch { + case selectorTypeName != "" && (v.IsField() || forceVar): + if typeDoc, ok := pkgDoc.Types[selectorTypeName]; ok { + detail = typeDoc.Fields[v.Name()] + } + case varKind(v) == types.PackageVar: detail = pkgDoc.Vars[v.Name()] - } else if typeDoc, ok := pkgDoc.Types[selectorTypeName]; ok { - detail = typeDoc.Fields[v.Name()] } } @@ -658,7 +658,7 @@ func GetSpxDefinitionForVar(v *types.Var, selectorTypeName string, forceVar bool idName = selectorTypeDisplayName + "." + idName } completionItemKind := VariableCompletion - if strings.HasPrefix(overview.String(), "field ") { + if v.IsField() && !forceVar { completionItemKind = FieldCompletion } def = SpxDefinition{ diff --git a/internal/server/spx_definition_test.go b/internal/server/spx_definition_test.go index d53541f5..4bef97b1 100644 --- a/internal/server/spx_definition_test.go +++ b/internal/server/spx_definition_test.go @@ -278,3 +278,87 @@ func TestCanonicalSpxResourceNameType(t *testing.T) { }) } } + +func TestGetSpxDefinitionForVarKinds(t *testing.T) { + pkg := types.NewPackage("main", "main") + + newVar := func(name string, kind types.VarKind) *types.Var { + v := types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int]) + v.SetKind(kind) + return v + } + newParam := func(name string, kind types.VarKind) *types.Var { + v := types.NewParam(token.NoPos, pkg, name, types.Typ[types.Int]) + v.SetKind(kind) + return v + } + + tests := []struct { + name string + v *types.Var + selectorTypeName string + forceVar bool + wantOverviewPrefix string + wantItemKind CompletionItemKind + }{ + { + name: "PackageVar", + v: newVar("pkgVar", types.PackageVar), + wantOverviewPrefix: "var pkgVar int", + wantItemKind: VariableCompletion, + }, + { + name: "LocalVar", + v: newVar("localVar", types.LocalVar), + wantOverviewPrefix: "var localVar int", + wantItemKind: VariableCompletion, + }, + { + name: "ParamVar", + v: newParam("paramVar", types.ParamVar), + wantOverviewPrefix: "param paramVar int", + wantItemKind: VariableCompletion, + }, + { + name: "OptionalParamVar", + v: newParam("optionalParamVar", optionalParamVarKind), + wantOverviewPrefix: "param optionalParamVar int", + wantItemKind: VariableCompletion, + }, + { + name: "RecvVar", + v: newParam("recvVar", types.RecvVar), + wantOverviewPrefix: "recv recvVar int", + wantItemKind: VariableCompletion, + }, + { + name: "ResultVar", + v: newParam("resultVar", types.ResultVar), + wantOverviewPrefix: "result resultVar int", + wantItemKind: VariableCompletion, + }, + { + name: "FieldVar", + v: types.NewField(token.NoPos, pkg, "fieldVar", types.Typ[types.Int], false), + selectorTypeName: "Sprite", + wantOverviewPrefix: "field fieldVar int", + wantItemKind: FieldCompletion, + }, + { + name: "ForcedFieldVar", + v: types.NewField(token.NoPos, pkg, "forcedFieldVar", types.Typ[types.Int], false), + selectorTypeName: "Sprite", + forceVar: true, + wantOverviewPrefix: "var forcedFieldVar int", + wantItemKind: VariableCompletion, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + def := GetSpxDefinitionForVar(tt.v, tt.selectorTypeName, tt.forceVar, nil) + assert.Equal(t, tt.wantOverviewPrefix, def.Overview) + assert.Equal(t, tt.wantItemKind, def.CompletionItemKind) + }) + } +} diff --git a/internal/server/spx_util.go b/internal/server/spx_util.go index 1bfcdaf3..24abbad3 100644 --- a/internal/server/spx_util.go +++ b/internal/server/spx_util.go @@ -156,7 +156,7 @@ func extractTypeName(typ types.Type) string { // findFieldOwnerType finds the type that owns a given field. func findFieldOwnerType(typeInfo *xgotypes.Info, field *types.Var) string { - if !field.IsField() { + if field == nil || !field.IsField() { return "" } diff --git a/internal/server/var_kind.go b/internal/server/var_kind.go new file mode 100644 index 00000000..8b976d86 --- /dev/null +++ b/internal/server/var_kind.go @@ -0,0 +1,48 @@ +package server + +import ( + "go/types" + + "github.com/goplus/gogen" +) + +// optionalParamVarKind is the custom kind used by gogen for optional parameters. +const optionalParamVarKind = types.VarKind(gogen.ParamOptionalVar) + +// varKind returns the semantic kind of v. +func varKind(v *types.Var) types.VarKind { + if v == nil { + return 0 + } + return v.Kind() +} + +// isParameterLikeVarKind reports whether kind should be treated like a parameter. +func isParameterLikeVarKind(kind types.VarKind) bool { + switch kind { + case types.ParamVar, types.RecvVar, optionalParamVarKind: + return true + default: + return false + } +} + +// varOverviewPrefix returns the overview prefix used for variable definitions. +func varOverviewPrefix(v *types.Var, forceVar bool) string { + if forceVar { + return "var" + } + + switch varKind(v) { + case types.FieldVar: + return "field" + case types.RecvVar: + return "recv" + case types.ParamVar, optionalParamVarKind: + return "param" + case types.ResultVar: + return "result" + default: + return "var" + } +} diff --git a/xgo/typeinfo_test.go b/xgo/typeinfo_test.go index eaa0622c..9693d7b1 100644 --- a/xgo/typeinfo_test.go +++ b/xgo/typeinfo_test.go @@ -185,4 +185,37 @@ func getCounter() int { assert.Equal(t, ErrUnknownCacheKind, err) assert.Nil(t, typeInfo) }) + + t.Run("VarKinds", func(t *testing.T) { + proj := NewProject(nil, map[string]*File{ + "main.xgo": { + Content: []byte(` +type T struct{} + +func (r T) M(x int) (y int) { + z := x + return z +} +`), + }, + }, FeatAll) + + typeInfo, err := proj.TypeInfo() + require.NoError(t, err) + require.NotNil(t, typeInfo) + + kinds := make(map[string]types.VarKind) + for ident, obj := range typeInfo.Defs { + v, ok := obj.(*types.Var) + if !ok { + continue + } + kinds[ident.Name] = v.Kind() + } + + assert.Equal(t, types.RecvVar, kinds["r"]) + assert.Equal(t, types.ParamVar, kinds["x"]) + assert.Equal(t, types.ResultVar, kinds["y"]) + assert.Equal(t, types.LocalVar, kinds["z"]) + }) }