blob: bb5fb80900bea317f2dbdef011725675ffac02f6 [file] [log] [blame]
// Copyright 2020 The Go 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 fake
import (
"fmt"
"sort"
"strings"
"golang.org/x/tools/gopls/internal/lsp/protocol"
)
// Pos represents a position in a text buffer. Both Line and Column are
// 0-indexed.
type Pos struct {
Line, Column int
}
func (p Pos) String() string {
return fmt.Sprintf("%v:%v", p.Line, p.Column)
}
// Range corresponds to protocol.Range, but uses the editor friend Pos
// instead of UTF-16 oriented protocol.Position
type Range struct {
Start Pos
End Pos
}
func (p Pos) ToProtocolPosition() protocol.Position {
return protocol.Position{
Line: uint32(p.Line),
Character: uint32(p.Column),
}
}
func fromProtocolPosition(pos protocol.Position) Pos {
return Pos{
Line: int(pos.Line),
Column: int(pos.Character),
}
}
// Edit represents a single (contiguous) buffer edit.
type Edit struct {
Start, End Pos
Text string
}
// Location is the editor friendly equivalent of protocol.Location
type Location struct {
Path string
Range Range
}
// SymbolInformation is an editor friendly version of
// protocol.SymbolInformation, with location information transformed to byte
// offsets. Field names correspond to the protocol type.
type SymbolInformation struct {
Name string
Kind protocol.SymbolKind
Location Location
}
// NewEdit creates an edit replacing all content between
// (startLine, startColumn) and (endLine, endColumn) with text.
func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit {
return Edit{
Start: Pos{Line: startLine, Column: startColumn},
End: Pos{Line: endLine, Column: endColumn},
Text: text,
}
}
func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent {
return protocol.TextDocumentContentChangeEvent{
Range: &protocol.Range{
Start: e.Start.ToProtocolPosition(),
End: e.End.ToProtocolPosition(),
},
Text: e.Text,
}
}
func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit {
return Edit{
Start: fromProtocolPosition(textEdit.Range.Start),
End: fromProtocolPosition(textEdit.Range.End),
Text: textEdit.NewText,
}
}
// inText reports whether p is a valid position in the text buffer.
func inText(p Pos, content []string) bool {
if p.Line < 0 || p.Line >= len(content) {
return false
}
// Note the strict right bound: the column indexes character _separators_,
// not characters.
if p.Column < 0 || p.Column > len([]rune(content[p.Line])) {
return false
}
return true
}
// editContent implements a simplistic, inefficient algorithm for applying text
// edits to our buffer representation. It returns an error if the edit is
// invalid for the current content.
//
// TODO(rfindley): this function does not handle non-ascii text correctly.
// TODO(rfindley): replace this with diff.Apply: we should not be
// maintaining an additional representation of edits.
func editContent(content []string, edits []Edit) ([]string, error) {
newEdits := make([]Edit, len(edits))
copy(newEdits, edits)
sort.SliceStable(newEdits, func(i, j int) bool {
ei := newEdits[i]
ej := newEdits[j]
// Sort by edit start position followed by end position. Given an edit
// 3:1-3:1 followed by an edit 3:1-3:15, we must process the empty edit
// first.
if cmp := comparePos(ei.Start, ej.Start); cmp != 0 {
return cmp < 0
}
return comparePos(ei.End, ej.End) < 0
})
// Validate edits.
for _, edit := range newEdits {
if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) {
return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start)
}
if !inText(edit.Start, content) {
return nil, fmt.Errorf("start position %v is out of bounds", edit.Start)
}
if !inText(edit.End, content) {
return nil, fmt.Errorf("end position %v is out of bounds", edit.End)
}
}
var (
b strings.Builder
line, column int
)
advance := func(toLine, toColumn int) {
for ; line < toLine; line++ {
b.WriteString(string([]rune(content[line])[column:]) + "\n")
column = 0
}
b.WriteString(string([]rune(content[line])[column:toColumn]))
column = toColumn
}
for _, edit := range newEdits {
advance(edit.Start.Line, edit.Start.Column)
b.WriteString(edit.Text)
line = edit.End.Line
column = edit.End.Column
}
advance(len(content)-1, len([]rune(content[len(content)-1])))
return strings.Split(b.String(), "\n"), nil
}
// comparePos returns -1 if left < right, 0 if left == right, and 1 if left > right.
func comparePos(left, right Pos) int {
if left.Line < right.Line {
return -1
}
if left.Line > right.Line {
return 1
}
if left.Column < right.Column {
return -1
}
if left.Column > right.Column {
return 1
}
return 0
}