Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/service: add support for didChangeWatchedFiles #790

Merged
merged 5 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions internal/langserver/handlers/did_change_watched_files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package handlers

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/creachadair/jrpc2"
"github.com/hashicorp/terraform-ls/internal/document"
"github.com/hashicorp/terraform-ls/internal/job"
"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 {
rawURI := string(change.URI)

rawPath, err := uri.PathFromURI(rawURI)
if err != nil {
svc.logger.Printf("error parsing %q: %s", rawURI, 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 := filepath.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
Copy link
Contributor Author

@danishprakash danishprakash May 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When can this happen? The user deleted the only file and then subsequently the directory, too? If yes, then those would also be different events here and that's probably why you're just ensuring the directory is still present or not. Is my understanding correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accounts for the deletion of a whole directory.

If yes, then those would also be different events here

Frankly, I don't know, since LSP doesn't go into detail about how clients should or shouldn't group file events. In fact when I tried to rm -rf submodule in VS Code where submodule had some *.tf files, no events were fired at all - which may be a bug in the VS Code language client - unsure.

So in the face of uncertainty I'm just being a little more defensive here.

Copy link
Contributor Author

@danishprakash danishprakash May 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for the explanation and yes, it's still a little nebulous when it comes to understanding client-side event emits.

may be a bug in the VS Code language client

Do you mean https://github.com/Microsoft/vscode-languageserver-node?

Copy link
Member

@radeksimko radeksimko May 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be either VS Code itself or the language client you linked and it may also be OS-specific problem since file watching is highly dependent on the OS and implementations/behaviours can differ between systems - it would take some debugging to find this out.

svc.removeIndexedModule(ctx, rawURI)
continue
}
svc.logger.Printf("error checking existence (%q deleted): %s", parentDir, err)
continue
}
if !fi.IsDir() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious as to why this check is here since we're doing this earlier:

parentDir := filepath.Dir(rawPath)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this should basically never happen - I was just trying to be a bit more defensive here too.

The only realistic scenarios which come to mind involve cases where it's not a file nor a directory, but e.g. symlink or any special type of file - which is mostly relevant on Unix-based systems:
https://pkg.go.dev/io/fs#FileMode

Copy link
Contributor Author

@danishprakash danishprakash May 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so just yesterday, I got bit by a severe production bug wherein I was copying files from one directory to another. Without a safety check of whether the file is a regular file or not, we ended up doing a cp on a file which was a UNIX pipe and the application just froze indefinitely waiting for the copy to finish which would never be the case. I can totally understand the need for this check here howsoever obvious it might seem.

svc.logger.Printf("error: %q (deleted) is not a directory", parentDir)
continue
}

// 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
}

ids = append(ids, jobIds...)
}

if change.Type == protocol.Changed {
ph, err := modHandleFromRawOsPath(ctx, rawPath)
if err != nil {
if err == ErrorSkip {
continue
}
svc.logger.Printf("error (%q changed): %s", rawURI, err)
continue
}

_, 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...)
if err != nil {
return err
}

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"
}
Loading