// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 The Gleam contributors

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::{BeamCompilerIO, 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::{HashMap, HashSet},
    sync::Arc,
};

use crate::{
    code_action::{
        ConvertIntToDifferentBase, DiscardUnusedVariable, RemoveRedundantRecordUpdate,
        ReplaceUnderscoreWithType, code_action_fix_deprecated_pipe, type_errors_for_module,
    },
    reference::find_module_references_in_module,
    rename::{rename_module_alias, rename_module_occurrences, rename_type_variable},
};

use super::{
    DownloadDependencies, MakeLocker,
    code_action::{
        AddAnnotations, AddMissingTypeParameter, AddOmittedLabels, AnnotateTopLevelDefinitions,
        CodeActionBuilder, CollapseNestedCase, ConvertBetweenDocAndRegularComment, 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_generate_type,
        code_action_import_module, code_action_inexhaustive_let_to_case,
    },
    compiler::LspProjectCompiler,
    completer::Completer,
    files::FileSystemProxy,
    progress::ProgressReporter,
    reference::{
        FindVariableReferences, Referenced, VariableReferenceKind, find_label_references,
        find_label_references_in_module, find_module_references, reference_for_ast_node,
    },
    rename::{
        RenameOutcome, RenameTarget, Renamed, rename_label, rename_local_variable,
        rename_module_entity,
    },
    signature_help, src_span_to_lsp_range,
};

#[derive(Debug, PartialEq, Eq)]
pub struct Response<T> {
    pub result: Result<T, Error>,
    pub warnings: Vec<Warning>,
    pub compilation: Compilation,
}

#[derive(Debug, PartialEq, Eq)]
pub enum Compilation {
    /// Compilation was attempted and succeeded for these modules.
    Yes(Vec<Utf8PathBuf>),
    /// Compilation was not attempted for this operation.
    No,
}

#[derive(Debug)]
pub struct LanguageServerEngine<IO, Reporter> {
    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<FileSystemProxy<IO>>,

    modules_compiled_since_last_feedback: Vec<Utf8PathBuf>,
    compiled_since_last_feedback: bool,
    error: Option<Error>,

    // 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<EcoString>,
}

