use camino::Utf8PathBuf; use ecow::{EcoString, eco_format}; use gleam_core::{ Error, Result, Warning, analyse::name::correct_name_case, ast::{ self, Constant, CustomType, DefinitionLocation, ModuleConstant, PatternUnusedArguments, SrcSpan, TypedArg, TypedClauseGuard, TypedConstant, TypedExpr, TypedFunction, TypedModule, TypedPattern, TypedRecordConstructor, }, build::{ ExpressionPosition, Located, Module, UnqualifiedImport, type_constructor_from_modules, }, config::PackageConfig, io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter}, line_numbers::LineNumbers, paths::ProjectPaths, type_::{ self, Deprecation, ModuleInterface, Type, TypeConstructor, ValueConstructor, ValueConstructorVariant, error::{Named, VariableSyntax}, printer::Printer, }, }; use itertools::Itertools; use lsp::CodeAction; use lsp_server::ResponseError; use lsp_types::{ self as lsp, Contents, DocumentSymbol, FoldingRange, FoldingRangeKind, Hover, MarkedString, MarkupContent, Position, PrepareRenameResult, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Uri as Url, WorkspaceEdit, }; use std::{collections::HashSet, sync::Arc}; use crate::{ code_action::{RemoveRedundantRecordUpdate, ReplaceUnderscoreWithType, type_errors_for_module}, rename::rename_module_alias, }; use super::{ DownloadDependencies, MakeLocker, code_action::{ AddAnnotations, AddMissingTypeParameter, AddOmittedLabels, AnnotateTopLevelDefinitions, CodeActionBuilder, CollapseNestedCase, ConvertFromUse, ConvertToFunctionCall, ConvertToPipe, ConvertToUse, CreateUnknownModule, ExpandFunctionCapture, ExtractConstant, ExtractFunction, ExtractVariable, FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation, FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder, GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, MergeCaseBranches, PatternMatchOnValue, RedundantTupleInCaseSubject, RemoveBlock, RemoveEchos, RemovePrivateOpaque, RemoveUnreachableCaseClauses, RemoveUnusedImports, UnwrapAnonymousFunction, UseLabelShorthandSyntax, WrapInAnonymousFunction, WrapInBlock, code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, code_action_inexhaustive_let_to_case, }, compiler::LspProjectCompiler, completer::Completer, files::FileSystemProxy, progress::ProgressReporter, reference::{ FindVariableReferences, Referenced, VariableReferenceKind, find_module_references, reference_for_ast_node, }, rename::{RenameOutcome, RenameTarget, Renamed, rename_local_variable, rename_module_entity}, signature_help, src_span_to_lsp_range, }; #[derive(Debug, PartialEq, Eq)] pub struct Response { pub result: Result, pub warnings: Vec, pub compilation: Compilation, } #[derive(Debug, PartialEq, Eq)] pub enum Compilation { /// Compilation was attempted and succeeded for these modules. Yes(Vec), /// Compilation was not attempted for this operation. No, } #[derive(Debug)] pub struct LanguageServerEngine { pub(crate) paths: ProjectPaths, /// A compiler for the project that supports repeat compilation of the root /// package. /// In the event the project config changes this will need to be /// discarded and reloaded to handle any changes to dependencies. pub(crate) compiler: LspProjectCompiler>, modules_compiled_since_last_feedback: Vec, compiled_since_last_feedback: bool, error: Option, // Used to publish progress notifications to the client without waiting for // the usual request-response loop. progress_reporter: Reporter, /// Used to know if to show the "View on HexDocs" link /// when hovering on an imported value hex_deps: HashSet, } impl<'a, IO, Reporter> LanguageServerEngine where // IO to be supplied from outside of gleam-core IO: FileSystemReader + FileSystemWriter + BeamCompiler + CommandExecutor + DownloadDependencies + MakeLocker + Clone, // IO to be supplied from inside of gleam-core Reporter: ProgressReporter + Clone + 'a, { pub fn new( config: PackageConfig, progress_reporter: Reporter, io: FileSystemProxy, paths: ProjectPaths, ) -> Result { let locker = io.inner().make_locker(&paths, config.target)?; // Download dependencies to ensure they are up-to-date for this new // configuration and new instance of the compiler progress_reporter.dependency_downloading_started(); let manifest = io.inner().download_dependencies(&paths); progress_reporter.dependency_downloading_finished(); // NOTE: This must come after the progress reporter has finished! let manifest = manifest?; let compiler: LspProjectCompiler> = LspProjectCompiler::new(manifest, config, paths.clone(), io.clone(), locker)?; let hex_deps = compiler .project_compiler .packages .iter() .flat_map(|(k, v)| match &v.source { gleam_core::manifest::ManifestPackageSource::Hex { .. } => { Some(EcoString::from(k.as_str())) } gleam_core::manifest::ManifestPackageSource::Git { .. } | gleam_core::manifest::ManifestPackageSource::Local { .. } => None, }) .collect(); Ok(Self { modules_compiled_since_last_feedback: vec![], compiled_since_last_feedback: false, progress_reporter, compiler, paths, error: None, hex_deps, }) } pub fn compile_please(&mut self) -> Response<()> { self.respond(Self::compile) } /// Compile the project if we are in one. Otherwise do nothing. fn compile(&mut self) -> Result<(), Error> { self.compiled_since_last_feedback = true; self.progress_reporter.compilation_started(); let outcome = self.compiler.compile(); self.progress_reporter.compilation_finished(); let result = outcome // Register which modules have changed .map(|modules| self.modules_compiled_since_last_feedback.extend(modules)) // Return the error, if present .into_result(); self.error = match &result { Ok(_) => None, Err(error) => Some(error.clone()), }; result } fn take_warnings(&mut self) -> Vec { self.compiler.take_warnings() } pub fn goto_definition( &mut self, params: lsp::DefinitionParams, ) -> Response> { self.respond(|this| { let params = params.text_document_position_params; let (line_numbers, node) = match this.node_at_position(¶ms) { Some(location) => location, None => return Ok(None), }; let Some(location) = node.definition_location(this.compiler.project_compiler.get_importable_modules()) else { return Ok(None); }; Ok(this.definition_location_to_lsp_location(&line_numbers, ¶ms, location)) }) } pub(crate) fn goto_type_definition( &mut self, params: lsp_types::TypeDefinitionParams, ) -> Response> { self.respond(|this| { let params = params.text_document_position_params; let (line_numbers, node) = match this.node_at_position(¶ms) { Some(location) => location, None => return Ok(vec![]), }; let Some(locations) = node .type_definition_locations(this.compiler.project_compiler.get_importable_modules()) else { return Ok(vec![]); }; let locations = locations .into_iter() .filter_map(|location| { this.definition_location_to_lsp_location(&line_numbers, ¶ms, location) }) .collect_vec(); Ok(locations) }) } fn definition_location_to_lsp_location( &self, line_numbers: &LineNumbers, params: &lsp_types::TextDocumentPositionParams, location: DefinitionLocation, ) -> Option { let (uri, line_numbers) = match location.module { None => (params.text_document.uri.clone(), line_numbers), Some(name) => { let module = self.compiler.get_source(&name)?; let url = Url::parse(&format!("file:///{}", &module.path)) .expect("goto definition URL parse"); (url, &module.line_numbers) } }; let range = src_span_to_lsp_range(location.span, line_numbers); Some(lsp::Location { uri, range }) } pub fn completion( &mut self, params: lsp::TextDocumentPositionParams, src: EcoString, ) -> Response>> { self.respond(|this| { let module = match this.module_for_uri(¶ms.text_document.uri) { Some(module) => module, None => return Ok(None), }; let mut completer = Completer::new(&src, ¶ms, &this.compiler, module); let byte_index = completer.module_line_numbers.byte_index(params.position); // If in comment context, do not provide completions if module.extra.is_within_comment(byte_index) { return Ok(None); } // Check current file contents if the user is writing an import // and handle separately from the rest of the completion flow // Check if an import is being written if let Some(value) = completer.import_completions() { return value; } let Some(found) = module.find_node(byte_index) else { return Ok(None); }; let completions = match found { Located::PatternSpread { .. } => None, Located::Pattern(_pattern) => None, Located::StringPrefixPatternVariable { .. } => None, // Do not show completions when typing inside a string. Located::Expression { expression: TypedExpr::String { .. }, .. } | Located::Constant(Constant::String { .. }) => None, Located::Expression { expression: TypedExpr::Call { fun, arguments, .. } | TypedExpr::RecordUpdate { constructor: fun, arguments, .. }, .. } => { let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append(&mut completer.completion_labels(fun, arguments)); Some(completions) } Located::Expression { expression: TypedExpr::RecordAccess { record, type_, .. }, .. } => { completer.expected_type = Some(type_.clone()); let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append(&mut completer.completion_field_accessors(record.type_())); Some(completions) } Located::Expression { position: ExpressionPosition::ArgumentOrLabel { called_function, function_arguments, }, .. } => { let mut completions = vec![]; completions.append(&mut completer.completion_values()); completions.append( &mut completer.completion_labels(called_function, function_arguments), ); Some(completions) } // If we're typing inside an expression body (and not being any // more specific than this, meaning we're not editing some // specific value inside it) we don't want to set the expected // type to the type of the entire anonymous function, otherwise // the language server would start recommending the wrong // values: // // ``` // fn(x: Int) -> String { // | // <- Typing here // // We don't want the language server to suggest values // // of type `fn(Int) -> String`! // todo // } // ``` // Located::Expression { position: ExpressionPosition::Expression, expression: TypedExpr::Fn { .. }, } => Some(completer.completion_values()), Located::Expression { expression, .. } => { completer.expected_type = Some(expression.type_()); Some(completer.completion_values()) } Located::ModuleFunction(_) => Some(completer.completion_types()), Located::Statement(_) => Some(completer.completion_values()), Located::FunctionBody(_) => Some(completer.completion_values()), Located::ModuleTypeAlias(_) | Located::ModuleCustomType(_) | Located::VariantConstructorDefinition(_) => Some(completer.completion_types()), // If the import completions returned no results and we are in an import then // we should try to provide completions for unqualified values Located::ModuleImport(import) => this .compiler .get_module_interface(import.module.as_str()) .map(|importing_module| { completer.unqualified_completions_from_module(importing_module, true) }), Located::ModuleConstant(_) | Located::Constant(_) => { Some(completer.completion_values()) } Located::UnqualifiedImport(_) => None, Located::Arg(_) => None, Located::Annotation { .. } => Some(completer.completion_types()), Located::Label(_, _) => None, Located::ModuleName { layer: ast::Layer::Type, .. } => Some(completer.completion_types()), Located::ModuleName { layer: ast::Layer::Value, .. } => Some(completer.completion_values()), Located::ClauseGuard(_) => Some(completer.completion_values()), }; Ok(completions) }) } pub fn code_actions( &mut self, params: lsp::CodeActionParams, ) -> Response>> { self.respond(|this| { let mut actions = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(None); }; let lines = LineNumbers::new(&module.code); code_action_unused_values(module, &lines, ¶ms, &mut actions); actions.extend(RemoveUnusedImports::new(module, &lines, ¶ms).code_actions()); code_action_fix_names(module, &lines, ¶ms, &this.error, &mut actions); code_action_import_module(module, &lines, ¶ms, &this.error, &mut actions); code_action_add_missing_patterns(module, &lines, ¶ms, &this.error, &mut actions); actions .extend(RemoveUnreachableCaseClauses::new(module, &lines, ¶ms).code_actions()); actions .extend(RemoveRedundantRecordUpdate::new(module, &lines, ¶ms).code_actions()); actions.extend(CollapseNestedCase::new(module, &lines, ¶ms).code_actions()); actions.extend(FixBinaryOperation::new(module, &lines, ¶ms).code_actions()); actions .extend(FixTruncatedBitArraySegment::new(module, &lines, ¶ms).code_actions()); actions.extend(RemovePrivateOpaque::new(module, &lines, ¶ms).code_actions()); actions.extend(AddMissingTypeParameter::new(module, &lines, ¶ms).code_actions()); code_action_convert_qualified_constructor_to_unqualified( module, &this.compiler, &lines, ¶ms, &mut actions, ); code_action_convert_unqualified_constructor_to_qualified( module, &lines, ¶ms, &mut actions, ); code_action_inexhaustive_let_to_case( module, &lines, ¶ms, &this.error, &mut actions, ); actions.extend(MergeCaseBranches::new(module, &lines, ¶ms).code_actions()); actions.extend(LetAssertToCase::new(module, &lines, ¶ms).code_actions()); actions .extend(RedundantTupleInCaseSubject::new(module, &lines, ¶ms).code_actions()); actions.extend(FillInMissingLabelledArgs::new(module, &lines, ¶ms).code_actions()); actions.extend(UseLabelShorthandSyntax::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertFromUse::new(module, &lines, ¶ms).code_actions()); actions.extend(RemoveEchos::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertToUse::new(module, &lines, ¶ms).code_actions()); actions.extend(ExpandFunctionCapture::new(module, &lines, ¶ms).code_actions()); actions.extend(FillUnusedFields::new(module, &lines, ¶ms).code_actions()); actions.extend(InterpolateString::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractVariable::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractConstant::new(module, &lines, ¶ms).code_actions()); actions.extend( GenerateFunction::new(module, &this.compiler.modules, &lines, ¶ms) .code_actions(), ); actions.extend( GenerateVariant::new(module, &this.compiler, &lines, ¶ms).code_actions(), ); actions.extend(ConvertToPipe::new(module, &lines, ¶ms).code_actions()); actions.extend(ConvertToFunctionCall::new(module, &lines, ¶ms).code_actions()); actions.extend( PatternMatchOnValue::new(module, &lines, ¶ms, &this.compiler).code_actions(), ); actions.extend(AddOmittedLabels::new(module, &lines, ¶ms).code_actions()); actions.extend(InlineVariable::new(module, &lines, ¶ms).code_actions()); actions.extend(WrapInBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(RemoveBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractFunction::new(module, &lines, ¶ms).code_actions()); GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions, &this.compiler) .code_actions(); actions.extend(WrapInAnonymousFunction::new(module, &lines, ¶ms).code_actions()); actions.extend(UnwrapAnonymousFunction::new(module, &lines, ¶ms).code_actions()); GenerateJsonEncoder::new( module, &lines, ¶ms, &mut actions, &this.compiler.project_compiler.config, ) .code_actions(); AddAnnotations::new(module, &lines, ¶ms).code_action(&mut actions); actions .extend(AnnotateTopLevelDefinitions::new(module, &lines, ¶ms).code_actions()); actions.extend(ReplaceUnderscoreWithType::new(module, &lines, ¶ms).code_actions()); actions.extend( CreateUnknownModule::new( module, &this.compiler, &lines, ¶ms, &this.paths, &this.error, ) .code_actions(), ); actions.sort_by_key(|one| { let preferred_key = if one.is_preferred == Some(true) { 0 } else { 1 }; let kind_key = match &one.kind { Some(lsp_types::CodeActionKind::QuickFix) => 1, Some(lsp_types::CodeActionKind::Refactor) => 2, Some(lsp_types::CodeActionKind::RefactorExtract) => 2, Some(lsp_types::CodeActionKind::RefactorInline) => 2, Some(lsp_types::CodeActionKind::RefactorMove) => 2, Some(lsp_types::CodeActionKind::RefactorRewrite) => 2, Some(lsp_types::CodeActionKind::Source) => 3, Some(lsp_types::CodeActionKind::SourceOrganizeImports) => 3, Some(lsp_types::CodeActionKind::SourceFixAll) => 3, Some(lsp_types::CodeActionKind::Custom(_)) => 4, Some(lsp_types::CodeActionKind::Notebook) => 5, Some(lsp_types::CodeActionKind::Empty) => 6, None => 7, }; (preferred_key, kind_key) }); Ok(if actions.is_empty() { None } else { Some(actions) }) }) } pub fn document_symbol( &mut self, params: lsp::DocumentSymbolParams, ) -> Response> { self.respond(|this| { let mut symbols = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(symbols); }; let line_numbers = LineNumbers::new(&module.code); for function in &module.ast.definitions.functions { // By default, the function's location ends right after the return type. // For the full symbol range, have it end at the end of the body. // Also include the documentation, if available. // // By convention, the symbol span starts from the leading slash in the // documentation comment's marker ('///'), not from its content (of which // we have the position), so we must convert the content start position // to the leading slash's position. let full_function_span = SrcSpan { start: function .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(function.location.start), end: function.end_position, }; let (name_location, name) = function .name .as_ref() .expect("Function in a definition must be named"); // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: name.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&get_function_type(function)) .to_string(), ), kind: SymbolKind::Function, tags: make_deprecated_symbol_tag(&function.deprecation), deprecated: None, range: src_span_to_lsp_range(full_function_span, &line_numbers), selection_range: src_span_to_lsp_range(*name_location, &line_numbers), children: None, }); } for alias in &module.ast.definitions.type_aliases { let full_alias_span = match alias.documentation { Some((doc_position, _)) => { SrcSpan::new(get_doc_marker_position(doc_position), alias.location.end) } None => alias.location, }; // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: alias.alias.to_string(), detail: Some( Printer::new(&module.ast.names) // If we print with aliases, we end up printing the alias which the user // is currently hovering, which is not helpful. Instead, we print the // raw type, so the user can see which type the alias represents .print_type_without_aliases(&alias.type_) .to_string(), ), kind: SymbolKind::Class, tags: make_deprecated_symbol_tag(&alias.deprecation), deprecated: None, range: src_span_to_lsp_range(full_alias_span, &line_numbers), selection_range: src_span_to_lsp_range(alias.name_location, &line_numbers), children: None, }); } for custom_type in &module.ast.definitions.custom_types { symbols.push(custom_type_symbol(custom_type, &line_numbers, module)); } for constant in &module.ast.definitions.constants { // `ModuleConstant.location` ends at the constant's name or type. // For the full symbol span, necessary for `range`, we need to // include the constant value as well. // Also include the documentation at the start, if available. let full_constant_span = SrcSpan { start: constant .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(constant.location.start), end: constant.value.location().end, }; // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] symbols.push(DocumentSymbol { name: constant.name.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&constant.type_) .to_string(), ), kind: SymbolKind::Constant, tags: make_deprecated_symbol_tag(&constant.deprecation), deprecated: None, range: src_span_to_lsp_range(full_constant_span, &line_numbers), selection_range: src_span_to_lsp_range(constant.name_location, &line_numbers), children: None, }); } Ok(symbols) }) } pub fn folding_range( &mut self, params: lsp::FoldingRangeParams, ) -> Response> { self.respond(|this| { let mut ranges: Vec = vec![]; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(vec![]); }; let line_numbers = LineNumbers::new(&module.code); for import in import_folding_spans(&module.ast.definitions.imports, &module.code, &line_numbers) { let Some(range) = folding_range_for_span(import, &line_numbers, Some(FoldingRangeKind::Imports)) else { continue; }; ranges.push(range); } for type_ in &module.ast.definitions.custom_types { let span = type_.full_location(); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); } for constant in &module.ast.definitions.constants { let span = SrcSpan::new(constant.location.start, constant.value.location().end); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); } for alias in &module.ast.definitions.type_aliases { let span = alias.location; let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); } for function in &module.ast.definitions.functions { let Some(body_start) = function.body_start else { continue; }; let span = SrcSpan::new(body_start, function.end_position); let Some(range) = folding_range_for_span(span, &line_numbers, None) else { continue; }; ranges.push(range); } ranges.sort_by_key(|range| range.start_line); Ok(ranges) }) } /// Check whether a particular module is in the same package as this one fn is_same_package(&self, current_module: &Module, module_name: &str) -> bool { let other_module = self .compiler .project_compiler .get_importable_modules() .get(module_name); match other_module { // We can't rename values from other packages if we are not aliasing an unqualified import. Some(module) => module.package == current_module.ast.type_info.package, None => false, } } pub fn prepare_rename( &mut self, params: lsp::PrepareRenameParams, ) -> Response> { self.respond(|this| { let (lines, found) = match this.node_at_position(¶ms.text_document_position_params) { Some(value) => value, None => return Ok(None), }; let Some(current_module) = this.module_for_uri(¶ms.text_document_position_params.text_document.uri) else { return Ok(None); }; let success_response = |location| { Some(PrepareRenameResult::Range(src_span_to_lsp_range( location, &lines, ))) }; let byte_index = lines.byte_index(params.text_document_position_params.position); let referenced = reference_for_ast_node(found, ¤t_module.name); Ok(match referenced { Some(Referenced::LocalVariable { location, origin, .. }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => None, Some( VariableSyntax::Variable(label) | VariableSyntax::LabelShorthand(label), ) => success_response(SrcSpan { start: location.start, end: label .len() .try_into() .map(|len: u32| location.start + len) .unwrap_or(location.end), }), Some(VariableSyntax::AssignmentPattern) | None => success_response(location), }, Some( Referenced::ModuleValue { module, location, target_kind, .. } | Referenced::ModuleType { module, location, target_kind, .. }, ) if location.contains(byte_index) => { // We can't rename types or values from other packages if we are not aliasing an unqualified import. let rename_allowed = match target_kind { RenameTarget::Qualified => this.is_same_package(current_module, &module), RenameTarget::Unqualified | RenameTarget::Definition => true, }; if rename_allowed { success_response(location) } else { None } } Some(Referenced::ModuleName { location, .. }) => success_response(location), _ => None, }) }) } pub fn rename( &mut self, params: lsp::RenameParams, ) -> Response, ResponseError>> { self.respond(|this| { let position = ¶ms.text_document_position_params; let (lines, found) = match this.node_at_position(position) { Some(value) => value, None => return Ok(RenameOutcome::NoRenames.into_result()), }; let Some(module) = this.module_for_uri(&position.text_document.uri) else { return Ok(RenameOutcome::NoRenames.into_result()); }; let referenced = reference_for_ast_node(found, &module.name); Ok(match referenced { Some(Referenced::LocalVariable { origin, definition_location, name, .. }) => { let rename_kind = match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => { return Ok(RenameOutcome::NoRenames.into_result()); } Some(VariableSyntax::LabelShorthand(_)) => { VariableReferenceKind::LabelShorthand } Some( VariableSyntax::AssignmentPattern | VariableSyntax::Variable { .. }, ) | None => VariableReferenceKind::Variable, }; rename_local_variable( module, &lines, ¶ms, definition_location, name, rename_kind, ) .into_result() } Some(Referenced::ModuleValue { module: module_name, target_kind, name, name_kind, .. }) => rename_module_entity( ¶ms, module, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, Renamed { module_name: &module_name, name: &name, name_kind, target_kind, layer: ast::Layer::Value, }, ) .into_result(), Some(Referenced::ModuleType { module: module_name, target_kind, name, .. }) => rename_module_entity( ¶ms, module, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, Renamed { module_name: &module_name, name: &name, name_kind: Named::Type, target_kind, layer: ast::Layer::Type, }, ) .into_result(), Some(Referenced::ModuleName { module_name, module_alias, .. }) => rename_module_alias(module, &lines, ¶ms, &module_name, &module_alias) .into_result(), None => RenameOutcome::NoRenames.into_result(), }) }) } pub fn find_references( &mut self, params: lsp::ReferenceParams, ) -> Response>> { self.respond(|this| { let position = ¶ms.text_document_position_params; let (lines, found) = match this.node_at_position(position) { Some(value) => value, None => return Ok(None), }; let uri = position.text_document.uri.clone(); let Some(module) = this.module_for_uri(&uri) else { return Ok(None); }; let byte_index = lines.byte_index(position.position); let referenced = reference_for_ast_node(found, &module.name); Ok(match referenced { Some(Referenced::LocalVariable { origin, definition_location, location, name, }) if location.contains(byte_index) => match origin.map(|origin| origin.syntax) { Some(VariableSyntax::Generated) => None, Some( VariableSyntax::LabelShorthand(_) | VariableSyntax::AssignmentPattern | VariableSyntax::Variable { .. }, ) | None => { let variable_references = FindVariableReferences::new(definition_location, name) .find_in_module(&module.ast); let mut reference_locations = Vec::with_capacity(variable_references.len() + 1); reference_locations.push(lsp::Location { uri: uri.clone(), range: src_span_to_lsp_range(definition_location, &lines), }); for reference in variable_references { reference_locations.push(lsp::Location { uri: uri.clone(), range: src_span_to_lsp_range(reference.location, &lines), }) } Some(reference_locations) } }, Some(Referenced::ModuleValue { module, name, location, .. }) if location.contains(byte_index) => Some(find_module_references( module, name, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, ast::Layer::Value, )), Some(Referenced::ModuleType { module, name, location, .. }) if location.contains(byte_index) => Some(find_module_references( module, name, this.compiler.project_compiler.get_importable_modules(), &this.compiler.sources, ast::Layer::Type, )), _ => None, }) }) } fn respond(&mut self, handler: impl FnOnce(&mut Self) -> Result) -> Response { let result = handler(self); let warnings = self.take_warnings(); // TODO: test. Ensure hover doesn't report as compiled let compilation = if self.compiled_since_last_feedback { let modules = std::mem::take(&mut self.modules_compiled_since_last_feedback); self.compiled_since_last_feedback = false; Compilation::Yes(modules) } else { Compilation::No }; Response { result, warnings, compilation, } } pub fn hover(&mut self, params: lsp::HoverParams) -> Response> { self.respond(|this| { let params = params.text_document_position_params; let (lines, found) = match this.node_at_position(¶ms) { Some(value) => value, None => return Ok(None), }; let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { return Ok(None); }; Ok(match found { Located::Statement(_) => None, // TODO: hover for statement Located::ModuleFunction(function) => { Some(hover_for_function_head(function, lines, module)) } Located::ModuleConstant(constant) => { Some(hover_for_module_constant(constant, lines, module)) } Located::Constant(constant) => Some(hover_for_constant(constant, lines, module)), Located::ModuleImport(import) => { let Some(module) = this.compiler.get_module_interface(&import.module) else { return Ok(None); }; Some(hover_for_module( module, import.location, &lines, &this.hex_deps, )) } Located::ModuleCustomType(custom_type) => { Some(hover_for_custom_type(custom_type, lines)) } Located::ModuleTypeAlias(_) => None, Located::VariantConstructorDefinition(constructor) => { Some(hover_for_constructor(constructor, lines, module)) } Located::UnqualifiedImport(UnqualifiedImport { name, module: module_name, is_type, location, }) => this .compiler .get_module_interface(module_name.as_str()) .and_then(|module_interface| { if is_type { module_interface.types.get(name).map(|constructor| { hover_for_annotation( *location, constructor.type_.as_ref(), Some(constructor), lines, module, ) }) } else { module_interface.values.get(name).map(|v| { let m = if this.hex_deps.contains(&module_interface.package) { Some(module_interface) } else { None }; hover_for_imported_value(v, location, lines, m, name, module) }) } }), Located::Pattern(pattern) => Some(hover_for_pattern(pattern, lines, module)), Located::PatternSpread { spread_location, pattern, } => { let range = Some(src_span_to_lsp_range(spread_location, &lines)); let mut printer = Printer::new(&module.ast.names); let PatternUnusedArguments { positional, labelled, } = pattern.unused_arguments().unwrap_or_default(); let positional = positional .iter() .map(|type_| format!("- `{}`", printer.print_type(type_))) .join("\n"); let labelled = labelled .iter() .map(|(label, type_)| { format!("- `{}: {}`", label, printer.print_type(type_)) }) .join("\n"); let content = match (positional.is_empty(), labelled.is_empty()) { (true, false) => format!("Unused labelled fields:\n{labelled}"), (false, true) => format!("Unused positional fields:\n{positional}"), (_, _) => format!( "Unused positional fields: {positional} Unused labelled fields: {labelled}" ), }; Some(Hover { contents: Contents::MarkupContent(MarkupContent { value: content, kind: lsp_types::MarkupKind::Markdown, }), range, }) } Located::StringPrefixPatternVariable { location, .. } => Some( hover_for_string_prefix_pattern_variable(location, &lines, module), ), Located::Expression { expression, position, } => Some(hover_for_expression( expression, position, lines, module, &this.hex_deps, )), Located::Arg(arg) => Some(hover_for_function_argument(arg, lines, module)), Located::FunctionBody(_) => None, Located::Annotation { ast, type_ } => { let type_constructor = type_constructor_from_modules( this.compiler.project_compiler.get_importable_modules(), type_.clone(), ); Some(hover_for_annotation( ast.location(), &type_, type_constructor, lines, module, )) } Located::Label(location, type_) => { Some(hover_for_label(location, type_, lines, module)) } Located::ModuleName { location, module_name, .. } => { let Some(module) = this.compiler.get_module_interface(&module_name) else { return Ok(None); }; Some(hover_for_module(module, location, &lines, &this.hex_deps)) } Located::ClauseGuard(guard) => Some(hover_for_clause_guard(guard, lines, module)), }) }) } pub(crate) fn signature_help( &mut self, params: lsp_types::SignatureHelpParams, ) -> Response> { self.respond(|this| { let Some(module) = this.module_for_uri(¶ms.text_document_position_params.text_document.uri) else { return Ok(None); }; match this.node_at_position(¶ms.text_document_position_params) { Some((_lines, Located::Expression { expression, .. })) => { Ok(signature_help::for_expression(expression, module)) } Some((_lines, _located)) => Ok(None), None => Ok(None), } }) } fn module_node_at_position( &self, params: &lsp::TextDocumentPositionParams, module: &'a Module, ) -> Option<(LineNumbers, Located<'a>)> { let line_numbers = LineNumbers::new(&module.code); let byte_index = line_numbers.byte_index(params.position); let node = module.find_node(byte_index); let node = node?; Some((line_numbers, node)) } fn node_at_position( &self, params: &lsp::TextDocumentPositionParams, ) -> Option<(LineNumbers, Located<'_>)> { let module = self.module_for_uri(¶ms.text_document.uri)?; self.module_node_at_position(params, module) } fn module_for_uri(&self, uri: &Url) -> Option<&Module> { // The to_file_path method is available on these platforms #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] let path = uri.to_file_path().expect("URL file"); #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] let path: Utf8PathBuf = uri.path().into(); let components = path .strip_prefix(self.paths.root()) .ok()? .components() .skip(1) .map(|c| c.as_os_str().to_string_lossy()); let module_name: EcoString = Itertools::intersperse(components, "/".into()) .collect::() .strip_suffix(".gleam")? .into(); self.compiler.modules.get(&module_name) } } fn import_folding_spans( imports: &[ast::Import], code: &str, line_numbers: &LineNumbers, ) -> Vec { let mut spans = vec![]; let mut imports = imports.iter(); let Some(first_import) = imports.next() else { return spans; }; let mut previous_line = src_span_to_lsp_range( SrcSpan::new(first_import.location.start, first_import.location.start), line_numbers, ) .start .line; let mut current_start = first_import.location.start; let mut current_end = first_import.location.end; let mut current_len = 1; for import in imports { let next_line = src_span_to_lsp_range( SrcSpan::new(import.location.start, import.location.start), line_numbers, ) .start .line; let separated_by_blank_line = next_line > previous_line + 1; let between = &code[current_end as usize..import.location.start as usize]; let has_non_whitespace_between = between.chars().any(|char| !char.is_whitespace()); if separated_by_blank_line || has_non_whitespace_between { if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); } current_start = import.location.start; current_end = import.location.end; current_len = 1; } else { current_end = import.location.end; current_len += 1; } previous_line = next_line; } if current_len > 1 { spans.push(SrcSpan::new(current_start, current_end)); } spans } fn folding_range_for_span( span: SrcSpan, line_numbers: &LineNumbers, kind: Option, ) -> Option { let range = src_span_to_lsp_range(span, line_numbers); if range.start.line >= range.end.line { return None; } Some(FoldingRange { start_line: range.start.line, start_character: None, end_line: range.end.line, end_character: None, kind, collapsed_text: None, }) } fn custom_type_symbol( type_: &CustomType>, line_numbers: &LineNumbers, module: &Module, ) -> DocumentSymbol { let constructors = type_ .constructors .iter() .map(|constructor| { let mut arguments = vec![]; // List named arguments as field symbols. for argument in &constructor.arguments { let Some((label_location, label)) = &argument.label else { continue; }; let full_arg_span = match argument.doc { Some((doc_position, _)) => { SrcSpan::new(get_doc_marker_position(doc_position), argument.location.end) } None => argument.location, }; // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] arguments.push(DocumentSymbol { name: label.to_string(), detail: Some( Printer::new(&module.ast.names) .print_type(&argument.type_) .to_string(), ), kind: SymbolKind::Field, tags: None, deprecated: None, range: src_span_to_lsp_range(full_arg_span, line_numbers), selection_range: src_span_to_lsp_range(*label_location, line_numbers), children: None, }); } // Start from the documentation if available, otherwise from the constructor's name, // all the way to the end of its arguments. let full_constructor_span = SrcSpan { start: constructor .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(constructor.location.start), end: constructor.location.end, }; // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] DocumentSymbol { name: constructor.name.to_string(), detail: None, kind: if constructor.arguments.is_empty() { SymbolKind::EnumMember } else { SymbolKind::Constructor }, tags: make_deprecated_symbol_tag(&constructor.deprecation), deprecated: None, range: src_span_to_lsp_range(full_constructor_span, line_numbers), selection_range: src_span_to_lsp_range(constructor.name_location, line_numbers), children: if arguments.is_empty() { None } else { Some(arguments) }, } }) .collect_vec(); // The type's location, by default, ranges from "(pub) type" to the end of its name. // We need it to range to the end of its constructors instead for the full symbol range. // We also include documentation, if available, by LSP convention. let full_type_span = SrcSpan { start: type_ .documentation .as_ref() .map(|(doc_start, _)| get_doc_marker_position(*doc_start)) .unwrap_or(type_.location.start), end: type_.end_position, }; // The 'deprecated' field is deprecated, but we have to specify it anyway // to be able to construct the 'DocumentSymbol' type, so // we suppress the warning. We specify 'None' as specifying 'Some' // is what is actually deprecated. #[allow(deprecated)] DocumentSymbol { name: type_.name.to_string(), detail: None, kind: SymbolKind::Class, tags: make_deprecated_symbol_tag(&type_.deprecation), deprecated: None, range: src_span_to_lsp_range(full_type_span, line_numbers), selection_range: src_span_to_lsp_range(type_.name_location, line_numbers), children: if constructors.is_empty() { None } else { Some(constructors) }, } } fn hover_for_pattern(pattern: &TypedPattern, line_numbers: LineNumbers, module: &Module) -> Hover { let documentation = pattern.get_documentation().unwrap_or_default(); // Show the type of the hovered node to the user let type_ = Printer::new(&module.ast.names).print_type(pattern.type_().as_ref()); let contents = format!( "```gleam {type_} ``` {documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(pattern.location(), &line_numbers)), } } fn get_function_type(fun: &TypedFunction) -> Type { Type::Fn { arguments: fun .arguments .iter() .map(|argument| argument.type_.clone()) .collect(), return_: fun.return_type.clone(), } } fn hover_for_function_head( fun: &TypedFunction, line_numbers: LineNumbers, module: &Module, ) -> Hover { let empty_str = EcoString::from(""); let documentation = fun .documentation .as_ref() .map(|(_, doc)| doc) .unwrap_or(&empty_str); let function_type = get_function_type(fun); let formatted_type = Printer::new(&module.ast.names).print_type(&function_type); let contents = format!( "```gleam {formatted_type} ``` {documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(fun.location, &line_numbers)), } } fn hover_for_function_argument( argument: &TypedArg, line_numbers: LineNumbers, module: &Module, ) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&argument.type_); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(argument.location, &line_numbers)), } } fn hover_for_annotation( location: SrcSpan, annotation_type: &Type, type_constructor: Option<&TypeConstructor>, line_numbers: LineNumbers, module: &Module, ) -> Hover { let empty_str = EcoString::from(""); let documentation = type_constructor .and_then(|constructor| constructor.documentation.as_ref()) .unwrap_or(&empty_str); // If a user is hovering an annotation, it's not very useful to show the // local representation of that type, since that's probably what they see // in the source code anyway. So here, we print the raw type, // which is probably more helpful. let type_ = Printer::new(&module.ast.names).print_type_without_aliases(annotation_type); let contents = format!( "```gleam {type_} ``` {documentation}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, &line_numbers)), } } fn hover_for_label( location: SrcSpan, type_: Arc, line_numbers: LineNumbers, module: &Module, ) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&type_); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, &line_numbers)), } } fn hover_for_module_constant( constant: &ModuleConstant>, line_numbers: LineNumbers, module: &Module, ) -> Hover { let empty_str = EcoString::from(""); let type_ = Printer::new(&module.ast.names).print_type(&constant.type_); let documentation = constant .documentation .as_ref() .map(|(_, doc)| doc) .unwrap_or(&empty_str); let contents = format!("```gleam\n{type_}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constant.location, &line_numbers)), } } fn hover_for_constant( constant: &TypedConstant, line_numbers: LineNumbers, module: &Module, ) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&constant.type_()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constant.location(), &line_numbers)), } } fn hover_for_clause_guard( guard: &TypedClauseGuard, line_numbers: LineNumbers, module: &Module, ) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&guard.type_()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(guard.location(), &line_numbers)), } } fn hover_for_string_prefix_pattern_variable( location: SrcSpan, lines: &LineNumbers, module: &Module, ) -> Hover { let type_ = Printer::new(&module.ast.names).print_type(&type_::string()); let contents = format!("```gleam\n{type_}\n```"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, lines)), } } fn hover_for_expression<'a>( expression: &'a TypedExpr, position: ExpressionPosition<'a>, line_numbers: LineNumbers, module: &Module, hex_deps: &HashSet, ) -> Hover { let documentation = expression.get_documentation().unwrap_or_default(); let link_section = get_expr_qualified_name(expression) .and_then(|(module_name, name)| { get_hexdocs_link_section(module_name, name, &module.ast, hex_deps) }) .unwrap_or("".to_string()); // Show the type of the hovered node to the user let mut printer = Printer::new(&module.ast.names); let type_ = printer.print_type(expression.type_().as_ref()); // If the expression is a record update and there's record fields that are // begin implicitly updated, then we want to list them in the hover. let unchanged_record_update_fields = match position { ExpressionPosition::Expression | ExpressionPosition::ArgumentOrLabel { .. } => "", ExpressionPosition::UpdatedRecord { unchanged_record_fields, } if !unchanged_record_fields.is_empty() => &format!( "Unchanged record fields:\n{}", unchanged_record_fields .iter() .map(|(label, type_)| format!("- `{}: {}`", label, printer.print_type(type_))) .join("\n") ), ExpressionPosition::UpdatedRecord { .. } => "", }; let description = [documentation, unchanged_record_update_fields, &link_section] .iter() .filter(|string| !string.is_empty()) .join("\n\n"); let contents = format!( "```gleam {type_} ``` {description}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(expression.location(), &line_numbers)), } } fn hover_for_imported_value( value: &ValueConstructor, location: &SrcSpan, line_numbers: LineNumbers, hex_module_imported_from: Option<&ModuleInterface>, name: &EcoString, module: &Module, ) -> Hover { let documentation = value.get_documentation().unwrap_or_default(); let link_section = hex_module_imported_from.map_or("".to_string(), |m| { format_hexdocs_link_section(m.package.as_str(), m.name.as_str(), Some(name)) }); // Show the type of the hovered node to the user let type_ = Printer::new(&module.ast.names).print_type(value.type_.as_ref()); let contents = format!( "```gleam {type_} ``` {documentation}{link_section}" ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(*location, &line_numbers)), } } fn hover_for_module( module: &ModuleInterface, location: SrcSpan, line_numbers: &LineNumbers, hex_deps: &HashSet, ) -> Hover { let documentation = module.documentation.join("\n"); let name = &module.name; let link_section = if hex_deps.contains(&module.package) { format_hexdocs_link_section(&module.package, name, None) } else { String::new() }; let contents = format!( "```gleam {name} ``` {documentation} {link_section}", ); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(location, line_numbers)), } } fn hover_for_custom_type(type_: &CustomType>, line_numbers: LineNumbers) -> Hover { let name = &type_.name; let documentation = type_ .documentation .as_ref() .map(|(_, documentation)| documentation.clone()) .unwrap_or_default(); let contents = format!("```gleam\n{name}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(type_.full_location(), &line_numbers)), } } fn hover_for_constructor( constructor: &TypedRecordConstructor, line_numbers: LineNumbers, module: &Module, ) -> Hover { let mut printer = Printer::new(&module.ast.names); let arguments = constructor .arguments .iter() .map(|argument| match &argument.label { Some((_, label)) => eco_format!("{label}: {}", printer.print_type(&argument.type_)), None => printer.print_type(&argument.type_), }) .join(", "); let documentation = constructor .documentation .as_ref() .map(|(_, documentation)| documentation.clone()) .unwrap_or_default(); let constructor_doc = if arguments.is_empty() { constructor.name.clone() } else { eco_format!("{}({arguments})", constructor.name) }; let contents = format!("```gleam\n{constructor_doc}\n```\n{documentation}"); Hover { contents: Contents::MarkedString(MarkedString::String(contents)), range: Some(src_span_to_lsp_range(constructor.location, &line_numbers)), } } /// Returns true if any part of either range overlaps with the other. pub fn overlaps(a: Range, b: Range) -> bool { position_within(a.start, b) || position_within(a.end, b) || position_within(b.start, a) || position_within(b.end, a) } /// Returns true if the first range is within the second range. /// The ranges might touch on their extremes and still be considered one within /// the other! /// /// `within(a, b)` is true in all of these cases: /// /// - ```txt /// |------| b /// |---| a /// ``` /// - ```txt /// |------| b /// |---| a /// ``` /// - ```txt /// |------| b /// |---| a /// ``` /// - ```txt /// |------| b /// |------| a /// ``` /// pub fn within(a: Range, b: Range) -> bool { position_within(a.start, b) && position_within(a.end, b) } /// Returns true if the first range is completely within the second range. /// The range cannot have any extreme in common to be considered completely /// within another. /// /// `completely_within(a, b)` is true this case: /// /// - ```txt /// |------| b /// |---| a /// ``` /// /// And `completely_within(a, b)` is false in all these cases: /// /// - ```txt /// |------| b /// |---| a /// ``` /// - ```txt /// |------| b /// |---| a /// ``` /// - ```txt /// |------| b /// |------| a /// ``` /// pub fn completely_within(a: Range, b: Range) -> bool { a.start > b.start && a.start < b.end && a.end > b.start && a.end < b.end } // Returns true if a position is within a range. pub fn position_within(position: Position, range: Range) -> bool { position >= range.start && position <= range.end } /// Builds the code action to assign an unused value to `_`. /// fn code_action_unused_values( module: &Module, line_numbers: &LineNumbers, params: &lsp::CodeActionParams, actions: &mut Vec, ) { let uri = ¶ms.text_document.uri; let mut unused_values: Vec<&SrcSpan> = module .ast .type_info .warnings .iter() .filter_map(|warning| { if let type_::Warning::ImplicitlyDiscardedResult { location } = warning { Some(location) } else { None } }) .collect(); if unused_values.is_empty() { return; } // Sort spans by start position, with longer spans coming first unused_values.sort_by_key(|span| (span.start, -(span.len() as i64))); let mut processed_lsp_range = Vec::new(); for unused in unused_values { let SrcSpan { start, end } = *unused; let hover_range = src_span_to_lsp_range(SrcSpan::new(start, end), line_numbers); // Check if this span is contained within any previously processed span if processed_lsp_range .iter() .any(|&prev_lsp_range| within(hover_range, prev_lsp_range)) { continue; } // Check if the cursor is within this span if !within(params.range, hover_range) { continue; } let edit = TextEdit { range: src_span_to_lsp_range(SrcSpan::new(start, start), line_numbers), new_text: "let _ = ".into(), }; CodeActionBuilder::new("Assign unused Result value to `_`") .kind(lsp_types::CodeActionKind::QuickFix) .changes(uri.clone(), vec![edit]) .preferred(true) .push_to(actions); processed_lsp_range.push(hover_range); } } struct NameCorrection { pub location: SrcSpan, pub correction: EcoString, } fn code_action_fix_names( module: &Module, line_numbers: &LineNumbers, params: &lsp::CodeActionParams, error: &Option, actions: &mut Vec, ) { let uri = ¶ms.text_document.uri; let Some(errors) = type_errors_for_module(error, module) else { return; }; let name_corrections = errors .iter() .filter_map(|error| { if let type_::Error::BadName { location, name, kind, } = error { Some(NameCorrection { correction: correct_name_case(name, *kind), location: *location, }) } else { None } }) .collect_vec(); if name_corrections.is_empty() { return; } for name_correction in name_corrections { let NameCorrection { location, correction, } = name_correction; let range = src_span_to_lsp_range(location, line_numbers); // Check if the user's cursor is on the invalid name if overlaps(params.range, range) { let edit = TextEdit { range, new_text: correction.to_string(), }; CodeActionBuilder::new(format!("Rename to {correction}")) .kind(lsp_types::CodeActionKind::QuickFix) .changes(uri.clone(), vec![edit]) .preferred(true) .push_to(actions); } } } fn get_expr_qualified_name(expression: &TypedExpr) -> Option<(&EcoString, &EcoString)> { match expression { TypedExpr::Var { name, constructor, .. } if constructor.publicity.is_importable() => match &constructor.variant { ValueConstructorVariant::ModuleFn { module: module_name, .. } => Some((module_name, name)), ValueConstructorVariant::ModuleConstant { module: module_name, .. } => Some((module_name, name)), ValueConstructorVariant::LocalVariable { .. } | ValueConstructorVariant::Record { .. } => None, }, TypedExpr::ModuleSelect { label, module_name, .. } => Some((module_name, label)), TypedExpr::Int { .. } | TypedExpr::Float { .. } | TypedExpr::String { .. } | TypedExpr::Block { .. } | TypedExpr::Pipeline { .. } | TypedExpr::Var { .. } | TypedExpr::Fn { .. } | TypedExpr::List { .. } | TypedExpr::Call { .. } | TypedExpr::BinOp { .. } | TypedExpr::Case { .. } | TypedExpr::RecordAccess { .. } | TypedExpr::PositionalAccess { .. } | TypedExpr::Tuple { .. } | TypedExpr::TupleIndex { .. } | TypedExpr::Todo { .. } | TypedExpr::Panic { .. } | TypedExpr::Echo { .. } | TypedExpr::BitArray { .. } | TypedExpr::RecordUpdate { .. } | TypedExpr::NegateBool { .. } | TypedExpr::NegateInt { .. } | TypedExpr::Invalid { .. } => None, } } fn format_hexdocs_link_section( package_name: &str, module_name: &str, name: Option<&str>, ) -> String { let link = match name { Some(name) => format!("https://hexdocs.pm/{package_name}/{module_name}.html#{name}"), None => format!("https://hexdocs.pm/{package_name}/{module_name}.html"), }; format!("\nView on [HexDocs]({link})") } fn get_hexdocs_link_section( module_name: &str, name: &str, ast: &TypedModule, hex_deps: &HashSet, ) -> Option { let package_name = ast.definitions.imports.iter().find_map(|import| { if import.module == module_name && hex_deps.contains(&import.package) { Some(&import.package) } else { None } })?; Some(format_hexdocs_link_section( package_name, module_name, Some(name), )) } /// Converts the source start position of a documentation comment's contents into /// the position of the leading slash in its marker ('///'). fn get_doc_marker_position(content_pos: u32) -> u32 { content_pos.saturating_sub(3) } fn make_deprecated_symbol_tag(deprecation: &Deprecation) -> Option> { deprecation .is_deprecated() .then(|| vec![SymbolTag::Deprecated]) }