github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/fake/edit.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 "fmt" 9 "sort" 10 "strings" 11 12 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 13 ) 14 15 // Pos represents a position in a text buffer. Both Line and Column are 16 // 0-indexed. 17 type Pos struct { 18 Line, Column int 19 } 20 21 func (p Pos) String() string { 22 return fmt.Sprintf("%v:%v", p.Line, p.Column) 23 } 24 25 // Range corresponds to protocol.Range, but uses the editor friend Pos 26 // instead of UTF-16 oriented protocol.Position 27 type Range struct { 28 Start Pos 29 End Pos 30 } 31 32 func (p Pos) ToProtocolPosition() protocol.Position { 33 return protocol.Position{ 34 Line: uint32(p.Line), 35 Character: uint32(p.Column), 36 } 37 } 38 39 func fromProtocolPosition(pos protocol.Position) Pos { 40 return Pos{ 41 Line: int(pos.Line), 42 Column: int(pos.Character), 43 } 44 } 45 46 // Edit represents a single (contiguous) buffer edit. 47 type Edit struct { 48 Start, End Pos 49 Text string 50 } 51 52 // Location is the editor friendly equivalent of protocol.Location 53 type Location struct { 54 Path string 55 Range Range 56 } 57 58 // SymbolInformation is an editor friendly version of 59 // protocol.SymbolInformation, with location information transformed to byte 60 // offsets. Field names correspond to the protocol type. 61 type SymbolInformation struct { 62 Name string 63 Kind protocol.SymbolKind 64 Location Location 65 } 66 67 // NewEdit creates an edit replacing all content between 68 // (startLine, startColumn) and (endLine, endColumn) with text. 69 func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit { 70 return Edit{ 71 Start: Pos{Line: startLine, Column: startColumn}, 72 End: Pos{Line: endLine, Column: endColumn}, 73 Text: text, 74 } 75 } 76 77 func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent { 78 return protocol.TextDocumentContentChangeEvent{ 79 Range: &protocol.Range{ 80 Start: e.Start.ToProtocolPosition(), 81 End: e.End.ToProtocolPosition(), 82 }, 83 Text: e.Text, 84 } 85 } 86 87 func fromProtocolTextEdit(textEdit protocol.TextEdit) Edit { 88 return Edit{ 89 Start: fromProtocolPosition(textEdit.Range.Start), 90 End: fromProtocolPosition(textEdit.Range.End), 91 Text: textEdit.NewText, 92 } 93 } 94 95 // inText reports whether p is a valid position in the text buffer. 96 func inText(p Pos, content []string) bool { 97 if p.Line < 0 || p.Line >= len(content) { 98 return false 99 } 100 // Note the strict right bound: the column indexes character _separators_, 101 // not characters. 102 if p.Column < 0 || p.Column > len([]rune(content[p.Line])) { 103 return false 104 } 105 return true 106 } 107 108 // editContent implements a simplistic, inefficient algorithm for applying text 109 // edits to our buffer representation. It returns an error if the edit is 110 // invalid for the current content. 111 func editContent(content []string, edits []Edit) ([]string, error) { 112 newEdits := make([]Edit, len(edits)) 113 copy(newEdits, edits) 114 sort.Slice(newEdits, func(i, j int) bool { 115 if newEdits[i].Start.Line < newEdits[j].Start.Line { 116 return true 117 } 118 if newEdits[i].Start.Line > newEdits[j].Start.Line { 119 return false 120 } 121 return newEdits[i].Start.Column < newEdits[j].Start.Column 122 }) 123 124 // Validate edits. 125 for _, edit := range newEdits { 126 if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) { 127 return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start) 128 } 129 if !inText(edit.Start, content) { 130 return nil, fmt.Errorf("start position %v is out of bounds", edit.Start) 131 } 132 if !inText(edit.End, content) { 133 return nil, fmt.Errorf("end position %v is out of bounds", edit.End) 134 } 135 } 136 137 var ( 138 b strings.Builder 139 line, column int 140 ) 141 advance := func(toLine, toColumn int) { 142 for ; line < toLine; line++ { 143 b.WriteString(string([]rune(content[line])[column:]) + "\n") 144 column = 0 145 } 146 b.WriteString(string([]rune(content[line])[column:toColumn])) 147 column = toColumn 148 } 149 for _, edit := range newEdits { 150 advance(edit.Start.Line, edit.Start.Column) 151 b.WriteString(edit.Text) 152 line = edit.End.Line 153 column = edit.End.Column 154 } 155 advance(len(content)-1, len([]rune(content[len(content)-1]))) 156 return strings.Split(b.String(), "\n"), nil 157 }