auto_lsp_default/server/
workspace_init.rs

1/*
2This file is part of auto-lsp.
3Copyright (C) 2025 CLAUZEL Adrien
4
5auto-lsp is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program.  If not, see <http://www.gnu.org/licenses/>
17*/
18
19use auto_lsp_core::errors::{ExtensionError, FileSystemError, RuntimeError};
20use auto_lsp_core::parsers::Parsers;
21use auto_lsp_server::Session;
22use lsp_types::{InitializeParams, Url};
23use rayon::prelude::*;
24use std::path::Path;
25use std::{fs::File, io::Read};
26use texter::core::text::Text;
27use walkdir::WalkDir;
28
29use crate::db::BaseDatabase;
30use crate::db::FileManager;
31
32pub trait WorkspaceInit {
33    fn init_workspace(
34        &mut self,
35        params: InitializeParams,
36    ) -> Result<Vec<Result<(), RuntimeError>>, RuntimeError>;
37
38    fn read_file(&self, file: &Path) -> Result<(&'static Parsers, Url, Text), FileSystemError>;
39}
40
41impl<Db: BaseDatabase> WorkspaceInit for Session<Db> {
42    /// Initializes the workspace by loading files and associating them with parsers.
43    fn init_workspace(
44        &mut self,
45        params: InitializeParams,
46    ) -> Result<Vec<Result<(), RuntimeError>>, RuntimeError> {
47        let mut errors: Vec<Result<(), RuntimeError>> = vec![];
48
49        if let Some(folders) = params.workspace_folders {
50            let files = folders
51                .into_iter()
52                .flat_map(|folder| {
53                    WalkDir::new(folder.uri.path())
54                        .into_iter()
55                        .filter_map(Result::ok)
56                        .filter(|entry| {
57                            entry.file_type().is_file()
58                                && entry.path().extension().is_some_and(|ext| {
59                                    self.extensions.contains_key(ext.to_string_lossy().as_ref())
60                                })
61                        })
62                })
63                .collect::<Vec<_>>();
64
65            errors.extend(rayon_par_bridge::par_bridge(
66                16,
67                files.into_par_iter(),
68                |file_iter| {
69                    file_iter
70                        .map(|file| match self.read_file(&file.into_path()) {
71                            Ok((parsers, url, text)) => self
72                                .db
73                                .add_file_from_texter(parsers, &url, text)
74                                .map_err(RuntimeError::from),
75                            Err(err) => Err(RuntimeError::from(err)),
76                        })
77                        .collect::<Vec<_>>()
78                },
79            ));
80        }
81
82        Ok(errors)
83    }
84
85    fn read_file(&self, file: &Path) -> Result<(&'static Parsers, Url, Text), FileSystemError> {
86        let url = Url::from_file_path(file).map_err(|_| FileSystemError::FilePathToUrl {
87            path: file.to_path_buf(),
88        })?;
89
90        let mut open_file = File::open(file).map_err(|e| FileSystemError::FileOpen {
91            path: url.clone(),
92            error: e.to_string(),
93        })?;
94        let mut buffer = String::new();
95        open_file
96            .read_to_string(&mut buffer)
97            .map_err(|e| FileSystemError::FileRead {
98                path: url.clone(),
99                error: e.to_string(),
100            })?;
101
102        let extension = get_extension(&url)?;
103
104        let text = (self.text_fn)(buffer.to_string());
105        let extension = match self.extensions.get(&extension) {
106            Some(extension) => extension,
107            None => {
108                return Err(FileSystemError::from(ExtensionError::UnknownExtension {
109                    extension: extension.clone(),
110                    available: self.extensions.clone(),
111                }))
112            }
113        };
114
115        let parsers = self
116            .init_options
117            .parsers
118            .get(extension.as_str())
119            .ok_or_else(|| {
120                FileSystemError::from(ExtensionError::UnknownParser {
121                    extension: extension.clone(),
122                    available: self.init_options.parsers.keys().cloned().collect(),
123                })
124            })?;
125        Ok((parsers, url, text))
126    }
127}
128
129/// Get the extension of a file from a [`Url`] path
130#[cfg(windows)]
131pub(crate) fn get_extension(path: &Url) -> Result<String, FileSystemError> {
132    // Ensure the host is either empty or "localhost" on Windows
133    if let Some(host) = path.host_str() {
134        if !host.is_empty() && host != "localhost" {
135            return Err(FileSystemError::FileUrlHost {
136                host: host.to_string(),
137                path: path.clone(),
138            });
139        }
140    }
141
142    path.to_file_path()
143        .map_err(|_| FileSystemError::FileUrlToFilePath { path: path.clone() })?
144        .extension()
145        .map_or_else(
146            || Err(FileSystemError::FileExtension { path: path.clone() }),
147            |ext| Ok(ext.to_string_lossy().to_string()),
148        )
149}
150
151#[cfg(not(windows))]
152pub(crate) fn get_extension(path: &Url) -> Result<String, FileSystemError> {
153    path.to_file_path()
154        .map_err(|_| FileSystemError::FileUrlToFilePath { path: path.clone() })?
155        .extension()
156        .map_or_else(
157            || Err(FileSystemError::FileExtension { path: path.clone() }),
158            |ext| Ok(ext.to_string_lossy().to_string()),
159        )
160}
161
162#[cfg(test)]
163mod tests {
164    use super::get_extension;
165    use auto_lsp_core::errors::FileSystemError;
166    use lsp_types::Url;
167
168    #[cfg(windows)]
169    #[test]
170    fn test_get_extension_windows() {
171        // Valid Windows paths
172        assert_eq!(
173            get_extension(&Url::parse("file:///C:/path/to/file.rs").unwrap())
174                .unwrap()
175                .as_str(),
176            "rs"
177        );
178
179        assert_eq!(
180            get_extension(&Url::parse("file:///C:/path/to/file.with.multiple.dots").unwrap())
181                .unwrap()
182                .as_str(),
183            "dots"
184        );
185
186        // Empty extension
187        assert_eq!(
188            get_extension(&Url::parse("file:///C:/path/to/file").unwrap()),
189            Err(FileSystemError::FileExtension {
190                path: Url::parse("file:///C:/path/to/file").unwrap()
191            })
192        );
193    }
194
195    #[cfg(not(windows))]
196    #[test]
197    fn test_get_extension_non_windows() {
198        // Valid Linux/Unix paths
199
200        assert_eq!(
201            get_extension(&Url::parse("file:///path/to/file.rs").unwrap())
202                .unwrap()
203                .as_str(),
204            "rs"
205        );
206
207        assert_eq!(
208            get_extension(&Url::parse("file:///path/to/file.with.multiple.dots").unwrap())
209                .unwrap()
210                .as_str(),
211            "dots"
212        );
213
214        // Empty extension
215        assert_eq!(
216            get_extension(&Url::parse("file:///path/to/file").unwrap()),
217            Err(FileSystemError::FileExtension {
218                path: Url::parse("file:///path/to/file").unwrap()
219            })
220        );
221
222        // Note: On non-Windows systems, the host is typically ignored, so this should work
223        assert_eq!(
224            get_extension(&Url::parse("file://localhost/path/to/file.rs").unwrap())
225                .unwrap()
226                .as_str(),
227            "rs"
228        );
229    }
230}