| 🛟 Updated. 28d5985 k33g 21h ago | 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "unicode/utf16" |
| 6 | ) |
| 7 | |
| 8 | // Position is a place in a document, as the protocol counts them: a zero-based |
| 9 | // line, and a character offset counted in **UTF-16 code units**. |
| 10 | // |
| 11 | // That last part is the trap. The editor counts columns in runes, so every |
| 12 | // position crossing this boundary goes through RuneToUTF16 or UTF16ToRune. On |
| 13 | // plain ASCII the two agree, which is exactly why getting it wrong survives |
| 14 | // testing until someone opens a file with an accent in it. |
| 15 | type Position struct { |
| 16 | Line int `json:"line"` |
| 17 | Character int `json:"character"` |
| 18 | } |
| 19 | |
| 20 | // Range is a span of a document. |
| 21 | type Range struct { |
| 22 | Start Position `json:"start"` |
| 23 | End Position `json:"end"` |
| 24 | } |
| 25 | |
| 26 | // Location is a range in a named document. |
| 27 | type Location struct { |
| 28 | URI string `json:"uri"` |
| 29 | Range Range `json:"range"` |
| 30 | } |
| 31 | |
| 32 | // TextDocumentIdentifier names a document. |
| 33 | type TextDocumentIdentifier struct { |
| 34 | URI string `json:"uri"` |
| 35 | } |
| 36 | |
| 37 | // VersionedTextDocumentIdentifier names a document and says which revision of |
| 38 | // it is being talked about. |
| 39 | type VersionedTextDocumentIdentifier struct { |
| 40 | URI string `json:"uri"` |
| 41 | Version int `json:"version"` |
| 42 | } |
| 43 | |
| 44 | // TextDocumentItem is a document being opened, content included. |
| 45 | type TextDocumentItem struct { |
| 46 | URI string `json:"uri"` |
| 47 | LanguageID string `json:"languageId"` |
| 48 | Version int `json:"version"` |
| 49 | Text string `json:"text"` |
| 50 | } |
| 51 | |
| 52 | // TextDocumentPositionParams is the shape most requests take: a document and a |
| 53 | // place in it. |
| 54 | type TextDocumentPositionParams struct { |
| 55 | TextDocument TextDocumentIdentifier `json:"textDocument"` |
| 56 | Position Position `json:"position"` |
| 57 | } |
| 58 | |
| 59 | // DidOpenParams announces a newly opened document. |
| 60 | type DidOpenParams struct { |
| 61 | TextDocument TextDocumentItem `json:"textDocument"` |
| 62 | } |
| 63 | |
| 64 | // DidChangeParams carries a new version of a document. |
| 65 | type DidChangeParams struct { |
| 66 | TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` |
| 67 | ContentChanges []ContentChange `json:"contentChanges"` |
| 68 | } |
| 69 | |
| 70 | // ContentChange is one edit. The editor always sends the whole document, so |
| 71 | // only Text is ever set: the servers this talks to are fast enough that |
| 72 | // incremental sync would buy |
| 73 | // complexity rather than speed. |
| 74 | type ContentChange struct { |
| 75 | Text string `json:"text"` |
| 76 | } |
| 77 | |
| 78 | // DidCloseParams announces that a document is no longer open. |
| 79 | type DidCloseParams struct { |
| 80 | TextDocument TextDocumentIdentifier `json:"textDocument"` |
| 81 | } |
| 82 | |
| 83 | // DidSaveParams announces that a document has been written to disk. |
| 84 | type DidSaveParams struct { |
| 85 | TextDocument TextDocumentIdentifier `json:"textDocument"` |
| 86 | Text string `json:"text,omitempty"` |
| 87 | } |
| 88 | |
| 89 | // Severity is how serious a diagnostic is. |
| 90 | type Severity int |
| 91 | |
| 92 | // The severities the protocol defines. |
| 93 | const ( |
| 94 | SeverityError Severity = 1 |
| 95 | SeverityWarning Severity = 2 |
| 96 | SeverityInformation Severity = 3 |
| 97 | SeverityHint Severity = 4 |
| 98 | ) |
| 99 | |
| 100 | // Diagnostic is one problem the server found in a document. |
| 101 | type Diagnostic struct { |
| 102 | Range Range `json:"range"` |
| 103 | Severity Severity `json:"severity,omitempty"` |
| 104 | Source string `json:"source,omitempty"` |
| 105 | Message string `json:"message"` |
| 106 | } |
| 107 | |
| 108 | // PublishDiagnosticsParams is the notification carrying a document's problems. |
| 109 | type PublishDiagnosticsParams struct { |
| 110 | URI string `json:"uri"` |
| 111 | Diagnostics []Diagnostic `json:"diagnostics"` |
| 112 | } |
| 113 | |
| 114 | // CompletionItemKind says what sort of thing a completion is, which is what |
| 115 | // the little tag beside each entry shows. |
| 116 | type CompletionItemKind int |
| 117 | |
| 118 | // The kinds a language server actually returns for source code. |
| 119 | const ( |
| 120 | KindText CompletionItemKind = 1 |
| 121 | KindMethod CompletionItemKind = 2 |
| 122 | KindFunction CompletionItemKind = 3 |
| 123 | KindConstructor CompletionItemKind = 4 |
| 124 | KindField CompletionItemKind = 5 |
| 125 | KindVariable CompletionItemKind = 6 |
| 126 | KindClass CompletionItemKind = 7 |
| 127 | KindInterface CompletionItemKind = 8 |
| 128 | KindModule CompletionItemKind = 9 |
| 129 | KindProperty CompletionItemKind = 10 |
| 130 | KindKeyword CompletionItemKind = 14 |
| 131 | KindSnippet CompletionItemKind = 15 |
| 132 | KindConstant CompletionItemKind = 21 |
| 133 | KindStruct CompletionItemKind = 22 |
| 134 | KindTypeParameter CompletionItemKind = 25 |
| 135 | ) |
| 136 | |
| 137 | // kindNames are the short tags shown beside a completion. |
| 138 | var kindNames = map[CompletionItemKind]string{ |
| 139 | KindMethod: "method", |
| 140 | KindFunction: "func", |
| 141 | KindConstructor: "new", |
| 142 | KindField: "field", |
| 143 | KindVariable: "var", |
| 144 | KindClass: "type", |
| 145 | KindInterface: "iface", |
| 146 | KindModule: "pkg", |
| 147 | KindProperty: "prop", |
| 148 | KindKeyword: "kw", |
| 149 | KindSnippet: "snip", |
| 150 | KindConstant: "const", |
| 151 | KindStruct: "struct", |
| 152 | KindTypeParameter: "typar", |
| 153 | KindText: "text", |
| 154 | } |
| 155 | |
| 156 | // String returns the short tag for a kind, or "?" for one this editor does not |
| 157 | // name. |
| 158 | func (k CompletionItemKind) String() string { |
| 159 | if name, ok := kindNames[k]; ok { |
| 160 | return name |
| 161 | } |
| 162 | return "?" |
| 163 | } |
| 164 | |
| 165 | // CompletionItem is one entry of a completion list. |
| 166 | type CompletionItem struct { |
| 167 | Label string `json:"label"` |
| 168 | Kind CompletionItemKind `json:"kind,omitempty"` |
| 169 | Detail string `json:"detail,omitempty"` |
| 170 | SortText string `json:"sortText,omitempty"` |
| 171 | FilterText string `json:"filterText,omitempty"` |
| 172 | InsertText string `json:"insertText,omitempty"` |
| 173 | } |
| 174 | |
| 175 | // Insertion returns the text to put into the buffer when this item is chosen. |
| 176 | // |
| 177 | // A snippet's placeholder syntax is stripped: the editor has nowhere to put |
| 178 | // the tab stops, and leaving "${1:x}" in the source would be worse than |
| 179 | // leaving it out. |
| 180 | func (i CompletionItem) Insertion() string { |
| 181 | text := i.InsertText |
| 182 | if text == "" { |
| 183 | text = i.Label |
| 184 | } |
| 185 | return stripSnippetPlaceholders(text) |
| 186 | } |
| 187 | |
| 188 | // stripSnippetPlaceholders removes "$0", "${1:name}" and the like, keeping the |
| 189 | // default text a placeholder proposes. |
| 190 | func stripSnippetPlaceholders(text string) string { |
| 191 | var out strings.Builder |
| 192 | |
| 193 | for i := 0; i < len(text); i++ { |
| 194 | if text[i] != '$' { |
| 195 | out.WriteByte(text[i]) |
| 196 | continue |
| 197 | } |
| 198 | i += skipPlaceholder(text[i:], &out) - 1 |
| 199 | } |
| 200 | return out.String() |
| 201 | } |
| 202 | |
| 203 | // skipPlaceholder writes the useful part of the placeholder starting at text |
| 204 | // and returns how many bytes of it to skip. |
| 205 | func skipPlaceholder(text string, out *strings.Builder) int { |
| 206 | if len(text) > 1 && text[1] == '{' { |
| 207 | end := strings.IndexByte(text, '}') |
| 208 | if end < 0 { |
| 209 | return len(text) |
| 210 | } |
| 211 | // "${1:name}" proposes "name"; "${1}" proposes nothing. |
| 212 | if colon := strings.IndexByte(text[:end], ':'); colon >= 0 { |
| 213 | out.WriteString(text[colon+1 : end]) |
| 214 | } |
| 215 | return end + 1 |
| 216 | } |
| 217 | |
| 218 | skip := 1 |
| 219 | for skip < len(text) && text[skip] >= '0' && text[skip] <= '9' { |
| 220 | skip++ |
| 221 | } |
| 222 | return skip |
| 223 | } |
| 224 | |
| 225 | // CompletionList is what a completion request answers with. |
| 226 | type CompletionList struct { |
| 227 | IsIncomplete bool `json:"isIncomplete"` |
| 228 | Items []CompletionItem `json:"items"` |
| 229 | } |
| 230 | |
| 231 | // MarkupContent is a piece of documentation, in plain text or Markdown. |
| 232 | type MarkupContent struct { |
| 233 | Kind string `json:"kind"` |
| 234 | Value string `json:"value"` |
| 235 | } |
| 236 | |
| 237 | // Hover is what a hover request answers with. |
| 238 | type Hover struct { |
| 239 | Contents MarkupContent `json:"contents"` |
| 240 | Range *Range `json:"range,omitempty"` |
| 241 | } |
| 242 | |
| 243 | // RuneToUTF16 converts a rune column on a line into the UTF-16 code-unit |
| 244 | // offset the protocol wants. |
| 245 | // |
| 246 | // lsp.RuneToUTF16("héllo", 3) // 3 — é is one UTF-16 unit |
| 247 | // lsp.RuneToUTF16("𝄞x", 1) // 2 — the clef needs a surrogate pair |
| 248 | func RuneToUTF16(line string, runeColumn int) int { |
| 249 | units := 0 |
| 250 | for i, r := range []rune(line) { |
| 251 | if i >= runeColumn { |
| 252 | break |
| 253 | } |
| 254 | units += utf16Len(r) |
| 255 | } |
| 256 | return units |
| 257 | } |
| 258 | |
| 259 | // UTF16ToRune converts a UTF-16 code-unit offset into a rune column, which is |
| 260 | // what the editor counts in. |
| 261 | func UTF16ToRune(line string, utf16Column int) int { |
| 262 | units := 0 |
| 263 | for i, r := range []rune(line) { |
| 264 | if units >= utf16Column { |
| 265 | return i |
| 266 | } |
| 267 | units += utf16Len(r) |
| 268 | } |
| 269 | return len([]rune(line)) |
| 270 | } |
| 271 | |
| 272 | // utf16Len returns how many UTF-16 code units a rune needs: two for anything |
| 273 | // outside the basic multilingual plane, one otherwise. |
| 274 | func utf16Len(r rune) int { |
| 275 | if r > 0xFFFF { |
| 276 | return len(utf16.Encode([]rune{r})) |
| 277 | } |
| 278 | return 1 |
| 279 | } |