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

Configure Feed

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

gleam / language-server / src / server.rs
28 kB 766 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2023 The Gleam contributors 3 4use super::{ 5 DownloadDependencies, MakeLocker, 6 engine::{self, LanguageServerEngine}, 7 feedback::{Feedback, FeedbackBookKeeper}, 8 files::FileSystemProxy, 9 messages::{Message, MessageBuffer, Next, Notification, Request}, 10 progress::ConnectionProgressReporter, 11 router::Router, 12 src_span_to_lsp_range, 13}; 14use camino::{Utf8Path, Utf8PathBuf}; 15use debug_ignore::DebugIgnore; 16use gleam_core::{ 17 Result, 18 diagnostic::{Diagnostic, ExtraLabel, Level}, 19 io::{BeamCompilerIO, CommandExecutor, FileSystemReader, FileSystemWriter}, 20 line_numbers::LineNumbers, 21}; 22use itertools::Itertools; 23use lsp_server::ResponseError; 24use lsp_types::{ 25 self as lsp, FileOperationFilter, FileOperationOptions, FileOperationPattern, 26 FileOperationPatternKind, FileOperationRegistrationOptions, InitializeParams, Position, 27 PublishDiagnosticsParams, Range, RenameFilesParams, RenameOptions, TextEdit, Uri as Url, 28 WorkspaceOptions, 29}; 30use serde_json::Value as Json; 31use std::collections::{HashMap, HashSet}; 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 changed_projects: HashSet<Utf8PathBuf>, 50 io: FileSystemProxy<IO>, 51} 52 53impl<'a, IO> LanguageServer<'a, IO> 54where 55 IO: FileSystemReader 56 + FileSystemWriter 57 + BeamCompilerIO 58 + CommandExecutor 59 + DownloadDependencies 60 + MakeLocker 61 + Clone, 62{ 63 pub fn new(connection: &'a lsp_server::Connection, io: IO) -> Result<Self> { 64 let initialise_params = initialisation_handshake(connection); 65 let reporter = ConnectionProgressReporter::new(connection, &initialise_params); 66 let io = FileSystemProxy::new(io); 67 let router = Router::new(reporter, io.clone()); 68 Ok(Self { 69 connection: connection.into(), 70 initialise_params, 71 changed_projects: HashSet::new(), 72 outside_of_project_feedback: FeedbackBookKeeper::default(), 73 router, 74 io, 75 }) 76 } 77 78 pub fn run(&mut self) -> Result<()> { 79 self.start_watching_gleam_toml(); 80 let mut buffer = MessageBuffer::new(); 81 82 loop { 83 match buffer.receive(*self.connection) { 84 Next::Stop => break, 85 Next::MorePlease => (), 86 Next::Handle(messages) => { 87 for message in messages { 88 self.handle_message(message); 89 } 90 } 91 } 92 } 93 94 Ok(()) 95 } 96 97 fn handle_message(&mut self, message: Message) { 98 match message { 99 Message::Request(id, request) => self.handle_request(id, request), 100 Message::Notification(notification) => self.handle_notification(notification), 101 } 102 } 103 104 fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) { 105 let (outcome, feedback) = match request { 106 Request::Format(param) => self.format(param), 107 Request::Hover(param) => self.hover(param), 108 Request::GoToDefinition(param) => self.goto_definition(param), 109 Request::Completion(param) => self.completion(param), 110 Request::CodeAction(param) => self.code_action(param), 111 Request::SignatureHelp(param) => self.signature_help(param), 112 Request::DocumentSymbol(param) => self.document_symbol(param), 113 Request::FoldingRange(param) => self.folding_range(param), 114 Request::PrepareRename(param) => self.prepare_rename(param), 115 Request::Rename(param) => self.rename(param), 116 Request::GoToTypeDefinition(param) => self.goto_type_definition(param), 117 Request::FindReferences(param) => self.find_references(param), 118 Request::DocumentHighlight(param) => self.document_highlight(param), 119 Request::RenameFiles(param) => self.rename_files(param), 120 }; 121 122 self.publish_feedback(feedback); 123 124 let response = match outcome { 125 Ok(payload) => lsp_server::Response { 126 id, 127 error: None, 128 result: Some(payload), 129 }, 130 Err(error) => lsp_server::Response { 131 id, 132 error: Some(error), 133 result: None, 134 }, 135 }; 136 137 self.connection 138 .sender 139 .send(lsp_server::Message::Response(response)) 140 .expect("channel send LSP response"); 141 } 142 143 fn handle_notification(&mut self, notification: Notification) { 144 let feedback = match notification { 145 Notification::CompilePlease => self.compile_please(), 146 Notification::SourceFileOpened { path, text } => self.source_file_opened(path, text), 147 Notification::SourceFileClosed { path } => self.source_file_closed(path), 148 Notification::SourceFileSaved { path } => self.discard_in_memory_cache(path), 149 Notification::SourceFileChangedInMemory { path, text } => { 150 self.cache_file_in_memory(path, text) 151 } 152 Notification::ConfigFileChanged { path } => self.watched_files_changed(path), 153 }; 154 self.publish_feedback(feedback); 155 } 156 157 fn publish_feedback(&self, feedback: Feedback) { 158 self.publish_diagnostics(feedback.diagnostics); 159 self.publish_messages(feedback.messages); 160 } 161 162 fn publish_diagnostics(&self, diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>) { 163 for (path, diagnostics) in diagnostics { 164 let diagnostics = diagnostics 165 .into_iter() 166 .flat_map(diagnostic_to_lsp) 167 .collect::<Vec<_>>(); 168 let uri = path_to_uri(path); 169 170 // Publish the diagnostics 171 let diagnostic_params = PublishDiagnosticsParams { 172 uri, 173 diagnostics, 174 version: None, 175 }; 176 let notification = lsp_server::Notification { 177 method: "textDocument/publishDiagnostics".into(), 178 params: serde_json::to_value(diagnostic_params) 179 .expect("textDocument/publishDiagnostics to json"), 180 }; 181 self.connection 182 .sender 183 .send(lsp_server::Message::Notification(notification)) 184 .expect("send textDocument/publishDiagnostics"); 185 } 186 } 187 188 fn start_watching_gleam_toml(&mut self) { 189 let supports_watch_files = self 190 .initialise_params 191 .capabilities 192 .workspace 193 .as_ref() 194 .and_then(|w| w.did_change_watched_files) 195 .map(|wf| wf.dynamic_registration.unwrap_or(false)) 196 .unwrap_or(false); 197 198 if !supports_watch_files { 199 tracing::warn!("lsp_client_cannot_watch_gleam_toml"); 200 return; 201 } 202 203 // Register gleam.toml as a watched file so we get a notification when 204 // it changes and thus know that we need to rebuild the entire project. 205 let watch_config = lsp::Registration { 206 id: "watch-gleam-toml".into(), 207 method: "workspace/didChangeWatchedFiles".into(), 208 register_options: Some( 209 serde_json::value::to_value(lsp::DidChangeWatchedFilesRegistrationOptions { 210 watchers: vec![lsp::FileSystemWatcher { 211 glob_pattern: "**/gleam.toml".to_string().into(), 212 kind: Some(lsp::WatchKind::Change), 213 }], 214 }) 215 .expect("workspace/didChangeWatchedFiles to json"), 216 ), 217 }; 218 let request = lsp_server::Request { 219 id: 1.into(), 220 method: "client/registerCapability".into(), 221 params: serde_json::value::to_value(lsp::RegistrationParams { 222 registrations: vec![watch_config], 223 }) 224 .expect("client/registerCapability to json"), 225 }; 226 self.connection 227 .sender 228 .send(lsp_server::Message::Request(request)) 229 .expect("send client/registerCapability"); 230 } 231 232 fn publish_messages(&self, messages: Vec<Diagnostic>) { 233 for message in messages { 234 let params = lsp::ShowMessageParams { 235 kind: match message.level { 236 Level::Error => lsp::MessageType::Error, 237 Level::Warning => lsp::MessageType::Warning, 238 }, 239 message: message.text, 240 }; 241 let notification = lsp_server::Notification { 242 method: "window/showMessage".into(), 243 params: serde_json::to_value(params).expect("window/showMessage to json"), 244 }; 245 self.connection 246 .sender 247 .send(lsp_server::Message::Notification(notification)) 248 .expect("send window/showMessage"); 249 } 250 } 251 252 fn respond_with_engine<T, Handler>( 253 &mut self, 254 path: Utf8PathBuf, 255 handler: Handler, 256 ) -> (Result<Json, ResponseError>, Feedback) 257 where 258 T: serde::Serialize, 259 Handler: FnOnce( 260 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>, 261 ) -> engine::Response<T>, 262 { 263 self.fallible_respond_with_engine(path, |engine| { 264 let response = handler(engine); 265 engine::Response { 266 result: response.result.map(Ok), 267 warnings: response.warnings, 268 compilation: response.compilation, 269 } 270 }) 271 } 272 273 fn fallible_respond_with_engine<T, Handler>( 274 &mut self, 275 path: Utf8PathBuf, 276 handler: Handler, 277 ) -> (Result<Json, ResponseError>, Feedback) 278 where 279 T: serde::Serialize, 280 Handler: FnOnce( 281 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>, 282 ) -> engine::Response<Result<T, ResponseError>>, 283 { 284 match self.router.project_for_path(path) { 285 Ok(Some(project)) => { 286 let engine::Response { 287 result, 288 warnings, 289 compilation, 290 } = handler(&mut project.engine); 291 match result { 292 Ok(Ok(value)) => { 293 let feedback = project.feedback.response(compilation, warnings); 294 let json = serde_json::to_value(value).expect("response to json"); 295 (Ok(json), feedback) 296 } 297 Ok(Err(error)) => { 298 let feedback = project.feedback.response(compilation, warnings); 299 (Err(error), feedback) 300 } 301 Err(e) => { 302 let feedback = project.feedback.build_with_error(e, compilation, warnings); 303 (Ok(Json::Null), feedback) 304 } 305 } 306 } 307 308 Ok(None) => (Ok(Json::Null), Feedback::default()), 309 310 Err(error) => ( 311 Ok(Json::Null), 312 self.outside_of_project_feedback.error(error), 313 ), 314 } 315 } 316 317 fn path_error_response( 318 &mut self, 319 path: Utf8PathBuf, 320 error: gleam_core::Error, 321 ) -> (Result<Json, ResponseError>, Feedback) { 322 let feedback = match self.router.project_for_path(path) { 323 Ok(Some(project)) => project.feedback.error(error), 324 Ok(None) | Err(_) => self.outside_of_project_feedback.error(error), 325 }; 326 (Ok(Json::Null), feedback) 327 } 328 329 fn format( 330 &mut self, 331 params: lsp::DocumentFormattingParams, 332 ) -> (Result<Json, ResponseError>, Feedback) { 333 let path = super::path(&params.text_document.uri); 334 let mut new_text = String::new(); 335 336 let src = match self.io.read(&path) { 337 Ok(src) => src.into(), 338 Err(error) => return self.path_error_response(path, error), 339 }; 340 341 if let Err(error) = gleam_format::pretty(&mut new_text, &src, &path) { 342 return self.path_error_response(path, error); 343 } 344 345 let line_count = src.lines().count() as u32; 346 347 let edit = TextEdit { 348 range: Range::new(Position::new(0, 0), Position::new(line_count, 0)), 349 new_text, 350 }; 351 let json = serde_json::to_value(vec![edit]).expect("to JSON value"); 352 353 (Ok(json), Feedback::default()) 354 } 355 356 fn hover(&mut self, params: lsp::HoverParams) -> (Result<Json, ResponseError>, Feedback) { 357 let path = super::path(&params.text_document_position_params.text_document.uri); 358 self.respond_with_engine(path, |engine| engine.hover(params)) 359 } 360 361 fn goto_definition( 362 &mut self, 363 params: lsp::DefinitionParams, 364 ) -> (Result<Json, ResponseError>, Feedback) { 365 let path = super::path(&params.text_document_position_params.text_document.uri); 366 self.respond_with_engine(path, |engine| engine.goto_definition(params)) 367 } 368 369 fn goto_type_definition( 370 &mut self, 371 params: lsp_types::TypeDefinitionParams, 372 ) -> (Result<Json, ResponseError>, Feedback) { 373 let path = super::path(&params.text_document_position_params.text_document.uri); 374 self.respond_with_engine(path, |engine| engine.goto_type_definition(params)) 375 } 376 377 fn completion( 378 &mut self, 379 params: lsp::CompletionParams, 380 ) -> (Result<Json, ResponseError>, Feedback) { 381 let path = super::path(&params.text_document_position_params.text_document.uri); 382 383 let src = match self.io.read(&path) { 384 Ok(src) => src.into(), 385 Err(error) => return self.path_error_response(path, error), 386 }; 387 self.respond_with_engine(path, |engine| { 388 engine.completion(params.text_document_position_params, src) 389 }) 390 } 391 392 fn signature_help( 393 &mut self, 394 params: lsp_types::SignatureHelpParams, 395 ) -> (Result<Json, ResponseError>, Feedback) { 396 let path = super::path(&params.text_document_position_params.text_document.uri); 397 self.respond_with_engine(path, |engine| engine.signature_help(params)) 398 } 399 400 fn code_action( 401 &mut self, 402 params: lsp::CodeActionParams, 403 ) -> (Result<Json, ResponseError>, Feedback) { 404 let path = super::path(&params.text_document.uri); 405 self.respond_with_engine(path, |engine| engine.code_actions(params)) 406 } 407 408 fn document_symbol( 409 &mut self, 410 params: lsp::DocumentSymbolParams, 411 ) -> (Result<Json, ResponseError>, Feedback) { 412 let path = super::path(&params.text_document.uri); 413 self.respond_with_engine(path, |engine| engine.document_symbol(params)) 414 } 415 416 fn folding_range( 417 &mut self, 418 params: lsp::FoldingRangeParams, 419 ) -> (Result<Json, ResponseError>, Feedback) { 420 let path = super::path(&params.text_document.uri); 421 self.respond_with_engine(path, |engine| engine.folding_range(params)) 422 } 423 424 fn prepare_rename( 425 &mut self, 426 params: lsp::PrepareRenameParams, 427 ) -> (Result<Json, ResponseError>, Feedback) { 428 let path = super::path(&params.text_document_position_params.text_document.uri); 429 self.respond_with_engine(path, |engine| engine.prepare_rename(params)) 430 } 431 432 fn rename(&mut self, params: lsp::RenameParams) -> (Result<Json, ResponseError>, Feedback) { 433 let path = super::path(&params.text_document_position_params.text_document.uri); 434 self.fallible_respond_with_engine( 435 path, 436 |engine: &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>| { 437 engine.rename(params) 438 }, 439 ) 440 } 441 442 fn rename_files( 443 &mut self, 444 params: RenameFilesParams, 445 ) -> (Result<Json, ResponseError>, Feedback) { 446 let renames = params 447 .files 448 .into_iter() 449 .map(|file| { 450 ( 451 Url::parse(&file.old_uri).expect("Uri should be valid"), 452 Url::parse(&file.new_uri).expect("Uri should be valid"), 453 ) 454 }) 455 .collect_vec(); 456 457 let Some((_, first_renamed_file)) = renames.first() else { 458 return (Ok(serde_json::json!(null)), Feedback::none()); 459 }; 460 461 self.respond_with_engine(super::path(first_renamed_file), |engine| { 462 engine.rename_files(renames) 463 }) 464 } 465 466 fn find_references( 467 &mut self, 468 params: lsp_types::ReferenceParams, 469 ) -> (Result<Json, ResponseError>, Feedback) { 470 let path = super::path(&params.text_document_position_params.text_document.uri); 471 self.respond_with_engine(path, |engine| engine.find_references(params)) 472 } 473 474 fn document_highlight( 475 &mut self, 476 params: lsp_types::DocumentHighlightParams, 477 ) -> (Result<Json, ResponseError>, Feedback) { 478 let path = super::path(&params.text_document_position_params.text_document.uri); 479 self.respond_with_engine(path, |engine| engine.document_highlight(params)) 480 } 481 482 fn cache_file_in_memory(&mut self, path: Utf8PathBuf, text: String) -> Feedback { 483 self.project_changed(&path); 484 if let Err(error) = self.io.write_mem_cache(&path, &text) { 485 return self.outside_of_project_feedback.error(error); 486 } 487 Feedback::none() 488 } 489 490 fn discard_in_memory_cache(&mut self, path: Utf8PathBuf) -> Feedback { 491 self.project_changed(&path); 492 if let Err(error) = self.io.delete_mem_cache(&path) { 493 return self.outside_of_project_feedback.error(error); 494 } 495 Feedback::none() 496 } 497 498 fn watched_files_changed(&mut self, path: Utf8PathBuf) -> Feedback { 499 self.router.delete_engine_for_path(&path); 500 Feedback::none() 501 } 502 503 fn compile_please(&mut self) -> Feedback { 504 let mut accumulator = Feedback::none(); 505 let projects = std::mem::take(&mut self.changed_projects); 506 for path in projects { 507 let (_, feedback) = self.respond_with_engine(path, |this| this.compile_please()); 508 accumulator.append_feedback(feedback); 509 } 510 accumulator 511 } 512 513 fn project_changed(&mut self, path: &Utf8Path) { 514 let project_path = self.router.project_path(path); 515 if let Some(project_path) = project_path { 516 _ = self.changed_projects.insert(project_path); 517 } 518 } 519 520 fn source_file_opened(&mut self, path: Utf8PathBuf, text: String) -> Feedback { 521 let mut feedback = self.cache_file_in_memory(path.clone(), text); 522 if let Ok(Some(project)) = self.router.project_for_path(path.clone()) { 523 feedback.append_feedback(project.feedback.open_file(path)); 524 } 525 feedback 526 } 527 528 fn source_file_closed(&mut self, path: Utf8PathBuf) -> Feedback { 529 let mut feedback = self.discard_in_memory_cache(path.clone()); 530 if let Ok(Some(project)) = self.router.project_for_path(path.clone()) { 531 feedback.append_feedback(project.feedback.close_file(&path)); 532 } 533 feedback 534 } 535} 536 537fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializeParams { 538 let server_capabilities = lsp::ServerCapabilities { 539 text_document_sync: Some( 540 lsp::TextDocumentSyncOptions { 541 open_close: Some(true), 542 change: Some(lsp::TextDocumentSyncKind::Full), 543 will_save: None, 544 will_save_wait_until: None, 545 save: Some( 546 lsp::SaveOptions { 547 include_text: Some(false), 548 } 549 .into(), 550 ), 551 } 552 .into(), 553 ), 554 selection_range_provider: None, 555 hover_provider: Some(true.into()), 556 completion_provider: Some(lsp::CompletionOptions { 557 resolve_provider: None, 558 trigger_characters: Some(vec![".".into()]), 559 all_commit_characters: None, 560 work_done_progress_options: lsp::WorkDoneProgressOptions { 561 work_done_progress: None, 562 }, 563 completion_item: None, 564 }), 565 signature_help_provider: Some(lsp::SignatureHelpOptions { 566 trigger_characters: Some(vec!["(".into(), ",".into(), ":".into()]), 567 retrigger_characters: None, 568 work_done_progress_options: lsp::WorkDoneProgressOptions { 569 work_done_progress: None, 570 }, 571 }), 572 definition_provider: Some(true.into()), 573 type_definition_provider: Some(true.into()), 574 implementation_provider: None, 575 references_provider: Some(true.into()), 576 document_highlight_provider: Some(true.into()), 577 document_symbol_provider: Some(true.into()), 578 workspace_symbol_provider: None, 579 code_action_provider: Some(true.into()), 580 code_lens_provider: None, 581 document_formatting_provider: Some(true.into()), 582 document_range_formatting_provider: None, 583 document_on_type_formatting_provider: None, 584 rename_provider: Some( 585 RenameOptions { 586 prepare_provider: Some(true), 587 work_done_progress_options: lsp::WorkDoneProgressOptions { 588 work_done_progress: None, 589 }, 590 } 591 .into(), 592 ), 593 document_link_provider: None, 594 color_provider: None, 595 folding_range_provider: Some(true.into()), 596 declaration_provider: None, 597 execute_command_provider: None, 598 workspace: Some(WorkspaceOptions { 599 workspace_folders: None, 600 file_operations: Some(FileOperationOptions { 601 did_create: None, 602 will_create: None, 603 will_rename: Some(FileOperationRegistrationOptions { 604 filters: vec![FileOperationFilter { 605 scheme: Some("file".into()), 606 pattern: FileOperationPattern { 607 glob: "**/*.gleam".into(), 608 matches: Some(FileOperationPatternKind::File), 609 options: None, 610 }, 611 }], 612 }), 613 did_rename: None, 614 did_delete: None, 615 will_delete: None, 616 }), 617 text_document_content: None, 618 }), 619 call_hierarchy_provider: None, 620 semantic_tokens_provider: None, 621 moniker_provider: None, 622 linked_editing_range_provider: None, 623 experimental: None, 624 position_encoding: None, 625 inline_value_provider: None, 626 inlay_hint_provider: None, 627 diagnostic_provider: None, 628 type_hierarchy_provider: None, 629 notebook_document_sync: None, 630 inline_completion_provider: None, 631 }; 632 let server_capabilities_json = 633 serde_json::to_value(server_capabilities).expect("server_capabilities_serde"); 634 let initialise_params_json = connection 635 .initialize(server_capabilities_json) 636 .expect("LSP initialize"); 637 let initialise_params: InitializeParams = 638 serde_json::from_value(initialise_params_json).expect("LSP InitializeParams from json"); 639 initialise_params 640} 641 642fn diagnostic_to_lsp(diagnostic: Diagnostic) -> Vec<lsp::Diagnostic> { 643 let severity = match diagnostic.level { 644 Level::Error => lsp::DiagnosticSeverity::Error, 645 Level::Warning => lsp::DiagnosticSeverity::Warning, 646 }; 647 let hint = diagnostic.hint; 648 let mut text = diagnostic.title; 649 650 if let Some(label) = diagnostic 651 .location 652 .as_ref() 653 .and_then(|location| location.label.text.as_deref()) 654 { 655 text.push_str("\n\n"); 656 text.push_str(label); 657 if !label.ends_with(['.', '?']) { 658 text.push('.'); 659 } 660 } 661 662 if !diagnostic.text.is_empty() { 663 text.push_str("\n\n"); 664 text.push_str(&diagnostic.text); 665 } 666 667 // TODO: Redesign the diagnostic type so that we can be sure there is always 668 // a location. Locationless diagnostics would be handled separately. 669 let location = diagnostic 670 .location 671 .expect("Diagnostic given to LSP without location"); 672 let line_numbers = LineNumbers::new(&location.src); 673 let path = path_to_uri(location.path); 674 let range = src_span_to_lsp_range(location.label.span, &line_numbers); 675 676 let main = lsp::Diagnostic { 677 range, 678 severity: Some(severity), 679 code: None, 680 code_description: None, 681 source: None, 682 message: text, 683 related_information: related_information( 684 &hint, 685 &location.extra_labels, 686 &path, 687 &line_numbers, 688 range, 689 ), 690 tags: None, 691 data: None, 692 }; 693 694 match hint { 695 Some(hint) => { 696 let hint = lsp::Diagnostic { 697 severity: Some(lsp::DiagnosticSeverity::Hint), 698 message: hint, 699 // Some editors require this kind of "link" to group diagnostics. 700 // For example, in Zed "go to next diagnostic" would move you from 701 // the warning to the hint in the same location without this. 702 related_information: Some(vec![lsp::DiagnosticRelatedInformation { 703 location: lsp::Location { uri: path, range }, 704 message: String::new(), 705 }]), 706 ..main.clone() 707 }; 708 vec![main, hint] 709 } 710 None => vec![main], 711 } 712} 713 714fn related_information( 715 hint: &Option<String>, 716 extra_labels: &[ExtraLabel], 717 path: &Url, 718 line_numbers: &LineNumbers, 719 range: Range, 720) -> Option<Vec<lsp::DiagnosticRelatedInformation>> { 721 let mut related_info = Vec::with_capacity(extra_labels.len() + 1); 722 723 // The hint is included as a dedicated diagnostic _and_ the related information 724 // to maximize compatibility 725 if let Some(hint) = hint { 726 let hint = lsp::DiagnosticRelatedInformation { 727 message: hint.clone(), 728 location: lsp::Location { 729 uri: path.clone(), 730 range, 731 }, 732 }; 733 related_info.push(hint); 734 } 735 736 let additional_info = extra_labels.iter().map(|extra| { 737 let message = extra.label.text.clone().unwrap_or_default(); 738 let location = match &extra.src_info { 739 Some((src, path)) => { 740 let line_numbers = LineNumbers::new(src); 741 lsp::Location { 742 uri: path_to_uri(path.clone()), 743 range: src_span_to_lsp_range(extra.label.span, &line_numbers), 744 } 745 } 746 _ => lsp::Location { 747 uri: path.clone(), 748 range: src_span_to_lsp_range(extra.label.span, line_numbers), 749 }, 750 }; 751 lsp::DiagnosticRelatedInformation { location, message } 752 }); 753 related_info.extend(additional_info); 754 755 if related_info.is_empty() { 756 None 757 } else { 758 Some(related_info) 759 } 760} 761 762fn path_to_uri(path: Utf8PathBuf) -> Url { 763 let mut file: String = "file://".into(); 764 file.push_str(&path.as_os_str().to_string_lossy()); 765 Url::parse(&file).expect("path_to_uri URL parse") 766}