this repo has no description
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(¶ms) {
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, ¶ms, 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(¶ms) {
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, ¶ms, 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(¶ms.text_document.uri) {
269 Some(m) => m,
270 None => return Ok(None),
271 };
272
273 let completer = Completer::new(&src, ¶ms, &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(¶ms.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, ¶ms, &mut actions);
394 actions.extend(RemoveUnusedImports::new(module, &lines, ¶ms).code_actions());
395 code_action_convert_qualified_constructor_to_unqualified(
396 module,
397 &this.compiler,
398 &lines,
399 ¶ms,
400 &mut actions,
401 );
402 code_action_convert_unqualified_constructor_to_qualified(
403 module,
404 &lines,
405 ¶ms,
406 &mut actions,
407 );
408 code_action_fix_names(&lines, ¶ms, &this.error, &mut actions);
409 code_action_import_module(module, &lines, ¶ms, &this.error, &mut actions);
410 code_action_add_missing_patterns(module, &lines, ¶ms, &this.error, &mut actions);
411 actions.extend(RemoveUnreachableBranches::new(module, &lines, ¶ms).code_actions());
412 actions.extend(CollapseNestedCase::new(module, &lines, ¶ms).code_actions());
413 code_action_inexhaustive_let_to_case(
414 module,
415 &lines,
416 ¶ms,
417 &this.error,
418 &mut actions,
419 );
420 actions.extend(FixBinaryOperation::new(module, &lines, ¶ms).code_actions());
421 actions
422 .extend(FixTruncatedBitArraySegment::new(module, &lines, ¶ms).code_actions());
423 actions.extend(LetAssertToCase::new(module, &lines, ¶ms).code_actions());
424 actions
425 .extend(RedundantTupleInCaseSubject::new(module, &lines, ¶ms).code_actions());
426 actions.extend(UseLabelShorthandSyntax::new(module, &lines, ¶ms).code_actions());
427 actions.extend(FillInMissingLabelledArgs::new(module, &lines, ¶ms).code_actions());
428 actions.extend(ConvertFromUse::new(module, &lines, ¶ms).code_actions());
429 actions.extend(RemoveEchos::new(module, &lines, ¶ms).code_actions());
430 actions.extend(ConvertToUse::new(module, &lines, ¶ms).code_actions());
431 actions.extend(ExpandFunctionCapture::new(module, &lines, ¶ms).code_actions());
432 actions.extend(FillUnusedFields::new(module, &lines, ¶ms).code_actions());
433 actions.extend(InterpolateString::new(module, &lines, ¶ms).code_actions());
434 actions.extend(ExtractVariable::new(module, &lines, ¶ms).code_actions());
435 actions.extend(ExtractConstant::new(module, &lines, ¶ms).code_actions());
436 actions.extend(
437 GenerateFunction::new(module, &this.compiler.modules, &lines, ¶ms)
438 .code_actions(),
439 );
440 actions.extend(
441 GenerateVariant::new(module, &this.compiler, &lines, ¶ms).code_actions(),
442 );
443 actions.extend(ConvertToPipe::new(module, &lines, ¶ms).code_actions());
444 actions.extend(ConvertToFunctionCall::new(module, &lines, ¶ms).code_actions());
445 actions.extend(
446 PatternMatchOnValue::new(module, &lines, ¶ms, &this.compiler).code_actions(),
447 );
448 actions.extend(AddOmittedLabels::new(module, &lines, ¶ms).code_actions());
449 actions.extend(InlineVariable::new(module, &lines, ¶ms).code_actions());
450 actions.extend(WrapInBlock::new(module, &lines, ¶ms).code_actions());
451 actions.extend(RemoveBlock::new(module, &lines, ¶ms).code_actions());
452 actions.extend(RemovePrivateOpaque::new(module, &lines, ¶ms).code_actions());
453 actions.extend(ExtractFunction::new(module, &lines, ¶ms).code_actions());
454 GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions).code_actions();
455 GenerateJsonEncoder::new(
456 module,
457 &lines,
458 ¶ms,
459 &mut actions,
460 &this.compiler.project_compiler.config,
461 )
462 .code_actions();
463 AddAnnotations::new(module, &lines, ¶ms).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(¶ms.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(¶ms) {
637 Some(value) => value,
638 None => return Ok(None),
639 };
640
641 let Some(current_module) = this.module_for_uri(¶ms.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, ¤t_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 = ¶ms.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 ¶ms,
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 ¶ms,
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 ¶ms,
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 = ¶ms.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(¶ms) {
888 Some(value) => value,
889 None => return Ok(None),
890 };
891
892 let Some(module) = this.module_for_uri(¶ms.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(¶ms.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(¶ms.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 = ¶ms.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 = ¶ms.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}