Skip to content

Commit eeaf4eb

Browse files
findleyrgopherbot
authored andcommitted
internal/lsp/fake: add rename file support for testing
This CL implements the fake.Editor.RenameFile method, which mimic's the behavior of VS Code when renaming files. Specifically: - open buffers affected by the renaming are closed, and then reopened with new URIs - files are moved on disk - didChangeWatchedFile notifications are sent Along the way, add missing comments and fix one place where the editor mutex was held while sending notifications (in Editor.CreateBuffer). Generally, the editor should not hold any mutex while making a remote call. For golang/go#41567 Change-Id: I2abfa846e923de566a21c096502a68f125e7e671 Reviewed-on: https://go-review.googlesource.com/c/tools/+/427903 Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 4754f75 commit eeaf4eb

File tree

5 files changed

+215
-25
lines changed

5 files changed

+215
-25
lines changed

gopls/internal/lsp/fake/client.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,8 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE
121121
return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil
122122
}
123123
for _, change := range params.Edit.DocumentChanges {
124-
// Todo: Add a handler for RenameFile edits
125-
if change.RenameFile != nil {
126-
panic("Fake editor does not support the RenameFile edits.")
127-
}
128-
if change.TextDocumentEdit != nil {
129-
if err := c.editor.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil {
130-
return nil, err
131-
}
124+
if err := c.editor.applyDocumentChange(ctx, change); err != nil {
125+
return nil, err
132126
}
133127
}
134128
return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil

gopls/internal/lsp/fake/editor.go

+109-16
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import (
1616
"strings"
1717
"sync"
1818

19-
"golang.org/x/tools/internal/jsonrpc2"
20-
"golang.org/x/tools/internal/jsonrpc2/servertest"
2119
"golang.org/x/tools/gopls/internal/lsp/command"
2220
"golang.org/x/tools/gopls/internal/lsp/protocol"
21+
"golang.org/x/tools/gopls/internal/lsp/source"
22+
"golang.org/x/tools/internal/jsonrpc2"
23+
"golang.org/x/tools/internal/jsonrpc2/servertest"
2324
"golang.org/x/tools/internal/span"
2425
"golang.org/x/tools/internal/xcontext"
2526
)
@@ -39,7 +40,7 @@ type Editor struct {
3940

4041
mu sync.Mutex // guards config, buffers, serverCapabilities
4142
config EditorConfig // editor configuration
42-
buffers map[string]buffer // open buffers
43+
buffers map[string]buffer // open buffers (relative path -> buffer content)
4344
serverCapabilities protocol.ServerCapabilities // capabilities / options
4445

4546
// Call metrics for the purpose of expectations. This is done in an ad-hoc
@@ -50,16 +51,18 @@ type Editor struct {
5051
calls CallCounts
5152
}
5253

54+
// CallCounts tracks the number of protocol notifications of different types.
5355
type CallCounts struct {
5456
DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64
5557
}
5658

59+
// buffer holds information about an open buffer in the editor.
5760
type buffer struct {
58-
windowsLineEndings bool
59-
version int
60-
path string
61-
lines []string
62-
dirty bool
61+
windowsLineEndings bool // use windows line endings when merging lines
62+
version int // monotonic version; incremented on edits
63+
path string // relative path in the workspace
64+
lines []string // line content
65+
dirty bool // if true, content is unsaved (TODO(rfindley): rename this field)
6366
}
6467

6568
func (b buffer) text() string {
@@ -374,7 +377,6 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
374377

375378
func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
376379
e.mu.Lock()
377-
defer e.mu.Unlock()
378380

379381
buf := buffer{
380382
windowsLineEndings: e.config.WindowsLineEndings,
@@ -385,13 +387,25 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont
385387
}
386388
e.buffers[path] = buf
387389

388-
item := protocol.TextDocumentItem{
390+
item := e.textDocumentItem(buf)
391+
e.mu.Unlock()
392+
393+
return e.sendDidOpen(ctx, item)
394+
}
395+
396+
// textDocumentItem builds a protocol.TextDocumentItem for the given buffer.
397+
//
398+
// Precondition: e.mu must be held.
399+
func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem {
400+
return protocol.TextDocumentItem{
389401
URI: e.sandbox.Workdir.URI(buf.path),
390402
LanguageID: languageID(buf.path, e.config.FileAssociations),
391403
Version: int32(buf.version),
392404
Text: buf.text(),
393405
}
406+
}
394407

408+
func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error {
395409
if e.Server != nil {
396410
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
397411
TextDocument: item,
@@ -451,9 +465,13 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
451465
delete(e.buffers, path)
452466
e.mu.Unlock()
453467

468+
return e.sendDidClose(ctx, e.TextDocumentIdentifier(path))
469+
}
470+
471+
func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error {
454472
if e.Server != nil {
455473
if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
456-
TextDocument: e.TextDocumentIdentifier(path),
474+
TextDocument: doc,
457475
}); err != nil {
458476
return fmt.Errorf("DidClose: %w", err)
459477
}
@@ -1157,16 +1175,91 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin
11571175
return err
11581176
}
11591177
for _, change := range wsEdits.DocumentChanges {
1160-
if change.TextDocumentEdit != nil {
1161-
if err := e.applyProtocolEdit(ctx, *change.TextDocumentEdit); err != nil {
1162-
return err
1163-
}
1178+
if err := e.applyDocumentChange(ctx, change); err != nil {
1179+
return err
11641180
}
11651181
}
11661182
return nil
11671183
}
11681184

1169-
func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocumentEdit) error {
1185+
func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error {
1186+
closed, opened, err := e.renameBuffers(ctx, oldPath, newPath)
1187+
if err != nil {
1188+
return err
1189+
}
1190+
1191+
for _, c := range closed {
1192+
if err := e.sendDidClose(ctx, c); err != nil {
1193+
return err
1194+
}
1195+
}
1196+
for _, o := range opened {
1197+
if err := e.sendDidOpen(ctx, o); err != nil {
1198+
return err
1199+
}
1200+
}
1201+
1202+
// Finally, perform the renaming on disk.
1203+
return e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath)
1204+
}
1205+
1206+
// renameBuffers renames in-memory buffers affected by the renaming of
1207+
// oldPath->newPath, returning the resulting text documents that must be closed
1208+
// and opened over the LSP.
1209+
func (e *Editor) renameBuffers(ctx context.Context, oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) {
1210+
e.mu.Lock()
1211+
defer e.mu.Unlock()
1212+
1213+
// In case either oldPath or newPath is absolute, convert to absolute paths
1214+
// before checking for containment.
1215+
oldAbs := e.sandbox.Workdir.AbsPath(oldPath)
1216+
newAbs := e.sandbox.Workdir.AbsPath(newPath)
1217+
1218+
// Collect buffers that are affected by the given file or directory renaming.
1219+
buffersToRename := make(map[string]string) // old path -> new path
1220+
1221+
for path := range e.buffers {
1222+
abs := e.sandbox.Workdir.AbsPath(path)
1223+
if oldAbs == abs || source.InDirLex(oldAbs, abs) {
1224+
rel, err := filepath.Rel(oldAbs, abs)
1225+
if err != nil {
1226+
return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err)
1227+
}
1228+
nabs := filepath.Join(newAbs, rel)
1229+
newPath := e.sandbox.Workdir.RelPath(nabs)
1230+
buffersToRename[path] = newPath
1231+
}
1232+
}
1233+
1234+
// Update buffers, and build protocol changes.
1235+
for old, new := range buffersToRename {
1236+
buf := e.buffers[old]
1237+
delete(e.buffers, old)
1238+
buf.version = 1
1239+
buf.path = new
1240+
e.buffers[new] = buf
1241+
1242+
closed = append(closed, e.TextDocumentIdentifier(old))
1243+
opened = append(opened, e.textDocumentItem(buf))
1244+
}
1245+
1246+
return closed, opened, nil
1247+
}
1248+
1249+
func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error {
1250+
if change.RenameFile != nil {
1251+
oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI)
1252+
newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI)
1253+
1254+
return e.RenameFile(ctx, oldPath, newPath)
1255+
}
1256+
if change.TextDocumentEdit != nil {
1257+
return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit)
1258+
}
1259+
panic("Internal error: one of RenameFile or TextDocumentEdit must be set")
1260+
}
1261+
1262+
func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error {
11701263
path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
11711264
if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version {
11721265
return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version)

gopls/internal/lsp/fake/workdir.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,39 @@ func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEven
315315
if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil {
316316
return FileEvent{}, err
317317
}
318+
return w.fileEvent(path, changeType), nil
319+
}
320+
321+
func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) FileEvent {
318322
return FileEvent{
319323
Path: path,
320324
ProtocolEvent: protocol.FileEvent{
321325
URI: w.URI(path),
322326
Type: changeType,
323327
},
324-
}, nil
328+
}
329+
}
330+
331+
// RenameFile performs an on disk-renaming of the workdir-relative oldPath to
332+
// workdir-relative newPath.
333+
func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error {
334+
oldAbs := w.AbsPath(oldPath)
335+
newAbs := w.AbsPath(newPath)
336+
337+
if err := os.Rename(oldAbs, newAbs); err != nil {
338+
return err
339+
}
340+
341+
// Send synthetic file events for the renaming. Renamed files are handled as
342+
// Delete+Create events:
343+
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileChangeType
344+
events := []FileEvent{
345+
w.fileEvent(oldPath, protocol.Deleted),
346+
w.fileEvent(newPath, protocol.Created),
347+
}
348+
w.sendEvents(ctx, events)
349+
350+
return nil
325351
}
326352

