From c86928eee7e7d6356822d215ec9cd3f8535baa28 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:27:50 +0800 Subject: [PATCH] fix(deployment): Windows absolute mapping paths, and a flaky plugin syntax test (#1130) An absolute local mapping path outside the project falls back to the same path under the project. On Windows, joining it onto the project root kept its drive and replaced the root, so an existing outside folder became a mapping. The drive and root are now dropped first, and the mapping tests use the platform's absolute paths. The Solidity syntax test asks again when the first call of a new instance runs past the edit-path budget and its answer is dropped. --- crates/ide-host/tests/plugins.rs | 11 ++++++++++- crates/ide-remote/src/mapping.rs | 28 +++++++++++++++++----------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 35139a57..b052cd72 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -1064,7 +1064,16 @@ fn plugin_syntax_for_languages_without_a_grammar() { let sol = LanguageId::new("solidity"); let text = "// c\ncontract A {\n uint256 public x;\n function f() public {}\n}\n"; assert!(provider.highlights(&LanguageId::new("rust"), text).is_none(), "only its own languages"); - let tokens = provider.highlights(&sol, text).unwrap(); + // The first call of a new instance can run past the edit-path budget on a + // slow machine; its answer is dropped and a later call asks again. + let deadline = Instant::now() + Duration::from_secs(10); + let tokens = loop { + match provider.highlights(&sol, text) { + Some(tokens) => break tokens, + None if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(50)), + None => panic!("no highlights"), + } + }; let kind_at = |s: &str| tokens.iter().find(|t| &text[t.start as usize..t.end as usize] == s).map(|t| t.kind.as_str()); assert_eq!(kind_at("contract"), Some("keyword")); diff --git a/crates/ide-remote/src/mapping.rs b/crates/ide-remote/src/mapping.rs index b9bbaefa..c8a2c36c 100644 --- a/crates/ide-remote/src/mapping.rs +++ b/crates/ide-remote/src/mapping.rs @@ -52,11 +52,16 @@ fn local_folder(local: &str, project_root: &Path) -> Option { { return Some(project_root.join(rel)); } - let rel = local.trim_matches('/'); - let relative = project_root.join(rel); + let relative = project_root.join(without_root(path)); if !path.is_absolute() || relative.is_dir() || !path.exists() { Some(relative) } else { None } } +/// `path` without its drive and root (`/a/b`, `C:\a\b` → `a/b`), to read it +/// relative to the project: joining an absolute path would replace the root. +fn without_root(path: &Path) -> PathBuf { + path.components().filter(|c| !matches!(c, Component::Prefix(_) | Component::RootDir)).collect() +} + impl Mappings { /// `remote_root` is the server root resolved on the host (its login /// directory when the setting is empty). A server without mappings maps the @@ -247,14 +252,15 @@ mod tests { #[test] fn local_paths_may_be_absolute_inside_the_project() { - let root = Path::new("/p"); - let m = Mappings::new(&server(vec![("/p", "/", ""), ("/p/static/", "cdn", "")]), root, "/mnt/deploy"); - assert_eq!(m.remote(Path::new("/p/src/a.py")).as_deref(), Some("/mnt/deploy/src/a.py")); - assert_eq!(m.remote(Path::new("/p/static/x.css")).as_deref(), Some("/mnt/deploy/cdn/x.css")); - assert_eq!(m.local("/mnt/deploy/cdn/x.css"), Some(PathBuf::from("/p/static/x.css"))); + let root = Path::new(if cfg!(windows) { r"C:\p" } else { "/p" }); + let (inside, statics) = (root.to_str().unwrap(), format!("{}/static/", root.display())); + let m = Mappings::new(&server(vec![(inside, "/", ""), (&statics, "cdn", "")]), root, "/mnt/deploy"); + assert_eq!(m.remote(&root.join("src/a.py")).as_deref(), Some("/mnt/deploy/src/a.py")); + assert_eq!(m.remote(&root.join("static/x.css")).as_deref(), Some("/mnt/deploy/cdn/x.css")); + assert_eq!(m.local("/mnt/deploy/cdn/x.css"), Some(root.join("static/x.css"))); // A leading `/` that is not the project stays project-relative. let m = Mappings::new(&server(vec![("/static/", "", "")]), root, "/mnt/deploy"); - assert_eq!(m.remote(Path::new("/p/static/x.css")).as_deref(), Some("/mnt/deploy/x.css")); + assert_eq!(m.remote(&root.join("static/x.css")).as_deref(), Some("/mnt/deploy/x.css")); } #[test] @@ -264,7 +270,7 @@ mod tests { let outside = tmp.path().join("outside"); std::fs::create_dir_all(&root).unwrap(); std::fs::create_dir(&outside).unwrap(); - let nested = root.join(outside.strip_prefix("/").unwrap()).join("a.txt"); + let nested = root.join(without_root(&outside)).join("a.txt"); std::fs::create_dir_all(nested.parent().unwrap().parent().unwrap()).unwrap(); // A nested project file at the outside path's spelling is not a mapping folder. std::fs::write(nested.parent().unwrap(), "nested").unwrap(); @@ -288,7 +294,7 @@ mod tests { assert_eq!(local_folder("/", &root), Some(root.clone())); let outside = tmp.path().join("outside"); std::fs::create_dir(&outside).unwrap(); - let relative = root.join(outside.strip_prefix("/").unwrap()); + let relative = root.join(without_root(&outside)); std::fs::create_dir_all(&relative).unwrap(); assert_eq!(local_folder(outside.to_str().unwrap(), &root), Some(relative.clone())); let nested = relative.join("a.txt"); @@ -297,7 +303,7 @@ mod tests { assert_eq!(m.remote(&nested).as_deref(), Some("/srv/a.txt")); assert_eq!(m.local("/srv/a.txt"), Some(nested)); let missing = tmp.path().join("missing"); - assert_eq!(local_folder(missing.to_str().unwrap(), &root), Some(root.join(missing.strip_prefix("/").unwrap()))); + assert_eq!(local_folder(missing.to_str().unwrap(), &root), Some(root.join(without_root(&missing)))); assert_eq!(local_folder("/src/", &root), Some(root.join("src"))); }