@@ -84,7 +84,19 @@ public struct DocumentURI: Codable, Hashable, Sendable {
8484 }
8585
8686 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)
88100 }
89101
90102 /// Equality check to handle escape sequences in file URLs.
@@ -97,7 +109,36 @@ public struct DocumentURI: Codable, Hashable, Sendable {
97109 hasher. combine ( self . pseudoPath)
98110 }
99111
112+ private static let additionalQueryEncodingCharacterSet = CharacterSet ( charactersIn: " ?=&% " ) . inverted
113+
100114 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)
102143 }
103144}
0 commit comments