blob: 39a16db7da2329c0160d5869761b171eef8e4efc [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 (
"errors"
"fmt"
"net/url"
"strings"
)
// An EditorFile 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 EditorFile 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[lsp.DocumentURI][]byte instead?
files map[EditorFile]string
}
// 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.
type Position struct {
Line int
Character int
}
// 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
RangeLength uint
Text string
}
// NewFileSystem returns an initialized empty FileSystem.
func NewFileSystem() *FileSystem {
return &FileSystem{
files: make(map[EditorFile]string),
}
}
// File is a read-only accessor for the in-memory file with ID `path`.
func (fs *FileSystem) File(path EditorFile) (string, error) {
if file, ok := fs.files[path]; ok {
return file, nil
}
return "", fmt.Errorf("file `%s` not in memory", path)
}
// Files returns all of the in-memory files for traversal.
func (fs *FileSystem) Files() map[EditorFile]string {
return fs.files
}
// OpenFile copies the contents of the TextDocumentItem into the FileSystem's
// in-memory file system ("opening" the file).
func (fs *FileSystem) OpenFile(path EditorFile, text string) {
fs.files[path] = text
}
// CloseFile deletes the in-memory representation of `file`.
func (fs *FileSystem) CloseFile(path EditorFile) {
delete(fs.files, path)
}
// ApplyChanges applies the specified changes to the relevant in-memory file.
func (fs *FileSystem) ApplyChanges(path EditorFile, changes []Change) error {
for _, change := range changes {
start, err := offsetInFile(fs.files[path], change.Range.Start)
if err != nil {
return err
}
end := start + change.RangeLength
// Copy file over to new buffer with inserted text change
var newFile strings.Builder
newFile.WriteString(fs.files[path][:start])
newFile.WriteString(change.Text)
newFile.WriteString(fs.files[path][end:])
fs.files[path] = newFile.String()
}
return nil
}
func offsetInFile(contents string, pos Position) (uint, error) {
line := 0
col := 0
var offset uint = 0
for _, c := range contents {
if line == pos.Line && col == pos.Character {
return offset, nil
}
offset++
if c == '\n' {
line++
col = 0
} else {
col++
}
}
// Check if pos is pointing at the end of the file
if line == pos.Line && col == pos.Character {
return offset, nil
}
return 0, errors.New("position was out of bounds in file")
}
// EditorFileToPath converts an EditorFile ID, which is in the form of a file
// schema URI ("file:///"), to an absolute path.
func EditorFileToPath(editorFile EditorFile) (string, error) {
fileURI, err := url.Parse(string(editorFile))
if err != nil {
return "", fmt.Errorf("could not parse EditorFile: %s", err)
}
return fileURI.Path, nil
}