Skip to content

Commit 8ca1bda

Browse files
authored
Fix URI parsing/formatting (#969)
1 parent d6ffd82 commit 8ca1bda

File tree

3 files changed

+149
-21
lines changed

3 files changed

+149
-21
lines changed

internal/ls/converters.go

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/microsoft/typescript-go/internal/core"
1212
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
13+
"github.com/microsoft/typescript-go/internal/tspath"
1314
)
1415

1516
type Converters struct {
@@ -80,23 +81,14 @@ func LanguageKindToScriptKind(languageID lsproto.LanguageKind) core.ScriptKind {
8081
}
8182

8283
func DocumentURIToFileName(uri lsproto.DocumentUri) string {
83-
uriStr := string(uri)
84-
if strings.HasPrefix(uriStr, "file:///") {
85-
path := uriStr[7:]
86-
if len(path) >= 4 {
87-
if nextSlash := strings.IndexByte(path[1:], '/'); nextSlash != -1 {
88-
if possibleDrive, _ := url.PathUnescape(path[1 : nextSlash+2]); strings.HasSuffix(possibleDrive, ":/") {
89-
return possibleDrive + path[nextSlash+2:]
90-
}
91-
}
84+
parsed := core.Must(url.Parse(string(uri)))
85+
if parsed.Scheme == "file" {
86+
if parsed.Host != "" {
87+
return "//" + parsed.Host + parsed.Path
9288
}
93-
return path
89+
return fixWindowsURIPath(parsed.Path)
9490
}
95-
if strings.HasPrefix(uriStr, "file://") {
96-
// UNC path
97-
return uriStr[5:]
98-
}
99-
parsed := core.Must(url.Parse(uriStr))
91+
10092
authority := parsed.Host
10193
if authority == "" {
10294
authority = "ts-nul-authority"
@@ -108,21 +100,76 @@ func DocumentURIToFileName(uri lsproto.DocumentUri) string {
108100
if !strings.HasPrefix(path, "/") {
109101
path = "/" + path
110102
}
103+
path = fixWindowsURIPath(path)
104+
if !strings.HasPrefix(path, "/") {
105+
path = "/" + path
106+
}
111107
fragment := parsed.Fragment
112108
if fragment != "" {
113109
fragment = "#" + fragment
114110
}
115111
return fmt.Sprintf("^/%s/%s%s%s", parsed.Scheme, authority, path, fragment)
116112
}
117113

114+
func fixWindowsURIPath(path string) string {
115+
if rest, ok := strings.CutPrefix(path, "/"); ok {
116+
if volume, rest, ok := splitVolumePath(rest); ok {
117+
return volume + rest
118+
}
119+
}
120+
return path
121+
}
122+
123+
func splitVolumePath(path string) (volume string, rest string, ok bool) {
124+
if len(path) >= 2 && tspath.IsVolumeCharacter(path[0]) && path[1] == ':' {
125+
return strings.ToLower(path[0:2]), path[2:], true
126+
}
127+
return "", path, false
128+
}
129+
130+
// https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455
131+
var extraEscapeReplacer = strings.NewReplacer(
132+
":", "%3A",
133+
"/", "%2F",
134+
"?", "%3F",
135+
"#", "%23",
136+
"[", "%5B",
137+
"]", "%5D",
138+
"@", "%40",
139+
140+
"!", "%21",
141+
"$", "%24",
142+
"&", "%26",
143+
"'", "%27",
144+
"(", "%28",
145+
")", "%29",
146+
"*", "%2A",
147+
"+", "%2B",
148+
",", "%2C",
149+
";", "%3B",
150+
"=", "%3D",
151+
152+
" ", "%20",
153+
)
154+
118155
func FileNameToDocumentURI(fileName string) lsproto.DocumentUri {
119156
if strings.HasPrefix(fileName, "^/") {
120157
return lsproto.DocumentUri(strings.Replace(fileName[2:], "/ts-nul-authority/", ":", 1))
121158
}
122-
if firstSlash := strings.IndexByte(fileName, '/'); firstSlash > 0 && fileName[firstSlash-1] == ':' {
123-
return lsproto.DocumentUri("file:///" + url.PathEscape(fileName[:firstSlash]) + fileName[firstSlash:])
159+
160+
volume, fileName, _ := splitVolumePath(fileName)
161+
if volume != "" {
162+
volume = "/" + extraEscapeReplacer.Replace(volume)
124163
}
125-
return lsproto.DocumentUri("file://" + fileName)
164+
165+
fileName = strings.TrimPrefix(fileName, "//")
166+
167+
parts := strings.Split(fileName, "/")
168+
for i, part := range parts {
169+
parts[i] = extraEscapeReplacer.Replace(url.PathEscape(part))
170+
}
171+
172+
return lsproto.DocumentUri("file://" + volume + strings.Join(parts, "/"))
126173
}
127174

128175
func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter lsproto.Position) core.TextPos {

internal/ls/converters_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package ls_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/ls"
7+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
8+
"gotest.tools/v3/assert"
9+
)
10+
11+
func TestDocumentURIToFileName(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
uri lsproto.DocumentUri
16+
fileName string
17+
}{
18+
{"file:///path/to/file.ts", "/path/to/file.ts"},
19+
{"file://server/share/file.ts", "//server/share/file.ts"},
20+
{"file:///d%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
21+
{"file:///D%3A/work/tsgo932/lib/utils.ts", "d:/work/tsgo932/lib/utils.ts"},
22+
{"file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx", "d:/work/tsgo932/app/(test)/comp/comp-test.tsx"},
23+
{"file:///path/to/file.ts#section", "/path/to/file.ts"},
24+
{"file:///c:/test/me", "c:/test/me"},
25+
{"file://shares/files/c%23/p.cs", "//shares/files/c#/p.cs"},
26+
{"file:///c:/Source/Z%C3%BCrich%20or%20Zurich%20(%CB%88zj%CA%8A%C9%99r%C9%AAk,/Code/resources/app/plugins/c%23/plugin.json", "c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json"},
27+
{"file:///c:/test %25/path", "c:/test %/path"},
28+
// {"file:?q", "/"},
29+
{"file:///_:/path", "/_:/path"},
30+
{"file:///users/me/c%23-projects/", "/users/me/c#-projects/"},
31+
{"file://localhost/c%24/GitDevelopment/express", "//localhost/c$/GitDevelopment/express"},
32+
{"file:///c%3A/test%20with%20%2525/c%23code", "c:/test with %25/c#code"},
33+
34+
{"untitled:Untitled-1", "^/untitled/ts-nul-authority/Untitled-1"},
35+
{"untitled:Untitled-1#fragment", "^/untitled/ts-nul-authority/Untitled-1#fragment"},
36+
{"untitled:c:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"},
37+
{"untitled:C:/Users/jrieken/Code/abc.txt", "^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt"},
38+
}
39+
40+
for _, test := range tests {
41+
t.Run(string(test.uri), func(t *testing.T) {
42+
t.Parallel()
43+
assert.Equal(t, ls.DocumentURIToFileName(test.uri), test.fileName)
44+
})
45+
}
46+
}
47+
48+
func TestFileNameToDocumentURI(t *testing.T) {
49+
t.Parallel()
50+
51+
tests := []struct {
52+
fileName string
53+
uri lsproto.DocumentUri
54+
}{
55+
{"/path/to/file.ts", "file:///path/to/file.ts"},
56+
{"//server/share/file.ts", "file://server/share/file.ts"},
57+
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
58+
{"d:/work/tsgo932/lib/utils.ts", "file:///d%3A/work/tsgo932/lib/utils.ts"},
59+
{"d:/work/tsgo932/app/(test)/comp/comp-test.tsx", "file:///d%3A/work/tsgo932/app/%28test%29/comp/comp-test.tsx"},
60+
{"/path/to/file.ts", "file:///path/to/file.ts"},
61+
{"c:/test/me", "file:///c%3A/test/me"},
62+
{"//shares/files/c#/p.cs", "file://shares/files/c%23/p.cs"},
63+
{"c:/Source/Zürich or Zurich (ˈzjʊərɪk,/Code/resources/app/plugins/c#/plugin.json", "file:///c%3A/Source/Z%C3%BCrich%20or%20Zurich%20%28%CB%88zj%CA%8A%C9%99r%C9%AAk%2C/Code/resources/app/plugins/c%23/plugin.json"},
64+
{"c:/test %/path", "file:///c%3A/test%20%25/path"},
65+
{"/", "file:///"},
66+
{"/_:/path", "file:///_%3A/path"},
67+
{"/users/me/c#-projects/", "file:///users/me/c%23-projects/"},
68+
{"//localhost/c$/GitDevelopment/express", "file://localhost/c%24/GitDevelopment/express"},
69+
{"c:/test with %25/c#code", "file:///c%3A/test%20with%20%2525/c%23code"},
70+
71+
{"^/untitled/ts-nul-authority/Untitled-1", "untitled:Untitled-1"},
72+
{"^/untitled/ts-nul-authority/c:/Users/jrieken/Code/abc.txt", "untitled:c:/Users/jrieken/Code/abc.txt"},
73+
}
74+
75+
for _, test := range tests {
76+
t.Run(test.fileName, func(t *testing.T) {
77+
t.Parallel()
78+
assert.Equal(t, ls.FileNameToDocumentURI(test.fileName), test.uri)
79+
})
80+
}
81+
}

internal/tspath/path.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func pathComponents(path string, rootLength int) []string {
136136
return append([]string{root}, rest...)
137137
}
138138

139-
func isVolumeCharacter(char byte) bool {
139+
func IsVolumeCharacter(char byte) bool {
140140
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
141141
}
142142

@@ -180,7 +180,7 @@ func GetEncodedRootLength(path string) int {
180180
}
181181

182182
// DOS
183-
if isVolumeCharacter(ch0) && ln > 1 && path[1] == ':' {
183+
if IsVolumeCharacter(ch0) && ln > 1 && path[1] == ':' {
184184
if ln == 2 {
185185
return 2 // DOS: "c:" (but not "c:d")
186186
}
@@ -203,7 +203,7 @@ func GetEncodedRootLength(path string) int {
203203
// special case interpreted as "the machine from which the URL is being interpreted".
204204
scheme := path[:schemeEnd]
205205
authority := path[authorityStart:authorityEnd]
206-
if scheme == "file" && (authority == "" || authority == "localhost") && (len(path) > authorityEnd+2) && isVolumeCharacter(path[authorityEnd+1]) {
206+
if scheme == "file" && (authority == "" || authority == "localhost") && (len(path) > authorityEnd+2) && IsVolumeCharacter(path[authorityEnd+1]) {
207207
volumeSeparatorEnd := getFileUrlVolumeSeparatorEnd(path, authorityEnd+2)
208208
if volumeSeparatorEnd != -1 {
209209
if volumeSeparatorEnd == len(path) {

0 commit comments

Comments
 (0)