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