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 }