php_lsp/backend/
drive_letter.rs1use std::collections::HashMap;
9use std::pin::Pin;
10use std::sync::{Arc, Mutex};
11
12use futures::{Stream, StreamExt};
13use serde_json::{Map, Value};
14use tower_lsp_server::jsonrpc::{Request, Response};
15use tower_lsp_server::{ClientSocket, Loopback};
16
17#[derive(Clone, Default)]
19pub(super) struct ClientSpellings(Arc<Mutex<HashMap<String, String>>>);
20
21impl ClientSpellings {
22 pub(super) fn normalize_request(&self, request: Request) -> Request {
25 if request.params().is_none_or(|p| !contains_drive_uri(p)) {
26 return request;
27 }
28 let mut seen = Vec::new();
29 let request = map_params(request, |params| normalize_value(params, &mut seen));
30 let mut spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
31 for (canonical, original) in seen {
32 spellings.entry(canonical).or_insert(original);
33 }
34 request
35 }
36
37 pub(super) fn restore_request(&self, request: Request) -> Request {
39 let spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
40 if spellings.is_empty() || request.params().is_none() {
41 return request;
42 }
43 map_params(request, |params| restore_value(params, &spellings))
44 }
45
46 pub(super) fn restore_response(&self, response: Response) -> Response {
48 let spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
49 if spellings.is_empty() {
50 return response;
51 }
52 let (id, body) = response.into_parts();
53 let body = body.map(|mut result| {
54 restore_value(&mut result, &spellings);
55 result
56 });
57 Response::from_parts(id, body)
58 }
59}
60
61pub struct ClientLoopback {
63 pub(super) socket: ClientSocket,
64 pub(super) spellings: ClientSpellings,
65}
66
67impl Loopback for ClientLoopback {
68 type RequestStream = Pin<Box<dyn Stream<Item = Request> + Send>>;
69 type ResponseSink = <ClientSocket as Loopback>::ResponseSink;
70
71 fn split(self) -> (Self::RequestStream, Self::ResponseSink) {
72 let (requests, responses) = Loopback::split(self.socket);
73 let spellings = self.spellings;
74 (
75 Box::pin(requests.map(move |request| spellings.restore_request(request))),
76 responses,
77 )
78 }
79}
80
81fn map_params(request: Request, f: impl FnOnce(&mut Value)) -> Request {
82 let (method, id, params) = request.into_parts();
83 let mut builder = Request::build(method);
84 if let Some(mut params) = params {
85 f(&mut params);
86 builder = builder.params(params);
87 }
88 if let Some(id) = id {
89 builder = builder.id(id);
90 }
91 builder.finish()
92}
93
94fn is_uri_key(key: &str) -> bool {
95 key.len() >= 3 && key[key.len() - 3..].eq_ignore_ascii_case("uri")
96}
97
98fn contains_drive_uri(value: &Value) -> bool {
101 match value {
102 Value::Array(items) => items.iter().any(contains_drive_uri),
103 Value::Object(map) => map.iter().any(|(key, v)| match v {
104 Value::String(s) => is_uri_key(key) && normalized_drive_uri(s).is_some(),
105 _ => contains_drive_uri(v),
106 }),
107 _ => false,
108 }
109}
110
111fn normalize_value(value: &mut Value, seen: &mut Vec<(String, String)>) {
113 match value {
114 Value::Array(items) => items.iter_mut().for_each(|v| normalize_value(v, seen)),
115 Value::Object(map) => {
116 for (key, v) in map.iter_mut() {
117 match v {
118 Value::String(s) if is_uri_key(key) => {
119 if let Some(normalized) = normalized_drive_uri(s) {
120 let original = std::mem::replace(s, normalized.clone());
121 seen.push((normalized, original));
122 }
123 }
124 _ => normalize_value(v, seen),
125 }
126 }
127 }
128 _ => {}
129 }
130}
131
132fn restore_value(value: &mut Value, spellings: &HashMap<String, String>) {
135 match value {
136 Value::Array(items) => items.iter_mut().for_each(|v| restore_value(v, spellings)),
137 Value::Object(map) => {
138 for (key, v) in map.iter_mut() {
139 match v {
140 Value::String(s) if is_uri_key(key) => {
141 if let Some(original) = spellings.get(s.as_str()) {
142 s.clone_from(original);
143 }
144 }
145 Value::Object(changes) if key == "changes" => {
146 *changes = std::mem::take(changes)
147 .into_iter()
148 .map(|(uri, mut edits)| {
149 restore_value(&mut edits, spellings);
150 (spellings.get(&uri).cloned().unwrap_or(uri), edits)
151 })
152 .collect::<Map<_, _>>();
153 }
154 _ => restore_value(v, spellings),
155 }
156 }
157 }
158 _ => {}
159 }
160}
161
162fn normalized_drive_uri(s: &str) -> Option<String> {
164 let rest = s.strip_prefix("file:///")?;
165 let drive = *rest.as_bytes().first()?;
166 if !drive.is_ascii_alphabetic() {
167 return None;
168 }
169 let after = &rest[1..];
170 let tail = after
171 .strip_prefix(':')
172 .or_else(|| after.strip_prefix("%3A"))
173 .or_else(|| after.strip_prefix("%3a"))?;
174 if !(tail.is_empty() || tail.starts_with('/')) {
175 return None;
176 }
177 let normalized = format!("file:///{}%3A{tail}", drive.to_ascii_uppercase() as char);
178 (normalized != s).then_some(normalized)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn drive_spellings_normalize_to_upper_case_encoded_colon() {
187 for input in [
188 "file:///c%3A/proj/a.php",
189 "file:///c:/proj/a.php",
190 "file:///C:/proj/a.php",
191 "file:///c%3a/proj/a.php",
192 ] {
193 assert_eq!(
194 normalized_drive_uri(input).as_deref(),
195 Some("file:///C%3A/proj/a.php"),
196 "{input}"
197 );
198 }
199 assert_eq!(normalized_drive_uri("file:///C%3A/proj/a.php"), None);
200 assert_eq!(normalized_drive_uri("file:///home/a.php"), None);
201 assert_eq!(normalized_drive_uri("file:///ab:/x"), None);
202 assert_eq!(normalized_drive_uri("<?php file:///c:/x"), None);
203 }
204
205 #[test]
206 fn document_text_holding_a_uri_is_untouched() {
207 let params = serde_json::json!({
208 "textDocument": {"uri": "file:///c:/a.php", "version": 2},
209 "contentChanges": [{"text": "file:///c:/pasted"}]
210 });
211 let request = Request::build("textDocument/didChange")
212 .params(params)
213 .finish();
214 let params = ClientSpellings::default()
215 .normalize_request(request)
216 .into_parts()
217 .2
218 .unwrap();
219 assert_eq!(params["textDocument"]["uri"], "file:///C%3A/a.php");
220 assert_eq!(params["contentChanges"][0]["text"], "file:///c:/pasted");
221 }
222
223 #[test]
224 fn nested_params_are_rewritten() {
225 let request = Request::build("workspace/didRenameFiles")
226 .params(serde_json::json!({
227 "files": [{"oldUri": "file:///d:/a.php", "newUri": "file:///d%3A/b.php"}]
228 }))
229 .finish();
230 let params = ClientSpellings::default()
231 .normalize_request(request)
232 .into_parts()
233 .2
234 .unwrap();
235 assert_eq!(
236 params,
237 serde_json::json!({
238 "files": [{"oldUri": "file:///D%3A/a.php", "newUri": "file:///D%3A/b.php"}]
239 })
240 );
241 }
242
243 #[test]
244 fn outgoing_uris_use_the_client_spelling() {
245 let spellings = ClientSpellings::default();
246 spellings.normalize_request(
247 Request::build("textDocument/didOpen")
248 .params(serde_json::json!({"textDocument": {"uri": "file:///c%3A/a.php"}}))
249 .finish(),
250 );
251 let request = Request::build("workspace/applyEdit")
252 .params(serde_json::json!({
253 "edit": {"changes": {"file:///C%3A/a.php": [], "file:///C%3A/b.php": []}},
254 "uri": "file:///C%3A/a.php"
255 }))
256 .finish();
257 let params = spellings.restore_request(request).into_parts().2.unwrap();
258 assert_eq!(
259 params,
260 serde_json::json!({
261 "edit": {"changes": {"file:///c%3A/a.php": [], "file:///C%3A/b.php": []}},
262 "uri": "file:///c%3A/a.php"
263 })
264 );
265 }
266}