@@ -84,7 +84,19 @@ public struct DocumentURI: Codable, Hashable, Sendable {
84
84
}
85
85
86
86
public init ( from decoder: Decoder ) throws {
87
- try self . init ( string: decoder. singleValueContainer ( ) . decode ( String . self) )
87
+ let string = try decoder. singleValueContainer ( ) . decode ( String . self)
88
+ guard let url = URL ( string: string) else {
89
+ throw FailedToConstructDocumentURIFromStringError ( string: string)
90
+ }
91
+ if url. query ( ) != nil , var urlComponents = URLComponents ( string: url. absoluteString) {
92
+ // See comment in `encode(to:)`
93
+ urlComponents. percentEncodedQuery = urlComponents. percentEncodedQuery!. removingPercentEncoding
94
+ if let rewrittenQuery = urlComponents. url {
95
+ self . init ( rewrittenQuery)
96
+ return
97
+ }
98
+ }
99
+ self . init ( url)
88
100
}
89
101
90
102
/// Equality check to handle escape sequences in file URLs.
@@ -97,7 +109,36 @@ public struct DocumentURI: Codable, Hashable, Sendable {
97
109
hasher. combine ( self . pseudoPath)
98
110
}
99
111
112
+ private static let additionalQueryEncodingCharacterSet = CharacterSet ( charactersIn: " ?=&% " ) . inverted
113
+
100
114
public func encode( to encoder: Encoder ) throws {
101
- try storage. absoluteString. encode ( to: encoder)
115
+ let urlToEncode : URL
116
+ if let query = storage. query ( percentEncoded: true ) , var components = URLComponents ( string: storage. absoluteString) {
117
+ // The URI standard RFC 3986 is ambiguous about whether percent encoding and their represented characters are
118
+ // considered equivalent. VS Code considers them equivalent and treats them the same:
119
+ //
120
+ // vscode.Uri.parse("x://a?b=xxxx%3Dyyyy").toString() -> 'x://a?b%3Dxxxx%3Dyyyy'
121
+ // vscode.Uri.parse("x://a?b=xxxx%3Dyyyy").toString(/*skipEncoding=*/true) -> 'x://a?b=xxxx=yyyy'
122
+ //
123
+ // This causes issues because SourceKit-LSP's macro expansion URLs encoded by URLComponents use `=` to denote the
124
+ // separation of a key and a value in the outer query. The value of the `parent` key may itself contain query
125
+ // items, which use the escaped form '%3D'. Simplified, such a URL may look like
126
+ // scheme://host?parent=scheme://host?line%3D2
127
+ // But after running this through VS Code's URI type `=` and `%3D` get canonicalized and are indistinguishable.
128
+ // To avoid this ambiguity, always percent escape the characters we use to distinguish URL query parameters,
129
+ // producing the following URL.
130
+ // scheme://host?parent%3Dscheme://host%3Fline%253D2
131
+ components. percentEncodedQuery =
132
+ query
133
+ . addingPercentEncoding ( withAllowedCharacters: Self . additionalQueryEncodingCharacterSet)
134
+ if let componentsUrl = components. url {
135
+ urlToEncode = componentsUrl
136
+ } else {
137
+ urlToEncode = self . storage
138
+ }
139
+ } else {
140
+ urlToEncode = self . storage
141
+ }
142
+ try urlToEncode. absoluteString. encode ( to: encoder)
102
143
}
103
144
}
0 commit comments