internal/lsp/fake: reflect on-disk changes in clean buffers
If an open buffer is unmodified from the on-disk version, and that
on-disk content changes, VS Code will update the buffer with the change.
This is relevant for our quick fixes that use the go command to make
changes to go.mod/sum -- we want tests to pick up the changes without
needing to close/reopen the buffer.
For whatever reason, VS Code doesn't do this for deletions, and it made
the implementation easier, so I followed suit. I also mimicked its
behavior of sending both in-memory and on-disk change notifications.
Change-Id: I838a64b2f48f3cbe1a86035293923951b53aecf3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/267577
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/codelens_test.go b/gopls/internal/regtest/codelens_test.go
index 9dfafde..a5fca84 100644
--- a/gopls/internal/regtest/codelens_test.go
+++ b/gopls/internal/regtest/codelens_test.go
@@ -9,6 +9,7 @@
"strings"
"testing"
+ "golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
@@ -110,7 +111,8 @@
runner.Run(t, shouldUpdateDep, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.ExecuteCodeLensCommand("go.mod", source.CommandUpgradeDependency)
- got := env.ReadWorkspaceFile("go.mod")
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+ got := env.Editor.BufferText("go.mod")
const wantGoMod = `module mod.com
go 1.12
@@ -164,7 +166,8 @@
runner.Run(t, shouldRemoveDep, func(t *testing.T, env *Env) {
env.OpenFile("go.mod")
env.ExecuteCodeLensCommand("go.mod", source.CommandTidy)
- got := env.ReadWorkspaceFile("go.mod")
+ env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 1))
+ got := env.Editor.BufferText("go.mod")
const wantGoMod = `module mod.com
go 1.14
diff --git a/gopls/internal/regtest/diagnostics_test.go b/gopls/internal/regtest/diagnostics_test.go
index 83decca..d3685dd 100644
--- a/gopls/internal/regtest/diagnostics_test.go
+++ b/gopls/internal/regtest/diagnostics_test.go
@@ -639,7 +639,6 @@
// Test for golang/go#38211.
func Test_Issue38211(t *testing.T) {
- t.Skip("Requires CL 267577 to work without the save hook.")
testenv.NeedsGo1Point(t, 14)
const ardanLabs = `
-- go.mod --
diff --git a/gopls/internal/regtest/modfile_test.go b/gopls/internal/regtest/modfile_test.go
index 69ff5e9..2f897a2 100644
--- a/gopls/internal/regtest/modfile_test.go
+++ b/gopls/internal/regtest/modfile_test.go
@@ -379,10 +379,8 @@
example.com/blah/v2 v2.0.0
)
`
- // We need to read from disk even though go.mod is open -- the regtests
- // currently don't apply on-disk changes to open but unmodified buffers
- // like most editors would.
- if got := env.ReadWorkspaceFile("go.mod"); got != want {
+ env.Await(EmptyDiagnostics("go.mod"))
+ if got := env.Editor.BufferText("go.mod"); got != want {
t.Fatalf("suggested fixes failed:\n%s", tests.Diff(want, got))
}
})
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 9ef80fe..3cc1c06 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -47,6 +47,7 @@
version int
path string
content []string
+ dirty bool
}
func (b buffer) text() string {
@@ -270,10 +271,28 @@
if e.Server == nil {
return
}
+ e.mu.Lock()
var lspevts []protocol.FileEvent
for _, evt := range evts {
+ // Always send an on-disk change, even for events that seem useless
+ // because they're shadowed by an open buffer.
lspevts = append(lspevts, evt.ProtocolEvent)
+
+ if buf, ok := e.buffers[evt.Path]; ok {
+ // Following VS Code, don't honor deletions or changes to dirty buffers.
+ if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
+ continue
+ }
+
+ content, err := e.sandbox.Workdir.ReadFile(evt.Path)
+ if err != nil {
+ continue // A race with some other operation.
+ }
+ // During shutdown, this call will fail. Ignore the error.
+ _ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
+ }
}
+ e.mu.Unlock()
e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
Changes: lspevts,
})
@@ -285,15 +304,7 @@
if err != nil {
return err
}
- return e.CreateBuffer(ctx, path, content)
-}
-
-func newBuffer(path, content string) buffer {
- return buffer{
- version: 1,
- path: path,
- content: strings.Split(content, "\n"),
- }
+ return e.createBuffer(ctx, path, false, content)
}
func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
@@ -314,7 +325,16 @@
// CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
// containing the given textual content.
func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
- buf := newBuffer(path, content)
+ return e.createBuffer(ctx, path, true, content)
+}
+
+func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
+ buf := buffer{
+ version: 1,
+ path: path,
+ content: strings.Split(content, "\n"),
+ dirty: dirty,
+ }
e.mu.Lock()
e.buffers[path] = buf
item := textDocumentItem(e.sandbox.Workdir, buf)
@@ -396,6 +416,12 @@
if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
return errors.Errorf("writing %q: %w", path, err)
}
+
+ e.mu.Lock()
+ buf.dirty = false
+ e.buffers[path] = buf
+ e.mu.Unlock()
+
if e.Server != nil {
params := &protocol.DidSaveTextDocumentParams{
TextDocument: protocol.VersionedTextDocumentIdentifier{
@@ -534,7 +560,7 @@
e.mu.Lock()
defer e.mu.Unlock()
lines := strings.Split(content, "\n")
- return e.setBufferContentLocked(ctx, path, lines, nil)
+ return e.setBufferContentLocked(ctx, path, true, lines, nil)
}
// BufferText returns the content of the buffer with the given name.
@@ -563,16 +589,17 @@
if err != nil {
return err
}
- return e.setBufferContentLocked(ctx, path, content, edits)
+ return e.setBufferContentLocked(ctx, path, true, content, edits)
}
-func (e *Editor) setBufferContentLocked(ctx context.Context, path string, content []string, fromEdits []Edit) error {
+func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error {
buf, ok := e.buffers[path]
if !ok {
return fmt.Errorf("unknown buffer %q", path)
}
buf.content = content
buf.version++
+ buf.dirty = dirty
e.buffers[path] = buf
// A simple heuristic: if there is only one edit, send it incrementally.
// Otherwise, send the entire content.
@@ -739,7 +766,16 @@
if !match {
return nil, fmt.Errorf("unsupported command %q", params.Command)
}
- return e.Server.ExecuteCommand(ctx, params)
+ result, err := e.Server.ExecuteCommand(ctx, params)
+ if err != nil {
+ return nil, err
+ }
+ // Some commands use the go command, which writes directly to disk.
+ // For convenience, check for those changes.
+ if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
+ return nil, err
+ }
+ return result, nil
}
func convertEdits(protocolEdits []protocol.TextEdit) []Edit {