Fork of daniellemaywood.uk/gleam — Wasm codegen work
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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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(¶ms.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}