Skip to main content

shuck_server/
workspace.rs

1#![allow(dead_code)]
2
3use std::ops::Deref;
4use std::path::PathBuf;
5
6use lsp_types::{Url, WorkspaceFolder};
7
8use crate::session::{ClientOptions, WorkspaceOptionsMap};
9
10/// Collection of workspace folders known to the LSP session.
11#[derive(Debug)]
12pub struct Workspaces(Vec<Workspace>);
13
14impl Workspaces {
15    /// Create a workspace collection from explicit workspace entries.
16    pub fn new(workspaces: Vec<Workspace>) -> Self {
17        Self(workspaces)
18    }
19
20    pub(crate) fn from_workspace_folders(
21        workspace_folders: Option<Vec<WorkspaceFolder>>,
22        root_uri: Option<Url>,
23        root_path: Option<String>,
24        mut workspace_options: WorkspaceOptionsMap,
25    ) -> crate::Result<Self> {
26        let mut client_options_for_url =
27            |url: &Url| workspace_options.remove(url).unwrap_or_default();
28
29        let workspaces =
30            if let Some(folders) = workspace_folders.filter(|folders| !folders.is_empty()) {
31                folders
32                    .into_iter()
33                    .map(|folder| {
34                        let options = client_options_for_url(&folder.uri);
35                        Workspace::new(folder.uri).with_options(options)
36                    })
37                    .collect()
38            } else {
39                let uri = default_workspace_uri(root_uri, root_path)?;
40                let options = client_options_for_url(&uri);
41                vec![Workspace::default(uri).with_options(options)]
42            };
43
44        Ok(Self(workspaces))
45    }
46}
47
48fn default_workspace_uri(root_uri: Option<Url>, root_path: Option<String>) -> crate::Result<Url> {
49    if let Some(root_uri) = root_uri {
50        return Ok(root_uri);
51    }
52
53    if let Some(root_path) = root_path
54        && let Ok(root_uri) = Url::from_file_path(PathBuf::from(root_path))
55    {
56        return Ok(root_uri);
57    }
58
59    let current_dir = std::env::current_dir()?;
60    Url::from_file_path(current_dir)
61        .map_err(|()| anyhow::anyhow!("failed to create URL from current directory"))
62}
63
64impl Deref for Workspaces {
65    type Target = [Workspace];
66
67    fn deref(&self) -> &Self::Target {
68        &self.0
69    }
70}
71
72/// One LSP workspace folder plus optional Shuck settings.
73#[derive(Debug)]
74pub struct Workspace {
75    url: Url,
76    options: Option<ClientOptions>,
77    is_default: bool,
78}
79
80impl Workspace {
81    /// Create a non-default workspace for `url`.
82    pub fn new(url: Url) -> Self {
83        Self {
84            url,
85            options: None,
86            is_default: false,
87        }
88    }
89
90    /// Create the default workspace for clients without workspace-folder support.
91    pub fn default(url: Url) -> Self {
92        Self {
93            url,
94            options: None,
95            is_default: true,
96        }
97    }
98
99    /// Return a copy with workspace-specific client options.
100    #[must_use]
101    pub fn with_options(mut self, options: ClientOptions) -> Self {
102        self.options = Some(options);
103        self
104    }
105
106    pub(crate) fn url(&self) -> &Url {
107        &self.url
108    }
109
110    pub(crate) fn options(&self) -> Option<&ClientOptions> {
111        self.options.as_ref()
112    }
113
114    pub(crate) fn is_default(&self) -> bool {
115        self.is_default
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn uses_root_uri_when_workspace_folders_are_missing() {
125        let root_uri = Url::parse("file:///tmp/shuck-root").expect("test URI should parse");
126
127        let workspaces = Workspaces::from_workspace_folders(
128            None,
129            Some(root_uri.clone()),
130            None,
131            WorkspaceOptionsMap::default(),
132        )
133        .expect("workspace fallback should succeed");
134
135        assert_eq!(workspaces.len(), 1);
136        assert_eq!(workspaces[0].url(), &root_uri);
137        assert!(workspaces[0].is_default());
138    }
139
140    #[test]
141    fn uses_root_path_when_root_uri_is_missing() {
142        let root_path = std::env::temp_dir().join("shuck-server-root-path");
143        let expected_uri =
144            Url::from_file_path(&root_path).expect("temporary directory should convert to a URL");
145
146        let workspaces = Workspaces::from_workspace_folders(
147            None,
148            None,
149            Some(root_path.display().to_string()),
150            WorkspaceOptionsMap::default(),
151        )
152        .expect("workspace fallback should succeed");
153
154        assert_eq!(workspaces.len(), 1);
155        assert_eq!(workspaces[0].url(), &expected_uri);
156        assert!(workspaces[0].is_default());
157    }
158}