Skip to content

gopls: report semantic tokens of top-level type constructor modifiers #511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
2 changes: 2 additions & 0 deletions gopls/doc/features/passive.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ a portion of it.
The client may use this information to provide syntax highlighting
that conveys semantic distinctions between, for example, functions and
types, constants and variables, or library functions and built-ins.
Gopls also reports a modifier for the top-level constructor of each symbols's type, one of:
`interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, `invalid`.
The client specifies the sets of types and modifiers it is interested in.

Settings:
Expand Down
7 changes: 7 additions & 0 deletions gopls/doc/release/v0.17.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ of paritial selection of a declration cannot invoke this code action.
Hovering over a standard library symbol now displays information about the first
Go release containing the symbol. For example, hovering over `errors.As` shows
"Added in go1.13".

## Semantic token modifiers of top-level constructor of types
The semantic tokens response now includes additional modifiers for the top-level
constructor of the type of each symbol:
`interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, and `invalid`.
Editors may use this for syntax coloring.

8 changes: 4 additions & 4 deletions gopls/doc/semantictokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ The references to *object* refer to the
1. __`keyword`__ All Go [keywords](https://golang.org/ref/spec#Keywords) are marked `keyword`.
1. __`namespace`__ All package names are marked `namespace`. In an import, if there is an
alias, it would be marked. Otherwise the last component of the import path is marked.
1. __`type`__ Objects of type ```types.TypeName``` are marked `type`.
If they are also ```types.Basic```
the modifier is `defaultLibrary`. (And in ```type B struct{C}```, ```B``` has modifier `definition`.)
1. __`type`__ Objects of type ```types.TypeName``` are marked `type`. It also reports
a modifier for the top-level constructor of the object's type, one of:
`interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, `invalid`.
1. __`parameter`__ The formal arguments in ```ast.FuncDecl``` and ```ast.FuncType``` nodes are marked `parameter`.
1. __`variable`__ Identifiers in the
scope of ```const``` are modified with `readonly`. ```nil``` is usually a `variable` modified with both
Expand Down Expand Up @@ -121,4 +121,4 @@ While a file is being edited it may temporarily contain either
parsing errors or type errors. In this case gopls cannot determine some (or maybe any)
of the semantic tokens. To avoid weird flickering it is the responsibility
of clients to maintain the semantic token information
in the unedited part of the file, and they do.
in the unedited part of the file, and they do.
2 changes: 1 addition & 1 deletion gopls/internal/cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ const c = 0
want := `
/*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a
/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f()
/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary]*/int
/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary number]*/int
/*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/c = /*⇒1,number,[]*/0
`[1:]
if got != want {
Expand Down
63 changes: 51 additions & 12 deletions gopls/internal/golang/semtok.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,18 @@ func (tv *tokenVisitor) comment(c *ast.Comment, importByName map[string]*types.P
}
}

tokenTypeByObject := func(obj types.Object) semtok.TokenType {
tokenTypeByObject := func(obj types.Object) (semtok.TokenType, []string) {
switch obj.(type) {
case *types.PkgName:
return semtok.TokNamespace
return semtok.TokNamespace, nil
case *types.Func:
return semtok.TokFunction
return semtok.TokFunction, nil
case *types.TypeName:
return semtok.TokType
return semtok.TokType, appendTypeModifiers(nil, obj)
case *types.Const, *types.Var:
return semtok.TokVariable
return semtok.TokVariable, nil
default:
return semtok.TokComment
return semtok.TokComment, nil
}
}

Expand All @@ -244,7 +244,8 @@ func (tv *tokenVisitor) comment(c *ast.Comment, importByName map[string]*types.P
}
id, rest, _ := strings.Cut(name, ".")
name = rest
tv.token(offset, len(id), tokenTypeByObject(obj), nil)
tok, mods := tokenTypeByObject(obj)
tv.token(offset, len(id), tok, mods)
offset += token.Pos(len(id))
}
last = idx[3]
Expand Down Expand Up @@ -483,6 +484,46 @@ func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) {
return true
}

// appendTypeModifiers appends optional modifiers that describe the top-level
// type constructor of obj.Type(): "pointer", "map", etc.
func appendTypeModifiers(mods []string, obj types.Object) []string {
switch t := obj.Type().Underlying().(type) {
case *types.Interface:
mods = append(mods, "interface")
case *types.Struct:
mods = append(mods, "struct")
case *types.Signature:
mods = append(mods, "signature")
case *types.Pointer:
mods = append(mods, "pointer")
case *types.Array:
mods = append(mods, "array")
case *types.Map:
mods = append(mods, "map")
case *types.Slice:
mods = append(mods, "slice")
case *types.Chan:
mods = append(mods, "chan")
case *types.Basic:
mods = append(mods, "defaultLibrary")
switch t.Kind() {
case types.Invalid:
mods = append(mods, "invalid")
case types.String:
mods = append(mods, "string")
case types.Bool:
mods = append(mods, "bool")
case types.UnsafePointer:
mods = append(mods, "pointer")
default:
if t.Info()&types.IsNumeric != 0 {
mods = append(mods, "number")
}
}
}
return mods
}

func (tv *tokenVisitor) ident(id *ast.Ident) {
var obj types.Object

Expand Down Expand Up @@ -535,10 +576,8 @@ func (tv *tokenVisitor) ident(id *ast.Ident) {
case *types.TypeName: // could be a TypeParam
if is[*types.TypeParam](aliases.Unalias(obj.Type())) {
emit(semtok.TokTypeParam)
} else if is[*types.Basic](obj.Type()) {
emit(semtok.TokType, "defaultLibrary")
} else {
emit(semtok.TokType)
emit(semtok.TokType, appendTypeModifiers(nil, obj)...)
}
case *types.Var:
if is[*types.Signature](aliases.Unalias(obj.Type())) {
Expand Down Expand Up @@ -795,11 +834,11 @@ func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.T
if fld, ok := fldm.(*ast.Field); ok {
// if len(fld.names) == 0 this is a semtok.TokType, being used
if len(fld.Names) == 0 {
return semtok.TokType, nil
return semtok.TokType, appendTypeModifiers(nil, obj)
}
return semtok.TokVariable, modifiers
}
return semtok.TokType, modifiers
return semtok.TokType, appendTypeModifiers(modifiers, obj)
}
}
// can't happen
Expand Down
2 changes: 2 additions & 0 deletions gopls/internal/protocol/semantic.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,7 @@ var (
semanticModifiers = [...]string{
"declaration", "definition", "readonly", "static",
"deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary",
// Additional modifiers
"interface", "struct", "signature", "pointer", "array", "map", "slice", "chan", "string", "number", "bool", "invalid",
}
)
2 changes: 2 additions & 0 deletions gopls/internal/test/integration/fake/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) {
capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{
"declaration", "definition", "readonly", "static",
"deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary",
// Additional modifiers supported by this client:
"interface", "struct", "signature", "pointer", "array", "map", "slice", "chan", "string", "number", "bool", "invalid",
}
// The LSP tests have historically enabled this flag,
// but really we should test both ways for older editors.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestSemantic_2527(t *testing.T) {
{Token: "func", TokenType: "keyword"},
{Token: "Add", TokenType: "function", Mod: "definition deprecated"},
{Token: "T", TokenType: "typeParameter", Mod: "definition"},
{Token: "int", TokenType: "type", Mod: "defaultLibrary"},
{Token: "int", TokenType: "type", Mod: "defaultLibrary number"},
{Token: "target", TokenType: "parameter", Mod: "definition"},
{Token: "T", TokenType: "typeParameter"},
{Token: "l", TokenType: "parameter", Mod: "definition"},
Expand Down
4 changes: 2 additions & 2 deletions gopls/internal/test/marker/testdata/token/comment.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var B = 2
type Foo int


// [F] accept a [Foo], and print it. //@token("F", "function", ""),token("Foo", "type", "")
// [F] accept a [Foo], and print it. //@token("F", "function", ""),token("Foo", "type", "defaultLibrary number")
func F(v Foo) {
println(v)

Expand All @@ -44,7 +44,7 @@ func F2(s string) {
-- b.go --
package p

// [F3] accept [*Foo] //@token("F3", "function", ""),token("Foo", "type", "")
// [F3] accept [*Foo] //@token("F3", "function", ""),token("Foo", "type", "defaultLibrary number")
func F3(v *Foo) {
println(*v)
}
Expand Down