diff options
Diffstat (limited to 'internal/lsp/fake')
-rw-r--r-- | internal/lsp/fake/client.go | 128 | ||||
-rw-r--r-- | internal/lsp/fake/doc.go | 19 | ||||
-rw-r--r-- | internal/lsp/fake/edit.go | 157 | ||||
-rw-r--r-- | internal/lsp/fake/edit_test.go | 97 | ||||
-rw-r--r-- | internal/lsp/fake/editor.go | 1258 | ||||
-rw-r--r-- | internal/lsp/fake/editor_test.go | 82 | ||||
-rw-r--r-- | internal/lsp/fake/proxy.go | 35 | ||||
-rw-r--r-- | internal/lsp/fake/sandbox.go | 273 | ||||
-rw-r--r-- | internal/lsp/fake/workdir.go | 365 | ||||
-rw-r--r-- | internal/lsp/fake/workdir_test.go | 192 | ||||
-rw-r--r-- | internal/lsp/fake/workdir_windows.go | 20 |
11 files changed, 0 insertions, 2626 deletions
diff --git a/internal/lsp/fake/client.go b/internal/lsp/fake/client.go deleted file mode 100644 index fdc67a6cc..000000000 --- a/internal/lsp/fake/client.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "context" - "fmt" - - "golang.org/x/tools/internal/lsp/protocol" -) - -// ClientHooks are called to handle the corresponding client LSP method. -type ClientHooks struct { - OnLogMessage func(context.Context, *protocol.LogMessageParams) error - OnDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error - OnWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error - OnProgress func(context.Context, *protocol.ProgressParams) error - OnShowMessage func(context.Context, *protocol.ShowMessageParams) error - OnShowMessageRequest func(context.Context, *protocol.ShowMessageRequestParams) error - OnRegistration func(context.Context, *protocol.RegistrationParams) error - OnUnregistration func(context.Context, *protocol.UnregistrationParams) error -} - -// Client is an adapter that converts an *Editor into an LSP Client. It mosly -// delegates functionality to hooks that can be configured by tests. -type Client struct { - editor *Editor - hooks ClientHooks -} - -func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { - if c.hooks.OnShowMessage != nil { - return c.hooks.OnShowMessage(ctx, params) - } - return nil -} - -func (c *Client) ShowMessageRequest(ctx context.Context, params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { - if c.hooks.OnShowMessageRequest != nil { - if err := c.hooks.OnShowMessageRequest(ctx, params); err != nil { - return nil, err - } - } - if len(params.Actions) == 0 || len(params.Actions) > 1 { - return nil, fmt.Errorf("fake editor cannot handle multiple action items") - } - return ¶ms.Actions[0], nil -} - -func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { - if c.hooks.OnLogMessage != nil { - return c.hooks.OnLogMessage(ctx, params) - } - return nil -} - -func (c *Client) Event(ctx context.Context, event *interface{}) error { - return nil -} - -func (c *Client) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error { - if c.hooks.OnDiagnostics != nil { - return c.hooks.OnDiagnostics(ctx, params) - } - return nil -} - -func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { - return []protocol.WorkspaceFolder{}, nil -} - -func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { - results := make([]interface{}, len(p.Items)) - for i, item := range p.Items { - if item.Section != "gopls" { - continue - } - results[i] = c.editor.configuration() - } - return results, nil -} - -func (c *Client) RegisterCapability(ctx context.Context, params *protocol.RegistrationParams) error { - if c.hooks.OnRegistration != nil { - return c.hooks.OnRegistration(ctx, params) - } - return nil -} - -func (c *Client) UnregisterCapability(ctx context.Context, params *protocol.UnregistrationParams) error { - if c.hooks.OnUnregistration != nil { - return c.hooks.OnUnregistration(ctx, params) - } - return nil -} - -func (c *Client) Progress(ctx context.Context, params *protocol.ProgressParams) error { - if c.hooks.OnProgress != nil { - return c.hooks.OnProgress(ctx, params) - } - return nil -} - -func (c *Client) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { - if c.hooks.OnWorkDoneProgressCreate != nil { - return c.hooks.OnWorkDoneProgressCreate(ctx, params) - } - return nil -} - -func (c *Client) ShowDocument(context.Context, *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { - return nil, nil -} - -// ApplyEdit applies edits sent from the server. -func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { - if len(params.Edit.Changes) != 0 { - return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil - } - for _, change := range params.Edit.DocumentChanges { - if err := c.editor.applyProtocolEdit(ctx, change); err != nil { - return nil, err - } - } - return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil -} diff --git a/internal/lsp/fake/doc.go b/internal/lsp/fake/doc.go deleted file mode 100644 index 6051781de..000000000 --- a/internal/lsp/fake/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package fake provides fake implementations of a text editor, LSP client -// plugin, and Sandbox environment for use in tests. -// -// The Editor type provides a high level API for text editor operations -// (open/modify/save/close a buffer, jump to definition, etc.), and the Client -// type exposes an LSP client for the editor that can be connected to a -// language server. By default, the Editor and Client should be compliant with -// the LSP spec: their intended use is to verify server compliance with the -// spec in a variety of environment. Possible future enhancements of these -// types may allow them to misbehave in configurable ways, but that is not -// their primary use. -// -// The Sandbox type provides a facility for executing tests with a temporary -// directory, module proxy, and GOPATH. -package fake diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go deleted file mode 100644 index 8b04c390f..000000000 --- a/internal/lsp/fake/edit.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "fmt" - "sort" - "strings" - - "golang.org/x/tools/internal/lsp/protocol" -) - -// Pos represents a position in a text buffer. Both Line and Column are -// 0-indexed. -type Pos struct { - Line, Column int -} - -func (p Pos) String() string { - return fmt.Sprintf("%v:%v", p.Line, p.Column) -} - -// Range corresponds to protocol.Range, but uses the editor friend Pos -// instead of UTF-16 oriented protocol.Position -type Range struct { - Start Pos - End Pos -} - -func (p Pos) ToProtocolPosition() protocol.Position { - return protocol.Position{ - Line: uint32(p.Line), - Character: uint32(p.Column), - } -} - -func fromProtocolPosition(pos protocol.Position) Pos { - return Pos{ - Line: int(pos.Line), - Column: int(pos.Character), - } -} - -// Edit represents a single (contiguous) buffer edit. -type Edit struct { - Start, End Pos - Text string -} - -// Location is the editor friendly equivalent of protocol.Location -type Location struct { - Path string - Range Range -} - -// SymbolInformation is an editor friendly version of -// protocol.SymbolInformation, with location information transformed to byte -// offsets. Field names correspond to the protocol type. -type SymbolInformation struct { - Name string - Kind protocol.SymbolKind - Location Location -} - -// NewEdit creates an edit replacing all content between -// (startLine, startColumn) and (endLine, endColumn) with text. -func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit { - return Edit{ - Start: Pos{Line: startLine, Column: startColumn}, - End: Pos{Line: endLine, Column: endColumn}, - Text: text, - } -} - -func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent { - return protocol.TextDocumentContentChangeEvent{ - Range: &protocol.Range{ - Start: e.Start.ToProtocolPosition(), - End: e.End.ToProtocolPosition(), - }, - Text: e.Text, - } -} - -func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit { - return Edit{ - Start: fromProtocolPosition(textEdit.Range.Start), - End: fromProtocolPosition(textEdit.Range.End), - Text: textEdit.NewText, - } -} - -// inText reports whether p is a valid position in the text buffer. -func inText(p Pos, content []string) bool { - if p.Line < 0 || p.Line >= len(content) { - return false - } - // Note the strict right bound: the column indexes character _separators_, - // not characters. - if p.Column < 0 || p.Column > len([]rune(content[p.Line])) { - return false - } - return true -} - -// editContent implements a simplistic, inefficient algorithm for applying text -// edits to our buffer representation. It returns an error if the edit is -// invalid for the current content. -func editContent(content []string, edits []Edit) ([]string, error) { - newEdits := make([]Edit, len(edits)) - copy(newEdits, edits) - sort.Slice(newEdits, func(i, j int) bool { - if newEdits[i].Start.Line < newEdits[j].Start.Line { - return true - } - if newEdits[i].Start.Line > newEdits[j].Start.Line { - return false - } - return newEdits[i].Start.Column < newEdits[j].Start.Column - }) - - // Validate edits. - for _, edit := range newEdits { - if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) { - return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start) - } - if !inText(edit.Start, content) { - return nil, fmt.Errorf("start position %v is out of bounds", edit.Start) - } - if !inText(edit.End, content) { - return nil, fmt.Errorf("end position %v is out of bounds", edit.End) - } - } - - var ( - b strings.Builder - line, column int - ) - advance := func(toLine, toColumn int) { - for ; line < toLine; line++ { - b.WriteString(string([]rune(content[line])[column:]) + "\n") - column = 0 - } - b.WriteString(string([]rune(content[line])[column:toColumn])) - column = toColumn - } - for _, edit := range newEdits { - advance(edit.Start.Line, edit.Start.Column) - b.WriteString(edit.Text) - line = edit.End.Line - column = edit.End.Column - } - advance(len(content)-1, len([]rune(content[len(content)-1]))) - return strings.Split(b.String(), "\n"), nil -} diff --git a/internal/lsp/fake/edit_test.go b/internal/lsp/fake/edit_test.go deleted file mode 100644 index 4fa23bdb7..000000000 --- a/internal/lsp/fake/edit_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "strings" - "testing" -) - -func TestApplyEdit(t *testing.T) { - tests := []struct { - label string - content string - edits []Edit - want string - wantErr bool - }{ - { - label: "empty content", - }, - { - label: "empty edit", - content: "hello", - edits: []Edit{}, - want: "hello", - }, - { - label: "unicode edit", - content: "hello, 日本語", - edits: []Edit{{ - Start: Pos{Line: 0, Column: 7}, - End: Pos{Line: 0, Column: 10}, - Text: "world", - }}, - want: "hello, world", - }, - { - label: "range edit", - content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 1}, - End: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, - want: "ABC\nD12\n345\nJKL", - }, - { - label: "end before start", - content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - End: Pos{Line: 1, Column: 1}, - Start: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, - wantErr: true, - }, - { - label: "out of bounds line", - content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 1}, - End: Pos{Line: 4, Column: 3}, - Text: "12\n345", - }}, - wantErr: true, - }, - { - label: "out of bounds column", - content: "ABC\nDEF\nGHI\nJKL", - edits: []Edit{{ - Start: Pos{Line: 1, Column: 4}, - End: Pos{Line: 2, Column: 3}, - Text: "12\n345", - }}, - wantErr: true, - }, - } - - for _, test := range tests { - test := test - t.Run(test.label, func(t *testing.T) { - lines := strings.Split(test.content, "\n") - newLines, err := editContent(lines, test.edits) - if (err != nil) != test.wantErr { - t.Errorf("got err %v, want error: %t", err, test.wantErr) - } - if err != nil { - return - } - if got := strings.Join(newLines, "\n"); got != test.want { - t.Errorf("got %q, want %q", got, test.want) - } - }) - } -} diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go deleted file mode 100644 index 5bce5609f..000000000 --- a/internal/lsp/fake/editor.go +++ /dev/null @@ -1,1258 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "bufio" - "context" - "fmt" - "os" - "path" - "path/filepath" - "regexp" - "strings" - "sync" - - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" - errors "golang.org/x/xerrors" -) - -// Editor is a fake editor client. It keeps track of client state and can be -// used for writing LSP tests. -type Editor struct { - Config EditorConfig - - // Server, client, and sandbox are concurrency safe and written only - // at construction time, so do not require synchronization. - Server protocol.Server - serverConn jsonrpc2.Conn - client *Client - sandbox *Sandbox - defaultEnv map[string]string - - // Since this editor is intended just for testing, we use very coarse - // locking. - mu sync.Mutex - // Editor state. - buffers map[string]buffer - // Capabilities / Options - serverCapabilities protocol.ServerCapabilities - - // Call metrics for the purpose of expectations. This is done in an ad-hoc - // manner for now. Perhaps in the future we should do something more - // systematic. Guarded with a separate mutex as calls may need to be accessed - // asynchronously via callbacks into the Editor. - callsMu sync.Mutex - calls CallCounts -} - -type CallCounts struct { - DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64 -} - -type buffer struct { - windowsLineEndings bool - version int - path string - lines []string - dirty bool -} - -func (b buffer) text() string { - eol := "\n" - if b.windowsLineEndings { - eol = "\r\n" - } - return strings.Join(b.lines, eol) -} - -// EditorConfig configures the editor's LSP session. This is similar to -// source.UserOptions, but we use a separate type here so that we expose only -// that configuration which we support. -// -// The zero value for EditorConfig should correspond to its defaults. -type EditorConfig struct { - Env map[string]string - BuildFlags []string - - // CodeLenses is a map defining whether codelens are enabled, keyed by the - // codeLens command. CodeLenses which are not present in this map are left in - // their default state. - CodeLenses map[string]bool - - // SymbolMatcher is the config associated with the "symbolMatcher" gopls - // config option. - SymbolMatcher, SymbolStyle *string - - // LimitWorkspaceScope is true if the user does not want to expand their - // workspace scope to the entire module. - LimitWorkspaceScope bool - - // WorkspaceFolders is the workspace folders to configure on the LSP server, - // relative to the sandbox workdir. - // - // As a special case, if WorkspaceFolders is nil the editor defaults to - // configuring a single workspace folder corresponding to the workdir root. - // To explicitly send no workspace folders, use an empty (non-nil) slice. - WorkspaceFolders []string - - // EnableStaticcheck enables staticcheck analyzers. - EnableStaticcheck bool - - // AllExperiments sets the "allExperiments" configuration, which enables - // all of gopls's opt-in settings. - AllExperiments bool - - // Whether to send the current process ID, for testing data that is joined to - // the PID. This can only be set by one test. - SendPID bool - - // Whether to edit files with windows line endings. - WindowsLineEndings bool - - // Map of language ID -> regexp to match, used to set the file type of new - // buffers. Applied as an overlay on top of the following defaults: - // "go" -> ".*\.go" - // "go.mod" -> "go\.mod" - // "go.sum" -> "go\.sum" - // "gotmpl" -> ".*tmpl" - FileAssociations map[string]string - - // Settings holds arbitrary additional settings to apply to the gopls config. - // TODO(rfindley): replace existing EditorConfig fields with Settings. - Settings map[string]interface{} - - ImportShortcut string - DirectoryFilters []string - VerboseOutput bool - ExperimentalUseInvalidMetadata bool -} - -// NewEditor Creates a new Editor. -func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { - return &Editor{ - buffers: make(map[string]buffer), - sandbox: sandbox, - defaultEnv: sandbox.GoEnv(), - Config: config, - } -} - -// Connect configures the editor to communicate with an LSP server on conn. It -// is not concurrency safe, and should be called at most once, before using the -// editor. -// -// It returns the editor, so that it may be called as follows: -// editor, err := NewEditor(s).Connect(ctx, conn) -func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) { - e.serverConn = conn - e.Server = protocol.ServerDispatcher(conn) - e.client = &Client{editor: e, hooks: hooks} - conn.Go(ctx, - protocol.Handlers( - protocol.ClientHandler(e.client, - jsonrpc2.MethodNotFound))) - if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil { - return nil, err - } - e.sandbox.Workdir.AddWatcher(e.onFileChanges) - return e, nil -} - -func (e *Editor) Stats() CallCounts { - e.callsMu.Lock() - defer e.callsMu.Unlock() - return e.calls -} - -// Shutdown issues the 'shutdown' LSP notification. -func (e *Editor) Shutdown(ctx context.Context) error { - if e.Server != nil { - if err := e.Server.Shutdown(ctx); err != nil { - return errors.Errorf("Shutdown: %w", err) - } - } - return nil -} - -// Exit issues the 'exit' LSP notification. -func (e *Editor) Exit(ctx context.Context) error { - if e.Server != nil { - // Not all LSP clients issue the exit RPC, but we do so here to ensure that - // we gracefully handle it on multi-session servers. - if err := e.Server.Exit(ctx); err != nil { - return errors.Errorf("Exit: %w", err) - } - } - return nil -} - -// Close issues the shutdown and exit sequence an editor should. -func (e *Editor) Close(ctx context.Context) error { - if err := e.Shutdown(ctx); err != nil { - return err - } - if err := e.Exit(ctx); err != nil { - return err - } - // called close on the editor should result in the connection closing - select { - case <-e.serverConn.Done(): - // connection closed itself - return nil - case <-ctx.Done(): - return errors.Errorf("connection not closed: %w", ctx.Err()) - } -} - -// Client returns the LSP client for this editor. -func (e *Editor) Client() *Client { - return e.client -} - -func (e *Editor) overlayEnv() map[string]string { - env := make(map[string]string) - for k, v := range e.defaultEnv { - v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) - env[k] = v - } - for k, v := range e.Config.Env { - v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) - env[k] = v - } - return env -} - -func (e *Editor) configuration() map[string]interface{} { - config := map[string]interface{}{ - "verboseWorkDoneProgress": true, - "env": e.overlayEnv(), - "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope, - "completionBudget": "10s", - } - - for k, v := range e.Config.Settings { - config[k] = v - } - - if e.Config.BuildFlags != nil { - config["buildFlags"] = e.Config.BuildFlags - } - if e.Config.DirectoryFilters != nil { - config["directoryFilters"] = e.Config.DirectoryFilters - } - if e.Config.ExperimentalUseInvalidMetadata { - config["experimentalUseInvalidMetadata"] = true - } - if e.Config.CodeLenses != nil { - config["codelenses"] = e.Config.CodeLenses - } - if e.Config.SymbolMatcher != nil { - config["symbolMatcher"] = *e.Config.SymbolMatcher - } - if e.Config.SymbolStyle != nil { - config["symbolStyle"] = *e.Config.SymbolStyle - } - if e.Config.EnableStaticcheck { - config["staticcheck"] = true - } - if e.Config.AllExperiments { - config["allExperiments"] = true - } - - if e.Config.VerboseOutput { - config["verboseOutput"] = true - } - - if e.Config.ImportShortcut != "" { - config["importShortcut"] = e.Config.ImportShortcut - } - - config["diagnosticsDelay"] = "10ms" - - // ExperimentalWorkspaceModule is only set as a mode, not a configuration. - return config -} - -func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error { - params := &protocol.ParamInitialize{} - params.ClientInfo.Name = "fakeclient" - params.ClientInfo.Version = "v1.0.0" - - if workspaceFolders == nil { - workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)} - } - for _, folder := range workspaceFolders { - params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{ - URI: string(e.sandbox.Workdir.URI(folder)), - Name: filepath.Base(folder), - }) - } - - params.Capabilities.Workspace.Configuration = true - params.Capabilities.Window.WorkDoneProgress = true - // TODO: set client capabilities - params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} - params.InitializationOptions = e.configuration() - if e.Config.SendPID { - params.ProcessID = int32(os.Getpid()) - } - - params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true - params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true - // copied from lsp/semantic.go to avoid import cycle in tests - params.Capabilities.TextDocument.SemanticTokens.TokenTypes = []string{ - "namespace", "type", "class", "enum", "interface", - "struct", "typeParameter", "parameter", "variable", "property", "enumMember", - "event", "function", "method", "macro", "keyword", "modifier", "comment", - "string", "number", "regexp", "operator", - } - - // This is a bit of a hack, since the fake editor doesn't actually support - // watching changed files that match a specific glob pattern. However, the - // editor does send didChangeWatchedFiles notifications, so set this to - // true. - params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true - - params.Trace = "messages" - // TODO: support workspace folders. - if e.Server != nil { - resp, err := e.Server.Initialize(ctx, params) - if err != nil { - return errors.Errorf("initialize: %w", err) - } - e.mu.Lock() - e.serverCapabilities = resp.Capabilities - e.mu.Unlock() - - if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { - return errors.Errorf("initialized: %w", err) - } - } - // TODO: await initial configuration here, or expect gopls to manage that? - return nil -} - -// onFileChanges is registered to be called by the Workdir on any writes that -// go through the Workdir API. It is called synchronously by the Workdir. -func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { - if e.Server == nil { - return - } - - // e may be locked when onFileChanges is called, but it is important that we - // synchronously increment this counter so that we can subsequently assert on - // the number of expected DidChangeWatchedFiles calls. - e.callsMu.Lock() - e.calls.DidChangeWatchedFiles++ - e.callsMu.Unlock() - - // Since e may be locked, we must run this mutation asynchronously. - go func() { - e.mu.Lock() - defer e.mu.Unlock() - 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. - } - // No need to update if the buffer content hasn't changed. - if content == buf.text() { - continue - } - // During shutdown, this call will fail. Ignore the error. - _ = e.setBufferContentLocked(ctx, evt.Path, false, lines(content), nil) - } - } - e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ - Changes: lspevts, - }) - }() -} - -// OpenFile creates a buffer for the given workdir-relative file. -func (e *Editor) OpenFile(ctx context.Context, path string) error { - content, err := e.sandbox.Workdir.ReadFile(path) - if err != nil { - return err - } - return e.createBuffer(ctx, path, false, content) -} - -// 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 { - return e.createBuffer(ctx, path, true, content) -} - -func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { - buf := buffer{ - windowsLineEndings: e.Config.WindowsLineEndings, - version: 1, - path: path, - lines: lines(content), - dirty: dirty, - } - e.mu.Lock() - defer e.mu.Unlock() - e.buffers[path] = buf - - item := protocol.TextDocumentItem{ - URI: e.sandbox.Workdir.URI(buf.path), - LanguageID: e.languageID(buf.path), - Version: int32(buf.version), - Text: buf.text(), - } - - if e.Server != nil { - if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ - TextDocument: item, - }); err != nil { - return errors.Errorf("DidOpen: %w", err) - } - e.callsMu.Lock() - e.calls.DidOpen++ - e.callsMu.Unlock() - } - return nil -} - -var defaultFileAssociations = map[string]*regexp.Regexp{ - "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! - "go.mod": regexp.MustCompile(`^go\.mod$`), - "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), - "go.work": regexp.MustCompile(`^go\.work$`), - "gotmpl": regexp.MustCompile(`^.*tmpl$`), -} - -func (e *Editor) languageID(p string) string { - base := path.Base(p) - for lang, re := range e.Config.FileAssociations { - re := regexp.MustCompile(re) - if re.MatchString(base) { - return lang - } - } - for lang, re := range defaultFileAssociations { - if re.MatchString(base) { - return lang - } - } - return "" -} - -// lines returns line-ending agnostic line representation of content. -func lines(content string) []string { - lines := strings.Split(content, "\n") - for i, l := range lines { - lines[i] = strings.TrimSuffix(l, "\r") - } - return lines -} - -// CloseBuffer removes the current buffer (regardless of whether it is saved). -func (e *Editor) CloseBuffer(ctx context.Context, path string) error { - e.mu.Lock() - _, ok := e.buffers[path] - if !ok { - e.mu.Unlock() - return ErrUnknownBuffer - } - delete(e.buffers, path) - e.mu.Unlock() - - if e.Server != nil { - if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ - TextDocument: e.textDocumentIdentifier(path), - }); err != nil { - return errors.Errorf("DidClose: %w", err) - } - e.callsMu.Lock() - e.calls.DidClose++ - e.callsMu.Unlock() - } - return nil -} - -func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier { - return protocol.TextDocumentIdentifier{ - URI: e.sandbox.Workdir.URI(path), - } -} - -// SaveBuffer writes the content of the buffer specified by the given path to -// the filesystem. -func (e *Editor) SaveBuffer(ctx context.Context, path string) error { - if err := e.OrganizeImports(ctx, path); err != nil { - return errors.Errorf("organizing imports before save: %w", err) - } - if err := e.FormatBuffer(ctx, path); err != nil { - return errors.Errorf("formatting before save: %w", err) - } - return e.SaveBufferWithoutActions(ctx, path) -} - -func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error { - e.mu.Lock() - defer e.mu.Unlock() - buf, ok := e.buffers[path] - if !ok { - return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path)) - } - content := buf.text() - includeText := false - syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions) - if ok { - includeText = syncOptions.Save.IncludeText - } - - docID := e.textDocumentIdentifier(buf.path) - if e.Server != nil { - if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ - TextDocument: docID, - Reason: protocol.Manual, - }); err != nil { - return errors.Errorf("WillSave: %w", err) - } - } - if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil { - return errors.Errorf("writing %q: %w", path, err) - } - - buf.dirty = false - e.buffers[path] = buf - - if e.Server != nil { - params := &protocol.DidSaveTextDocumentParams{ - TextDocument: docID, - } - if includeText { - params.Text = &content - } - if err := e.Server.DidSave(ctx, params); err != nil { - return errors.Errorf("DidSave: %w", err) - } - e.callsMu.Lock() - e.calls.DidSave++ - e.callsMu.Unlock() - } - return nil -} - -// contentPosition returns the (Line, Column) position corresponding to offset -// in the buffer referenced by path. -func contentPosition(content string, offset int) (Pos, error) { - scanner := bufio.NewScanner(strings.NewReader(content)) - start := 0 - line := 0 - for scanner.Scan() { - end := start + len([]rune(scanner.Text())) + 1 - if offset < end { - return Pos{Line: line, Column: offset - start}, nil - } - start = end - line++ - } - if err := scanner.Err(); err != nil { - return Pos{}, errors.Errorf("scanning content: %w", err) - } - // Scan() will drop the last line if it is empty. Correct for this. - if (strings.HasSuffix(content, "\n") || content == "") && offset == start { - return Pos{Line: line, Column: 0}, nil - } - return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start) -} - -// ErrNoMatch is returned if a regexp search fails. -var ( - ErrNoMatch = errors.New("no match") - ErrUnknownBuffer = errors.New("unknown buffer") -) - -// regexpRange returns the start and end of the first occurrence of either re -// or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. -func regexpRange(content, re string) (Pos, Pos, error) { - content = normalizeEOL(content) - var start, end int - rec, err := regexp.Compile(re) - if err != nil { - return Pos{}, Pos{}, err - } - indexes := rec.FindStringSubmatchIndex(content) - if indexes == nil { - return Pos{}, Pos{}, ErrNoMatch - } - switch len(indexes) { - case 2: - // no subgroups: return the range of the regexp expression - start, end = indexes[0], indexes[1] - case 4: - // one subgroup: return its range - start, end = indexes[2], indexes[3] - default: - return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) - } - startPos, err := contentPosition(content, start) - if err != nil { - return Pos{}, Pos{}, err - } - endPos, err := contentPosition(content, end) - if err != nil { - return Pos{}, Pos{}, err - } - return startPos, endPos, nil -} - -func normalizeEOL(content string) string { - return strings.Join(lines(content), "\n") -} - -// RegexpRange returns the first range in the buffer bufName matching re. See -// RegexpSearch for more information on matching. -func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) { - e.mu.Lock() - defer e.mu.Unlock() - buf, ok := e.buffers[bufName] - if !ok { - return Pos{}, Pos{}, ErrUnknownBuffer - } - return regexpRange(buf.text(), re) -} - -// RegexpSearch returns the position of the first match for re in the buffer -// bufName. For convenience, RegexpSearch supports the following two modes: -// 1. If re has no subgroups, return the position of the match for re itself. -// 2. If re has one subgroup, return the position of the first subgroup. -// It returns an error re is invalid, has more than one subgroup, or doesn't -// match the buffer. -func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) { - start, _, err := e.RegexpRange(bufName, re) - return start, err -} - -// RegexpReplace edits the buffer corresponding to path by replacing the first -// instance of re, or its first subgroup, with the replace text. See -// RegexpSearch for more explanation of these two modes. -// It returns an error if re is invalid, has more than one subgroup, or doesn't -// match the buffer. -func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error { - e.mu.Lock() - defer e.mu.Unlock() - buf, ok := e.buffers[path] - if !ok { - return ErrUnknownBuffer - } - content := buf.text() - start, end, err := regexpRange(content, re) - if err != nil { - return err - } - return e.editBufferLocked(ctx, path, []Edit{{ - Start: start, - End: end, - Text: replace, - }}) -} - -// EditBuffer applies the given test edits to the buffer identified by path. -func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error { - e.mu.Lock() - defer e.mu.Unlock() - return e.editBufferLocked(ctx, path, edits) -} - -func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { - e.mu.Lock() - defer e.mu.Unlock() - lines := lines(content) - return e.setBufferContentLocked(ctx, path, true, lines, nil) -} - -// HasBuffer reports whether the file name is open in the editor. -func (e *Editor) HasBuffer(name string) bool { - e.mu.Lock() - defer e.mu.Unlock() - _, ok := e.buffers[name] - return ok -} - -// BufferText returns the content of the buffer with the given name. -func (e *Editor) BufferText(name string) string { - e.mu.Lock() - defer e.mu.Unlock() - return e.buffers[name].text() -} - -// BufferVersion returns the current version of the buffer corresponding to -// name (or 0 if it is not being edited). -func (e *Editor) BufferVersion(name string) int { - e.mu.Lock() - defer e.mu.Unlock() - return e.buffers[name].version -} - -func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error { - buf, ok := e.buffers[path] - if !ok { - return fmt.Errorf("unknown buffer %q", path) - } - content := make([]string, len(buf.lines)) - copy(content, buf.lines) - content, err := editContent(content, edits) - if err != nil { - return err - } - return e.setBufferContentLocked(ctx, path, true, content, edits) -} - -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.lines = 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. - var evts []protocol.TextDocumentContentChangeEvent - if len(fromEdits) == 1 { - evts = append(evts, fromEdits[0].toProtocolChangeEvent()) - } else { - evts = append(evts, protocol.TextDocumentContentChangeEvent{ - Text: buf.text(), - }) - } - params := &protocol.DidChangeTextDocumentParams{ - TextDocument: protocol.VersionedTextDocumentIdentifier{ - Version: int32(buf.version), - TextDocumentIdentifier: e.textDocumentIdentifier(buf.path), - }, - ContentChanges: evts, - } - if e.Server != nil { - if err := e.Server.DidChange(ctx, params); err != nil { - return errors.Errorf("DidChange: %w", err) - } - e.callsMu.Lock() - e.calls.DidChange++ - e.callsMu.Unlock() - } - return nil -} - -// GoToDefinition jumps to the definition of the symbol at the given position -// in an open buffer. It returns the path and position of the resulting jump. -func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return "", Pos{}, err - } - params := &protocol.DefinitionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() - - resp, err := e.Server.Definition(ctx, params) - if err != nil { - return "", Pos{}, errors.Errorf("definition: %w", err) - } - return e.extractFirstPathAndPos(ctx, resp) -} - -// GoToTypeDefinition jumps to the type definition of the symbol at the given position -// in an open buffer. -func (e *Editor) GoToTypeDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return "", Pos{}, err - } - params := &protocol.TypeDefinitionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() - - resp, err := e.Server.TypeDefinition(ctx, params) - if err != nil { - return "", Pos{}, errors.Errorf("type definition: %w", err) - } - return e.extractFirstPathAndPos(ctx, resp) -} - -// extractFirstPathAndPos returns the path and the position of the first location. -// It opens the file if needed. -func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Location) (string, Pos, error) { - if len(locs) == 0 { - return "", Pos{}, nil - } - - newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) - newPos := fromProtocolPosition(locs[0].Range.Start) - if !e.HasBuffer(newPath) { - if err := e.OpenFile(ctx, newPath); err != nil { - return "", Pos{}, errors.Errorf("OpenFile: %w", err) - } - } - return newPath, newPos, nil -} - -// Symbol performs a workspace symbol search using query -func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) { - params := &protocol.WorkspaceSymbolParams{} - params.Query = query - - resp, err := e.Server.Symbol(ctx, params) - if err != nil { - return nil, errors.Errorf("symbol: %w", err) - } - var res []SymbolInformation - for _, si := range resp { - ploc := si.Location - path := e.sandbox.Workdir.URIToPath(ploc.URI) - start := fromProtocolPosition(ploc.Range.Start) - end := fromProtocolPosition(ploc.Range.End) - rnge := Range{ - Start: start, - End: end, - } - loc := Location{ - Path: path, - Range: rnge, - } - res = append(res, SymbolInformation{ - Name: si.Name, - Kind: si.Kind, - Location: loc, - }) - } - return res, nil -} - -// OrganizeImports requests and performs the source.organizeImports codeAction. -func (e *Editor) OrganizeImports(ctx context.Context, path string) error { - _, err := e.applyCodeActions(ctx, path, nil, nil, protocol.SourceOrganizeImports) - return err -} - -// RefactorRewrite requests and performs the source.refactorRewrite codeAction. -func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error { - applied, err := e.applyCodeActions(ctx, path, rng, nil, protocol.RefactorRewrite) - if applied == 0 { - return errors.Errorf("no refactorings were applied") - } - return err -} - -// ApplyQuickFixes requests and performs the quickfix codeAction. -func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error { - applied, err := e.applyCodeActions(ctx, path, rng, diagnostics, protocol.SourceFixAll, protocol.QuickFix) - if applied == 0 { - return errors.Errorf("no quick fixes were applied") - } - return err -} - -// ApplyCodeAction applies the given code action. -func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { - for _, change := range action.Edit.DocumentChanges { - path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) - if int32(e.buffers[path].version) != change.TextDocument.Version { - // Skip edits for old versions. - continue - } - edits := convertEdits(change.Edits) - if err := e.EditBuffer(ctx, path, edits); err != nil { - return errors.Errorf("editing buffer %q: %w", path, err) - } - } - // Execute any commands. The specification says that commands are - // executed after edits are applied. - if action.Command != nil { - if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ - Command: action.Command.Command, - Arguments: action.Command.Arguments, - }); err != nil { - return err - } - } - // Some commands may edit files on disk. - return e.sandbox.Workdir.CheckForFileChanges(ctx) -} - -// GetQuickFixes returns the available quick fix code actions. -func (e *Editor) GetQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { - return e.getCodeActions(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll) -} - -func (e *Editor) applyCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { - actions, err := e.getCodeActions(ctx, path, rng, diagnostics, only...) - if err != nil { - return 0, err - } - applied := 0 - for _, action := range actions { - if action.Title == "" { - return 0, errors.Errorf("empty title for code action") - } - var match bool - for _, o := range only { - if action.Kind == o { - match = true - break - } - } - if !match { - continue - } - applied++ - if err := e.ApplyCodeAction(ctx, action); err != nil { - return 0, err - } - } - return applied, nil -} - -func (e *Editor) getCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { - if e.Server == nil { - return nil, nil - } - params := &protocol.CodeActionParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Context.Only = only - if diagnostics != nil { - params.Context.Diagnostics = diagnostics - } - if rng != nil { - params.Range = *rng - } - return e.Server.CodeAction(ctx, params) -} - -func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { - if e.Server == nil { - return nil, nil - } - var match bool - // Ensure that this command was actually listed as a supported command. - for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { - if command == params.Command { - match = true - break - } - } - if !match { - return nil, fmt.Errorf("unsupported command %q", params.Command) - } - 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 { - var edits []Edit - for _, lspEdit := range protocolEdits { - edits = append(edits, fromProtocolTextEdit(lspEdit)) - } - return edits -} - -// FormatBuffer gofmts a Go file. -func (e *Editor) FormatBuffer(ctx context.Context, path string) error { - if e.Server == nil { - return nil - } - e.mu.Lock() - version := e.buffers[path].version - e.mu.Unlock() - params := &protocol.DocumentFormattingParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - resp, err := e.Server.Formatting(ctx, params) - if err != nil { - return errors.Errorf("textDocument/formatting: %w", err) - } - e.mu.Lock() - defer e.mu.Unlock() - if versionAfter := e.buffers[path].version; versionAfter != version { - return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) - } - edits := convertEdits(resp) - if len(edits) == 0 { - return nil - } - return e.editBufferLocked(ctx, path, edits) -} - -func (e *Editor) checkBufferPosition(path string, pos Pos) error { - e.mu.Lock() - defer e.mu.Unlock() - buf, ok := e.buffers[path] - if !ok { - return fmt.Errorf("buffer %q is not open", path) - } - if !inText(pos, buf.lines) { - return fmt.Errorf("position %v is invalid in buffer %q", pos, path) - } - return nil -} - -// RunGenerate runs `go generate` non-recursively in the workdir-relative dir -// path. It does not report any resulting file changes as a watched file -// change, so must be followed by a call to Workdir.CheckForFileChanges once -// the generate command has completed. -// TODO(rFindley): this shouldn't be necessary anymore. Delete it. -func (e *Editor) RunGenerate(ctx context.Context, dir string) error { - if e.Server == nil { - return nil - } - absDir := e.sandbox.Workdir.AbsPath(dir) - cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ - Dir: protocol.URIFromSpanURI(span.URIFromPath(absDir)), - Recursive: false, - }) - if err != nil { - return err - } - params := &protocol.ExecuteCommandParams{ - Command: cmd.Command, - Arguments: cmd.Arguments, - } - if _, err := e.ExecuteCommand(ctx, params); err != nil { - return fmt.Errorf("running generate: %v", err) - } - // Unfortunately we can't simply poll the workdir for file changes here, - // because server-side command may not have completed. In regtests, we can - // Await this state change, but here we must delegate that responsibility to - // the caller. - return nil -} - -// CodeLens executes a codelens request on the server. -func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) { - if e.Server == nil { - return nil, nil - } - e.mu.Lock() - _, ok := e.buffers[path] - e.mu.Unlock() - if !ok { - return nil, fmt.Errorf("buffer %q is not open", path) - } - params := &protocol.CodeLensParams{ - TextDocument: e.textDocumentIdentifier(path), - } - lens, err := e.Server.CodeLens(ctx, params) - if err != nil { - return nil, err - } - return lens, nil -} - -// Completion executes a completion request on the server. -func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) { - if e.Server == nil { - return nil, nil - } - e.mu.Lock() - _, ok := e.buffers[path] - e.mu.Unlock() - if !ok { - return nil, fmt.Errorf("buffer %q is not open", path) - } - params := &protocol.CompletionParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), - }, - } - completions, err := e.Server.Completion(ctx, params) - if err != nil { - return nil, err - } - return completions, nil -} - -// AcceptCompletion accepts a completion for the given item at the given -// position. -func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error { - if e.Server == nil { - return nil - } - e.mu.Lock() - defer e.mu.Unlock() - _, ok := e.buffers[path] - if !ok { - return fmt.Errorf("buffer %q is not open", path) - } - return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{ - *item.TextEdit, - }, item.AdditionalTextEdits...))) -} - -// Symbols executes a workspace/symbols request on the server. -func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInformation, error) { - if e.Server == nil { - return nil, nil - } - params := &protocol.WorkspaceSymbolParams{Query: sym} - ans, err := e.Server.Symbol(ctx, params) - return ans, err -} - -// References executes a reference request on the server. -func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { - if e.Server == nil { - return nil, nil - } - e.mu.Lock() - _, ok := e.buffers[path] - e.mu.Unlock() - if !ok { - return nil, fmt.Errorf("buffer %q is not open", path) - } - params := &protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), - }, - Context: protocol.ReferenceContext{ - IncludeDeclaration: true, - }, - } - locations, err := e.Server.References(ctx, params) - if err != nil { - return nil, err - } - return locations, nil -} - -func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error { - if e.Server == nil { - return nil - } - params := &protocol.RenameParams{ - TextDocument: e.textDocumentIdentifier(path), - Position: pos.ToProtocolPosition(), - NewName: newName, - } - wsEdits, err := e.Server.Rename(ctx, params) - if err != nil { - return err - } - for _, change := range wsEdits.DocumentChanges { - if err := e.applyProtocolEdit(ctx, change); err != nil { - return err - } - } - return nil -} - -func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocumentEdit) error { - path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) - if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { - return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) - } - if !e.HasBuffer(path) { - err := e.OpenFile(ctx, path) - if os.IsNotExist(err) { - // TODO: it's unclear if this is correct. Here we create the buffer (with - // version 1), then apply edits. Perhaps we should apply the edits before - // sending the didOpen notification. - e.CreateBuffer(ctx, path, "") - err = nil - } - if err != nil { - return err - } - } - fakeEdits := convertEdits(change.Edits) - return e.EditBuffer(ctx, path, fakeEdits) -} - -// CodeAction executes a codeAction request on the server. -func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { - if e.Server == nil { - return nil, nil - } - e.mu.Lock() - _, ok := e.buffers[path] - e.mu.Unlock() - if !ok { - return nil, fmt.Errorf("buffer %q is not open", path) - } - params := &protocol.CodeActionParams{ - TextDocument: e.textDocumentIdentifier(path), - Context: protocol.CodeActionContext{ - Diagnostics: diagnostics, - }, - } - if rng != nil { - params.Range = *rng - } - lens, err := e.Server.CodeAction(ctx, params) - if err != nil { - return nil, err - } - return lens, nil -} - -// Hover triggers a hover at the given position in an open buffer. -func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) { - if err := e.checkBufferPosition(path, pos); err != nil { - return nil, Pos{}, err - } - params := &protocol.HoverParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() - - resp, err := e.Server.Hover(ctx, params) - if err != nil { - return nil, Pos{}, errors.Errorf("hover: %w", err) - } - if resp == nil { - return nil, Pos{}, nil - } - return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil -} - -func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { - if e.Server == nil { - return nil, nil - } - params := &protocol.DocumentLinkParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - return e.Server.DocumentLink(ctx, params) -} - -func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos Pos) ([]protocol.DocumentHighlight, error) { - if e.Server == nil { - return nil, nil - } - if err := e.checkBufferPosition(path, pos); err != nil { - return nil, err - } - params := &protocol.DocumentHighlightParams{} - params.TextDocument.URI = e.sandbox.Workdir.URI(path) - params.Position = pos.ToProtocolPosition() - - return e.Server.DocumentHighlight(ctx, params) -} diff --git a/internal/lsp/fake/editor_test.go b/internal/lsp/fake/editor_test.go deleted file mode 100644 index 3ce5df6e0..000000000 --- a/internal/lsp/fake/editor_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "context" - "testing" -) - -func TestContentPosition(t *testing.T) { - content := "foo\n😀\nbar" - tests := []struct { - offset, wantLine, wantColumn int - }{ - {0, 0, 0}, - {3, 0, 3}, - {4, 1, 0}, - {5, 1, 1}, - {6, 2, 0}, - } - for _, test := range tests { - pos, err := contentPosition(content, test.offset) - if err != nil { - t.Fatal(err) - } - if pos.Line != test.wantLine { - t.Errorf("contentPosition(%q, %d): Line = %d, want %d", content, test.offset, pos.Line, test.wantLine) - } - if pos.Column != test.wantColumn { - t.Errorf("contentPosition(%q, %d): Column = %d, want %d", content, test.offset, pos.Column, test.wantColumn) - } - } -} - -const exampleProgram = ` --- go.mod -- -go 1.12 --- main.go -- -package main - -import "fmt" - -func main() { - fmt.Println("Hello World.") -} -` - -func TestClientEditing(t *testing.T) { - ws, err := NewSandbox(&SandboxConfig{Files: UnpackTxt(exampleProgram)}) - if err != nil { - t.Fatal(err) - } - defer ws.Close() - ctx := context.Background() - editor := NewEditor(ws, EditorConfig{}) - if err := editor.OpenFile(ctx, "main.go"); err != nil { - t.Fatal(err) - } - if err := editor.EditBuffer(ctx, "main.go", []Edit{ - { - Start: Pos{5, 14}, - End: Pos{5, 26}, - Text: "Hola, mundo.", - }, - }); err != nil { - t.Fatal(err) - } - got := editor.buffers["main.go"].text() - want := `package main - -import "fmt" - -func main() { - fmt.Println("Hola, mundo.") -} -` - if got != want { - t.Errorf("got text %q, want %q", got, want) - } -} diff --git a/internal/lsp/fake/proxy.go b/internal/lsp/fake/proxy.go deleted file mode 100644 index 9e56efeb1..000000000 --- a/internal/lsp/fake/proxy.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "fmt" - - "golang.org/x/tools/internal/proxydir" -) - -// WriteProxy creates a new proxy file tree using the txtar-encoded content, -// and returns its URL. -func WriteProxy(tmpdir string, files map[string][]byte) (string, error) { - type moduleVersion struct { - modulePath, version string - } - // Transform into the format expected by the proxydir package. - filesByModule := make(map[moduleVersion]map[string][]byte) - for name, data := range files { - modulePath, version, suffix := splitModuleVersionPath(name) - mv := moduleVersion{modulePath, version} - if _, ok := filesByModule[mv]; !ok { - filesByModule[mv] = make(map[string][]byte) - } - filesByModule[mv][suffix] = data - } - for mv, files := range filesByModule { - if err := proxydir.WriteModuleVersion(tmpdir, mv.modulePath, mv.version, files); err != nil { - return "", fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err) - } - } - return proxydir.ToURL(tmpdir), nil -} diff --git a/internal/lsp/fake/sandbox.go b/internal/lsp/fake/sandbox.go deleted file mode 100644 index f628f2d54..000000000 --- a/internal/lsp/fake/sandbox.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/txtar" - errors "golang.org/x/xerrors" -) - -// Sandbox holds a collection of temporary resources to use for working with Go -// code in tests. -type Sandbox struct { - gopath string - rootdir string - goproxy string - Workdir *Workdir -} - -// SandboxConfig controls the behavior of a test sandbox. The zero value -// defines a reasonable default. -type SandboxConfig struct { - // RootDir sets the base directory to use when creating temporary - // directories. If not specified, defaults to a new temporary directory. - RootDir string - // Files holds a txtar-encoded archive of files to populate the initial state - // of the working directory. - // - // For convenience, the special substring "$SANDBOX_WORKDIR" is replaced with - // the sandbox's resolved working directory before writing files. - Files map[string][]byte - // InGoPath specifies that the working directory should be within the - // temporary GOPATH. - InGoPath bool - // Workdir configures the working directory of the Sandbox. It behaves as - // follows: - // - if set to an absolute path, use that path as the working directory. - // - if set to a relative path, create and use that path relative to the - // sandbox. - // - if unset, default to a the 'work' subdirectory of the sandbox. - // - // This option is incompatible with InGoPath or Files. - Workdir string - // ProxyFiles holds a txtar-encoded archive of files to populate a file-based - // Go proxy. - ProxyFiles map[string][]byte - // GOPROXY is the explicit GOPROXY value that should be used for the sandbox. - // - // This option is incompatible with ProxyFiles. - GOPROXY string -} - -// NewSandbox creates a collection of named temporary resources, with a -// working directory populated by the txtar-encoded content in srctxt, and a -// file-based module proxy populated with the txtar-encoded content in -// proxytxt. -// -// If rootDir is non-empty, it will be used as the root of temporary -// directories created for the sandbox. Otherwise, a new temporary directory -// will be used as root. -func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) { - if config == nil { - config = new(SandboxConfig) - } - if err := validateConfig(*config); err != nil { - return nil, fmt.Errorf("invalid SandboxConfig: %v", err) - } - - sb := &Sandbox{} - defer func() { - // Clean up if we fail at any point in this constructor. - if err != nil { - sb.Close() - } - }() - - rootDir := config.RootDir - if rootDir == "" { - rootDir, err = ioutil.TempDir(config.RootDir, "gopls-sandbox-") - if err != nil { - return nil, fmt.Errorf("creating temporary workdir: %v", err) - } - } - sb.rootdir = rootDir - sb.gopath = filepath.Join(sb.rootdir, "gopath") - if err := os.Mkdir(sb.gopath, 0755); err != nil { - return nil, err - } - if config.GOPROXY != "" { - sb.goproxy = config.GOPROXY - } else { - proxydir := filepath.Join(sb.rootdir, "proxy") - if err := os.Mkdir(proxydir, 0755); err != nil { - return nil, err - } - sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles) - if err != nil { - return nil, err - } - } - // Short-circuit writing the workdir if we're given an absolute path, since - // this is used for running in an existing directory. - // TODO(findleyr): refactor this to be less of a workaround. - if filepath.IsAbs(config.Workdir) { - sb.Workdir = NewWorkdir(config.Workdir) - return sb, nil - } - var workdir string - if config.Workdir == "" { - if config.InGoPath { - // Set the working directory as $GOPATH/src. - workdir = filepath.Join(sb.gopath, "src") - } else if workdir == "" { - workdir = filepath.Join(sb.rootdir, "work") - } - } else { - // relative path - workdir = filepath.Join(sb.rootdir, config.Workdir) - } - if err := os.MkdirAll(workdir, 0755); err != nil { - return nil, err - } - sb.Workdir = NewWorkdir(workdir) - if err := sb.Workdir.writeInitialFiles(config.Files); err != nil { - return nil, err - } - return sb, nil -} - -// Tempdir creates a new temp directory with the given txtar-encoded files. It -// is the responsibility of the caller to call os.RemoveAll on the returned -// file path when it is no longer needed. -func Tempdir(files map[string][]byte) (string, error) { - dir, err := ioutil.TempDir("", "gopls-tempdir-") - if err != nil { - return "", err - } - for name, data := range files { - if err := WriteFileData(name, data, RelativeTo(dir)); err != nil { - return "", errors.Errorf("writing to tempdir: %w", err) - } - } - return dir, nil -} - -func UnpackTxt(txt string) map[string][]byte { - dataMap := make(map[string][]byte) - archive := txtar.Parse([]byte(txt)) - for _, f := range archive.Files { - dataMap[f.Name] = f.Data - } - return dataMap -} - -func validateConfig(config SandboxConfig) error { - if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) { - return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath") - } - if config.Workdir != "" && config.InGoPath { - return errors.New("Workdir cannot be set in conjunction with InGoPath") - } - if config.GOPROXY != "" && config.ProxyFiles != nil { - return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles") - } - return nil -} - -// splitModuleVersionPath extracts module information from files stored in the -// directory structure modulePath@version/suffix. -// For example: -// splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package") -func splitModuleVersionPath(path string) (modulePath, version, suffix string) { - parts := strings.Split(path, "/") - var modulePathParts []string - for i, p := range parts { - if strings.Contains(p, "@") { - mv := strings.SplitN(p, "@", 2) - modulePathParts = append(modulePathParts, mv[0]) - return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/") - } - modulePathParts = append(modulePathParts, p) - } - // Default behavior: this is just a module path. - return path, "", "" -} - -func (sb *Sandbox) RootDir() string { - return sb.rootdir -} - -// GOPATH returns the value of the Sandbox GOPATH. -func (sb *Sandbox) GOPATH() string { - return sb.gopath -} - -// GoEnv returns the default environment variables that can be used for -// invoking Go commands in the sandbox. -func (sb *Sandbox) GoEnv() map[string]string { - vars := map[string]string{ - "GOPATH": sb.GOPATH(), - "GOPROXY": sb.goproxy, - "GO111MODULE": "", - "GOSUMDB": "off", - "GOPACKAGESDRIVER": "off", - } - if testenv.Go1Point() >= 5 { - vars["GOMODCACHE"] = "" - } - return vars -} - -// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is -// true, the sandbox scans the working directory and emits file change events -// for any file changes it finds. -func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { - var vars []string - for k, v := range sb.GoEnv() { - vars = append(vars, fmt.Sprintf("%s=%s", k, v)) - } - inv := gocommand.Invocation{ - Verb: verb, - Args: args, - Env: vars, - } - // Use the provided directory for the working directory, if available. - // sb.Workdir may be nil if we exited the constructor with errors (we call - // Close to clean up any partial state from the constructor, which calls - // RunGoCommand). - if dir != "" { - inv.WorkingDir = sb.Workdir.AbsPath(dir) - } else if sb.Workdir != nil { - inv.WorkingDir = string(sb.Workdir.RelativeTo) - } - gocmdRunner := &gocommand.Runner{} - stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv) - if err != nil { - return errors.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) - } - // Since running a go command may result in changes to workspace files, - // check if we need to send any any "watched" file events. - // - // TODO(rFindley): this side-effect can impact the usability of the sandbox - // for benchmarks. Consider refactoring. - if sb.Workdir != nil && checkForFileChanges { - if err := sb.Workdir.CheckForFileChanges(ctx); err != nil { - return errors.Errorf("checking for file changes: %w", err) - } - } - return nil -} - -// Close removes all state associated with the sandbox. -func (sb *Sandbox) Close() error { - var goCleanErr error - if sb.gopath != "" { - goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false) - } - err := os.RemoveAll(sb.rootdir) - if err != nil || goCleanErr != nil { - return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) - } - return nil -} diff --git a/internal/lsp/fake/workdir.go b/internal/lsp/fake/workdir.go deleted file mode 100644 index 0be1d8fdf..000000000 --- a/internal/lsp/fake/workdir.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" - errors "golang.org/x/xerrors" -) - -// FileEvent wraps the protocol.FileEvent so that it can be associated with a -// workdir-relative path. -type FileEvent struct { - Path, Content string - ProtocolEvent protocol.FileEvent -} - -// RelativeTo is a helper for operations relative to a given directory. -type RelativeTo string - -// AbsPath returns an absolute filesystem path for the workdir-relative path. -func (r RelativeTo) AbsPath(path string) string { - fp := filepath.FromSlash(path) - if filepath.IsAbs(fp) { - return fp - } - return filepath.Join(string(r), filepath.FromSlash(path)) -} - -// RelPath returns a '/'-encoded path relative to the working directory (or an -// absolute path if the file is outside of workdir) -func (r RelativeTo) RelPath(fp string) string { - root := string(r) - if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") { - return filepath.ToSlash(rel) - } - return filepath.ToSlash(fp) -} - -// WriteFileData writes content to the relative path, replacing the special -// token $SANDBOX_WORKDIR with the relative root given by rel. -func WriteFileData(path string, content []byte, rel RelativeTo) error { - content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) - fp := rel.AbsPath(path) - if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { - return errors.Errorf("creating nested directory: %w", err) - } - backoff := 1 * time.Millisecond - for { - err := ioutil.WriteFile(fp, []byte(content), 0644) - if err != nil { - if isWindowsErrLockViolation(err) { - time.Sleep(backoff) - backoff *= 2 - continue - } - return errors.Errorf("writing %q: %w", path, err) - } - return nil - } -} - -// isWindowsErrLockViolation reports whether err is ERROR_LOCK_VIOLATION -// on Windows. -var isWindowsErrLockViolation = func(err error) bool { return false } - -// Workdir is a temporary working directory for tests. It exposes file -// operations in terms of relative paths, and fakes file watching by triggering -// events on file operations. -type Workdir struct { - RelativeTo - - watcherMu sync.Mutex - watchers []func(context.Context, []FileEvent) - - fileMu sync.Mutex - files map[string]string -} - -// NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a -// Workir for operating on these files using -func NewWorkdir(dir string) *Workdir { - return &Workdir{RelativeTo: RelativeTo(dir)} -} - -func hashFile(data []byte) string { - return fmt.Sprintf("%x", sha256.Sum256(data)) -} - -func (w *Workdir) writeInitialFiles(files map[string][]byte) error { - w.files = map[string]string{} - for name, data := range files { - w.files[name] = hashFile(data) - if err := WriteFileData(name, data, w.RelativeTo); err != nil { - return errors.Errorf("writing to workdir: %w", err) - } - } - return nil -} - -// RootURI returns the root URI for this working directory of this scratch -// environment. -func (w *Workdir) RootURI() protocol.DocumentURI { - return toURI(string(w.RelativeTo)) -} - -// AddWatcher registers the given func to be called on any file change. -func (w *Workdir) AddWatcher(watcher func(context.Context, []FileEvent)) { - w.watcherMu.Lock() - w.watchers = append(w.watchers, watcher) - w.watcherMu.Unlock() -} - -// URI returns the URI to a the workdir-relative path. -func (w *Workdir) URI(path string) protocol.DocumentURI { - return toURI(w.AbsPath(path)) -} - -// URIToPath converts a uri to a workdir-relative path (or an absolute path, -// if the uri is outside of the workdir). -func (w *Workdir) URIToPath(uri protocol.DocumentURI) string { - fp := uri.SpanURI().Filename() - return w.RelPath(fp) -} - -func toURI(fp string) protocol.DocumentURI { - return protocol.DocumentURI(span.URIFromPath(fp)) -} - -// ReadFile reads a text file specified by a workdir-relative path. -func (w *Workdir) ReadFile(path string) (string, error) { - backoff := 1 * time.Millisecond - for { - b, err := ioutil.ReadFile(w.AbsPath(path)) - if err != nil { - if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") { - // Plan 9 enforces exclusive access to locked files. - // Give the owner time to unlock it and retry. - time.Sleep(backoff) - backoff *= 2 - continue - } - return "", err - } - return string(b), nil - } -} - -func (w *Workdir) RegexpRange(path, re string) (Pos, Pos, error) { - content, err := w.ReadFile(path) - if err != nil { - return Pos{}, Pos{}, err - } - return regexpRange(content, re) -} - -// RegexpSearch searches the file corresponding to path for the first position -// matching re. -func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) { - content, err := w.ReadFile(path) - if err != nil { - return Pos{}, err - } - start, _, err := regexpRange(content, re) - return start, err -} - -// ChangeFilesOnDisk executes the given on-disk file changes in a batch, -// simulating the action of changing branches outside of an editor. -func (w *Workdir) ChangeFilesOnDisk(ctx context.Context, events []FileEvent) error { - for _, e := range events { - switch e.ProtocolEvent.Type { - case protocol.Deleted: - fp := w.AbsPath(e.Path) - if err := os.Remove(fp); err != nil { - return errors.Errorf("removing %q: %w", e.Path, err) - } - case protocol.Changed, protocol.Created: - if _, err := w.writeFile(ctx, e.Path, e.Content); err != nil { - return err - } - } - } - w.sendEvents(ctx, events) - return nil -} - -// RemoveFile removes a workdir-relative file path. -func (w *Workdir) RemoveFile(ctx context.Context, path string) error { - fp := w.AbsPath(path) - if err := os.RemoveAll(fp); err != nil { - return errors.Errorf("removing %q: %w", path, err) - } - w.fileMu.Lock() - defer w.fileMu.Unlock() - - evts := []FileEvent{{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: protocol.Deleted, - }, - }} - w.sendEvents(ctx, evts) - delete(w.files, path) - return nil -} - -func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) { - if len(evts) == 0 { - return - } - w.watcherMu.Lock() - watchers := make([]func(context.Context, []FileEvent), len(w.watchers)) - copy(watchers, w.watchers) - w.watcherMu.Unlock() - for _, w := range watchers { - w(ctx, evts) - } -} - -// WriteFiles writes the text file content to workdir-relative paths. -// It batches notifications rather than sending them consecutively. -func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { - var evts []FileEvent - for filename, content := range files { - evt, err := w.writeFile(ctx, filename, content) - if err != nil { - return err - } - evts = append(evts, evt) - } - w.sendEvents(ctx, evts) - return nil -} - -// WriteFile writes text file content to a workdir-relative path. -func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { - evt, err := w.writeFile(ctx, path, content) - if err != nil { - return err - } - w.sendEvents(ctx, []FileEvent{evt}) - return nil -} - -func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEvent, error) { - fp := w.AbsPath(path) - _, err := os.Stat(fp) - if err != nil && !os.IsNotExist(err) { - return FileEvent{}, errors.Errorf("checking if %q exists: %w", path, err) - } - var changeType protocol.FileChangeType - if os.IsNotExist(err) { - changeType = protocol.Created - } else { - changeType = protocol.Changed - } - if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { - return FileEvent{}, err - } - return FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: changeType, - }, - }, nil -} - -// listFiles lists files in the given directory, returning a map of relative -// path to modification time. -func (w *Workdir) listFiles(dir string) (map[string]string, error) { - files := make(map[string]string) - absDir := w.AbsPath(dir) - if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - path := w.RelPath(fp) - data, err := ioutil.ReadFile(fp) - if err != nil { - return err - } - files[path] = hashFile(data) - return nil - }); err != nil { - return nil, err - } - return files, nil -} - -// CheckForFileChanges walks the working directory and checks for any files -// that have changed since the last poll. -func (w *Workdir) CheckForFileChanges(ctx context.Context) error { - evts, err := w.pollFiles() - if err != nil { - return err - } - w.sendEvents(ctx, evts) - return nil -} - -// pollFiles updates w.files and calculates FileEvents corresponding to file -// state changes since the last poll. It does not call sendEvents. -func (w *Workdir) pollFiles() ([]FileEvent, error) { - w.fileMu.Lock() - defer w.fileMu.Unlock() - - files, err := w.listFiles(".") - if err != nil { - return nil, err - } - var evts []FileEvent - // Check which files have been added or modified. - for path, hash := range files { - oldhash, ok := w.files[path] - delete(w.files, path) - var typ protocol.FileChangeType - switch { - case !ok: - typ = protocol.Created - case oldhash != hash: - typ = protocol.Changed - default: - continue - } - evts = append(evts, FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: typ, - }, - }) - } - // Any remaining files must have been deleted. - for path := range w.files { - evts = append(evts, FileEvent{ - Path: path, - ProtocolEvent: protocol.FileEvent{ - URI: w.URI(path), - Type: protocol.Deleted, - }, - }) - } - w.files = files - return evts, nil -} diff --git a/internal/lsp/fake/workdir_test.go b/internal/lsp/fake/workdir_test.go deleted file mode 100644 index 33fbb9fa1..000000000 --- a/internal/lsp/fake/workdir_test.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "context" - "io/ioutil" - "os" - "sort" - "testing" - "time" - - "golang.org/x/tools/internal/lsp/protocol" -) - -const data = ` --- go.mod -- -go 1.12 --- nested/README.md -- -Hello World! -` - -func newWorkdir(t *testing.T) (*Workdir, <-chan []FileEvent, func()) { - t.Helper() - - tmpdir, err := ioutil.TempDir("", "goplstest-workdir-") - if err != nil { - t.Fatal(err) - } - wd := NewWorkdir(tmpdir) - if err := wd.writeInitialFiles(UnpackTxt(data)); err != nil { - t.Fatal(err) - } - cleanup := func() { - if err := os.RemoveAll(tmpdir); err != nil { - t.Error(err) - } - } - - fileEvents := make(chan []FileEvent) - watch := func(_ context.Context, events []FileEvent) { - go func() { - fileEvents <- events - }() - } - wd.AddWatcher(watch) - return wd, fileEvents, cleanup -} - -func TestWorkdir_ReadFile(t *testing.T) { - wd, _, cleanup := newWorkdir(t) - defer cleanup() - - got, err := wd.ReadFile("nested/README.md") - if err != nil { - t.Fatal(err) - } - want := "Hello World!\n" - if got != want { - t.Errorf("reading workdir file, got %q, want %q", got, want) - } -} - -func TestWorkdir_WriteFile(t *testing.T) { - wd, events, cleanup := newWorkdir(t) - defer cleanup() - ctx := context.Background() - - tests := []struct { - path string - wantType protocol.FileChangeType - }{ - {"data.txt", protocol.Created}, - {"nested/README.md", protocol.Changed}, - } - - for _, test := range tests { - if err := wd.WriteFile(ctx, test.path, "42"); err != nil { - t.Fatal(err) - } - es := <-events - if got := len(es); got != 1 { - t.Fatalf("len(events) = %d, want 1", got) - } - if es[0].Path != test.path { - t.Errorf("event.Path = %q, want %q", es[0].Path, test.path) - } - if es[0].ProtocolEvent.Type != test.wantType { - t.Errorf("event type = %v, want %v", es[0].ProtocolEvent.Type, test.wantType) - } - got, err := wd.ReadFile(test.path) - if err != nil { - t.Fatal(err) - } - want := "42" - if got != want { - t.Errorf("ws.ReadFile(%q) = %q, want %q", test.path, got, want) - } - } -} - -func TestWorkdir_ListFiles(t *testing.T) { - wd, _, cleanup := newWorkdir(t) - defer cleanup() - - checkFiles := func(dir string, want []string) { - files, err := wd.listFiles(dir) - if err != nil { - t.Fatal(err) - } - sort.Strings(want) - var got []string - for p := range files { - got = append(got, p) - } - sort.Strings(got) - if len(got) != len(want) { - t.Fatalf("ListFiles(): len = %d, want %d; got=%v; want=%v", len(got), len(want), got, want) - } - for i, f := range got { - if f != want[i] { - t.Errorf("ListFiles()[%d] = %s, want %s", i, f, want[i]) - } - } - } - - checkFiles(".", []string{"go.mod", "nested/README.md"}) - checkFiles("nested", []string{"nested/README.md"}) -} - -func TestWorkdir_CheckForFileChanges(t *testing.T) { - t.Skip("broken on darwin-amd64-10_12") - wd, events, cleanup := newWorkdir(t) - defer cleanup() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - checkChange := func(path string, typ protocol.FileChangeType) { - if err := wd.CheckForFileChanges(ctx); err != nil { - t.Fatal(err) - } - var gotEvt FileEvent - select { - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case ev := <-events: - gotEvt = ev[0] - } - // Only check relative path and Type - if gotEvt.Path != path || gotEvt.ProtocolEvent.Type != typ { - t.Errorf("file events: got %v, want {Path: %s, Type: %v}", gotEvt, path, typ) - } - } - // Sleep some positive amount of time to ensure a distinct mtime. - time.Sleep(100 * time.Millisecond) - if err := WriteFileData("go.mod", []byte("module foo.test\n"), wd.RelativeTo); err != nil { - t.Fatal(err) - } - checkChange("go.mod", protocol.Changed) - if err := WriteFileData("newFile", []byte("something"), wd.RelativeTo); err != nil { - t.Fatal(err) - } - checkChange("newFile", protocol.Created) - fp := wd.AbsPath("newFile") - if err := os.Remove(fp); err != nil { - t.Fatal(err) - } - checkChange("newFile", protocol.Deleted) -} - -func TestSplitModuleVersionPath(t *testing.T) { - tests := []struct { - path string - wantModule, wantVersion, wantSuffix string - }{ - {"foo.com@v1.2.3/bar", "foo.com", "v1.2.3", "bar"}, - {"foo.com/module@v1.2.3/bar", "foo.com/module", "v1.2.3", "bar"}, - {"foo.com@v1.2.3", "foo.com", "v1.2.3", ""}, - {"std@v1.14.0", "std", "v1.14.0", ""}, - {"another/module/path", "another/module/path", "", ""}, - } - - for _, test := range tests { - module, version, suffix := splitModuleVersionPath(test.path) - if module != test.wantModule || version != test.wantVersion || suffix != test.wantSuffix { - t.Errorf("splitModuleVersionPath(%q) =\n\t(%q, %q, %q)\nwant\n\t(%q, %q, %q)", - test.path, module, version, suffix, test.wantModule, test.wantVersion, test.wantSuffix) - } - } -} diff --git a/internal/lsp/fake/workdir_windows.go b/internal/lsp/fake/workdir_windows.go deleted file mode 100644 index ed2b4bb36..000000000 --- a/internal/lsp/fake/workdir_windows.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package fake - -import ( - "syscall" - - errors "golang.org/x/xerrors" -) - -func init() { - // from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- - const ERROR_LOCK_VIOLATION syscall.Errno = 33 - - isWindowsErrLockViolation = func(err error) bool { - return errors.Is(err, ERROR_LOCK_VIOLATION) - } -} |