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