Skip to content

Commit 9735bb1

Browse files
committed
fix: handle multiple workspaceFolders and subdirs
Handle edge cases when workspaceFolders are something like: ``` \"workspaceFolders\": [ {\"uri\":\"file:///home/usr/app/apps\",\"name\":\"apps\"}, {\"uri\":\"file:///home/usr/app\",\"name\":\"app\"}]}}" ] where apps is a subdirectory and should be handled properly. Also, previous implementation doesn't capture multiple workspaces. Now that works properly.
1 parent 7206aba commit 9735bb1

File tree

4 files changed

+157
-13
lines changed

4 files changed

+157
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "lspdock"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
edition = "2024"
55

66
[dependencies]

src/lsp/binding.rs

Lines changed: 149 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -304,27 +304,56 @@ pub fn ensure_root(msg: &mut Bytes, config: &ProxyConfig) {
304304
let key = b"\"workspaceFolders\":[";
305305
if let Some(beg) = find(msg, key).map(|p| p + key.len())
306306
&& let Some(end) = find(&msg[beg..], b"]").map(|p| p + beg)
307-
&& let Some(ws) = patch_workspace_folders(&msg[beg..end], &docker_uri)
307+
&& let Some(ws) = patch_workspace_folders(&msg[beg..end], &docker_uri, &config.local_path)
308308
{
309309
let before = &msg[..beg];
310310
let after = &msg[end..];
311311
*msg = Bytes::from([before, &ws, after].concat());
312312
}
313313
}
314314

315-
fn patch_workspace_folders(msg: &[u8], docker_uri: &str) -> Option<Bytes> {
315+
fn patch_workspace_folders(msg: &[u8], docker_uri: &str, local_path: &str) -> Option<Bytes> {
316316
let key = b"\"uri\":\"";
317-
let mut result = None;
318-
for uri_beg in find_iter(msg, key) {
317+
318+
// Check if we have any matches before allocating
319+
let mut matches = find_iter(msg, key).peekable();
320+
matches.peek()?;
321+
322+
let local_path_uri = format!("file://{local_path}");
323+
324+
let mut new_bytes = Vec::new();
325+
// Cursor to track the position of replacements
326+
let mut last_pos = 0;
327+
328+
// Iterate and accumulate
329+
for uri_beg in matches {
319330
let beg = uri_beg + key.len();
320331
if let Some(end) = find(&msg[beg..], b"\"").map(|p| p + beg) {
321-
let before = &msg[..beg];
322-
let after = &msg[end..];
323-
result = Some(Bytes::from([before, docker_uri.as_bytes(), after].concat()));
332+
// Append everything from the last position up to the start of the URI value
333+
new_bytes.extend_from_slice(&msg[last_pos..beg]);
334+
335+
// Force the path to the directory, this occurres in VSCode when in some
336+
// cases the workspaceFolders is set to a relative dir like `file://app`
337+
new_bytes.extend_from_slice(docker_uri.as_bytes());
338+
339+
// First try to change the root dir if it is match with the local dir
340+
if let Some(pattern_init) =
341+
find(&msg[beg..end], local_path_uri.as_bytes()).map(|p| p + beg)
342+
{
343+
let pattern_end = pattern_init + local_path_uri.len();
344+
// Fill the gap, when local path is `/my/local/path` and root is `/my/local/path/subdir`
345+
// we add here `/subdir`
346+
new_bytes.extend_from_slice(&msg[pattern_end..end]);
347+
}
348+
349+
last_pos = end;
324350
}
325351
}
326352

327-
result
353+
// Append the remainder of the message
354+
new_bytes.extend_from_slice(&msg[last_pos..]);
355+
356+
Some(Bytes::from(new_bytes))
328357
}
329358