impl<'a, IO, Reporter> LanguageServerEngine<IO, Reporter>
where
    // IO to be supplied from outside of gleam-core
    IO: FileSystemReader
        + FileSystemWriter
        + BeamCompilerIO
        + 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<IO>,
        paths: ProjectPaths,
    ) -> Result<Self> {
        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<FileSystemProxy<IO>> =
            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<Warning> {
        self.compiler.take_warnings()
    }

    pub fn goto_definition(
        &mut self,
        params: lsp::DefinitionParams,
    ) -> Response<Option<lsp::Location>> {
        self.respond(|this| {
            let params = params.text_document_position_params;
            let (line_numbers, node) = match this.node_at_position(&params) {
                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, &params, location))
        })
    }

    pub(crate) fn goto_type_definition(
        &mut self,
        params: lsp_types::TypeDefinitionParams,
    ) -> Response<Vec<lsp::Location>> {
        self.respond(|this| {
            let params = params.text_document_position_params;
            let (line_numbers, node) = match this.node_at_position(&params) {
                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, &params, location)
                })
                .collect_vec();

            Ok(locations)
        })
    }

    fn definition_location_to_lsp_location(
        &self,
        line_numbers: &LineNumbers,
        params: &lsp_types::TextDocumentPositionParams,
        location: DefinitionLocation,
    ) -> Option<lsp::Location> {
        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<Option<Vec<lsp::CompletionItem>>> {
        self.respond(|this| {
            let module = match this.module_for_uri(&params.text_document.uri) {
                Some(module) => module,
                None => return Ok(None),
            };

            let mut completer = Completer::new(&src, &params, &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 { .. }
                | Located::RecordLabelDefinition { .. }
                | Located::RecordLabelUsage { .. } => None,

                Located::RecordAccessLabel {
                    field_type,
                    record_type,
                    ..
                } => {
                    completer.expected_type = Some(field_type);
                    let mut completions = vec![];
                    completions.append(&mut completer.completion_values());
                    completions.append(&mut completer.completion_field_accessors(record_type));
                    Some(completions)
                }

                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()),

                Located::TypeVariable { .. } => None,
            };

            Ok(completions)
        })
    }

    pub fn code_actions(
        &mut self,
        params: lsp::CodeActionParams,
    ) -> Response<Option<Vec<CodeAction>>> {
        self.respond(|this| {
            let mut actions = vec![];
            let Some(module) = this.module_for_uri(&params.text_document.uri) else {
                return Ok(None);
            };

            let lines = LineNumbers::new(&module.code);

            code_action_unused_values(module, &lines, &params, &mut actions);
            actions.extend(RemoveUnusedImports::new(module, &lines, &params).code_actions());
            code_action_fix_names(module, &lines, &params, &this.error, &mut actions);
            code_action_import_module(module, &lines, &params, &this.error, &mut actions);
            code_action_generate_type(module, &lines, &params, &this.error, &mut actions);
            code_action_add_missing_patterns(module, &lines, &params, &this.error, &mut actions);
            actions
                .extend(RemoveUnreachableCaseClauses::new(module, &lines, &params).code_actions());
            actions
                .extend(RemoveRedundantRecordUpdate::new(module, &lines, &params).code_actions());
            actions.extend(CollapseNestedCase::new(module, &lines, &params).code_actions());
            actions.extend(FixBinaryOperation::new(module, &lines, &params).code_actions());
            actions
                .extend(FixTruncatedBitArraySegment::new(module, &lines, &params).code_actions());
            actions.extend(RemovePrivateOpaque::new(module, &lines, &params).code_actions());
            actions.extend(AddMissingTypeParameter::new(module, &lines, &params).code_actions());
            code_action_convert_qualified_constructor_to_unqualified(
                module,
                &this.compiler,
                &lines,
                &params,
                &mut actions,
            );
            code_action_convert_unqualified_constructor_to_qualified(
                module,
                &lines,
                &params,
                &mut actions,
            );
            code_action_inexhaustive_let_to_case(
                module,
                &lines,
                &params,
                &this.error,
                &mut actions,
            );
            actions.extend(MergeCaseBranches::new(module, &lines, &params).code_actions());
            actions.extend(LetAssertToCase::new(module, &lines, &params).code_actions());
            actions
                .extend(RedundantTupleInCaseSubject::new(module, &lines, &params).code_actions());
            actions.extend(FillInMissingLabelledArgs::new(module, &lines, &params).code_actions());
            actions.extend(UseLabelShorthandSyntax::new(module, &lines, &params).code_actions());
            actions.extend(ConvertFromUse::new(module, &lines, &params).code_actions());
            actions.extend(RemoveEchos::new(module, &lines, &params).code_actions());
            actions.extend(ConvertToUse::new(module, &lines, &params).code_actions());
            actions.extend(ExpandFunctionCapture::new(module, &lines, &params).code_actions());
            actions.extend(FillUnusedFields::new(module, &lines, &params).code_actions());
            actions.extend(InterpolateString::new(module, &lines, &params).code_actions());
            actions.extend(ExtractVariable::new(module, &lines, &params).code_actions());
            actions.extend(ExtractConstant::new(module, &lines, &params).code_actions());
            actions.extend(
                GenerateFunction::new(module, &this.compiler.modules, &lines, &params)
                    .code_actions(),
            );
            actions.extend(
                GenerateVariant::new(module, &this.compiler, &lines, &params).code_actions(),
            );
            actions.extend(ConvertToPipe::new(module, &lines, &params).code_actions());
            actions.extend(ConvertToFunctionCall::new(module, &lines, &params).code_actions());
            actions.extend(
                PatternMatchOnValue::new(module, &lines, &params, &this.compiler).code_actions(),
            );
            actions.extend(AddOmittedLabels::new(module, &lines, &params).code_actions());
            actions.extend(InlineVariable::new(module, &lines, &params).code_actions());
            actions.extend(WrapInBlock::new(module, &lines, &params).code_actions());
            actions.extend(RemoveBlock::new(module, &lines, &params).code_actions());
            actions.extend(ExtractFunction::new(module, &lines, &params).code_actions());
            GenerateDynamicDecoder::new(module, &lines, &params, &mut actions, &this.compiler)
                .code_actions();
            actions.extend(WrapInAnonymousFunction::new(module, &lines, &params).code_actions());
            actions.extend(UnwrapAnonymousFunction::new(module, &lines, &params).code_actions());
            GenerateJsonEncoder::new(
                module,
                &lines,
                &params,
                &mut actions,
                &this.compiler.project_compiler.config,
            )
            .code_actions();
            AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
            actions
                .extend(AnnotateTopLevelDefinitions::new(module, &lines, &params).code_actions());
            actions.extend(ReplaceUnderscoreWithType::new(module, &lines, &params).code_actions());
            actions.extend(
                CreateUnknownModule::new(
                    module,
                    &this.compiler,
                    &lines,
                    &params,
                    &this.paths,
                    &this.error,
                )
                .code_actions(),
            );
            actions.extend(DiscardUnusedVariable::new(module, &lines, &params).code_actions());
            code_action_fix_deprecated_pipe(module, &lines, &params, &mut actions);
            actions.extend(
                ConvertBetweenDocAndRegularComment::new(module, &lines, &params).code_actions(),
            );
            actions.extend(ConvertIntToDifferentBase::new(module, &lines, &params).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<Vec<DocumentSymbol>> {
        self.respond(|this| {
            let mut symbols = vec![];
            let Some(module) = this.module_for_uri(&params.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<Vec<FoldingRange>> {
        self.respond(|this| {
            let mut ranges: Vec<FoldingRange> = vec![];
            let Some(module) = this.module_for_uri(&params.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);
            }

            for doc_comment in comment_folding_spans(
                &module.extra.doc_comments,
                &module.code,
                &line_numbers,
                "///",
            ) {
                let Some(range) = folding_range_for_span(
                    doc_comment,
                    &line_numbers,
                    Some(FoldingRangeKind::Comment),
                ) else {
                    continue;
                };
                ranges.push(range);
            }

            for module_comment in comment_folding_spans(
                &module.extra.module_comments,
                &module.code,
                &line_numbers,
                "////",
            ) {
                let Some(range) = folding_range_for_span(
                    module_comment,
                    &line_numbers,
                    Some(FoldingRangeKind::Comment),
                ) else {
                    continue;
                };
                ranges.push(range);
            }

            for comment in
                comment_folding_spans(&module.extra.comments, &module.code, &line_numbers, "//")
            {
                let Some(range) =
                    folding_range_for_span(comment, &line_numbers, Some(FoldingRangeKind::Comment))
                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<Option<PrepareRenameResult>> {
        self.respond(|this| {
            let (lines, found) = match this.node_at_position(&params.text_document_position_params)
            {
                Some(value) => value,
                None => return Ok(None),
            };

            let Some(current_module) =
                this.module_for_uri(&params.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, &current_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,
                        name_start,
                        target_kind,
                        ..
                    }
                    | Referenced::ModuleType {
                        module,
                        location,
                        name_start,
                        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 {
                        // The location field is sometimes larger than the
                        // location of the actual name. In most cases they are
                        // the same, but in import statements that are for types
                        // and/or are aliased, the location is larger than the
                        // name.
                        //
                        // For example, in `import m.{type Wibble as Wobble}`,
                        // location will cover `type Wibble as Wobble` but
                        // the name is just `Wobble`. In every case (including
                        // non-imports), the name is at the very end of the
                        // location span.
                        let location = SrcSpan {
                            start: name_start,
                            end: location.end,
                        };
                        success_response(location)
                    } else {
                        None
                    }
                }
                Some(Referenced::ModuleName {
                    location,
                    module_alias,
                    ..
                }) => success_response(SrcSpan::new(
                    // Since the location contains the full module name (e.g. `wibble/wobble/woo`),
                    // we just want to include the last segment so we get a rename of the string
                    // `woo`, as that's the being referenced in module access expressions.
                    location.end - module_alias.len() as u32,
                    location.end,
                )),

                Some(Referenced::TypeVariable { location, name: _ }) => success_response(location),

                // For a label written using the shorthand syntax the location
                // spans the whole `label:`, so we trim it down to just the
                // label.
                Some(Referenced::Label {
                    location,
                    label,
                    type_module,
                    ..
                }) if this.is_same_package(current_module, &type_module) => {
                    success_response(SrcSpan {
                        start: location.start,
                        end: location.start + label.len() as u32,
                    })
                }

                _ => None,
            })
        })
    }

    pub fn rename(
        &mut self,
        params: lsp::RenameParams,
    ) -> Response<Result<Option<WorkspaceEdit>, ResponseError>> {
        self.respond(|this| {
            let position = &params.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,
                        &params,
                        definition_location,
                        name,
                        rename_kind,
                    )
                    .into_result()
                }
                Some(Referenced::ModuleValue {
                    module: module_name,
                    target_kind,
                    name,
                    name_kind,
                    ..
                }) => rename_module_entity(
                    &params,
                    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(
                    &params,
                    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, .. }) => {
                    rename_module_alias(module, &lines, &params, &module_name).into_result()
                }

                Some(Referenced::TypeVariable { location, name }) => {
                    rename_type_variable(module, &lines, &params, location, name).into_result()
                }

                Some(Referenced::Label {
                    type_module,
                    type_name,
                    label,
                    ..
                }) => rename_label(
                    &params,
                    &type_module,
                    &type_name,
                    &label,
                    this.compiler.project_compiler.get_importable_modules(),
                    &this.compiler.sources,
                )
                .into_result(),

                None => RenameOutcome::NoRenames.into_result(),
            })
        })
    }

    /// Shared core of find references and document highlight.
    fn find_references_in_scope(
        &mut self,
        position: &lsp_types::TextDocumentPositionParams,
        search_scope: FindReferencesSearchScope,
    ) -> Option<Vec<lsp::Location>> {
        let (lines, found) = self.node_at_position(position)?;

        let uri = position.text_document.uri.clone();

        let source_module = self.module_for_uri(&uri)?;

        let byte_index = lines.byte_index(position.position);

        let referenced = reference_for_ast_node(found, &source_module.name);

        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(&source_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) => match search_scope {
                FindReferencesSearchScope::AllModules => Some(find_module_references(
                    module,
                    name,
                    self.compiler.project_compiler.get_importable_modules(),
                    &self.compiler.sources,
                    ast::Layer::Value,
                )),
                FindReferencesSearchScope::CurrentModule => {
                    let source_information = self.compiler.get_source(&source_module.name)?;
                    let source_module = self.compiler.get_module_interface(&source_module.name)?;
                    Some(find_module_references_in_module(
                        module,
                        name,
                        source_module,
                        source_information,
                        ast::Layer::Value,
                    ))
                }
            },
            Some(Referenced::ModuleType { module, name, .. }) => match search_scope {
                FindReferencesSearchScope::AllModules => Some(find_module_references(
                    module,
                    name,
                    self.compiler.project_compiler.get_importable_modules(),
                    &self.compiler.sources,
                    ast::Layer::Type,
                )),
                FindReferencesSearchScope::CurrentModule => {
                    let source_information = self.compiler.get_source(&source_module.name)?;
                    let source_module = self.compiler.get_module_interface(&source_module.name)?;
                    Some(find_module_references_in_module(
                        module,
                        name,
                        source_module,
                        source_information,
                        ast::Layer::Type,
                    ))
                }
            },
            Some(Referenced::Label {
                type_module,
                type_name,
                label,
                ..
            }) => match search_scope {
                FindReferencesSearchScope::AllModules => Some(find_label_references(
                    type_module,
                    type_name,
                    label,
                    self.compiler.project_compiler.get_importable_modules(),
                    &self.compiler.sources,
                )),
                FindReferencesSearchScope::CurrentModule => {
                    let source_information = self.compiler.get_source(&source_module.name)?;
                    let source_module = self.compiler.get_module_interface(&source_module.name)?;
                    Some(find_label_references_in_module(
                        type_module,
                        type_name,
                        label,
                        source_module,
                        source_information,
                    ))
                }
            },
            _ => None,
        }
    }

    pub fn find_references(
        &mut self,
        params: lsp::ReferenceParams,
    ) -> Response<Option<Vec<lsp::Location>>> {
        self.respond(|this| {
            let position = &params.text_document_position_params;
            Ok(this.find_references_in_scope(position, FindReferencesSearchScope::AllModules))
        })
    }

    pub fn document_highlight(
        &mut self,
        params: lsp::DocumentHighlightParams,
    ) -> Response<Option<Vec<lsp::DocumentHighlight>>> {
        self.respond(|this| {
            let position = &params.text_document_position_params;
            Ok(this
                .find_references_in_scope(position, FindReferencesSearchScope::CurrentModule)
                .map(|references| {
                    references
                        .into_iter()
                        .map(|loc| lsp::DocumentHighlight {
                            range: loc.range,
                            kind: None,
                        })
                        .collect()
                }))
        })
    }

    /// Triggers after the renaming of one or more `.gleam` files, updating any
    /// imports to those modules.
    pub fn rename_files(&mut self, renames: Vec<(Url, Url)>) -> Response<Option<WorkspaceEdit>> {
        self.respond(|this| {
            let mut changes = HashMap::new();

            for (old_uri, new_uri) in renames {
                let Some(old_name) = this.module_name_for_uri(&old_uri) else {
                    continue;
                };
                let Some(new_name) = this.module_name_for_uri(&new_uri) else {
                    continue;
                };
                rename_module_occurrences(
                    old_name,
                    new_name,
                    this.compiler.project_compiler.get_importable_modules(),
                    &this.compiler.sources,
                    &mut changes,
                );
            }

            Ok(Some(WorkspaceEdit {
                changes: Some(changes),
                document_changes: None,
                change_annotations: None,
            }))
        })
    }

    fn respond<T>(&mut self, handler: impl FnOnce(&mut Self) -> Result<T>) -> Response<T> {
        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<Option<Hover>> {
        self.respond(|this| {
            let params = params.text_document_position_params;

            let (lines, found) = match this.node_at_position(&params) {
                Some(value) => value,
                None => return Ok(None),
            };

            let Some(module) = this.module_for_uri(&params.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(
                    import @ UnqualifiedImport {
                        name,
                        module: module_name,
                        location,
                        name_position: _,
                        as_name: _,
                    },
                ) => this
                    .compiler
                    .get_module_interface(module_name.as_str())
                    .and_then(|module_interface| {
                        if import.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,
                    field_type,
                }
                | Located::RecordLabelDefinition {
                    location,
                    field_type,
                    ..
                }
                | Located::RecordLabelUsage {
                    location,
                    field_type,
                    ..
                } => Some(hover_for_label(location, field_type, None, lines, module)),
                Located::RecordAccessLabel {
                    location,
                    field_type,
                    documentation,
                    ..
                } => Some(hover_for_label(
                    location,
                    field_type,
                    documentation.as_ref(),
                    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)),

                Located::TypeVariable { .. } => None,
            })
        })
    }

    pub(crate) fn signature_help(
        &mut self,
        params: lsp_types::SignatureHelpParams,
    ) -> Response<Option<SignatureHelp>> {
        self.respond(|this| {
            let Some(module) =
                this.module_for_uri(&params.text_document_position_params.text_document.uri)
            else {
                return Ok(None);
            };

            match this.node_at_position(&params.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(&params.text_document.uri)?;
        self.module_node_at_position(params, module)
    }

    fn module_name_for_uri(&self, uri: &Url) -> Option<EcoString> {
        // 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 name = Itertools::intersperse(components, "/".into())
            .collect::<String>()
            .strip_suffix(".gleam")?
            .into();
        Some(name)
    }

    fn module_for_uri(&self, uri: &Url) -> Option<&Module> {
        let module_name = self.module_name_for_uri(uri)?;
        self.compiler.modules.get(&module_name)
    }

    #[cfg(test)]
    pub fn path_for_module_name(&self, module_name: &str) -> Utf8PathBuf {
        let src_directory = self.paths.src_directory();
        src_directory.join(module_name).with_extension("gleam")
    }
}

fn import_folding_spans(
    imports: &[ast::Import<EcoString>],
    code: &str,
    line_numbers: &LineNumbers,
) -> Vec<SrcSpan> {
    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 comment_folding_spans(
    comments: &[SrcSpan],
    code: &str,
    line_numbers: &LineNumbers,
    comment_prefix: &str,
) -> Vec<SrcSpan> {
    let mut spans = vec![];
    let comments = comments.iter();

    let mut comments = comments.map(|span| {
        let line = src_span_to_lsp_range(*span, line_numbers).start.line;
        let start = line_numbers
            .line_starts
            .get(line as usize)
            .copied()
            .unwrap_or_default();
        let end = line_numbers
            .line_starts
            .get(line as usize + 1)
            .copied()
            .map(|next_line_start| next_line_start.saturating_sub(1))
            .unwrap_or(line_numbers.length);

        // Find prefix (`//`, `///` or `////`), so we use start of the prefix
        // as start of comment. This allows us to not fold comments before and
        // after some code, like here:
        // ```gleam
        // // wibble
        // // wobble
        // wibble // wubble
        // ```
        let between = &code[start as usize..end as usize];
        let start = start + between.find(comment_prefix).unwrap_or_default() as u32;
        (start, line, end)
    });

    let Some((start, line, end)) = comments.next() else {
        return spans;
    };

    let mut current_start = start;
    let mut current_line = line;
    let mut current_end = end;
    let mut current_len = 1;

    for (start, line, end) in comments {
        let separated_by_blank_line = line > current_line + 1;
        let between = &code[current_end as usize..start as usize];
        let has_non_comments_between_between = between.chars().any(|char| !char.is_whitespace());

        if separated_by_blank_line || has_non_comments_between_between {
            if current_len > 1 {
                spans.push(SrcSpan::new(current_start, current_end));
            }
            current_start = start;
            current_end = end;
            current_len = 1;
        } else {
            current_end = end;
            current_len += 1;
        }
        current_line = 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<FoldingRangeKind>,
) -> Option<FoldingRange> {
    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<Arc<Type>>,
    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<Type>,
    documentation: Option<&EcoString>,
    line_numbers: LineNumbers,
    module: &Module,
) -> Hover {
    let type_ = Printer::new(&module.ast.names).print_type(&type_);
    let contents = match documentation {
        Some(documentation) => format!("```gleam\n{type_}\n```\n{documentation}"),
        None => 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<Arc<Type>>,
    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<EcoString>,
) -> 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<EcoString>,
) -> 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<Arc<Type>>, 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<CodeAction>,
) {
    let uri = &params.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<Error>,
    actions: &mut Vec<CodeAction>,
) {
    let uri = &params.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 package_name = package_name.replace('_', "-");
    let link = match name {
        Some(name) => format!("https://{package_name}.hexdocs.pm/{module_name}.html#{name}"),
        None => format!("https://{package_name}.hexdocs.pm/{module_name}.html"),
    };
    format!("\nView on [HexDocs]({link})")
}

fn get_hexdocs_link_section(
    module_name: &str,
    name: &str,
    ast: &TypedModule,
    hex_deps: &HashSet<EcoString>,
) -> Option<String> {
    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<Vec<SymbolTag>> {
    deprecation
        .is_deprecated()
        .then(|| vec![SymbolTag::Deprecated])
}

enum FindReferencesSearchScope {
    AllModules,
    CurrentModule,
}
