csharp_language_server/
path.rs1use 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(¬ification[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 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_eq!(found, expected);
215 }
216}