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}