Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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