⭐️ A friendly language for building type-safe, scalable systems!
at main 22 kB view raw
1use super::{ 2 messages::{Message, MessageBuffer, Next, Notification, Request}, 3 progress::ConnectionProgressReporter, 4}; 5use crate::{ 6 Result, 7 diagnostic::{Diagnostic, Level}, 8 io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter}, 9 language_server::{ 10 DownloadDependencies, MakeLocker, 11 engine::{self, LanguageServerEngine}, 12 feedback::{Feedback, FeedbackBookKeeper}, 13 files::FileSystemProxy, 14 router::Router, 15 src_span_to_lsp_range, 16 }, 17 line_numbers::LineNumbers, 18}; 19use camino::{Utf8Path, Utf8PathBuf}; 20use debug_ignore::DebugIgnore; 21use itertools::Itertools; 22use lsp_types::{ 23 self as lsp, HoverProviderCapability, InitializeParams, Position, PublishDiagnosticsParams, 24 Range, RenameOptions, TextEdit, Url, 25}; 26use serde_json::Value as Json; 27use std::collections::{HashMap, HashSet}; 28 29/// This class is responsible for handling the language server protocol and 30/// delegating the work to the engine. 31/// 32/// - Configuring watching of the `gleam.toml` file. 33/// - Decoding requests. 34/// - Encoding responses. 35/// - Sending diagnostics and messages to the client. 36/// - Tracking the state of diagnostics and messages. 37/// - Performing the initialisation handshake. 38/// 39#[derive(Debug)] 40pub struct LanguageServer<'a, IO> { 41 initialise_params: InitializeParams, 42 connection: DebugIgnore<&'a lsp_server::Connection>, 43 outside_of_project_feedback: FeedbackBookKeeper, 44 router: Router<IO, ConnectionProgressReporter<'a>>, 45 changed_projects: HashSet<Utf8PathBuf>, 46 io: FileSystemProxy<IO>, 47} 48 49impl<'a, IO> LanguageServer<'a, IO> 50where 51 IO: FileSystemReader 52 + FileSystemWriter 53 + BeamCompiler 54 + CommandExecutor 55 + DownloadDependencies 56 + MakeLocker 57 + Clone, 58{ 59 pub fn new(connection: &'a lsp_server::Connection, io: IO) -> Result<Self> { 60 let initialise_params = initialisation_handshake(connection); 61 let reporter = ConnectionProgressReporter::new(connection, &initialise_params); 62 let io = FileSystemProxy::new(io); 63 let router = Router::new(reporter, io.clone()); 64 Ok(Self { 65 connection: connection.into(), 66 initialise_params, 67 changed_projects: HashSet::new(), 68 outside_of_project_feedback: FeedbackBookKeeper::default(), 69 router, 70 io, 71 }) 72 } 73 74 pub fn run(&mut self) -> Result<()> { 75 self.start_watching_gleam_toml(); 76 let mut buffer = MessageBuffer::new(); 77 78 loop { 79 match buffer.receive(*self.connection) { 80 Next::Stop => break, 81 Next::MorePlease => (), 82 Next::Handle(messages) => { 83 for message in messages { 84 self.handle_message(message); 85 } 86 } 87 } 88 } 89 90 Ok(()) 91 } 92 93 fn handle_message(&mut self, message: Message) { 94 match message { 95 Message::Request(id, request) => self.handle_request(id, request), 96 Message::Notification(notification) => self.handle_notification(notification), 97 } 98 } 99 100 fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) { 101 let (payload, feedback) = match request { 102 Request::Format(param) => self.format(param), 103 Request::Hover(param) => self.hover(param), 104 Request::GoToDefinition(param) => self.goto_definition(param), 105 Request::Completion(param) => self.completion(param), 106 Request::CodeAction(param) => self.code_action(param), 107 Request::SignatureHelp(param) => self.signature_help(param), 108 Request::DocumentSymbol(param) => self.document_symbol(param), 109 Request::PrepareRename(param) => self.prepare_rename(param), 110 Request::Rename(param) => self.rename(param), 111 Request::GoToTypeDefinition(param) => self.goto_type_definition(param), 112 Request::FindReferences(param) => self.find_references(param), 113 }; 114 115 self.publish_feedback(feedback); 116 117 let response = lsp_server::Response { 118 id, 119 error: None, 120 result: Some(payload), 121 }; 122 self.connection 123 .sender 124 .send(lsp_server::Message::Response(response)) 125 .expect("channel send LSP response") 126 } 127 128 fn handle_notification(&mut self, notification: Notification) { 129 let feedback = match notification { 130 Notification::CompilePlease => self.compile_please(), 131 Notification::SourceFileMatchesDisc { path } => self.discard_in_memory_cache(path), 132 Notification::SourceFileChangedInMemory { path, text } => { 133 self.cache_file_in_memory(path, text) 134 } 135 Notification::ConfigFileChanged { path } => self.watched_files_changed(path), 136 }; 137 self.publish_feedback(feedback); 138 } 139 140 fn publish_feedback(&self, feedback: Feedback) { 141 self.publish_diagnostics(feedback.diagnostics); 142 self.publish_messages(feedback.messages); 143 } 144 145 fn publish_diagnostics(&self, diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>) { 146 for (path, diagnostics) in diagnostics { 147 let diagnostics = diagnostics 148 .into_iter() 149 .flat_map(diagnostic_to_lsp) 150 .collect::<Vec<_>>(); 151 let uri = path_to_uri(path); 152 153 // Publish the diagnostics 154 let diagnostic_params = PublishDiagnosticsParams { 155 uri, 156 diagnostics, 157 version: None, 158 }; 159 let notification = lsp_server::Notification { 160 method: "textDocument/publishDiagnostics".into(), 161 params: serde_json::to_value(diagnostic_params) 162 .expect("textDocument/publishDiagnostics to json"), 163 }; 164 self.connection 165 .sender 166 .send(lsp_server::Message::Notification(notification)) 167 .expect("send textDocument/publishDiagnostics"); 168 } 169 } 170 171 fn start_watching_gleam_toml(&mut self) { 172 let supports_watch_files = self 173 .initialise_params 174 .capabilities 175 .workspace 176 .as_ref() 177 .and_then(|w| w.did_change_watched_files) 178 .map(|wf| wf.dynamic_registration.unwrap_or(false)) 179 .unwrap_or(false); 180 181 if !supports_watch_files { 182 tracing::warn!("lsp_client_cannot_watch_gleam_toml"); 183 return; 184 } 185 186 // Register gleam.toml as a watched file so we get a notification when 187 // it changes and thus know that we need to rebuild the entire project. 188 let watch_config = lsp::Registration { 189 id: "watch-gleam-toml".into(), 190 method: "workspace/didChangeWatchedFiles".into(), 191 register_options: Some( 192 serde_json::value::to_value(lsp::DidChangeWatchedFilesRegistrationOptions { 193 watchers: vec![lsp::FileSystemWatcher { 194 glob_pattern: "**/gleam.toml".to_string().into(), 195 kind: Some(lsp::WatchKind::Change), 196 }], 197 }) 198 .expect("workspace/didChangeWatchedFiles to json"), 199 ), 200 }; 201 let request = lsp_server::Request { 202 id: 1.into(), 203 method: "client/registerCapability".into(), 204 params: serde_json::value::to_value(lsp::RegistrationParams { 205 registrations: vec![watch_config], 206 }) 207 .expect("client/registerCapability to json"), 208 }; 209 self.connection 210 .sender 211 .send(lsp_server::Message::Request(request)) 212 .expect("send client/registerCapability"); 213 } 214 215 fn publish_messages(&self, messages: Vec<Diagnostic>) { 216 for message in messages { 217 let params = lsp::ShowMessageParams { 218 typ: match message.level { 219 Level::Error => lsp::MessageType::ERROR, 220 Level::Warning => lsp::MessageType::WARNING, 221 }, 222 message: message.text, 223 }; 224 let notification = lsp_server::Notification { 225 method: "window/showMessage".into(), 226 params: serde_json::to_value(params).expect("window/showMessage to json"), 227 }; 228 self.connection 229 .sender 230 .send(lsp_server::Message::Notification(notification)) 231 .expect("send window/showMessage"); 232 } 233 } 234 235 fn respond_with_engine<T, Handler>( 236 &mut self, 237 path: Utf8PathBuf, 238 handler: Handler, 239 ) -> (Json, Feedback) 240 where 241 T: serde::Serialize, 242 Handler: FnOnce( 243 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>, 244 ) -> engine::Response<T>, 245 { 246 match self.router.project_for_path(path) { 247 Ok(Some(project)) => { 248 let engine::Response { 249 result, 250 warnings, 251 compilation, 252 } = handler(&mut project.engine); 253 match result { 254 Ok(value) => { 255 let feedback = project.feedback.response(compilation, warnings); 256 let json = serde_json::to_value(value).expect("response to json"); 257 (json, feedback) 258 } 259 Err(e) => { 260 let feedback = project.feedback.build_with_error(e, compilation, warnings); 261 (Json::Null, feedback) 262 } 263 } 264 } 265 266 Ok(None) => (Json::Null, Feedback::default()), 267 268 Err(error) => (Json::Null, self.outside_of_project_feedback.error(error)), 269 } 270 } 271 272 fn path_error_response(&mut self, path: Utf8PathBuf, error: crate::Error) -> (Json, Feedback) { 273 let feedback = match self.router.project_for_path(path) { 274 Ok(Some(project)) => project.feedback.error(error), 275 Ok(None) | Err(_) => self.outside_of_project_feedback.error(error), 276 }; 277 (Json::Null, feedback) 278 } 279 280 fn format(&mut self, params: lsp::DocumentFormattingParams) -> (Json, Feedback) { 281 let path = super::path(&params.text_document.uri); 282 let mut new_text = String::new(); 283 284 let src = match self.io.read(&path) { 285 Ok(src) => src.into(), 286 Err(error) => return self.path_error_response(path, error), 287 }; 288 289 if let Err(error) = crate::format::pretty(&mut new_text, &src, &path) { 290 return self.path_error_response(path, error); 291 } 292 293 let line_count = src.lines().count() as u32; 294 295 let edit = TextEdit { 296 range: Range::new(Position::new(0, 0), Position::new(line_count, 0)), 297 new_text, 298 }; 299 let json = serde_json::to_value(vec![edit]).expect("to JSON value"); 300 301 (json, Feedback::default()) 302 } 303 304 fn hover(&mut self, params: lsp::HoverParams) -> (Json, Feedback) { 305 let path = super::path(&params.text_document_position_params.text_document.uri); 306 self.respond_with_engine(path, |engine| engine.hover(params)) 307 } 308 309 fn goto_definition(&mut self, params: lsp::GotoDefinitionParams) -> (Json, Feedback) { 310 let path = super::path(&params.text_document_position_params.text_document.uri); 311 self.respond_with_engine(path, |engine| engine.goto_definition(params)) 312 } 313 314 fn goto_type_definition( 315 &mut self, 316 params: lsp_types::GotoDefinitionParams, 317 ) -> (Json, Feedback) { 318 let path = super::path(&params.text_document_position_params.text_document.uri); 319 self.respond_with_engine(path, |engine| engine.goto_type_definition(params)) 320 } 321 322 fn completion(&mut self, params: lsp::CompletionParams) -> (Json, Feedback) { 323 let path = super::path(&params.text_document_position.text_document.uri); 324 325 let src = match self.io.read(&path) { 326 Ok(src) => src.into(), 327 Err(error) => return self.path_error_response(path, error), 328 }; 329 self.respond_with_engine(path, |engine| { 330 engine.completion(params.text_document_position, src) 331 }) 332 } 333 334 fn signature_help(&mut self, params: lsp_types::SignatureHelpParams) -> (Json, Feedback) { 335 let path = super::path(&params.text_document_position_params.text_document.uri); 336 self.respond_with_engine(path, |engine| engine.signature_help(params)) 337 } 338 339 fn code_action(&mut self, params: lsp::CodeActionParams) -> (Json, Feedback) { 340 let path = super::path(&params.text_document.uri); 341 self.respond_with_engine(path, |engine| engine.code_actions(params)) 342 } 343 344 fn document_symbol(&mut self, params: lsp::DocumentSymbolParams) -> (Json, Feedback) { 345 let path = super::path(&params.text_document.uri); 346 self.respond_with_engine(path, |engine| engine.document_symbol(params)) 347 } 348 349 fn prepare_rename(&mut self, params: lsp::TextDocumentPositionParams) -> (Json, Feedback) { 350 let path = super::path(&params.text_document.uri); 351 self.respond_with_engine(path, |engine| engine.prepare_rename(params)) 352 } 353 354 fn rename(&mut self, params: lsp::RenameParams) -> (Json, Feedback) { 355 let path = super::path(&params.text_document_position.text_document.uri); 356 self.respond_with_engine(path, |engine| engine.rename(params)) 357 } 358 359 fn find_references(&mut self, params: lsp_types::ReferenceParams) -> (Json, Feedback) { 360 let path = super::path(&params.text_document_position.text_document.uri); 361 self.respond_with_engine(path, |engine| engine.find_references(params)) 362 } 363 364 fn cache_file_in_memory(&mut self, path: Utf8PathBuf, text: String) -> Feedback { 365 self.project_changed(&path); 366 if let Err(error) = self.io.write_mem_cache(&path, &text) { 367 return self.outside_of_project_feedback.error(error); 368 } 369 Feedback::none() 370 } 371 372 fn discard_in_memory_cache(&mut self, path: Utf8PathBuf) -> Feedback { 373 self.project_changed(&path); 374 if let Err(error) = self.io.delete_mem_cache(&path) { 375 return self.outside_of_project_feedback.error(error); 376 } 377 Feedback::none() 378 } 379 380 fn watched_files_changed(&mut self, path: Utf8PathBuf) -> Feedback { 381 self.router.delete_engine_for_path(&path); 382 Feedback::none() 383 } 384 385 fn compile_please(&mut self) -> Feedback { 386 let mut accumulator = Feedback::none(); 387 let projects = std::mem::take(&mut self.changed_projects); 388 for path in projects { 389 let (_, feedback) = self.respond_with_engine(path, |this| this.compile_please()); 390 accumulator.append_feedback(feedback); 391 } 392 accumulator 393 } 394 395 fn project_changed(&mut self, path: &Utf8Path) { 396 let project_path = self.router.project_path(path); 397 if let Some(project_path) = project_path { 398 _ = self.changed_projects.insert(project_path); 399 } 400 } 401} 402 403fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializeParams { 404 let server_capabilities = lsp::ServerCapabilities { 405 text_document_sync: Some(lsp::TextDocumentSyncCapability::Options( 406 lsp::TextDocumentSyncOptions { 407 open_close: Some(true), 408 change: Some(lsp::TextDocumentSyncKind::FULL), 409 will_save: None, 410 will_save_wait_until: None, 411 save: Some(lsp::TextDocumentSyncSaveOptions::SaveOptions( 412 lsp::SaveOptions { 413 include_text: Some(false), 414 }, 415 )), 416 }, 417 )), 418 selection_range_provider: None, 419 hover_provider: Some(HoverProviderCapability::Simple(true)), 420 completion_provider: Some(lsp::CompletionOptions { 421 resolve_provider: None, 422 trigger_characters: Some(vec![".".into()]), 423 all_commit_characters: None, 424 work_done_progress_options: lsp::WorkDoneProgressOptions { 425 work_done_progress: None, 426 }, 427 completion_item: None, 428 }), 429 signature_help_provider: Some(lsp::SignatureHelpOptions { 430 trigger_characters: Some(vec!["(".into(), ",".into(), ":".into()]), 431 retrigger_characters: None, 432 work_done_progress_options: lsp::WorkDoneProgressOptions { 433 work_done_progress: None, 434 }, 435 }), 436 definition_provider: Some(lsp::OneOf::Left(true)), 437 type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)), 438 implementation_provider: None, 439 references_provider: Some(lsp::OneOf::Left(true)), 440 document_highlight_provider: None, 441 document_symbol_provider: Some(lsp::OneOf::Left(true)), 442 workspace_symbol_provider: None, 443 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)), 444 code_lens_provider: None, 445 document_formatting_provider: Some(lsp::OneOf::Left(true)), 446 document_range_formatting_provider: None, 447 document_on_type_formatting_provider: None, 448 rename_provider: Some(lsp::OneOf::Right(RenameOptions { 449 prepare_provider: Some(true), 450 work_done_progress_options: lsp::WorkDoneProgressOptions { 451 work_done_progress: None, 452 }, 453 })), 454 document_link_provider: None, 455 color_provider: None, 456 folding_range_provider: None, 457 declaration_provider: None, 458 execute_command_provider: None, 459 workspace: None, 460 call_hierarchy_provider: None, 461 semantic_tokens_provider: None, 462 moniker_provider: None, 463 linked_editing_range_provider: None, 464 experimental: None, 465 position_encoding: None, 466 inline_value_provider: None, 467 inlay_hint_provider: None, 468 diagnostic_provider: None, 469 }; 470 let server_capabilities_json = 471 serde_json::to_value(server_capabilities).expect("server_capabilities_serde"); 472 let initialise_params_json = connection 473 .initialize(server_capabilities_json) 474 .expect("LSP initialize"); 475 let initialise_params: InitializeParams = 476 serde_json::from_value(initialise_params_json).expect("LSP InitializeParams from json"); 477 initialise_params 478} 479 480fn diagnostic_to_lsp(diagnostic: Diagnostic) -> Vec<lsp::Diagnostic> { 481 let severity = match diagnostic.level { 482 Level::Error => lsp::DiagnosticSeverity::ERROR, 483 Level::Warning => lsp::DiagnosticSeverity::WARNING, 484 }; 485 let hint = diagnostic.hint; 486 let mut text = diagnostic.title; 487 488 if let Some(label) = diagnostic 489 .location 490 .as_ref() 491 .and_then(|location| location.label.text.as_deref()) 492 { 493 text.push_str("\n\n"); 494 text.push_str(label); 495 if !label.ends_with(['.', '?']) { 496 text.push('.'); 497 } 498 } 499 500 if !diagnostic.text.is_empty() { 501 text.push_str("\n\n"); 502 text.push_str(&diagnostic.text); 503 } 504 505 // TODO: Redesign the diagnostic type so that we can be sure there is always 506 // a location. Locationless diagnostics would be handled separately. 507 let location = diagnostic 508 .location 509 .expect("Diagnostic given to LSP without location"); 510 let line_numbers = LineNumbers::new(&location.src); 511 let path = path_to_uri(location.path); 512 let range = src_span_to_lsp_range(location.label.span, &line_numbers); 513 514 let related_info = location 515 .extra_labels 516 .iter() 517 .map(|extra| { 518 let message = extra.label.text.clone().unwrap_or_default(); 519 let location = match &extra.src_info { 520 Some((src, path)) => { 521 let line_numbers = LineNumbers::new(src); 522 lsp::Location { 523 uri: path_to_uri(path.clone()), 524 range: src_span_to_lsp_range(extra.label.span, &line_numbers), 525 } 526 } 527 _ => lsp::Location { 528 uri: path.clone(), 529 range: src_span_to_lsp_range(extra.label.span, &line_numbers), 530 }, 531 }; 532 lsp::DiagnosticRelatedInformation { location, message } 533 }) 534 .collect_vec(); 535 536 let main = lsp::Diagnostic { 537 range, 538 severity: Some(severity), 539 code: None, 540 code_description: None, 541 source: None, 542 message: text, 543 related_information: if related_info.is_empty() { 544 None 545 } else { 546 Some(related_info) 547 }, 548 tags: None, 549 data: None, 550 }; 551 552 match hint { 553 Some(hint) => { 554 let hint = lsp::Diagnostic { 555 severity: Some(lsp::DiagnosticSeverity::HINT), 556 message: hint, 557 ..main.clone() 558 }; 559 vec![main, hint] 560 } 561 None => vec![main], 562 } 563} 564 565fn path_to_uri(path: Utf8PathBuf) -> Url { 566 let mut file: String = "file://".into(); 567 file.push_str(&path.as_os_str().to_string_lossy()); 568 Url::parse(&file).expect("path_to_uri URL parse") 569}