blob: bd3f501149bf42df88cf393162fda304357c25c3 [file] [log] [blame]
// 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)
}