From acb603c4077d00e57aa90733c34ea1217861fe69 Mon Sep 17 00:00:00 2001 From: danishprakash Date: Sat, 15 Jan 2022 18:36:37 +0530 Subject: [PATCH 1/5] internal/service: add support for didChangeWatchedFiles Signed-off-by: danishprakash --- .../handlers/did_change_watched_files.go | 71 +++++++++++++++++++ internal/langserver/handlers/service.go | 8 +++ 2 files changed, 79 insertions(+) create mode 100644 internal/langserver/handlers/did_change_watched_files.go diff --git a/internal/langserver/handlers/did_change_watched_files.go b/internal/langserver/handlers/did_change_watched_files.go new file mode 100644 index 000000000..c910f2f60 --- /dev/null +++ b/internal/langserver/handlers/did_change_watched_files.go @@ -0,0 +1,71 @@ +package handlers + +import ( + "context" + "fmt" + "os" + + "github.com/creachadair/jrpc2" + "github.com/hashicorp/terraform-ls/internal/job" + ilsp "github.com/hashicorp/terraform-ls/internal/lsp" + "github.com/hashicorp/terraform-ls/internal/protocol" + lsp "github.com/hashicorp/terraform-ls/internal/protocol" + "github.com/hashicorp/terraform-ls/internal/terraform/ast" +) + +func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidChangeWatchedFilesParams) error { + var ids job.IDs + + for _, change := range params.Changes { + uri := string(change.URI) + _, err := os.Stat(uri) + if err != nil { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Unable to update: %s, Failed to open directory", uri), + }) + continue + } + + // `uri` can either be a file or a director baed on the spec. + // We're not making any assumptions on the above and passing + // the uri as the module path itself for validation. + // + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles + if !ast.IsModuleFilename(uri) && !ast.IsVarsFilename(uri) { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Unable to update file: %s, filetype not supported", uri), + }) + continue + } + + // only handle `Changed` event type + if change.Type == protocol.Changed { + dh := ilsp.HandleFromDocumentURI(change.URI) + + // check existence + _, err = svc.modStore.ModuleByPath(dh.Dir.Path()) + if err != nil { + continue + } + + jobIds, err := svc.parseAndDecodeModule(dh.Dir) + if err != nil { + continue + } + + ids = append(ids, jobIds...) + + } + + } + + // wait for all jobs (slowest usually) to complete + err := svc.stateStore.JobStore.WaitForJobs(ctx, ids...) + if err != nil { + return err + } + + return nil +} diff --git a/internal/langserver/handlers/service.go b/internal/langserver/handlers/service.go index 770f02cd1..81dc67b05 100644 --- a/internal/langserver/handlers/service.go +++ b/internal/langserver/handlers/service.go @@ -287,6 +287,14 @@ func (svc *service) Assigner() (jrpc2.Assigner, error) { return handle(ctx, req, svc.DidChangeWorkspaceFolders) }, + "workspace/didChangeWatchedFiles": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { + err := session.CheckInitializationIsConfirmed() + if err != nil { + return nil, err + } + + return handle(ctx, req, svc.DidChangeWatchedFiles) + }, "textDocument/references": func(ctx context.Context, req *jrpc2.Request) (interface{}, error) { err := session.CheckInitializationIsConfirmed() if err != nil { From e9bf8071eda325d251c9102d7960cadac110169b Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 13 May 2022 14:49:54 +0100 Subject: [PATCH 2/5] Handle both directory and file URIs correctly --- .../handlers/did_change_watched_files.go | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/internal/langserver/handlers/did_change_watched_files.go b/internal/langserver/handlers/did_change_watched_files.go index c910f2f60..2a3ac6fc8 100644 --- a/internal/langserver/handlers/did_change_watched_files.go +++ b/internal/langserver/handlers/did_change_watched_files.go @@ -6,62 +6,64 @@ import ( "os" "github.com/creachadair/jrpc2" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/job" - ilsp "github.com/hashicorp/terraform-ls/internal/lsp" "github.com/hashicorp/terraform-ls/internal/protocol" lsp "github.com/hashicorp/terraform-ls/internal/protocol" "github.com/hashicorp/terraform-ls/internal/terraform/ast" + "github.com/hashicorp/terraform-ls/internal/uri" ) func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidChangeWatchedFilesParams) error { var ids job.IDs for _, change := range params.Changes { - uri := string(change.URI) - _, err := os.Stat(uri) + rawURI := string(change.URI) + + fullPath, err := uri.PathFromURI(rawURI) if err != nil { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Unable to update: %s, Failed to open directory", uri), - }) + svc.logger.Printf("Unable to update %q: %s", rawURI, err) continue } - // `uri` can either be a file or a director baed on the spec. - // We're not making any assumptions on the above and passing - // the uri as the module path itself for validation. - // - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles - if !ast.IsModuleFilename(uri) && !ast.IsVarsFilename(uri) { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Unable to update file: %s, filetype not supported", uri), - }) + fi, err := os.Stat(fullPath) + if err != nil { + svc.logger.Printf("Unable to update %q: %s ", fullPath, err) continue } - // only handle `Changed` event type - if change.Type == protocol.Changed { - dh := ilsp.HandleFromDocumentURI(change.URI) + // URI can either be a file or a directory based on the LSP spec. + var dirHandle document.DirHandle + if !fi.IsDir() { + if !ast.IsModuleFilename(fi.Name()) && !ast.IsVarsFilename(fi.Name()) { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Unable to update %q: filetype not supported. "+ + "This is likely a bug which should be reported.", fullPath), + }) + continue + } + docHandle := document.HandleFromPath(fullPath) + dirHandle = docHandle.Dir + } else { + dirHandle = document.DirHandleFromPath(fullPath) + } - // check existence - _, err = svc.modStore.ModuleByPath(dh.Dir.Path()) + if change.Type == protocol.Changed { + _, err = svc.modStore.ModuleByPath(dirHandle.Path()) if err != nil { continue } - jobIds, err := svc.parseAndDecodeModule(dh.Dir) + jobIds, err := svc.parseAndDecodeModule(dirHandle) if err != nil { continue } ids = append(ids, jobIds...) - } - } - // wait for all jobs (slowest usually) to complete err := svc.stateStore.JobStore.WaitForJobs(ctx, ids...) if err != nil { return err From 9671ce6d6f44e2a0b450afacfd2e64fd79081a86 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 13 May 2022 15:03:45 +0100 Subject: [PATCH 3/5] Add tests --- .../handlers/did_change_watched_files_test.go | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 internal/langserver/handlers/did_change_watched_files_test.go diff --git a/internal/langserver/handlers/did_change_watched_files_test.go b/internal/langserver/handlers/did_change_watched_files_test.go new file mode 100644 index 000000000..715f7150e --- /dev/null +++ b/internal/langserver/handlers/did_change_watched_files_test.go @@ -0,0 +1,233 @@ +package handlers + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-ls/internal/langserver" + "github.com/hashicorp/terraform-ls/internal/state" + "github.com/hashicorp/terraform-ls/internal/terraform/exec" + "github.com/hashicorp/terraform-ls/internal/terraform/module" + "github.com/stretchr/testify/mock" +) + +func TestLangServer_DidChangeWatchedFiles_file(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Change main.tf on disk + newSrc := `variable "original" { + default = "foo" +} +` + err = os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(newSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + // Verify nothing has changed yet + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": "%s/main.tf", + "type": 2 + } + ] +}`, TempDir(t).URI)}) + + // Verify file was re-parsed + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(newSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } +} + +func TestLangServer_DidChangeWatchedFiles_dir(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Change main.tf on disk + newSrc := `variable "original" { + default = "foo" +} +` + err = os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(newSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + // Verify nothing has changed yet + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": %q, + "type": 2 + } + ] +}`, TempDir(t).URI)}) + + // Verify file was re-parsed + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(newSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } +} From fb717436a6f48adc58946b80cc9ad8cfb0338cb1 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 16 May 2022 20:13:46 +0100 Subject: [PATCH 4/5] account for file/dir deletions and creations too --- .../handlers/did_change_watched_files.go | 152 +++++- .../handlers/did_change_watched_files_test.go | 464 +++++++++++++++++- .../handlers/did_change_workspace_folders.go | 105 ++-- 3 files changed, 644 insertions(+), 77 deletions(-) diff --git a/internal/langserver/handlers/did_change_watched_files.go b/internal/langserver/handlers/did_change_watched_files.go index 2a3ac6fc8..02228ee94 100644 --- a/internal/langserver/handlers/did_change_watched_files.go +++ b/internal/langserver/handlers/did_change_watched_files.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path" "github.com/creachadair/jrpc2" "github.com/hashicorp/terraform-ls/internal/document" @@ -20,48 +21,112 @@ func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidCha for _, change := range params.Changes { rawURI := string(change.URI) - fullPath, err := uri.PathFromURI(rawURI) + rawPath, err := uri.PathFromURI(rawURI) if err != nil { - svc.logger.Printf("Unable to update %q: %s", rawURI, err) + svc.logger.Printf("error parsing %q: %s", rawURI, err) continue } - fi, err := os.Stat(fullPath) - if err != nil { - svc.logger.Printf("Unable to update %q: %s ", fullPath, err) - continue - } + if change.Type == protocol.Deleted { + // We don't know whether file or dir is being deleted + // 1st we just blindly try to look it up as a directory + _, err = svc.modStore.ModuleByPath(rawPath) + if err == nil { + svc.removeIndexedModule(ctx, rawURI) + continue + } + + // 2nd we try again assuming it is a file + parentDir := path.Dir(rawPath) + _, err = svc.modStore.ModuleByPath(parentDir) + if err != nil { + svc.logger.Printf("error finding module (%q deleted): %s", parentDir, err) + continue + } + + // and check the parent directory still exists + fi, err := os.Stat(parentDir) + if err != nil { + if os.IsNotExist(err) { + // if not, we remove the indexed module + svc.removeIndexedModule(ctx, rawURI) + continue + } + svc.logger.Printf("error checking existence (%q deleted): %s", parentDir, err) + continue + } + if !fi.IsDir() { + svc.logger.Printf("error: %q (deleted) is not a directory", parentDir) + continue + } - // URI can either be a file or a directory based on the LSP spec. - var dirHandle document.DirHandle - if !fi.IsDir() { - if !ast.IsModuleFilename(fi.Name()) && !ast.IsVarsFilename(fi.Name()) { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Unable to update %q: filetype not supported. "+ - "This is likely a bug which should be reported.", fullPath), - }) + // if the parent directory exists, we just need to + // reparse the module after a file was deleted from it + dirHandle := document.DirHandleFromPath(parentDir) + jobIds, err := svc.parseAndDecodeModule(dirHandle) + if err != nil { + svc.logger.Printf("error parsing module (%q deleted): %s", rawURI, err) continue } - docHandle := document.HandleFromPath(fullPath) - dirHandle = docHandle.Dir - } else { - dirHandle = document.DirHandleFromPath(fullPath) + + ids = append(ids, jobIds...) } if change.Type == protocol.Changed { - _, err = svc.modStore.ModuleByPath(dirHandle.Path()) + ph, err := modHandleFromRawOsPath(ctx, rawPath) if err != nil { + if err == ErrorSkip { + continue + } + svc.logger.Printf("error (%q changed): %s", rawURI, err) continue } - jobIds, err := svc.parseAndDecodeModule(dirHandle) + _, err = svc.modStore.ModuleByPath(ph.DirHandle.Path()) + if err != nil { + svc.logger.Printf("error finding module (%q changed): %s", rawURI, err) + continue + } + + jobIds, err := svc.parseAndDecodeModule(ph.DirHandle) if err != nil { + svc.logger.Printf("error parsing module (%q changed): %s", rawURI, err) continue } ids = append(ids, jobIds...) } + + if change.Type == protocol.Created { + ph, err := modHandleFromRawOsPath(ctx, rawPath) + if err != nil { + if err == ErrorSkip { + continue + } + svc.logger.Printf("error (%q created): %s", rawURI, err) + continue + } + + if ph.IsDir { + err = svc.stateStore.WalkerPaths.EnqueueDir(ph.DirHandle) + if err != nil { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Ignoring new folder %s: %s."+ + " This is most likely bug, please report it.", rawURI, err), + }) + continue + } + } else { + jobIds, err := svc.parseAndDecodeModule(ph.DirHandle) + if err != nil { + svc.logger.Printf("error parsing module (%q created): %s", rawURI, err) + continue + } + + ids = append(ids, jobIds...) + } + } } err := svc.stateStore.JobStore.WaitForJobs(ctx, ids...) @@ -71,3 +136,46 @@ func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidCha return nil } + +func modHandleFromRawOsPath(ctx context.Context, rawPath string) (*parsedModuleHandle, error) { + fi, err := os.Stat(rawPath) + if err != nil { + return nil, err + } + + // URI can either be a file or a directory based on the LSP spec. + if fi.IsDir() { + return &parsedModuleHandle{ + DirHandle: document.DirHandleFromPath(rawPath), + IsDir: true, + }, nil + } + + if !ast.IsModuleFilename(fi.Name()) && !ast.IsVarsFilename(fi.Name()) { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Unable to update %q: filetype not supported. "+ + "This is likely a bug which should be reported.", rawPath), + }) + return nil, ErrorSkip + } + + docHandle := document.HandleFromPath(rawPath) + return &parsedModuleHandle{ + DirHandle: docHandle.Dir, + IsDir: false, + }, nil +} + +type parsedModuleHandle struct { + DirHandle document.DirHandle + IsDir bool +} + +var ErrorSkip = errSkip{} + +type errSkip struct{} + +func (es errSkip) Error() string { + return "skip" +} diff --git a/internal/langserver/handlers/did_change_watched_files_test.go b/internal/langserver/handlers/did_change_watched_files_test.go index 715f7150e..bed61dad2 100644 --- a/internal/langserver/handlers/did_change_watched_files_test.go +++ b/internal/langserver/handlers/did_change_watched_files_test.go @@ -7,6 +7,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-ls/internal/document" "github.com/hashicorp/terraform-ls/internal/langserver" "github.com/hashicorp/terraform-ls/internal/state" "github.com/hashicorp/terraform-ls/internal/terraform/exec" @@ -14,7 +17,7 @@ import ( "github.com/stretchr/testify/mock" ) -func TestLangServer_DidChangeWatchedFiles_file(t *testing.T) { +func TestLangServer_DidChangeWatchedFiles_change_file(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) @@ -74,7 +77,7 @@ func TestLangServer_DidChangeWatchedFiles_file(t *testing.T) { } // Change main.tf on disk - newSrc := `variable "original" { + newSrc := `variable "new" { default = "foo" } ` @@ -123,7 +126,254 @@ func TestLangServer_DidChangeWatchedFiles_file(t *testing.T) { } } -func TestLangServer_DidChangeWatchedFiles_dir(t *testing.T) { +func TestLangServer_DidChangeWatchedFiles_create_file(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): { + { + Method: "Version", + Repeatability: 2, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + version.Must(version.NewVersion("0.12.0")), + nil, + nil, + }, + }, + { + Method: "GetExecPath", + Repeatability: 1, + ReturnArguments: []interface{}{ + "", + }, + }, + { + Method: "ProviderSchemas", + Repeatability: 2, + Arguments: []interface{}{ + mock.AnythingOfType(""), + }, + ReturnArguments: []interface{}{ + &tfjson.ProviderSchemas{ + FormatVersion: "0.1", + Schemas: map[string]*tfjson.ProviderSchema{ + "test": { + ConfigSchema: &tfjson.Schema{}, + }, + }, + }, + nil, + }, + }, + }, + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Create another.tf on disk + newSrc := `variable "another" { + default = "foo" +} +` + err = os.WriteFile(filepath.Join(tmpDir.Path(), "another.tf"), []byte(newSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + // Verify another.tf was not parsed *yet* + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["another.tf"] + if ok { + t.Fatalf("not expected to be parsed: %q", "another.tf") + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": "%s/main.tf", + "type": 1 + } + ] +}`, TempDir(t).URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + + // Verify another.tf was parsed + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["another.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "another.tf") + } + if diff := cmp.Diff(newSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "another.tf", diff) + } +} + +func TestLangServer_DidChangeWatchedFiles_delete_file(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Delete main.tf from disk + err = os.Remove(filepath.Join(tmpDir.Path(), "main.tf")) + if err != nil { + t.Fatal(err) + } + + // Verify main.tf still remains parsed + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": "%s/main.tf", + "type": 3 + } + ] +}`, TempDir(t).URI)}) + + // Verify main.tf was deleted + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if ok { + t.Fatalf("not expected file to be parsed: %q", "main.tf") + } +} + +func TestLangServer_DidChangeWatchedFiles_change_dir(t *testing.T) { tmpDir := TempDir(t) InitPluginCache(t, tmpDir.Path()) @@ -183,7 +433,7 @@ func TestLangServer_DidChangeWatchedFiles_dir(t *testing.T) { } // Change main.tf on disk - newSrc := `variable "original" { + newSrc := `variable "new" { default = "foo" } ` @@ -231,3 +481,209 @@ func TestLangServer_DidChangeWatchedFiles_dir(t *testing.T) { t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) } } + +func TestLangServer_DidChangeWatchedFiles_create_dir(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Create new ./submodule w/ main.tf on disk + submodPath := filepath.Join(tmpDir.Path(), "submodule") + submodHandle := document.DirHandleFromPath(submodPath) + err = os.Mkdir(submodPath, 0o755) + if err != nil { + t.Fatal(err) + } + newSrc := `variable "new" { + default = "foo" +} +` + err = os.WriteFile(filepath.Join(submodPath, "main.tf"), []byte(newSrc), 0o755) + if err != nil { + t.Fatal(err) + } + InitPluginCache(t, submodHandle.Path()) + + // Verify submodule was not parsed yet + mod, err = ss.Modules.ModuleByPath(submodPath) + if err == nil { + t.Fatalf("%q: expected module not to be found", submodPath) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": %q, + "type": 1 + } + ] +}`, submodHandle.URI)}) + waitForWalkerPath(t, ss, wc, submodHandle) + + // Verify submodule was parsed + mod, err = ss.Modules.ModuleByPath(submodPath) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(newSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } +} + +func TestLangServer_DidChangeWatchedFiles_delete_dir(t *testing.T) { + tmpDir := TempDir(t) + + InitPluginCache(t, tmpDir.Path()) + + originalSrc := `variable "original" { + default = "foo" +} +` + err := os.WriteFile(filepath.Join(tmpDir.Path(), "main.tf"), []byte(originalSrc), 0o755) + if err != nil { + t.Fatal(err) + } + + ss, err := state.NewStateStore() + if err != nil { + t.Fatal(err) + } + wc := module.NewWalkerCollector() + + ls := langserver.NewLangServerMock(t, NewMockSession(&MockSessionInput{ + TerraformCalls: &exec.TerraformMockCalls{ + PerWorkDir: map[string][]*mock.Call{ + tmpDir.Path(): validTfMockCalls(), + }, + }, + StateStore: ss, + WalkerCollector: wc, + })) + stop := ls.Start(t) + defer stop() + + ls.Call(t, &langserver.CallRequest{ + Method: "initialize", + ReqParams: fmt.Sprintf(`{ + "capabilities": {}, + "rootUri": %q, + "processId": 12345 + }`, tmpDir.URI)}) + waitForWalkerPath(t, ss, wc, tmpDir) + ls.Notify(t, &langserver.CallRequest{ + Method: "initialized", + ReqParams: "{}", + }) + + // Verify main.tf was parsed + mod, err := ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles := mod.ParsedModuleFiles.AsMap() + parsedFile, ok := parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + // Delete directory from disk + err = os.RemoveAll(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + + // Verify nothing has changed yet + mod, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err != nil { + t.Fatal(err) + } + parsedFiles = mod.ParsedModuleFiles.AsMap() + parsedFile, ok = parsedFiles["main.tf"] + if !ok { + t.Fatalf("file not parsed: %q", "main.tf") + } + if diff := cmp.Diff(originalSrc, string(parsedFile.Bytes)); diff != "" { + t.Fatalf("bytes mismatch for %q: %s", "main.tf", diff) + } + + ls.Call(t, &langserver.CallRequest{ + Method: "workspace/didChangeWatchedFiles", + ReqParams: fmt.Sprintf(`{ + "changes": [ + { + "uri": %q, + "type": 3 + } + ] +}`, TempDir(t).URI)}) + + // Verify module is gone + _, err = ss.Modules.ModuleByPath(tmpDir.Path()) + if err == nil { + t.Fatalf("expected module at %q to be gone", tmpDir.Path()) + } +} diff --git a/internal/langserver/handlers/did_change_workspace_folders.go b/internal/langserver/handlers/did_change_workspace_folders.go index caa88f82f..77518f116 100644 --- a/internal/langserver/handlers/did_change_workspace_folders.go +++ b/internal/langserver/handlers/did_change_workspace_folders.go @@ -5,72 +5,75 @@ import ( "fmt" "github.com/creachadair/jrpc2" - lsctx "github.com/hashicorp/terraform-ls/internal/context" "github.com/hashicorp/terraform-ls/internal/document" lsp "github.com/hashicorp/terraform-ls/internal/protocol" ) func (svc *service) DidChangeWorkspaceFolders(ctx context.Context, params lsp.DidChangeWorkspaceFoldersParams) error { - watcher, err := lsctx.Watcher(ctx) - if err != nil { - return err + for _, removed := range params.Event.Removed { + svc.removeIndexedModule(ctx, removed.URI) } - for _, removed := range params.Event.Removed { - modHandle := document.DirHandleFromURI(removed.URI) + for _, added := range params.Event.Added { + svc.indexNewModule(ctx, added.URI) + } - err := svc.stateStore.WalkerPaths.DequeueDir(modHandle) - if err != nil { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Ignoring removed workspace folder %s: %s."+ - " This is most likely bug, please report it.", removed.URI, err), - }) - continue - } + return nil +} - err = watcher.RemoveModule(modHandle.Path()) - if err != nil { - svc.logger.Printf("failed to remove module from watcher: %s", err) - continue - } +func (svc *service) indexNewModule(ctx context.Context, modURI string) { + modHandle := document.DirHandleFromURI(modURI) - err = svc.stateStore.JobStore.DequeueJobsForDir(modHandle) - if err != nil { - svc.logger.Printf("failed to dequeue jobs for module: %s", err) - continue - } + err := svc.stateStore.WalkerPaths.EnqueueDir(modHandle) + if err != nil { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Ignoring new folder %s: %s."+ + " This is most likely bug, please report it.", modURI, err), + }) + return + } - callers, err := svc.modStore.CallersOfModule(modHandle.Path()) - if err != nil { - svc.logger.Printf("failed to remove module from watcher: %s", err) - continue - } - if len(callers) == 0 { - err = svc.modStore.Remove(modHandle.Path()) - svc.logger.Printf("failed to remove module: %s", err) - } + err = svc.watcher.AddModule(modHandle.Path()) + if err != nil { + svc.logger.Printf("failed to add module to watcher: %s", err) + return } +} - for _, added := range params.Event.Added { - modHandle := document.DirHandleFromURI(added.URI) +func (svc *service) removeIndexedModule(ctx context.Context, modURI string) { + modHandle := document.DirHandleFromURI(modURI) - err = svc.stateStore.WalkerPaths.EnqueueDir(modHandle) - if err != nil { - jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ - Type: lsp.Warning, - Message: fmt.Sprintf("Ignoring new workspace folder %s: %s."+ - " This is most likely bug, please report it.", added.URI, err), - }) - continue - } + err := svc.stateStore.WalkerPaths.DequeueDir(modHandle) + if err != nil { + jrpc2.ServerFromContext(ctx).Notify(ctx, "window/showMessage", &lsp.ShowMessageParams{ + Type: lsp.Warning, + Message: fmt.Sprintf("Ignoring removed folder %s: %s."+ + " This is most likely bug, please report it.", modURI, err), + }) + return + } - err = watcher.AddModule(modHandle.Path()) - if err != nil { - svc.logger.Printf("failed to add module to watcher: %s", err) - continue - } + err = svc.watcher.RemoveModule(modHandle.Path()) + if err != nil { + svc.logger.Printf("failed to remove module from watcher: %s", err) + return } - return nil + err = svc.stateStore.JobStore.DequeueJobsForDir(modHandle) + if err != nil { + svc.logger.Printf("failed to dequeue jobs for module: %s", err) + return + } + + callers, err := svc.modStore.CallersOfModule(modHandle.Path()) + if err != nil { + svc.logger.Printf("failed to remove module from watcher: %s", err) + return + } + + if len(callers) == 0 { + err = svc.modStore.Remove(modHandle.Path()) + svc.logger.Printf("failed to remove module: %s", err) + } } From 0e23e3e51f653ebed249e957b55dcd2adff91c1d Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 16 May 2022 20:31:54 +0100 Subject: [PATCH 5/5] use filepath.Dir instead path.Dir to account for OS differences --- internal/langserver/handlers/did_change_watched_files.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/langserver/handlers/did_change_watched_files.go b/internal/langserver/handlers/did_change_watched_files.go index 02228ee94..f9200f1b4 100644 --- a/internal/langserver/handlers/did_change_watched_files.go +++ b/internal/langserver/handlers/did_change_watched_files.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "os" - "path" + "path/filepath" "github.com/creachadair/jrpc2" "github.com/hashicorp/terraform-ls/internal/document" @@ -37,7 +37,7 @@ func (svc *service) DidChangeWatchedFiles(ctx context.Context, params lsp.DidCha } // 2nd we try again assuming it is a file - parentDir := path.Dir(rawPath) + parentDir := filepath.Dir(rawPath) _, err = svc.modStore.ModuleByPath(parentDir) if err != nil { svc.logger.Printf("error finding module (%q deleted): %s", parentDir, err)