diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 7535eb3b..2096e98b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -356,10 +356,10 @@ func (l *LanguageServer) StartDiagnosticsWorker(ctx context.Context) { case <-ctx.Done(): return case job := <-workspaceLintRuns: - // if there are no files in the cache, then there is no need to - // run the aggregate report. This can happen if the server is - // very slow to start up. - if len(l.cache.GetAllFiles()) == 0 { + // if there are no parsed modules in the cache, then there is + // no need to run the aggregate report. This can happen if the + // server is very slow to start up. + if len(l.cache.GetAllModules()) == 0 { continue } @@ -989,7 +989,9 @@ func (l *LanguageServer) StartTemplateWorker(ctx context.Context) { // determine the new contents for the file, if permitted newContents, err := l.templateContentsForFile(job.URI) if err != nil { - l.logError(fmt.Errorf("failed to template new file: %w", err)) + if !errors.Is(err, &templatingInRootError{}) { + l.logError(fmt.Errorf("failed to template new file: %w", err)) + } continue } @@ -1083,6 +1085,12 @@ func (l *LanguageServer) StartWebServer(ctx context.Context) { l.webServer.Start(ctx) } +type templatingInRootError struct{} + +func (*templatingInRootError) Error() string { + return "templating is not supported in the root of the workspace" +} + func (l *LanguageServer) templateContentsForFile(fileURI string) (string, error) { content, ok := l.cache.GetFileContents(fileURI) if !ok { @@ -1093,6 +1101,11 @@ func (l *LanguageServer) templateContentsForFile(fileURI string) (string, error) return "", errors.New("file already has contents, templating not allowed") } + if filepath.Dir(uri.ToPath(l.clientIdentifier, fileURI)) == + uri.ToPath(l.clientIdentifier, l.workspaceRootURI) { + return "", &templatingInRootError{} + } + diskContent, err := os.ReadFile(uri.ToPath(l.clientIdentifier, fileURI)) if err == nil { // then we found the file on disk @@ -1109,6 +1122,16 @@ func (l *LanguageServer) templateContentsForFile(fileURI string) (string, error) return "", fmt.Errorf("failed to get potential roots during templating of new file: %w", err) } + // handle the case where the root is unknown by providing the server's root + // dir as a defacto root. This allows templating of files when there is no + // known root, but the package could be determined based on the file path + // relative to the server's workspace root + if len(roots) == 1 && roots[0] == dir { + roots = []string{uri.ToPath(l.clientIdentifier, l.workspaceRootURI)} + } + + roots = append(roots, uri.ToPath(l.clientIdentifier, l.workspaceRootURI)) + longestPrefixRoot := "" for _, root := range roots { @@ -2028,7 +2051,9 @@ func (l *LanguageServer) handleTextDocumentFormatting( // instead if oldContent == "" { newContent, err := l.templateContentsForFile(params.TextDocument.URI) - if err != nil { + if err != nil && errors.Is(err, &templatingInRootError{}) { + return nil, nil + } else if err != nil { return nil, fmt.Errorf("failed to template contents as a templating fallback: %w", err) } diff --git a/internal/lsp/server_template_test.go b/internal/lsp/server_template_test.go index bd9bd24f..9a3d9426 100644 --- a/internal/lsp/server_template_test.go +++ b/internal/lsp/server_template_test.go @@ -3,6 +3,7 @@ package lsp import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -41,14 +42,6 @@ func TestTemplateContentsForFile(t *testing.T) { }, ExpectedError: "file on disk already has contents", }, - "empty file is templated as main when no root": { - FileKey: "foo/bar.rego", - CacheFileContents: "", - DiskContents: map[string]string{ - "foo/bar.rego": "", - }, - ExpectedContents: "package main\n\nimport rego.v1\n", - }, "empty file is templated based on root": { FileKey: "foo/bar.rego", CacheFileContents: "", @@ -100,7 +93,6 @@ func TestTemplateContentsForFile(t *testing.T) { ctx := context.Background() - // create a new language server s := NewLanguageServer(ctx, &LanguageServerOptions{ErrorLog: newTestLogger(t)}) s.workspaceRootURI = uri.FromPath(clients.IdentifierGeneric, td) @@ -124,6 +116,73 @@ func TestTemplateContentsForFile(t *testing.T) { } } +func TestTemplateContentsForFileInWorkspaceRoot(t *testing.T) { + t.Parallel() + + td := t.TempDir() + + err := os.MkdirAll(filepath.Join(td, ".regal"), 0o755) + if err != nil { + t.Fatalf("failed to create directory %s: %s", filepath.Join(td, ".regal"), err) + } + + err = os.WriteFile(filepath.Join(td, ".regal/config.yaml"), []byte{}, 0o600) + if err != nil { + t.Fatalf("failed to create directory %s: %s", filepath.Join(td, ".regal"), err) + } + + ctx := context.Background() + + s := NewLanguageServer(ctx, &LanguageServerOptions{ErrorLog: newTestLogger(t)}) + s.workspaceRootURI = uri.FromPath(clients.IdentifierGeneric, td) + + fileURI := uri.FromPath(clients.IdentifierGeneric, filepath.Join(td, "foo.rego")) + + s.cache.SetFileContents(fileURI, "") + + _, err = s.templateContentsForFile(fileURI) + if err == nil { + t.Fatalf("expected error") + } + + if !errors.Is(err, &templatingInRootError{}) { + t.Fatalf("expected error to be templatingInRootError, got %T", err) + } +} + +func TestTemplateContentsForFileWithUnknownRoot(t *testing.T) { + t.Parallel() + + td := t.TempDir() + + ctx := context.Background() + + s := NewLanguageServer(ctx, &LanguageServerOptions{ErrorLog: newTestLogger(t)}) + s.workspaceRootURI = uri.FromPath(clients.IdentifierGeneric, td) + + err := os.MkdirAll(filepath.Join(td, "foo"), 0o755) + if err != nil { + t.Fatalf("failed to create directory %s: %s", filepath.Join(td, "foo"), err) + } + + fileURI := uri.FromPath(clients.IdentifierGeneric, filepath.Join(td, "foo/bar.rego")) + + s.cache.SetFileContents(fileURI, "") + + newContents, err := s.templateContentsForFile(fileURI) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + exp := `package foo + +import rego.v1 +` + if exp != newContents { + t.Errorf("unexpected content: %s, want %s", newContents, exp) + } +} + func TestNewFileTemplating(t *testing.T) { t.Parallel()