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