Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion internal/server/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions internal/server/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`)
})
}
28 changes: 16 additions & 12 deletions internal/server/semantic_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +75 to +82

This comment was marked as outdated.

}

// 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)
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions internal/server/semantic_token_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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
}
18 changes: 9 additions & 9 deletions internal/server/spx_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
}
}

Expand All @@ -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{
Expand Down
84 changes: 84 additions & 0 deletions internal/server/spx_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
2 changes: 1 addition & 1 deletion internal/server/spx_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}

Expand Down
Loading