csharp_language_server/
path.rs

1use serde_json::Value;
2use std::{ffi::OsStr, path::PathBuf};
3use url::Url;
4
5use anyhow::{Context, Result};
6
7use crate::notification::{Notification, Params, ProjectParams, SolutionParams};
8
9pub fn create_open_notification(
10    notification: &str,
11    solution_override: Option<String>,
12    projects_override: Option<Vec<String>>,
13) -> String {
14    let root_path =
15        parse_root_path(notification).expect("Root path not part of initialize notification");
16
17    let open_solution_notification = open_solution_notification(&root_path, solution_override);
18
19    if let Some(open_solution_notification) = open_solution_notification {
20        return open_solution_notification;
21    }
22
23    open_projects_notification(&root_path, projects_override)
24}
25
26fn open_solution_notification(root_path: &Path, override_path: Option<String>) -> Option<String> {
27    let solution_path = match override_path {
28        Some(p) => root_path.join(&p),
29        None => find_extension(root_path, &vec![OsStr::new("sln"), OsStr::new("slnx")]).next()?,
30    };
31
32    Some(
33        Notification {
34            jsonrpc: "2.0".to_string(),
35            method: "solution/open".to_string(),
36            params: Params::Solution(SolutionParams {
37                solution: solution_path.to_uri_string(),
38            }),
39        }
40        .serialize(),
41    )
42}
43
44fn open_projects_notification(root_path: &Path, override_paths: Option<Vec<String>>) -> String {
45    let file_paths = match override_paths {
46        Some(p) => p,
47        None => find_extension(root_path, &vec![OsStr::new("csproj")])
48            .map(|p| p.to_uri_string())
49            .collect(),
50    };
51
52    let notification = Notification {
53        jsonrpc: "2.0".to_string(),
54        method: "project/open".to_string(),
55        params: Params::Project(ProjectParams {
56            projects: file_paths,
57        }),
58    };
59
60    notification.serialize()
61}
62
63#[derive(Debug, Clone)]
64struct Path(PathBuf);
65
66impl Path {
67    fn try_from_uri(uri: &str) -> Option<Self> {
68        uri.parse::<Url>()
69            .expect("Couldn't parse path URI")
70            .to_file_path()
71            .ok()
72            .map(Self)
73    }
74
75    fn to_uri_string(&self) -> String {
76        Url::from_file_path(&self.0)
77            .unwrap_or_else(|_| panic!("Couldn't turn {:?} into Uri", self.0))
78            .to_string()
79    }
80
81    fn join(&self, part: &str) -> Self {
82        Path(self.0.join(part.trim()))
83    }
84}
85
86impl From<&str> for Path {
87    fn from(path: &str) -> Self {
88        Self(PathBuf::from(path))
89    }
90}
91
92impl From<PathBuf> for Path {
93    fn from(path: PathBuf) -> Self {
94        Self(path)
95    }
96}
97
98impl From<&ignore::DirEntry> for Path {
99    fn from(value: &ignore::DirEntry) -> Self {
100        value.path().to_str().unwrap().into()
101    }
102}
103
104fn parse_root_path(notification: &str) -> Result<Path> {
105    let json_start = notification
106        .find('{')
107        .context("Notification was not json")?;
108
109    let parsed_notification: Value = serde_json::from_str(&notification[json_start..])?;
110
111    let root_path = parsed_notification["params"]["rootUri"]
112        .as_str()
113        .map_or_else(
114            || {
115                parsed_notification["params"]["rootPath"]
116                    .as_str()
117                    .map(|p| p.into())
118            },
119            Path::try_from_uri,
120        )
121        .context("Root URI/path was not given by the client")?;
122
123    Ok(root_path)
124}
125
126fn path_for_file_with_extension(dir: &ignore::DirEntry, ext: &Vec<&'static OsStr>) -> Option<Path> {
127    if dir.path().is_file() && dir.path().extension().is_some_and(|e| ext.contains(&e)) {
128        return Some(dir.into());
129    }
130    None
131}
132
133fn find_extension(root_path: &Path, ext: &Vec<&'static OsStr>) -> impl Iterator<Item = Path> {
134    let mut found_paths: Vec<(usize, Path)> = ignore::Walk::new(&root_path.0)
135        .filter_map(|res| res.ok())
136        .filter_map(|d| path_for_file_with_extension(&d, ext).map(|p| (d.depth(), p)))
137        .collect();
138
139    found_paths.sort_by(|(depth_a, path_a), (depth_b, path_b)| {
140        depth_a.cmp(depth_b).then_with(|| path_a.0.cmp(&path_b.0))
141    });
142
143    found_paths.into_iter().map(|(_, p)| p)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::{ffi::OsStr, fs};
150    use tempfile::TempDir;
151
152    macro_rules! from_uri_test {
153    ([$target_family:literal], $($test_name:ident: ($input:expr, $expected:expr),)*) => {
154        $(
155            #[cfg(target_family = $target_family)]
156            #[test]
157            fn $test_name() {
158                let from_uri =
159                    Path::try_from_uri($input);
160                    let expected = $expected.replace('/', std::path::MAIN_SEPARATOR_STR);
161                assert!(from_uri.as_ref().is_some_and(|p| p.0.to_str().is_some_and(|s| s.eq(&expected))),
162                    "try_from_uri for '{}' was '{:?}' and not '{}'", $input, &from_uri, expected
163                )
164            }
165        )*
166    }
167}
168
169    from_uri_test! { ["windows"],
170        rooted_windows: ("file:///C:/_Foo/bar/baz","C:/_Foo/bar/baz"),
171        with_host_windows: ("file://localhost/C:/_Foo/bar/baz","C:/_Foo/bar/baz"),
172        network_path_windows: ("file://_Foo/bar/baz","//_foo/bar/baz"),
173        with_parent_windows: ("file://_Foo/bar/../baz","//_foo/baz"),
174    }
175
176    from_uri_test! { ["unix"],
177        rooted_unix: ("file:///var/_Foo/bar/baz","/var/_Foo/bar/baz"),
178        with_host_unix: ("file://localhost/_Foo/bar/baz","/_Foo/bar/baz"),
179        with_parent_unix: ("file:///var/_Foo/bar/../baz","/var/_Foo/baz"),
180    }
181
182    fn touch(path: &std::path::Path) {
183        if let Some(parent) = path.parent() {
184            fs::create_dir_all(parent).unwrap();
185        }
186        fs::write(path, b"").unwrap();
187    }
188
189    #[test]
190    fn finds_files_with_ext_in_root_and_subdir() {
191        let tmp = TempDir::new().unwrap();
192        let root = tmp.path();
193
194        // Layout:
195        // root/
196        //   b.ext          <-- match
197        //   b.txt          <-- non-match
198        //   a/
199        //     a.ext        <-- match
200        //     other        <-- non-match (no extension)
201        touch(&root.join("b.ext"));
202        touch(&root.join("b.txt"));
203        touch(&root.join("a").join("a.ext"));
204        touch(&root.join("a").join("other"));
205
206        let ext = vec![OsStr::new("ext")];
207
208        let results: Vec<Path> = find_extension(&root.to_path_buf().into(), &ext).collect();
209
210        let found: Vec<std::path::PathBuf> = results.into_iter().map(|p| p.0).collect();
211        let expected = vec![root.join("b.ext"), root.join("a").join("a.ext")];
212
213        // Assert
214        assert_eq!(found, expected);
215    }
216}