1use anyhow::{anyhow, Context};
8use fennec_common::{types, util, MODULE_MANIFEST_FILENAME, PROJECT_NAME};
9use lsp_types::{notification::Notification, request::Request};
10use std::path::{Path, PathBuf};
11
12const FILE_SCHEME: &str = "file";
13
14pub struct Server {
15 conn: lsp_server::Connection,
16 io_threads: lsp_server::IoThreads,
17 request_id: i32,
18
19 workspace_folders: Vec<PathBuf>,
21 _utf8_pos: bool, }
23
24impl Server {
25 pub fn new_stdio(version: &str) -> Result<Server, anyhow::Error> {
26 let (conn, io_threads) = lsp_server::Connection::stdio();
27
28 let (id, init_params) = conn
29 .initialize_start()
30 .context("failed to wait for InitializeParams")?;
31 let init_params: lsp_types::InitializeParams = serde_json::from_value(init_params)
32 .context("failed to deserialize InitializeParams")?;
33 if log::log_enabled!(log::Level::Debug) {
34 let init_pretty =
35 serde_json::to_string_pretty(&init_params).unwrap_or_else(|e| e.to_string());
36 log::debug!("InitializeParams: {init_pretty}");
37 }
38
39 let dyn_watch = cap_fs_watch_dynamic(&init_params);
40 if !dyn_watch {
41 return Err(anyhow!("Fennec LSP server requires client to support dynamic registration in DidChangeWatchedFilesClientCapabilities"));
42 }
43
44 let utf8_pos = cap_utf8_positions(&init_params);
45 let folders = workspace_roots(&init_params);
46
47 let init_result = lsp_types::InitializeResult {
48 capabilities: lsp_types::ServerCapabilities {
49 position_encoding: if utf8_pos {
50 Some(lsp_types::PositionEncodingKind::UTF8)
51 } else {
52 None
53 },
54 ..Default::default()
55 },
56 server_info: Some(lsp_types::ServerInfo {
57 name: PROJECT_NAME.to_owned(),
58 version: Some(version.to_owned()),
59 }),
60 };
61 let init_result =
62 serde_json::to_value(init_result).context("failed to serialize InitializeResult")?;
63
64 conn.initialize_finish(id, init_result)
65 .context("failed to send InitializeResult")?;
66
67 Ok(Server {
73 conn,
74 io_threads,
75 request_id: 0,
76 _utf8_pos: utf8_pos,
77 workspace_folders: folders,
78 })
79 }
80
81 #[must_use]
82 pub fn watch_for_roots(&self) -> bool {
83 true }
85
86 pub fn join(self) -> Result<(), anyhow::Error> {
87 self.io_threads.join()?;
88 Ok(())
89 }
90
91 fn next_id(&mut self) -> lsp_server::RequestId {
92 let id = self.request_id;
93 self.request_id += 1;
94 lsp_server::RequestId::from(id)
95 }
96
97 pub fn run(&mut self, state: &types::SyncState) -> Result<(), anyhow::Error> {
98 let reg_id = self.next_id();
99 let mut registered_manifest_watchers = false;
100 register_module_manifest_watchers(&self.conn, reg_id.clone()).context(format!(
101 "failed to register {MODULE_MANIFEST_FILENAME} watchers"
102 ))?;
103
104 for msg in &self.conn.receiver {
107 match msg {
108 lsp_server::Message::Request(req) => {
109 if self.conn.handle_shutdown(&req)? {
110 return Ok(());
111 }
112 if !registered_manifest_watchers {
113 let lsp_server::Request { id, method, .. } = req;
114 let msg = format!(
115 r#"got "{method}" (id {id}) request before module manifest watchers were registered, ignoring"#
116 );
117 log::warn!("{msg}");
118 let _ = self.conn.sender.send(
119 lsp_server::Response::new_err(
120 id,
121 lsp_server::ErrorCode::ContentModified as i32,
122 msg,
123 )
124 .into(),
125 );
126 }
127 }
128 lsp_server::Message::Response(resp) => {
129 if resp.id == reg_id {
130 if let Some(lsp_server::ResponseError { code, message, .. }) = resp.error {
131 return Err(anyhow!("failed to register {MODULE_MANIFEST_FILENAME} watchers: [{code}] {message}"));
132 }
133 registered_manifest_watchers = true;
134
135 let roots = find_module_roots(&self.workspace_folders);
139 state.signal_vfs_new_roots(roots);
140 }
141 }
142 lsp_server::Message::Notification(note) => {
143 if !registered_manifest_watchers {
144 let method = note.method;
145 log::warn!(
146 r#"got "{method}" notification before module manifest watchers were registered, ignoring"#
147 );
148 continue;
149 }
150
151 match extract_note::<lsp_types::notification::DidChangeWatchedFiles>(note) {
152 Ok(params) => {
153 let mut roots: Vec<PathBuf> = vec![];
154 for change in params.changes {
155 if change.typ != lsp_types::FileChangeType::CREATED {
156 continue;
160 }
161 let uri = change.uri;
162 if uri.scheme() != FILE_SCHEME {
163 log::warn!(
164 r#"ignoring non-file-scheme change event for "{uri}""#
165 );
166 continue;
167 }
168 if let Ok(manifest) = uri.to_file_path() {
169 roots.extend(module_manifest_parent(&manifest));
170 } else {
171 log::warn!(
172 r#"ignoring change event with invalid file path "{uri}""#
173 );
174 }
175 }
176 state.signal_vfs_new_roots(roots);
177 }
178 Err(err) => {
179 let method = lsp_types::notification::DidChangeWatchedFiles::METHOD;
180 log::warn!(
181 r#"failed to extract "{method}" notification params, ignoring: {err}"#
182 );
183 }
184 }
185 }
186 }
187 }
188 Ok(())
189 }
190}
191
192fn extract_note<N>(
193 note: lsp_server::Notification,
194) -> Result<N::Params, lsp_server::ExtractError<lsp_server::Notification>>
195where
196 N: lsp_types::notification::Notification,
197 N::Params: serde::de::DeserializeOwned,
198{
199 note.extract(N::METHOD)
200}
201
202fn cap_fs_watch_dynamic(init_params: &lsp_types::InitializeParams) -> bool {
203 if let Some(ref workspace_caps) = init_params.capabilities.workspace {
204 if let Some(ref change_watched) = workspace_caps.did_change_watched_files {
205 return change_watched.dynamic_registration.unwrap_or(false);
206 }
207 }
208 false
209}
210
211fn cap_utf8_positions(init_params: &lsp_types::InitializeParams) -> bool {
212 if let Some(ref general_caps) = init_params.capabilities.general {
213 if let Some(ref encodings) = general_caps.position_encodings {
214 return encodings.contains(&lsp_types::PositionEncodingKind::UTF8);
215 }
216 }
217 false
218}
219
220fn workspace_roots(init_params: &lsp_types::InitializeParams) -> Vec<PathBuf> {
221 if let Some(ref wf) = init_params.workspace_folders {
222 return wf
223 .iter()
224 .filter(|f| f.uri.scheme() == FILE_SCHEME)
225 .filter_map(|f| f.uri.to_file_path().ok())
226 .collect();
227 }
228 init_params
229 .root_uri
230 .iter()
231 .filter(|uri| uri.scheme() == FILE_SCHEME)
232 .filter_map(|uri| uri.to_file_path().ok())
233 .collect()
234}
235
236fn register_module_manifest_watchers(
237 conn: &lsp_server::Connection,
238 id: lsp_server::RequestId,
239) -> Result<(), anyhow::Error> {
240 let opts = lsp_types::DidChangeWatchedFilesRegistrationOptions {
241 watchers: vec![lsp_types::FileSystemWatcher {
242 glob_pattern: lsp_types::GlobPattern::String(format!("**/{MODULE_MANIFEST_FILENAME}")),
243 kind: Some(lsp_types::WatchKind::Create),
244 }],
245 };
246 let opts = serde_json::to_value(opts)
247 .context("failed to serialize DidChangeWatchedFilesRegistrationOptions")?;
248
249 let params = lsp_types::RegistrationParams {
250 registrations: vec![lsp_types::Registration {
251 id: MODULE_MANIFEST_FILENAME.to_owned(),
252 method: lsp_types::notification::DidChangeWatchedFiles::METHOD.to_owned(),
253 register_options: Some(opts),
254 }],
255 };
256 let params = serde_json::to_value(params).context("failed to serialize RegistrationParams")?;
257
258 let req = lsp_server::Request {
259 id,
260 method: lsp_types::request::RegisterCapability::METHOD.to_owned(),
261 params,
262 };
263 conn.sender
264 .send(req.into())
265 .context("failed to send client/registerCapability request")?;
266
267 Ok(())
268}
269
270fn find_module_roots(workspace_folders: &Vec<PathBuf>) -> Vec<PathBuf> {
271 let mut roots: Vec<PathBuf> = Vec::with_capacity(workspace_folders.len());
272 for folder in workspace_folders {
273 let walker = walkdir::WalkDir::new(folder).into_iter();
274 for entry in walker.filter_entry(|e| util::is_valid_utf8_visible(e.file_name())) {
275 match entry {
276 Ok(entry) => {
277 if entry.file_type().is_file() && entry.file_name() == MODULE_MANIFEST_FILENAME
278 {
279 roots.extend(module_manifest_parent(&entry.into_path()));
280 }
281 }
282 Err(err) => {
283 log::warn!("error while scanning for module roots, ignoring: {err}");
284 }
285 }
286 }
287 }
288 roots
289}
290
291fn module_manifest_parent(manifest: &Path) -> Option<PathBuf> {
292 assert!(manifest.file_name() == Some(MODULE_MANIFEST_FILENAME.as_ref()));
293 Some(util::normalize_path(manifest).parent()?.to_path_buf())
294}