fennec_server/
server.rs

1// Copyright 2023 Gregory Petrosyan <pgregory@pgregory.net>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7use 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    // From LSP InitializeParams:
20    workspace_folders: Vec<PathBuf>,
21    _utf8_pos: bool, // TODO: use
22}
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        // We don't handle init_params.process_id in any way here.
68        // Ideally, lsp_server should react to EOF from stdin and
69        // initiate clean shutdown (disconnect the sending side of conn.receiver).
70        // However, I have not tested that it actually works.
71
72        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 // ¯\_(ツ)_/¯
84    }
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        // TODO: server must wait for responses from the core (and be able to cancel them)
105
106        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                        // We find the roots only after watchers are registered to avoid possible races
136                        // where we would miss new roots that appeared after the walk is complete but
137                        // before the watch is set up.
138                        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                                    // We react to create events only because we expect to get change/delete events from our VFS.
157                                    // Note that VSCode seems to miss e.g. delete events for module manifests
158                                    // when module manifest parent folder is deleted.
159                                    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}