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 call_hierarchy_provider: Some(types::CallHierarchyServerCapability::Simple(true)),
156 references_provider: Some(OneOf::Left(true)),
157 document_highlight_provider: Some(OneOf::Left(true)),
158 document_formatting_provider: Some(OneOf::Left(true)),
159 document_range_formatting_provider: Some(OneOf::Left(true)),
160 document_symbol_provider: Some(OneOf::Left(true)),
161 workspace_symbol_provider: Some(OneOf::Right(types::WorkspaceSymbolOptions {
162 work_done_progress_options: WorkDoneProgressOptions {
163 work_done_progress: Some(true),
164 },
165 resolve_provider: Some(false),
166 })),
167 diagnostic_provider: Some(types::DiagnosticServerCapabilities::Options(
168 DiagnosticOptions {
169 identifier: Some(crate::DIAGNOSTIC_NAME.into()),
170 inter_file_dependencies: false,
171 workspace_diagnostics: false,
172 work_done_progress_options: WorkDoneProgressOptions {
173 work_done_progress: Some(true),
174 },
175 },
176 )),
177 execute_command_provider: Some(types::ExecuteCommandOptions {
178 commands: SupportedCommand::all()
179 .map(|command| command.identifier().to_string())
180 .collect(),
181 work_done_progress_options: WorkDoneProgressOptions {
182 work_done_progress: Some(false),
183 },
184 }),
185 hover_provider: Some(types::HoverProviderCapability::Simple(true)),
186 rename_provider: Some(OneOf::Right(types::RenameOptions {
187 prepare_provider: Some(true),
188 work_done_progress_options: WorkDoneProgressOptions {
189 work_done_progress: Some(true),
190 },
191 })),
192 text_document_sync: Some(TextDocumentSyncCapability::Options(
193 TextDocumentSyncOptions {
194 open_close: Some(true),
195 change: Some(TextDocumentSyncKind::INCREMENTAL),
196 will_save: Some(false),
197 will_save_wait_until: Some(false),
198 ..Default::default()
199 },
200 )),
201 ..Default::default()
202 }
203 }
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
207pub(crate) enum SupportedCodeAction {
208 QuickFix,
209 SourceFixAll,
210}
211
212impl SupportedCodeAction {
213 fn all() -> impl Iterator<Item = Self> {
214 [Self::QuickFix, Self::SourceFixAll].into_iter()
215 }
216
217 fn to_kind(self) -> CodeActionKind {
218 match self {
219 Self::QuickFix => CodeActionKind::QUICKFIX,
220 Self::SourceFixAll => crate::SOURCE_FIX_ALL_SHUCK,
221 }
222 }
223}
224
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
226pub(crate) enum SupportedCommand {
227 ApplyAutofix,
228 ApplyDirective,
229 PrintDebugInformation,
230}
231
232impl SupportedCommand {
233 fn all() -> impl Iterator<Item = Self> {
234 [
235 Self::ApplyAutofix,
236 Self::ApplyDirective,
237 Self::PrintDebugInformation,
238 ]
239 .into_iter()
240 }
241
242 fn identifier(self) -> &'static str {
243 match self {
244 Self::ApplyAutofix => "shuck.applyAutofix",
245 Self::ApplyDirective => "shuck.applyDirective",
246 Self::PrintDebugInformation => "shuck.printDebugInformation",
247 }
248 }
249}
250
251type PanicHook = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
252
253struct PanicHookGuard {
254 previous: Option<PanicHook>,
255}
256
257impl Drop for PanicHookGuard {
258 fn drop(&mut self) {
259 if let Some(previous) = self.previous.take() {
260 std::panic::set_hook(previous);
261 }
262 }
263}
264
265fn install_panic_hook(client: Client) -> PanicHookGuard {
266 let previous = std::panic::take_hook();
267 std::panic::set_hook(Box::new(move |panic_info| {
268 report_panic(&client, panic_info);
269 }));
270 PanicHookGuard {
271 previous: Some(previous),
272 }
273}
274
275fn report_panic(client: &Client, panic_info: &std::panic::PanicHookInfo<'_>) {
276 let summary = panic_info
277 .payload()
278 .downcast_ref::<String>()
279 .cloned()
280 .or_else(|| {
281 panic_info
282 .payload()
283 .downcast_ref::<&'static str>()
284 .map(|message| (*message).to_owned())
285 })
286 .unwrap_or_else(|| "unknown panic".to_owned());
287 let location = panic_info.location().map(|location| {
288 format!(
289 "{}:{}:{}",
290 location.file(),
291 location.line(),
292 location.column()
293 )
294 });
295 let backtrace = std::backtrace::Backtrace::force_capture().to_string();
296 emit_panic_report(client, &summary, location.as_deref(), &backtrace);
297}
298
299fn emit_panic_report(client: &Client, summary: &str, location: Option<&str>, backtrace: &str) {
300 let location = location.unwrap_or("unknown location");
301 let details = format!("Shuck server panicked at {location}: {summary}\n{backtrace}");
302 tracing::error!("{details}");
303 eprintln!("{details}");
304 if let Err(error) = client.log_message(&details, lsp_types::MessageType::ERROR) {
305 tracing::error!("Failed to send panic log message to client: {error}");
306 }
307 client.show_error_message(format!("Shuck server panicked: {summary}"));
308}
309
310#[cfg(test)]
311mod tests {
312 use crossbeam::channel;
313 use lsp_server::Message;
314 use lsp_types::notification::Notification;
315
316 use super::*;
317 use crate::Client;
318
319 #[test]
320 fn advertises_formatting_capabilities() {
321 let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
322 assert_eq!(
323 capabilities.document_formatting_provider,
324 Some(OneOf::Left(true))
325 );
326 assert_eq!(
327 capabilities.document_range_formatting_provider,
328 Some(OneOf::Left(true))
329 );
330 }
331
332 #[test]
333 fn advertises_navigation_completion_and_rename_capabilities() {
334 let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
335 assert!(capabilities.completion_provider.is_some());
336 assert_eq!(capabilities.definition_provider, Some(OneOf::Left(true)));
337 assert_eq!(capabilities.references_provider, Some(OneOf::Left(true)));
338 assert_eq!(
339 capabilities.document_highlight_provider,
340 Some(OneOf::Left(true))
341 );
342 let Some(OneOf::Right(rename)) = capabilities.rename_provider else {
343 panic!("expected rename options");
344 };
345 assert_eq!(rename.prepare_provider, Some(true));
346 }
347
348 #[test]
349 fn advertises_document_symbol_capability() {
350 let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
351 assert_eq!(
352 capabilities.document_symbol_provider,
353 Some(OneOf::Left(true))
354 );
355 }
356
357 #[test]
358 fn advertises_workspace_symbol_capability_without_resolve() {
359 let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
360 let Some(OneOf::Right(options)) = capabilities.workspace_symbol_provider else {
361 panic!("expected workspace symbol options");
362 };
363 assert_eq!(options.resolve_provider, Some(false));
364 }
365
366 #[test]
367 fn advertises_only_non_formatting_execute_commands() {
368 let capabilities = Server::server_capabilities(PositionEncoding::UTF16);
369 let commands = capabilities
370 .execute_command_provider
371 .expect("server should advertise execute commands")
372 .commands;
373
374 assert!(commands.contains(&"shuck.applyAutofix".to_owned()));
375 assert!(commands.contains(&"shuck.applyDirective".to_owned()));
376 assert!(commands.contains(&"shuck.printDebugInformation".to_owned()));
377 assert!(!commands.contains(&"shuck.applyFormat".to_owned()));
378 }
379
380 #[test]
381 fn panic_reports_are_sent_to_the_client() {
382 let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
383 let (client_sender, client_receiver) = channel::unbounded();
384 let client = Client::new(main_loop_sender, client_sender);
385
386 emit_panic_report(&client, "boom", Some("test.rs:1:1"), "stack backtrace");
387
388 let first = client_receiver
389 .recv_timeout(std::time::Duration::from_secs(1))
390 .expect("panic log notification should be sent");
391 let second = client_receiver
392 .recv_timeout(std::time::Duration::from_secs(1))
393 .expect("panic showMessage notification should be sent");
394
395 let messages = [first, second];
396 assert!(messages.iter().any(|message| matches!(
397 message,
398 Message::Notification(notification)
399 if notification.method == lsp_types::notification::LogMessage::METHOD
400 )));
401 assert!(messages.iter().any(|message| matches!(
402 message,
403 Message::Notification(notification)
404 if notification.method == lsp_types::notification::ShowMessage::METHOD
405 )));
406 }
407}