| // 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" |
| "sync" |
| |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/txtar" |
| ) |
| |
| // FileEvent wraps the protocol.FileEvent so that it can be associated with a |
| // workspace-relative path. |
| type FileEvent struct { |
| Path string |
| ProtocolEvent protocol.FileEvent |
| } |
| |
| // The Workspace type represents a temporary workspace to use for editing Go |
| // files in tests. |
| type Workspace struct { |
| name string |
| gopath string |
| workdir string |
| |
| watcherMu sync.Mutex |
| watchers []func(context.Context, []FileEvent) |
| } |
| |
| // NewWorkspace creates a named workspace populated by the txtar-encoded |
| // content given by txt. It creates temporary directories for the workspace |
| // content and for GOPATH. |
| func NewWorkspace(name string, txt []byte) (_ *Workspace, err error) { |
| w := &Workspace{name: name} |
| defer func() { |
| // Clean up if we fail at any point in this constructor. |
| if err != nil { |
| w.removeAll() |
| } |
| }() |
| dir, err := ioutil.TempDir("", fmt.Sprintf("goplstest-ws-%s-", name)) |
| if err != nil { |
| return nil, fmt.Errorf("creating temporary workdir: %v", err) |
| } |
| w.workdir = dir |
| gopath, err := ioutil.TempDir("", fmt.Sprintf("goplstest-gopath-%s-", name)) |
| if err != nil { |
| return nil, fmt.Errorf("creating temporary gopath: %v", err) |
| } |
| w.gopath = gopath |
| archive := txtar.Parse(txt) |
| for _, f := range archive.Files { |
| if err := w.writeFileData(f.Name, string(f.Data)); err != nil { |
| return nil, err |
| } |
| } |
| return w, nil |
| } |
| |
| // RootURI returns the root URI for this workspace. |
| func (w *Workspace) RootURI() protocol.DocumentURI { |
| return toURI(w.workdir) |
| } |
| |
| // GOPATH returns the value that GOPATH should be set to for this workspace. |
| func (w *Workspace) GOPATH() string { |
| return w.gopath |
| } |
| |
| // AddWatcher registers the given func to be called on any file change. |
| func (w *Workspace) AddWatcher(watcher func(context.Context, []FileEvent)) { |
| w.watcherMu.Lock() |
| w.watchers = append(w.watchers, watcher) |
| w.watcherMu.Unlock() |
| } |
| |
| // filePath returns the absolute filesystem path to a the workspace-relative |
| // path. |
| func (w *Workspace) filePath(path string) string { |
| fp := filepath.FromSlash(path) |
| if filepath.IsAbs(fp) { |
| return fp |
| } |
| return filepath.Join(w.workdir, filepath.FromSlash(path)) |
| } |
| |
| // URI returns the URI to a the workspace-relative path. |
| func (w *Workspace) URI(path string) protocol.DocumentURI { |
| return toURI(w.filePath(path)) |
| } |
| |
| // URIToPath converts a uri to a workspace-relative path (or an absolute path, |
| // if the uri is outside of the workspace). |
| func (w *Workspace) URIToPath(uri protocol.DocumentURI) string { |
| root := w.RootURI().SpanURI().Filename() |
| path := uri.SpanURI().Filename() |
| if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, "..") { |
| return filepath.ToSlash(rel) |
| } |
| return filepath.ToSlash(path) |
| } |
| |
| func toURI(fp string) protocol.DocumentURI { |
| return protocol.DocumentURI(span.URIFromPath(fp)) |
| } |
| |
| // ReadFile reads a text file specified by a workspace-relative path. |
| func (w *Workspace) ReadFile(path string) (string, error) { |
| b, err := ioutil.ReadFile(w.filePath(path)) |
| if err != nil { |
| return "", err |
| } |
| return string(b), nil |
| } |
| |
| // RegexpSearch searches the file |
| func (w *Workspace) 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 |
| } |
| |
| // RemoveFile removes a workspace-relative file path. |
| func (w *Workspace) RemoveFile(ctx context.Context, path string) error { |
| fp := w.filePath(path) |
| if err := os.Remove(fp); err != nil { |
| return fmt.Errorf("removing %q: %v", path, err) |
| } |
| evts := []FileEvent{{ |
| Path: path, |
| ProtocolEvent: protocol.FileEvent{ |
| URI: w.URI(path), |
| Type: protocol.Deleted, |
| }, |
| }} |
| w.sendEvents(ctx, evts) |
| return nil |
| } |
| |
| func (w *Workspace) sendEvents(ctx context.Context, evts []FileEvent) { |
| w.watcherMu.Lock() |
| watchers := make([]func(context.Context, []FileEvent), len(w.watchers)) |
| copy(watchers, w.watchers) |
| w.watcherMu.Unlock() |
| for _, w := range watchers { |
| go w(ctx, evts) |
| } |
| } |
| |
| // WriteFile writes text file content to a workspace-relative path. |
| func (w *Workspace) WriteFile(ctx context.Context, path, content string) error { |
| fp := w.filePath(path) |
| _, err := os.Stat(fp) |
| if err != nil && !os.IsNotExist(err) { |
| return fmt.Errorf("checking if %q exists: %v", path, err) |
| } |
| var changeType protocol.FileChangeType |
| if os.IsNotExist(err) { |
| changeType = protocol.Created |
| } else { |
| changeType = protocol.Changed |
| } |
| if err := w.writeFileData(path, content); err != nil { |
| return err |
| } |
| evts := []FileEvent{{ |
| Path: path, |
| ProtocolEvent: protocol.FileEvent{ |
| URI: w.URI(path), |
| Type: changeType, |
| }, |
| }} |
| w.sendEvents(ctx, evts) |
| return nil |
| } |
| |
| func (w *Workspace) writeFileData(path string, content string) error { |
| fp := w.filePath(path) |
| if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { |
| return fmt.Errorf("creating nested directory: %v", err) |
| } |
| if err := ioutil.WriteFile(fp, []byte(content), 0644); err != nil { |
| return fmt.Errorf("writing %q: %v", path, err) |
| } |
| return nil |
| } |
| |
| func (w *Workspace) removeAll() error { |
| var werr, perr error |
| if w.workdir != "" { |
| werr = os.RemoveAll(w.workdir) |
| } |
| if w.gopath != "" { |
| perr = os.RemoveAll(w.gopath) |
| } |
| if werr != nil || perr != nil { |
| return fmt.Errorf("error(s) cleaning workspace: removing workdir: %v; removing gopath: %v", werr, perr) |
| } |
| return nil |
| } |
| |
| // Close removes all state associated with the workspace. |
| func (w *Workspace) Close() error { |
| return w.removeAll() |
| } |