css_variable_lsp/
path_display.rs

1use 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}