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