Skip to content

Commit

Permalink
Add support for textDocument/inlayHint (#702)
Browse files Browse the repository at this point in the history
* Add stubs for inlay hint methods (LSP 3.17)

The following are added manually to the generated code:
* inlayHint/resolve
* textDocument/inlayHint

Generating this code is currently not possible since the upstream gopls
generator was migrated from Typescript to Go just before the 3.17 LSP
release. Using the new generator requires some more work and adaptions
on our side.

* Implement support for textDocument/inlayHint

* Implement dynamic registration for InlayHint
  • Loading branch information
mliszcz authored Jul 23, 2024
1 parent 8ce0b24 commit 56d60ca
Show file tree
Hide file tree
Showing 8 changed files with 457 additions and 5 deletions.
19 changes: 19 additions & 0 deletions internal/lsp/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
const DIAGNOSTICS_CONFIG_KEY = "ttcn3.experimental.diagnostics.enabled"
const FORMATTER_CONFIG_KEY = "ttcn3.experimental.format.enabled"
const SEMANTIC_TOKENS_CONFIG_KEY = "ttcn3.experimental.semanticTokens.enabled"
const INLAY_HINT_CONFIG_KEY = "ttcn3.experimental.inlayHint.enabled"

func (s *Server) Config(section string) interface{} {
v, err := s.client.Configuration(context.TODO(), &protocol.ParamConfiguration{
Expand Down Expand Up @@ -79,6 +80,24 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan
// NOTE: dynamic registration of diagnostics is only available from lsp 3.17 on
}

confRes, ok = s.Config(INLAY_HINT_CONFIG_KEY).(bool)
if !ok {
confRes = false
}
if s.clientCapability.HasDynRegForInlayHint && s.serverConfig.InlayHintEnabled != confRes {
s.serverConfig.InlayHintEnabled = confRes
if confRes {
regList = append(regList, protocol.Registration{
ID: "TEXTDOCUMENT_INLAYHINT",
Method: "textDocument/inlayHint",
RegisterOptions: newInlayHintRegistrationOptions()})
} else {
unregList = append(unregList, protocol.Unregistration{
ID: "TEXTDOCUMENT_INLAYHINT",
Method: "textDocument/inlayHint"})
}
}

if len(regList) > 0 {
s.client.RegisterCapability(ctx, &protocol.RegistrationParams{Registrations: regList})
}
Expand Down
28 changes: 28 additions & 0 deletions internal/lsp/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ func (s *Server) registerFormatterIfNoDynReg() bool {
return !s.clientCapability.HasDynRegForFormatter
}

func (s *Server) registerInlayHintIfNoDynReg() *protocol.InlayHintRegistrationOptions {
if s.clientCapability.HasDynRegForInlayHint {
return nil
}
return newInlayHintRegistrationOptions()
}

func newInlayHintRegistrationOptions() *protocol.InlayHintRegistrationOptions {
return &protocol.InlayHintRegistrationOptions{
InlayHintOptions: protocol.InlayHintOptions{
ResolveProvider: false,
WorkDoneProgressOptions: protocol.WorkDoneProgressOptions{
WorkDoneProgress: false,
},
},
TextDocumentRegistrationOptions: protocol.TextDocumentRegistrationOptions{
DocumentSelector: protocol.DocumentSelector{
protocol.DocumentFilter{Language: "ttcn3", Scheme: "file", Pattern: "**/*.ttcn3"},
},
},
StaticRegistrationOptions: protocol.StaticRegistrationOptions{
ID: "TEXTDOCUMENT_INLAYHINT",
},
}
}

func newSemanticTokens() *protocol.SemanticTokensRegistrationOptions {
return &protocol.SemanticTokensRegistrationOptions{

Expand Down Expand Up @@ -74,6 +100,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ

return &protocol.InitializeResult{
Capabilities: protocol.ServerCapabilities{
InlayHintProvider: s.registerInlayHintIfNoDynReg(),
CodeActionProvider: false,
CompletionProvider: protocol.CompletionOptions{TriggerCharacters: []string{"."}},
DefinitionProvider: true,
Expand Down Expand Up @@ -123,6 +150,7 @@ func (s *Server) evaluateClientCapabilities(params *protocol.ParamInitialize) {
s.clientCapability.HasDynRegForDiagnostics = false // NOTE: available only from LSP 3.17 on
s.clientCapability.HasDynRegForFormatter = params.Capabilities.TextDocument.Formatting.DynamicRegistration
s.clientCapability.HasDynRegForSemTok = params.Capabilities.TextDocument.SemanticTokens.DynamicRegistration
s.clientCapability.HasDynRegForInlayHint = params.Capabilities.TextDocument.InlayHint.DynamicRegistration
}

func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error {
Expand Down
81 changes: 81 additions & 0 deletions internal/lsp/inlay_hint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package lsp

import (
"context"

"github.com/nokia/ntt/internal/lsp/protocol"
"github.com/nokia/ntt/ttcn3"
"github.com/nokia/ntt/ttcn3/syntax"
)

func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) {
if !s.serverConfig.InlayHintEnabled {
return nil, nil
}

file := string(params.TextDocument.URI)
tree := ttcn3.ParseFile(file)
begin := tree.PosFor(int(params.Range.Start.Line)+1, int(params.Range.Start.Character+1))
end := tree.PosFor(int(params.Range.End.Line+1), int(params.Range.End.Character+1))
return ProcessInlayHint(tree, &s.db, begin, end), nil
}

func ProcessInlayHint(tree *ttcn3.Tree, db *ttcn3.DB, begin int, end int) []protocol.InlayHint {
var hints []protocol.InlayHint

tree.Inspect(func(n syntax.Node) bool {
if n == nil || n.End() < begin || end < n.Pos() {
return false
}

if callExpr, ok := n.(*syntax.CallExpr); ok {

for _, decl := range tree.LookupWithDB(callExpr.Fun, db) {

if params := getDeclarationParams(decl.Node); params != nil {

for idx, arg := range callExpr.Args.List {

// Stop processing further arguments after the first assignment notation.
// Value arguments are not allowed: ES 201 873-1, 5.4.2, Restrictions, point o).
if binaryExpr, ok := arg.(*syntax.BinaryExpr); ok {
if binaryExpr.Op.String() == ":=" {
break
}
}

name := params.List[idx].Name.Tok.String()
lbl := protocol.InlayHintLabelPart{Value: name + " :="}
pos := syntax.Begin(arg)
pos.Line -= 1
pos.Column -= 1
ppos := protocol.Position{Line: uint32(pos.Line), Character: uint32(pos.Column)}
hint := protocol.InlayHint{
Position: ppos,
Label: []protocol.InlayHintLabelPart{lbl},
Kind: protocol.Parameter,
PaddingRight: true,
}
hints = append(hints, hint)

}

// Stop after the first declaration is processed.
break
}
}
}
return true
})
return hints
}

func getDeclarationParams(node syntax.Node) *syntax.FormalPars {
if decl, ok := node.(*syntax.FuncDecl); ok {
return decl.Params
}
if decl, ok := node.(*syntax.TemplateDecl); ok {
return decl.Params
}
return nil
}
115 changes: 115 additions & 0 deletions internal/lsp/inlay_hint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package lsp_test

import (
"fmt"
"testing"

"github.com/nokia/ntt/internal/fs"
"github.com/nokia/ntt/internal/lsp"
"github.com/nokia/ntt/internal/lsp/protocol"
"github.com/nokia/ntt/ttcn3"
"github.com/stretchr/testify/assert"
)

type Hint struct {
Line uint32
Char uint32
Label string
}

func TestInlayHintForFunction(t *testing.T) {
actual := testInlayHint(t, nil, `
module Test {
function func(integer a := 0, integer b := 0, integer c := 0) {}
function test() {
func(1, 2, 3)
func(1,
2,
3)
func(a := 1, b := 2, c := 3)
func(1 + 2)
func(1, 2, c := 3)
}
}`)

assert.Equal(t, []Hint{
// All parameters in the same line.
{Line: 4, Char: 13, Label: "a :="},
{Line: 4, Char: 16, Label: "b :="},
{Line: 4, Char: 19, Label: "c :="},
// Parameters spanning multiple lines.
{Line: 5, Char: 13, Label: "a :="},
{Line: 6, Char: 13, Label: "b :="},
{Line: 7, Char: 13, Label: "c :="},
// Binary expression parameter.
{Line: 8, Char: 13, Label: "a :="},
// Mixed assignment / value list notation.
{Line: 9, Char: 13, Label: "a :="},
{Line: 9, Char: 14, Label: "b :="},
}[0], actual[0])
}

func TestInlayHintForTemplate(t *testing.T) {
actual := testInlayHint(t, nil, `
module Test {
template integer templ(integer x, integer y) := (x .. y)
function test() {
var template integer t := templ(2, 3)
}
}`)

assert.Equal(t, []Hint{
{Line: 4, Char: 40, Label: "x :="},
{Line: 6, Char: 43, Label: "y :="},
}[0], actual[0])
}

func TestInlayHintNestedCalls(t *testing.T) {
actual := testInlayHint(t, nil, `
module Test {
function foo(integer a) return integer { return 1; }
function bar(integer b) return integer { return 1; }
function baz(integer c) return integer { return 1; }
function test() {
foo(bar(baz(1)))
}
}`)

assert.Equal(t, []Hint{
{Line: 6, Char: 12, Label: "a :="},
{Line: 6, Char: 16, Label: "b :="},
{Line: 6, Char: 20, Label: "c :="},
}[0], actual[0])
}

func testInlayHint(t *testing.T, rng *protocol.Range, text string) []Hint {
t.Helper()

file := fmt.Sprintf("%s.ttcn3", t.Name())
fs.SetContent(file, []byte(text))
tree := ttcn3.ParseFile(file)
if tree.Err != nil {
t.Fatal(tree.Err)
}

// Build index to for tree.Lookup to resolve imported symbols.
db := &ttcn3.DB{}
db.Index(file)

begin := tree.Pos()
end := tree.End()
if rng != nil {
begin = tree.PosFor(int(rng.Start.Line), int(rng.Start.Character))
end = tree.PosFor(int(rng.End.Line), int(rng.End.Character))
}

var hints []Hint
for _, h := range lsp.ProcessInlayHint(tree, db, begin, end) {
hints = append(hints, Hint{
Line: h.Position.Line,
Char: h.Position.Character,
Label: h.Label[0].Value,
})
}
return hints
}
Loading

0 comments on commit 56d60ca

Please sign in to comment.