github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/fake/workdir.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package fake
     6  
     7  import (
     8  	"context"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/april1989/origin-go-tools/internal/lsp/protocol"
    17  	"github.com/april1989/origin-go-tools/internal/span"
    18  	errors "golang.org/x/xerrors"
    19  )
    20  
    21  // FileEvent wraps the protocol.FileEvent so that it can be associated with a
    22  // workdir-relative path.
    23  type FileEvent struct {
    24  	Path, Content string
    25  	ProtocolEvent protocol.FileEvent
    26  }
    27  
    28  // Workdir is a temporary working directory for tests. It exposes file
    29  // operations in terms of relative paths, and fakes file watching by triggering
    30  // events on file operations.
    31  type Workdir struct {
    32  	workdir string
    33  
    34  	watcherMu sync.Mutex
    35  	watchers  []func(context.Context, []FileEvent)
    36  
    37  	fileMu sync.Mutex
    38  	files  map[string]time.Time
    39  }
    40  
    41  // NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a
    42  // Workir for operating on these files using
    43  func NewWorkdir(dir string) *Workdir {
    44  	return &Workdir{workdir: dir}
    45  }
    46  
    47  func (w *Workdir) WriteInitialFiles(txt string) error {
    48  	files := unpackTxt(txt)
    49  	for name, data := range files {
    50  		if err := w.writeFileData(name, string(data)); err != nil {
    51  			return errors.Errorf("writing to workdir: %w", err)
    52  		}
    53  	}
    54  	// Poll to capture the current file state.
    55  	if _, err := w.pollFiles(); err != nil {
    56  		return errors.Errorf("polling files: %w", err)
    57  	}
    58  	return nil
    59  }
    60  
    61  // RootURI returns the root URI for this working directory of this scratch
    62  // environment.
    63  func (w *Workdir) RootURI() protocol.DocumentURI {
    64  	return toURI(w.workdir)
    65  }
    66  
    67  // AddWatcher registers the given func to be called on any file change.
    68  func (w *Workdir) AddWatcher(watcher func(context.Context, []FileEvent)) {
    69  	w.watcherMu.Lock()
    70  	w.watchers = append(w.watchers, watcher)
    71  	w.watcherMu.Unlock()
    72  }
    73  
    74  // filePath returns an absolute filesystem path for the workdir-relative path.
    75  func (w *Workdir) filePath(path string) string {
    76  	fp := filepath.FromSlash(path)
    77  	if filepath.IsAbs(fp) {
    78  		return fp
    79  	}
    80  	return filepath.Join(w.workdir, filepath.FromSlash(path))
    81  }
    82  
    83  // URI returns the URI to a the workdir-relative path.
    84  func (w *Workdir) URI(path string) protocol.DocumentURI {
    85  	return toURI(w.filePath(path))
    86  }
    87  
    88  // URIToPath converts a uri to a workdir-relative path (or an absolute path,
    89  // if the uri is outside of the workdir).
    90  func (w *Workdir) URIToPath(uri protocol.DocumentURI) string {
    91  	fp := uri.SpanURI().Filename()
    92  	return w.relPath(fp)
    93  }
    94  
    95  // relPath returns a '/'-encoded path relative to the working directory (or an
    96  // absolute path if the file is outside of workdir)
    97  func (w *Workdir) relPath(fp string) string {
    98  	root := w.RootURI().SpanURI().Filename()
    99  	if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") {
   100  		return filepath.ToSlash(rel)
   101  	}
   102  	return filepath.ToSlash(fp)
   103  }
   104  
   105  func toURI(fp string) protocol.DocumentURI {
   106  	return protocol.DocumentURI(span.URIFromPath(fp))
   107  }
   108  
   109  // ReadFile reads a text file specified by a workdir-relative path.
   110  func (w *Workdir) ReadFile(path string) (string, error) {
   111  	b, err := ioutil.ReadFile(w.filePath(path))
   112  	if err != nil {
   113  		return "", err
   114  	}
   115  	return string(b), nil
   116  }
   117  
   118  // RegexpSearch searches the file corresponding to path for the first position
   119  // matching re.
   120  func (w *Workdir) RegexpSearch(path string, re string) (Pos, error) {
   121  	content, err := w.ReadFile(path)
   122  	if err != nil {
   123  		return Pos{}, err
   124  	}
   125  	start, _, err := regexpRange(content, re)
   126  	return start, err
   127  }
   128  
   129  // ChangeFilesOnDisk executes the given on-disk file changes in a batch,
   130  // simulating the action of changing branches outside of an editor.
   131  func (w *Workdir) ChangeFilesOnDisk(ctx context.Context, events []FileEvent) error {
   132  	for _, e := range events {
   133  		switch e.ProtocolEvent.Type {
   134  		case protocol.Deleted:
   135  			fp := w.filePath(e.Path)
   136  			if err := os.Remove(fp); err != nil {
   137  				return errors.Errorf("removing %q: %w", e.Path, err)
   138  			}
   139  		case protocol.Changed, protocol.Created:
   140  			if _, err := w.writeFile(ctx, e.Path, e.Content); err != nil {
   141  				return err
   142  			}
   143  		}
   144  	}
   145  	w.sendEvents(ctx, events)
   146  	return nil
   147  }
   148  
   149  // RemoveFile removes a workdir-relative file path.
   150  func (w *Workdir) RemoveFile(ctx context.Context, path string) error {
   151  	fp := w.filePath(path)
   152  	if err := os.Remove(fp); err != nil {
   153  		return errors.Errorf("removing %q: %w", path, err)
   154  	}
   155  	evts := []FileEvent{{
   156  		Path: path,
   157  		ProtocolEvent: protocol.FileEvent{
   158  			URI:  w.URI(path),
   159  			Type: protocol.Deleted,
   160  		},
   161  	}}
   162  	w.sendEvents(ctx, evts)
   163  	return nil
   164  }
   165  
   166  func (w *Workdir) sendEvents(ctx context.Context, evts []FileEvent) {
   167  	if len(evts) == 0 {
   168  		return
   169  	}
   170  	w.watcherMu.Lock()
   171  	watchers := make([]func(context.Context, []FileEvent), len(w.watchers))
   172  	copy(watchers, w.watchers)
   173  	w.watcherMu.Unlock()
   174  	for _, w := range watchers {
   175  		go w(ctx, evts)
   176  	}
   177  }
   178  
   179  // WriteFiles writes the text file content to workdir-relative paths.
   180  // It batches notifications rather than sending them consecutively.
   181  func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error {
   182  	var evts []FileEvent
   183  	for filename, content := range files {
   184  		evt, err := w.writeFile(ctx, filename, content)
   185  		if err != nil {
   186  			return err
   187  		}
   188  		evts = append(evts, evt)
   189  	}
   190  	w.sendEvents(ctx, evts)
   191  	return nil
   192  }
   193  
   194  // WriteFile writes text file content to a workdir-relative path.
   195  func (w *Workdir) WriteFile(ctx context.Context, path, content string) error {
   196  	evt, err := w.writeFile(ctx, path, content)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	w.sendEvents(ctx, []FileEvent{evt})
   201  	return nil
   202  }
   203  
   204  func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEvent, error) {
   205  	fp := w.filePath(path)
   206  	_, err := os.Stat(fp)
   207  	if err != nil && !os.IsNotExist(err) {
   208  		return FileEvent{}, errors.Errorf("checking if %q exists: %w", path, err)
   209  	}
   210  	var changeType protocol.FileChangeType
   211  	if os.IsNotExist(err) {
   212  		changeType = protocol.Created
   213  	} else {
   214  		changeType = protocol.Changed
   215  	}
   216  	if err := w.writeFileData(path, content); err != nil {
   217  		return FileEvent{}, err
   218  	}
   219  	return FileEvent{
   220  		Path: path,
   221  		ProtocolEvent: protocol.FileEvent{
   222  			URI:  w.URI(path),
   223  			Type: changeType,
   224  		},
   225  	}, nil
   226  }
   227  
   228  func (w *Workdir) writeFileData(path string, content string) error {
   229  	fp := w.filePath(path)
   230  	if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil {
   231  		return errors.Errorf("creating nested directory: %w", err)
   232  	}
   233  	if err := ioutil.WriteFile(fp, []byte(content), 0644); err != nil {
   234  		return errors.Errorf("writing %q: %w", path, err)
   235  	}
   236  	return nil
   237  }
   238  
   239  // ListFiles lists files in the given directory, returning a map of relative
   240  // path to modification time.
   241  func (w *Workdir) ListFiles(dir string) (map[string]time.Time, error) {
   242  	files := make(map[string]time.Time)
   243  	absDir := w.filePath(dir)
   244  	if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error {
   245  		if err != nil {
   246  			return err
   247  		}
   248  		if info.IsDir() {
   249  			return nil
   250  		}
   251  		path := w.relPath(fp)
   252  		files[path] = info.ModTime()
   253  		return nil
   254  	}); err != nil {
   255  		return nil, err
   256  	}
   257  	return files, nil
   258  }
   259  
   260  // CheckForFileChanges walks the working directory and checks for any files
   261  // that have changed since the last poll.
   262  func (w *Workdir) CheckForFileChanges(ctx context.Context) error {
   263  	evts, err := w.pollFiles()
   264  	if err != nil {
   265  		return err
   266  	}
   267  	w.sendEvents(ctx, evts)
   268  	return nil
   269  }
   270  
   271  // pollFiles updates w.files and calculates FileEvents corresponding to file
   272  // state changes since the last poll. It does not call sendEvents.
   273  func (w *Workdir) pollFiles() ([]FileEvent, error) {
   274  	w.fileMu.Lock()
   275  	defer w.fileMu.Unlock()
   276  
   277  	files, err := w.ListFiles(".")
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	var evts []FileEvent
   282  	// Check which files have been added or modified.
   283  	for path, mtime := range files {
   284  		oldmtime, ok := w.files[path]
   285  		delete(w.files, path)
   286  		var typ protocol.FileChangeType
   287  		switch {
   288  		case !ok:
   289  			typ = protocol.Created
   290  		case oldmtime != mtime:
   291  			typ = protocol.Changed
   292  		default:
   293  			continue
   294  		}
   295  		evts = append(evts, FileEvent{
   296  			Path: path,
   297  			ProtocolEvent: protocol.FileEvent{
   298  				URI:  w.URI(path),
   299  				Type: typ,
   300  			},
   301  		})
   302  	}
   303  	// Any remaining files must have been deleted.
   304  	for path := range w.files {
   305  		evts = append(evts, FileEvent{
   306  			Path: path,
   307  			ProtocolEvent: protocol.FileEvent{
   308  				URI:  w.URI(path),
   309  				Type: protocol.Deleted,
   310  			},
   311  		})
   312  	}
   313  	w.files = files
   314  	return evts, nil
   315  }