| 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "net/url" |
| 6 | "path/filepath" |
| 7 | "runtime" |
| 8 | "strings" |
| 9 | "unicode/utf16" |
| 10 | ) |
| 11 | |
| 12 | // Position is a zero-based LSP position. Character is counted in the encoding the |
| 13 | // server negotiated at initialize (utf-16 by default, utf-8 when both sides |
| 14 | // agree). |
| 15 | type Position struct { |
| 16 | Line int `json:"line"` |
| 17 | Character int `json:"character"` |
| 18 | } |
| 19 | |
| 20 | // Range is a half-open span between two positions. |
| 21 | type Range struct { |
| 22 | Start Position `json:"start"` |
| 23 | End Position `json:"end"` |
| 24 | } |
| 25 | |
| 26 | // Location is a file URI plus a range, the shape definition/references return. |
| 27 | type Location struct { |
| 28 | URI string `json:"uri"` |
| 29 | Range Range `json:"range"` |
| 30 | } |
| 31 | |
| 32 | func pathToURI(p string) string { |
| 33 | p = filepath.ToSlash(p) |
| 34 | if runtime.GOOS == "windows" && len(p) > 1 && p[1] == ':' { |
| 35 | p = "/" + p // C:/x → /C:/x so the URI becomes file:///C:/x |
| 36 | } |
| 37 | u := url.URL{Scheme: "file", Path: p} |
| 38 | return u.String() |
| 39 | } |
| 40 | |
| 41 | func uriToPath(uri string) string { |
| 42 | u, err := url.Parse(uri) |
| 43 | if err != nil { |
| 44 | return uri |
| 45 | } |
| 46 | p := u.Path |
| 47 | if runtime.GOOS == "windows" && len(p) > 2 && p[0] == '/' && p[2] == ':' { |
| 48 | p = p[1:] |
| 49 | } |
| 50 | return filepath.FromSlash(p) |
| 51 | } |
| 52 | |
| 53 | // locate finds symbol on the 1-based line of content and returns the LSP position |
| 54 | // of its first byte, converting the byte column into the server's encoding. |
| 55 | func locate(content string, line1 int, symbol, enc string) (Position, error) { |
| 56 | lines := strings.Split(content, "\n") |
| 57 | if line1 < 1 || line1 > len(lines) { |
| 58 | return Position{}, fmt.Errorf("line %d out of range (file has %d lines)", line1, len(lines)) |
| 59 | } |
| 60 | text := strings.TrimSuffix(lines[line1-1], "\r") |
| 61 | col := strings.Index(text, symbol) |
| 62 | if col < 0 { |
| 63 | return Position{}, fmt.Errorf("symbol %q not found on line %d", symbol, line1) |
| 64 | } |
| 65 | return Position{Line: line1 - 1, Character: encodeChar(text[:col], enc)}, nil |
| 66 | } |
| 67 | |
| 68 | func encodeChar(prefix, enc string) int { |
| 69 | if enc == encodingUTF8 { |
| 70 | return len(prefix) |
| 71 | } |
| 72 | return len(utf16.Encode([]rune(prefix))) |
| 73 | } |
| 74 | |
| 75 | const ( |
| 76 | encodingUTF8 = "utf-8" |
| 77 | encodingUTF16 = "utf-16" |
| 78 | ) |
| 79 |