Skip to content

Commit 4f2ce16

Browse files
authored
feat(lsp): add primitive document symbol support (#848)
1 parent 079e286 commit 4f2ce16

File tree

2 files changed

+197
-3
lines changed

2 files changed

+197
-3
lines changed

cmd/templ/lspcmd/lsp_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package lspcmd
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"io"
78
"os"
@@ -14,6 +15,7 @@ import (
1415
"github.com/a-h/templ/cmd/templ/generatecmd/modcheck"
1516
"github.com/a-h/templ/cmd/templ/lspcmd/lspdiff"
1617
"github.com/a-h/templ/cmd/templ/testproject"
18+
"github.com/google/go-cmp/cmp"
1719
"go.lsp.dev/jsonrpc2"
1820
"go.lsp.dev/uri"
1921
"go.uber.org/zap"
@@ -603,6 +605,139 @@ func TestCodeAction(t *testing.T) {
603605
}
604606
}
605607

608+
func TestDocumentSymbol(t *testing.T) {
609+
if testing.Short() {
610+
return
611+
}
612+
613+
ctx, cancel := context.WithCancel(context.Background())
614+
log, _ := zap.NewProduction()
615+
616+
ctx, appDir, _, server, teardown, err := Setup(ctx, log)
617+
if err != nil {
618+
t.Fatalf("failed to setup test: %v", err)
619+
}
620+
defer teardown(t)
621+
defer cancel()
622+
623+
tests := []struct {
624+
uri string
625+
expect []any
626+
}{
627+
{
628+
uri: "file://" + appDir + "/templates.templ",
629+
expect: []any{
630+
protocol.SymbolInformation{
631+
Name: "Page",
632+
Kind: protocol.SymbolKindFunction,
633+
Location: protocol.Location{
634+
Range: protocol.Range{
635+
Start: protocol.Position{Line: 11, Character: 0},
636+
End: protocol.Position{Line: 50, Character: 1},
637+
},
638+
},
639+
},
640+
protocol.SymbolInformation{
641+
Name: "nihao",
642+
Kind: protocol.SymbolKindVariable,
643+
Location: protocol.Location{
644+
Range: protocol.Range{
645+
Start: protocol.Position{Line: 18, Character: 4},
646+
End: protocol.Position{Line: 18, Character: 16},
647+
},
648+
},
649+
},
650+
protocol.SymbolInformation{
651+
Name: "Struct",
652+
Kind: protocol.SymbolKindStruct,
653+
Location: protocol.Location{
654+
Range: protocol.Range{
655+
Start: protocol.Position{Line: 20, Character: 5},
656+
End: protocol.Position{Line: 22, Character: 1},
657+
},
658+
},
659+
},
660+
protocol.SymbolInformation{
661+
Name: "s",
662+
Kind: protocol.SymbolKindVariable,
663+
Location: protocol.Location{
664+
Range: protocol.Range{
665+
Start: protocol.Position{Line: 24, Character: 4},
666+
End: protocol.Position{Line: 24, Character: 16},
667+
},
668+
},
669+
},
670+
},
671+
},
672+
{
673+
uri: "file://" + appDir + "/remoteparent.templ",
674+
expect: []any{
675+
protocol.SymbolInformation{
676+
Name: "RemoteInclusionTest",
677+
Kind: protocol.SymbolKindFunction,
678+
Location: protocol.Location{
679+
Range: protocol.Range{
680+
Start: protocol.Position{Line: 9, Character: 0},
681+
End: protocol.Position{Line: 35, Character: 1},
682+
},
683+
},
684+
},
685+
protocol.SymbolInformation{
686+
Name: "Remote2",
687+
Kind: protocol.SymbolKindFunction,
688+
Location: protocol.Location{
689+
Range: protocol.Range{
690+
Start: protocol.Position{Line: 37, Character: 0},
691+
End: protocol.Position{Line: 63, Character: 1},
692+
},
693+
},
694+
},
695+
},
696+
},
697+
}
698+
699+
for i, test := range tests {
700+
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
701+
actual, err := server.DocumentSymbol(ctx, &protocol.DocumentSymbolParams{
702+
TextDocument: protocol.TextDocumentIdentifier{
703+
URI: uri.URI(test.uri),
704+
},
705+
})
706+
if err != nil {
707+
t.Errorf("failed to get document symbol: %v", err)
708+
}
709+
710+
// set expected URI
711+
for i := range test.expect {
712+
switch v := test.expect[i].(type) {
713+
case protocol.SymbolInformation:
714+
v.Location.URI = uri.URI(test.uri)
715+
test.expect[i] = v
716+
}
717+
}
718+
719+
expectdSlice, err := sliceToAnySlice(test.expect)
720+
if err != nil {
721+
t.Errorf("failed to convert expect to any slice: %v", err)
722+
}
723+
diff := cmp.Diff(expectdSlice, actual)
724+
if diff != "" {
725+
t.Errorf("unexpected document symbol: %v", diff)
726+
}
727+
})
728+
}
729+
}
730+
731+
func sliceToAnySlice(in []any) ([]any, error) {
732+
b, err := json.Marshal(in)
733+
if err != nil {
734+
return nil, err
735+
}
736+
out := make([]any, 0, len(in))
737+
err = json.Unmarshal(b, &out)
738+
return out, err
739+
}
740+
606741
func runeIndexToUTF8ByteIndex(s string, runeIndex int) (lspChar uint32, err error) {
607742
for i, r := range []rune(s) {
608743
if i == runeIndex {

cmd/templ/lspcmd/proxy/server.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proxy
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -825,9 +826,46 @@ func (p *Server) DocumentLinkResolve(ctx context.Context, params *lsp.DocumentLi
825826
func (p *Server) DocumentSymbol(ctx context.Context, params *lsp.DocumentSymbolParams) (result []interface{} /* []SymbolInformation | []DocumentSymbol */, err error) {
826827
p.Log.Info("client -> server: DocumentSymbol")
827828
defer p.Log.Info("client -> server: DocumentSymbol end")
828-
// TODO: Rewrite the request and response, but for now, ignore it.
829-
// return p.Target.DocumentSymbol(ctx params)
830-
return
829+
isTemplFile, goURI := convertTemplToGoURI(params.TextDocument.URI)
830+
if !isTemplFile {
831+
return p.Target.DocumentSymbol(ctx, params)
832+
}
833+
templURI := params.TextDocument.URI
834+
params.TextDocument.URI = goURI
835+
symbols, err := p.Target.DocumentSymbol(ctx, params)
836+
if err != nil {
837+
return nil, err
838+
}
839+
840+
// recursively convert the ranges of the symbols and their children
841+
var convertRange func(s *lsp.DocumentSymbol)
842+
convertRange = func(s *lsp.DocumentSymbol) {
843+
s.Range = p.convertGoRangeToTemplRange(templURI, s.Range)
844+
s.SelectionRange = p.convertGoRangeToTemplRange(templURI, s.SelectionRange)
845+
for i := 0; i < len(s.Children); i++ {
846+
convertRange(&s.Children[i])
847+
}
848+
}
849+
850+
for _, s := range symbols {
851+
if m, ok := s.(map[string]interface{}); ok {
852+
s, err = mapToSymbol(m)
853+
if err != nil {
854+
return nil, err
855+
}
856+
}
857+
switch s := s.(type) {
858+
case lsp.DocumentSymbol:
859+
convertRange(&s)
860+
result = append(result, s)
861+
case lsp.SymbolInformation:
862+
s.Location.URI = templURI
863+
s.Location.Range = p.convertGoRangeToTemplRange(templURI, s.Location.Range)
864+
result = append(result, s)
865+
}
866+
}
867+
868+
return result, err
831869
}
832870

833871
func (p *Server) ExecuteCommand(ctx context.Context, params *lsp.ExecuteCommandParams) (result interface{}, err error) {
@@ -1216,3 +1254,24 @@ func (p *Server) Request(ctx context.Context, method string, params interface{})
12161254
defer p.Log.Info("client -> server: Request end")
12171255
return p.Target.Request(ctx, method, params)
12181256
}
1257+
1258+
func mapToSymbol(m map[string]interface{}) (interface{}, error) {
1259+
b, err := json.Marshal(m)
1260+
if err != nil {
1261+
return nil, err
1262+
}
1263+
1264+
if _, ok := m["selectionRange"]; ok {
1265+
var s lsp.DocumentSymbol
1266+
if err := json.Unmarshal(b, &s); err != nil {
1267+
return nil, err
1268+
}
1269+
return s, nil
1270+
}
1271+
1272+
var s lsp.SymbolInformation
1273+
if err := json.Unmarshal(b, &s); err != nil {
1274+
return nil, err
1275+
}
1276+
return s, nil
1277+
}

0 commit comments

Comments
 (0)