Skip to main content

shuck_server/
server.rs

1use lsp_server::Connection;
2use lsp_types as types;
3use lsp_types::InitializeParams;
4use lsp_types::{
5    ClientCapabilities, CodeActionKind, CodeActionOptions, DiagnosticOptions, OneOf,
6    TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
7    WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities,
8};
9use std::num::NonZeroUsize;
10
11pub(crate) use self::connection::ConnectionInitializer;
12pub use self::connection::ConnectionSender;
13use self::schedule::spawn_main_loop;
14use crate::PositionEncoding;
15pub use crate::server::main_loop::MainLoopSender;
16pub(crate) use crate::server::main_loop::{Event, MainLoopReceiver};
17use crate::session::{AllOptions, Client, Session};
18use crate::workspace::Workspaces;
19pub(crate) use api::Error;
20
21mod api;
22mod connection;
23mod main_loop;
24mod schedule;
25
26pub(crate) type Result<T> = std::result::Result<T, api::Error>;
27
28pub struct Server {
29    connection: Connection,
30    client_capabilities: ClientCapabilities,
31    worker_threads: NonZeroUsize,
32    main_loop_receiver: MainLoopReceiver,
33    main_loop_sender: MainLoopSender,
34    session: Session,
35}
36
37impl Server {
38    pub(crate) fn new(
39        worker_threads: NonZeroUsize,
40        connection: ConnectionInitializer,
41    ) -> crate::Result<Self> {
42        let (id, init_params) = connection.initialize_start()?;
43        let client_capabilities = init_params.capabilities;
44        let position_encoding = Self::find_best_position_encoding(&client_capabilities);
45        let server_capabilities = Self::server_capabilities(position_encoding);
46        let connection = connection.initialize_finish(
47            id,
48            &server_capabilities,
49            crate::SERVER_NAME,
50            crate::version(),
51        )?;
52
53        let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32);
54
55        #[allow(deprecated)]
56        let InitializeParams {
57            initialization_options,
58            root_path,
59            root_uri,
60            workspace_folders,
61            ..
62        } = init_params;
63
64        let client = Client::new(main_loop_sender.clone(), connection.sender.clone());
65        let AllOptions { global, workspace } = AllOptions::from_value(
66            initialization_options.unwrap_or(serde_json::Value::Null),
67            &client,
68        );
69
70        crate::logging::init_logging(
71            global.tracing.log_level.unwrap_or_default(),
72            global.tracing.log_file.as_deref(),
73        );
74
75        let workspaces = Workspaces::from_workspace_folders(
76            workspace_folders,
77            root_uri,
78            root_path,
79            workspace.unwrap_or_default(),
80        )?;
81        let global = global.into_settings(client.clone());
82
83        Ok(Self {
84            connection,
85            client_capabilities: client_capabilities.clone(),
86            worker_threads,
87            main_loop_receiver,
88            main_loop_sender,
89            session: Session::new(
90                &client_capabilities,
91                position_encoding,
92                global,
93                &workspaces,
94                &client,
95            )?,
96        })
97    }
98
99    pub fn run(mut self) -> crate::Result<()> {
100        let panic_client = Client::new(
101            self.main_loop_sender.clone(),
102            self.connection.sender.clone(),
103        );
104        let _panic_hook = install_panic_hook(panic_client);
105        spawn_main_loop(move || self.main_loop())?
106            .join()
107            .map_err(|_| anyhow::anyhow!("main loop thread panicked"))?
108    }
109
110    fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding {
111        client_capabilities
112            .general
113            .as_ref()
114            .and_then(|general| general.position_encodings.as_ref())
115            .and_then(|encodings| {
116                encodings
117                    .iter()
118                    .filter_map(|encoding| PositionEncoding::try_from(encoding).ok())
119                    .max()
120            })
121            .unwrap_or_default()
122    }
123
124    fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities {
125        types::ServerCapabilities {
126            position_encoding: Some(position_encoding.into()),
127            code_action_provider: Some(types::CodeActionProviderCapability::Options(
128                CodeActionOptions {
129                    code_action_kinds: Some(
130                        SupportedCodeAction::all()
131                            .map(SupportedCodeAction::to_kind)
132                            .collect(),
133                    ),
134                    work_done_progress_options: WorkDoneProgressOptions {
135                        work_done_progress: Some(true),
136                    },
137                    resolve_provider: Some(true),
138                },
139            )),
140            workspace: Some(types::WorkspaceServerCapabilities {
141                workspace_folders: Some(WorkspaceFoldersServerCapabilities {
142                    supported: Some(true),
143                    change_notifications: Some(OneOf::Left(true)),
144                }),
145                file_operations: None,
146            }),
147            document_formatting_provider: None,
148            document_range_formatting_provider: None,
149            diagnostic_provider: Some(types::DiagnosticServerCapabilities::Options(
150                DiagnosticOptions {
151                    identifier: Some(crate::DIAGNOSTIC_NAME.into()),
152                    inter_file_dependencies: false,
153                    workspace_diagnostics: false,
154                    work_done_progress_options: WorkDoneProgressOptions {
155                        work_done_progress: Some(true),
156                    },
157                },
158            )),
159            execute_command_provider: Some(types::ExecuteCommandOptions {
160                commands: SupportedCommand::all()
161                    .map(|command| command.identifier().to_string())
162                    .collect(),
163                work_done_progress_options: WorkDoneProgressOptions {
164                    work_done_progress: Some(false),
165                },
166            }),
167            hover_provider: Some(types::HoverProviderCapability::Simple(true)),
168            text_document_sync: Some(TextDocumentSyncCapability::Options(
169                TextDocumentSyncOptions {
170                    open_close: Some(true),
171                    change: Some(TextDocumentSyncKind::INCREMENTAL),
172                    will_save: Some(false),
173                    will_save_wait_until: Some(false),
174                    ..Default::default()
175                },
176            )),
177            ..Default::default()
178        }
179    }
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub(crate) enum SupportedCodeAction {
184    QuickFix,
185    SourceFixAll,
186}
187
188impl SupportedCodeAction {
189    fn all() -> impl Iterator<Item = Self> {
190        [Self::QuickFix, Self::SourceFixAll].into_iter()
191    }
192
193    fn to_kind(self) -> CodeActionKind {
194        match self {
195            Self::QuickFix => CodeActionKind::QUICKFIX,
196            Self::SourceFixAll => crate::SOURCE_FIX_ALL_SHUCK,
197        }
198    }
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
202pub(crate) enum SupportedCommand {
203    ApplyAutofix,
204    ApplyDirective,
205    PrintDebugInformation,
206}
207
208impl SupportedCommand {
209    fn all() -> impl Iterator<Item = Self> {
210        [
211            Self::ApplyAutofix,
212            Self::ApplyDirective,
213            Self::PrintDebugInformation,
214        ]
215        .into_iter()
216    }
217
218    fn identifier(self) -> &'static str {
219        match self {
220            Self::ApplyAutofix => "shuck.applyAutofix",
221            Self::ApplyDirective => "shuck.applyDirective",
222            Self::PrintDebugInformation => "shuck.printDebugInformation",
223        }
224    }
225}
226
227type PanicHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
228
229struct PanicHookGuard {
230    previous: Option<PanicHook>,
231}
232
233impl Drop for PanicHookGuard {
234    fn drop(&mut self) {
235        if let Some(previous) = self.previous.take() {
236            std::panic::set_hook(previous);
237        }
238    }
239}
240
241fn install_panic_hook(client: Client) -> PanicHookGuard {
242    let previous = std::panic::take_hook();
243    std::panic::set_hook(Box::new(move |panic_info| {
244        report_panic(&client, panic_info);
245    }));
246    PanicHookGuard {
247        previous: Some(previous),
248    }
249}
250
251fn report_panic(client: &Client, panic_info: &std::panic::PanicHookInfo<'_>) {
252    let summary = panic_info
253        .payload()
254        .downcast_ref::<String>()
255        .cloned()
256        .or_else(|| {
257            panic_info
258                .payload()
259                .downcast_ref::<&'static str>()
260                .map(|message| (*message).to_owned())
261        })
262        .unwrap_or_else(|| "unknown panic".to_owned());
263    let location = panic_info.location().map(|location| {
264        format!(
265            "{}:{}:{}",
266            location.file(),
267            location.line(),
268            location.column()
269        )
270    });
271    let backtrace = std::backtrace::Backtrace::force_capture().to_string();
272    emit_panic_report(client, &summary, location.as_deref(), &backtrace);
273}
274
275fn emit_panic_report(client: &Client, summary: &str, location: Option<&str>, backtrace: &str) {
276    let location = location.unwrap_or("unknown location");
277    let details = format!("Shuck server panicked at {location}: {summary}\n{backtrace}");
278    tracing::error!("{details}");
279    eprintln!("{details}");
280    if let Err(error) = client.log_message(&details, lsp_types::MessageType::ERROR) {
281        tracing::error!("Failed to send panic log message to client: {error}");
282    }
283    client.show_error_message(format!("Shuck server panicked: {summary}"));
284}
285
286#[cfg(test)]
287mod tests {
288    use crossbeam::channel;
289    use lsp_server::Message;
290    use lsp_types::notification::Notification;
291
292    use super::*;
293    use crate::Client;
294
295    #[test]
296    fn does_not_advertise_formatting_capabilities() {
297        let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
298        assert!(capabilities.document_formatting_provider.is_none());
299        assert!(capabilities.document_range_formatting_provider.is_none());
300    }
301
302    #[test]
303    fn advertises_only_non_formatting_execute_commands() {
304        let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
305        let commands = capabilities
306            .execute_command_provider
307            .expect("server should advertise execute commands")
308            .commands;
309
310        assert!(commands.contains(&"shuck.applyAutofix".to_owned()));
311        assert!(commands.contains(&"shuck.applyDirective".to_owned()));
312        assert!(commands.contains(&"shuck.printDebugInformation".to_owned()));
313        assert!(!commands.contains(&"shuck.applyFormat".to_owned()));
314    }
315
316    #[test]
317    fn panic_reports_are_sent_to_the_client() {
318        let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
319        let (client_sender, client_receiver) = channel::unbounded();
320        let client = Client::new(main_loop_sender, client_sender);
321
322        emit_panic_report(&client, "boom", Some("test.rs:1:1"), "stack backtrace");
323
324        let first = client_receiver
325            .recv_timeout(std::time::Duration::from_secs(1))
326            .expect("panic log notification should be sent");
327        let second = client_receiver
328            .recv_timeout(std::time::Duration::from_secs(1))
329            .expect("panic showMessage notification should be sent");
330
331        let messages = [first, second];
332        assert!(messages.iter().any(|message| matches!(
333            message,
334            Message::Notification(notification)
335                if notification.method == lsp_types::notification::LogMessage::METHOD
336        )));
337        assert!(messages.iter().any(|message| matches!(
338            message,
339            Message::Notification(notification)
340                if notification.method == lsp_types::notification::ShowMessage::METHOD
341        )));
342    }
343}