From d072b8d84f7287b43236895df670ed26d3a05b92 Mon Sep 17 00:00:00 2001 From: tris203 Date: Mon, 9 Sep 2024 20:13:54 +0100 Subject: [PATCH 1/5] feat(lsp): add workspace folder initialization - Added logic to walk through workspace folders and initialize templ files. - Logs warnings when source maps are not found in cache. - Parses templates and sets source map cache contents. - Handles errors during file reading, template parsing, and generation. --- cmd/templ/lspcmd/proxy/server.go | 54 ++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go index fe6689454..e4423d4a1 100644 --- a/cmd/templ/lspcmd/proxy/server.go +++ b/cmd/templ/lspcmd/proxy/server.go @@ -3,17 +3,20 @@ package proxy import ( "context" "fmt" + "os" + "path/filepath" "regexp" "strings" "github.com/a-h/parse" lsp "github.com/a-h/protocol" + "go.lsp.dev/uri" + "go.uber.org/zap" + "github.com/a-h/templ" "github.com/a-h/templ/cmd/templ/imports" "github.com/a-h/templ/generator" "github.com/a-h/templ/parser/v2" - "go.lsp.dev/uri" - "go.uber.org/zap" ) // Server is responsible for rewriting messages that are @@ -81,6 +84,7 @@ func (p *Server) convertTemplRangeToGoRange(templURI lsp.DocumentURI, input lsp. var sourceMap *parser.SourceMap sourceMap, ok = p.SourceMapCache.Get(string(templURI)) if !ok { + p.Log.Warn("templ->go: sourcemap not found in cache") return } // Map from the source position to target Go position. @@ -101,6 +105,7 @@ func (p *Server) convertGoRangeToTemplRange(templURI lsp.DocumentURI, input lsp. output = input sourceMap, ok := p.SourceMapCache.Get(string(templURI)) if !ok { + p.Log.Warn("go->templ: sourcemap not found in cache") return } // Map from the source position to target Go position. @@ -228,6 +233,51 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( Save: &lsp.SaveOptions{IncludeText: true}, } + for _, c := range params.WorkspaceFolders { + path := strings.TrimPrefix(c.URI, "file://") + werr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + uri := uri.URI("file://" + path) + isTemplFile, _ := convertTemplToGoURI(uri) + if isTemplFile { + b, err := os.ReadFile(path) + if err != nil { + return err + } + p.Log.Info("found file", zap.String("path", path)) + if !isTemplFile { + return fmt.Errorf("not a templ file") + } + p.TemplSource.Set(string(uri), NewDocument(p.Log, string(b))) + // Parse the template. + template, ok, err := p.parseTemplate(ctx, uri, string(b)) + if err != nil { + p.Log.Error("parseTemplate failure", zap.Error(err)) + } + if !ok { + p.Log.Info("parsing template did not succeed", zap.String("uri", string(uri))) + return nil + } + w := new(strings.Builder) + sm, _, err := generator.Generate(template, w) + if err != nil { + return fmt.Errorf("generate failure: %w", err) + } + p.Log.Info("setting source map cache contents", zap.String("uri", string(uri))) + p.SourceMapCache.Set(string(uri), sm) + // Set the Go contents. + p.GoSource[string(uri)] = w.String() + + } + return nil + }) + if werr != nil { + p.Log.Error("walk error", zap.Error(werr)) + } + } + result.ServerInfo.Name = "templ-lsp" result.ServerInfo.Version = templ.Version() From ab06581c85ac4c71219a3d7c35b603aeddcb55fb Mon Sep 17 00:00:00 2001 From: tris203 Date: Tue, 10 Sep 2024 23:37:02 +0100 Subject: [PATCH 2/5] feat(proxy): preload URIs during initialization - Added a global variable `preLoadURIs` to store URIs to be preloaded. - Modified `Initialize` method to append URIs to `preLoadURIs`. - Updated `Initialized` method to preload URIs by calling `DidOpen`. - Ensured `DidOpenTextDocumentParams` are created and appended correctly. --- cmd/templ/lspcmd/proxy/server.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go index e4423d4a1..40a802395 100644 --- a/cmd/templ/lspcmd/proxy/server.go +++ b/cmd/templ/lspcmd/proxy/server.go @@ -53,6 +53,8 @@ func NewServer(log *zap.Logger, target lsp.Server, cache *SourceMapCache, diagno } } +var preLoadURIs = []*lsp.DidOpenTextDocumentParams{} + // updatePosition maps positions and filenames from source templ files into the target *.go files. func (p *Server) updatePosition(templURI lsp.DocumentURI, current lsp.Position) (ok bool, goURI lsp.DocumentURI, updated lsp.Position) { log := p.Log.With(zap.String("uri", string(templURI))) @@ -240,7 +242,7 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( return err } uri := uri.URI("file://" + path) - isTemplFile, _ := convertTemplToGoURI(uri) + isTemplFile, goURI := convertTemplToGoURI(uri) if isTemplFile { b, err := os.ReadFile(path) if err != nil { @@ -270,6 +272,16 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( // Set the Go contents. p.GoSource[string(uri)] = w.String() + didOpenParams := &lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: goURI, + Text: w.String(), + Version: 1, + LanguageID: "go", + }, + } + + preLoadURIs = append(preLoadURIs, didOpenParams) } return nil }) @@ -287,7 +299,16 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( func (p *Server) Initialized(ctx context.Context, params *lsp.InitializedParams) (err error) { p.Log.Info("client -> server: Initialized") defer p.Log.Info("client -> server: Initialized end") - return p.Target.Initialized(ctx, params) + goInitErr := p.Target.Initialized(ctx, params) + + for _, doParams := range preLoadURIs { + doErr := p.Target.DidOpen(ctx, doParams) + if doErr != nil { + return doErr + } + } + + return goInitErr } func (p *Server) Shutdown(ctx context.Context) (err error) { @@ -514,8 +535,8 @@ func getPackageFromItemDetail(pkg string) string { } type importInsert struct { - LineIndex int Text string + LineIndex int } var nonImportKeywordRegexp = regexp.MustCompile(`^(?:templ|func|css|script|var|const|type)\s`) From 60631f964f1c347f3ef5ff34caab3e01b968855c Mon Sep 17 00:00:00 2001 From: tris203 Date: Sat, 21 Sep 2024 19:36:29 +0100 Subject: [PATCH 3/5] refactor(server): move preLoadURIs to Server struct Moved the preLoadURIs variable from a package-level variable to a field within the Server struct --- cmd/templ/lspcmd/proxy/server.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go index 40a802395..7f7abc555 100644 --- a/cmd/templ/lspcmd/proxy/server.go +++ b/cmd/templ/lspcmd/proxy/server.go @@ -40,6 +40,7 @@ type Server struct { DiagnosticCache *DiagnosticCache TemplSource *DocumentContents GoSource map[string]string + preLoadURIs []*lsp.DidOpenTextDocumentParams } func NewServer(log *zap.Logger, target lsp.Server, cache *SourceMapCache, diagnosticCache *DiagnosticCache) (s *Server) { @@ -53,8 +54,6 @@ func NewServer(log *zap.Logger, target lsp.Server, cache *SourceMapCache, diagno } } -var preLoadURIs = []*lsp.DidOpenTextDocumentParams{} - // updatePosition maps positions and filenames from source templ files into the target *.go files. func (p *Server) updatePosition(templURI lsp.DocumentURI, current lsp.Position) (ok bool, goURI lsp.DocumentURI, updated lsp.Position) { log := p.Log.With(zap.String("uri", string(templURI))) @@ -281,7 +280,7 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( }, } - preLoadURIs = append(preLoadURIs, didOpenParams) + p.preLoadURIs = append(p.preLoadURIs, didOpenParams) } return nil }) @@ -301,11 +300,12 @@ func (p *Server) Initialized(ctx context.Context, params *lsp.InitializedParams) defer p.Log.Info("client -> server: Initialized end") goInitErr := p.Target.Initialized(ctx, params) - for _, doParams := range preLoadURIs { + for i, doParams := range p.preLoadURIs { doErr := p.Target.DidOpen(ctx, doParams) if doErr != nil { return doErr } + p.preLoadURIs[i] = nil } return goInitErr From 53a80c593ac94bc602bdf42e7e5a21a60d193768 Mon Sep 17 00:00:00 2001 From: tris203 Date: Mon, 23 Sep 2024 15:41:08 +0100 Subject: [PATCH 4/5] refactor(proxy): reverse isTempl logic check --- cmd/templ/lspcmd/proxy/server.go | 77 ++++++++++++++++---------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/cmd/templ/lspcmd/proxy/server.go b/cmd/templ/lspcmd/proxy/server.go index 7f7abc555..02fc83571 100644 --- a/cmd/templ/lspcmd/proxy/server.go +++ b/cmd/templ/lspcmd/proxy/server.go @@ -240,48 +240,49 @@ func (p *Server) Initialize(ctx context.Context, params *lsp.InitializeParams) ( if err != nil { return err } + p.Log.Info("found file", zap.String("path", path)) uri := uri.URI("file://" + path) isTemplFile, goURI := convertTemplToGoURI(uri) - if isTemplFile { - b, err := os.ReadFile(path) - if err != nil { - return err - } - p.Log.Info("found file", zap.String("path", path)) - if !isTemplFile { - return fmt.Errorf("not a templ file") - } - p.TemplSource.Set(string(uri), NewDocument(p.Log, string(b))) - // Parse the template. - template, ok, err := p.parseTemplate(ctx, uri, string(b)) - if err != nil { - p.Log.Error("parseTemplate failure", zap.Error(err)) - } - if !ok { - p.Log.Info("parsing template did not succeed", zap.String("uri", string(uri))) - return nil - } - w := new(strings.Builder) - sm, _, err := generator.Generate(template, w) - if err != nil { - return fmt.Errorf("generate failure: %w", err) - } - p.Log.Info("setting source map cache contents", zap.String("uri", string(uri))) - p.SourceMapCache.Set(string(uri), sm) - // Set the Go contents. - p.GoSource[string(uri)] = w.String() - - didOpenParams := &lsp.DidOpenTextDocumentParams{ - TextDocument: lsp.TextDocumentItem{ - URI: goURI, - Text: w.String(), - Version: 1, - LanguageID: "go", - }, - } - p.preLoadURIs = append(p.preLoadURIs, didOpenParams) + if !isTemplFile { + p.Log.Info("not a templ file", zap.String("uri", string(uri))) + return nil + } + + b, err := os.ReadFile(path) + if err != nil { + return err + } + p.TemplSource.Set(string(uri), NewDocument(p.Log, string(b))) + // Parse the template. + template, ok, err := p.parseTemplate(ctx, uri, string(b)) + if err != nil { + p.Log.Error("parseTemplate failure", zap.Error(err)) + } + if !ok { + p.Log.Info("parsing template did not succeed", zap.String("uri", string(uri))) + return nil } + w := new(strings.Builder) + sm, _, err := generator.Generate(template, w) + if err != nil { + return fmt.Errorf("generate failure: %w", err) + } + p.Log.Info("setting source map cache contents", zap.String("uri", string(uri))) + p.SourceMapCache.Set(string(uri), sm) + // Set the Go contents. + p.GoSource[string(uri)] = w.String() + + didOpenParams := &lsp.DidOpenTextDocumentParams{ + TextDocument: lsp.TextDocumentItem{ + URI: goURI, + Text: w.String(), + Version: 1, + LanguageID: "go", + }, + } + + p.preLoadURIs = append(p.preLoadURIs, didOpenParams) return nil }) if werr != nil { From 3b75c1408a19eb28910d363d3e98bce71b1a21c1 Mon Sep 17 00:00:00 2001 From: tris203 Date: Mon, 23 Sep 2024 16:29:08 +0100 Subject: [PATCH 5/5] tests(proxy): add remote file inclusion test cases - Add new test cases in `TestReferences` to handle references from remote files that have not been opened. - Introduce `remoteChild.templ` and `remoteParent.templ` files for testing remote inclusions. - Update existing test cases to include filename in the test struct. --- cmd/templ/lspcmd/lsp_test.go | 63 +++++++++++++------ .../testproject/testdata/remoteChild.templ | 5 ++ .../testproject/testdata/remoteParent.templ | 9 +++ 3 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 cmd/templ/testproject/testdata/remoteChild.templ create mode 100644 cmd/templ/testproject/testdata/remoteParent.templ diff --git a/cmd/templ/lspcmd/lsp_test.go b/cmd/templ/lspcmd/lsp_test.go index 2331e49df..041605a55 100644 --- a/cmd/templ/lspcmd/lsp_test.go +++ b/cmd/templ/lspcmd/lsp_test.go @@ -335,35 +335,19 @@ func TestReferences(t *testing.T) { defer teardown(t) defer cancel() - templFile, err := os.ReadFile(appDir + "/templates.templ") - if err != nil { - t.Fatalf("failed to read file %q: %v", appDir+"/templates.templ", err) - return - - } - err = server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ - TextDocument: protocol.TextDocumentItem{ - URI: uri.URI("file://" + appDir + "/templates.templ"), - LanguageID: "templ", - Version: 1, - Text: string(templFile), - }, - }) - if err != nil { - t.Errorf("failed to register open file: %v", err) - return - } log.Info("Calling References") tests := []struct { line int character int + filename string assert func(t *testing.T, l []protocol.Location) (msg string, ok bool) }{ { // this is the definition of the templ function in the templates.templ file. line: 5, character: 9, + filename: "/templates.templ", assert: func(t *testing.T, actual []protocol.Location) (msg string, ok bool) { expectedReference := []protocol.Location{ { @@ -391,6 +375,7 @@ func TestReferences(t *testing.T) { // this is the definition of the struct in the templates.templ file. line: 21, character: 9, + filename: "/templates.templ", assert: func(t *testing.T, actual []protocol.Location) (msg string, ok bool) { expectedReference := []protocol.Location{ { @@ -414,6 +399,46 @@ func TestReferences(t *testing.T) { return "", true }, }, + { + // this test is for inclusions from a remote file that has not been explicitly called with didOpen + line: 3, + character: 9, + filename: "/remoteChild.templ", + assert: func(t *testing.T, actual []protocol.Location) (msg string, ok bool) { + expectedReference := []protocol.Location{ + { + URI: uri.URI("file://" + appDir + "/remoteParent.templ"), + Range: protocol.Range{ + Start: protocol.Position{ + Line: uint32(3), + Character: uint32(2), + }, + End: protocol.Position{ + Line: uint32(3), + Character: uint32(8), + }, + }, + }, + { + URI: uri.URI("file://" + appDir + "/remoteParent.templ"), + Range: protocol.Range{ + Start: protocol.Position{ + Line: uint32(7), + Character: uint32(2), + }, + End: protocol.Position{ + Line: uint32(7), + Character: uint32(8), + }, + }, + }, + } + if diff := lspdiff.References(expectedReference, actual); diff != "" { + return fmt.Sprintf("Expected: %+v\nActual: %+v", expectedReference, actual), false + } + return "", true + }, + }, } for i, test := range tests { @@ -429,7 +454,7 @@ func TestReferences(t *testing.T) { actual, err := server.References(ctx, &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ TextDocument: protocol.TextDocumentIdentifier{ - URI: uri.URI("file://" + appDir + "/templates.templ"), + URI: uri.URI("file://" + appDir + test.filename), }, // Positions are zero indexed. Position: protocol.Position{ diff --git a/cmd/templ/testproject/testdata/remoteChild.templ b/cmd/templ/testproject/testdata/remoteChild.templ new file mode 100644 index 000000000..17366eaa4 --- /dev/null +++ b/cmd/templ/testproject/testdata/remoteChild.templ @@ -0,0 +1,5 @@ +package main + +templ Remote() { +

This is remote content

+} diff --git a/cmd/templ/testproject/testdata/remoteParent.templ b/cmd/templ/testproject/testdata/remoteParent.templ new file mode 100644 index 000000000..bf66800ce --- /dev/null +++ b/cmd/templ/testproject/testdata/remoteParent.templ @@ -0,0 +1,9 @@ +package main + +templ RemoteInclusionTest() { + @Remote +} + +templ Remote2() { + @Remote +}