css_variable_lsp/
path_display.rs1use std::path::{Component, Path, PathBuf};
2use tower_lsp::lsp_types::Url;
3
4use crate::runtime_config::PathDisplayMode;
5
6pub struct PathDisplayOptions<'a> {
7 pub mode: PathDisplayMode,
8 pub abbrev_length: usize,
9 pub workspace_folder_paths: &'a [PathBuf],
10 pub root_folder_path: Option<&'a PathBuf>,
11}
12
13pub fn to_normalized_fs_path(uri: &Url) -> Option<PathBuf> {
14 uri.to_file_path().ok()
15}
16
17fn find_best_relative_path(fs_path: &Path, roots: &[PathBuf]) -> Option<PathBuf> {
18 let mut best: Option<PathBuf> = None;
19 for root in roots {
20 let relative = match pathdiff::diff_paths(fs_path, root) {
21 Some(rel) => rel,
22 None => continue,
23 };
24 if relative.as_os_str().is_empty() {
25 continue;
26 }
27 if relative.is_absolute() {
28 continue;
29 }
30 if matches!(relative.components().next(), Some(Component::ParentDir)) {
31 continue;
32 }
33 let rel_len = relative.to_string_lossy().len();
34 let best_len = best
35 .as_ref()
36 .map(|p| p.to_string_lossy().len())
37 .unwrap_or(usize::MAX);
38 if rel_len < best_len {
39 best = Some(relative);
40 }
41 }
42 best
43}
44
45fn abbreviate_path(path: &Path, abbrev_length: usize) -> String {
46 let path_str = path.to_string_lossy();
47 if abbrev_length == 0 {
48 return path_str.to_string();
49 }
50 let mut parts: Vec<String> = path
51 .components()
52 .filter_map(|comp| match comp {
53 Component::Normal(p) => Some(p.to_string_lossy().to_string()),
54 Component::Prefix(prefix) => Some(prefix.as_os_str().to_string_lossy().to_string()),
55 Component::RootDir => Some(std::path::MAIN_SEPARATOR.to_string()),
56 _ => None,
57 })
58 .collect();
59
60 if parts.len() <= 1 {
61 return path_str.to_string();
62 }
63
64 let last_index = parts.len() - 1;
65 for (idx, part) in parts.iter_mut().enumerate() {
66 if idx == last_index {
67 continue;
68 }
69 if part.len() > abbrev_length {
70 *part = part[..abbrev_length].to_string();
71 }
72 }
73
74 if parts
75 .first()
76 .map(|p| p == &std::path::MAIN_SEPARATOR.to_string())
77 .unwrap_or(false)
78 {
79 let mut rebuilt = String::new();
80 rebuilt.push(std::path::MAIN_SEPARATOR);
81 rebuilt.push_str(&parts[1..].join(std::path::MAIN_SEPARATOR_STR));
82 return rebuilt;
83 }
84
85 parts.join(std::path::MAIN_SEPARATOR_STR)
86}
87
88pub fn format_uri_for_display(uri: &Url, options: PathDisplayOptions<'_>) -> String {
89 let fs_path = match to_normalized_fs_path(uri) {
90 Some(path) => path,
91 None => return uri.to_string(),
92 };
93
94 let roots: Vec<PathBuf> = if !options.workspace_folder_paths.is_empty() {
95 options.workspace_folder_paths.to_vec()
96 } else if let Some(root) = options.root_folder_path {
97 vec![root.clone()]
98 } else {
99 Vec::new()
100 };
101
102 let relative = if roots.is_empty() {
103 None
104 } else {
105 find_best_relative_path(&fs_path, &roots)
106 };
107
108 match options.mode {
109 PathDisplayMode::Absolute => fs_path.to_string_lossy().to_string(),
110 PathDisplayMode::Abbreviated => {
111 let base = relative.as_ref().unwrap_or(&fs_path);
112 abbreviate_path(base, options.abbrev_length)
113 }
114 PathDisplayMode::Relative => relative.unwrap_or(fs_path).to_string_lossy().to_string(),
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use std::path::MAIN_SEPARATOR;
122
123 #[test]
124 fn format_uri_respects_relative_and_abbreviated_modes() {
125 let root = std::env::temp_dir().join("css-lsp-test-root");
126 let file_path = root.join("src").join("styles").join("main.css");
127 let uri = Url::from_file_path(&file_path).unwrap();
128
129 let workspace_paths = vec![root.clone()];
130 let relative = format_uri_for_display(
131 &uri,
132 PathDisplayOptions {
133 mode: PathDisplayMode::Relative,
134 abbrev_length: 1,
135 workspace_folder_paths: &workspace_paths,
136 root_folder_path: None,
137 },
138 );
139
140 let expected_relative = format!("src{sep}styles{sep}main.css", sep = MAIN_SEPARATOR);
141 assert_eq!(relative, expected_relative);
142
143 let abbreviated = format_uri_for_display(
144 &uri,
145 PathDisplayOptions {
146 mode: PathDisplayMode::Abbreviated,
147 abbrev_length: 1,
148 workspace_folder_paths: &workspace_paths,
149 root_folder_path: None,
150 },
151 );
152
153 let expected_abbrev = format!("s{sep}s{sep}main.css", sep = MAIN_SEPARATOR);
154 assert_eq!(abbreviated, expected_abbrev);
155 }
156}