330359
#[cfg(test)]
@@ -362,8 +391,8 @@ mod tests {
362391

363392
// From Client to Server
364393

365-
let rq = lspmsg!("uri": "/test/path", "method": "text/document", "workspaceFolders": "[\"name\": \"something\", \"uri\": \"/test/path\"]");
366-
let ex = lspmsg!("uri": "/usr/home/app", "method": "text/document", "workspaceFolders": "[\"name\": \"something\", \"uri\": \"/usr/home/app\"]");
394+
let rq = lspmsg!("uri": "/test/path", "method": "text/document", "workspaceFolders": "[\"name\":\"something\", \"uri\":\"file:///test/path\"]");
395+
let ex = lspmsg!("uri": "/usr/home/app", "method": "text/document", "workspaceFolders": "[\"name\":\"something\", \"uri\":\"file:///usr/home/app\"]");
367396

368397
let mut request = Bytes::from(rq.clone());
369398
let mut expected = Bytes::from(ex);
@@ -412,6 +441,116 @@ mod tests {
412441
.is_some()
413442
);
414443
}
444+
445+
#[test]
446+
fn ensure_root_patches_multiple_workspace_folders() {
447+
let mut config = construct_config();
448+
config.docker_internal_path = "/usr/src/app".to_string();
449+
450+
// We simulate a client sending TWO workspace folders
451+
let input_json = json!({
452+
"jsonrpc": "2.0",
453+
"id": 1,
454+
"method": "initialize",
455+
"params": {
456+
"workspaceFolders": [
457+
{ "uri": "file:///local/path/one", "name": "one" },
458+
{ "uri": "file:///local/path/two", "name": "two" }
459+
]
460+
}
461+
});
462+
463+
let mut request = Bytes::from(serde_json::to_vec(&input_json).unwrap());
464+
465+
// Run the patcher
466+
ensure_root(&mut request, &config);
467+
468+
let body = String::from_utf8(request.to_vec()).unwrap();
469+
470+
// EXPECTATION:
471+
// Both URIs should be replaced by the docker internal path.
472+
let expected_uri = "file:///usr/src/app";
473+
474+
// Check that the first original URI is GONE
475+
assert!(
476+
!body.contains("file:///local/path/one"),
477+
"The first workspace folder was NOT patched (or was reverted)!"
478+
);
479+
480+
// Check that the second original URI is GONE
481+
assert!(
482+
!body.contains("file:///local/path/two"),
483+
"The second workspace folder was NOT patched!"
484+
);
485+
486+
// Check that the target URI appears twice
487+
let matches = body.matches(expected_uri).count();
488+
assert_eq!(
489+
matches, 2,
490+
"Expected the docker URI to appear twice, found it {} times",
491+
matches
492+
);
493+
}
494+
495+
#[test]
496+
fn ensure_root_patches_with_subdirs() {
497+
let mut config = construct_config();
498+
config.local_path = "/local/path".to_string(); // <- this is the root path
499+
config.docker_internal_path = "/usr/src/app".to_string();
500+
501+
// We simulate a client sending TWO workspace folders
502+
let input_json = json!({
503+
"jsonrpc": "2.0",
504+
"id": 1,
505+
"method": "initialize",
506+
"params": {
507+
"workspaceFolders": [
508+
{ "uri": "file:///local/path/one", "name": "one" },
509+
{ "uri": "file:///local/path/two", "name": "two" }
510+
]
511+
}
512+
});
513+
514+
let mut request = Bytes::from(serde_json::to_vec(&input_json).unwrap());
515+
516+
// Run the patcher
517+
ensure_root(&mut request, &config);
518+
519+
let body = String::from_utf8(request.to_vec()).unwrap();
520+
521+
// EXPECTATION:
522+
// Both URIs should be replaced by the docker internal path.
523+
let expected_uri_one = "file:///usr/src/app/one";
524+
let expected_uri_two = "file:///usr/src/app/two";
525+
526+
// Check that the first original URI is GONE
527+
assert!(
528+
!body.contains("file:///local/path/one"),
529+
"The first workspace folder was NOT patched (or was reverted)!"
530+
);
531+
532+
// Check that the second original URI is GONE
533+
assert!(
534+
!body.contains("file:///local/path/two"),
535+
"The second workspace folder was NOT patched!"
536+
);
537+
538+
// Check that the target URI appears twice
539+
let matches = body.matches(expected_uri_one).count();
540+
assert_eq!(
541+
matches, 1,
542+
"Expected the docker URI to appear once, found it {} times",
543+
matches
544+
);
545+
546+
// Check that the target URI appears twice
547+
let matches = body.matches(expected_uri_two).count();
548+
assert_eq!(
549+
matches, 1,
550+
"Expected the docker URI to appear once, found it {} times",
551+
matches
552+
);
553+
}
415554
}
416555

417556
#[cfg(test)]

src/lsp/pid.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@ impl PidHandler {
2222
raw_bytes: &mut Bytes,
2323
) -> serde_json::error::Result<()> {
2424
debug!("Initialize method found, patching");
25-
trace!(?raw_bytes, "before patch");
2625

2726
let mut v: Value = serde_json::from_slice(raw_bytes.as_ref())?;
2827
if let Some(process_id) = v.pointer_mut("/params/processId") {
2928
debug!(
3029
"The PID has been captured from the initialize method, setting pid_handler to None"
3130
);
3231
self.pid = process_id.as_u64();
32+
if self.pid.is_none() {
33+
trace!("PID is null, skipping patch");
34+
return Ok(());
35+
}
36+
37+
trace!(?raw_bytes, "before patch");
3338
trace!(self.pid, "captured PID");
3439
*process_id = json!(null);
3540
*raw_bytes = Bytes::from(serde_json::to_vec(&v)?);

0 commit comments

Comments
 (0)