// Copyright 2018-2026 the Deno authors. MIT license.

use std::str::FromStr;
use std::sync::Arc;

use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::futures::future::Shared;
use deno_resolver::deno_json::CompilerOptionsKey;
use indexmap::IndexMap;
use indexmap::IndexSet;
use lsp_types::Uri;
use tokio_util::sync::CancellationToken;
use tower_lsp::lsp_types as lsp;

use super::documents::DocumentModule;
use super::language_server::StateSnapshot;
use super::tsc;
use crate::lsp::analysis::TsFixActionCollector;
use crate::lsp::analysis::fix_ts_import_changes_for_file_rename;
use crate::lsp::completions;
use crate::lsp::completions::CompletionItemData;
use crate::lsp::config::CodeLensSettings;
use crate::lsp::config::UpdateImportsOnFileMoveEnabled;
use crate::lsp::diagnostics::ts_json_to_diagnostics;
use crate::lsp::documents::Document;
use crate::lsp::language_server;
use crate::lsp::logging::lsp_warn;
use crate::lsp::lsp_custom;
use crate::lsp::performance::Performance;
use crate::lsp::refactor;
use crate::lsp::tsc::TsJsServer;
use crate::lsp::tsc::file_text_changes_to_workspace_edit;
use crate::lsp::urls::uri_to_url;

#[derive(Debug)]
pub enum TsServer {
  Js(TsJsServer),
}

impl TsServer {
  pub fn new(performance: Arc<Performance>) -> Self {
    Self::Js(TsJsServer::new(performance))
  }

  pub fn is_started(&self) -> bool {
    match self {
      Self::Js(ts_server) => ts_server.is_started(),
    }
  }

  pub fn project_changed(
    &self,
    documents: &[(Document, super::tsc::ChangeKind)],
    configuration_changed: bool,
    snapshot: Arc<StateSnapshot>,
  ) {
    match self {
      Self::Js(ts_server) => {
        let new_compiler_options_by_key = configuration_changed.then(|| {
          snapshot
            .compiler_options_resolver
            .entries()
            .map(|(k, d)| (k.clone(), d.compiler_options.clone()))
            .collect()
        });
        let new_notebook_keys = configuration_changed.then(|| {
          snapshot
            .document_modules
            .documents
            .cells_by_notebook_uri()
            .keys()
            .map(|u| {
              let compiler_options_key = snapshot
                .compiler_options_resolver
                .entry_for_specifier(&uri_to_url(u))
                .0;
              (u.clone(), compiler_options_key.clone())
            })
            .collect()
        });
        ts_server.project_changed(
          snapshot,
          documents,
          new_compiler_options_by_key,
          new_notebook_keys,
        );
      }
    }
  }

