package lsp import ( "strings" "unicode/utf16" ) // Position is a place in a document, as the protocol counts them: a zero-based // line, and a character offset counted in **UTF-16 code units**. // // That last part is the trap. The editor counts columns in runes, so every // position crossing this boundary goes through RuneToUTF16 or UTF16ToRune. On // plain ASCII the two agree, which is exactly why getting it wrong survives // testing until someone opens a file with an accent in it. type Position struct { Line int `json:"line"` Character int `json:"character"` } // Range is a span of a document. type Range struct { Start Position `json:"start"` End Position `json:"end"` } // Location is a range in a named document. type Location struct { URI string `json:"uri"` Range Range `json:"range"` } // TextDocumentIdentifier names a document. type TextDocumentIdentifier struct { URI string `json:"uri"` } // VersionedTextDocumentIdentifier names a document and says which revision of // it is being talked about. type VersionedTextDocumentIdentifier struct { URI string `json:"uri"` Version int `json:"version"` } // TextDocumentItem is a document being opened, content included. type TextDocumentItem struct { URI string `json:"uri"` LanguageID string `json:"languageId"` Version int `json:"version"` Text string `json:"text"` } // TextDocumentPositionParams is the shape most requests take: a document and a // place in it. type TextDocumentPositionParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Position Position `json:"position"` } // DidOpenParams announces a newly opened document. type DidOpenParams struct { TextDocument TextDocumentItem `json:"textDocument"` } // DidChangeParams carries a new version of a document. type DidChangeParams struct { TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` ContentChanges []ContentChange `json:"contentChanges"` } // ContentChange is one edit. The editor always sends the whole document, so // only Text is ever set: the servers this talks to are fast enough that // incremental sync would buy // complexity rather than speed. type ContentChange struct { Text string `json:"text"` } // DidCloseParams announces that a document is no longer open. type DidCloseParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` } // DidSaveParams announces that a document has been written to disk. type DidSaveParams struct { TextDocument TextDocumentIdentifier `json:"textDocument"` Text string `json:"text,omitempty"` } // Severity is how serious a diagnostic is. type Severity int // The severities the protocol defines. const ( SeverityError Severity = 1 SeverityWarning Severity = 2 SeverityInformation Severity = 3 SeverityHint Severity = 4 ) // Diagnostic is one problem the server found in a document. type Diagnostic struct { Range Range `json:"range"` Severity Severity `json:"severity,omitempty"` Source string `json:"source,omitempty"` Message string `json:"message"` } // PublishDiagnosticsParams is the notification carrying a document's problems. type PublishDiagnosticsParams struct { URI string `json:"uri"` Diagnostics []Diagnostic `json:"diagnostics"` } // CompletionItemKind says what sort of thing a completion is, which is what // the little tag beside each entry shows. type CompletionItemKind int // The kinds a language server actually returns for source code. const ( KindText CompletionItemKind = 1 KindMethod CompletionItemKind = 2 KindFunction CompletionItemKind = 3 KindConstructor CompletionItemKind = 4 KindField CompletionItemKind = 5 KindVariable CompletionItemKind = 6 KindClass CompletionItemKind = 7 KindInterface CompletionItemKind = 8 KindModule CompletionItemKind = 9 KindProperty CompletionItemKind = 10 KindKeyword CompletionItemKind = 14 KindSnippet CompletionItemKind = 15 KindConstant CompletionItemKind = 21 KindStruct CompletionItemKind = 22 KindTypeParameter CompletionItemKind = 25 ) // kindNames are the short tags shown beside a completion. var kindNames = map[CompletionItemKind]string{ KindMethod: "method", KindFunction: "func", KindConstructor: "new", KindField: "field", KindVariable: "var", KindClass: "type", KindInterface: "iface", KindModule: "pkg", KindProperty: "prop", KindKeyword: "kw", KindSnippet: "snip", KindConstant: "const", KindStruct: "struct", KindTypeParameter: "typar", KindText: "text", } // String returns the short tag for a kind, or "?" for one this editor does not // name. func (k CompletionItemKind) String() string { if name, ok := kindNames[k]; ok { return name } return "?" } // CompletionItem is one entry of a completion list. type CompletionItem struct { Label string `json:"label"` Kind CompletionItemKind `json:"kind,omitempty"` Detail string `json:"detail,omitempty"` SortText string `json:"sortText,omitempty"` FilterText string `json:"filterText,omitempty"` InsertText string `json:"insertText,omitempty"` } // Insertion returns the text to put into the buffer when this item is chosen. // // A snippet's placeholder syntax is stripped: the editor has nowhere to put // the tab stops, and leaving "${1:x}" in the source would be worse than // leaving it out. func (i CompletionItem) Insertion() string { text := i.InsertText if text == "" { text = i.Label } return stripSnippetPlaceholders(text) } // stripSnippetPlaceholders removes "$0", "${1:name}" and the like, keeping the // default text a placeholder proposes. func stripSnippetPlaceholders(text string) string { var out strings.Builder for i := 0; i < len(text); i++ { if text[i] != '$' { out.WriteByte(text[i]) continue } i += skipPlaceholder(text[i:], &out) - 1 } return out.String() } // skipPlaceholder writes the useful part of the placeholder starting at text // and returns how many bytes of it to skip. func skipPlaceholder(text string, out *strings.Builder) int { if len(text) > 1 && text[1] == '{' { end := strings.IndexByte(text, '}') if end < 0 { return len(text) } // "${1:name}" proposes "name"; "${1}" proposes nothing. if colon := strings.IndexByte(text[:end], ':'); colon >= 0 { out.WriteString(text[colon+1 : end]) } return end + 1 } skip := 1 for skip < len(text) && text[skip] >= '0' && text[skip] <= '9' { skip++ } return skip } // CompletionList is what a completion request answers with. type CompletionList struct { IsIncomplete bool `json:"isIncomplete"` Items []CompletionItem `json:"items"` } // MarkupContent is a piece of documentation, in plain text or Markdown. type MarkupContent struct { Kind string `json:"kind"` Value string `json:"value"` } // Hover is what a hover request answers with. type Hover struct { Contents MarkupContent `json:"contents"` Range *Range `json:"range,omitempty"` } // RuneToUTF16 converts a rune column on a line into the UTF-16 code-unit // offset the protocol wants. // // lsp.RuneToUTF16("héllo", 3) // 3 — é is one UTF-16 unit // lsp.RuneToUTF16("𝄞x", 1) // 2 — the clef needs a surrogate pair func RuneToUTF16(line string, runeColumn int) int { units := 0 for i, r := range []rune(line) { if i >= runeColumn { break } units += utf16Len(r) } return units } // UTF16ToRune converts a UTF-16 code-unit offset into a rune column, which is // what the editor counts in. func UTF16ToRune(line string, utf16Column int) int { units := 0 for i, r := range []rune(line) { if units >= utf16Column { return i } units += utf16Len(r) } return len([]rune(line)) } // utf16Len returns how many UTF-16 code units a rune needs: two for anything // outside the basic multilingual plane, one otherwise. func utf16Len(r rune) int { if r > 0xFFFF { return len(utf16.Encode([]rune{r})) } return 1 }