327353
// listFiles lists files in the given directory, returning a map of relative

gopls/internal/lsp/regtest/wrappers.go

+9
Original file line numberDiff line numberDiff line change
@@ -400,13 +400,22 @@ func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
400400
return locations
401401
}
402402

403+
// Rename wraps Editor.Rename, calling t.Fatal on any error.
403404
func (e *Env) Rename(path string, pos fake.Pos, newName string) {
404405
e.T.Helper()
405406
if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil {
406407
e.T.Fatal(err)
407408
}
408409
}
409410

411+
// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error.
412+
func (e *Env) RenameFile(oldPath, newPath string) {
413+
e.T.Helper()
414+
if err := e.Editor.RenameFile(e.Ctx, oldPath, newPath); err != nil {
415+
e.T.Fatal(err)
416+
}
417+
}
418+
410419
// Completion executes a completion request on the server.
411420
func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList {
412421
e.T.Helper()

gopls/internal/regtest/misc/rename_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,71 @@ func main() {
132132
}
133133
})
134134
}
135+
136+
// This is a test that rename operation initiated by the editor function as expected.
137+
func TestRenameFileFromEditor(t *testing.T) {
138+
const files = `
139+
-- go.mod --
140+
module mod.com
141+
142+
go 1.16
143+
-- a/a.go --
144+
package a
145+
146+
const X = 1
147+
-- a/x.go --
148+
package a
149+
150+
const X = 2
151+
-- b/b.go --
152+
package b
153+
`
154+
155+
Run(t, files, func(t *testing.T, env *Env) {
156+
// Rename files and verify that diagnostics are affected accordingly.
157+
158+
// Initially, we should have diagnostics on both X's, for their duplicate declaration.
159+
env.Await(
160+
OnceMet(
161+
InitialWorkspaceLoad,
162+
env.DiagnosticAtRegexp("a/a.go", "X"),
163+
env.DiagnosticAtRegexp("a/x.go", "X"),
164+
),
165+
)
166+
167+
// Moving x.go should make the diagnostic go away.
168+
env.RenameFile("a/x.go", "b/x.go")
169+
env.Await(
170+
OnceMet(
171+
env.DoneWithChangeWatchedFiles(),
172+
EmptyDiagnostics("a/a.go"), // no more duplicate declarations
173+
env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch
174+
),
175+
)
176+
177+
// Renaming should also work on open buffers.
178+
env.OpenFile("b/x.go")
179+
180+
// Moving x.go back to a/ should cause the diagnostics to reappear.
181+
env.RenameFile("b/x.go", "a/x.go")
182+
// TODO(rfindley): enable using a OnceMet precondition here. We can't
183+
// currently do this because DidClose, DidOpen and DidChangeWatchedFiles
184+
// are sent, and it is not easy to use all as a precondition.
185+
env.Await(
186+
env.DiagnosticAtRegexp("a/a.go", "X"),
187+
env.DiagnosticAtRegexp("a/x.go", "X"),
188+
)
189+
190+
// Renaming the entire directory should move both the open and closed file.
191+
env.RenameFile("a", "x")
192+
env.Await(
193+
env.DiagnosticAtRegexp("x/a.go", "X"),
194+
env.DiagnosticAtRegexp("x/x.go", "X"),
195+
)
196+
197+
// As a sanity check, verify that x/x.go is open.
198+
if text := env.Editor.BufferText("x/x.go"); text == "" {
199+
t.Fatal("got empty buffer for x/x.go")
200+
}
201+
})
202+
}

0 commit comments

Comments
 (0)