  pub async fn get_ambient_modules(
    &self,
    compiler_options_key: &CompilerOptionsKey,
    notebook_uri: Option<&Arc<Uri>>,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Vec<String>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        ts_server
          .get_ambient_modules(
            snapshot,
            compiler_options_key,
            notebook_uri,
            token,
          )
          .await
      }
    }
  }

  pub async fn provide_diagnostics(
    &self,
    module: &DocumentModule,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Vec<lsp::Diagnostic>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut ts_diagnostics = ts_server
          .get_diagnostics(snapshot.clone(), module, token)
          .await?;
        let suggestion_actions_settings = snapshot
          .config
          .language_settings_for_specifier(&module.specifier)
          .map(|s| s.suggestion_actions.clone())
          .unwrap_or_default();
        if !suggestion_actions_settings.enabled {
          ts_diagnostics.retain(|d| {
            d.category != crate::tsc::DiagnosticCategory::Suggestion
              // Still show deprecated and unused diagnostics.
              // https://github.com/microsoft/vscode/blob/ce50bd4876af457f64d83cfd956bc916535285f4/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts#L113-L114
              || d.reports_deprecated == Some(true)
              || d.reports_unnecessary == Some(true)
          });
        }
        Ok(ts_json_to_diagnostics(
          ts_diagnostics,
          module,
          &snapshot.document_modules,
        ))
      }
    }
  }

  pub async fn provide_references(
    &self,
    document: &Document,
    _module: &DocumentModule,
    position: lsp::Position,
    context: lsp::ReferenceContext,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::Location>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut locations = IndexSet::new();
        for module in snapshot
          .document_modules
          .get_or_temp_modules_by_compiler_options_key(document)
          .into_values()
        {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let symbols = ts_server
            .find_references(
              snapshot.clone(),
              &module,
              module.line_index.offset_tsc(position)?,
              token,
            )
            .await
            .inspect_err(|err| {
              if !err.to_string().contains("Could not find source file") {
                lsp_warn!(
                  "Unable to get references from TypeScript: {:#}\nScope: {}",
                  err,
                  module.scope.as_ref().map(|s| s.as_str()).unwrap_or("null"),
                );
              }
            })
            .unwrap_or_default();
          for reference in symbols.iter().flatten().flat_map(|s| &s.references)
          {
            if token.is_cancelled() {
              return Err(anyhow!("request cancelled"));
            }
            if !context.include_declaration && reference.is_definition {
              continue;
            }
            let Some(location) = reference.entry.to_location(&module, snapshot)
            else {
              continue;
            };
            locations.insert(location);
          }
        }
        Ok(Some(locations.into_iter().collect()))
      }
    }
  }

  pub async fn provide_code_lenses(
    &self,
    module: &DocumentModule,
    settings: &CodeLensSettings,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::CodeLens>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        if !settings.implementations && !settings.references {
          return Ok(None);
        }
        let navigation_tree = ts_server
          .get_navigation_tree(snapshot, module, token)
          .await
          .map_err(|err| {
            anyhow!(
              "Error getting navigation tree for \"{}\": {:#}",
              &module.specifier,
              err,
            )
          })?;
        let code_lenses = crate::lsp::code_lens::collect_tsc(
          &module.uri,
          settings,
          &module.line_index,
          &navigation_tree,
          token,
        )?;
        if code_lenses.is_empty() {
          Ok(None)
        } else {
          Ok(Some(code_lenses))
        }
      }
    }
  }

  pub async fn provide_document_symbols(
    &self,
    module: &DocumentModule,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::DocumentSymbolResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let navigation_tree = ts_server
          .get_navigation_tree(snapshot, module, token)
          .await
          .map_err(|err| {
            anyhow!(
              "Error getting navigation tree for \"{}\": {:#}",
              &module.specifier,
              err,
            )
          })?;
        let response = if let Some(child_items) = &navigation_tree.child_items {
          let mut document_symbols = Vec::new();
          for item in child_items {
            if token.is_cancelled() {
              return Err(anyhow!("request cancelled"));
            }
            item.collect_document_symbols(
              &module.line_index,
              &mut document_symbols,
            );
          }
          Some(lsp::DocumentSymbolResponse::Nested(document_symbols))
        } else {
          None
        };
        Ok(response)
      }
    }
  }

  pub async fn provide_hover(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::Hover>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let position = module.line_index.offset_tsc(position)?;
        let quick_info = ts_server
          .get_quick_info(snapshot.clone(), module, position, token)
          .await?;
        Ok(quick_info.map(|qi| qi.to_hover(module, &snapshot)))
      }
    }
  }

  pub async fn provide_inferred_type(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp_custom::InferredTypeResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let position = module.line_index.offset_tsc(position)?;
        let quick_info = ts_server
          .get_quick_info(snapshot.clone(), module, position, token)
          .await?;
        Ok(quick_info.and_then(|quick_info| {
          quick_info.display_string(module, &snapshot).map(|text| {
            lsp_custom::InferredTypeResponse {
              text,
              range: quick_info.to_range(module),
            }
          })
        }))
      }
    }
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub async fn provide_code_actions(
    &self,
    module: &DocumentModule,
    range: lsp::Range,
    context: &lsp::CodeActionContext,
    file_diagnostics: Shared<impl Future<Output = Arc<Vec<lsp::Diagnostic>>>>,
    has_deno_code_actions: bool,
    language_server: &language_server::Inner,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::CodeActionResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let supported_code_fixes =
          ts_server.get_supported_code_fixes(snapshot.clone()).await?;
        let fixable_diagnostics = context.diagnostics.iter().filter(|d| {
          d.source.as_deref() == Some("deno-ts")
            && match &d.code {
              Some(lsp::NumberOrString::String(code)) => {
                supported_code_fixes.contains(code)
              }
              Some(lsp::NumberOrString::Number(code)) => {
                supported_code_fixes.contains(&code.to_string())
              }
              _ => false,
            }
        });
        let mut collector = TsFixActionCollector::default();
        let file_diagnostics = file_diagnostics.await;
        for diagnostic in fixable_diagnostics {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let code = match &diagnostic.code {
            Some(lsp::NumberOrString::String(code)) => match code.parse() {
              Ok(c) => c,
              Err(e) => {
                lsp_warn!("Invalid diagnostic code {code}: {e}");
                continue;
              }
            },
            Some(lsp::NumberOrString::Number(code)) => *code,
            _ => {
              lsp_warn!("Missing diagnostic code for: {:#?}", diagnostic);
              continue;
            }
          };
          let fix_actions = ts_server
            .get_code_fixes(
              snapshot.clone(),
              module,
              module.line_index.offset_tsc(diagnostic.range.start)?
                ..module.line_index.offset_tsc(diagnostic.range.end)?,
              vec![code],
              token,
            )
            .await
            .unwrap_or_else(|err| {
              // sometimes tsc reports errors when retrieving code actions
              // because they don't reflect the current state of the document
              // so we will log them to the output, but we won't send an error
              // message back to the client.
              if !token.is_cancelled() {
                lsp_warn!(
                  "Unable to get code fixes from TypeScript: {:#}",
                  err
                );
              }
              vec![]
            });
          for fix_action in fix_actions {
            if token.is_cancelled() {
              return Err(anyhow!("request cancelled"));
            }
            collector
              .add_ts_fix_action(
                &fix_action,
                diagnostic,
                module,
                language_server,
              )
              .map_err(|err| anyhow!("Unable to convert fix: {:#}", err))?;
            if collector.is_fix_all_action(
              &fix_action,
              diagnostic,
              &file_diagnostics,
            ) {
              collector.add_ts_fix_all_action(&fix_action, module, diagnostic);
            }
          }
        }

        let mut actions = collector
          .into_code_actions(has_deno_code_actions)
          .map(lsp::CodeActionOrCommand::CodeAction)
          .collect::<Vec<_>>();

        let only = context
          .only
          .as_ref()
          .and_then(|o| o.first())
          .map(|v| v.as_str().to_owned())
          .unwrap_or_default();
        let refactor_infos = ts_server
          .get_applicable_refactors(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(range.start)?
              ..module.line_index.offset_tsc(range.end)?,
            context.trigger_kind,
            only,
            token,
          )
          .await
          .map_err(|err| {
            anyhow!("Unable to get refactor info from TypeScript: {:#}", err)
          })?;
        let refactor_actions = refactor_infos
          .into_iter()
          .map(|refactor_info| {
            refactor_info
              .to_code_actions(&module.uri, &range, token)
              .map_err(|err| {
                anyhow!("Unable to convert refactor info: {:#}", err)
              })
          })
          .collect::<Result<Vec<_>, _>>()?
          .into_iter()
          .flatten();
        actions.extend(
          refactor::prune_invalid_actions(refactor_actions, 5)
            .into_iter()
            .map(lsp::CodeActionOrCommand::CodeAction),
        );

        if !snapshot.config.client_provided_organize_imports_capable()
          && context.only.as_ref().is_none_or(|o| {
            o.contains(&lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
          })
        {
          let organize_imports_edit = ts_server
            .organize_imports(snapshot.clone(), module, token)
            .await
            .map_err(|err| {
              anyhow!(
                "Unable to get organize imports edit from TypeScript: {:#}",
                err
              )
            })?;
          let text_edits = organize_imports_edit.first().map(|c| {
            c.text_changes
              .iter()
              .map(|c| c.as_text_edit(&module.line_index))
              .collect::<Vec<_>>()
          });
          if let Some(text_edits) = text_edits
            && !text_edits.is_empty()
          {
            actions.push(lsp::CodeActionOrCommand::CodeAction(
              lsp::CodeAction {
                title: "Organize Imports".to_string(),
                kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
                edit: Some(lsp::WorkspaceEdit {
                  changes: Some(
                    std::iter::once((module.uri.as_ref().clone(), text_edits))
                      .collect(),
                  ),
                  ..Default::default()
                }),
                ..Default::default()
              },
            ));
          }
        }

        Ok(Some(actions))
      }
    }
  }

  pub async fn provide_document_highlights(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::DocumentHighlight>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let highlights = ts_server
          .get_document_highlights(
            snapshot,
            module,
            module.line_index.offset_tsc(position)?,
            token,
          )
          .await?;
        highlights
          .map(|highlights| {
            highlights
              .into_iter()
              .map(|dh| {
                dh.to_highlight(&module.line_index, token).map_err(|err| {
                  anyhow!("Unable to convert document highlights: {:#}", err)
                })
              })
              .collect::<Result<Vec<_>, _>>()
              .map(|s| s.into_iter().flatten().collect())
          })
          .transpose()
      }
    }
  }

  pub async fn provide_definition(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::GotoDefinitionResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let definition_info = ts_server
          .get_definition(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(position)?,
            token,
          )
          .await?;
        super::tsc::DocumentSpan::collect_into_goto_definition_response(
          definition_info
            .iter()
            .flat_map(|i| &i.definitions)
            .flatten()
            .map(|i| (&i.document_span, module)),
          snapshot,
          token,
        )
        .map_err(|err| anyhow!("Unable to convert definition info: {:#}", err))
      }
    }
  }

  pub async fn provide_type_definition(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::request::GotoTypeDefinitionResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let definition_info = ts_server
          .get_type_definition(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(position)?,
            token,
          )
          .await?;
        super::tsc::DocumentSpan::collect_into_goto_definition_response(
          definition_info
            .iter()
            .flatten()
            .map(|i| (&i.document_span, module)),
          snapshot,
          token,
        )
        .map_err(|err| {
          anyhow!("Unable to convert type definition info: {:#}", err)
        })
      }
    }
  }

  pub async fn provide_completion(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    context: Option<&lsp::CompletionContext>,
    language_server: &language_server::Inner,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::CompletionResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let position = module.line_index.offset_tsc(position)?;
        let completion_info = ts_server
          .get_completions(
            snapshot.clone(),
            module,
            position,
            context.as_ref().and_then(|c| c.trigger_character.clone()),
            context.as_ref().map(|c| c.trigger_kind.into()),
            token,
          )
          .await
          .unwrap_or_else(|err| {
            if !token.is_cancelled() {
              lsp_warn!(
                "Unable to get completion info from TypeScript: {:#}",
                err
              );
            }
            None
          });
        completion_info
          .map(|completion_info| {
            completion_info
              .as_completion_response(
                &module.line_index,
                &snapshot
                  .config
                  .language_settings_for_specifier(&module.specifier)
                  .cloned()
                  .unwrap_or_default()
                  .suggest,
                module,
                position,
                language_server,
                token,
              )
              .map_err(|err| {
                anyhow!("Unable to convert completion info: {:#}", err)
              })
          })
          .transpose()
      }
    }
  }

  pub async fn resolve_completion_item(
    &self,
    module: &DocumentModule,
    item: lsp::CompletionItem,
    data: completions::CompletionItemData,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<lsp::CompletionItem, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let CompletionItemData::TsJs(data) = data else {
          return Ok(item);
        };
        match ts_server
          .get_completion_details(
            snapshot.clone(),
            module,
            data.position,
            data.name.clone(),
            data.source.clone(),
            data.data.clone(),
            token,
          )
          .await
        {
          Ok(Some(completion_details)) => completion_details
            .as_completion_item(&item, &data, module, &snapshot)
            .map_err(|err| {
              anyhow!("Unable to convert completion details: {:#}", err)
            }),
          Ok(None) => {
            if !token.is_cancelled() {
              lsp_warn!(
                "Received undefined completion details from TypeScript for item: {:#?}",
                &item,
              );
            }
            Ok(item)
          }
          Err(err) => {
            if !token.is_cancelled() {
              lsp_warn!(
                "Unable to get completion details from TypeScript: {:#}",
                err
              );
            }
            Ok(item)
          }
        }
      }
    }
  }

  pub async fn provide_implementations(
    &self,
    document: &Document,
    _module: &DocumentModule,
    position: lsp::Position,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::request::GotoImplementationResponse>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut implementations_with_modules = IndexMap::new();
        for module in snapshot
          .document_modules
          .get_or_temp_modules_by_compiler_options_key(document)
          .into_values()
        {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let implementations = ts_server
            .get_implementations(
              snapshot.clone(),
              &module,
              module
                .line_index
                .offset_tsc(position)?,
              token,
            )
            .await
            .inspect_err(|err| {
              if !err.to_string().contains("Could not find source file") {
                lsp_warn!(
                  "Unable to get implementation locations from TypeScript: {:#}\nScope: {}",
                  err,
                  module.scope.as_ref().map(|s| s.as_str()).unwrap_or("null"),
                );
              }
            })
            .unwrap_or_default();
          if let Some(implementations) = implementations {
            implementations_with_modules
              .extend(implementations.into_iter().map(|i| (i, module.clone())))
          }
        }
        super::tsc::DocumentSpan::collect_into_goto_definition_response(
          implementations_with_modules
            .iter()
            .map(|(i, m)| (&i.document_span, m.as_ref())),
          snapshot,
          token,
        )
        .map_err(|err| {
          anyhow!("Unable to convert implementation info: {:#}", err)
        })
      }
    }
  }

  pub async fn provide_folding_range(
    &self,
    module: &DocumentModule,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::FoldingRange>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let outlining_spans = ts_server
          .get_outlining_spans(snapshot.clone(), module, token)
          .await?;
        if !outlining_spans.is_empty() {
          let folding_ranges = outlining_spans
            .iter()
            .map(|span| {
              if token.is_cancelled() {
                return Err(anyhow!("request cancelled"));
              }
              Ok(span.to_folding_range(
                &module.line_index,
                module.text.as_bytes(),
                snapshot.config.line_folding_only_capable(),
              ))
            })
            .collect::<Result<Vec<_>, _>>()?;
          Ok(Some(folding_ranges))
        } else {
          Ok(None)
        }
      }
    }
  }

  pub async fn provide_call_hierarchy_incoming_calls(
    &self,
    document: &Document,
    _module: &DocumentModule,
    item: &lsp::CallHierarchyItem,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::CallHierarchyIncomingCall>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut incoming_calls_with_modules = IndexMap::new();
        for module in snapshot
          .document_modules
          .get_or_temp_modules_by_compiler_options_key(document)
          .into_values()
        {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let incoming_calls = ts_server
            .provide_call_hierarchy_incoming_calls(
              snapshot.clone(),
              &module,
              module.line_index.offset_tsc(item.selection_range.start)?,
              token,
            )
            .await
            .inspect_err(|err| {
              lsp_warn!(
                "Unable to get incoming calls from TypeScript: {:#}\nScope: {}",
                err,
                module.scope.as_ref().map(|s| s.as_str()).unwrap_or("null"),
              );
            })
            .unwrap_or_default();
          incoming_calls_with_modules
            .extend(incoming_calls.into_iter().map(|c| (c, module.clone())));
        }
        let incoming_calls = incoming_calls_with_modules
          .iter()
          .flat_map(|(c, module)| {
            if token.is_cancelled() {
              return Some(Err(anyhow!("request cancelled")));
            }
            Some(Ok(
              c.try_resolve_call_hierarchy_incoming_call(module, snapshot)?,
            ))
          })
          .collect::<Result<Vec<_>, _>>()?;
        Ok(Some(incoming_calls))
      }
    }
  }

  pub async fn provide_call_hierarchy_outgoing_calls(
    &self,
    module: &DocumentModule,
    item: &lsp::CallHierarchyItem,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::CallHierarchyOutgoingCall>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let outgoing_calls = ts_server
          .provide_call_hierarchy_outgoing_calls(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(item.selection_range.start)?,
            token,
          )
          .await?;
        let outgoing_calls = outgoing_calls
          .iter()
          .flat_map(|c| {
            if token.is_cancelled() {
              return Some(Err(anyhow!("request cancelled")));
            }
            Some(Ok(
              c.try_resolve_call_hierarchy_outgoing_call(module, snapshot)?,
            ))
          })
          .collect::<Result<_, _>>()?;
        Ok(Some(outgoing_calls))
      }
    }
  }

  pub async fn provide_prepare_call_hierarchy(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::CallHierarchyItem>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let items = ts_server
          .prepare_call_hierarchy(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(position)?,
            token,
          )
          .await?;
        items
          .map(|items| {
            let items = items
              .into_vec()
              .into_iter()
              .flat_map(|item| {
                if token.is_cancelled() {
                  return Some(Err(anyhow!("request cancelled")));
                }
                let item =
                  item.try_resolve_call_hierarchy_item(module, snapshot)?;
                Some(Ok(item))
              })
              .collect::<Result<Vec<_>, _>>()?;
            Ok(items)
          })
          .transpose()
      }
    }
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub async fn provide_rename(
    &self,
    document: &Document,
    _module: &DocumentModule,
    position: lsp::Position,
    new_name: &str,
    language_server: &language_server::Inner,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut locations_with_modules = IndexMap::new();
        for module in snapshot
          .document_modules
          .get_or_temp_modules_by_compiler_options_key(document)
          .into_values()
        {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let locations = ts_server
          .find_rename_locations(
            snapshot.clone(),
            &module,
            module
              .line_index
              .offset_tsc(position)?,
            token,
          )
          .await
          .inspect_err(|err| {
            if !err.to_string().contains("Could not find source file") {
              lsp_warn!(
                "Unable to get rename locations from TypeScript: {:#}\nScope: {}",
                err,
                module.scope.as_ref().map(|s| s.as_str()).unwrap_or("null"),
              );
            }
          })
          .unwrap_or_default();
          if let Some(locations) = locations {
            locations_with_modules
              .extend(locations.into_iter().map(|l| (l, module.clone())));
          }
        }
        if locations_with_modules.is_empty() {
          Ok(None)
        } else {
          let workspace_edit =
            tsc::RenameLocation::collect_into_workspace_edit(
              locations_with_modules,
              new_name,
              language_server,
              token,
            )
            .map_err(|err| {
              anyhow!("Unable to covert rename locations: {:#}", err)
            })?;
          Ok(Some(workspace_edit))
        }
      }
    }
  }

  pub async fn provide_selection_ranges(
    &self,
    module: &DocumentModule,
    positions: &[lsp::Position],
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::SelectionRange>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut selection_ranges = Vec::with_capacity(positions.len());
        for &position in positions {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let selection_range = ts_server
            .get_smart_selection_range(
              snapshot.clone(),
              module,
              module.line_index.offset_tsc(position)?,
              token,
            )
            .await?;
          selection_ranges
            .push(selection_range.to_selection_range(&module.line_index));
        }
        Ok(Some(selection_ranges))
      }
    }
  }

  pub async fn provide_semantic_tokens_full(
    &self,
    module: &DocumentModule,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::SemanticTokensResult>, AnyError> {
    let semantic_tokens = module
      .semantic_tokens_full
      .get_or_try_init(async || match self {
        Self::Js(ts_server) => ts_server
          .get_encoded_semantic_classifications(
            snapshot,
            module,
            0..module.line_index.text_content_length_utf16().into(),
            token,
          )
          .await?
          .to_semantic_tokens(&module.line_index, token),
      })
      .await?
      .clone();
    if semantic_tokens.data.is_empty() {
      Ok(None)
    } else {
      Ok(Some(lsp::SemanticTokensResult::Tokens(semantic_tokens)))
    }
  }

  pub async fn provide_semantic_tokens_range(
    &self,
    module: &DocumentModule,
    range: lsp::Range,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::SemanticTokensRangeResult>, AnyError> {
    if let Some(tokens) = module.semantic_tokens_full.get() {
      let tokens = super::semantic_tokens::tokens_within_range(tokens, range);
      let result = if !tokens.data.is_empty() {
        Some(lsp::SemanticTokensRangeResult::Tokens(tokens))
      } else {
        None
      };
      return Ok(result);
    }
    let semantic_tokens = match self {
      Self::Js(ts_server) => ts_server
        .get_encoded_semantic_classifications(
          snapshot,
          module,
          module.line_index.offset_tsc(range.start)?
            ..module.line_index.offset_tsc(range.end)?,
          token,
        )
        .await?
        .to_semantic_tokens(&module.line_index, token)?,
    };
    if semantic_tokens.data.is_empty() {
      Ok(None)
    } else {
      Ok(Some(lsp::SemanticTokensRangeResult::Tokens(
        semantic_tokens,
      )))
    }
  }

  pub async fn provide_signature_help(
    &self,
    module: &DocumentModule,
    position: lsp::Position,
    context: Option<&lsp::SignatureHelpContext>,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::SignatureHelp>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let options = if let Some(context) = context {
          tsc::SignatureHelpItemsOptions {
            trigger_reason: Some(tsc::SignatureHelpTriggerReason {
              kind: context.trigger_kind.clone().into(),
              trigger_character: context.trigger_character.clone(),
            }),
          }
        } else {
          tsc::SignatureHelpItemsOptions {
            trigger_reason: None,
          }
        };
        let signature_help = ts_server
          .get_signature_help_items(
            snapshot.clone(),
            module,
            module.line_index.offset_tsc(position)?,
            options,
            token,
          )
          .await?;
        signature_help
          .map(|signature_help| {
            signature_help.into_signature_help(module, &snapshot, token)
          })
          .transpose()
          .map_err(|err| {
            anyhow!("Unable to convert signature help items: {:#}", err)
          })
      }
    }
  }

  pub async fn provide_will_rename_files(
    &self,
    file_renames: &[lsp::FileRename],
    language_server: &language_server::Inner,
    snapshot: Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut changes_with_modules = IndexMap::new();
        for rename in file_renames {
          let Some(document) = snapshot
            .document_modules
            .documents
            .get(&Uri::from_str(&rename.old_uri).unwrap())
          else {
            continue;
          };
          for module in snapshot
            .document_modules
            .get_or_temp_modules_by_compiler_options_key(&document)
            .into_values()
          {
            if token.is_cancelled() {
              return Err(anyhow!("request cancelled"));
            }
            let options = snapshot
              .config
              .language_settings_for_specifier(&module.specifier)
              .map(|s| s.update_imports_on_file_move.clone())
              .unwrap_or_default();
            // Note that `Always` and `Prompt` are treated the same in the server, the
            // client will worry about that after receiving the edits.
            if options.enabled == UpdateImportsOnFileMoveEnabled::Never {
              continue;
            }
            let changes = ts_server
              .get_edits_for_file_rename(
                snapshot.clone(),
                &module,
                &uri_to_url(&Uri::from_str(&rename.new_uri).unwrap()),
                token,
              )
              .await?;
            let changes = fix_ts_import_changes_for_file_rename(
              changes,
              &rename.new_uri,
              &module,
              language_server,
              token,
            )
            .map_err(|err| {
              anyhow!("Unable to fix import changes: {:#}", err)
            })?;
            if !changes.is_empty() {
              changes_with_modules
                .extend(changes.into_iter().map(|c| (c, module.clone())));
            }
          }
        }
        file_text_changes_to_workspace_edit(
          changes_with_modules.iter().map(|(c, m)| (c, m.as_ref())),
          &snapshot,
          token,
        )
      }
    }
  }

  pub async fn provide_inlay_hint(
    &self,
    module: &DocumentModule,
    range: lsp::Range,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::InlayHint>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let text_span = tsc::TextSpan::from_range(range, &module.line_index)
          .map_err(|err| {
            anyhow!("Failed to convert range to tsc text span: {:#}", err)
          })?;
        let mut inlay_hints = ts_server
          .provide_inlay_hints(snapshot.clone(), module, text_span, token)
          .await;
        // Silence tsc debug failures.
        // See https://github.com/denoland/deno/issues/30455.
        // TODO(nayeemrmn): Keeps tabs on whether this is still necessary.
        if let Err(err) = &inlay_hints
          && err.to_string().contains("Debug Failure")
        {
          lsp_warn!("Unable to get inlay hints from TypeScript: {:#}", err);
          inlay_hints = Ok(None)
        }
        inlay_hints?
          .map(|inlay_hints| {
            inlay_hints
              .into_iter()
              .map(|inlay_hint| {
                if token.is_cancelled() {
                  return Err(anyhow!("request cancelled"));
                }
                Ok(inlay_hint.to_lsp(module, snapshot))
              })
              .collect()
          })
          .transpose()
      }
    }
  }

  pub async fn provide_workspace_symbol(
    &self,
    query: &str,
    snapshot: &Arc<StateSnapshot>,
    token: &CancellationToken,
  ) -> Result<Option<Vec<lsp::SymbolInformation>>, AnyError> {
    match self {
      Self::Js(ts_server) => {
        let mut items_with_scopes = IndexMap::new();
        for (compiler_options_key, compiler_options_data) in
          snapshot.compiler_options_resolver.entries()
        {
          let scope = compiler_options_data
            .workspace_dir_or_source_url
            .as_ref()
            .and_then(|s| snapshot.config.tree.scope_for_specifier(s));
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          let items = ts_server
          .get_navigate_to_items(
            snapshot.clone(),
            query.to_string(),
            // this matches vscode's hard coded result count
            Some(256),
            compiler_options_key,
            scope,
            // TODO(nayeemrmn): Support notebook scopes here.
            None,
            token,
          )
          .await
          .inspect_err(|err| {
            lsp_warn!(
              "Unable to get signature help items from TypeScript: {:#}\nScope: {}",
              err,
              scope.map(|s| s.as_str()).unwrap_or("null"),
            );
          })
          .unwrap_or_default();
          items_with_scopes.extend(
            items
              .into_iter()
              .map(|i| (i, (scope, compiler_options_key))),
          );
        }
        let symbol_information = items_with_scopes
          .into_iter()
          .flat_map(|(item, (scope, compiler_options_key))| {
            if token.is_cancelled() {
              return Some(Err(anyhow!("request cancelled")));
            }
            Some(Ok(item.to_symbol_information(
              scope.map(|s| s.as_ref()),
              compiler_options_key,
              snapshot,
            )?))
          })
          .collect::<Result<Vec<_>, _>>()?;
        let symbol_information = if symbol_information.is_empty() {
          None
        } else {
          Some(symbol_information)
        };
        Ok(symbol_information)
      }
    }
  }
}
