| // Copyright 2020 The Fuchsia 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 state |
| |
| import ( |
| "fmt" |
| "net/url" |
| "strings" |
| ) |
| |
| // A FileID is a path to a file open in the client editor. |
| // |
| // We never assume the "saved-ness" of a file, and maintain all files that are |
| // open in the editor in memory. Therefore this path does not necessarily |
| // correspond to an absolute filepath; it's just used as an ID to access the |
| // in-memory file. |
| type FileID string |
| |
| // FileSystem manages file synchronization between client and server. |
| // `files` is an in-memory mapping of URI to file contents of the currently open |
| // files on the client. |
| type FileSystem struct { |
| // TODO: should this be a map[FileID][]byte instead? |
| |
| // `files` is stored as a map of FileIDs to files that are split by line |
| // (but retaining their '\n' characters). |
| // This speeds up the common operation of searching for a symbol at a given |
| // (line, column) in the file as the file is indexed by line. |
| files map[FileID][]string |
| |
| // TODO: RWLock for safe concurrency |
| } |
| |
| // Range represents a span of text in a source file. |
| type Range struct { |
| Start Position |
| End Position |
| } |
| |
| // Position represents the location of one character in a source file. |
| // Both are zero-indexed, i.e. lines start at 0 and the first character of a |
| // line is at index 0. |
| type Position struct { |
| Line int |
| Character int |
| } |
| |
| // Location represents a range in a particular file. |
| type Location struct { |
| FileID FileID |
| Range Range |
| } |
| |
| // Change represents a single diff in a source file. |
| // Range is the range that is being changed. RangeLength is the length of the |
| // replacement text (0 if the range is being deleted). Text is the text to |
| // insert in the source at that range. |
| type Change struct { |
| Range Range |
| NewContent string |
| } |
| |
| // NewFileSystem returns an initialized empty FileSystem. |
| func NewFileSystem() *FileSystem { |
| return &FileSystem{ |
| files: make(map[FileID][]string), |
| } |
| } |
| |
| // File is a read-only accessor for the in-memory file with ID `path`. |
| func (fs *FileSystem) File(path FileID) (string, error) { |
| if lines, ok := fs.files[path]; ok { |
| return strings.Join(lines, ""), nil |
| } |
| return "", fmt.Errorf("file `%s` not in memory", path) |
| } |
| |
| // NFiles returns the number of in-memory files. |
| func (fs *FileSystem) NFiles() int { |
| return len(fs.files) |
| } |
| |
| // Files returns the FileIDs of the in-memory files. |
| func (fs *FileSystem) Files() []FileID { |
| files := make([]FileID, len(fs.files)) |
| i := 0 |
| for f := range fs.files { |
| files[i] = f |
| i++ |
| } |
| return files |
| } |
| |
| // NewFile copies `text` into the FileSystem's in-memory file system ("opening" |
| // the file), indexed by the FileID `path`. |
| func (fs *FileSystem) NewFile(path FileID, text string) { |
| fs.files[path] = strings.SplitAfter(text, "\n") |
| } |
| |
| // DeleteFile deletes the FileSystem's in-memory representation of the file |
| // indexed by `path`. |
| func (fs *FileSystem) DeleteFile(path FileID) { |
| delete(fs.files, path) |
| } |
| |
| // ApplyChanges applies the specified changes to the relevant in-memory file. |
| func (fs *FileSystem) ApplyChanges(path FileID, changes []Change) error { |
| for _, change := range changes { |
| start, err := OffsetInFile(fs.files[path], change.Range.Start) |
| if err != nil { |
| return err |
| } |
| end, err := OffsetInFile(fs.files[path], change.Range.End) |
| if err != nil { |
| return err |
| } |
| |
| file := strings.Join(fs.files[path], "") |
| |
| // Copy file over to new buffer with inserted text change |
| var newFile strings.Builder |
| newFile.WriteString(file[:start]) |
| newFile.WriteString(change.NewContent) |
| newFile.WriteString(file[end:]) |
| fs.files[path] = strings.SplitAfter(newFile.String(), "\n") |
| } |
| return nil |
| } |
| |
| // OffsetInFile converts a Position (line, character) to a rune offset in |
| // `contents`. |
| func OffsetInFile(lines []string, pos Position) (uint, error) { |
| var offset uint |
| for line, l := range lines { |
| for col := range l { |
| if line == pos.Line && col == pos.Character { |
| return offset, nil |
| } |
| offset++ |
| } |
| } |
| // Check if pos is pointing at the end of the file |
| lastLine := len(lines) - 1 |
| if pos.Line == lastLine && pos.Character == len(lines[lastLine]) { |
| return offset, nil |
| } |
| return 0, fmt.Errorf( |
| "position %#v was out of bounds in file with %d lines, %d total chars", |
| pos, |
| len(lines), |
| offset, |
| ) |
| } |
| |
| // FileIDToPath converts a FileID, which is in the form of a file schema URI |
| // ("file:///"), to an absolute path. |
| func FileIDToPath(fileID FileID) (string, error) { |
| fileURI, err := url.Parse(string(fileID)) |
| if err != nil { |
| return "", fmt.Errorf("could not parse FileID: %s", err) |
| } |
| return fileURI.Path, nil |
| } |
| |
| // PathToFileID converts an absolute file path to a file schema URI. |
| func PathToFileID(path string) FileID { |
| return FileID("file://" + path) |
| } |