LSP-server-example
220 строк · 6.9 Кб
1// //nolint
2
3// Copyright 2023 The Go Authors. All rights reserved.
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7package protocol
8
9// This file declares URI, DocumentURI, and its methods.
10//
11// For the LSP definition of these types, see
12// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri
13
14import (
15"fmt"
16"net/url"
17"path/filepath"
18"strings"
19"unicode"
20)
21
22// A DocumentURI is the URI of a client editor document.
23//
24// According to the LSP specification:
25//
26// Care should be taken to handle encoding in URIs. For
27// example, some clients (such as VS Code) may encode colons
28// in drive letters while others do not. The URIs below are
29// both valid, but clients and servers should be consistent
30// with the form they use themselves to ensure the other party
31// doesn’t interpret them as distinct URIs. Clients and
32// servers should not assume that each other are encoding the
33// same way (for example a client encoding colons in drive
34// letters cannot assume server responses will have encoded
35// colons). The same applies to casing of drive letters - one
36// party should not assume the other party will return paths
37// with drive letters cased the same as it.
38//
39// file:///c:/project/readme.md
40// file:///C%3A/project/readme.md
41//
42// This is done during JSON unmarshalling;
43// see [DocumentURI.UnmarshalText] for details.
44type DocumentURI string
45
46// A URI is an arbitrary URL (e.g. https), not necessarily a file.
47type URI = string
48
49// UnmarshalText implements decoding of DocumentURI values.
50//
51// In particular, it implements a systematic correction of various odd
52// features of the definition of DocumentURI in the LSP spec that
53// appear to be workarounds for bugs in VS Code. For example, it may
54// URI-encode the URI itself, so that colon becomes %3A, and it may
55// send file://foo.go URIs that have two slashes (not three) and no
56// hostname.
57//
58// We use UnmarshalText, not UnmarshalJSON, because it is called even
59// for non-addressable values such as keys and values of map[K]V,
60// where there is no pointer of type *K or *V on which to call
61// UnmarshalJSON. (See Go issue #28189 for more detail.)
62//
63// Non-empty DocumentURIs are valid "file"-scheme URIs.
64// The empty DocumentURI is valid.
65func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
66*uri, err = ParseDocumentURI(string(data))
67return
68}
69
70// Path returns the file path for the given URI.
71//
72// DocumentURI("").Path() returns the empty string.
73//
74// Path panics if called on a URI that is not a valid filename.
75func (uri DocumentURI) Path() string {
76filename, err := filename(uri)
77if err != nil {
78// e.g. ParseRequestURI failed.
79//
80// This can only affect DocumentURIs created by
81// direct string manipulation; all DocumentURIs
82// received from the client pass through
83// ParseRequestURI, which ensures validity.
84panic(err)
85}
86return filepath.FromSlash(filename)
87}
88
89// Dir returns the URI for the directory containing the receiver.
90func (uri DocumentURI) Dir() DocumentURI {
91// This function could be more efficiently implemented by avoiding any call
92// to Path(), but at least consolidates URI manipulation.
93return URIFromPath(filepath.Dir(uri.Path()))
94}
95
96// Encloses reports whether uri's path, considered as a sequence of segments,
97// is a prefix of file's path.
98func (uri DocumentURI) Encloses(file DocumentURI) bool {
99return InDir(uri.Path(), file.Path())
100}
101
102func filename(uri DocumentURI) (string, error) {
103if uri == "" {
104return "", nil
105}
106
107// This conservative check for the common case
108// of a simple non-empty absolute POSIX filename
109// avoids the allocation of a net.URL.
110if strings.HasPrefix(string(uri), "file:///") {
111rest := string(uri)[len("file://"):] // leave one slash
112for i := 0; i < len(rest); i++ {
113b := rest[i]
114// Reject these cases:
115if b < ' ' || b == 0x7f || // control character
116b == '%' || b == '+' || // URI escape
117b == ':' || // Windows drive letter
118b == '@' || b == '&' || b == '?' { // authority or query
119goto slow
120}
121}
122return rest, nil
123}
124slow:
125
126u, err := url.ParseRequestURI(string(uri))
127if err != nil {
128return "", err
129}
130if u.Scheme != fileScheme {
131return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
132}
133// If the URI is a Windows URI, we trim the leading "/" and uppercase
134// the drive letter, which will never be case sensitive.
135if isWindowsDriveURIPath(u.Path) {
136u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
137}
138
139return u.Path, nil
140}
141
142// ParseDocumentURI interprets a string as a DocumentURI, applying VS
143// Code workarounds; see [DocumentURI.UnmarshalText] for details.
144func ParseDocumentURI(s string) (DocumentURI, error) {
145if s == "" {
146return "", nil
147}
148
149if !strings.HasPrefix(s, "file://") {
150return "", fmt.Errorf("DocumentURI scheme is not 'file': %s", s)
151}
152
153// VS Code sends URLs with only two slashes,
154// which are invalid. golang/go#39789.
155if !strings.HasPrefix(s, "file:///") {
156s = "file:///" + s[len("file://"):]
157}
158
159// Even though the input is a URI, it may not be in canonical form. VS Code
160// in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
161path, err := url.PathUnescape(s[len("file://"):])
162if err != nil {
163return "", err
164}
165
166// File URIs from Windows may have lowercase drive letters.
167// Since drive letters are guaranteed to be case insensitive,
168// we change them to uppercase to remain consistent.
169// For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
170if isWindowsDriveURIPath(path) {
171path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
172}
173u := url.URL{Scheme: fileScheme, Path: path}
174return DocumentURI(u.String()), nil
175}
176
177// URIFromPath returns DocumentURI for the supplied file path.
178// Given "", it returns "".
179func URIFromPath(path string) DocumentURI {
180if path == "" {
181return ""
182}
183if !isWindowsDrivePath(path) {
184if abs, err := filepath.Abs(path); err == nil {
185path = abs
186}
187}
188// Check the file path again, in case it became absolute.
189if isWindowsDrivePath(path) {
190path = "/" + strings.ToUpper(string(path[0])) + path[1:]
191}
192path = filepath.ToSlash(path)
193u := url.URL{
194Scheme: fileScheme,
195Path: path,
196}
197return DocumentURI(u.String())
198}
199
200const fileScheme = "file"
201
202// isWindowsDrivePath returns true if the file path is of the form used by
203// Windows. We check if the path begins with a drive letter, followed by a ":".
204// For example: C:/x/y/z.
205func isWindowsDrivePath(path string) bool {
206if len(path) < 3 {
207return false
208}
209return unicode.IsLetter(rune(path[0])) && path[1] == ':'
210}
211
212// isWindowsDriveURIPath returns true if the file URI is of the format used by
213// Windows URIs. The url.Parse package does not specially handle Windows paths
214// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
215func isWindowsDriveURIPath(uri string) bool {
216if len(uri) < 4 {
217return false
218}
219return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
220}
221