Skip to content

Commit

Permalink
Implement textDocument/semanticTokens (semantic highlighting)
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Dec 9, 2020
1 parent bb6ed57 commit a3e77ca
Show file tree
Hide file tree
Showing 10 changed files with 797 additions and 5 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/google/uuid v1.1.2
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.1
github.com/hashicorp/hcl-lang v0.0.0-20201116081236-948e43712a65
github.com/hashicorp/hcl-lang v0.0.0-20201207122824-8cd7a941aa8f
github.com/hashicorp/hcl/v2 v2.6.0
github.com/hashicorp/terraform-exec v0.11.1-0.20201207223938-9186a7c3bb24
github.com/hashicorp/terraform-json v0.7.0
Expand Down
5 changes: 2 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,9 @@ github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+Db
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl-lang v0.0.0-20201110071249-4e412924f52b h1:EjnMRaTQlomBMNRQfyWoLEg9IdqxeN1R2mb3ZZetCBs=
github.com/hashicorp/hcl-lang v0.0.0-20201110071249-4e412924f52b/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg=
github.com/hashicorp/hcl-lang v0.0.0-20201116081236-948e43712a65 h1:kF6Dxt2kPNj8+Px7LyK7nxPDQjYKwGrKxxYnSu+LOXM=
github.com/hashicorp/hcl-lang v0.0.0-20201116081236-948e43712a65/go.mod h1:vd3BPEDWrYMAgAnB0MRlBdZknrpUXf8Jk2PNaHIbwhg=
github.com/hashicorp/hcl-lang v0.0.0-20201207122824-8cd7a941aa8f h1:XppSUhj2DLqAkl/TnlowKXdzC4geEEZi5wtgpKQDU8o=
github.com/hashicorp/hcl-lang v0.0.0-20201207122824-8cd7a941aa8f/go.mod h1:TZ5tpvmgJSHfmIndN4WP9SpZvyWK8tHPBY8LDRyU+pI=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o=
github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
Expand Down
7 changes: 7 additions & 0 deletions internal/langserver/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ func initializeResponse(t *testing.T, commandPrefix string) string {
"commands": %s,
"workDoneProgress":true
},
"semanticTokensProvider": {
"legend": {
"tokenTypes": [],
"tokenModifiers": []
},
"full": false
},
"workspace": {
"workspaceFolders": {}
}
Expand Down
24 changes: 23 additions & 1 deletion internal/langserver/handlers/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
return serverCaps, err
}

err = lsctx.SetClientCapabilities(ctx, &params.Capabilities)
clientCaps := params.Capabilities
err = lsctx.SetClientCapabilities(ctx, &clientCaps)
if err != nil {
return serverCaps, err
}
Expand Down Expand Up @@ -80,6 +81,27 @@ func (lh *logHandler) Initialize(ctx context.Context, params lsp.InitializeParam
return serverCaps, err
}

stCaps := clientCaps.TextDocument.SemanticTokens
semanticTokensOpts := lsp.SemanticTokensOptions{
Legend: lsp.SemanticTokensLegend{
TokenTypes: ilsp.TokenTypesLegend(stCaps.TokenTypes).AsStrings(),
TokenModifiers: ilsp.TokenModifiersLegend(stCaps.TokenModifiers).AsStrings(),
},
}
type semanticTokensFull struct {
Delta bool `json:"delta,omitempty"`
}
switch fullSupported := stCaps.Requests.Full.(type) {
case bool:
semanticTokensOpts.Full = fullSupported
case nil:
semanticTokensOpts.Full = false
case semanticTokensFull:
semanticTokensOpts.Full = true
}

serverCaps.Capabilities.SemanticTokensProvider = semanticTokensOpts

// set commandPrefix for session
lsctx.SetCommandPrefix(ctx, out.Options.CommandPrefix)
// apply prefix to executeCommand handler names
Expand Down
64 changes: 64 additions & 0 deletions internal/langserver/handlers/semantic_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package handlers

import (
"context"
"fmt"

lsctx "github.com/hashicorp/terraform-ls/internal/context"
ilsp "github.com/hashicorp/terraform-ls/internal/lsp"
lsp "github.com/hashicorp/terraform-ls/internal/protocol"
)

