aboutsummaryrefslogtreecommitdiff
path: root/internal/lsp/fake
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp/fake')
-rw-r--r--internal/lsp/fake/client.go128
-rw-r--r--internal/lsp/fake/doc.go19
-rw-r--r--internal/lsp/fake/edit.go157
-rw-r--r--internal/lsp/fake/edit_test.go97
-rw-r--r--internal/lsp/fake/editor.go1258
-rw-r--r--internal/lsp/fake/editor_test.go82
-rw-r--r--internal/lsp/fake/proxy.go35
-rw-r--r--internal/lsp/fake/sandbox.go273
-rw-r--r--internal/lsp/fake/workdir.go365
-rw-r--r--internal/lsp/fake/workdir_test.go192
-rw-r--r--internal/lsp/fake/workdir_windows.go20
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 &params.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)
- }
-}