this repo has no description
at wasm 1632 lines 62 kB view raw
1use crate::{ 2 Error, Result, Warning, 3 analyse::name::correct_name_case, 4 ast::{ 5 self, Constant, CustomType, Definition, DefinitionLocation, ModuleConstant, 6 PatternUnusedArguments, SrcSpan, TypedArg, TypedConstant, TypedExpr, TypedFunction, 7 TypedModule, TypedPattern, 8 }, 9 build::{ 10 ExpressionPosition, Located, Module, UnqualifiedImport, type_constructor_from_modules, 11 }, 12 config::PackageConfig, 13 io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter}, 14 language_server::{ 15 code_action::{ 16 AddOmittedLabels, CollapseNestedCase, ExtractFunction, RemoveBlock, 17 RemovePrivateOpaque, RemoveUnreachableBranches, 18 }, 19 compiler::LspProjectCompiler, 20 files::FileSystemProxy, 21 progress::ProgressReporter, 22 reference::FindVariableReferences, 23 }, 24 line_numbers::LineNumbers, 25 paths::ProjectPaths, 26 type_::{ 27 self, Deprecation, ModuleInterface, Type, TypeConstructor, ValueConstructor, 28 ValueConstructorVariant, 29 error::{Named, VariableSyntax}, 30 printer::Printer, 31 }, 32}; 33use camino::Utf8PathBuf; 34use ecow::EcoString; 35use itertools::Itertools; 36use lsp::CodeAction; 37use lsp_types::{ 38 self as lsp, DocumentSymbol, Hover, HoverContents, MarkedString, Position, 39 PrepareRenameResponse, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Url, 40 WorkspaceEdit, 41}; 42use std::{collections::HashSet, sync::Arc}; 43 44use super::{ 45 DownloadDependencies, MakeLocker, 46 code_action::{ 47 AddAnnotations, CodeActionBuilder, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe, 48 ConvertToUse, ExpandFunctionCapture, ExtractConstant, ExtractVariable, 49 FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation, 50 FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder, 51 GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue, 52 RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UseLabelShorthandSyntax, 53 WrapInBlock, code_action_add_missing_patterns, 54 code_action_convert_qualified_constructor_to_unqualified, 55 code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, 56 code_action_inexhaustive_let_to_case, 57 }, 58 completer::Completer, 59 reference::{ 60 Referenced, VariableReferenceKind, find_module_references, reference_for_ast_node, 61 }, 62 rename::{RenameTarget, Renamed, rename_local_variable, rename_module_entity}, 63 signature_help, src_span_to_lsp_range, 64}; 65 66#[derive(Debug, PartialEq, Eq)] 67pub struct Response<T> { 68 pub result: Result<T, Error>, 69 pub warnings: Vec<Warning>, 70 pub compilation: Compilation, 71} 72 73#[derive(Debug, PartialEq, Eq)] 74pub enum Compilation { 75 /// Compilation was attempted and succeeded for these modules. 76 Yes(Vec<Utf8PathBuf>), 77 /// Compilation was not attempted for this operation. 78 No, 79} 80 81#[derive(Debug)] 82pub struct LanguageServerEngine<IO, Reporter> { 83 pub(crate) paths: ProjectPaths, 84 85 /// A compiler for the project that supports repeat compilation of the root 86 /// package. 87 /// In the event the project config changes this will need to be 88 /// discarded and reloaded to handle any changes to dependencies. 89 pub(crate) compiler: LspProjectCompiler<FileSystemProxy<IO>>, 90 91 modules_compiled_since_last_feedback: Vec<Utf8PathBuf>, 92 compiled_since_last_feedback: bool, 93 error: Option<Error>, 94 95 // Used to publish progress notifications to the client without waiting for 96 // the usual request-response loop. 97 progress_reporter: Reporter, 98 99 /// Used to know if to show the "View on HexDocs" link 100 /// when hovering on an imported value 101 hex_deps: HashSet<EcoString>, 102} 103 104impl<'a, IO, Reporter> LanguageServerEngine<IO, Reporter> 105where 106 // IO to be supplied from outside of gleam-core 107 IO: FileSystemReader 108 + FileSystemWriter 109 + BeamCompiler 110 + CommandExecutor 111 + DownloadDependencies 112 + MakeLocker 113 + Clone, 114 // IO to be supplied from inside of gleam-core 115 Reporter: ProgressReporter + Clone + 'a, 116{ 117 pub fn new( 118 config: PackageConfig, 119 progress_reporter: Reporter, 120 io: FileSystemProxy<IO>, 121 paths: ProjectPaths, 122 ) -> Result<Self> { 123 let locker = io.inner().make_locker(&paths, config.target)?; 124 125 // Download dependencies to ensure they are up-to-date for this new 126 // configuration and new instance of the compiler 127 progress_reporter.dependency_downloading_started(); 128 let manifest = io.inner().download_dependencies(&paths); 129 progress_reporter.dependency_downloading_finished(); 130 131 // NOTE: This must come after the progress reporter has finished! 132 let manifest = manifest?; 133 134 let compiler: LspProjectCompiler<FileSystemProxy<IO>> = 135 LspProjectCompiler::new(manifest, config, paths.clone(), io.clone(), locker)?; 136 137 let hex_deps = compiler 138 .project_compiler 139 .packages 140 .iter() 141 .flat_map(|(k, v)| match &v.source { 142 crate::manifest::ManifestPackageSource::Hex { .. } => { 143 Some(EcoString::from(k.as_str())) 144 } 145 146 _ => None, 147 }) 148 .collect(); 149 150 Ok(Self { 151 modules_compiled_since_last_feedback: vec![], 152 compiled_since_last_feedback: false, 153 progress_reporter, 154 compiler, 155 paths, 156 error: None, 157 hex_deps, 158 }) 159 } 160 161 pub fn compile_please(&mut self) -> Response<()> { 162 self.respond(Self::compile) 163 } 164 165 /// Compile the project if we are in one. Otherwise do nothing. 166 fn compile(&mut self) -> Result<(), Error> { 167 self.compiled_since_last_feedback = true; 168 169 self.progress_reporter.compilation_started(); 170 let outcome = self.compiler.compile(); 171 self.progress_reporter.compilation_finished(); 172 173 let result = outcome 174 // Register which modules have changed 175 .map(|modules| self.modules_compiled_since_last_feedback.extend(modules)) 176 // Return the error, if present 177 .into_result(); 178 179 self.error = match &result { 180 Ok(_) => None, 181 Err(error) => Some(error.clone()), 182 }; 183 184 result 185 } 186 187 fn take_warnings(&mut self) -> Vec<Warning> { 188 self.compiler.take_warnings() 189 } 190 191 // TODO: implement unqualified imported module functions 192 // 193 pub fn goto_definition( 194 &mut self, 195 params: lsp::GotoDefinitionParams, 196 ) -> Response<Option<lsp::Location>> { 197 self.respond(|this| { 198 let params = params.text_document_position_params; 199 let (line_numbers, node) = match this.node_at_position(&params) { 200 Some(location) => location, 201 None => return Ok(None), 202 }; 203 204 let Some(location) = 205 node.definition_location(this.compiler.project_compiler.get_importable_modules()) 206 else { 207 return Ok(None); 208 }; 209 210 Ok(this.definition_location_to_lsp_location(&line_numbers, &params, location)) 211 }) 212 } 213 214 pub(crate) fn goto_type_definition( 215 &mut self, 216 params: lsp_types::GotoDefinitionParams, 217 ) -> Response<Vec<lsp::Location>> { 218 self.respond(|this| { 219 let params = params.text_document_position_params; 220 let (line_numbers, node) = match this.node_at_position(&params) { 221 Some(location) => location, 222 None => return Ok(vec![]), 223 }; 224 225 let Some(locations) = node 226 .type_definition_locations(this.compiler.project_compiler.get_importable_modules()) 227 else { 228 return Ok(vec![]); 229 }; 230 231 let locations = locations 232 .into_iter() 233 .filter_map(|location| { 234 this.definition_location_to_lsp_location(&line_numbers, &params, location) 235 }) 236 .collect_vec(); 237 238 Ok(locations) 239 }) 240 } 241 242 fn definition_location_to_lsp_location( 243 &self, 244 line_numbers: &LineNumbers, 245 params: &lsp_types::TextDocumentPositionParams, 246 location: DefinitionLocation, 247 ) -> Option<lsp::Location> { 248 let (uri, line_numbers) = match location.module { 249 None => (params.text_document.uri.clone(), line_numbers), 250 Some(name) => { 251 let module = self.compiler.get_source(&name)?; 252 let url = Url::parse(&format!("file:///{}", &module.path)) 253 .expect("goto definition URL parse"); 254 (url, &module.line_numbers) 255 } 256 }; 257 let range = src_span_to_lsp_range(location.span, line_numbers); 258 259 Some(lsp::Location { uri, range }) 260 } 261 262 pub fn completion( 263 &mut self, 264 params: lsp::TextDocumentPositionParams, 265 src: EcoString, 266 ) -> Response<Option<Vec<lsp::CompletionItem>>> { 267 self.respond(|this| { 268 let module = match this.module_for_uri(&params.text_document.uri) { 269 Some(m) => m, 270 None => return Ok(None), 271 }; 272 273 let completer = Completer::new(&src, &params, &this.compiler, module); 274 let byte_index = completer.module_line_numbers.byte_index(params.position); 275 276 // If in comment context, do not provide completions 277 if module.extra.is_within_comment(byte_index) { 278 return Ok(None); 279 } 280 281 // Check current filercontents if the user is writing an import 282 // and handle separately from the rest of the completion flow 283 // Check if an import is being written 284 if let Some(value) = completer.import_completions() { 285 return value; 286 } 287 288 let Some(found) = module.find_node(byte_index) else { 289 return Ok(None); 290 }; 291 292 let completions = match found { 293 Located::PatternSpread { .. } => None, 294 Located::Pattern(_pattern) => None, 295 // Do not show completions when typing inside a string. 296 Located::Expression { 297 expression: TypedExpr::String { .. }, 298 .. 299 } 300 | Located::Constant(Constant::String { .. }) => None, 301 Located::Expression { 302 expression: TypedExpr::Call { fun, arguments, .. }, 303 .. 304 } => { 305 let mut completions = vec![]; 306 completions.append(&mut completer.completion_values()); 307 completions.append(&mut completer.completion_labels(fun, arguments)); 308 Some(completions) 309 } 310 Located::Expression { 311 expression: TypedExpr::RecordAccess { record, .. }, 312 .. 313 } => { 314 let mut completions = vec![]; 315 completions.append(&mut completer.completion_values()); 316 completions.append(&mut completer.completion_field_accessors(record.type_())); 317 Some(completions) 318 } 319 Located::Expression { 320 position: 321 ExpressionPosition::ArgumentOrLabel { 322 called_function, 323 function_arguments, 324 }, 325 .. 326 } => { 327 let mut completions = vec![]; 328 completions.append(&mut completer.completion_values()); 329 completions.append( 330 &mut completer.completion_labels(called_function, function_arguments), 331 ); 332 Some(completions) 333 } 334 Located::Statement(_) | Located::Expression { .. } => { 335 Some(completer.completion_values()) 336 } 337 Located::ModuleStatement(Definition::Function(_)) => { 338 Some(completer.completion_types()) 339 } 340 341 Located::FunctionBody(_) => Some(completer.completion_values()), 342 343 Located::ModuleStatement(Definition::TypeAlias(_) | Definition::CustomType(_)) 344 | Located::VariantConstructorDefinition(_) => Some(completer.completion_types()), 345 346 // If the import completions returned no results and we are in an import then 347 // we should try to provide completions for unqualified values 348 Located::ModuleStatement(Definition::Import(import)) => this 349 .compiler 350 .get_module_interface(import.module.as_str()) 351 .map(|importing_module| { 352 completer.unqualified_completions_from_module(importing_module, true) 353 }), 354 355 Located::ModuleStatement(Definition::ModuleConstant(_)) | Located::Constant(_) => { 356 Some(completer.completion_values()) 357 } 358 359 Located::UnqualifiedImport(_) => None, 360 361 Located::Arg(_) => None, 362 363 Located::Annotation { .. } => Some(completer.completion_types()), 364 365 Located::Label(_, _) => None, 366 367 Located::ModuleName { 368 layer: ast::Layer::Type, 369 .. 370 } => Some(completer.completion_types()), 371 Located::ModuleName { 372 layer: ast::Layer::Value, 373 .. 374 } => Some(completer.completion_values()), 375 }; 376 377 Ok(completions) 378 }) 379 } 380 381 pub fn code_actions( 382 &mut self, 383 params: lsp::CodeActionParams, 384 ) -> Response<Option<Vec<CodeAction>>> { 385 self.respond(|this| { 386 let mut actions = vec![]; 387 let Some(module) = this.module_for_uri(&params.text_document.uri) else { 388 return Ok(None); 389 }; 390 391 let lines = LineNumbers::new(&module.code); 392 393 code_action_unused_values(module, &lines, &params, &mut actions); 394 actions.extend(RemoveUnusedImports::new(module, &lines, &params).code_actions()); 395 code_action_convert_qualified_constructor_to_unqualified( 396 module, 397 &this.compiler, 398 &lines, 399 &params, 400 &mut actions, 401 ); 402 code_action_convert_unqualified_constructor_to_qualified( 403 module, 404 &lines, 405 &params, 406 &mut actions, 407 ); 408 code_action_fix_names(&lines, &params, &this.error, &mut actions); 409 code_action_import_module(module, &lines, &params, &this.error, &mut actions); 410 code_action_add_missing_patterns(module, &lines, &params, &this.error, &mut actions); 411 actions.extend(RemoveUnreachableBranches::new(module, &lines, &params).code_actions()); 412 actions.extend(CollapseNestedCase::new(module, &lines, &params).code_actions()); 413 code_action_inexhaustive_let_to_case( 414 module, 415 &lines, 416 &params, 417 &this.error, 418 &mut actions, 419 ); 420 actions.extend(FixBinaryOperation::new(module, &lines, &params).code_actions()); 421 actions 422 .extend(FixTruncatedBitArraySegment::new(module, &lines, &params).code_actions()); 423 actions.extend(LetAssertToCase::new(module, &lines, &params).code_actions()); 424 actions 425 .extend(RedundantTupleInCaseSubject::new(module, &lines, &params).code_actions()); 426 actions.extend(UseLabelShorthandSyntax::new(module, &lines, &params).code_actions()); 427 actions.extend(FillInMissingLabelledArgs::new(module, &lines, &params).code_actions()); 428 actions.extend(ConvertFromUse::new(module, &lines, &params).code_actions()); 429 actions.extend(RemoveEchos::new(module, &lines, &params).code_actions()); 430 actions.extend(ConvertToUse::new(module, &lines, &params).code_actions()); 431 actions.extend(ExpandFunctionCapture::new(module, &lines, &params).code_actions()); 432 actions.extend(FillUnusedFields::new(module, &lines, &params).code_actions()); 433 actions.extend(InterpolateString::new(module, &lines, &params).code_actions()); 434 actions.extend(ExtractVariable::new(module, &lines, &params).code_actions()); 435 actions.extend(ExtractConstant::new(module, &lines, &params).code_actions()); 436 actions.extend( 437 GenerateFunction::new(module, &this.compiler.modules, &lines, &params) 438 .code_actions(), 439 ); 440 actions.extend( 441 GenerateVariant::new(module, &this.compiler, &lines, &params).code_actions(), 442 ); 443 actions.extend(ConvertToPipe::new(module, &lines, &params).code_actions()); 444 actions.extend(ConvertToFunctionCall::new(module, &lines, &params).code_actions()); 445 actions.extend( 446 PatternMatchOnValue::new(module, &lines, &params, &this.compiler).code_actions(), 447 ); 448 actions.extend(AddOmittedLabels::new(module, &lines, &params).code_actions()); 449 actions.extend(InlineVariable::new(module, &lines, &params).code_actions()); 450 actions.extend(WrapInBlock::new(module, &lines, &params).code_actions()); 451 actions.extend(RemoveBlock::new(module, &lines, &params).code_actions()); 452 actions.extend(RemovePrivateOpaque::new(module, &lines, &params).code_actions()); 453 actions.extend(ExtractFunction::new(module, &lines, &params).code_actions()); 454 GenerateDynamicDecoder::new(module, &lines, &params, &mut actions).code_actions(); 455 GenerateJsonEncoder::new( 456 module, 457 &lines, 458 &params, 459 &mut actions, 460 &this.compiler.project_compiler.config, 461 ) 462 .code_actions(); 463 AddAnnotations::new(module, &lines, &params).code_action(&mut actions); 464 Ok(if actions.is_empty() { 465 None 466 } else { 467 Some(actions) 468 }) 469 }) 470 } 471 472 pub fn document_symbol( 473 &mut self, 474 params: lsp::DocumentSymbolParams, 475 ) -> Response<Vec<DocumentSymbol>> { 476 self.respond(|this| { 477 let mut symbols = vec![]; 478 let Some(module) = this.module_for_uri(&params.text_document.uri) else { 479 return Ok(symbols); 480 }; 481 let line_numbers = LineNumbers::new(&module.code); 482 483 for definition in &module.ast.definitions { 484 match definition { 485 // Typically, imports aren't considered document symbols. 486 Definition::Import(_) => {} 487 488 Definition::Function(function) => { 489 // By default, the function's location ends right after the return type. 490 // For the full symbol range, have it end at the end of the body. 491 // Also include the documentation, if available. 492 // 493 // By convention, the symbol span starts from the leading slash in the 494 // documentation comment's marker ('///'), not from its content (of which 495 // we have the position), so we must convert the content start position 496 // to the leading slash's position using 'get_doc_marker_pos'. 497 let full_function_span = SrcSpan { 498 start: function 499 .documentation 500 .as_ref() 501 .map(|(doc_start, _)| get_doc_marker_pos(*doc_start)) 502 .unwrap_or(function.location.start), 503 504 end: function.end_position, 505 }; 506 507 let (name_location, name) = function 508 .name 509 .as_ref() 510 .expect("Function in a definition must be named"); 511 512 // The 'deprecated' field is deprecated, but we have to specify it anyway 513 // to be able to construct the 'DocumentSymbol' type, so 514 // we suppress the warning. We specify 'None' as specifying 'Some' 515 // is what is actually deprecated. 516 #[allow(deprecated)] 517 symbols.push(DocumentSymbol { 518 name: name.to_string(), 519 detail: Some( 520 Printer::new(&module.ast.names) 521 .print_type(&get_function_type(function)) 522 .to_string(), 523 ), 524 kind: SymbolKind::FUNCTION, 525 tags: make_deprecated_symbol_tag(&function.deprecation), 526 deprecated: None, 527 range: src_span_to_lsp_range(full_function_span, &line_numbers), 528 selection_range: src_span_to_lsp_range(*name_location, &line_numbers), 529 children: None, 530 }); 531 } 532 533 Definition::TypeAlias(alias) => { 534 let full_alias_span = match alias.documentation { 535 Some((doc_position, _)) => { 536 SrcSpan::new(get_doc_marker_pos(doc_position), alias.location.end) 537 } 538 None => alias.location, 539 }; 540 541 // The 'deprecated' field is deprecated, but we have to specify it anyway 542 // to be able to construct the 'DocumentSymbol' type, so 543 // we suppress the warning. We specify 'None' as specifying 'Some' 544 // is what is actually deprecated. 545 #[allow(deprecated)] 546 symbols.push(DocumentSymbol { 547 name: alias.alias.to_string(), 548 detail: Some( 549 Printer::new(&module.ast.names) 550 // If we print with aliases, we end up printing the alias which the user 551 // is currently hovering, which is not helpful. Instead, we print the 552 // raw type, so the user can see which type the alias represents 553 .print_type_without_aliases(&alias.type_) 554 .to_string(), 555 ), 556 kind: SymbolKind::CLASS, 557 tags: make_deprecated_symbol_tag(&alias.deprecation), 558 deprecated: None, 559 range: src_span_to_lsp_range(full_alias_span, &line_numbers), 560 selection_range: src_span_to_lsp_range( 561 alias.name_location, 562 &line_numbers, 563 ), 564 children: None, 565 }); 566 } 567 568 Definition::CustomType(type_) => { 569 symbols.push(custom_type_symbol(type_, &line_numbers, module)); 570 } 571 572 Definition::ModuleConstant(constant) => { 573 // `ModuleConstant.location` ends at the constant's name or type. 574 // For the full symbol span, necessary for `range`, we need to 575 // include the constant value as well. 576 // Also include the documentation at the start, if available. 577 let full_constant_span = SrcSpan { 578 start: constant 579 .documentation 580 .as_ref() 581 .map(|(doc_start, _)| get_doc_marker_pos(*doc_start)) 582 .unwrap_or(constant.location.start), 583 584 end: constant.value.location().end, 585 }; 586 587 // The 'deprecated' field is deprecated, but we have to specify it anyway 588 // to be able to construct the 'DocumentSymbol' type, so 589 // we suppress the warning. We specify 'None' as specifying 'Some' 590 // is what is actually deprecated. 591 #[allow(deprecated)] 592 symbols.push(DocumentSymbol { 593 name: constant.name.to_string(), 594 detail: Some( 595 Printer::new(&module.ast.names) 596 .print_type(&constant.type_) 597 .to_string(), 598 ), 599 kind: SymbolKind::CONSTANT, 600 tags: make_deprecated_symbol_tag(&constant.deprecation), 601 deprecated: None, 602 range: src_span_to_lsp_range(full_constant_span, &line_numbers), 603 selection_range: src_span_to_lsp_range( 604 constant.name_location, 605 &line_numbers, 606 ), 607 children: None, 608 }); 609 } 610 } 611 } 612 613 Ok(symbols) 614 }) 615 } 616 617 /// Check whether a particular module is in the same package as this one 618 fn is_same_package(&self, current_module: &Module, module_name: &str) -> bool { 619 let other_module = self 620 .compiler 621 .project_compiler 622 .get_importable_modules() 623 .get(module_name); 624 match other_module { 625 // We can't rename values from other packages if we are not aliasing an unqualified import. 626 Some(module) => module.package == current_module.ast.type_info.package, 627 None => false, 628 } 629 } 630 631 pub fn prepare_rename( 632 &mut self, 633 params: lsp::TextDocumentPositionParams, 634 ) -> Response<Option<PrepareRenameResponse>> { 635 self.respond(|this| { 636 let (lines, found) = match this.node_at_position(&params) { 637 Some(value) => value, 638 None => return Ok(None), 639 }; 640 641 let Some(current_module) = this.module_for_uri(&params.text_document.uri) else { 642 return Ok(None); 643 }; 644 645 let success_response = |location| { 646 Some(PrepareRenameResponse::Range(src_span_to_lsp_range( 647 location, &lines, 648 ))) 649 }; 650 651 let byte_index = lines.byte_index(params.position); 652 653 Ok(match reference_for_ast_node(found, &current_module.name) { 654 Some(Referenced::LocalVariable { 655 location, origin, .. 656 }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { 657 Some(VariableSyntax::Generated) => None, 658 Some( 659 VariableSyntax::Variable(label) | VariableSyntax::LabelShorthand(label), 660 ) => success_response(SrcSpan { 661 start: location.start, 662 end: label 663 .len() 664 .try_into() 665 .map(|len: u32| location.start + len) 666 .unwrap_or(location.end), 667 }), 668 Some(VariableSyntax::AssignmentPattern) | None => success_response(location), 669 }, 670 Some( 671 Referenced::ModuleValue { 672 module, 673 location, 674 target_kind, 675 .. 676 } 677 | Referenced::ModuleType { 678 module, 679 location, 680 target_kind, 681 .. 682 }, 683 ) if location.contains(byte_index) => { 684 // We can't rename types or values from other packages if we are not aliasing an unqualified import. 685 let rename_allowed = match target_kind { 686 RenameTarget::Qualified => this.is_same_package(current_module, &module), 687 RenameTarget::Unqualified | RenameTarget::Definition => true, 688 }; 689 if rename_allowed { 690 success_response(location) 691 } else { 692 None 693 } 694 } 695 _ => None, 696 }) 697 }) 698 } 699 700 pub fn rename(&mut self, params: lsp::RenameParams) -> Response<Option<WorkspaceEdit>> { 701 self.respond(|this| { 702 let position = &params.text_document_position; 703 704 let (lines, found) = match this.node_at_position(position) { 705 Some(value) => value, 706 None => return Ok(None), 707 }; 708 709 let Some(module) = this.module_for_uri(&position.text_document.uri) else { 710 return Ok(None); 711 }; 712 713 Ok(match reference_for_ast_node(found, &module.name) { 714 Some(Referenced::LocalVariable { 715 origin, 716 definition_location, 717 name, 718 .. 719 }) => { 720 let rename_kind = match origin.map(|origin| origin.syntax) { 721 Some(VariableSyntax::Generated) => return Ok(None), 722 Some(VariableSyntax::LabelShorthand(_)) => { 723 VariableReferenceKind::LabelShorthand 724 } 725 Some( 726 VariableSyntax::AssignmentPattern | VariableSyntax::Variable { .. }, 727 ) 728 | None => VariableReferenceKind::Variable, 729 }; 730 rename_local_variable( 731 module, 732 &lines, 733 &params, 734 definition_location, 735 name, 736 rename_kind, 737 ) 738 } 739 Some(Referenced::ModuleValue { 740 module: module_name, 741 target_kind, 742 name, 743 name_kind, 744 .. 745 }) => rename_module_entity( 746 &params, 747 module, 748 this.compiler.project_compiler.get_importable_modules(), 749 &this.compiler.sources, 750 Renamed { 751 module_name: &module_name, 752 name: &name, 753 name_kind, 754 target_kind, 755 layer: ast::Layer::Value, 756 }, 757 ), 758 Some(Referenced::ModuleType { 759 module: module_name, 760 target_kind, 761 name, 762 .. 763 }) => rename_module_entity( 764 &params, 765 module, 766 this.compiler.project_compiler.get_importable_modules(), 767 &this.compiler.sources, 768 Renamed { 769 module_name: &module_name, 770 name: &name, 771 name_kind: Named::Type, 772 target_kind, 773 layer: ast::Layer::Type, 774 }, 775 ), 776 None => None, 777 }) 778 }) 779 } 780 781 pub fn find_references( 782 &mut self, 783 params: lsp::ReferenceParams, 784 ) -> Response<Option<Vec<lsp::Location>>> { 785 self.respond(|this| { 786 let position = &params.text_document_position; 787 788 let (lines, found) = match this.node_at_position(position) { 789 Some(value) => value, 790 None => return Ok(None), 791 }; 792 793 let uri = position.text_document.uri.clone(); 794 795 let Some(module) = this.module_for_uri(&uri) else { 796 return Ok(None); 797 }; 798 799 let byte_index = lines.byte_index(position.position); 800 801 Ok(match reference_for_ast_node(found, &module.name) { 802 Some(Referenced::LocalVariable { 803 origin, 804 definition_location, 805 location, 806 name, 807 }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { 808 Some(VariableSyntax::Generated) => None, 809 Some( 810 VariableSyntax::LabelShorthand(_) 811 | VariableSyntax::AssignmentPattern 812 | VariableSyntax::Variable { .. }, 813 ) 814 | None => { 815 let variable_references = 816 FindVariableReferences::new(definition_location, name) 817 .find_in_module(&module.ast); 818 819 let mut reference_locations = 820 Vec::with_capacity(variable_references.len() + 1); 821 reference_locations.push(lsp::Location { 822 uri: uri.clone(), 823 range: src_span_to_lsp_range(definition_location, &lines), 824 }); 825 826 for reference in variable_references { 827 reference_locations.push(lsp::Location { 828 uri: uri.clone(), 829 range: src_span_to_lsp_range(reference.location, &lines), 830 }) 831 } 832 833 Some(reference_locations) 834 } 835 }, 836 Some(Referenced::ModuleValue { 837 module, 838 name, 839 location, 840 .. 841 }) if location.contains(byte_index) => Some(find_module_references( 842 module, 843 name, 844 this.compiler.project_compiler.get_importable_modules(), 845 &this.compiler.sources, 846 ast::Layer::Value, 847 )), 848 Some(Referenced::ModuleType { 849 module, 850 name, 851 location, 852 .. 853 }) if location.contains(byte_index) => Some(find_module_references( 854 module, 855 name, 856 this.compiler.project_compiler.get_importable_modules(), 857 &this.compiler.sources, 858 ast::Layer::Type, 859 )), 860 _ => None, 861 }) 862 }) 863 } 864 865 fn respond<T>(&mut self, handler: impl FnOnce(&mut Self) -> Result<T>) -> Response<T> { 866 let result = handler(self); 867 let warnings = self.take_warnings(); 868 // TODO: test. Ensure hover doesn't report as compiled 869 let compilation = if self.compiled_since_last_feedback { 870 let modules = std::mem::take(&mut self.modules_compiled_since_last_feedback); 871 self.compiled_since_last_feedback = false; 872 Compilation::Yes(modules) 873 } else { 874 Compilation::No 875 }; 876 Response { 877 result, 878 warnings, 879 compilation, 880 } 881 } 882 883 pub fn hover(&mut self, params: lsp::HoverParams) -> Response<Option<Hover>> { 884 self.respond(|this| { 885 let params = params.text_document_position_params; 886 887 let (lines, found) = match this.node_at_position(&params) { 888 Some(value) => value, 889 None => return Ok(None), 890 }; 891 892 let Some(module) = this.module_for_uri(&params.text_document.uri) else { 893 return Ok(None); 894 }; 895 896 Ok(match found { 897 Located::Statement(_) => None, // TODO: hover for statement 898 Located::ModuleStatement(Definition::Function(fun)) => { 899 Some(hover_for_function_head(fun, lines, module)) 900 } 901 Located::ModuleStatement(Definition::ModuleConstant(constant)) => { 902 Some(hover_for_module_constant(constant, lines, module)) 903 } 904 Located::Constant(constant) => Some(hover_for_constant(constant, lines, module)), 905 Located::ModuleStatement(Definition::Import(import)) => { 906 let Some(module) = this.compiler.get_module_interface(&import.module) else { 907 return Ok(None); 908 }; 909 Some(hover_for_module( 910 module, 911 import.location, 912 &lines, 913 &this.hex_deps, 914 )) 915 } 916 Located::ModuleStatement(_) => None, 917 Located::VariantConstructorDefinition(_) => None, 918 Located::UnqualifiedImport(UnqualifiedImport { 919 name, 920 module: module_name, 921 is_type, 922 location, 923 }) => this 924 .compiler 925 .get_module_interface(module_name.as_str()) 926 .and_then(|module_interface| { 927 if is_type { 928 module_interface.types.get(name).map(|t| { 929 hover_for_annotation( 930 *location, 931 t.type_.as_ref(), 932 Some(t), 933 lines, 934 module, 935 ) 936 }) 937 } else { 938 module_interface.values.get(name).map(|v| { 939 let m = if this.hex_deps.contains(&module_interface.package) { 940 Some(module_interface) 941 } else { 942 None 943 }; 944 hover_for_imported_value(v, location, lines, m, name, module) 945 }) 946 } 947 }), 948 Located::Pattern(pattern) => Some(hover_for_pattern(pattern, lines, module)), 949 Located::PatternSpread { 950 spread_location, 951 pattern, 952 } => { 953 let range = Some(src_span_to_lsp_range(spread_location, &lines)); 954 955 let mut printer = Printer::new(&module.ast.names); 956 957 let PatternUnusedArguments { 958 positional, 959 labelled, 960 } = pattern.unused_arguments().unwrap_or_default(); 961 962 let positional = positional 963 .iter() 964 .map(|type_| format!("- `{}`", printer.print_type(type_))) 965 .join("\n"); 966 let labelled = labelled 967 .iter() 968 .map(|(label, type_)| { 969 format!("- `{}: {}`", label, printer.print_type(type_)) 970 }) 971 .join("\n"); 972 973 let content = match (positional.is_empty(), labelled.is_empty()) { 974 (true, false) => format!("Unused labelled fields:\n{labelled}"), 975 (false, true) => format!("Unused positional fields:\n{positional}"), 976 (_, _) => format!( 977 "Unused positional fields: 978{positional} 979 980Unused labelled fields: 981{labelled}" 982 ), 983 }; 984 985 Some(Hover { 986 contents: HoverContents::Scalar(MarkedString::from_markdown(content)), 987 range, 988 }) 989 } 990 Located::Expression { expression, .. } => Some(hover_for_expression( 991 expression, 992 lines, 993 module, 994 &this.hex_deps, 995 )), 996 Located::Arg(arg) => Some(hover_for_function_argument(arg, lines, module)), 997 Located::FunctionBody(_) => None, 998 Located::Annotation { ast, type_ } => { 999 let type_constructor = type_constructor_from_modules( 1000 this.compiler.project_compiler.get_importable_modules(), 1001 type_.clone(), 1002 ); 1003 Some(hover_for_annotation( 1004 ast.location(), 1005 &type_, 1006 type_constructor, 1007 lines, 1008 module, 1009 )) 1010 } 1011 Located::Label(location, type_) => { 1012 Some(hover_for_label(location, type_, lines, module)) 1013 } 1014 Located::ModuleName { location, name, .. } => { 1015 let Some(module) = this.compiler.get_module_interface(name) else { 1016 return Ok(None); 1017 }; 1018 Some(hover_for_module(module, location, &lines, &this.hex_deps)) 1019 } 1020 }) 1021 }) 1022 } 1023 1024 pub(crate) fn signature_help( 1025 &mut self, 1026 params: lsp_types::SignatureHelpParams, 1027 ) -> Response<Option<SignatureHelp>> { 1028 self.respond( 1029 |this| match this.node_at_position(&params.text_document_position_params) { 1030 Some((_lines, Located::Expression { expression, .. })) => { 1031 Ok(signature_help::for_expression(expression)) 1032 } 1033 Some((_lines, _located)) => Ok(None), 1034 None => Ok(None), 1035 }, 1036 ) 1037 } 1038 1039 fn module_node_at_position( 1040 &self, 1041 params: &lsp::TextDocumentPositionParams, 1042 module: &'a Module, 1043 ) -> Option<(LineNumbers, Located<'a>)> { 1044 let line_numbers = LineNumbers::new(&module.code); 1045 let byte_index = line_numbers.byte_index(params.position); 1046 let node = module.find_node(byte_index); 1047 let node = node?; 1048 Some((line_numbers, node)) 1049 } 1050 1051 fn node_at_position( 1052 &self, 1053 params: &lsp::TextDocumentPositionParams, 1054 ) -> Option<(LineNumbers, Located<'_>)> { 1055 let module = self.module_for_uri(&params.text_document.uri)?; 1056 self.module_node_at_position(params, module) 1057 } 1058 1059 fn module_for_uri(&self, uri: &Url) -> Option<&Module> { 1060 // The to_file_path method is available on these platforms 1061 #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] 1062 let path = uri.to_file_path().expect("URL file"); 1063 1064 #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] 1065 let path: Utf8PathBuf = uri.path().into(); 1066 1067 let components = path 1068 .strip_prefix(self.paths.root()) 1069 .ok()? 1070 .components() 1071 .skip(1) 1072 .map(|c| c.as_os_str().to_string_lossy()); 1073 let module_name: EcoString = Itertools::intersperse(components, "/".into()) 1074 .collect::<String>() 1075 .strip_suffix(".gleam")? 1076 .into(); 1077 1078 self.compiler.modules.get(&module_name) 1079 } 1080} 1081 1082fn custom_type_symbol( 1083 type_: &CustomType<Arc<Type>>, 1084 line_numbers: &LineNumbers, 1085 module: &Module, 1086) -> DocumentSymbol { 1087 let constructors = type_ 1088 .constructors 1089 .iter() 1090 .map(|constructor| { 1091 let mut arguments = vec![]; 1092 1093 // List named arguments as field symbols. 1094 for argument in &constructor.arguments { 1095 let Some((label_location, label)) = &argument.label else { 1096 continue; 1097 }; 1098 1099 let full_arg_span = match argument.doc { 1100 Some((doc_position, _)) => { 1101 SrcSpan::new(get_doc_marker_pos(doc_position), argument.location.end) 1102 } 1103 None => argument.location, 1104 }; 1105 1106 // The 'deprecated' field is deprecated, but we have to specify it anyway 1107 // to be able to construct the 'DocumentSymbol' type, so 1108 // we suppress the warning. We specify 'None' as specifying 'Some' 1109 // is what is actually deprecated. 1110 #[allow(deprecated)] 1111 arguments.push(DocumentSymbol { 1112 name: label.to_string(), 1113 detail: Some( 1114 Printer::new(&module.ast.names) 1115 .print_type(&argument.type_) 1116 .to_string(), 1117 ), 1118 kind: SymbolKind::FIELD, 1119 tags: None, 1120 deprecated: None, 1121 range: src_span_to_lsp_range(full_arg_span, line_numbers), 1122 selection_range: src_span_to_lsp_range(*label_location, line_numbers), 1123 children: None, 1124 }); 1125 } 1126 1127 // Start from the documentation if available, otherwise from the constructor's name, 1128 // all the way to the end of its arguments. 1129 let full_constructor_span = SrcSpan { 1130 start: constructor 1131 .documentation 1132 .as_ref() 1133 .map(|(doc_start, _)| get_doc_marker_pos(*doc_start)) 1134 .unwrap_or(constructor.location.start), 1135 1136 end: constructor.location.end, 1137 }; 1138 1139 // The 'deprecated' field is deprecated, but we have to specify it anyway 1140 // to be able to construct the 'DocumentSymbol' type, so 1141 // we suppress the warning. We specify 'None' as specifying 'Some' 1142 // is what is actually deprecated. 1143 #[allow(deprecated)] 1144 DocumentSymbol { 1145 name: constructor.name.to_string(), 1146 detail: None, 1147 kind: if constructor.arguments.is_empty() { 1148 SymbolKind::ENUM_MEMBER 1149 } else { 1150 SymbolKind::CONSTRUCTOR 1151 }, 1152 tags: make_deprecated_symbol_tag(&constructor.deprecation), 1153 deprecated: None, 1154 range: src_span_to_lsp_range(full_constructor_span, line_numbers), 1155 selection_range: src_span_to_lsp_range(constructor.name_location, line_numbers), 1156 children: if arguments.is_empty() { 1157 None 1158 } else { 1159 Some(arguments) 1160 }, 1161 } 1162 }) 1163 .collect_vec(); 1164 1165 // The type's location, by default, ranges from "(pub) type" to the end of its name. 1166 // We need it to range to the end of its constructors instead for the full symbol range. 1167 // We also include documentation, if available, by LSP convention. 1168 let full_type_span = SrcSpan { 1169 start: type_ 1170 .documentation 1171 .as_ref() 1172 .map(|(doc_start, _)| get_doc_marker_pos(*doc_start)) 1173 .unwrap_or(type_.location.start), 1174 1175 end: type_.end_position, 1176 }; 1177 1178 // The 'deprecated' field is deprecated, but we have to specify it anyway 1179 // to be able to construct the 'DocumentSymbol' type, so 1180 // we suppress the warning. We specify 'None' as specifying 'Some' 1181 // is what is actually deprecated. 1182 #[allow(deprecated)] 1183 DocumentSymbol { 1184 name: type_.name.to_string(), 1185 detail: None, 1186 kind: SymbolKind::CLASS, 1187 tags: make_deprecated_symbol_tag(&type_.deprecation), 1188 deprecated: None, 1189 range: src_span_to_lsp_range(full_type_span, line_numbers), 1190 selection_range: src_span_to_lsp_range(type_.name_location, line_numbers), 1191 children: if constructors.is_empty() { 1192 None 1193 } else { 1194 Some(constructors) 1195 }, 1196 } 1197} 1198 1199fn hover_for_pattern(pattern: &TypedPattern, line_numbers: LineNumbers, module: &Module) -> Hover { 1200 let documentation = pattern.get_documentation().unwrap_or_default(); 1201 1202 // Show the type of the hovered node to the user 1203 let type_ = Printer::new(&module.ast.names).print_type(pattern.type_().as_ref()); 1204 let contents = format!( 1205 "```gleam 1206{type_} 1207``` 1208{documentation}" 1209 ); 1210 Hover { 1211 contents: HoverContents::Scalar(MarkedString::String(contents)), 1212 range: Some(src_span_to_lsp_range(pattern.location(), &line_numbers)), 1213 } 1214} 1215 1216fn get_function_type(fun: &TypedFunction) -> Type { 1217 Type::Fn { 1218 arguments: fun 1219 .arguments 1220 .iter() 1221 .map(|argument| argument.type_.clone()) 1222 .collect(), 1223 return_: fun.return_type.clone(), 1224 } 1225} 1226 1227fn hover_for_function_head( 1228 fun: &TypedFunction, 1229 line_numbers: LineNumbers, 1230 module: &Module, 1231) -> Hover { 1232 let empty_str = EcoString::from(""); 1233 let documentation = fun 1234 .documentation 1235 .as_ref() 1236 .map(|(_, doc)| doc) 1237 .unwrap_or(&empty_str); 1238 let function_type = get_function_type(fun); 1239 let formatted_type = Printer::new(&module.ast.names).print_type(&function_type); 1240 let contents = format!( 1241 "```gleam 1242{formatted_type} 1243``` 1244{documentation}" 1245 ); 1246 Hover { 1247 contents: HoverContents::Scalar(MarkedString::String(contents)), 1248 range: Some(src_span_to_lsp_range(fun.location, &line_numbers)), 1249 } 1250} 1251 1252fn hover_for_function_argument( 1253 argument: &TypedArg, 1254 line_numbers: LineNumbers, 1255 module: &Module, 1256) -> Hover { 1257 let type_ = Printer::new(&module.ast.names).print_type(&argument.type_); 1258 let contents = format!("```gleam\n{type_}\n```"); 1259 Hover { 1260 contents: HoverContents::Scalar(MarkedString::String(contents)), 1261 range: Some(src_span_to_lsp_range(argument.location, &line_numbers)), 1262 } 1263} 1264 1265fn hover_for_annotation( 1266 location: SrcSpan, 1267 annotation_type: &Type, 1268 type_constructor: Option<&TypeConstructor>, 1269 line_numbers: LineNumbers, 1270 module: &Module, 1271) -> Hover { 1272 let empty_str = EcoString::from(""); 1273 let documentation = type_constructor 1274 .and_then(|t| t.documentation.as_ref()) 1275 .unwrap_or(&empty_str); 1276 // If a user is hovering an annotation, it's not very useful to show the 1277 // local representation of that type, since that's probably what they see 1278 // in the source code anyway. So here, we print the raw type, 1279 // which is probably more helpful. 1280 let type_ = Printer::new(&module.ast.names).print_type_without_aliases(annotation_type); 1281 let contents = format!( 1282 "```gleam 1283{type_} 1284``` 1285{documentation}" 1286 ); 1287 Hover { 1288 contents: HoverContents::Scalar(MarkedString::String(contents)), 1289 range: Some(src_span_to_lsp_range(location, &line_numbers)), 1290 } 1291} 1292 1293fn hover_for_label( 1294 location: SrcSpan, 1295 type_: Arc<Type>, 1296 line_numbers: LineNumbers, 1297 module: &Module, 1298) -> Hover { 1299 let type_ = Printer::new(&module.ast.names).print_type(&type_); 1300 let contents = format!("```gleam\n{type_}\n```"); 1301 Hover { 1302 contents: HoverContents::Scalar(MarkedString::String(contents)), 1303 range: Some(src_span_to_lsp_range(location, &line_numbers)), 1304 } 1305} 1306 1307fn hover_for_module_constant( 1308 constant: &ModuleConstant<Arc<Type>, EcoString>, 1309 line_numbers: LineNumbers, 1310 module: &Module, 1311) -> Hover { 1312 let empty_str = EcoString::from(""); 1313 let type_ = Printer::new(&module.ast.names).print_type(&constant.type_); 1314 let documentation = constant 1315 .documentation 1316 .as_ref() 1317 .map(|(_, doc)| doc) 1318 .unwrap_or(&empty_str); 1319 let contents = format!("```gleam\n{type_}\n```\n{documentation}"); 1320 Hover { 1321 contents: HoverContents::Scalar(MarkedString::String(contents)), 1322 range: Some(src_span_to_lsp_range(constant.location, &line_numbers)), 1323 } 1324} 1325 1326fn hover_for_constant( 1327 constant: &TypedConstant, 1328 line_numbers: LineNumbers, 1329 module: &Module, 1330) -> Hover { 1331 let type_ = Printer::new(&module.ast.names).print_type(&constant.type_()); 1332 let contents = format!("```gleam\n{type_}\n```"); 1333 Hover { 1334 contents: HoverContents::Scalar(MarkedString::String(contents)), 1335 range: Some(src_span_to_lsp_range(constant.location(), &line_numbers)), 1336 } 1337} 1338 1339fn hover_for_expression( 1340 expression: &TypedExpr, 1341 line_numbers: LineNumbers, 1342 module: &Module, 1343 hex_deps: &HashSet<EcoString>, 1344) -> Hover { 1345 let documentation = expression.get_documentation().unwrap_or_default(); 1346 1347 let link_section = get_expr_qualified_name(expression) 1348 .and_then(|(module_name, name)| { 1349 get_hexdocs_link_section(module_name, name, &module.ast, hex_deps) 1350 }) 1351 .unwrap_or("".to_string()); 1352 1353 // Show the type of the hovered node to the user 1354 let type_ = Printer::new(&module.ast.names).print_type(expression.type_().as_ref()); 1355 let contents = format!( 1356 "```gleam 1357{type_} 1358``` 1359{documentation}{link_section}" 1360 ); 1361 Hover { 1362 contents: HoverContents::Scalar(MarkedString::String(contents)), 1363 range: Some(src_span_to_lsp_range(expression.location(), &line_numbers)), 1364 } 1365} 1366 1367fn hover_for_imported_value( 1368 value: &ValueConstructor, 1369 location: &SrcSpan, 1370 line_numbers: LineNumbers, 1371 hex_module_imported_from: Option<&ModuleInterface>, 1372 name: &EcoString, 1373 module: &Module, 1374) -> Hover { 1375 let documentation = value.get_documentation().unwrap_or_default(); 1376 1377 let link_section = hex_module_imported_from.map_or("".to_string(), |m| { 1378 format_hexdocs_link_section(m.package.as_str(), m.name.as_str(), Some(name)) 1379 }); 1380 1381 // Show the type of the hovered node to the user 1382 let type_ = Printer::new(&module.ast.names).print_type(value.type_.as_ref()); 1383 let contents = format!( 1384 "```gleam 1385{type_} 1386``` 1387{documentation}{link_section}" 1388 ); 1389 Hover { 1390 contents: HoverContents::Scalar(MarkedString::String(contents)), 1391 range: Some(src_span_to_lsp_range(*location, &line_numbers)), 1392 } 1393} 1394 1395fn hover_for_module( 1396 module: &ModuleInterface, 1397 location: SrcSpan, 1398 line_numbers: &LineNumbers, 1399 hex_deps: &HashSet<EcoString>, 1400) -> Hover { 1401 let documentation = module.documentation.join("\n"); 1402 let name = &module.name; 1403 1404 let link_section = if hex_deps.contains(&module.package) { 1405 format_hexdocs_link_section(&module.package, name, None) 1406 } else { 1407 String::new() 1408 }; 1409 1410 let contents = format!( 1411 "```gleam 1412{name} 1413``` 1414{documentation} 1415{link_section}", 1416 ); 1417 Hover { 1418 contents: HoverContents::Scalar(MarkedString::String(contents)), 1419 range: Some(src_span_to_lsp_range(location, line_numbers)), 1420 } 1421} 1422 1423// Returns true if any part of either range overlaps with the other. 1424pub fn overlaps(a: Range, b: Range) -> bool { 1425 position_within(a.start, b) 1426 || position_within(a.end, b) 1427 || position_within(b.start, a) 1428 || position_within(b.end, a) 1429} 1430 1431// Returns true if a range is contained within another. 1432pub fn within(a: Range, b: Range) -> bool { 1433 position_within(a.start, b) && position_within(a.end, b) 1434} 1435 1436// Returns true if a position is within a range. 1437fn position_within(position: Position, range: Range) -> bool { 1438 position >= range.start && position <= range.end 1439} 1440 1441/// Builds the code action to assign an unused value to `_`. 1442/// 1443fn code_action_unused_values( 1444 module: &Module, 1445 line_numbers: &LineNumbers, 1446 params: &lsp::CodeActionParams, 1447 actions: &mut Vec<CodeAction>, 1448) { 1449 let uri = &params.text_document.uri; 1450 let mut unused_values: Vec<&SrcSpan> = module 1451 .ast 1452 .type_info 1453 .warnings 1454 .iter() 1455 .filter_map(|warning| match warning { 1456 type_::Warning::ImplicitlyDiscardedResult { location } => Some(location), 1457 _ => None, 1458 }) 1459 .collect(); 1460 1461 if unused_values.is_empty() { 1462 return; 1463 } 1464 1465 // Sort spans by start position, with longer spans coming first 1466 unused_values.sort_by_key(|span| (span.start, -(span.end as i64 - span.start as i64))); 1467 1468 let mut processed_lsp_range = Vec::new(); 1469 1470 for unused in unused_values { 1471 let SrcSpan { start, end } = *unused; 1472 let hover_range = src_span_to_lsp_range(SrcSpan::new(start, end), line_numbers); 1473 1474 // Check if this span is contained within any previously processed span 1475 if processed_lsp_range 1476 .iter() 1477 .any(|&prev_lsp_range| within(hover_range, prev_lsp_range)) 1478 { 1479 continue; 1480 } 1481 1482 // Check if the cursor is within this span 1483 if !within(params.range, hover_range) { 1484 continue; 1485 } 1486 1487 let edit = TextEdit { 1488 range: src_span_to_lsp_range(SrcSpan::new(start, start), line_numbers), 1489 new_text: "let _ = ".into(), 1490 }; 1491 1492 CodeActionBuilder::new("Assign unused Result value to `_`") 1493 .kind(lsp_types::CodeActionKind::QUICKFIX) 1494 .changes(uri.clone(), vec![edit]) 1495 .preferred(true) 1496 .push_to(actions); 1497 1498 processed_lsp_range.push(hover_range); 1499 } 1500} 1501 1502struct NameCorrection { 1503 pub location: SrcSpan, 1504 pub correction: EcoString, 1505} 1506 1507fn code_action_fix_names( 1508 line_numbers: &LineNumbers, 1509 params: &lsp::CodeActionParams, 1510 error: &Option<Error>, 1511 actions: &mut Vec<CodeAction>, 1512) { 1513 let uri = &params.text_document.uri; 1514 let Some(Error::Type { errors, .. }) = error else { 1515 return; 1516 }; 1517 let name_corrections = errors 1518 .iter() 1519 .filter_map(|error| match error { 1520 type_::Error::BadName { 1521 location, 1522 name, 1523 kind, 1524 } => Some(NameCorrection { 1525 correction: correct_name_case(name, *kind), 1526 location: *location, 1527 }), 1528 _ => None, 1529 }) 1530 .collect_vec(); 1531 1532 if name_corrections.is_empty() { 1533 return; 1534 } 1535 1536 for name_correction in name_corrections { 1537 let NameCorrection { 1538 location, 1539 correction, 1540 } = name_correction; 1541 1542 let range = src_span_to_lsp_range(location, line_numbers); 1543 // Check if the user's cursor is on the invalid name 1544 if overlaps(params.range, range) { 1545 let edit = TextEdit { 1546 range, 1547 new_text: correction.to_string(), 1548 }; 1549 1550 CodeActionBuilder::new(&format!("Rename to {correction}")) 1551 .kind(lsp_types::CodeActionKind::QUICKFIX) 1552 .changes(uri.clone(), vec![edit]) 1553 .preferred(true) 1554 .push_to(actions); 1555 } 1556 } 1557} 1558 1559fn get_expr_qualified_name(expression: &TypedExpr) -> Option<(&EcoString, &EcoString)> { 1560 match expression { 1561 TypedExpr::Var { 1562 name, constructor, .. 1563 } if constructor.publicity.is_importable() => match &constructor.variant { 1564 ValueConstructorVariant::ModuleFn { 1565 module: module_name, 1566 .. 1567 } => Some((module_name, name)), 1568 1569 ValueConstructorVariant::ModuleConstant { 1570 module: module_name, 1571 .. 1572 } => Some((module_name, name)), 1573 1574 _ => None, 1575 }, 1576 1577 TypedExpr::ModuleSelect { 1578 label, module_name, .. 1579 } => Some((module_name, label)), 1580 1581 _ => None, 1582 } 1583} 1584 1585fn format_hexdocs_link_section( 1586 package_name: &str, 1587 module_name: &str, 1588 name: Option<&str>, 1589) -> String { 1590 let link = match name { 1591 Some(name) => format!("https://hexdocs.pm/{package_name}/{module_name}.html#{name}"), 1592 None => format!("https://hexdocs.pm/{package_name}/{module_name}.html"), 1593 }; 1594 format!("\nView on [HexDocs]({link})") 1595} 1596 1597fn get_hexdocs_link_section( 1598 module_name: &str, 1599 name: &str, 1600 ast: &TypedModule, 1601 hex_deps: &HashSet<EcoString>, 1602) -> Option<String> { 1603 let package_name = ast 1604 .definitions 1605 .iter() 1606 .find_map(|definition| match definition { 1607 Definition::Import(import) 1608 if import.module == module_name && hex_deps.contains(&import.package) => 1609 { 1610 Some(&import.package) 1611 } 1612 _ => None, 1613 })?; 1614 1615 Some(format_hexdocs_link_section( 1616 package_name, 1617 module_name, 1618 Some(name), 1619 )) 1620} 1621 1622/// Converts the source start position of a documentation comment's contents into 1623/// the position of the leading slash in its marker ('///'). 1624fn get_doc_marker_pos(content_pos: u32) -> u32 { 1625 content_pos.saturating_sub(3) 1626} 1627 1628fn make_deprecated_symbol_tag(deprecation: &Deprecation) -> Option<Vec<SymbolTag>> { 1629 deprecation 1630 .is_deprecated() 1631 .then(|| vec![SymbolTag::DEPRECATED]) 1632}