func (lh *logHandler) TextDocumentSemanticTokensFull(ctx context.Context, params lsp.SemanticTokensParams) (lsp.SemanticTokens, error) {
tks := lsp.SemanticTokens{}

cc, err := lsctx.ClientCapabilities(ctx)
if err != nil {
return tks, err
}

ds, err := lsctx.DocumentStorage(ctx)
if err != nil {
return tks, err
}

rmf, err := lsctx.RootModuleFinder(ctx)
if err != nil {
return tks, err
}

fh := ilsp.FileHandlerFromDocumentURI(params.TextDocument.URI)
doc, err := ds.GetDocument(fh)
if err != nil {
return tks, err
}

rm, err := rmf.RootModuleByPath(doc.Dir())
if err != nil {
return tks, fmt.Errorf("finding compatible decoder failed: %w", err)
}

schema, err := rmf.SchemaForPath(doc.Dir())
if err != nil {
return tks, err
}

d, err := rm.DecoderWithSchema(schema)
if err != nil {
return tks, err
}

tokens, err := d.SemanticTokensInFile(doc.Filename())
if err != nil {
return tks, err
}

te := &ilsp.TokenEncoder{
Lines: doc.Lines(),
Tokens: tokens,
ClientCaps: cc.TextDocument.SemanticTokens,
}
tks.Data = te.Encode()

return tks, nil
}
121 changes: 121 additions & 0 deletions internal/langserver/handlers/semantic_tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package handlers

import (
"encoding/json"
"fmt"
"testing"

"github.com/hashicorp/go-version"
tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-ls/internal/langserver"
"github.com/hashicorp/terraform-ls/internal/terraform/exec"
"github.com/hashicorp/terraform-ls/internal/terraform/rootmodule"
"github.com/stretchr/testify/mock"
)

func TestSemanticTokensFull(t *testing.T) {
tmpDir := TempDir(t)
InitPluginCache(t, tmpDir.Dir())

var testSchema tfjson.ProviderSchemas
err := json.Unmarshal([]byte(testSchemaOutput), &testSchema)
if err != nil {
t.Fatal(err)
}

ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{
RootModules: map[string]*rootmodule.RootModuleMock{
tmpDir.Dir(): {
TfExecFactory: exec.NewMockExecutor([]*mock.Call{
{
Method: "Version",
Repeatability: 1,
Arguments: []interface{}{
mock.AnythingOfType(""),
},
ReturnArguments: []interface{}{
version.Must(version.NewVersion("0.12.0")),
nil,
nil,
},
},
{
Method: "GetExecPath",
Repeatability: 1,
ReturnArguments: []interface{}{
"",
},
},
{
Method: "ProviderSchemas",
Repeatability: 1,
Arguments: []interface{}{
mock.AnythingOfType(""),
},
ReturnArguments: []interface{}{
&testSchema,
nil,
},
},
}),
},
}}))
stop := ls.Start(t)
defer stop()

ls.Call(t, &langserver.CallRequest{
Method: "initialize",
ReqParams: fmt.Sprintf(`{
"capabilities": {
"textDocument": {
"semanticTokens": {
"tokenTypes": [
"type",
"property",
"string"
],
"tokenModifiers": [
"deprecated",
"modification"
],
"requests": {
"full": true
}
}
}
},
"rootUri": %q,
"processId": 12345
}`, TempDir(t).URI())})
ls.Notify(t, &langserver.CallRequest{
Method: "initialized",
ReqParams: "{}",
})
ls.Call(t, &langserver.CallRequest{
Method: "textDocument/didOpen",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"version": 0,
"languageId": "terraform",
"text": "provider \"test\" {\n\n}\n",
"uri": "%s/main.tf"
}
}`, TempDir(t).URI())})

ls.CallAndExpectResponse(t, &langserver.CallRequest{
Method: "textDocument/semanticTokens/full",
ReqParams: fmt.Sprintf(`{
"textDocument": {
"uri": "%s/main.tf"
}
}`, TempDir(t).URI())}, `{
"jsonrpc": "2.0",
"id": 3,
"result": {
"data": [
0,0,8,0,0,
0,9,6,1,2
]
}
}`)
}
12 changes: 12 additions & 0 deletions internal/langserver/handlers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) {

return handle(ctx, req, lh.TextDocumentFormatting)
},
"textDocument/semanticTokens/full": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
return nil, err
}

ctx = lsctx.WithDocumentStorage(ctx, svc.fs)
ctx = lsctx.WithClientCapabilities(ctx, cc)
ctx = lsctx.WithRootModuleFinder(ctx, svc.modMgr)

return handle(ctx, req, lh.TextDocumentSemanticTokensFull)
},
"workspace/executeCommand": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) {
err := session.CheckInitializationIsConfirmed()
if err != nil {
Expand Down
Loading

0 comments on commit a3e77ca

Please sign in to comment.