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

use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::Infallible;
use std::ffi::c_void;
use std::fs;
use std::net::SocketAddr;
use std::ops::Range;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::thread;

use dashmap::DashMap;
use deno_ast::MediaType;
use deno_config::deno_json::CompilerOptions;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
use deno_core::OpState;
use deno_core::RuntimeOptions;
use deno_core::anyhow::anyhow;
use deno_core::convert::Number;
use deno_core::convert::Smi;
use deno_core::convert::ToV8;
use deno_core::error::AnyError;
use deno_core::futures::FutureExt;
use deno_core::op2;
use deno_core::parking_lot::Mutex;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde::de;
use deno_core::serde_json;
use deno_core::serde_json::Value;
use deno_core::serde_json::json;
use deno_core::serde_v8;
use deno_core::url::Url;
use deno_core::v8;
use deno_error::JsErrorBox;
use deno_graph::source::Resolver;
use deno_lib::util::result::InfallibleResultExt;
use deno_lib::worker::create_isolate_create_params;
use deno_path_util::url_to_file_path;
use deno_resolver::deno_json::CompilerOptionsKey;
use deno_runtime::deno_inspector_server::InspectPublishUid;
use deno_runtime::deno_inspector_server::InspectorServer;
use deno_runtime::deno_node::SUPPORTED_BUILTIN_NODE_MODULES;
use deno_runtime::tokio_util::create_basic_runtime;
use indexmap::IndexSet;
use lazy_regex::lazy_regex;
use lsp_types::Uri;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
use node_resolver::cache::NodeResolutionThreadLocalCache;
use once_cell::sync::Lazy;
use regex::Captures;
use regex::Regex;
use serde_repr::Deserialize_repr;
use serde_repr::Serialize_repr;
use text_size::TextRange;
use text_size::TextSize;
use tokio::sync::mpsc;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use tower_lsp::jsonrpc::Error as LspError;
use tower_lsp::lsp_types as lsp;

use super::code_lens;
use super::code_lens::CodeLensData;
use super::config;
use super::documents::DocumentModule;
use super::documents::DocumentText;
use super::language_server;
use super::language_server::StateSnapshot;
use super::logging::lsp_log;
use super::performance::Performance;
use super::performance::PerformanceMark;
use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS;
use super::refactor::EXTRACT_CONSTANT;
use super::refactor::EXTRACT_INTERFACE;
use super::refactor::EXTRACT_TYPE;
use super::refactor::RefactorCodeActionData;
use super::semantic_tokens;
use super::semantic_tokens::SemanticTokensBuilder;
use super::text::LineIndex;
use super::urls::uri_to_url;
use super::urls::url_to_uri;
use crate::args::FmtOptionsConfig;
use crate::args::jsr_url;
use crate::lsp::completions::CompletionItemData;
use crate::lsp::documents::Document;
use crate::lsp::logging::lsp_warn;
use crate::lsp::resolver::SingleReferrerGraphResolver;
use crate::lsp::urls::normalize_path;
use crate::tsc::MISSING_DEPENDENCY_SPECIFIER;
use crate::tsc::ResolveArgs;
use crate::util::path::relative_specifier;
use crate::util::path::to_percent_decoded_str;
use crate::util::v8::convert;

const QUICK_INFO_MAX_LENGTH: u32 = 1_000_000;

static BRACKET_ACCESSOR_RE: Lazy<Regex> =
  lazy_regex!(r#"^\[['"](.+)[\['"]\]$"#);
static CODEBLOCK_RE: Lazy<Regex> = lazy_regex!(r"^\s*[~`]{3}"m);
static HTTP_RE: Lazy<Regex> = lazy_regex!(r#"(?i)^https?:"#);
static SIMPLE_NAMED_IMPORT_RE: Lazy<Regex> = lazy_regex!(
  r#"(?s)^\s*import\s+(?P<type>type\s+)?\{\s*(?P<names>[^{}]+?)\s*\}\s+from\s+["'](?P<specifier>[^"']+)["'];?\s*$"#
);
static JSDOC_LINKS_RE: Lazy<Regex> = lazy_regex!(
  r"(?i)\{@(link|linkplain|linkcode) (https?://[^ |}]+?)(?:[| ]([^{}\n]+?))?\}"
);
static PART_KIND_MODIFIER_RE: Lazy<Regex> = lazy_regex!(r",|\s+");
static SCOPE_RE: Lazy<Regex> = lazy_regex!(r"scope_(\d)");

const FILE_EXTENSION_KIND_MODIFIERS: &[&str] =
  &[".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"];

type Request = (
  TscRequest,
  CompilerOptionsKey,
  Option<Arc<Url>>,
  Option<Arc<Uri>>,
  Arc<StateSnapshot>,
  oneshot::Sender<Result<String, AnyError>>,
  CancellationToken,
  Option<PendingChange>,
  Option<super::trace::Context>,
);

#[derive(Debug, Clone, Copy, Serialize_repr)]
#[repr(u8)]
pub enum IndentStyle {
  #[allow(dead_code, reason = "unsupported")]
  None = 0,
  Block = 1,
  #[allow(dead_code, reason = "unsupported")]
  Smart = 2,
}

/// Relevant subset of https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6658.
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FormatCodeSettings {
  pub base_indent_size: Option<u8>,
  pub indent_size: Option<u8>,
  pub tab_size: Option<u8>,
  pub new_line_character: Option<String>,
  pub convert_tabs_to_spaces: Option<bool>,
  pub indent_style: Option<IndentStyle>,
  pub trim_trailing_whitespace: Option<bool>,
  pub insert_space_after_comma_delimiter: Option<bool>,
  pub insert_space_after_semicolon_in_for_statements: Option<bool>,
  pub insert_space_before_and_after_binary_operators: Option<bool>,
  pub insert_space_after_constructor: Option<bool>,
  pub insert_space_after_keywords_in_control_flow_statements: Option<bool>,
  pub insert_space_after_function_keyword_for_anonymous_functions: Option<bool>,
  pub insert_space_after_opening_and_before_closing_nonempty_parenthesis:
    Option<bool>,
  pub insert_space_after_opening_and_before_closing_nonempty_brackets:
    Option<bool>,
  pub insert_space_after_opening_and_before_closing_nonempty_braces:
    Option<bool>,
  pub insert_space_after_opening_and_before_closing_template_string_braces:
    Option<bool>,
  pub insert_space_after_opening_and_before_closing_jsx_expression_braces:
    Option<bool>,
  pub insert_space_after_type_assertion: Option<bool>,
  pub insert_space_before_function_parenthesis: Option<bool>,
  pub place_open_brace_on_new_line_for_functions: Option<bool>,
  pub place_open_brace_on_new_line_for_control_blocks: Option<bool>,
  pub insert_space_before_type_annotation: Option<bool>,
  pub indent_multi_line_object_literal_beginning_on_blank_line: Option<bool>,
  pub semicolons: Option<SemicolonPreference>,
  pub indent_switch_case: Option<bool>,
}

impl From<&FmtOptionsConfig> for FormatCodeSettings {
  fn from(config: &FmtOptionsConfig) -> Self {
    FormatCodeSettings {
      base_indent_size: Some(0),
      indent_size: Some(config.indent_width.unwrap_or(2)),
      tab_size: Some(config.indent_width.unwrap_or(2)),
      new_line_character: Some("\n".to_string()),
      convert_tabs_to_spaces: Some(!config.use_tabs.unwrap_or(false)),
      indent_style: Some(IndentStyle::Block),
      trim_trailing_whitespace: Some(false),
      insert_space_after_comma_delimiter: Some(true),
      insert_space_after_semicolon_in_for_statements: Some(true),
      insert_space_before_and_after_binary_operators: Some(true),
      insert_space_after_constructor: Some(false),
      insert_space_after_keywords_in_control_flow_statements: Some(true),
      insert_space_after_function_keyword_for_anonymous_functions: Some(true),
      insert_space_after_opening_and_before_closing_nonempty_parenthesis: Some(
        false,
      ),
      insert_space_after_opening_and_before_closing_nonempty_brackets: Some(
        false,
      ),
      insert_space_after_opening_and_before_closing_nonempty_braces: Some(true),
      insert_space_after_opening_and_before_closing_template_string_braces:
        Some(false),
      insert_space_after_opening_and_before_closing_jsx_expression_braces: Some(
        false,
      ),
      insert_space_after_type_assertion: Some(false),
      insert_space_before_function_parenthesis: Some(false),
      place_open_brace_on_new_line_for_functions: Some(false),
      place_open_brace_on_new_line_for_control_blocks: Some(false),
      insert_space_before_type_annotation: Some(false),
      indent_multi_line_object_literal_beginning_on_blank_line: Some(false),
      semicolons: match config.semi_colons {
        Some(false) => Some(SemicolonPreference::Remove),
        _ => Some(SemicolonPreference::Insert),
      },
      indent_switch_case: Some(true),
    }
  }
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum SemicolonPreference {
  Insert,
  Remove,
}

// Allow due to false positive https://github.com/rust-lang/rust-clippy/issues/13170
#[allow(clippy::needless_borrows_for_generic_args, reason = "clippy bug")]
fn normalize_diagnostic(
  diagnostic: &mut crate::tsc::Diagnostic,
  specifier_map: &TscSpecifierMap,
) -> Result<(), AnyError> {
  if let Some(file_name) = &mut diagnostic.file_name {
    *file_name = specifier_map.normalize(&file_name)?.to_string();
  }
  for ri in diagnostic.related_information.iter_mut().flatten() {
    normalize_diagnostic(ri, specifier_map)?;
  }
  Ok(())
}

pub struct TsJsServer {
  performance: Arc<Performance>,
  sender: mpsc::UnboundedSender<Request>,
  receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>,
  pub specifier_map: Arc<TscSpecifierMap>,
  inspector_server_addr: Mutex<Option<String>>,
  inspector_server: Mutex<Option<Arc<InspectorServer>>>,
  pending_change: Mutex<Option<PendingChange>>,
  enable_tracing: Arc<AtomicBool>,
  start_once: std::sync::Once,
  supported_code_fixes: tokio::sync::OnceCell<Vec<String>>,
}

impl std::fmt::Debug for TsJsServer {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("TsServer")
      .field("performance", &self.performance)
      .field("sender", &self.sender)
      .field("receiver", &self.receiver)
      .field("specifier_map", &self.specifier_map)
      .field("inspector_server_addr", &self.inspector_server_addr.lock())
      .field("inspector_server", &self.inspector_server.lock().is_some())
      .field("start_once", &self.start_once)
      .finish()
  }
}

#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum ChangeKind {
  Opened = 0,
  Modified = 1,
  Closed = 2,
}

impl<'a> ToV8<'a> for ChangeKind {
  type Error = Infallible;
  fn to_v8(
    self,
    scope: &mut v8::PinScope<'a, '_>,
  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
    Smi(self as u8).to_v8(scope)
  }
}

impl Serialize for ChangeKind {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: serde::Serializer,
  {
    serializer.serialize_i32(*self as i32)
  }
}

#[derive(Debug)]
#[cfg_attr(test, derive(Serialize))]
pub struct PendingChange {
  pub modified_scripts: Vec<(String, ChangeKind)>,
  pub project_version: usize,
  pub new_compiler_options_by_key:
    Option<BTreeMap<CompilerOptionsKey, Arc<CompilerOptions>>>,
  pub new_notebook_keys: Option<BTreeMap<Arc<Uri>, CompilerOptionsKey>>,
}

impl<'a> ToV8<'a> for PendingChange {
  type Error = Infallible;
  fn to_v8(
    self,
    scope: &mut v8::PinScope<'a, '_>,
  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
    let modified_scripts = {
      let mut modified_scripts_v8 =
        Vec::with_capacity(self.modified_scripts.len());
      for (specifier, kind) in &self.modified_scripts {
        let specifier = v8::String::new(scope, specifier).unwrap().into();
        let kind = kind.to_v8(scope).unwrap_infallible();
        let pair =
          v8::Array::new_with_elements(scope, &[specifier, kind]).into();
        modified_scripts_v8.push(pair);
      }
      v8::Array::new_with_elements(scope, &modified_scripts_v8).into()
    };
    let project_version =
      v8::Integer::new_from_unsigned(scope, self.project_version as u32).into();
    let new_compiler_options_by_key = if let Some(new_compiler_options_by_key) =
      self.new_compiler_options_by_key
    {
      serde_v8::to_v8(
        scope,
        new_compiler_options_by_key.into_iter().collect::<Vec<_>>(),
      )
      .unwrap_or_else(|err| {
        lsp_warn!("Couldn't serialize ts configs: {err}");
        v8::null(scope).into()
      })
    } else {
      v8::null(scope).into()
    };
    let new_notebook_keys = if let Some(new_notebook_keys) =
      self.new_notebook_keys
    {
      serde_v8::to_v8(scope, new_notebook_keys.into_iter().collect::<Vec<_>>())
        .unwrap_or_else(|err| {
          lsp_warn!("Couldn't serialize ts configs: {err}");
          v8::null(scope).into()
        })
    } else {
      v8::null(scope).into()
    };

    Ok(
      v8::Array::new_with_elements(
        scope,
        &[
          modified_scripts,
          project_version,
          new_compiler_options_by_key,
          new_notebook_keys,
        ],
      )
      .into(),
    )
  }
}

impl PendingChange {
  fn coalesce(
    &mut self,
    new_version: usize,
    modified_scripts: Vec<(String, ChangeKind)>,
    new_compiler_options_by_key: Option<
      BTreeMap<CompilerOptionsKey, Arc<CompilerOptions>>,
    >,
    new_notebook_keys: Option<BTreeMap<Arc<Uri>, CompilerOptionsKey>>,
  ) {
    use ChangeKind::*;
    self.project_version = self.project_version.max(new_version);
    if let Some(new_compiler_options_by_key) = new_compiler_options_by_key {
      self.new_compiler_options_by_key = Some(new_compiler_options_by_key);
    }
    if let Some(new_notebook_keys) = new_notebook_keys {
      self.new_notebook_keys = Some(new_notebook_keys);
    }
    for (spec, new) in modified_scripts {
      if let Some((_, current)) =
        self.modified_scripts.iter_mut().find(|(s, _)| s == &spec)
      {
        // already a pending change for this specifier,
        // coalesce the change kinds
        match (*current, new) {
          (_, Closed) => {
            *current = Closed;
          }
          (Opened | Closed, Opened) => {
            *current = Opened;
          }
          (Modified, Opened) => {
            lsp_warn!("Unexpected change from Modified -> Opened");
            *current = Opened;
          }
          (Opened, Modified) => {
            // Opening may change the set of files in the project
            *current = Opened;
          }
          (Closed, Modified) => {
            lsp_warn!("Unexpected change from Closed -> Modifed");
            // Shouldn't happen, but if it does treat it as closed
            // since it's "stronger" than modifying an open doc
            *current = Closed;
          }
          (Modified, Modified) => {
            // no change
          }
        }
      } else {
        self.modified_scripts.push((spec, new));
      }
    }
  }
}

impl TsJsServer {
  pub fn new(performance: Arc<Performance>) -> Self {
    let (tx, request_rx) = mpsc::unbounded_channel::<Request>();
    Self {
      performance,
      sender: tx,
      receiver: Mutex::new(Some(request_rx)),
      specifier_map: Arc::new(TscSpecifierMap::new()),
      inspector_server_addr: Mutex::new(None),
      inspector_server: Mutex::new(None),
      pending_change: Mutex::new(None),
      enable_tracing: Default::default(),
      start_once: std::sync::Once::new(),
      supported_code_fixes: Default::default(),
    }
  }

  pub fn set_tracing_enabled(&self, enabled: bool) {
    self
      .enable_tracing
      .store(enabled, std::sync::atomic::Ordering::Relaxed);
  }

  /// This should be called before `self.ensure_started()`.
  pub fn set_inspector_server_addr(&self, addr: Option<String>) {
    *self.inspector_server_addr.lock() = addr;
  }

  pub fn ensure_started(&self) {
    self.start_once.call_once(|| {
      let maybe_inspector_server = self
        .inspector_server_addr
        .lock()
        .as_ref()
        .and_then(|addr| {
          addr
            .parse::<SocketAddr>()
            .inspect_err(|err| {
              lsp_warn!("Invalid inspector server address: {:#}", err);
            })
            .ok()
        })
        .map(|addr| {
          Arc::new(
            InspectorServer::new(
              addr,
              "deno-lsp-tsc",
              InspectPublishUid::default(),
            )
            .unwrap(),
          )
        });
      self
        .inspector_server
        .lock()
        .clone_from(&maybe_inspector_server);
      // TODO(bartlomieju): why is the join_handle ignored here? Should we store it
      // on the `TsServer` struct.
      let receiver = self.receiver.lock().take().unwrap();
      let performance = self.performance.clone();
      let specifier_map = self.specifier_map.clone();
      let enable_tracing = self.enable_tracing.clone();
      let _join_handle = thread::spawn(move || {
        run_tsc_thread(
          receiver,
          performance,
          specifier_map,
          maybe_inspector_server,
          enable_tracing,
        )
      });
      lsp_log!("TS server started.");
    });
  }

  pub fn is_started(&self) -> bool {
    self.start_once.is_completed()
  }

  pub fn project_changed(
    &self,
    snapshot: Arc<StateSnapshot>,
    documents: &[(Document, ChangeKind)],
    new_compiler_options_by_key: Option<
      BTreeMap<CompilerOptionsKey, Arc<CompilerOptions>>,
    >,
    new_notebook_keys: Option<BTreeMap<Arc<Uri>, CompilerOptionsKey>>,
  ) {
    let modified_scripts = documents
      .iter()
      .filter_map(|(document, change_kind)| {
        let (specifier, media_type) =
          snapshot.document_modules.primary_specifier(document)?;
        let specifier = self.specifier_map.denormalize(&specifier, media_type);
        Some((specifier, *change_kind))
      })
      .collect::<Vec<_>>();
    match &mut *self.pending_change.lock() {
      Some(pending_change) => {
        pending_change.coalesce(
          snapshot.project_version,
          modified_scripts,
          new_compiler_options_by_key,
          new_notebook_keys,
        );
      }
      pending => {
        let pending_change = PendingChange {
          modified_scripts,
          project_version: snapshot.project_version,
          new_compiler_options_by_key,
          new_notebook_keys,
        };
        *pending = Some(pending_change);
      }
    }
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_diagnostics(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    token: &CancellationToken,
  ) -> Result<Vec<crate::tsc::Diagnostic>, AnyError> {
    let req = TscRequest::GetDiagnostics((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      snapshot.project_version,
    ));
    self
      .request::<Vec<crate::tsc::Diagnostic>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut diagnostics| {
        for diagnostic in &mut diagnostics {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          normalize_diagnostic(diagnostic, &self.specifier_map)?;
        }
        Ok(diagnostics)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_ambient_modules(
    &self,
    snapshot: Arc<StateSnapshot>,
    compiler_options_key: &CompilerOptionsKey,
    notebook_uri: Option<&Arc<Uri>>,
    token: &CancellationToken,
  ) -> Result<Vec<String>, AnyError> {
    let req = TscRequest::GetAmbientModules;
    self
      .request::<Vec<String>>(
        snapshot.clone(),
        req,
        compiler_options_key,
        None,
        notebook_uri,
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn cleanup_semantic_cache(&self, snapshot: Arc<StateSnapshot>) {
    if !self.is_started() {
      return;
    }
    let req = TscRequest::CleanupSemanticCache;
    self
      .request::<()>(
        snapshot.clone(),
        req,
        &Default::default(),
        None,
        None,
        &Default::default(),
      )
      .await
      .map_err(|err| {
        log::error!("Failed to request to tsserver {}", err);
        LspError::invalid_request()
      })
      .ok();
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn find_references(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<Vec<ReferencedSymbol>>, AnyError> {
    let req = TscRequest::FindReferences((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Option<Vec<ReferencedSymbol>>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut symbols| {
        for symbol in symbols.iter_mut().flatten() {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          symbol.normalize(&self.specifier_map)?;
        }
        Ok(symbols)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_navigation_tree(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    token: &CancellationToken,
  ) -> Result<Arc<NavigationTree>, AnyError> {
    module
      .navigation_tree
      .get_or_try_init(|| async {
        let req = TscRequest::GetNavigationTree((self
          .specifier_map
          .denormalize(&module.specifier, module.media_type),));
        self
          .request(
            snapshot,
            req,
            &module.compiler_options_key,
            module.scope.as_ref(),
            module.notebook_uri.as_ref(),
            token,
          )
          .await
          .map(Arc::new)
      })
      .await
      .map(Clone::clone)
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_supported_code_fixes(
    &self,
    snapshot: Arc<StateSnapshot>,
  ) -> Result<&Vec<String>, AnyError> {
    self
      .supported_code_fixes
      .get_or_try_init(|| async move {
        let req = TscRequest::GetSupportedCodeFixes;
        self
          .request(
            snapshot,
            req,
            &Default::default(),
            None,
            None,
            &Default::default(),
          )
          .await
      })
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_quick_info(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<QuickInfo>, AnyError> {
    let req = TscRequest::GetQuickInfoAtPosition((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
      QUICK_INFO_MAX_LENGTH,
    ));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_code_fixes(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    range: Range<u32>,
    codes: Vec<i32>,
    token: &CancellationToken,
  ) -> Result<Vec<CodeFixAction>, AnyError> {
    let req = TscRequest::GetCodeFixesAtPosition(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      range.start,
      range.end,
      codes,
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
    )));
    self
      .request::<Vec<CodeFixAction>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut actions| {
        for action in &mut actions {
          action.normalize(&self.specifier_map, token)?;
        }
        Ok(actions)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_applicable_refactors(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    range: Range<u32>,
    trigger_kind: Option<lsp::CodeActionTriggerKind>,
    only: String,
    token: &CancellationToken,
  ) -> Result<Vec<ApplicableRefactorInfo>, AnyError> {
    let trigger_kind = trigger_kind.map(|reason| match reason {
      lsp::CodeActionTriggerKind::INVOKED => "invoked",
      lsp::CodeActionTriggerKind::AUTOMATIC => "implicit",
      _ => unreachable!(),
    });
    let req = TscRequest::GetApplicableRefactors(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      range.into(),
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
      trigger_kind,
      only,
    )));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub async fn get_combined_code_fix(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    fix_id: &str,
    token: &CancellationToken,
  ) -> Result<CombinedCodeActions, AnyError> {
    let req = TscRequest::GetCombinedCodeFix(Box::new((
      CombinedCodeFixScope {
        r#type: "file",
        file_name: self
          .specifier_map
          .denormalize(&module.specifier, module.media_type),
      },
      fix_id.to_string(),
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
    )));
    self
      .request::<CombinedCodeActions>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut actions| {
        actions.normalize(&self.specifier_map)?;
        Ok(actions)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_edits_for_refactor(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    range: Range<u32>,
    refactor_name: String,
    action_name: String,
    token: &CancellationToken,
  ) -> Result<RefactorEditInfo, AnyError> {
    let req = TscRequest::GetEditsForRefactor(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      range.into(),
      refactor_name,
      action_name,
      Some(UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      )),
    )));
    self
      .request::<RefactorEditInfo>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut info| {
        info.normalize(&self.specifier_map)?;
        Ok(info)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub async fn get_edits_for_file_rename(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    new_specifier: &Url,
    token: &CancellationToken,
  ) -> Result<Vec<FileTextChanges>, AnyError> {
    let req = TscRequest::GetEditsForFileRename(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      self
        .specifier_map
        .denormalize(new_specifier, module.media_type),
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
    )));
    self
      .request::<Vec<FileTextChanges>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut changes| {
        for changes in &mut changes {
          changes.normalize(&self.specifier_map)?;
          for text_changes in &mut changes.text_changes {
            if token.is_cancelled() {
              return Err(anyhow!("request cancelled"));
            }
            text_changes.new_text =
              to_percent_decoded_str(&text_changes.new_text);
          }
        }
        Ok(changes)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_document_highlights(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<Vec<DocumentHighlights>>, AnyError> {
    let denormalized_specifier = self
      .specifier_map
      .denormalize(&module.specifier, module.media_type);
    let req = TscRequest::GetDocumentHighlights(Box::new((
      denormalized_specifier.clone(),
      position,
      vec![denormalized_specifier],
    )));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_definition(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<DefinitionInfoAndBoundSpan>, AnyError> {
    let req = TscRequest::GetDefinitionAndBoundSpan((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Option<DefinitionInfoAndBoundSpan>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut info| {
        if let Some(info) = &mut info {
          info.normalize(&self.specifier_map)?;
        }
        Ok(info)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_type_definition(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<Vec<DefinitionInfo>>, AnyError> {
    let req = TscRequest::GetTypeDefinitionAtPosition((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Option<Vec<DefinitionInfo>>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut infos| {
        for info in infos.iter_mut().flatten() {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          info.normalize(&self.specifier_map)?;
        }
        Ok(infos)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_completions(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    trigger_character: Option<String>,
    trigger_kind: Option<CompletionTriggerKind>,
    token: &CancellationToken,
  ) -> Result<Option<CompletionInfo>, AnyError> {
    let req = TscRequest::GetCompletionsAtPosition(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
      GetCompletionsAtPositionOptions {
        user_preferences: UserPreferences::from_config_for_specifier(
          &snapshot.config,
          &module.specifier,
        ),
        trigger_character,
        trigger_kind,
      },
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
    )));
    self
      .request::<Option<CompletionInfo>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut info| {
        if let Some(info) = &mut info {
          info.normalize(&self.specifier_map, token)?;
        }
        Ok(info)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub async fn get_completion_details(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    name: String,
    source: Option<String>,
    data: Option<Value>,
    token: &CancellationToken,
  ) -> Result<Option<CompletionEntryDetails>, AnyError> {
    let req = TscRequest::GetCompletionEntryDetails(Box::new((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
      name,
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      source,
      Some(UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      )),
      data,
    )));
    self
      .request::<Option<CompletionEntryDetails>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut details| {
        if let Some(details) = &mut details {
          details.normalize(&self.specifier_map)?;
        }
        Ok(details)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_implementations(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<Vec<ImplementationLocation>>, AnyError> {
    let req = TscRequest::GetImplementationAtPosition((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Option<Vec<ImplementationLocation>>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut locations| {
        for location in locations.iter_mut().flatten() {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          location.normalize(&self.specifier_map)?;
        }
        Ok(locations)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_outlining_spans(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    token: &CancellationToken,
  ) -> Result<Vec<OutliningSpan>, AnyError> {
    let req = TscRequest::GetOutliningSpans((self
      .specifier_map
      .denormalize(&module.specifier, module.media_type),));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn provide_call_hierarchy_incoming_calls(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Vec<CallHierarchyIncomingCall>, AnyError> {
    let req = TscRequest::ProvideCallHierarchyIncomingCalls((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Vec<CallHierarchyIncomingCall>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut calls| {
        for call in &mut calls {
          call.normalize(&self.specifier_map)?;
        }
        Ok(calls)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn provide_call_hierarchy_outgoing_calls(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Vec<CallHierarchyOutgoingCall>, AnyError> {
    let req = TscRequest::ProvideCallHierarchyOutgoingCalls((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Vec<CallHierarchyOutgoingCall>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut calls| {
        for call in &mut calls {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          call.normalize(&self.specifier_map)?;
        }
        Ok(calls)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn prepare_call_hierarchy(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<OneOrMany<CallHierarchyItem>>, AnyError> {
    let req = TscRequest::PrepareCallHierarchy((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request::<Option<OneOrMany<CallHierarchyItem>>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut items| {
        match &mut items {
          Some(OneOrMany::One(item)) => {
            item.normalize(&self.specifier_map)?;
          }
          Some(OneOrMany::Many(items)) => {
            for item in items {
              item.normalize(&self.specifier_map)?;
            }
          }
          None => {}
        }
        Ok(items)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn find_rename_locations(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<Option<Vec<RenameLocation>>, AnyError> {
    let req = TscRequest::FindRenameLocations((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
      false,
      false,
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
    ));
    self
      .request::<Option<Vec<RenameLocation>>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut locations| {
        for location in locations.iter_mut().flatten() {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          location.normalize(&self.specifier_map)?;
        }
        Ok(locations)
      })
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_smart_selection_range(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    token: &CancellationToken,
  ) -> Result<SelectionRange, AnyError> {
    let req = TscRequest::GetSmartSelectionRange((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
    ));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_encoded_semantic_classifications(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    range: Range<u32>,
    token: &CancellationToken,
  ) -> Result<Classifications, AnyError> {
    let req = TscRequest::GetEncodedSemanticClassifications((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      TextSpan {
        start: range.start,
        length: range.end - range.start,
      },
      "2020",
    ));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_signature_help_items(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    position: u32,
    options: SignatureHelpItemsOptions,
    token: &CancellationToken,
  ) -> Result<Option<SignatureHelpItems>, AnyError> {
    let req = TscRequest::GetSignatureHelpItems((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      position,
      options,
    ));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn get_navigate_to_items(
    &self,
    snapshot: Arc<StateSnapshot>,
    search: String,
    max_result_count: Option<u32>,
    compiler_options_key: &CompilerOptionsKey,
    scope: Option<&Arc<Url>>,
    notebook_uri: Option<&Arc<Uri>>,
    token: &CancellationToken,
  ) -> Result<Vec<NavigateToItem>, AnyError> {
    let req = TscRequest::GetNavigateToItems((search, max_result_count, None));
    self
      .request::<Vec<NavigateToItem>>(
        snapshot,
        req,
        compiler_options_key,
        scope,
        notebook_uri,
        token,
      )
      .await
      .and_then(|mut items| {
        for item in &mut items {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          item.normalize(&self.specifier_map)?;
        }
        Ok(items)
      })
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn provide_inlay_hints(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    text_span: TextSpan,
    token: &CancellationToken,
  ) -> Result<Option<Vec<InlayHint>>, AnyError> {
    let req = TscRequest::ProvideInlayHints((
      self
        .specifier_map
        .denormalize(&module.specifier, module.media_type),
      text_span,
      UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      ),
    ));
    self
      .request(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))]
  pub async fn organize_imports(
    &self,
    snapshot: Arc<StateSnapshot>,
    module: &DocumentModule,
    token: &CancellationToken,
  ) -> Result<Vec<FileTextChanges>, AnyError> {
    let req = TscRequest::OrganizeImports((
      OrganizeImportsArgs {
        scope: CombinedCodeFixScope {
          r#type: "file",
          file_name: self
            .specifier_map
            .denormalize(&module.specifier, module.media_type),
        },
        mode: Some(OrganizeImportsMode::All),
      },
      (&snapshot
        .config
        .tree
        .fmt_config_for_specifier(&module.specifier)
        .options)
        .into(),
      Some(UserPreferences::from_config_for_specifier(
        &snapshot.config,
        &module.specifier,
      )),
    ));
    self
      .request::<Vec<FileTextChanges>>(
        snapshot,
        req,
        &module.compiler_options_key,
        module.scope.as_ref(),
        module.notebook_uri.as_ref(),
        token,
      )
      .await
      .and_then(|mut changes| {
        for file_change in &mut changes {
          if token.is_cancelled() {
            return Err(anyhow!("request cancelled"));
          }
          file_change.normalize(&self.specifier_map)?;
        }
        Ok(changes)
      })
  }

  async fn request<R>(
    &self,
    snapshot: Arc<StateSnapshot>,
    req: TscRequest,
    compiler_options_key: &CompilerOptionsKey,
    scope: Option<&Arc<Url>>,
    notebook_uri: Option<&Arc<Uri>>,
    token: &CancellationToken,
  ) -> Result<R, AnyError>
  where
    R: de::DeserializeOwned,
  {
    use super::trace::SpanExt;
    self.ensure_started();
    let context = super::trace::Span::current().context();
    let mark = self
      .performance
      .mark(format!("tsc.request.{}", req.method()));
    let (tx, mut rx) = oneshot::channel::<Result<String, AnyError>>();
    let change = self.pending_change.lock().take();

    if self
      .sender
      .send((
        req,
        compiler_options_key.clone(),
        scope.cloned(),
        notebook_uri.cloned(),
        snapshot,
        tx,
        token.clone(),
        change,
        Some(context),
      ))
      .is_err()
    {
      return Err(anyhow!("failed to send request to tsc thread"));
    }
    tokio::select! {
      value = &mut rx => {
        let value = value??;
        let _span = super::logging::lsp_tracing_info_span!("Tsc response deserialization");
        let r = Ok(serde_json::from_str(&value)?);
        self.performance.measure(mark);
        r
      }
      _ = token.cancelled() => {
        Err(anyhow!("request cancelled"))
      }
    }
  }
}

fn get_tag_documentation(
  tag: &JsDocTagInfo,
  module: &DocumentModule,
  snapshot: &StateSnapshot,
) -> String {
  let mut result = format!("*@{}*", tag.name);
  let Some(display_parts) = &tag.text else {
    return result;
  };
  match tag.name.as_str() {
    "param" => {
      let mut display_parts = display_parts.iter().peekable();
      if display_parts
        .peek()
        .is_some_and(|p| p.kind == "parameterName")
      {
        let parameter_name = &display_parts.next().unwrap().text;
        result.push_str(" `");
        result.push_str(parameter_name);
        result.push('`');
        if display_parts.peek().is_some_and(|p| p.kind == "space") {
          display_parts.next();
        }
      }
      let doc =
        replace_links(display_parts_to_string(display_parts, module, snapshot));
      if !doc.is_empty() {
        if doc.starts_with('-') {
          result.push(' ');
          result.push_str(&doc);
        } else {
          result.push_str(" — ");
          result.push_str(&doc);
        }
      }
    }
    "example" => {
      let text = display_parts_to_string(display_parts, module, snapshot);
      let code = if let Some(suffix_for_caption) =
        text.strip_prefix("<caption>")
        && let Some((caption, code)) =
          suffix_for_caption.split_once("</caption>")
      {
        result.push_str(" — ");
        result.push_str(&replace_links(caption));
        code.trim_start()
      } else {
        text.as_str()
      };
      if !code.is_empty() {
        result.push('\n');
        result.push_str(&make_codeblock(code));
      }
    }
    _ => {
      let doc =
        replace_links(display_parts_to_string(display_parts, module, snapshot));
      if !doc.is_empty() {
        if doc.starts_with('-') {
          result.push(' ');
          result.push_str(&doc);
        } else {
          result.push_str(" — ");
          result.push_str(&doc);
        }
      }
    }
  }
  result.push('\n');
  result
}

fn make_codeblock(text: &str) -> String {
  if CODEBLOCK_RE.is_match(text) {
    text.to_string()
  } else {
    format!("```tsx\n{text}\n```")
  }
}

/// Replace JSDoc like links (`{@link http://example.com}`) with markdown links
fn replace_links<S: AsRef<str>>(text: S) -> String {
  JSDOC_LINKS_RE
    .replace_all(text.as_ref(), |c: &Captures| match &c[1] {
      "linkcode" => format!(
        "[`{}`]({})",
        if c.get(3).is_none() {
          &c[2]
        } else {
          c[3].trim()
        },
        &c[2]
      ),
      _ => format!(
        "[{}]({})",
        if c.get(3).is_none() {
          &c[2]
        } else {
          c[3].trim()
        },
        &c[2]
      ),
    })
    .to_string()
}

fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> {
  PART_KIND_MODIFIER_RE.split(kind_modifiers).collect()
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OneOrMany<T> {
  One(T),
  Many(Vec<T>),
}

impl<T> OneOrMany<T> {
  pub fn into_vec(self) -> Vec<T> {
    match self {
      Self::One(i) => vec![i],
      Self::Many(v) => v,
    }
  }
}

/// Aligns with ts.ScriptElementKind
#[derive(
  Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Hash,
)]
pub enum ScriptElementKind {
  #[serde(rename = "")]
  #[default]
  Unknown,
  #[serde(rename = "warning")]
  Warning,
  #[serde(rename = "keyword")]
  Keyword,
  #[serde(rename = "script")]
  ScriptElement,
  #[serde(rename = "module")]
  ModuleElement,
  #[serde(rename = "class")]
  ClassElement,
  #[serde(rename = "local class")]
  LocalClassElement,
  #[serde(rename = "interface")]
  InterfaceElement,
  #[serde(rename = "type")]
  TypeElement,
  #[serde(rename = "enum")]
  EnumElement,
  #[serde(rename = "enum member")]
  EnumMemberElement,
  #[serde(rename = "var")]
  VariableElement,
  #[serde(rename = "local var")]
  LocalVariableElement,
  #[serde(rename = "using")]
  VariableUsingElement,
  #[serde(rename = "await using")]
  VariableAwaitUsingElement,
  #[serde(rename = "function")]
  FunctionElement,
  #[serde(rename = "local function")]
  LocalFunctionElement,
  #[serde(rename = "method")]
  MemberFunctionElement,
  #[serde(rename = "getter")]
  MemberGetAccessorElement,
  #[serde(rename = "setter")]
  MemberSetAccessorElement,
  #[serde(rename = "property")]
  MemberVariableElement,
  #[serde(rename = "accessor")]
  MemberAccessorVariableElement,
  #[serde(rename = "constructor")]
  ConstructorImplementationElement,
  #[serde(rename = "call")]
  CallSignatureElement,
  #[serde(rename = "index")]
  IndexSignatureElement,
  #[serde(rename = "construct")]
  ConstructSignatureElement,
  #[serde(rename = "parameter")]
  ParameterElement,
  #[serde(rename = "type parameter")]
  TypeParameterElement,
  #[serde(rename = "primitive type")]
  PrimitiveType,
  #[serde(rename = "label")]
  Label,
  #[serde(rename = "alias")]
  Alias,
  #[serde(rename = "const")]
  ConstElement,
  #[serde(rename = "let")]
  LetElement,
  #[serde(rename = "directory")]
  Directory,
  #[serde(rename = "external module name")]
  ExternalModuleName,
  #[serde(rename = "JSX attribute")]
  JsxAttribute,
  #[serde(rename = "string")]
  String,
  #[serde(rename = "link")]
  Link,
  #[serde(rename = "link name")]
  LinkName,
  #[serde(rename = "link text")]
  LinkText,
}

/// This mirrors the method `convertKind` in `completions.ts` in vscode (extensions/typescript-language-features)
/// https://github.com/microsoft/vscode/blob/bd2df940d74b51105aefb11304e028d2fb56a9dc/extensions/typescript-language-features/src/languageFeatures/completions.ts#L440
impl From<ScriptElementKind> for lsp::CompletionItemKind {
  fn from(kind: ScriptElementKind) -> Self {
    match kind {
      ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => {
        lsp::CompletionItemKind::KEYWORD
      }
      ScriptElementKind::ConstElement
      | ScriptElementKind::LetElement
      | ScriptElementKind::VariableElement
      | ScriptElementKind::LocalVariableElement
      | ScriptElementKind::Alias
      | ScriptElementKind::ParameterElement => {
        lsp::CompletionItemKind::VARIABLE
      }
      ScriptElementKind::MemberVariableElement
      | ScriptElementKind::MemberGetAccessorElement
      | ScriptElementKind::MemberSetAccessorElement => {
        lsp::CompletionItemKind::FIELD
      }
      ScriptElementKind::FunctionElement
      | ScriptElementKind::LocalFunctionElement => {
        lsp::CompletionItemKind::FUNCTION
      }
      ScriptElementKind::MemberFunctionElement
      | ScriptElementKind::ConstructSignatureElement
      | ScriptElementKind::CallSignatureElement
      | ScriptElementKind::IndexSignatureElement => {
        lsp::CompletionItemKind::METHOD
      }
      ScriptElementKind::EnumElement => lsp::CompletionItemKind::ENUM,
      ScriptElementKind::EnumMemberElement => {
        lsp::CompletionItemKind::ENUM_MEMBER
      }
      ScriptElementKind::ModuleElement
      | ScriptElementKind::ExternalModuleName => {
        lsp::CompletionItemKind::MODULE
      }
      ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => {
        lsp::CompletionItemKind::CLASS
      }
      ScriptElementKind::InterfaceElement => lsp::CompletionItemKind::INTERFACE,
      ScriptElementKind::Warning => lsp::CompletionItemKind::TEXT,
      ScriptElementKind::ScriptElement => lsp::CompletionItemKind::FILE,
      ScriptElementKind::Directory => lsp::CompletionItemKind::FOLDER,
      ScriptElementKind::String => lsp::CompletionItemKind::CONSTANT,
      ScriptElementKind::LocalClassElement
      | ScriptElementKind::ConstructorImplementationElement
      | ScriptElementKind::TypeParameterElement
      | ScriptElementKind::Label
      | ScriptElementKind::JsxAttribute
      | ScriptElementKind::Link
      | ScriptElementKind::LinkName
      | ScriptElementKind::LinkText
      | ScriptElementKind::VariableUsingElement
      | ScriptElementKind::VariableAwaitUsingElement
      | ScriptElementKind::MemberAccessorVariableElement
      | ScriptElementKind::Unknown => lsp::CompletionItemKind::PROPERTY,
    }
  }
}

/// This mirrors `fromProtocolScriptElementKind` in vscode
impl From<ScriptElementKind> for lsp::SymbolKind {
  fn from(kind: ScriptElementKind) -> Self {
    match kind {
      ScriptElementKind::ModuleElement => Self::MODULE,
      // this is only present in `getSymbolKind` in `workspaceSymbols` in
      // vscode, but seems strange it isn't consistent.
      ScriptElementKind::TypeElement => Self::CLASS,
      ScriptElementKind::ClassElement => Self::CLASS,
      ScriptElementKind::EnumElement => Self::ENUM,
      ScriptElementKind::EnumMemberElement => Self::ENUM_MEMBER,
      ScriptElementKind::InterfaceElement => Self::INTERFACE,
      ScriptElementKind::IndexSignatureElement => Self::METHOD,
      ScriptElementKind::CallSignatureElement => Self::METHOD,
      ScriptElementKind::MemberFunctionElement => Self::METHOD,
      // workspaceSymbols in vscode treats them as fields, which does seem more
      // semantically correct while `fromProtocolScriptElementKind` treats them
      // as properties.
      ScriptElementKind::MemberVariableElement => Self::FIELD,
      ScriptElementKind::MemberGetAccessorElement => Self::FIELD,
      ScriptElementKind::MemberSetAccessorElement => Self::FIELD,
      ScriptElementKind::VariableElement => Self::VARIABLE,
      ScriptElementKind::LetElement => Self::VARIABLE,
      ScriptElementKind::ConstElement => Self::VARIABLE,
      ScriptElementKind::LocalVariableElement => Self::VARIABLE,
      ScriptElementKind::Alias => Self::VARIABLE,
      ScriptElementKind::FunctionElement => Self::FUNCTION,
      ScriptElementKind::LocalFunctionElement => Self::FUNCTION,
      ScriptElementKind::ConstructSignatureElement => Self::CONSTRUCTOR,
      ScriptElementKind::ConstructorImplementationElement => Self::CONSTRUCTOR,
      ScriptElementKind::TypeParameterElement => Self::TYPE_PARAMETER,
      ScriptElementKind::String => Self::STRING,
      _ => Self::VARIABLE,
    }
  }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TextSpan {
  pub start: u32,
  pub length: u32,
}

impl TextSpan {
  pub fn from_range(
    range: lsp::Range,
    line_index: &LineIndex,
  ) -> Result<Self, AnyError> {
    let start = line_index.offset_tsc(range.start)?;
    let length = line_index.offset_tsc(range.end)? - start;
    Ok(Self { start, length })
  }

  pub fn to_range(&self, line_index: &LineIndex) -> lsp::Range {
    lsp::Range {
      start: line_index.position_utf16(self.start.into()),
      end: line_index.position_utf16(TextSize::from(self.start + self.length)),
    }
  }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SymbolDisplayPart {
  text: String,
  kind: String,
  // This is only on `JSDocLinkDisplayPart` which extends `SymbolDisplayPart`
  // but is only used as an upcast of a `SymbolDisplayPart` and not explicitly
  // returned by any API, so it is safe to add it as an optional value.
  #[serde(skip_serializing_if = "Option::is_none")]
  target: Option<DocumentSpan>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsDocTagInfo {
  name: String,
  text: Option<Vec<SymbolDisplayPart>>,
}

// Note: the tsc protocol contains fields that are part of the protocol but
// not currently used.  They are commented out in the structures so it is clear
// that they exist.

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuickInfo {
  // kind: ScriptElementKind,
  // kind_modifiers: String,
  text_span: TextSpan,
  display_parts: Option<Vec<SymbolDisplayPart>>,
  documentation: Option<Vec<SymbolDisplayPart>>,
  tags: Option<Vec<JsDocTagInfo>>,
}

#[derive(Default)]
struct Link {
  name: Option<String>,
  target: Option<DocumentSpan>,
  text: Option<String>,
  linkcode: bool,
}

/// Takes `SymbolDisplayPart` items and converts them into a string, handling
/// any `{@link Symbol}` and `{@linkcode Symbol}` JSDoc tags and linking them
/// to the their source location.
fn display_parts_to_string<'a>(
  parts: impl IntoIterator<Item = &'a SymbolDisplayPart>,
  module: &DocumentModule,
  snapshot: &StateSnapshot,
) -> String {
  let mut out = Vec::<String>::new();

  let mut current_link: Option<Link> = None;
  let mut parts = parts.into_iter().peekable();
  while let Some(part) = parts.next() {
    // Skip `import` lines.
    if part.kind == "lineBreak"
      && parts
        .peek()
        .is_some_and(|p| p.kind == "keyword" && p.text == "import")
    {
      while parts.next().is_some()
        && parts.peek().is_none_or(|p| p.kind != "lineBreak")
      {}
      continue;
    }
    match part.kind.as_str() {
      "link" => {
        if let Some(link) = current_link.as_mut() {
          if let Some(target) = &link.target {
            if let Some(specifier) = target.to_target(module, snapshot) {
              let link_text = link.text.clone().unwrap_or_else(|| {
                link
                  .name
                  .clone()
                  .map(|ref n| n.replace('`', "\\`"))
                  .unwrap_or_else(|| "".to_string())
              });
              let link_str = if link.linkcode {
                format!("[`{link_text}`]({specifier})")
              } else {
                format!("[{link_text}]({specifier})")
              };
              out.push(link_str);
            }
          } else {
            let maybe_text = link.text.clone().or_else(|| link.name.clone());
            if let Some(text) = maybe_text {
              if HTTP_RE.is_match(&text) {
                let parts: Vec<&str> = text.split(' ').collect();
                if parts.len() == 1 {
                  out.push(parts[0].to_string());
                } else {
                  let link_text = parts[1..].join(" ").replace('`', "\\`");
                  let link_str = if link.linkcode {
                    format!("[`{}`]({})", link_text, parts[0])
                  } else {
                    format!("[{}]({})", link_text, parts[0])
                  };
                  out.push(link_str);
                }
              } else {
                out.push(text.replace('`', "\\`"));
              }
            }
          }
          current_link = None;
        } else {
          current_link = Some(Link {
            linkcode: part.text.as_str() == "{@linkcode ",
            ..Default::default()
          });
        }
      }
      "linkName" => {
        if let Some(link) = current_link.as_mut() {
          link.name = Some(part.text.clone());
          link.target.clone_from(&part.target);
        }
      }
      "linkText" => {
        if let Some(link) = current_link.as_mut() {
          link.name = Some(part.text.clone());
        }
      }
      _ => out.push(
        // should decode percent-encoding string when hovering over the right edge of module specifier like below
        // module "file:///path/to/🦕"
        to_percent_decoded_str(&part.text),
      ),
    }
  }

  replace_links(out.join(""))
}

impl QuickInfo {
  pub fn display_string(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<String> {
    self
      .display_parts
      .as_ref()
      .map(|p| display_parts_to_string(p, module, snapshot))
      .filter(|s| !s.is_empty())
  }

  pub fn to_range(&self, module: &DocumentModule) -> lsp::Range {
    self.text_span.to_range(&module.line_index)
  }

  pub fn to_hover(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> lsp::Hover {
    let mut value = String::new();
    if let Some(display_string) = self.display_string(module, snapshot) {
      value.push_str("```tsx\n");
      value.push_str(&display_string);
      value.push_str("\n```\n");
    }
    if let Some(documentation) = self
      .documentation
      .clone()
      .map(|p| display_parts_to_string(&p, module, snapshot))
    {
      value.push_str(&documentation);
    }
    if let Some(tags) = &self.tags {
      for tag_info in tags {
        value.push_str("\n\n");
        value.push_str(&get_tag_documentation(tag_info, module, snapshot));
      }
    }
    lsp::Hover {
      contents: lsp::HoverContents::Markup(lsp::MarkupContent {
        kind: lsp::MarkupKind::Markdown,
        value,
      }),
      range: Some(self.text_span.to_range(&module.line_index)),
    }
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSpan {
  text_span: TextSpan,
  pub file_name: String,
  original_text_span: Option<TextSpan>,
  // original_file_name: Option<String>,
  context_span: Option<TextSpan>,
  original_context_span: Option<TextSpan>,
}

impl DocumentSpan {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.file_name = specifier_map.normalize(&self.file_name)?.to_string();
    Ok(())
  }
}

impl DocumentSpan {
  pub fn to_link(
    &self,
    origin_module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::LocationLink> {
    let target_specifier = resolve_url(&self.file_name).ok()?;
    let target_module = snapshot.document_modules.module_for_specifier(
      &target_specifier,
      origin_module.scope.as_deref(),
      Some(&origin_module.compiler_options_key),
    )?;
    let (target_range, target_selection_range) =
      if let Some(context_span) = &self.context_span {
        (
          context_span.to_range(&target_module.line_index),
          self.text_span.to_range(&target_module.line_index),
        )
      } else {
        (
          self.text_span.to_range(&target_module.line_index),
          self.text_span.to_range(&target_module.line_index),
        )
      };
    let origin_selection_range =
      if let Some(original_context_span) = &self.original_context_span {
        Some(original_context_span.to_range(&origin_module.line_index))
      } else {
        self.original_text_span.as_ref().map(|original_text_span| {
          original_text_span.to_range(&origin_module.line_index)
        })
      };
    let link = lsp::LocationLink {
      origin_selection_range,
      target_uri: target_uri_for_client(&target_module, snapshot)
        .unwrap_or_else(|| target_module.uri.as_ref().clone()),
      target_range,
      target_selection_range,
    };
    Some(link)
  }

  /// Convert the `DocumentSpan` into a specifier that can be sent to the client
  /// to link to the target document span. Used for converting JSDoc symbol
  /// links to markdown links.
  fn to_target(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<ModuleSpecifier> {
    let target_specifier = resolve_url(&self.file_name).ok()?;
    let target_module = snapshot.document_modules.module_for_specifier(
      &target_specifier,
      module.scope.as_deref(),
      Some(&module.compiler_options_key),
    )?;
    let range = self.text_span.to_range(&target_module.line_index);
    let mut target = uri_to_url(&target_module.uri);
    target.set_fragment(Some(&format!(
      "{},{}-{},{}",
      range.start.line + 1,
      range.start.character + 1,
      range.end.line + 1,
      range.end.character + 1,
    )));

    Some(target)
  }

  pub fn collect_into_goto_definition_response<'a>(
    document_spans: impl IntoIterator<Item = (&'a DocumentSpan, &'a DocumentModule)>,
    snapshot: &StateSnapshot,
    token: &CancellationToken,
  ) -> Result<Option<lsp::GotoDefinitionResponse>, AnyError> {
    let mut links = Vec::new();
    for (document_span, origin_module) in document_spans {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      links.extend(document_span.to_link(origin_module, snapshot));
    }
    if links.is_empty() {
      return Ok(None);
    }
    Ok(Some(lsp::GotoDefinitionResponse::Link(links)))
  }
}

fn target_uri_for_client(
  target_module: &DocumentModule,
  snapshot: &StateSnapshot,
) -> Option<Uri> {
  if !snapshot.client_needs_file_uris_for_virtual_documents
    || !target_module
      .uri
      .scheme()
      .as_str()
      .eq_ignore_ascii_case("deno")
  {
    return None;
  }
  let file_path = virtual_document_file_path(target_module, snapshot)?;
  if let Some(parent) = file_path.parent() {
    fs::create_dir_all(parent).ok()?;
  }
  fs::write(&file_path, target_module.text.as_bytes()).ok()?;
  url_to_uri(&Url::from_file_path(file_path).ok()?).ok()
}

fn virtual_document_file_path(
  target_module: &DocumentModule,
  snapshot: &StateSnapshot,
) -> Option<PathBuf> {
  let checksum =
    deno_lib::util::checksum::r#gen(&[target_module.uri.as_str().as_bytes()]);
  let mut file_name = target_module
    .uri
    .path()
    .as_str()
    .rsplit('/')
    .next()
    .filter(|s| !s.is_empty())
    .map(sanitize_virtual_document_file_name)
    .filter(|s| !s.is_empty())
    .unwrap_or_else(|| "mod.ts".to_string());
  if !file_name.contains('.') {
    let extension = target_module.media_type.as_ts_extension();
    if !extension.is_empty() {
      file_name.push_str(extension);
    }
  }
  Some(
    snapshot
      .cache
      .deno_dir()
      .root
      .join("lsp")
      .join("virtual_documents")
      .join(checksum)
      .join(file_name),
  )
}

fn sanitize_virtual_document_file_name(value: &str) -> String {
  value
    .chars()
    .map(|c| {
      if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
        c
      } else {
        '_'
      }
    })
    .collect()
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigateToItem {
  name: String,
  kind: ScriptElementKind,
  kind_modifiers: String,
  // match_kind: MatchKind,
  // is_case_sensitive: bool,
  file_name: String,
  text_span: TextSpan,
  container_name: Option<String>,
  // container_kind: ScriptElementKind,
}

impl NavigateToItem {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.file_name = specifier_map.normalize(&self.file_name)?.to_string();
    Ok(())
  }
}

impl NavigateToItem {
  pub fn to_symbol_information(
    &self,
    scope: Option<&Url>,
    compiler_options_key: &CompilerOptionsKey,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::SymbolInformation> {
    let target_specifier = resolve_url(&self.file_name).ok()?;
    let target_module = snapshot.document_modules.module_for_specifier(
      &target_specifier,
      scope,
      Some(compiler_options_key),
    )?;
    let range = self.text_span.to_range(&target_module.line_index);
    let location = lsp::Location {
      uri: target_module.uri.as_ref().clone(),
      range,
    };

    let mut tags: Option<Vec<lsp::SymbolTag>> = None;
    let kind_modifiers = parse_kind_modifier(&self.kind_modifiers);
    if kind_modifiers.contains("deprecated") {
      tags = Some(vec![lsp::SymbolTag::DEPRECATED]);
    }

    // The field `deprecated` is deprecated but SymbolInformation does not have
    // a default, therefore we have to supply the deprecated
    // field. It is like a bad version of Inception.
    #[allow(deprecated, reason = "see comment")]
    Some(lsp::SymbolInformation {
      name: self.name.clone(),
      kind: self.kind.clone().into(),
      tags,
      deprecated: None,
      location,
      container_name: self
        .container_name
        .as_ref()
        .filter(|s| !s.is_empty())
        .cloned(),
    })
  }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintDisplayPart {
  pub text: String,
  pub span: Option<TextSpan>,
  pub file: Option<String>,
}

impl InlayHintDisplayPart {
  pub fn to_lsp(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> lsp::InlayHintLabelPart {
    let location = self.file.as_ref().and_then(|f| {
      let target_specifier = resolve_url(f).ok()?;
      let target_module = snapshot.document_modules.module_for_specifier(
        &target_specifier,
        module.scope.as_deref(),
        Some(&module.compiler_options_key),
      )?;
      let range = self
        .span
        .as_ref()
        .map(|s| s.to_range(&target_module.line_index))
        .unwrap_or_else(|| {
          lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0))
        });
      Some(lsp::Location {
        uri: target_module.uri.as_ref().clone(),
        range,
      })
    });
    lsp::InlayHintLabelPart {
      value: self.text.clone(),
      tooltip: None,
      location,
      command: None,
    }
  }
}

#[derive(Debug, Clone, Deserialize)]
pub enum InlayHintKind {
  Type,
  Parameter,
  Enum,
}

impl InlayHintKind {
  pub fn to_lsp(&self) -> Option<lsp::InlayHintKind> {
    match self {
      Self::Enum => None,
      Self::Parameter => Some(lsp::InlayHintKind::PARAMETER),
      Self::Type => Some(lsp::InlayHintKind::TYPE),
    }
  }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlayHint {
  pub text: String,
  pub display_parts: Option<Vec<InlayHintDisplayPart>>,
  pub position: u32,
  pub kind: InlayHintKind,
  pub whitespace_before: Option<bool>,
  pub whitespace_after: Option<bool>,
}

impl InlayHint {
  pub fn to_lsp(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> lsp::InlayHint {
    lsp::InlayHint {
      position: module.line_index.position_utf16(self.position.into()),
      label: if let Some(display_parts) = &self.display_parts {
        lsp::InlayHintLabel::LabelParts(
          display_parts
            .iter()
            .map(|p| p.to_lsp(module, snapshot))
            .collect(),
        )
      } else {
        lsp::InlayHintLabel::String(self.text.clone())
      },
      kind: self.kind.to_lsp(),
      padding_left: self.whitespace_before,
      padding_right: self.whitespace_after,
      text_edits: None,
      tooltip: None,
      data: None,
    }
  }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationTree {
  pub text: String,
  pub kind: ScriptElementKind,
  pub kind_modifiers: String,
  pub spans: Vec<TextSpan>,
  pub name_span: Option<TextSpan>,
  pub child_items: Option<Vec<NavigationTree>>,
}

impl NavigationTree {
  pub fn to_code_lens(
    &self,
    line_index: &LineIndex,
    uri: &Uri,
    source: code_lens::CodeLensSource,
  ) -> lsp::CodeLens {
    let range = if let Some(name_span) = &self.name_span {
      name_span.to_range(line_index)
    } else if !self.spans.is_empty() {
      let span = &self.spans[0];
      span.to_range(line_index)
    } else {
      lsp::Range::default()
    };
    lsp::CodeLens {
      range,
      command: None,
      data: Some(json!(CodeLensData {
        source,
        uri: uri.clone(),
      })),
    }
  }

  pub fn collect_document_symbols(
    &self,
    line_index: &LineIndex,
    document_symbols: &mut Vec<lsp::DocumentSymbol>,
  ) -> bool {
    let mut should_include = self.should_include_entry();
    if !should_include
      && self
        .child_items
        .as_ref()
        .map(|v| v.is_empty())
        .unwrap_or(true)
    {
      return false;
    }

    let children = self
      .child_items
      .as_deref()
      .unwrap_or(&[] as &[NavigationTree]);
    for span in self.spans.iter() {
      let range = TextRange::at(span.start.into(), span.length.into());
      let mut symbol_children = Vec::<lsp::DocumentSymbol>::new();
      for child in children.iter() {
        let should_traverse_child = child
          .spans
          .iter()
          .map(|child_span| {
            TextRange::at(child_span.start.into(), child_span.length.into())
          })
          .any(|child_range| range.intersect(child_range).is_some());
        if should_traverse_child {
          let included_child =
            child.collect_document_symbols(line_index, &mut symbol_children);
          should_include = should_include || included_child;
        }
      }

      if should_include {
        let mut selection_span = span;
        if let Some(name_span) = self.name_span.as_ref() {
          let name_range =
            TextRange::at(name_span.start.into(), name_span.length.into());
          if range.contains_range(name_range) {
            selection_span = name_span;
          }
        }

        let name = match self.kind {
          ScriptElementKind::MemberGetAccessorElement => {
            format!("(get) {}", self.text)
          }
          ScriptElementKind::MemberSetAccessorElement => {
            format!("(set) {}", self.text)
          }
          _ => self.text.clone(),
        };

        let mut tags: Option<Vec<lsp::SymbolTag>> = None;
        let kind_modifiers = parse_kind_modifier(&self.kind_modifiers);
        if kind_modifiers.contains("deprecated") {
          tags = Some(vec![lsp::SymbolTag::DEPRECATED]);
        }

        let children = if !symbol_children.is_empty() {
          Some(symbol_children)
        } else {
          None
        };

        // The field `deprecated` is deprecated but DocumentSymbol does not have
        // a default, therefore we have to supply the deprecated
        // field. It is like a bad version of Inception.
        #[allow(deprecated, reason = "see comment")]
        document_symbols.push(lsp::DocumentSymbol {
          name,
          kind: self.kind.clone().into(),
          range: span.to_range(line_index),
          selection_range: selection_span.to_range(line_index),
          tags,
          children,
          detail: None,
          deprecated: None,
        })
      }
    }

    should_include
  }

  fn should_include_entry(&self) -> bool {
    if let ScriptElementKind::Alias = self.kind {
      return false;
    }

    !self.text.is_empty() && self.text != "<function>" && self.text != "<class>"
  }

  pub fn walk<F>(
    &self,
    token: &CancellationToken,
    callback: &F,
  ) -> Result<(), AnyError>
  where
    F: Fn(&NavigationTree, Option<&NavigationTree>),
  {
    callback(self, None);
    if let Some(child_items) = &self.child_items {
      for child in child_items {
        if token.is_cancelled() {
          return Err(anyhow!("request cancelled"));
        }
        child.walk_child(token, callback, self)?;
      }
    }
    Ok(())
  }

  fn walk_child<F>(
    &self,
    token: &CancellationToken,
    callback: &F,
    parent: &NavigationTree,
  ) -> Result<(), AnyError>
  where
    F: Fn(&NavigationTree, Option<&NavigationTree>),
  {
    callback(self, Some(parent));
    if let Some(child_items) = &self.child_items {
      for child in child_items {
        if token.is_cancelled() {
          return Err(anyhow!("request cancelled"));
        }
        child.walk_child(token, callback, self)?;
      }
    }
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImplementationLocation {
  #[serde(flatten)]
  pub document_span: DocumentSpan,
  // ImplementationLocation props
  // kind: ScriptElementKind,
  // display_parts: Vec<SymbolDisplayPart>,
}

impl ImplementationLocation {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.document_span.normalize(specifier_map)?;
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameLocation {
  #[serde(flatten)]
  document_span: DocumentSpan,
  prefix_text: Option<String>,
  suffix_text: Option<String>,
}

impl RenameLocation {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.document_span.normalize(specifier_map)?;
    Ok(())
  }
}

impl RenameLocation {
  pub fn collect_into_workspace_edit(
    locations_with_modules: impl IntoIterator<
      Item = (RenameLocation, Arc<DocumentModule>),
    >,
    new_name: &str,
    language_server: &language_server::Inner,
    token: &CancellationToken,
  ) -> Result<lsp::WorkspaceEdit, AnyError> {
    let mut changes = HashMap::new();
    let mut includes_non_files = false;
    for (location, module) in locations_with_modules {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      let target_specifier = resolve_url(&location.document_span.file_name)?;
      if target_specifier.scheme() != "file" {
        includes_non_files = true;
        continue;
      }
      let Some(target_module) =
        language_server.document_modules.module_for_specifier(
          &target_specifier,
          module.scope.as_deref(),
          Some(&module.compiler_options_key),
        )
      else {
        continue;
      };
      let text_edits: &mut Vec<_> = changes
        .entry(target_module.uri.as_ref().clone())
        .or_default();
      let new_text = [
        location.prefix_text.as_deref(),
        Some(new_name),
        location.suffix_text.as_deref(),
      ]
      .into_iter()
      .flatten()
      .collect::<Vec<_>>()
      .join("");
      text_edits.push(lsp::TextEdit {
        range: location
          .document_span
          .text_span
          .to_range(&target_module.line_index),
        new_text,
      });
    }

    if includes_non_files {
      language_server.client.show_message(lsp::MessageType::WARNING, "The renamed symbol had references in non-file schemed modules. These have not been modified.");
    }

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

#[derive(Debug, Deserialize)]
pub enum HighlightSpanKind {
  #[serde(rename = "none")]
  None,
  #[serde(rename = "definition")]
  Definition,
  #[serde(rename = "reference")]
  Reference,
  #[serde(rename = "writtenReference")]
  WrittenReference,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HighlightSpan {
  // file_name: Option<String>,
  // is_in_string: Option<bool>,
  text_span: TextSpan,
  // context_span: Option<TextSpan>,
  kind: HighlightSpanKind,
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DefinitionInfo {
  // kind: ScriptElementKind,
  // name: String,
  // container_kind: Option<ScriptElementKind>,
  // container_name: Option<String>,
  #[serde(flatten)]
  pub document_span: DocumentSpan,
}

impl DefinitionInfo {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.document_span.normalize(specifier_map)?;
    Ok(())
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefinitionInfoAndBoundSpan {
  pub definitions: Option<Vec<DefinitionInfo>>,
  // text_span: TextSpan,
}

impl DefinitionInfoAndBoundSpan {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    for definition in self.definitions.iter_mut().flatten() {
      definition.normalize(specifier_map)?;
    }
    Ok(())
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentHighlights {
  // file_name: String,
  highlight_spans: Vec<HighlightSpan>,
}

impl DocumentHighlights {
  pub fn to_highlight(
    &self,
    line_index: &LineIndex,
    token: &CancellationToken,
  ) -> Result<Vec<lsp::DocumentHighlight>, AnyError> {
    let mut highlights = Vec::with_capacity(self.highlight_spans.len());
    for hs in &self.highlight_spans {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      highlights.push(lsp::DocumentHighlight {
        range: hs.text_span.to_range(line_index),
        kind: match hs.kind {
          HighlightSpanKind::WrittenReference => {
            Some(lsp::DocumentHighlightKind::WRITE)
          }
          _ => Some(lsp::DocumentHighlightKind::READ),
        },
      });
    }
    Ok(highlights)
  }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TextChange {
  pub span: TextSpan,
  pub new_text: String,
}

impl TextChange {
  pub fn as_text_edit(&self, line_index: &LineIndex) -> lsp::TextEdit {
    lsp::TextEdit {
      range: self.span.to_range(line_index),
      new_text: self.new_text.clone(),
    }
  }

  pub fn as_text_or_annotated_text_edit(
    &self,
    line_index: &LineIndex,
  ) -> lsp::OneOf<lsp::TextEdit, lsp::AnnotatedTextEdit> {
    lsp::OneOf::Left(lsp::TextEdit {
      range: self.span.to_range(line_index),
      new_text: self.new_text.clone(),
    })
  }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct FileTextChanges {
  pub file_name: String,
  pub text_changes: Vec<TextChange>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub is_new_file: Option<bool>,
}

impl FileTextChanges {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.file_name = specifier_map.normalize(&self.file_name)?.to_string();
    Ok(())
  }

  pub fn to_text_edits(
    &self,
    module: &DocumentModule,
    language_server: &language_server::Inner,
  ) -> Option<(Uri, Vec<lsp::TextEdit>)> {
    let is_new_file = self.is_new_file.unwrap_or(false);
    let target_specifier = resolve_url(&self.file_name).ok()?;
    let target_module = if is_new_file {
      None
    } else {
      Some(language_server.document_modules.module_for_specifier(
        &target_specifier,
        module.scope.as_deref(),
        Some(&module.compiler_options_key),
      )?)
    };
    let target_uri = target_module
      .as_ref()
      .map(|m| m.uri.clone())
      .or_else(|| url_to_uri(&target_specifier).ok().map(Arc::new))?;
    let line_index = target_module
      .as_ref()
      .map(|m| Cow::Borrowed(m.line_index.as_ref()))
      .unwrap_or_else(|| Cow::Owned(LineIndex::new("")));
    let edits = self
      .text_changes
      .iter()
      .map(|tc| tc.as_text_edit(&line_index))
      .collect();
    Some((target_uri.as_ref().clone(), edits))
  }

  pub fn to_text_document_change_ops(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<Vec<lsp::DocumentChangeOperation>> {
    let is_new_file = self.is_new_file.unwrap_or(false);
    let mut ops = Vec::<lsp::DocumentChangeOperation>::new();
    let target_specifier = resolve_url(&self.file_name).ok()?;
    let target_module = if is_new_file {
      None
    } else {
      Some(snapshot.document_modules.module_for_specifier(
        &target_specifier,
        module.scope.as_deref(),
        Some(&module.compiler_options_key),
      )?)
    };
    let target_uri = target_module
      .as_ref()
      .map(|m| m.uri.clone())
      .or_else(|| url_to_uri(&target_specifier).ok().map(Arc::new))?;
    let line_index = target_module
      .as_ref()
      .map(|m| Cow::Borrowed(m.line_index.as_ref()))
      .unwrap_or_else(|| Cow::Owned(LineIndex::new("")));

    if is_new_file {
      ops.push(lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(
        lsp::CreateFile {
          uri: target_uri.as_ref().clone(),
          options: Some(lsp::CreateFileOptions {
            ignore_if_exists: Some(true),
            overwrite: None,
          }),
          annotation_id: None,
        },
      )));
    }

    let edits = self
      .text_changes
      .iter()
      .map(|tc| tc.as_text_or_annotated_text_edit(&line_index))
      .collect();
    ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit {
      text_document: lsp::OptionalVersionedTextDocumentIdentifier {
        uri: target_uri.as_ref().clone(),
        version: target_module
          .as_ref()
          .and_then(|m| m.open_data.as_ref())
          .map(|d| d.version),
      },
      edits,
    }));

    Some(ops)
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Classifications {
  spans: Vec<u32>,
}

impl Classifications {
  pub fn to_semantic_tokens(
    &self,
    line_index: &LineIndex,
    token: &CancellationToken,
  ) -> Result<lsp::SemanticTokens, AnyError> {
    // https://github.com/microsoft/vscode/blob/1.89.0/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts#L89-L115
    let token_count = self.spans.len() / 3;
    let mut builder = SemanticTokensBuilder::new();
    for i in 0..token_count {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      let src_offset = 3 * i;
      let offset = self.spans[src_offset];
      let length = self.spans[src_offset + 1];
      let ts_classification = self.spans[src_offset + 2];

      let token_type =
        Classifications::get_token_type_from_classification(ts_classification);
      let token_modifiers =
        Classifications::get_token_modifier_from_classification(
          ts_classification,
        );

      let start_pos = line_index.position_utf16(offset.into());
      let end_pos = line_index.position_utf16(TextSize::from(offset + length));

      for line in start_pos.line..(end_pos.line + 1) {
        let start_character = if line == start_pos.line {
          start_pos.character
        } else {
          0
        };
        let end_character = if line == end_pos.line {
          end_pos.character
        } else {
          line_index.line_length_utf16(line).into()
        };
        builder.push(
          line,
          start_character,
          end_character - start_character,
          token_type,
          token_modifiers,
        );
      }
    }
    Ok(builder.build(None))
  }

  fn get_token_type_from_classification(ts_classification: u32) -> u32 {
    assert!(ts_classification > semantic_tokens::MODIFIER_MASK);
    (ts_classification >> semantic_tokens::TYPE_OFFSET) - 1
  }

  fn get_token_modifier_from_classification(ts_classification: u32) -> u32 {
    ts_classification & semantic_tokens::MODIFIER_MASK
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefactorActionInfo {
  name: String,
  description: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  not_applicable_reason: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  kind: Option<String>,
}

impl RefactorActionInfo {
  pub fn get_action_kind(&self) -> lsp::CodeActionKind {
    if let Some(kind) = &self.kind {
      kind.clone().into()
    } else {
      let maybe_match = ALL_KNOWN_REFACTOR_ACTION_KINDS
        .iter()
        .find(|action| action.matches(&self.name));
      maybe_match
        .map(|action| action.kind.clone())
        .unwrap_or(lsp::CodeActionKind::REFACTOR)
    }
  }

  pub fn is_preferred(&self, all_actions: &[RefactorActionInfo]) -> bool {
    if EXTRACT_CONSTANT.matches(&self.name) {
      let get_scope = |name: &str| -> Option<u32> {
        if let Some(captures) = SCOPE_RE.captures(name) {
          captures[1].parse::<u32>().ok()
        } else {
          None
        }
      };

      return if let Some(scope) = get_scope(&self.name) {
        all_actions
          .iter()
          .filter(|other| {
            !std::ptr::eq(&self, other) && EXTRACT_CONSTANT.matches(&other.name)
          })
          .all(|other| {
            if let Some(other_scope) = get_scope(&other.name) {
              scope < other_scope
            } else {
              true
            }
          })
      } else {
        false
      };
    }
    if EXTRACT_TYPE.matches(&self.name) || EXTRACT_INTERFACE.matches(&self.name)
    {
      return true;
    }
    false
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplicableRefactorInfo {
  name: String,
  // description: String,
  // #[serde(skip_serializing_if = "Option::is_none")]
  // inlineable: Option<bool>,
  actions: Vec<RefactorActionInfo>,
}

impl ApplicableRefactorInfo {
  pub fn to_code_actions(
    &self,
    uri: &Uri,
    range: &lsp::Range,
    token: &CancellationToken,
  ) -> Result<Vec<lsp::CodeAction>, AnyError> {
    let mut code_actions = Vec::<lsp::CodeAction>::new();
    // All typescript refactoring actions are inlineable
    for action in self.actions.iter() {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      code_actions
        .push(self.as_inline_code_action(action, uri, range, &self.name));
    }
    Ok(code_actions)
  }

  fn as_inline_code_action(
    &self,
    action: &RefactorActionInfo,
    uri: &Uri,
    range: &lsp::Range,
    refactor_name: &str,
  ) -> lsp::CodeAction {
    let disabled = action.not_applicable_reason.as_ref().map(|reason| {
      lsp::CodeActionDisabled {
        reason: reason.clone(),
      }
    });

    lsp::CodeAction {
      title: action.description.to_string(),
      kind: Some(action.get_action_kind()),
      is_preferred: Some(action.is_preferred(&self.actions)),
      disabled,
      data: Some(
        serde_json::to_value(RefactorCodeActionData {
          uri: uri.clone(),
          range: *range,
          refactor_name: refactor_name.to_owned(),
          action_name: action.name.clone(),
        })
        .unwrap(),
      ),
      ..Default::default()
    }
  }
}

pub fn file_text_changes_to_workspace_edit<'a>(
  changes_with_modules: impl IntoIterator<
    Item = (&'a FileTextChanges, &'a DocumentModule),
  >,
  snapshot: &StateSnapshot,
  token: &CancellationToken,
) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
  let mut all_ops = Vec::<lsp::DocumentChangeOperation>::new();
  for (change, module) in changes_with_modules {
    if token.is_cancelled() {
      return Err(anyhow!("request cancelled"));
    }
    let Some(ops) = change.to_text_document_change_ops(module, snapshot) else {
      continue;
    };
    all_ops.extend(ops);
  }
  if all_ops.is_empty() {
    return Ok(None);
  }
  Ok(Some(lsp::WorkspaceEdit {
    document_changes: Some(lsp::DocumentChanges::Operations(all_ops)),
    ..Default::default()
  }))
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefactorEditInfo {
  pub edits: Vec<FileTextChanges>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub rename_filename: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub rename_location: Option<u32>,
}

impl RefactorEditInfo {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    for changes in &mut self.edits {
      changes.normalize(specifier_map)?;
    }
    if let Some(rename_filename) = &mut self.rename_filename {
      *rename_filename = specifier_map
        .normalize(rename_filename.as_str())?
        .to_string();
    }
    Ok(())
  }

  pub fn to_workspace_edit(
    &self,
    module: &Arc<DocumentModule>,
    snapshot: &StateSnapshot,
    token: &CancellationToken,
  ) -> Result<Option<lsp::WorkspaceEdit>, AnyError> {
    file_text_changes_to_workspace_edit(
      self.edits.iter().map(|c| (c, module.as_ref())),
      snapshot,
      token,
    )
  }

  pub fn to_rename_command(
    &self,
    module: &Arc<DocumentModule>,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::Command> {
    let rename_location = self.rename_location?;
    let rename_filename = self.rename_filename.as_ref()?;
    let target_specifier = resolve_url(rename_filename).ok()?;
    let target_module = snapshot.document_modules.module_for_specifier(
      &target_specifier,
      module.scope.as_deref(),
      Some(&module.compiler_options_key),
    )?;
    let changes = self
      .edits
      .iter()
      .find(|c| &c.file_name == rename_filename)?;
    let mut text = target_module.text.to_string();
    for change in changes.text_changes.iter().rev() {
      let range = change.span.to_range(&target_module.line_index);
      let start = target_module.line_index.offset(range.start).ok()?;
      let end = target_module.line_index.offset(range.end).ok()?;
      text.replace_range(
        u32::from(start) as usize..u32::from(end) as usize,
        &change.new_text,
      );
    }
    Some(lsp::Command {
      title: "".to_string(),
      command: "editor.action.rename".to_string(),
      arguments: Some(vec![json!([
        target_module.uri.as_ref(),
        LineIndex::new(&text).position_utf16(rename_location.into())
      ])]),
    })
  }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
  description: String,
  changes: Vec<FileTextChanges>,
  #[serde(skip_serializing_if = "Option::is_none")]
  commands: Option<Vec<Value>>,
}

impl CodeAction {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    for changes in &mut self.changes {
      changes.normalize(specifier_map)?;
    }
    Ok(())
  }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodeFixAction {
  pub description: String,
  pub changes: Vec<FileTextChanges>,
  // These are opaque types that should just be passed back when applying the
  // action.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub commands: Option<Vec<Value>>,
  pub fix_name: String,
  // It appears currently that all fixIds are strings, but the protocol
  // specifies an opaque type, the problem is that we need to use the id as a
  // hash key, and `Value` does not implement hash (and it could provide a false
  // positive depending on JSON whitespace, so we deserialize it but it might
  // break in the future)
  #[serde(skip_serializing_if = "Option::is_none")]
  pub fix_id: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub fix_all_description: Option<String>,
}

impl CodeFixAction {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
    token: &CancellationToken,
  ) -> Result<(), AnyError> {
    for changes in &mut self.changes {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      changes.normalize(specifier_map)?;
    }
    Ok(())
  }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CombinedCodeActions {
  pub changes: Vec<FileTextChanges>,
  pub commands: Option<Vec<Value>>,
}

impl CombinedCodeActions {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    for changes in &mut self.changes {
      changes.normalize(specifier_map)?;
    }
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferencedSymbol {
  pub definition: ReferencedSymbolDefinitionInfo,
  pub references: Vec<ReferencedSymbolEntry>,
}

impl ReferencedSymbol {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.definition.normalize(specifier_map)?;
    for reference in &mut self.references {
      reference.normalize(specifier_map)?;
    }
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferencedSymbolDefinitionInfo {
  #[serde(flatten)]
  pub definition_info: DefinitionInfo,
}

impl ReferencedSymbolDefinitionInfo {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.definition_info.normalize(specifier_map)?;
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferencedSymbolEntry {
  #[serde(default)]
  pub is_definition: bool,
  #[serde(flatten)]
  pub entry: ReferenceEntry,
}

impl ReferencedSymbolEntry {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.entry.normalize(specifier_map)?;
    Ok(())
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceEntry {
  // is_write_access: bool,
  // is_in_string: Option<bool>,
  #[serde(flatten)]
  pub document_span: DocumentSpan,
}

impl ReferenceEntry {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.document_span.normalize(specifier_map)?;
    Ok(())
  }
}

impl ReferenceEntry {
  pub fn to_location(
    &self,
    module: &Arc<DocumentModule>,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::Location> {
    let target_specifier = resolve_url(&self.document_span.file_name).ok()?;
    let target_module = if target_specifier == *module.specifier {
      module.clone()
    } else {
      snapshot.document_modules.module_for_specifier(
        &target_specifier,
        module.scope.as_deref(),
        Some(&module.compiler_options_key),
      )?
    };
    Some(lsp::Location {
      uri: target_module.uri.as_ref().clone(),
      range: self
        .document_span
        .text_span
        .to_range(&target_module.line_index),
    })
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyItem {
  name: String,
  kind: ScriptElementKind,
  #[serde(skip_serializing_if = "Option::is_none")]
  kind_modifiers: Option<String>,
  file: String,
  span: TextSpan,
  selection_span: TextSpan,
  #[serde(skip_serializing_if = "Option::is_none")]
  container_name: Option<String>,
}

impl CallHierarchyItem {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.file = specifier_map.normalize(&self.file)?.to_string();
    Ok(())
  }

  pub fn try_resolve_call_hierarchy_item(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::CallHierarchyItem> {
    let (item, _) = self.to_call_hierarchy_item(module, snapshot)?;
    Some(item)
  }

  fn to_call_hierarchy_item(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<(lsp::CallHierarchyItem, Arc<DocumentModule>)> {
    let target_specifier = resolve_url(&self.file).ok()?;
    let target_module = snapshot.document_modules.module_for_specifier(
      &target_specifier,
      module.scope.as_deref(),
      Some(&module.compiler_options_key),
    )?;

    let use_file_name = self.is_source_file_item();
    let maybe_file_path = url_to_file_path(&target_module.specifier).ok();
    let name = if use_file_name {
      if let Some(file_path) = &maybe_file_path {
        normalize_path(file_path).to_string_lossy().into_owned()
      } else {
        target_module.uri.to_string()
      }
    } else {
      self.name.clone()
    };

    let mut tags: Option<Vec<lsp::SymbolTag>> = None;
    if let Some(modifiers) = self.kind_modifiers.as_ref() {
      let kind_modifiers = parse_kind_modifier(modifiers);
      if kind_modifiers.contains("deprecated") {
        tags = Some(vec![lsp::SymbolTag::DEPRECATED]);
      }
    }

    Some((
      lsp::CallHierarchyItem {
        name,
        tags,
        uri: target_module.uri.as_ref().clone(),
        detail: self.container_name.clone(),
        kind: self.kind.clone().into(),
        range: self.span.to_range(&target_module.line_index),
        selection_range: self
          .selection_span
          .to_range(&target_module.line_index),
        data: None,
      },
      target_module,
    ))
  }

  fn is_source_file_item(&self) -> bool {
    self.kind == ScriptElementKind::ScriptElement
      || self.kind == ScriptElementKind::ModuleElement
        && self.selection_span.start == 0
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCall {
  from: CallHierarchyItem,
  from_spans: Vec<TextSpan>,
}

impl CallHierarchyIncomingCall {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.from.normalize(specifier_map)?;
    Ok(())
  }

  pub fn try_resolve_call_hierarchy_incoming_call(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::CallHierarchyIncomingCall> {
    let (from, target_module) =
      self.from.to_call_hierarchy_item(module, snapshot)?;
    Some(lsp::CallHierarchyIncomingCall {
      from,
      from_ranges: self
        .from_spans
        .iter()
        .map(|span| span.to_range(&target_module.line_index))
        .collect(),
    })
  }
}

#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCall {
  to: CallHierarchyItem,
  from_spans: Vec<TextSpan>,
}

impl CallHierarchyOutgoingCall {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    self.to.normalize(specifier_map)?;
    Ok(())
  }

  pub fn try_resolve_call_hierarchy_outgoing_call(
    &self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Option<lsp::CallHierarchyOutgoingCall> {
    let (to, _) = self.to.to_call_hierarchy_item(module, snapshot)?;
    Some(lsp::CallHierarchyOutgoingCall {
      to,
      from_ranges: self
        .from_spans
        .iter()
        .map(|span| span.to_range(&module.line_index))
        .collect(),
    })
  }
}

/// Used to convert completion code actions into a command and additional text
/// edits to pass in the completion item.
fn parse_code_actions(
  maybe_code_actions: Option<&Vec<CodeAction>>,
  data: &TsJsCompletionItemData,
  module: &DocumentModule,
) -> Result<(Option<lsp::Command>, Option<Vec<lsp::TextEdit>>), AnyError> {
  if let Some(code_actions) = maybe_code_actions {
    let mut additional_text_edits: Vec<lsp::TextEdit> = Vec::new();
    let mut has_remaining_commands_or_edits = false;
    for ts_action in code_actions {
      if ts_action.commands.is_some() {
        has_remaining_commands_or_edits = true;
      }

      for change in &ts_action.changes {
        if module.specifier.as_str() == change.file_name {
          additional_text_edits.extend(change.text_changes.iter().map(|tc| {
            let mut text_edit = tc.as_text_edit(&module.line_index);
            if let Some(specifier_rewrite) = &data.specifier_rewrite {
              rewrite_first_quoted_specifier(
                &mut text_edit.new_text,
                &specifier_rewrite.new_specifier,
              );
              if let Some(deno_types_specifier) =
                &specifier_rewrite.new_deno_types_specifier
              {
                text_edit.new_text = format!(
                  "// @ts-types=\"{}\"\n{}",
                  deno_types_specifier, &text_edit.new_text
                );
              }
            }
            merge_completion_import_edit(&mut text_edit, module);
            text_edit
          }));
        } else {
          has_remaining_commands_or_edits = true;
        }
      }
    }

    let mut command: Option<lsp::Command> = None;
    if has_remaining_commands_or_edits {
      let actions: Vec<Value> = code_actions
        .iter()
        .map(|ca| {
          let changes: Vec<FileTextChanges> = ca
            .changes
            .clone()
            .into_iter()
            .filter(|ch| ch.file_name == module.specifier.as_str())
            .collect();
          json!({
            "commands": ca.commands,
            "description": ca.description,
            "changes": changes,
          })
        })
        .collect();
      command = Some(lsp::Command {
        title: "".to_string(),
        command: "_typescript.applyCompletionCodeAction".to_string(),
        arguments: Some(vec![
          json!(module.specifier.to_string()),
          json!(actions),
        ]),
      });
    }

    if additional_text_edits.is_empty() {
      Ok((command, None))
    } else {
      Ok((command, Some(additional_text_edits)))
    }
  } else {
    Ok((None, None))
  }
}

fn merge_completion_import_edit(
  text_edit: &mut lsp::TextEdit,
  module: &DocumentModule,
) {
  let Some(new_import) = parse_simple_named_import(&text_edit.new_text) else {
    return;
  };
  let Some(existing_import) =
    find_mergeable_named_import(&module.text, &new_import)
  else {
    return;
  };
  text_edit.range = lsp::Range {
    start: byte_offset_to_position(&module.text, existing_import.insert_offset),
    end: byte_offset_to_position(&module.text, existing_import.insert_offset),
  };
  text_edit.new_text = existing_import.new_text;
}

struct SimpleNamedImport<'a> {
  is_type_only: bool,
  specifier: &'a str,
  names: Vec<&'a str>,
}

struct ExistingNamedImport {
  insert_offset: usize,
  new_text: String,
}

fn parse_simple_named_import(text: &str) -> Option<SimpleNamedImport<'_>> {
  let captures = SIMPLE_NAMED_IMPORT_RE.captures(text)?;
  let names = captures
    .name("names")?
    .as_str()
    .split(',')
    .map(str::trim)
    .filter(|name| !name.is_empty())
    .collect::<Vec<_>>();
  if names.is_empty() {
    return None;
  }
  Some(SimpleNamedImport {
    is_type_only: captures.name("type").is_some(),
    specifier: captures.name("specifier")?.as_str(),
    names,
  })
}

fn find_mergeable_named_import(
  text: &str,
  new_import: &SimpleNamedImport,
) -> Option<ExistingNamedImport> {
  for quote in ['"', '\''] {
    let needle = format!("from {}{}{}", quote, new_import.specifier, quote);
    let mut search_start = 0;
    while let Some(relative_index) = text[search_start..].find(&needle) {
      let from_index = search_start + relative_index;
      search_start = from_index + needle.len();
      let Some(import_start) = find_import_start(text, from_index) else {
        continue;
      };
      let statement_before_from = &text[import_start..from_index];
      if statement_before_from.contains(';') {
        continue;
      }
      let Some(import_clause) =
        statement_before_from.trim_start().strip_prefix("import ")
      else {
        continue;
      };
      let is_type_only = import_clause.trim_start().starts_with("type ");
      if is_type_only && !new_import.is_type_only {
        continue;
      }
      let Some(close_brace) = statement_before_from.rfind('}') else {
        continue;
      };
      let close_brace = import_start + close_brace;
      let Some(open_brace) = text[import_start..close_brace].rfind('{') else {
        continue;
      };
      let open_brace = import_start + open_brace;
      let existing_names = &text[open_brace + 1..close_brace];
      if new_import
        .names
        .iter()
        .any(|name| import_list_contains_name(existing_names, name))
      {
        continue;
      }
      let insert_offset =
        import_list_insert_offset(text, open_brace, close_brace);
      let names = new_import
        .names
        .iter()
        .map(|name| {
          if new_import.is_type_only && !is_type_only {
            format!("type {name}")
          } else {
            (*name).to_string()
          }
        })
        .collect::<Vec<_>>();
      return Some(ExistingNamedImport {
        insert_offset,
        new_text: format_import_list_insertion(
          text,
          open_brace,
          close_brace,
          &names,
        ),
      });
    }
  }
  None
}

fn find_import_start(text: &str, from_index: usize) -> Option<usize> {
  let mut line_start = text[..from_index].rfind('\n').map_or(0, |i| i + 1);
  loop {
    let line = &text[line_start..from_index];
    if line.trim_start().starts_with("import ") {
      return Some(line_start);
    }
    if line_start == 0 {
      return None;
    }
    line_start = text[..line_start - 1].rfind('\n').map_or(0, |i| i + 1);
  }
}

fn import_list_contains_name(existing_names: &str, name: &str) -> bool {
  let name = name.trim_start_matches("type ").trim();
  existing_names.split(',').any(|existing_name| {
    let existing_name = existing_name.trim().trim_start_matches("type ").trim();
    existing_name
      .split_once(" as ")
      .map(|(_, alias)| alias.trim())
      .unwrap_or(existing_name)
      == name
  })
}

fn format_import_list_insertion(
  text: &str,
  open_brace: usize,
  close_brace: usize,
  names: &[String],
) -> String {
  let existing_names = &text[open_brace + 1..close_brace];
  if existing_names.contains('\n') {
    let close_line_start = text[..close_brace].rfind('\n').map_or(0, |i| i + 1);
    let indent = text[close_line_start..close_brace]
      .chars()
      .take_while(|c| c.is_whitespace())
      .collect::<String>();
    names
      .iter()
      .map(|name| format!("{indent}{name},\n"))
      .collect()
  } else if existing_names.trim().is_empty() {
    names.join(", ")
  } else {
    format!(", {}", names.join(", "))
  }
}

fn import_list_insert_offset(
  text: &str,
  open_brace: usize,
  close_brace: usize,
) -> usize {
  let existing_names = &text[open_brace + 1..close_brace];
  if existing_names.contains('\n') {
    close_brace
  } else {
    close_brace
      - existing_names
        .chars()
        .rev()
        .take_while(|c| c.is_whitespace())
        .map(char::len_utf8)
        .sum::<usize>()
  }
}

fn byte_offset_to_position(text: &str, offset: usize) -> lsp::Position {
  let mut line = 0;
  let mut character = 0;
  for ch in text[..offset].chars() {
    if ch == '\n' {
      line += 1;
      character = 0;
    } else {
      character += ch.len_utf16() as u32;
    }
  }
  lsp::Position { line, character }
}

fn rewrite_first_quoted_specifier(text: &mut String, new_specifier: &str) {
  let Some((quote_start, quote)) = text
    .char_indices()
    .find_map(|(i, c)| (c == '\'' || c == '"').then_some((i, c)))
  else {
    return;
  };
  let specifier_start = quote_start + quote.len_utf8();
  let Some(quote_end) = text[specifier_start..]
    .char_indices()
    .find_map(|(i, c)| (c == quote).then_some(specifier_start + i))
  else {
    return;
  };
  text.replace_range(specifier_start..quote_end, new_specifier);
}

// Based on https://github.com/microsoft/vscode/blob/1.81.1/extensions/typescript-language-features/src/languageFeatures/util/snippetForFunctionCall.ts#L49.
fn get_parameters_from_parts(parts: &[SymbolDisplayPart]) -> Vec<String> {
  let mut parameters = Vec::with_capacity(3);
  let mut is_in_fn = false;
  let mut paren_count = 0;
  let mut brace_count = 0;
  for (idx, part) in parts.iter().enumerate() {
    if ["methodName", "functionName", "text", "propertyName"]
      .contains(&part.kind.as_str())
    {
      if paren_count == 0 && brace_count == 0 {
        is_in_fn = true;
      }
    } else if part.kind == "parameterName" {
      if paren_count == 1 && brace_count == 0 && is_in_fn {
        let is_optional =
          matches!(parts.get(idx + 1), Some(next) if next.text == "?");
        // Skip `this` and optional parameters.
        if !is_optional && part.text != "this" {
          parameters.push(format!(
            "${{{}:{}}}",
            parameters.len() + 1,
            &part.text
          ));
        }
      }
    } else if part.kind == "punctuation" {
      if part.text == "(" {
        paren_count += 1;
      } else if part.text == ")" {
        paren_count -= 1;
        if paren_count <= 0 && is_in_fn {
          break;
        }
      } else if part.text == "..." && paren_count == 1 {
        // Found rest parameter. Do not fill in any further arguments.
        break;
      } else if part.text == "{" {
        brace_count += 1;
      } else if part.text == "}" {
        brace_count -= 1;
      }
    }
  }
  parameters
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntryDetails {
  display_parts: Vec<SymbolDisplayPart>,
  documentation: Option<Vec<SymbolDisplayPart>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  tags: Option<Vec<JsDocTagInfo>>,
  name: String,
  kind: ScriptElementKind,
  kind_modifiers: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  code_actions: Option<Vec<CodeAction>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  source_display: Option<Vec<SymbolDisplayPart>>,
}

impl CompletionEntryDetails {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
  ) -> Result<(), AnyError> {
    for action in self.code_actions.iter_mut().flatten() {
      action.normalize(specifier_map)?;
    }
    Ok(())
  }

  pub fn as_completion_item(
    &self,
    original_item: &lsp::CompletionItem,
    data: &TsJsCompletionItemData,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> Result<lsp::CompletionItem, AnyError> {
    let detail = if original_item.detail.is_some() {
      original_item.detail.clone()
    } else if !self.display_parts.is_empty() {
      Some(replace_links(display_parts_to_string(
        &self.display_parts,
        module,
        snapshot,
      )))
    } else {
      None
    };
    let documentation = if let Some(parts) = &self.documentation {
      // NOTE: similar as `QuickInfo::to_hover()`
      let mut value = display_parts_to_string(parts, module, snapshot);
      if let Some(tags) = &self.tags {
        let tags_preview = tags
          .iter()
          .map(|tag_info| get_tag_documentation(tag_info, module, snapshot))
          .collect::<Vec<String>>()
          .join("\n\n");
        if !tags_preview.is_empty() {
          value = format!("{value}\n\n{tags_preview}");
        }
      }
      if value.is_empty() {
        None
      } else {
        Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value,
        }))
      }
    } else {
      None
    };
    let mut text_edit = original_item.text_edit.clone();
    let mut code_action_descriptions = self
      .code_actions
      .iter()
      .flatten()
      .map(|a| Cow::Borrowed(a.description.as_str()))
      .collect::<Vec<_>>();
    if let Some(specifier_rewrite) = &data.specifier_rewrite {
      for description in &mut code_action_descriptions {
        rewrite_first_quoted_specifier(
          description.to_mut(),
          &specifier_rewrite.new_specifier,
        );
      }
      if let Some(text_edit) = &mut text_edit {
        let new_text = match text_edit {
          lsp::CompletionTextEdit::Edit(text_edit) => &mut text_edit.new_text,
          lsp::CompletionTextEdit::InsertAndReplace(insert_replace_edit) => {
            &mut insert_replace_edit.new_text
          }
        };
        rewrite_first_quoted_specifier(
          new_text,
          &specifier_rewrite.new_specifier,
        );
        if let Some(deno_types_specifier) =
          &specifier_rewrite.new_deno_types_specifier
        {
          *new_text =
            format!("// @ts-types=\"{}\"\n{}", deno_types_specifier, new_text);
        }
      }
    }
    let code_action_description =
      Some(code_action_descriptions.join("\n\n")).filter(|s| !s.is_empty());
    let detail = Some(
      [code_action_description, detail]
        .into_iter()
        .flatten()
        .collect::<Vec<_>>()
        .join("\n\n"),
    )
    .filter(|s| !s.is_empty());
    let (command, additional_text_edits) =
      parse_code_actions(self.code_actions.as_ref(), data, module)?;
    let mut insert_text_format = original_item.insert_text_format;
    let mut insert_text = if data.use_code_snippet {
      insert_text_format = Some(lsp::InsertTextFormat::SNIPPET);
      Some(format!(
        "{}({})",
        original_item
          .insert_text
          .as_ref()
          .unwrap_or(&original_item.label),
        get_parameters_from_parts(&self.display_parts).join(", "),
      ))
    } else {
      original_item.insert_text.clone()
    };
    let mut filter_text = original_item.filter_text.clone();
    if let Some(specifier_rewrite) = &data.specifier_rewrite {
      if let Some(insert_text) = &mut insert_text {
        rewrite_first_quoted_specifier(
          insert_text,
          &specifier_rewrite.new_specifier,
        );
      }
      if let Some(filter_text) = &mut filter_text {
        rewrite_first_quoted_specifier(
          filter_text,
          &specifier_rewrite.new_specifier,
        );
      }
    }

    Ok(lsp::CompletionItem {
      data: None,
      detail,
      documentation,
      command,
      text_edit,
      additional_text_edits,
      insert_text,
      insert_text_format,
      filter_text,
      // NOTE(bartlomieju): it's not entirely clear to me why we need to do that,
      // but when `completionItem/resolve` is called, we get a list of commit chars
      // even though we might have returned an empty list in `completion` request.
      commit_characters: None,
      ..original_item.clone()
    })
  }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionInfo {
  entries: Vec<CompletionEntry>,
  // this is only used by Microsoft's telemetrics, which Deno doesn't use and
  // there are issues with the value not matching the type definitions.
  // flags: Option<CompletionInfoFlags>,
  is_global_completion: bool,
  is_member_completion: bool,
  is_new_identifier_location: bool,
  metadata: Option<Value>,
  optional_replacement_span: Option<TextSpan>,
}

impl CompletionInfo {
  fn normalize(
    &mut self,
    specifier_map: &TscSpecifierMap,
    token: &CancellationToken,
  ) -> Result<(), AnyError> {
    for entry in &mut self.entries {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      entry.normalize(specifier_map);
    }
    Ok(())
  }

  #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all, fields(entries = %self.entries.len())))]
  pub fn as_completion_response(
    &self,
    line_index: &LineIndex,
    settings: &config::CompletionSettings,
    module: &DocumentModule,
    position: u32,
    language_server: &language_server::Inner,
    token: &CancellationToken,
  ) -> Result<lsp::CompletionResponse, AnyError> {
    // A cache for costly resolution computations.
    // On a test project, it was found to speed up completion requests
    // by 10-20x and contained ~300 entries for 8000 completion items.
    let mut cache = HashMap::with_capacity(512);
    let mut items = Vec::with_capacity(self.entries.len());
    for entry in &self.entries {
      if token.is_cancelled() {
        return Err(anyhow!("request cancelled"));
      }
      if let Some(item) = entry.as_completion_item(
        line_index,
        self,
        settings,
        module,
        position,
        language_server,
        &mut cache,
      ) {
        items.push(item);
      }
    }
    let is_incomplete = self
      .metadata
      .clone()
      .map(|v| {
        v.as_object()
          .unwrap()
          .get("isIncomplete")
          .unwrap_or(&json!(false))
          .as_bool()
          .unwrap()
      })
      .unwrap_or(false);
    Ok(lsp::CompletionResponse::List(lsp::CompletionList {
      is_incomplete,
      items,
    }))
  }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CompletionSpecifierRewrite {
  new_specifier: String,
  new_deno_types_specifier: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TsJsCompletionItemData {
  pub uri: Uri,
  pub position: u32,
  pub name: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub source: Option<String>,
  /// If present, the code action / text edit corresponding to this item should
  /// be rewritten by replacing the first string with the second. Intended for
  /// auto-import specifiers to be reverse-import-mapped.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub specifier_rewrite: Option<CompletionSpecifierRewrite>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub data: Option<Value>,
  pub use_code_snippet: bool,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CompletionEntryDataAutoImport {
  module_specifier: String,
  file_name: Option<String>,
}

#[derive(Debug)]
pub struct CompletionNormalizedAutoImportData {
  raw: CompletionEntryDataAutoImport,
  normalized: ModuleSpecifier,
}

#[derive(Debug, Clone, Eq, PartialEq)]
enum ResolutionLookup {
  PrettySpecifier(String),
  Preserve,
  Invalid,
}

#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntry {
  name: String,
  kind: ScriptElementKind,
  #[serde(skip_serializing_if = "Option::is_none")]
  kind_modifiers: Option<String>,
  sort_text: String,
  #[serde(skip_serializing_if = "Option::is_none")]
  insert_text: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  is_snippet: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  replacement_span: Option<TextSpan>,
  #[serde(skip_serializing_if = "Option::is_none")]
  has_action: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  source: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  source_display: Option<Vec<SymbolDisplayPart>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  label_details: Option<CompletionEntryLabelDetails>,
  #[serde(skip_serializing_if = "Option::is_none")]
  is_recommended: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  is_from_unchecked_file: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  is_package_json_import: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  is_import_statement_completion: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  data: Option<Value>,
  #[serde(flatten)]
  other: serde_json::Map<String, Value>,
  /// This is not from tsc, we add it for convenience during normalization.
  /// Represents `self.data.file_name`, but normalized.
  #[serde(skip)]
  auto_import_data: Option<CompletionNormalizedAutoImportData>,
}

impl CompletionEntry {
  fn normalize(&mut self, specifier_map: &TscSpecifierMap) {
    let Some(data) = &self.data else {
      return;
    };
    let Ok(raw) =
      serde_json::from_value::<CompletionEntryDataAutoImport>(data.clone())
    else {
      return;
    };
    if let Some(file_name) = &raw.file_name {
      if let Ok(normalized) = specifier_map.normalize(file_name) {
        self.auto_import_data =
          Some(CompletionNormalizedAutoImportData { raw, normalized });
      }
    } else if SUPPORTED_BUILTIN_NODE_MODULES
      .contains(&raw.module_specifier.as_str())
      && let Ok(normalized) =
        resolve_url(&format!("node:{}", &raw.module_specifier))
    {
      self.auto_import_data =
        Some(CompletionNormalizedAutoImportData { raw, normalized });
    }
  }

  fn get_commit_characters(
    &self,
    info: &CompletionInfo,
    settings: &config::CompletionSettings,
  ) -> Option<Vec<String>> {
    if info.is_new_identifier_location {
      return None;
    }

    let mut commit_characters = vec![];
    match self.kind {
      ScriptElementKind::MemberGetAccessorElement
      | ScriptElementKind::MemberSetAccessorElement
      | ScriptElementKind::ConstructSignatureElement
      | ScriptElementKind::CallSignatureElement
      | ScriptElementKind::IndexSignatureElement
      | ScriptElementKind::EnumElement
      | ScriptElementKind::InterfaceElement => {
        commit_characters.push(".");
        commit_characters.push(";");
      }
      ScriptElementKind::ModuleElement
      | ScriptElementKind::Alias
      | ScriptElementKind::ConstElement
      | ScriptElementKind::LetElement
      | ScriptElementKind::VariableElement
      | ScriptElementKind::LocalVariableElement
      | ScriptElementKind::MemberVariableElement
      | ScriptElementKind::ClassElement
      | ScriptElementKind::FunctionElement
      | ScriptElementKind::MemberFunctionElement
      | ScriptElementKind::Keyword
      | ScriptElementKind::ParameterElement => {
        commit_characters.push(".");
        commit_characters.push(",");
        commit_characters.push(";");
        if !settings.complete_function_calls {
          commit_characters.push("(");
        }
      }
      _ => (),
    }

    if commit_characters.is_empty() {
      None
    } else {
      Some(commit_characters.into_iter().map(String::from).collect())
    }
  }

  // https://github.com/microsoft/vscode/blob/52eae268f764fd41d69705eb629010f4c0e28ae9/extensions/typescript-language-features/src/languageFeatures/completions.ts#L391-L425
  fn get_filter_text(
    &self,
    context: Option<(&DocumentModule, u32)>,
  ) -> Option<String> {
    if self.name.starts_with('#') {
      if let Some(insert_text) = &self.insert_text {
        if insert_text.starts_with("this.#") {
          let prefix_starts_with_hash = context
            .map(|(module, position)| {
              for (_, c) in module
                .text
                .char_indices()
                .rev()
                .skip_while(|(i, _)| *i as u32 >= position)
              {
                if c == '#' {
                  return true;
                }
                if !c.is_ascii_alphanumeric() && c != '_' && c != '$' {
                  break;
                }
              }
              false
            })
            .unwrap_or(false);
          if prefix_starts_with_hash {
            return Some(insert_text.clone());
          } else {
            return Some(insert_text.replace("this.#", ""));
          }
        } else {
          return Some(insert_text.clone());
        }
      } else {
        return None;
      }
    }

    if let Some(insert_text) = &self.insert_text {
      if insert_text.starts_with("this.") {
        return None;
      }
      if insert_text.starts_with('[') {
        return Some(
          BRACKET_ACCESSOR_RE
            .replace(insert_text, |caps: &Captures| format!(".{}", &caps[1]))
            .to_string(),
        );
      }
    }

    self.insert_text.clone()
  }

  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  fn as_completion_item(
    &self,
    line_index: &LineIndex,
    info: &CompletionInfo,
    settings: &config::CompletionSettings,
    module: &DocumentModule,
    position: u32,
    language_server: &language_server::Inner,
    resolution_lookup_cache: &mut HashMap<
      (ModuleSpecifier, Arc<ModuleSpecifier>),
      ResolutionLookup,
    >,
  ) -> Option<lsp::CompletionItem> {
    let mut label = self.name.clone();
    let mut label_details: Option<lsp::CompletionItemLabelDetails> = None;
    let mut kind: Option<lsp::CompletionItemKind> =
      Some(self.kind.clone().into());
    let mut specifier_rewrite = None;
    let mut sort_text = self.sort_text.clone();

    let preselect = self.is_recommended;
    let use_code_snippet = settings.complete_function_calls
      && (kind == Some(lsp::CompletionItemKind::FUNCTION)
        || kind == Some(lsp::CompletionItemKind::METHOD));
    let commit_characters = self.get_commit_characters(info, settings);
    let mut insert_text = self.insert_text.clone();
    let insert_text_format = match self.is_snippet {
      Some(true) => Some(lsp::InsertTextFormat::SNIPPET),
      _ => None,
    };
    let range = self.replacement_span.clone();
    let mut filter_text = self.get_filter_text(Some((module, position)));
    let mut tags = None;
    let mut detail = None;

    if let Some(kind_modifiers) = &self.kind_modifiers {
      let kind_modifiers = parse_kind_modifier(kind_modifiers);
      if kind_modifiers.contains("optional") {
        if insert_text.is_none() {
          insert_text = Some(label.clone());
        }
        if filter_text.is_none() {
          filter_text = Some(label.clone());
        }
        label += "?";
      }
      if kind_modifiers.contains("deprecated") {
        tags = Some(vec![lsp::CompletionItemTag::DEPRECATED]);
      }
      if kind_modifiers.contains("color") {
        kind = Some(lsp::CompletionItemKind::COLOR);
      }
      if self.kind == ScriptElementKind::ScriptElement {
        for ext_modifier in FILE_EXTENSION_KIND_MODIFIERS {
          if kind_modifiers.contains(ext_modifier) {
            detail = if self.name.to_lowercase().ends_with(ext_modifier) {
              Some(self.name.clone())
            } else {
              Some(format!("{}{}", self.name, ext_modifier))
            };
            break;
          }
        }
      }
    }
    if let Some(source) = &self.source
      && let Some(import_data) = &self.auto_import_data
    {
      sort_text = format!("\u{ffff}{}", self.sort_text);
      let mut display_source = source.clone();
      let import_mapper = language_server.get_ts_response_import_mapper(module);
      let resolution_lookup = resolution_lookup_cache
        .entry((import_data.normalized.clone(), module.specifier.clone()))
        .or_insert_with(|| {
          if let Some(specifier) = import_mapper
            .check_specifier(&import_data.normalized, &module.specifier)
          {
            return ResolutionLookup::PrettySpecifier(specifier);
          }
          if language_server
            .resolver
            .in_node_modules(&import_data.normalized)
            || language_server
              .cache
              .in_cache_directory(&import_data.normalized)
            || import_data
              .normalized
              .as_str()
              .starts_with(jsr_url().as_str())
          {
            return ResolutionLookup::Invalid;
          }
          if let Some(specifier) =
            relative_specifier(&module.specifier, &import_data.normalized)
          {
            return ResolutionLookup::PrettySpecifier(specifier);
          }
          if Url::parse(&import_data.raw.module_specifier).is_ok() {
            return ResolutionLookup::PrettySpecifier(
              import_data.normalized.to_string(),
            );
          }
          ResolutionLookup::Preserve
        });
      if let ResolutionLookup::Invalid = resolution_lookup {
        return None;
      }
      if let ResolutionLookup::PrettySpecifier(new_specifier) =
        resolution_lookup
      {
        let mut new_specifier = new_specifier.clone();
        let mut new_deno_types_specifier = None;
        if let Some(code_specifier) = language_server
          .resolver
          .get_scoped_resolver(module.scope.as_deref())
          .deno_types_to_code_resolution(&import_data.normalized)
          .and_then(|s| {
            import_mapper
              .check_specifier(&s, &module.specifier)
              .or_else(|| relative_specifier(&module.specifier, &s))
          })
        {
          new_deno_types_specifier =
            Some(std::mem::replace(&mut new_specifier, code_specifier));
        }
        display_source.clone_from(&new_specifier);
        if new_specifier != import_data.raw.module_specifier
          || new_deno_types_specifier.is_some()
        {
          specifier_rewrite = Some(CompletionSpecifierRewrite {
            new_specifier,
            new_deno_types_specifier,
          });
        }
      }
      if let Some(specifier_rewrite) = &specifier_rewrite {
        if let Some(insert_text) = &mut insert_text {
          rewrite_first_quoted_specifier(
            insert_text,
            &specifier_rewrite.new_specifier,
          );
        }
        if let Some(filter_text) = &mut filter_text {
          rewrite_first_quoted_specifier(
            filter_text,
            &specifier_rewrite.new_specifier,
          );
        }
      }
      // We want relative or bare (import-mapped or otherwise) specifiers to
      // appear at the top.
      if resolve_url(&display_source).is_err() {
        sort_text += "_0";
      } else {
        sort_text += "_1";
      }
      label_details
        .get_or_insert_with(Default::default)
        .description = Some(display_source);
    }

    let text_edit = if let Some(text_span) = &range {
      let range = text_span.to_range(line_index);
      // TSC may provide a `replacementSpan` without a corresponding
      // `insertText` (e.g. for string union literal members). In that case
      // fall back to the entry name as the text to insert, matching the
      // behavior of VSCode's TypeScript integration. Without this, the editor
      // is left to apply its own word-based replacement, which breaks on
      // string values containing characters like `.` (the part before the
      // last dot is forgotten).
      let new_text = insert_text.clone().unwrap_or_else(|| self.name.clone());
      let insert_replace_edit = lsp::InsertReplaceEdit {
        new_text,
        insert: range,
        replace: range,
      };
      Some(insert_replace_edit.into())
    } else {
      None
    };

    let data = TsJsCompletionItemData {
      uri: module.uri.as_ref().clone(),
      position,
      name: self.name.clone(),
      source: self.source.clone(),
      specifier_rewrite,
      data: self.data.clone(),
      use_code_snippet,
    };

    Some(lsp::CompletionItem {
      label,
      label_details,
      kind,
      sort_text: Some(sort_text),
      preselect,
      text_edit,
      filter_text,
      insert_text,
      insert_text_format,
      detail,
      tags,
      commit_characters,
      data: Some(json!(CompletionItemData::TsJs(data))),
      ..Default::default()
    })
  }
}

#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CompletionEntryLabelDetails {
  #[serde(skip_serializing_if = "Option::is_none")]
  detail: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  description: Option<String>,
}

#[derive(Debug, Deserialize)]
pub enum OutliningSpanKind {
  #[serde(rename = "comment")]
  Comment,
  #[serde(rename = "region")]
  Region,
  #[serde(rename = "code")]
  Code,
  #[serde(rename = "imports")]
  Imports,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutliningSpan {
  text_span: TextSpan,
  // hint_span: TextSpan,
  // banner_text: String,
  // auto_collapse: bool,
  kind: OutliningSpanKind,
}

const FOLD_END_PAIR_CHARACTERS: &[u8] = b"}])`";

impl OutliningSpan {
  pub fn to_folding_range(
    &self,
    line_index: &LineIndex,
    content: &[u8],
    line_folding_only: bool,
  ) -> lsp::FoldingRange {
    let range = self.text_span.to_range(line_index);
    lsp::FoldingRange {
      start_line: range.start.line,
      start_character: if line_folding_only {
        None
      } else {
        Some(range.start.character)
      },
      end_line: self.adjust_folding_end_line(
        &range,
        line_index,
        content,
        line_folding_only,
      ),
      end_character: if line_folding_only {
        None
      } else {
        Some(range.end.character)
      },
      kind: self.get_folding_range_kind(&self.kind),
      collapsed_text: None,
    }
  }

  fn adjust_folding_end_line(
    &self,
    range: &lsp::Range,
    line_index: &LineIndex,
    content: &[u8],
    line_folding_only: bool,
  ) -> u32 {
    if line_folding_only && range.end.line > 0 && range.end.character > 0 {
      let offset_end: usize = line_index.offset(range.end).unwrap().into();
      let fold_end_char = content[offset_end - 1];
      if FOLD_END_PAIR_CHARACTERS.contains(&fold_end_char) {
        return cmp::max(range.end.line - 1, range.start.line);
      }
    }

    range.end.line
  }

  fn get_folding_range_kind(
    &self,
    span_kind: &OutliningSpanKind,
  ) -> Option<lsp::FoldingRangeKind> {
    match span_kind {
      OutliningSpanKind::Comment => Some(lsp::FoldingRangeKind::Comment),
      OutliningSpanKind::Region => Some(lsp::FoldingRangeKind::Region),
      OutliningSpanKind::Imports => Some(lsp::FoldingRangeKind::Imports),
      _ => None,
    }
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItems {
  items: Vec<SignatureHelpItem>,
  // applicable_span: TextSpan,
  selected_item_index: u32,
  argument_index: u32,
  // argument_count: u32,
}

impl SignatureHelpItems {
  pub fn into_signature_help(
    self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
    token: &CancellationToken,
  ) -> Result<lsp::SignatureHelp, AnyError> {
    let mut signatures = self
      .items
      .into_iter()
      .map(|item| {
        if token.is_cancelled() {
          return Err(anyhow!("request cancelled"));
        }
        Ok(item.into_signature_information(module, snapshot))
      })
      .collect::<Result<Vec<_>, _>>()?;
    if let Some(active_signature) =
      signatures.get_mut(self.selected_item_index as usize)
    {
      active_signature.active_parameter = Some(self.argument_index);
      Ok(lsp::SignatureHelp {
        signatures,
        active_parameter: None,
        active_signature: Some(self.selected_item_index),
      })
    } else {
      Ok(lsp::SignatureHelp {
        signatures,
        active_parameter: Some(self.argument_index),
        active_signature: Some(self.selected_item_index),
      })
    }
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItem {
  // is_variadic: bool,
  prefix_display_parts: Vec<SymbolDisplayPart>,
  suffix_display_parts: Vec<SymbolDisplayPart>,
  // separator_display_parts: Vec<SymbolDisplayPart>,
  parameters: Vec<SignatureHelpParameter>,
  documentation: Vec<SymbolDisplayPart>,
  // tags: Vec<JsDocTagInfo>,
}

impl SignatureHelpItem {
  pub fn into_signature_information(
    self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> lsp::SignatureInformation {
    let prefix_text =
      display_parts_to_string(&self.prefix_display_parts, module, snapshot);
    let params_text = self
      .parameters
      .iter()
      .map(|param| {
        display_parts_to_string(&param.display_parts, module, snapshot)
      })
      .collect::<Vec<String>>()
      .join(", ");
    let suffix_text =
      display_parts_to_string(&self.suffix_display_parts, module, snapshot);
    let documentation =
      display_parts_to_string(&self.documentation, module, snapshot);
    lsp::SignatureInformation {
      label: format!("{prefix_text}{params_text}{suffix_text}"),
      documentation: Some(lsp::Documentation::MarkupContent(
        lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value: documentation,
        },
      )),
      parameters: Some(
        self
          .parameters
          .into_iter()
          .map(|param| param.into_parameter_information(module, snapshot))
          .collect(),
      ),
      active_parameter: None,
    }
  }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpParameter {
  // name: String,
  documentation: Vec<SymbolDisplayPart>,
  display_parts: Vec<SymbolDisplayPart>,
  // is_optional: bool,
}

impl SignatureHelpParameter {
  pub fn into_parameter_information(
    self,
    module: &DocumentModule,
    snapshot: &StateSnapshot,
  ) -> lsp::ParameterInformation {
    let documentation = format!(
      "{}\n",
      display_parts_to_string(&self.documentation, module, snapshot)
    );
    lsp::ParameterInformation {
      label: lsp::ParameterLabel::Simple(display_parts_to_string(
        &self.display_parts,
        module,
        snapshot,
      )),
      documentation: Some(lsp::Documentation::MarkupContent(
        lsp::MarkupContent {
          kind: lsp::MarkupKind::Markdown,
          value: documentation,
        },
      )),
    }
  }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SelectionRange {
  text_span: TextSpan,
  #[serde(skip_serializing_if = "Option::is_none")]
  parent: Option<Box<SelectionRange>>,
}

impl SelectionRange {
  pub fn to_selection_range(
    &self,
    line_index: &LineIndex,
  ) -> lsp::SelectionRange {
    lsp::SelectionRange {
      range: self.text_span.to_range(line_index),
      parent: self.parent.as_ref().map(|parent_selection| {
        Box::new(parent_selection.to_selection_range(line_index))
      }),
    }
  }
}

#[derive(Debug, Default)]
pub struct TscSpecifierMap {
  normalized_specifiers: DashMap<String, ModuleSpecifier>,
  denormalized_specifiers: DashMap<(ModuleSpecifier, bool), String>,
}

impl TscSpecifierMap {
  pub fn new() -> Self {
    Self::default()
  }

  /// Convert the specifier to one compatible with tsc. Cache the resulting
  /// mapping in case it needs to be reversed.
  // TODO(nayeemrmn): Factor in out-of-band media type here.
  pub fn denormalize(
    &self,
    specifier: &ModuleSpecifier,
    media_type: MediaType,
  ) -> String {
    self.denormalize_inner(specifier, media_type, false)
  }

  /// Denormalizes a specifier while hiding `node_modules` from TypeScript.
  ///
  /// TypeScript suppresses auto-import candidates from `node_modules/.deno`,
  /// so direct package entry points that should be auto-importable are exposed
  /// under a synthetic `$node_modules` path. Avoid using this for every
  /// node_modules file, as that makes TypeScript treat the whole dependency
  /// tree like user code.
  pub fn denormalize_with_node_modules_alias(
    &self,
    specifier: &ModuleSpecifier,
    media_type: MediaType,
  ) -> String {
    self.denormalize_inner(specifier, media_type, true)
  }

  fn denormalize_inner(
    &self,
    specifier: &ModuleSpecifier,
    media_type: MediaType,
    alias_node_modules: bool,
  ) -> String {
    let original = specifier;
    if let Some(specifier) = self
      .denormalized_specifiers
      .get(&(original.clone(), alias_node_modules))
    {
      return specifier.to_string();
    }
    let mut specifier = original.to_string();
    if alias_node_modules && !specifier.contains("/node_modules/@types/node/") {
      // The ts server doesn't give completions from files in
      // `node_modules/.deno/`. We work around it like this.
      specifier = specifier.replace("/node_modules/", "/$node_modules/");
    }
    // If the module's media type doesn't correspond to tsc's path-inferred
    // media type, force it to be the same by appending an extension.
    if MediaType::from_path(Path::new(&specifier)) != media_type {
      specifier += media_type.as_ts_extension();
    }
    if specifier != original.as_str() {
      self
        .normalized_specifiers
        .insert(specifier.clone(), original.clone());
    }
    specifier
  }

  /// Convert the specifier from one compatible with tsc. Cache the resulting
  /// mapping in case it needs to be reversed.
  pub fn normalize<S: AsRef<str>>(
    &self,
    specifier: S,
  ) -> Result<ModuleSpecifier, deno_core::url::ParseError> {
    let original = specifier.as_ref();
    if let Some(specifier) = self.normalized_specifiers.get(original) {
      return Ok(specifier.clone());
    }
    let specifier_str = original
      .replace(".d.ts.d.ts", ".d.ts")
      .replace("$node_modules", "node_modules");
    let specifier = ModuleSpecifier::parse(&specifier_str)?;
    if specifier.as_str() != original {
      self.denormalized_specifiers.insert(
        (specifier.clone(), original.contains("$node_modules")),
        original.to_string(),
      );
    }
    Ok(specifier)
  }
}

// TODO(bartlomieju): we have similar struct in `cli/tsc/mod.rs` - maybe at least change
// the name of the struct to avoid confusion?
struct State {
  last_id: usize,
  performance: Arc<Performance>,
  // the response from JS, as a JSON string
  response_tx: Option<oneshot::Sender<Result<String, AnyError>>>,
  state_snapshot: Arc<StateSnapshot>,
  specifier_map: Arc<TscSpecifierMap>,
  last_compiler_options_key: CompilerOptionsKey,
  last_scope: Option<Arc<Url>>,
  last_notebook_uri: Option<Arc<Uri>>,
  token: CancellationToken,
  // Shared with the isolate's near-heap-limit callback so that a runaway
  // request can be cancelled when the TSC isolate is about to run out of
  // memory, instead of crashing the whole language server. See
  // `run_tsc_thread`.
  request_cancellation: Arc<Mutex<CancellationToken>>,
  pending_requests: Option<UnboundedReceiver<Request>>,
  mark: Option<PerformanceMark>,
  context: Option<super::trace::Context>,
  enable_tracing: Arc<AtomicBool>,
  // Whether a real request has been serviced since the last idle memory
  // release. Used by `op_poll_requests` to arm the idle timer only when there's
  // actually something to release, so a quiescent language server doesn't keep
  // waking up to run GCs forever.
  serviced_since_idle_release: bool,
}

impl State {
  fn new(
    state_snapshot: Arc<StateSnapshot>,
    specifier_map: Arc<TscSpecifierMap>,
    performance: Arc<Performance>,
    pending_requests: UnboundedReceiver<Request>,
    enable_tracing: Arc<AtomicBool>,
    request_cancellation: Arc<Mutex<CancellationToken>>,
  ) -> Self {
    Self {
      last_id: 1,
      performance,
      response_tx: None,
      state_snapshot,
      specifier_map,
      last_compiler_options_key: Default::default(),
      last_scope: None,
      last_notebook_uri: None,
      token: Default::default(),
      request_cancellation,
      mark: None,
      pending_requests: Some(pending_requests),
      context: None,
      enable_tracing,
      serviced_since_idle_release: false,
    }
  }

  fn tracing_enabled(&self) -> bool {
    self
      .enable_tracing
      .load(std::sync::atomic::Ordering::Relaxed)
  }

  fn get_module(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<Arc<DocumentModule>> {
    self.state_snapshot.document_modules.module_for_specifier(
      specifier,
      self.last_scope.as_deref(),
      Some(&self.last_compiler_options_key),
    )
  }

  fn script_version(&self, specifier: &ModuleSpecifier) -> Option<String> {
    self.get_module(specifier).map(|m| m.script_version.clone())
  }
}

#[op2(fast)]
fn op_is_cancelled(state: &mut OpState) -> bool {
  let state = state.borrow_mut::<State>();
  state.token.is_cancelled()
}

#[op2(fast)]
fn op_is_node_file(state: &mut OpState, #[string] path: String) -> bool {
  let state = state.borrow::<State>();
  let mark = state.performance.mark("tsc.op.op_is_node_file");
  let r = match state.specifier_map.normalize(path) {
    Ok(specifier) => {
      state.state_snapshot.resolver.in_node_modules(&specifier)
        || specifier.as_str().starts_with("asset:///node/")
    }
    Err(_) => false,
  };
  state.performance.measure(mark);
  r
}

#[op2]
fn op_libs() -> Vec<String> {
  crate::tsc::lib_names()
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
enum LoadError {
  #[error("{0}")]
  #[class(inherit)]
  UrlParse(#[from] deno_core::url::ParseError),
  #[error("{0}")]
  #[class(inherit)]
  JsErrorBox(#[from] deno_error::JsErrorBox),
}

#[derive(Debug, deno_core::ToV8)]
struct LoadResponse {
  data: DocumentText,
  script_kind: i32,
  version: Option<String>,
  is_cjs: bool,
  is_classic_script: bool,
}

#[op2]
fn op_load<'s>(
  scope: &'s mut v8::PinScope<'_, '_>,
  state: &mut OpState,
  #[string] specifier: &str,
) -> Result<v8::Local<'s, v8::Value>, LoadError> {
  let _span = super::logging::lsp_tracing_info_span!("op_load").entered();
  let state = state.borrow_mut::<State>();
  let mark = state
    .performance
    .mark_with_args("tsc.op.op_load", specifier);
  let specifier = state.specifier_map.normalize(specifier)?;
  let maybe_load_response =
    if let Some(source) = crate::tsc::load_raw_import_source(&specifier) {
      Some(LoadResponse {
        data: DocumentText::Static(source),
        script_kind: crate::tsc::as_ts_script_kind(MediaType::TypeScript),
        version: Some("1".to_string()),
        is_cjs: false,
        is_classic_script: false,
      })
    } else {
      let module = if specifier.as_str() == MISSING_DEPENDENCY_SPECIFIER {
        None
      } else {
        state.get_module(&specifier)
      };
      module.as_ref().map(|m| {
        let data =
          if m.media_type == MediaType::Json && m.text.len() > 10_000_000 {
            // VSCode's TS server types large JSON files this way.
            DocumentText::Static("{}\n")
          } else {
            m.text.clone()
          };
        LoadResponse {
          data,
          script_kind: crate::tsc::as_ts_script_kind(m.media_type),
          version: state.script_version(&specifier),
          is_cjs: m.resolution_mode == ResolutionMode::Require,
          is_classic_script: m.notebook_uri.is_some(),
        }
      })
    };
  let serialized = maybe_load_response.to_v8(scope)?;
  state.performance.measure(mark);
  Ok(serialized)
}

#[op2(fast)]
fn op_release(
  state: &mut OpState,
  #[string] specifier: &str,
) -> Result<(), deno_core::url::ParseError> {
  let _span = super::logging::lsp_tracing_info_span!("op_release").entered();
  let state = state.borrow_mut::<State>();
  let mark = state
    .performance
    .mark_with_args("tsc.op.op_release", specifier);
  let specifier = state.specifier_map.normalize(specifier)?;
  state.state_snapshot.document_modules.release(
    &specifier,
    state.last_scope.as_deref(),
    Some(&state.last_compiler_options_key),
  );
  state.performance.measure(mark);
  Ok(())
}

#[op2]
#[allow(clippy::type_complexity, reason = "op")]
fn op_resolve(
  state: &mut OpState,
  #[string] base: &str,
  #[scoped] specifiers: Vec<(bool, String)>,
) -> Result<Vec<Option<(String, Option<String>)>>, deno_core::url::ParseError> {
  let _span = super::logging::lsp_tracing_info_span!("op_resolve").entered();
  op_resolve_inner(state, ResolveArgs { base, specifiers })
}

struct TscRequestArray {
  request: TscRequest,
  compiler_options_key: CompilerOptionsKey,
  notebook_uri: Option<Arc<Uri>>,
  id: Smi<usize>,
  change: convert::OptionNull<PendingChange>,
}

impl<'a> ToV8<'a> for TscRequestArray {
  type Error = JsErrorBox;

  fn to_v8(
    self,
    scope: &mut v8::PinScope<'a, '_>,
  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
    let id = self.id.to_v8(scope).unwrap_infallible();

    let (method_name, args) = self.request.into_server_request(scope)?;

    let method_name = deno_core::FastString::from_static(method_name)
      .v8_string(scope)
      .unwrap()
      .into();
    let args = args.unwrap_or_else(|| v8::Array::new(scope, 0).into());
    let compiler_options_key =
      serde_v8::to_v8(scope, self.compiler_options_key)
        .map_err(JsErrorBox::from_err)?;
    let notebook_uri = serde_v8::to_v8(scope, self.notebook_uri)
      .map_err(JsErrorBox::from_err)?;

    let change = self.change.to_v8(scope).unwrap_infallible();

    Ok(
      v8::Array::new_with_elements(
        scope,
        &[
          id,
          method_name,
          args,
          compiler_options_key,
          notebook_uri,
          change,
        ],
      )
      .into(),
    )
  }
}

/// How long the TSC isolate sits without a request before it prompts V8 to
/// collect garbage and return memory to the OS. Kept comfortably longer than
/// typical bursts of editor activity so we don't run a collection in the middle
/// of someone's editing session. Overridable via the
/// `DENO_LSP_IDLE_MEMORY_RELEASE_MS` env var (mainly so tests don't have to wait
/// out the full delay; a very large value effectively disables the behavior).
static IDLE_MEMORY_RELEASE_DELAY: std::sync::LazyLock<std::time::Duration> =
  std::sync::LazyLock::new(|| {
    const DEFAULT_MS: u64 = 5000;
    let ms = std::env::var("DENO_LSP_IDLE_MEMORY_RELEASE_MS")
      .ok()
      .and_then(|s| s.parse::<u64>().ok())
      .unwrap_or(DEFAULT_MS);
    std::time::Duration::from_millis(ms)
  });

#[op2]
async fn op_poll_requests(
  state: Rc<RefCell<OpState>>,
) -> convert::OptionNull<TscRequestArray> {
  let (mut pending_requests, arm_idle_timer) = {
    let mut state = state.borrow_mut();
    let state = state.try_borrow_mut::<State>().unwrap();
    (
      state.pending_requests.take().unwrap(),
      state.serviced_since_idle_release,
    )
  };

  // clear the resolution cache after each request
  NodeResolutionThreadLocalCache::clear();

  // While the language server sits idle the TSC isolate can hold on to a lot of
  // dead type-checker allocations (and the V8 heap pages behind them) that won't
  // be handed back to the OS until something forces a collection. If we've
  // serviced a request since the last release, wait for the next one with a
  // timeout; when it elapses, ask the JS side to prompt V8 to collect and return
  // that memory. See denoland/deno#23577.
  let next_request = if arm_idle_timer {
    match tokio::time::timeout(
      *IDLE_MEMORY_RELEASE_DELAY,
      pending_requests.recv(),
    )
    .await
    {
      Ok(maybe_request) => maybe_request,
      Err(_) => {
        let mut state = state.borrow_mut();
        let state = state.try_borrow_mut::<State>().unwrap();
        state.pending_requests = Some(pending_requests);
        // Only fire once per idle period; the next real request re-arms it.
        state.serviced_since_idle_release = false;
        // Measurement is finalized in `op_lsp_release_memory`.
        let mark = state
          .performance
          .mark(format!("tsc.host.{}", TscRequest::ReleaseMemory.method()));
        state.mark = Some(mark);
        return Some(TscRequestArray {
          request: TscRequest::ReleaseMemory,
          compiler_options_key: Default::default(),
          notebook_uri: None,
          id: Smi(0),
          change: None.into(),
        })
        .into();
      }
    }
  } else {
    pending_requests.recv().await
  };

  let Some((
    request,
    compiler_options_key,
    scope,
    notebook_uri,
    snapshot,
    response_tx,
    token,
    change,
    context,
  )) = next_request
  else {
    return None.into();
  };

  let mut state = state.borrow_mut();
  let state = state.try_borrow_mut::<State>().unwrap();
  state.pending_requests = Some(pending_requests);
  // We've done real work; arm the idle timer so memory is released once the
  // language server goes quiet again.
  state.serviced_since_idle_release = true;
  state.state_snapshot = snapshot;
  // Publish this request's cancellation token so the isolate's
  // near-heap-limit callback can cancel it if we're about to run out of
  // memory while servicing it.
  *state.request_cancellation.lock() = token.clone();
  state.token = token;
  state.response_tx = Some(response_tx);
  let id = state.last_id;
  state.last_id += 1;
  state
    .last_compiler_options_key
    .clone_from(&compiler_options_key);
  state.last_scope = scope;
  state.last_notebook_uri.clone_from(&notebook_uri);
  let mark = state
    .performance
    .mark_with_args(format!("tsc.host.{}", request.method()), &request);
  state.mark = Some(mark);
  state.context = context;

  Some(TscRequestArray {
    request,
    compiler_options_key,
    notebook_uri,
    id: Smi(id),
    change: change.into(),
  })
  .into()
}

#[inline]
#[allow(clippy::type_complexity, reason = "op")]
fn op_resolve_inner(
  state: &mut OpState,
  args: ResolveArgs,
) -> Result<Vec<Option<(String, Option<String>)>>, deno_core::url::ParseError> {
  let state = state.borrow_mut::<State>();
  let mark = state.performance.mark_with_args("tsc.op.op_resolve", &args);
  let referrer = state.specifier_map.normalize(args.base)?;
  let specifiers = state
    .state_snapshot
    .document_modules
    .resolve(
      &args.specifiers,
      &referrer,
      state.last_scope.as_deref(),
      Some(&state.last_compiler_options_key),
    )
    .into_iter()
    .map(|o| {
      o.map(|(s, mt)| {
        (
          denormalize_with_auto_import_alias(state, &s, mt, Some(&referrer)),
          match mt {
            MediaType::Unknown => None,
            // surface these as .js for typescript so side-effect imports
            // (e.g. `import "./styles.css"`) don't trigger TS6263
            MediaType::Css => Some(".js".to_string()),
            _ => Some(mt.as_ts_extension().to_string()),
          },
        )
      })
    })
    .collect();
  state.performance.measure(mark);
  Ok(specifiers)
}

#[op2(fast)]
fn op_respond(
  state: &mut OpState,
  #[string] response: String,
  #[string] error: String,
) {
  let _span = super::logging::lsp_tracing_info_span!("op_respond").entered();
  let state = state.borrow_mut::<State>();
  state.performance.measure(state.mark.take().unwrap());
  state.last_compiler_options_key = Default::default();
  state.last_scope = None;
  state.last_notebook_uri = None;
  let response = if !error.is_empty() {
    Err(anyhow!("tsc error: {error}"))
  } else {
    Ok(response)
  };

  let was_sent = state.response_tx.take().unwrap().send(response).is_ok();
  // Don't print the send error if the token is cancelled, it's expected
  // to fail in that case and this commonly occurs.
  if !was_sent && !state.token.is_cancelled() {
    lsp_warn!("Unable to send result to client.");
  }
}

/// Prompt V8 to free as much memory as it can and hand it back to the OS.
/// Called from the JS side during an idle memory release (see
/// `op_poll_requests` and `serverMainLoop`). This is a stop-the-world
/// collection, so it must only run when the isolate would otherwise be idle.
#[op2(fast)]
fn op_lsp_release_memory(state: &mut OpState, scope: &mut v8::PinScope) {
  scope.low_memory_notification();
  // Finalize the measurement started in `op_poll_requests` for this idle
  // release. Done here (rather than via `op_respond`, which an idle release
  // never reaches) so the work shows up in the perf log.
  let state = state.borrow_mut::<State>();
  if let Some(mark) = state.mark.take() {
    state.performance.measure(mark);
  }
}

struct TracingSpan(
  #[allow(dead_code, reason = "unsupported")] Option<super::trace::EnteredSpan>,
);

deno_core::external!(TracingSpan, "lsp::TracingSpan");

fn span_with_context(
  _state: &State,
  span: super::trace::Span,
) -> super::trace::EnteredSpan {
  #[cfg(feature = "lsp-tracing")]
  {
    use tracing_opentelemetry::OpenTelemetrySpanExt;

    if let Some(context) = &_state.context {
      span.set_parent(context.clone());
    }
    span.entered()
  }
  #[cfg(not(feature = "lsp-tracing"))]
  {
    span.entered()
  }
}

fn should_alias_node_modules_for_auto_import(
  state: &State,
  specifier: &ModuleSpecifier,
  scope: Option<&ModuleSpecifier>,
) -> bool {
  if !state.state_snapshot.resolver.in_node_modules(specifier) {
    return false;
  }
  let scoped_resolver =
    state.state_snapshot.resolver.get_scoped_resolver(scope);
  let referrer = scope.unwrap_or(specifier);
  scoped_resolver
    .resource_url_to_configured_dep_key(specifier, referrer)
    .is_some()
}

fn denormalize_with_auto_import_alias(
  state: &State,
  specifier: &ModuleSpecifier,
  media_type: MediaType,
  scope: Option<&ModuleSpecifier>,
) -> String {
  if should_alias_node_modules_for_auto_import(state, specifier, scope) {
    state
      .specifier_map
      .denormalize_with_node_modules_alias(specifier, media_type)
  } else {
    state.specifier_map.denormalize(specifier, media_type)
  }
}

#[op2(fast)]
fn op_make_span(
  op_state: &mut OpState,
  #[string] _s: &str,
  needs_context: bool,
) -> *const c_void {
  let state = op_state.borrow_mut::<State>();
  if !state.tracing_enabled() {
    return deno_core::ExternalPointer::new(TracingSpan(None)).into_raw();
  }
  let sp = super::logging::lsp_tracing_info_span!(
    "js",
    otel.name = format!("js::{_s}").as_str()
  );
  let span = if needs_context {
    span_with_context(state, sp)
  } else {
    sp.entered()
  };
  deno_core::ExternalPointer::new(TracingSpan(Some(span))).into_raw()
}

#[op2(fast)]
fn op_log_event(op_state: &OpState, #[string] _msg: &str) {
  let state = op_state.borrow::<State>();
  if state.tracing_enabled() {
    super::logging::lsp_tracing_info!(msg = _msg);
  }
}

#[op2(fast)]
fn op_exit_span(op_state: &mut OpState, span: *const c_void, root: bool) {
  let ptr = deno_core::ExternalPointer::<TracingSpan>::from_raw(span);
  // SAFETY: trust me
  let _span = unsafe { ptr.unsafely_take().0 };
  let state = op_state.borrow_mut::<State>();
  if root {
    state.context = None;
  }
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ScriptNames {
  by_compiler_options_key: BTreeMap<CompilerOptionsKey, IndexSet<String>>,
  by_notebook_uri: BTreeMap<Arc<Uri>, IndexSet<String>>,
}

fn insert_root_module_script_names(
  state: &State,
  script_names: &mut IndexSet<String>,
  module: &DocumentModule,
  scope: Option<&ModuleSpecifier>,
  is_open: bool,
) {
  let types_entry = (|| {
    let types_specifier = module
      .types_dependency
      .as_ref()?
      .dependency
      .maybe_specifier()?;
    state.state_snapshot.document_modules.resolve_dependency(
      types_specifier,
      &module.specifier,
      module.resolution_mode,
      module.scope.as_deref(),
      Some(&module.compiler_options_key),
    )
  })();
  // If there is a types dep, use that as the root instead. But if the doc
  // is open, include both as roots.
  if let Some((types_specifier, types_media_type, _)) = &types_entry {
    script_names.insert(denormalize_with_auto_import_alias(
      state,
      types_specifier,
      *types_media_type,
      scope,
    ));
  }
  if types_entry.is_none() || is_open {
    script_names.insert(denormalize_with_auto_import_alias(
      state,
      &module.specifier,
      module.media_type,
      scope,
    ));
    // The auto-import alias hides `node_modules/.deno` from tsc, but
    // requests for open documents (diagnostics, hover, etc.) use the
    // unaliased name. Include it as a root too so they don't fail with
    // "Could not find source file". See
    // https://github.com/denoland/deno/issues/35170.
    if is_open {
      script_names.insert(
        state
          .specifier_map
          .denormalize(&module.specifier, module.media_type),
      );
    }
  }
}

#[op2]
#[serde]
fn op_script_names(state: &mut OpState) -> ScriptNames {
  let _span =
    super::logging::lsp_tracing_info_span!("op_script_names").entered();
  let state = state.borrow_mut::<State>();
  let mark = state.performance.mark("tsc.op.op_script_names");
  let mut result = ScriptNames {
    by_compiler_options_key: Default::default(),
    by_notebook_uri: Default::default(),
  };

  let scopes_with_node_specifier = state
    .state_snapshot
    .document_modules
    .scopes_with_node_specifier();

  // Insert global scripts.
  for (compiler_options_key, compiler_options_data) in
    state.state_snapshot.compiler_options_resolver.entries()
  {
    let script_names = result
      .by_compiler_options_key
      .entry(compiler_options_key.clone())
      .or_default();
    let scope = compiler_options_data
      .workspace_dir_or_source_url
      .as_ref()
      .and_then(|s| state.state_snapshot.config.tree.scope_for_specifier(s))
      .cloned();
    let scoped_resolver = state
      .state_snapshot
      .resolver
      .get_scoped_resolver(scope.as_deref());
    for specifier in scoped_resolver.configured_auto_import_roots() {
      if specifier.scheme() != "jsr"
        && !specifier.as_str().starts_with(jsr_url().as_str())
      {
        continue;
      }
      let referrer = compiler_options_data
        .workspace_dir_or_source_url
        .as_deref()
        .or(scope.as_deref())
        .unwrap_or(&specifier);
      let Some((specifier, media_type, _)) =
        state.state_snapshot.document_modules.resolve_dependency(
          &specifier,
          referrer,
          ResolutionMode::Import,
          scope.as_deref(),
          Some(compiler_options_key),
        )
      else {
        continue;
      };
      script_names
        .insert(state.specifier_map.denormalize(&specifier, media_type));
    }
    if scopes_with_node_specifier.contains(&scope) {
      script_names.insert("asset:///reference_types_node.d.ts".to_string());
    }
    for (referrer, relative_specifiers) in compiler_options_data
      .ts_config_files
      .iter()
      .map(|(r, f)| {
        let relative_specifiers =
          Box::new(f.iter().map(|f| &f.relative_specifier))
            as Box<dyn Iterator<Item = &String>>;
        (r.as_ref(), relative_specifiers)
      })
      .chain(
        compiler_options_data
          .compiler_options_types
          .iter()
          .map(|(r, t)| (r, Box::new(t.iter()) as _)),
      )
    {
      let resolver = SingleReferrerGraphResolver {
        valid_referrer: referrer,
        module_resolution_mode: ResolutionMode::Import,
        cli_resolver: scoped_resolver.as_cli_resolver(),
        jsx_import_source_config: compiler_options_data
          .jsx_import_source_config
          .as_deref(),
      };
      for relative_specifier in relative_specifiers {
        let Ok(mut specifier) = resolver
          .resolve(
            relative_specifier,
            &deno_graph::Range {
              specifier: referrer.clone(),
              range: deno_graph::PositionRange::zeroed(),
              resolution_mode: None,
            },
            deno_graph::source::ResolutionKind::Types,
          )
          .inspect_err(|err| {
            lsp_warn!(
              "Failed to resolve {relative_specifier} from `compilerOptions.types`: {err:#}"
            );
          })
        else {
          continue;
        };
        if let Ok(req_ref) =
          deno_semver::npm::NpmPackageReqReference::from_specifier(&specifier)
        {
          let Some((resolved, _)) = scoped_resolver.npm_to_file_url(
            &req_ref,
            referrer,
            NodeResolutionKind::Types,
            ResolutionMode::Import,
          ) else {
            lsp_log!("Failed to resolve {req_ref} to a file URL.");
            continue;
          };
          specifier = resolved;
        }
        let Some(module) =
          state.state_snapshot.document_modules.module_for_specifier(
            &specifier,
            scope.as_deref(),
            Some(compiler_options_key),
          )
        else {
          continue;
        };
        script_names.insert(denormalize_with_auto_import_alias(
          state,
          &module.specifier,
          module.media_type,
          scope.as_deref(),
        ));
      }
    }
    for specifier in compiler_options_data.ts_config_roots.iter() {
      let scope = state
        .state_snapshot
        .config
        .tree
        .scope_for_specifier(specifier)
        .cloned();
      let Some(module) =
        state.state_snapshot.document_modules.module_for_specifier(
          specifier,
          scope.as_deref(),
          Some(compiler_options_key),
        )
      else {
        continue;
      };
      insert_root_module_script_names(
        state,
        script_names,
        &module,
        scope.as_deref(),
        false,
      );
    }
  }

  // roots for notebook scopes
  for (notebook_uri, cell_uris) in state
    .state_snapshot
    .document_modules
    .documents
    .cells_by_notebook_uri()
  {
    let mut script_names = IndexSet::default();
    let scope = state
      .state_snapshot
      .document_modules
      .primary_scope(notebook_uri)
      .flatten();
    let compiler_options_key = state
      .state_snapshot
      .compiler_options_resolver
      .entry_for_specifier(&uri_to_url(notebook_uri))
      .0;

    // Copy over the globals from the containing regular scopes.
    if let Some(global_script_names) =
      result.by_compiler_options_key.get(compiler_options_key)
    {
      script_names.extend(global_script_names.iter().cloned());
    }

    // Add the cells as roots.
    script_names.extend(cell_uris.iter().filter_map(|u| {
      let document = state.state_snapshot.document_modules.documents.get(u)?;
      let module = state
        .state_snapshot
        .document_modules
        .module(&document, scope.map(|s| s.as_ref()))?;
      Some(denormalize_with_auto_import_alias(
        state,
        &module.specifier,
        module.media_type,
        scope.map(|s| s.as_ref()),
      ))
    }));

    result
      .by_notebook_uri
      .insert(notebook_uri.clone(), script_names);
  }

  // finally include the documents
  for (scope, modules) in state
    .state_snapshot
    .document_modules
    .workspace_file_modules_by_scope()
    .into_iter()
  {
    for module in modules {
      let script_names = result
        .by_compiler_options_key
        .entry(module.compiler_options_key.clone())
        .or_default();
      insert_root_module_script_names(
        state,
        script_names,
        &module,
        scope.as_deref(),
        module.open_data.is_some(),
      );
    }
  }

  state.performance.measure(mark);
  result
}

#[op2]
#[string]
fn op_script_version(
  state: &mut OpState,
  #[string] specifier: &str,
) -> Result<Option<String>, deno_core::url::ParseError> {
  let state = state.borrow_mut::<State>();
  let mark = state.performance.mark("tsc.op.op_script_version");
  let specifier = state.specifier_map.normalize(specifier)?;
  let r = state.script_version(&specifier);
  state.performance.measure(mark);
  Ok(r)
}

#[op2(fast)]
#[number]
fn op_project_version(state: &mut OpState) -> usize {
  let state: &mut State = state.borrow_mut::<State>();
  let mark = state.performance.mark("tsc.op.op_project_version");
  let r = state.state_snapshot.project_version;
  state.performance.measure(mark);
  r
}

#[op2]
fn op_tsc_constants() -> crate::tsc::TscConstants {
  crate::tsc::TscConstants::new()
}

struct TscRuntime {
  js_runtime: JsRuntime,
  server_main_loop_fn_global: v8::Global<v8::Function>,
}

impl TscRuntime {
  fn new(mut js_runtime: JsRuntime) -> Self {
    let server_main_loop_fn_global = {
      let context = js_runtime.main_context();
      deno_core::scope!(scope, &mut js_runtime);
      let context_local = v8::Local::new(scope, context);
      let global_obj = context_local.global(scope);
      let server_main_loop_fn_str =
        v8::String::new_external_onebyte_static(scope, b"serverMainLoop")
          .unwrap();
      let server_main_loop_fn = v8::Local::try_from(
        global_obj
          .get(scope, server_main_loop_fn_str.into())
          .unwrap(),
      )
      .unwrap();
      v8::Global::new(scope, server_main_loop_fn)
    };
    Self {
      server_main_loop_fn_global,
      js_runtime,
    }
  }
}

fn run_tsc_thread(
  request_rx: UnboundedReceiver<Request>,
  performance: Arc<Performance>,
  specifier_map: Arc<TscSpecifierMap>,
  maybe_inspector_server: Option<Arc<InspectorServer>>,
  enable_tracing: Arc<AtomicBool>,
) {
  let has_inspector_server = maybe_inspector_server.is_some();
  // Shared with the isolate's near-heap-limit callback. `op_poll_requests`
  // keeps this updated with the in-flight request's cancellation token.
  let request_cancellation: Arc<Mutex<CancellationToken>> = Default::default();
  let mut extensions =
    deno_runtime::snapshot_info::get_extensions_in_snapshot();
  extensions.push(deno_tsc::init(
    performance,
    specifier_map,
    request_rx,
    enable_tracing,
    request_cancellation.clone(),
  ));
  let mut tsc_runtime = JsRuntime::new(RuntimeOptions {
    extensions,
    create_params: create_isolate_create_params(&crate::sys::CliSys::default()),
    startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
    inspector: has_inspector_server,
    // See cli/tsc/js.rs: the LSP TSC isolate shares CLI_SNAPSHOT and needs
    // the residual lazy-ESM/JS sources for node:* lookups (e.g. `process`).
    residual_lazy_esm_sources: deno_snapshots::RESIDUAL_LAZY_ESM,
    residual_lazy_js_sources: deno_snapshots::RESIDUAL_LAZY_JS,
    ..Default::default()
  });

  // The TSC isolate runs the TypeScript language service, whose type checker
  // can occasionally blow up to many gigabytes for pathological inputs (e.g.
  // deeply recursive library types while a file is mid-edit). Without
  // intervention V8 aborts the whole process with a fatal OOM, taking the
  // entire language server down and forcing a slow restart + reindex. Install
  // a near-heap-limit callback that instead cancels the in-flight request
  // (the language service host polls this token via `op_is_cancelled`, so the
  // computation unwinds with an `OperationCanceledError` that's already
  // handled gracefully) and grants a bounded amount of extra headroom so the
  // unwinding has room to run. See denoland/deno#25613.
  {
    // Extra heap we're willing to hand out, total, before giving up and
    // letting V8 OOM as a last resort (better than exhausting all of system
    // memory). Granted in steps so we never overshoot by much.
    const HEADROOM_BYTES: usize = 1024 * 1024 * 1024;
    const STEP_BYTES: usize = 128 * 1024 * 1024;
    let request_cancellation = request_cancellation.clone();
    tsc_runtime.add_near_heap_limit_callback(
      move |current_limit, initial_limit| {
        // Cancel the request being serviced so the language service unwinds
        // at its next cancellation checkpoint and frees the memory.
        request_cancellation.lock().cancel();
        let ceiling = initial_limit.saturating_add(HEADROOM_BYTES);
        if current_limit >= ceiling {
          lsp_warn!(
            "TSC isolate exceeded its heap headroom; cancelling the request. The language server may run out of memory."
          );
          current_limit
        } else {
          lsp_warn!(
            "TSC isolate is near its heap limit; cancelling the in-flight request to recover."
          );
          (current_limit + STEP_BYTES).min(ceiling)
        }
      },
    );
  }

  if let Some(server) = maybe_inspector_server {
    server.register_inspector(
      "ext:deno_tsc/99_main_compiler.js".to_string(),
      tsc_runtime.inspector(),
      false,
    );
  }

  let tsc_future = async {
    // start_tsc(&mut tsc_runtime, false).unwrap();
    let tsc_runtime =
      Rc::new(tokio::sync::Mutex::new(TscRuntime::new(tsc_runtime)));
    let tsc_runtime_ = tsc_runtime.clone();

    let event_loop_fut = async {
      loop {
        if let Err(e) = tsc_runtime_
          .lock()
          .await
          .js_runtime
          .run_event_loop(Default::default())
          .await
        {
          log::error!("Error in TSC event loop: {e}");
        }
      }
    };
    let main_loop_fut = {
      let enable_debug = std::env::var("DENO_TSC_DEBUG")
        .map(|s| {
          let s = s.trim();
          s == "1" || s.eq_ignore_ascii_case("true")
        })
        .unwrap_or(false);
      let mut runtime = tsc_runtime.lock().await;
      let main_loop = runtime.server_main_loop_fn_global.clone();
      let args = {
        deno_core::scope!(scope, &mut runtime.js_runtime);
        let enable_debug_local =
          v8::Local::<v8::Value>::from(v8::Boolean::new(scope, enable_debug));
        [v8::Global::new(scope, enable_debug_local)]
      };

      runtime.js_runtime.call_with_args(&main_loop, &args)
    };

    tokio::select! {
      biased;
      _ = event_loop_fut => {},
      res = main_loop_fut => {
        if let Err(err) = res {
          log::error!("Error in TSC main loop: {err}");
        }
      }
    }
  }
  .boxed_local();

  let runtime = create_basic_runtime();
  runtime.block_on(tsc_future)
}

deno_core::extension!(deno_tsc,
  ops = [
    op_is_cancelled,
    op_is_node_file,
    op_load,
    op_tsc_constants,
    op_release,
    op_resolve,
    op_respond,
    op_lsp_release_memory,
    op_script_names,
    op_script_version,
    op_project_version,
    op_poll_requests,
    op_make_span,
    op_exit_span,
    op_log_event,
    op_libs,
  ],
  options = {
    performance: Arc<Performance>,
    specifier_map: Arc<TscSpecifierMap>,
    request_rx: UnboundedReceiver<Request>,
    enable_tracing: Arc<AtomicBool>,
    request_cancellation: Arc<Mutex<CancellationToken>>,
  },
  state = |state, options| {
    state.put(State::new(
      Default::default(),
      options.specifier_map,
      options.performance,
      options.request_rx,
      options.enable_tracing,
      options.request_cancellation,
    ));
  },
  customizer = |ext: &mut deno_core::Extension| {
    use deno_core::ExtensionFileSource;
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_tsc/99_main_compiler.js", crate::tsc::MAIN_COMPILER_SOURCE.as_str().into()));
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_tsc/97_ts_host.js", crate::tsc::TS_HOST_SOURCE.as_str().into()));
    ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_tsc/98_lsp.js", crate::tsc::LSP_SOURCE.as_str().into()));
    ext.js_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/00_typescript.js", crate::tsc::TYPESCRIPT_SOURCE.as_str().into()));
    ext.esm_entry_point = Some("ext:deno_tsc/99_main_compiler.js");
  }
);

#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u32)]
pub enum CompletionTriggerKind {
  Invoked = 1,
  TriggerCharacter = 2,
  TriggerForIncompleteCompletions = 3,
}

impl From<lsp::CompletionTriggerKind> for CompletionTriggerKind {
  fn from(kind: lsp::CompletionTriggerKind) -> Self {
    match kind {
      lsp::CompletionTriggerKind::INVOKED => Self::Invoked,
      lsp::CompletionTriggerKind::TRIGGER_CHARACTER => Self::TriggerCharacter,
      lsp::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => {
        Self::TriggerForIncompleteCompletions
      }
      _ => Self::Invoked,
    }
  }
}

pub type QuotePreference = config::QuoteStyle;

pub type ImportModuleSpecifierPreference = config::ImportModuleSpecifier;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code, reason = "unsupported")]
pub enum ImportModuleSpecifierEnding {
  Auto,
  Minimal,
  Index,
  Js,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code, reason = "unsupported")]
pub enum IncludeInlayParameterNameHints {
  None,
  Literals,
  All,
}

impl From<&config::InlayHintsParamNamesEnabled>
  for IncludeInlayParameterNameHints
{
  fn from(setting: &config::InlayHintsParamNamesEnabled) -> Self {
    match setting {
      config::InlayHintsParamNamesEnabled::All => Self::All,
      config::InlayHintsParamNamesEnabled::Literals => Self::Literals,
      config::InlayHintsParamNamesEnabled::None => Self::None,
    }
  }
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code, reason = "unsupported")]
pub enum IncludePackageJsonAutoImports {
  Auto,
  On,
  Off,
}

pub type JsxAttributeCompletionStyle = config::JsxAttributeCompletionStyle;

#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct GetCompletionsAtPositionOptions {
  #[serde(flatten)]
  pub user_preferences: UserPreferences,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub trigger_character: Option<String>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub trigger_kind: Option<CompletionTriggerKind>,
}

#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPreferences {
  #[serde(skip_serializing_if = "Option::is_none")]
  pub disable_suggestions: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub quote_preference: Option<QuotePreference>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_for_module_exports: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_for_import_statements: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_with_snippet_text: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_automatic_optional_chain_completions: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_with_insert_text: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_with_class_member_snippets: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_completions_with_object_literal_method_snippets: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub use_label_details_in_completion_entries: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub allow_incomplete_completions: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub import_module_specifier_preference:
    Option<ImportModuleSpecifierPreference>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub import_module_specifier_ending: Option<ImportModuleSpecifierEnding>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub allow_text_changes_in_new_files: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub provide_prefix_and_suffix_text_for_rename: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_package_json_auto_imports: Option<IncludePackageJsonAutoImports>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub provide_refactor_not_applicable_reason: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub jsx_attribute_completion_style: Option<JsxAttributeCompletionStyle>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_parameter_name_hints:
    Option<IncludeInlayParameterNameHints>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_parameter_name_hints_when_argument_matches_name:
    Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_function_parameter_type_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_variable_type_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_variable_type_hints_when_type_matches_name: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_property_declaration_type_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_function_like_return_type_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub include_inlay_enum_member_value_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub allow_rename_of_import_path: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub auto_import_file_exclude_patterns: Option<Vec<String>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub interactive_inlay_hints: Option<bool>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub prefer_type_only_auto_imports: Option<bool>,
}

impl UserPreferences {
  pub fn from_config_for_specifier(
    config: &config::Config,
    specifier: &ModuleSpecifier,
  ) -> Self {
    let fmt_options = config.tree.fmt_config_for_specifier(specifier);
    let fmt_config = &fmt_options.options;
    let base_preferences = Self {
      allow_incomplete_completions: Some(true),
      allow_text_changes_in_new_files: Some(specifier.scheme() == "file"),
      // TODO(nayeemrmn): Investigate why we use `Index` here.
      import_module_specifier_ending: Some(ImportModuleSpecifierEnding::Index),
      include_completions_with_snippet_text: Some(
        config.snippet_support_capable(),
      ),
      interactive_inlay_hints: Some(true),
      provide_refactor_not_applicable_reason: Some(true),
      quote_preference: Some(fmt_config.into()),
      use_label_details_in_completion_entries: Some(true),
      ..Default::default()
    };
    let Some(language_settings) =
      config.language_settings_for_specifier(specifier)
    else {
      return base_preferences;
    };
    Self {
      auto_import_file_exclude_patterns: Some(
        language_settings
          .preferences
          .auto_import_file_exclude_patterns
          .clone(),
      ),
      include_automatic_optional_chain_completions: Some(
        language_settings.suggest.enabled
          && language_settings
            .suggest
            .include_automatic_optional_chain_completions,
      ),
      include_completions_for_import_statements: Some(
        language_settings.suggest.enabled
          && language_settings
            .suggest
            .include_completions_for_import_statements,
      ),
      include_completions_for_module_exports: Some(
        language_settings.suggest.enabled
          && language_settings.suggest.auto_imports,
      ),
      include_completions_with_class_member_snippets: Some(
        language_settings.suggest.enabled
          && language_settings.suggest.class_member_snippets.enabled
          && config.snippet_support_capable(),
      ),
      include_completions_with_insert_text: Some(
        language_settings.suggest.enabled,
      ),
      include_completions_with_object_literal_method_snippets: Some(
        language_settings.suggest.enabled
          && language_settings
            .suggest
            .object_literal_method_snippets
            .enabled
          && config.snippet_support_capable(),
      ),
      import_module_specifier_preference: Some(
        language_settings.preferences.import_module_specifier,
      ),
      include_inlay_parameter_name_hints: Some(
        (&language_settings.inlay_hints.parameter_names.enabled).into(),
      ),
      include_inlay_parameter_name_hints_when_argument_matches_name: Some(
        !language_settings
          .inlay_hints
          .parameter_names
          .suppress_when_argument_matches_name,
      ),
      include_inlay_function_parameter_type_hints: Some(
        language_settings.inlay_hints.parameter_types.enabled,
      ),
      include_inlay_variable_type_hints: Some(
        language_settings.inlay_hints.variable_types.enabled,
      ),
      include_inlay_variable_type_hints_when_type_matches_name: Some(
        !language_settings
          .inlay_hints
          .variable_types
          .suppress_when_type_matches_name,
      ),
      include_inlay_property_declaration_type_hints: Some(
        language_settings
          .inlay_hints
          .property_declaration_types
          .enabled,
      ),
      include_inlay_function_like_return_type_hints: Some(
        language_settings
          .inlay_hints
          .function_like_return_types
          .enabled,
      ),
      include_inlay_enum_member_value_hints: Some(
        language_settings.inlay_hints.enum_member_values.enabled,
      ),
      jsx_attribute_completion_style: Some(
        language_settings.preferences.jsx_attribute_completion_style,
      ),
      provide_prefix_and_suffix_text_for_rename: Some(
        language_settings.preferences.use_aliases_for_renames,
      ),
      // Only use workspace settings for quote style if there's no `deno.json`.
      quote_preference: if config
        .tree
        .workspace_dir_for_specifier(specifier)
        .is_some_and(|ctx| ctx.member_or_root_deno_json().is_some())
      {
        base_preferences.quote_preference
      } else {
        Some(language_settings.preferences.quote_style)
      },
      prefer_type_only_auto_imports: Some(
        language_settings.preferences.prefer_type_only_auto_imports,
      ),
      ..base_preferences
    }
  }
}

// `Serialize` is retained because the whole `TscRequest` is serialized to a
// JSON performance-trace via `Performance::mark_with_args`; `ToV8` is what
// actually crosses the V8 boundary in `into_server_request`.
#[derive(Debug, Clone, Serialize, deno_core::ToV8)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItemsOptions {
  #[serde(skip_serializing_if = "Option::is_none")]
  #[to_v8(skip_if = Option::is_none)]
  pub trigger_reason: Option<SignatureHelpTriggerReason>,
}

#[derive(Debug, Clone, Serialize, deno_core::ToV8)]
pub enum SignatureHelpTriggerKind {
  #[serde(rename = "characterTyped")]
  CharacterTyped,
  #[serde(rename = "invoked")]
  Invoked,
  #[serde(rename = "retrigger")]
  Retrigger,
  #[serde(rename = "unknown")]
  Unknown,
}

impl From<lsp::SignatureHelpTriggerKind> for SignatureHelpTriggerKind {
  fn from(kind: lsp::SignatureHelpTriggerKind) -> Self {
    match kind {
      lsp::SignatureHelpTriggerKind::INVOKED => Self::Invoked,
      lsp::SignatureHelpTriggerKind::TRIGGER_CHARACTER => Self::CharacterTyped,
      lsp::SignatureHelpTriggerKind::CONTENT_CHANGE => Self::Retrigger,
      _ => Self::Unknown,
    }
  }
}

#[derive(Debug, Clone, Serialize, deno_core::ToV8)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpTriggerReason {
  pub kind: SignatureHelpTriggerKind,
  #[serde(skip_serializing_if = "Option::is_none")]
  #[to_v8(skip_if = Option::is_none)]
  pub trigger_character: Option<String>,
}

#[derive(Debug, Serialize, Clone, Copy)]
pub struct TscTextRange {
  pos: u32,
  end: u32,
}

impl From<Range<u32>> for TscTextRange {
  fn from(range: Range<u32>) -> Self {
    Self {
      pos: range.start,
      end: range.end,
    }
  }
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CombinedCodeFixScope {
  r#type: &'static str,
  file_name: String,
}

#[derive(Debug, Serialize, Clone)]
pub enum OrganizeImportsMode {
  All,
  #[allow(unused, reason = "unsupported")]
  SortAndCombine,
  #[allow(unused, reason = "unsupported")]
  RemoveUnused,
}

#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct OrganizeImportsArgs {
  #[serde(flatten)]
  pub scope: CombinedCodeFixScope,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub mode: Option<OrganizeImportsMode>,
}

#[derive(Serialize, Clone, Copy)]
#[allow(dead_code, reason = "currently unused")]
pub struct JsNull;

#[derive(Debug, Clone, Serialize)]
enum TscRequest {
  GetDiagnostics((String, usize)),

  GetAmbientModules,
  CleanupSemanticCache,
  /// Synthesized by `op_poll_requests` when the language server has been idle.
  /// Not a real client request: the JS main loop prompts V8 to collect and
  /// return memory, and never sends a response.
  ReleaseMemory,
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6230
  FindReferences((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6235
  GetNavigationTree((String,)),
  GetSupportedCodeFixes,
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6214
  GetQuickInfoAtPosition((String, u32, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6257
  GetCodeFixesAtPosition(
    Box<(
      String,
      u32,
      u32,
      Vec<i32>,
      FormatCodeSettings,
      UserPreferences,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6274
  GetApplicableRefactors(
    Box<(
      String,
      TscTextRange,
      UserPreferences,
      Option<&'static str>,
      String,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6258
  GetCombinedCodeFix(
    Box<(
      CombinedCodeFixScope,
      String,
      FormatCodeSettings,
      UserPreferences,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6275
  GetEditsForRefactor(
    Box<(
      String,
      FormatCodeSettings,
      TscTextRange,
      String,
      String,
      Option<UserPreferences>,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6281
  GetEditsForFileRename(
    Box<(String, String, FormatCodeSettings, UserPreferences)>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6231
  GetDocumentHighlights(Box<(String, u32, Vec<String>)>),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6226
  GetDefinitionAndBoundSpan((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6227
  GetTypeDefinitionAtPosition((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6193
  GetCompletionsAtPosition(
    Box<(
      String,
      u32,
      GetCompletionsAtPositionOptions,
      FormatCodeSettings,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6205
  #[allow(clippy::type_complexity, reason = "TODO: cleanup")]
  GetCompletionEntryDetails(
    Box<(
      String,
      u32,
      String,
      FormatCodeSettings,
      Option<String>,
      Option<UserPreferences>,
      Option<Value>,
    )>,
  ),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6228
  GetImplementationAtPosition((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6240
  GetOutliningSpans((String,)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6237
  ProvideCallHierarchyIncomingCalls((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6238
  ProvideCallHierarchyOutgoingCalls((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6236
  PrepareCallHierarchy((String, u32)),
  // https://github.com/denoland/deno/blob/v2.2.2/cli/tsc/dts/typescript.d.ts#L6674
  FindRenameLocations((String, u32, bool, bool, UserPreferences)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6224
  GetSmartSelectionRange((String, u32)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6183
  GetEncodedSemanticClassifications((String, TextSpan, &'static str)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6217
  GetSignatureHelpItems((String, u32, SignatureHelpItemsOptions)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6233
  GetNavigateToItems((String, Option<u32>, Option<String>)),
  // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6239
  ProvideInlayHints((String, TextSpan, UserPreferences)),
  // https://github.com/denoland/deno/blob/v2.5.2/cli/tsc/dts/typescript.d.ts#L6769
  OrganizeImports(
    (
      OrganizeImportsArgs,
      FormatCodeSettings,
      Option<UserPreferences>,
    ),
  ),
}

impl TscRequest {
  /// Converts the request into a tuple containing the method name and the
  /// arguments (in the form of a V8 value) to be passed to the server request
  /// function
  fn into_server_request<'s>(
    self,
    scope: &mut v8::PinScope<'s, '_>,
  ) -> Result<(&'static str, Option<v8::Local<'s, v8::Value>>), JsErrorBox> {
    // Requests whose arguments are positional tuples of primitives serialize to
    // a `v8::Array` via the `ToV8` derives/impls (matching serde's tuple
    // encoding). Bare integers are wrapped in `Number` so they map to a JS
    // `number` exactly as serde did. Requests carrying richer config structs
    // (`UserPreferences`, `FormatCodeSettings`, …) or `#[serde(flatten)]` stay
    // on `serde_v8::to_v8` until those types grow `ToV8` impls.
    let args = match self {
      TscRequest::GetDiagnostics(args) => (
        "$getDiagnostics",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetAmbientModules => ("$getAmbientModules", None),
      TscRequest::FindReferences((specifier, position)) => (
        "findReferences",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::GetNavigationTree(args) => {
        ("getNavigationTree", Some(args.to_v8(scope)?))
      }
      TscRequest::GetSupportedCodeFixes => ("$getSupportedCodeFixes", None),
      TscRequest::GetQuickInfoAtPosition((
        specifier,
        position,
        maximum_length,
      )) => (
        "getQuickInfoAtPosition",
        Some(
          (specifier, Number(position), Number(maximum_length)).to_v8(scope)?,
        ),
      ),
      TscRequest::GetCodeFixesAtPosition(args) => (
        "getCodeFixesAtPosition",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetApplicableRefactors(args) => (
        "getApplicableRefactors",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetCombinedCodeFix(args) => (
        "getCombinedCodeFix",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetEditsForRefactor(args) => (
        "getEditsForRefactor",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetEditsForFileRename(args) => (
        "getEditsForFileRename",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetDocumentHighlights(args) => {
        let (specifier, position, files_to_search) = *args;
        (
          "getDocumentHighlights",
          Some((specifier, Number(position), files_to_search).to_v8(scope)?),
        )
      }
      TscRequest::GetDefinitionAndBoundSpan((specifier, position)) => (
        "getDefinitionAndBoundSpan",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::GetTypeDefinitionAtPosition((specifier, position)) => (
        "getTypeDefinitionAtPosition",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::GetCompletionsAtPosition(args) => (
        "getCompletionsAtPosition",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetCompletionEntryDetails(args) => (
        "getCompletionEntryDetails",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetImplementationAtPosition((specifier, position)) => (
        "getImplementationAtPosition",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::GetOutliningSpans(args) => {
        ("getOutliningSpans", Some(args.to_v8(scope)?))
      }
      TscRequest::ProvideCallHierarchyIncomingCalls((specifier, position)) => (
        "provideCallHierarchyIncomingCalls",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::ProvideCallHierarchyOutgoingCalls((specifier, position)) => (
        "provideCallHierarchyOutgoingCalls",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::PrepareCallHierarchy((specifier, position)) => (
        "prepareCallHierarchy",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::FindRenameLocations(args) => (
        "findRenameLocations",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetSmartSelectionRange((specifier, position)) => (
        "getSmartSelectionRange",
        Some((specifier, Number(position)).to_v8(scope)?),
      ),
      TscRequest::GetEncodedSemanticClassifications(args) => (
        "getEncodedSemanticClassifications",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::GetSignatureHelpItems((specifier, position, options)) => (
        "getSignatureHelpItems",
        Some((specifier, Number(position), options).to_v8(scope)?),
      ),
      TscRequest::GetNavigateToItems((search, max_result_count, file)) => (
        "getNavigateToItems",
        Some((search, max_result_count.map(Number), file).to_v8(scope)?),
      ),
      TscRequest::ProvideInlayHints(args) => (
        "provideInlayHints",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::OrganizeImports(args) => (
        "organizeImports",
        Some(serde_v8::to_v8(scope, args).map_err(JsErrorBox::from_err)?),
      ),
      TscRequest::CleanupSemanticCache => ("$cleanupSemanticCache", None),
      TscRequest::ReleaseMemory => ("$releaseMemory", None),
    };

    Ok(args)
  }

  fn method(&self) -> &'static str {
    match self {
      TscRequest::GetDiagnostics(_) => "$getDiagnostics",
      TscRequest::GetAmbientModules => "$getAmbientModules",
      TscRequest::CleanupSemanticCache => "$cleanupSemanticCache",
      TscRequest::ReleaseMemory => "$releaseMemory",
      TscRequest::FindReferences(_) => "findReferences",
      TscRequest::GetNavigationTree(_) => "getNavigationTree",
      TscRequest::GetSupportedCodeFixes => "$getSupportedCodeFixes",
      TscRequest::GetQuickInfoAtPosition(_) => "getQuickInfoAtPosition",
      TscRequest::GetCodeFixesAtPosition(_) => "getCodeFixesAtPosition",
      TscRequest::GetApplicableRefactors(_) => "getApplicableRefactors",
      TscRequest::GetCombinedCodeFix(_) => "getCombinedCodeFix",
      TscRequest::GetEditsForRefactor(_) => "getEditsForRefactor",
      TscRequest::GetEditsForFileRename(_) => "getEditsForFileRename",
      TscRequest::GetDocumentHighlights(_) => "getDocumentHighlights",
      TscRequest::GetDefinitionAndBoundSpan(_) => "getDefinitionAndBoundSpan",
      TscRequest::GetTypeDefinitionAtPosition(_) => {
        "getTypeDefinitionAtPosition"
      }
      TscRequest::GetCompletionsAtPosition(_) => "getCompletionsAtPosition",
      TscRequest::GetCompletionEntryDetails(_) => "getCompletionEntryDetails",
      TscRequest::GetImplementationAtPosition(_) => {
        "getImplementationAtPosition"
      }
      TscRequest::GetOutliningSpans(_) => "getOutliningSpans",
      TscRequest::ProvideCallHierarchyIncomingCalls(_) => {
        "provideCallHierarchyIncomingCalls"
      }
      TscRequest::ProvideCallHierarchyOutgoingCalls(_) => {
        "provideCallHierarchyOutgoingCalls"
      }
      TscRequest::PrepareCallHierarchy(_) => "prepareCallHierarchy",
      TscRequest::FindRenameLocations(_) => "findRenameLocations",
      TscRequest::GetSmartSelectionRange(_) => "getSmartSelectionRange",
      TscRequest::GetEncodedSemanticClassifications(_) => {
        "getEncodedSemanticClassifications"
      }
      TscRequest::GetSignatureHelpItems(_) => "getSignatureHelpItems",
      TscRequest::GetNavigateToItems(_) => "getNavigateToItems",
      TscRequest::ProvideInlayHints(_) => "provideInlayHints",
      TscRequest::OrganizeImports(_) => "organizeImports",
    }
  }
}

#[cfg(test)]
mod tests {
  use pretty_assertions::assert_eq;
  use test_util::TempDir;

  use super::*;
  use crate::cache::HttpCache;
  use crate::lsp::cache::LspCache;
  use crate::lsp::compiler_options::LspCompilerOptionsResolver;
  use crate::lsp::config::Config;
  use crate::lsp::config::WorkspaceSettings;
  use crate::lsp::documents::DocumentModules;
  use crate::lsp::documents::LanguageId;
  use crate::lsp::lint::LspLinterResolver;
  use crate::lsp::resolver::LspResolver;
  use crate::lsp::text::LineIndex;

  #[test]
  fn test_find_mergeable_named_import() {
    let new_import =
      parse_simple_named_import("import { think } from \"cowsay\";\n\n")
        .unwrap();
    let existing_import = find_mergeable_named_import(
      "import { say } from \"cowsay\";\n\nthink();\n",
      &new_import,
    )
    .unwrap();
    assert_eq!(existing_import.insert_offset, 12);
    assert_eq!(existing_import.new_text, ", think");
  }

  #[test]
  fn test_find_mergeable_named_import_multiline() {
    let new_import =
      parse_simple_named_import("import { think } from \"cowsay\";\n\n")
        .unwrap();
    let existing_import = find_mergeable_named_import(
      "import {\n  say,\n} from \"cowsay\";\n\nthink();\n",
      &new_import,
    )
    .unwrap();
    assert_eq!(existing_import.insert_offset, 16);
    assert_eq!(existing_import.new_text, "think,\n");
  }

  async fn setup(
    deno_json_content: Value,
    sources: &[(&str, &str, i32, LanguageId)],
  ) -> (TempDir, TsJsServer, Arc<StateSnapshot>) {
    let temp_dir = TempDir::new();
    let cache = LspCache::new(Some(temp_dir.url().join(".deno_dir").unwrap()));
    let mut config = Config::default();
    config
      .tree
      .inject_config_file(
        deno_config::deno_json::ConfigFile::new(
          &deno_json_content.to_string(),
          temp_dir.url().join("deno.json").unwrap(),
        )
        .unwrap(),
      )
      .await;
    let resolver =
      Arc::new(LspResolver::from_config(&config, &cache, None).await);
    let compiler_options_resolver =
      Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None));
    resolver.set_compiler_options_resolver(&compiler_options_resolver.inner);
    let linter_resolver = Arc::new(LspLinterResolver::new(
      &config,
      &compiler_options_resolver,
      &resolver,
    ));
    let mut document_modules = DocumentModules::default();
    document_modules.update_config(
      &config,
      &compiler_options_resolver,
      &resolver,
      &cache,
      &Default::default(),
    );
    for (relative_specifier, source, version, language_id) in sources {
      let specifier = temp_dir.url().join(relative_specifier).unwrap();
      document_modules.open_document(
        url_to_uri(&specifier).unwrap(),
        *version,
        *language_id,
        (*source).into(),
        None,
      );
    }
    let snapshot = Arc::new(StateSnapshot {
      project_version: 0,
      document_modules,
      config: Arc::new(config),
      compiler_options_resolver,
      linter_resolver,
      resolver,
      cache: Arc::new(cache),
      client_needs_file_uris_for_virtual_documents: false,
    });
    let performance = Arc::new(Performance::default());
    let ts_server = TsJsServer::new(performance);
    ts_server.project_changed(
      snapshot.clone(),
      &[],
      Some(
        snapshot
          .compiler_options_resolver
          .entries()
          .map(|(k, d)| (k.clone(), d.compiler_options.clone()))
          .collect(),
      ),
      None,
    );
    (temp_dir, ts_server, snapshot)
  }

  fn setup_op_state(state_snapshot: Arc<StateSnapshot>) -> OpState {
    let (_tx, rx) = mpsc::unbounded_channel();
    let state = State::new(
      state_snapshot,
      Default::default(),
      Default::default(),
      rx,
      Arc::new(AtomicBool::new(true)),
      Default::default(),
    );
    let mut op_state = OpState::new(None);
    op_state.put(state);
    op_state
  }

  #[test]
  fn test_replace_links() {
    let actual = replace_links(r"test {@link http://deno.land/x/mod.ts} test");
    assert_eq!(
      actual,
      r"test [http://deno.land/x/mod.ts](http://deno.land/x/mod.ts) test"
    );
    let actual =
      replace_links(r"test {@link http://deno.land/x/mod.ts a link} test");
    assert_eq!(actual, r"test [a link](http://deno.land/x/mod.ts) test");
    let actual =
      replace_links(r"test {@linkcode http://deno.land/x/mod.ts a link} test");
    assert_eq!(actual, r"test [`a link`](http://deno.land/x/mod.ts) test");
  }

  #[test]
  fn test_rewrite_first_quoted_specifier() {
    let mut text = r#"import { rollup } from "";"#.to_string();
    rewrite_first_quoted_specifier(&mut text, "$rollup");
    assert_eq!(text, r#"import { rollup } from "$rollup";"#);

    let mut text = r#"import { rollup } from "npm:rollup";"#.to_string();
    rewrite_first_quoted_specifier(&mut text, "$rollup");
    assert_eq!(text, r#"import { rollup } from "$rollup";"#);
  }

  #[tokio::test]
  async fn test_get_diagnostics() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({
        "compilerOptions": {
          "lib": [],
        },
      }),
      &[(
        "a.ts",
        r#"console.log("hello deno");"#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(
      json!(diagnostics),
      json!([
        {
          "start": { "line": 0, "character": 0 },
          "end": { "line": 0, "character": 7 },
          "fileName": specifier,
          "messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the \'lib\' compiler option to include 'dom'.",
          "sourceLine": "console.log(\"hello deno\");",
          "category": 1,
          "code": 2584,
        }
      ]),
    );
  }

  #[tokio::test]
  async fn test_get_diagnostics_lib() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({
        "compilerOptions": {
          "lib": ["dom"],
        },
      }),
      &[(
        "a.ts",
        r#"console.log(document.location);"#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(json!(diagnostics), json!([]));
  }

  #[tokio::test]
  async fn test_module_resolution() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"
        import { B } from "https://deno.land/x/b/mod.ts";

        const b = new B();

        console.log(b);
      "#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(json!(diagnostics), json!([]));
  }

  #[tokio::test]
  async fn test_bad_module_specifiers() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"
        import { A } from ".";
        "#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(
      json!(diagnostics),
      json!([
        {
          "start": {
            "line": 1,
            "character": 8
          },
          "end": {
            "line": 1,
            "character": 30
          },
          "fileName": specifier,
          "messageText": "\'A\' is declared but its value is never read.",
          "sourceLine": "        import { A } from \".\";",
          "category": 2,
          "code": 6133,
          "reportsUnnecessary": true,
        }
      ]),
    );
  }

  #[tokio::test]
  async fn test_remote_modules() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"
        import { B } from "https://deno.land/x/b/mod.ts";

        const b = new B();

        console.log(b);
      "#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(json!(diagnostics), json!([]));
  }

  #[tokio::test]
  async fn test_partial_modules() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"
        import {
          Application,
          Context,
          Router,
          Status,
        } from "https://deno.land/x/oak@v6.3.2/mod.ts";

        import * as test from
      "#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(
      json!(diagnostics),
      json!([
        {
          "start": {
            "line": 1,
            "character": 8
          },
          "end": {
            "line": 6,
            "character": 55,
          },
          "fileName": specifier.clone(),
          "messageText": "All imports in import declaration are unused.",
          "sourceLine": "        import {",
          "category": 2,
          "code": 6192,
          "reportsUnnecessary": true,
        },
        {
          "start": {
            "line": 8,
            "character": 29
          },
          "end": {
            "line": 8,
            "character": 29
          },
          "fileName": specifier,
          "messageText": "Expression expected.",
          "sourceLine": "        import * as test from",
          "category": 1,
          "code": 1109
        }
      ]),
    );
  }

  #[tokio::test]
  async fn test_no_debug_failure() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"const url = new URL("b.js", import."#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(
      json!(diagnostics),
      json!([
        {
          "start": {
            "line": 0,
            "character": 35,
          },
          "end": {
            "line": 0,
            "character": 35
          },
          "fileName": specifier,
          "messageText": "Identifier expected.",
          "sourceLine": "const url = new URL(\"b.js\", import.",
          "category": 1,
          "code": 1003,
        }
      ]),
    );
  }

  #[tokio::test]
  async fn test_modify_sources() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[(
        "a.ts",
        r#"
          import * as a from "https://deno.land/x/example/a.ts";
          if (a.a === "b") {
            console.log("fail");
          }
        "#,
        1,
        LanguageId::TypeScript,
      )],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let scope = snapshot
      .config
      .tree
      .scope_for_specifier(&specifier)
      .map(|s| s.as_ref());
    let dep_specifier =
      resolve_url("https://deno.land/x/example/a.ts").unwrap();
    snapshot
      .cache
      .global()
      .set(
        &dep_specifier,
        Default::default(),
        b"export const b = \"b\";\n",
      )
      .unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(&specifier, scope, None)
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(
      json!(diagnostics),
      json!([
        {
          "category": 1,
          "code": 2339,
          "start": {
            "line": 2,
            "character": 16,
          },
          "end": {
            "line": 2,
            "character": 17
          },
          "messageText": "Property \'a\' does not exist on type \'typeof import(\"https://deno.land/x/example/a.ts\", { with: { \"resolution-mode\": \"import\" } })\'.",
          "sourceLine": "          if (a.a === \"b\") {",
          "fileName": specifier,
        }
      ]),
    );
    let dep_document = snapshot
      .document_modules
      .documents
      .get_for_specifier(&dep_specifier, scope, &snapshot.cache)
      .unwrap();
    snapshot
      .cache
      .global()
      .set(
        &dep_specifier,
        Default::default(),
        b"export const b = \"b\";\n\nexport const a = \"b\";\n",
      )
      .unwrap();
    let snapshot = {
      Arc::new(StateSnapshot {
        project_version: snapshot.project_version + 1,
        ..snapshot.as_ref().clone()
      })
    };
    ts_server.project_changed(
      snapshot.clone(),
      &[(dep_document, ChangeKind::Modified)],
      None,
      None,
    );
    snapshot.document_modules.release(
      &dep_specifier,
      snapshot
        .config
        .tree
        .scope_for_specifier(&specifier)
        .map(|s| s.as_ref()),
      None,
    );
    let module = snapshot
      .document_modules
      .module_for_specifier(&specifier, scope, None)
      .unwrap();
    let diagnostics = ts_server
      .get_diagnostics(snapshot.clone(), &module, &Default::default())
      .await
      .unwrap();
    assert_eq!(json!(diagnostics), json!([]));
  }

  #[test]
  fn test_completion_entry_filter_text() {
    let fixture = CompletionEntry {
      kind: ScriptElementKind::MemberVariableElement,
      name: "['foo']".to_string(),
      insert_text: Some("['foo']".to_string()),
      ..Default::default()
    };
    let actual = fixture.get_filter_text(None);
    assert_eq!(actual, Some(".foo".to_string()));

    let fixture = CompletionEntry {
      kind: ScriptElementKind::MemberVariableElement,
      name: "#abc".to_string(),
      ..Default::default()
    };
    let actual = fixture.get_filter_text(None);
    assert_eq!(actual, None);

    let fixture = CompletionEntry {
      kind: ScriptElementKind::MemberVariableElement,
      name: "#abc".to_string(),
      insert_text: Some("this.#abc".to_string()),
      ..Default::default()
    };
    let actual = fixture.get_filter_text(None);
    assert_eq!(actual, Some("abc".to_string()));
  }

  #[test]
  fn test_tsc_specifier_map_node_modules_alias_is_opt_in() {
    let map = TscSpecifierMap::new();
    let specifier = ModuleSpecifier::parse(
      "file:///project/node_modules/.deno/pkg@1.0.0/node_modules/pkg/mod.d.ts",
    )
    .unwrap();

    let denormalized = map.denormalize(&specifier, MediaType::Dts);
    assert_eq!(denormalized, specifier.as_str());

    let aliased =
      map.denormalize_with_node_modules_alias(&specifier, MediaType::Dts);
    assert_eq!(
      aliased,
      "file:///project/$node_modules/.deno/pkg@1.0.0/$node_modules/pkg/mod.d.ts",
    );
    assert_eq!(map.normalize(&aliased).unwrap(), specifier);
  }

  #[tokio::test]
  async fn test_completions() {
    let fixture = r#"
      import { B } from "https://deno.land/x/b/mod.ts";

      const b = new B();

      console.
    "#;
    let line_index = LineIndex::new(fixture);
    let position = line_index
      .offset_tsc(lsp::Position {
        line: 5,
        character: 16,
      })
      .unwrap();
    let (temp_dir, ts_server, snapshot) =
      setup(json!({}), &[("a.ts", fixture, 1, LanguageId::TypeScript)]).await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let info = ts_server
      .get_completions(
        snapshot.clone(),
        &module,
        position,
        Some(".".to_string()),
        None,
        &Default::default(),
      )
      .await
      .unwrap()
      .unwrap();
    assert_eq!(info.entries.len(), 22);
    let details = ts_server
      .get_completion_details(
        snapshot.clone(),
        &module,
        position,
        "log".to_string(),
        None,
        None,
        &Default::default(),
      )
      .await
      .unwrap()
      .unwrap();
    assert_eq!(
      json!(details),
      json!({
        "name": "log",
        "kindModifiers": "declare",
        "kind": "method",
        "displayParts": [
          {
            "text": "(",
            "kind": "punctuation"
          },
          {
            "text": "method",
            "kind": "text"
          },
          {
            "text": ")",
            "kind": "punctuation"
          },
          {
            "text": " ",
            "kind": "space"
          },
          {
            "text": "Console",
            "kind": "interfaceName"
          },
          {
            "text": ".",
            "kind": "punctuation"
          },
          {
            "text": "log",
            "kind": "methodName"
          },
          {
            "text": "(",
            "kind": "punctuation"
          },
          {
            "text": "...",
            "kind": "punctuation"
          },
          {
            "text": "data",
            "kind": "parameterName"
          },
          {
            "text": ":",
            "kind": "punctuation"
          },
          {
            "text": " ",
            "kind": "space"
          },
          {
            "text": "any",
            "kind": "keyword"
          },
          {
            "text": "[",
            "kind": "punctuation"
          },
          {
            "text": "]",
            "kind": "punctuation"
          },
          {
            "text": ")",
            "kind": "punctuation"
          },
          {
            "text": ":",
            "kind": "punctuation"
          },
          {
            "text": " ",
            "kind": "space"
          },
          {
            "text": "void",
            "kind": "keyword"
          }
        ],
        "documentation": [
          {
            "text": "Outputs a message to the console",
            "kind": "text",
          },
        ],
        "tags": [
          {
            "name": "param",
            "text": [
              {
                "text": "data",
                "kind": "parameterName",
              },
              {
                "text": " ",
                "kind": "space",
              },
              {
                "text": "Values to be printed to the console",
                "kind": "text",
              },
            ],
          },
          {
            "name": "example",
            "text": [
              {
                "text": "```ts\nconsole.log('Hello', 'World', 123);\n```",
                "kind": "text",
              },
            ],
          },
        ]
      })
    );
  }

  #[tokio::test]
  async fn test_completions_fmt() {
    let fixture_a = r#"
      console.log(someLongVaria)
    "#;
    let fixture_b = r#"
      export const someLongVariable = 1
    "#;
    let line_index = LineIndex::new(fixture_a);
    let position = line_index
      .offset_tsc(lsp::Position {
        line: 1,
        character: 33,
      })
      .unwrap();
    let (temp_dir, ts_server, snapshot) = setup(
      json!({
        "fmt": {
          "semiColons": false,
          "singleQuote": true,
        },
      }),
      &[
        ("a.ts", fixture_a, 1, LanguageId::TypeScript),
        ("b.ts", fixture_b, 1, LanguageId::TypeScript),
      ],
    )
    .await;
    let specifier = temp_dir.url().join("a.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let info = ts_server
      .get_completions(
        snapshot.clone(),
        &module,
        position,
        None,
        None,
        &Default::default(),
      )
      .await
      .unwrap()
      .unwrap();
    let entry = info
      .entries
      .iter()
      .find(|e| &e.name == "someLongVariable")
      .unwrap();
    let details = ts_server
      .get_completion_details(
        snapshot.clone(),
        &module,
        position,
        entry.name.clone(),
        entry.source.clone(),
        entry.data.clone(),
        &Default::default(),
      )
      .await
      .unwrap()
      .unwrap();
    let actions = details.code_actions.unwrap();
    let action = actions
      .iter()
      .find(|a| &a.description == r#"Add import from "./b.ts""#)
      .unwrap();
    let changes = action.changes.first().unwrap();
    let change = changes.text_changes.first().unwrap();
    assert_eq!(
      change.new_text,
      "import { someLongVariable } from './b.ts'\n"
    );
  }

  #[test]
  fn test_classification_to_semantic_tokens_multiline_tokens() {
    let line_index = LineIndex::new("  to\nken  \n");
    let classifications = Classifications {
      spans: vec![2, 6, 2057],
    };
    let semantic_tokens = classifications
      .to_semantic_tokens(&line_index, &Default::default())
      .unwrap();
    assert_eq!(
      &semantic_tokens.data,
      &[
        lsp::SemanticToken {
          delta_line: 0,
          delta_start: 2,
          length: 3,
          token_type: 7,
          token_modifiers_bitset: 9,
        },
        lsp::SemanticToken {
          delta_line: 1,
          delta_start: 0,
          length: 3,
          token_type: 7,
          token_modifiers_bitset: 9,
        },
      ]
    );
  }

  #[tokio::test]
  async fn test_get_edits_for_file_rename() {
    let (temp_dir, ts_server, snapshot) = setup(
      json!({}),
      &[
        ("a.ts", r#"import "./b.ts";"#, 1, LanguageId::TypeScript),
        ("b.ts", r#""#, 1, LanguageId::TypeScript),
      ],
    )
    .await;
    let specifier = temp_dir.url().join("b.ts").unwrap();
    let module = snapshot
      .document_modules
      .module_for_specifier(
        &specifier,
        snapshot
          .config
          .tree
          .scope_for_specifier(&specifier)
          .map(|s| s.as_ref()),
        None,
      )
      .unwrap();
    let changes = ts_server
      .get_edits_for_file_rename(
        snapshot,
        &module,
        &temp_dir.url().join("🦕.ts").unwrap(),
        &Default::default(),
      )
      .await
      .unwrap();
    assert_eq!(
      changes,
      vec![FileTextChanges {
        file_name: temp_dir.url().join("a.ts").unwrap().to_string(),
        text_changes: vec![TextChange {
          span: TextSpan {
            start: 8,
            length: 6,
          },
          new_text: "./🦕.ts".to_string(),
        }],
        is_new_file: None,
      }]
    );
  }

  #[test]
  fn include_suppress_inlay_hint_settings() {
    let mut settings = WorkspaceSettings::default();
    settings
      .typescript
      .inlay_hints
      .parameter_names
      .suppress_when_argument_matches_name = true;
    settings
      .typescript
      .inlay_hints
      .variable_types
      .suppress_when_type_matches_name = true;
    let mut config = config::Config::default();
    config.set_workspace_settings(settings, vec![]);
    let user_preferences = UserPreferences::from_config_for_specifier(
      &config,
      &ModuleSpecifier::parse("file:///foo.ts").unwrap(),
    );
    assert_eq!(
      user_preferences.include_inlay_variable_type_hints_when_type_matches_name,
      Some(false)
    );
    assert_eq!(
      user_preferences
        .include_inlay_parameter_name_hints_when_argument_matches_name,
      Some(false)
    );
  }

  #[tokio::test]
  async fn resolve_unknown_dependency() {
    let (temp_dir, _, snapshot) =
      setup(json!({}), &[("a.ts", "", 1, LanguageId::TypeScript)]).await;
    let mut state = setup_op_state(snapshot);
    let base = temp_dir.url().join("a.ts").unwrap().to_string();
    let resolved = op_resolve_inner(
      &mut state,
      ResolveArgs {
        base: &base,
        specifiers: vec![(false, "./b.ts".to_string())],
      },
    )
    .unwrap();
    assert_eq!(
      resolved,
      vec![Some((
        temp_dir.url().join("b.ts").unwrap().to_string(),
        Some(MediaType::TypeScript.as_ts_extension().to_string())
      ))]
    );
  }

  #[test]
  fn coalesce_pending_change() {
    use ChangeKind::*;
    fn change<S: AsRef<str>>(
      project_version: usize,
      scripts: impl IntoIterator<Item = (S, ChangeKind)>,
      new_compiler_options_by_key: Option<
        BTreeMap<CompilerOptionsKey, Arc<CompilerOptions>>,
      >,
    ) -> PendingChange {
      PendingChange {
        project_version,
        modified_scripts: scripts
          .into_iter()
          .map(|(s, c)| (s.as_ref().into(), c))
          .collect(),
        new_compiler_options_by_key,
        new_notebook_keys: None,
      }
    }
    let cases = [
      (
        // start
        change(1, [("file:///a.ts", Closed)], None),
        // new
        change(2, Some(("file:///b.ts", Opened)), None),
        // expected
        change(
          2,
          [("file:///a.ts", Closed), ("file:///b.ts", Opened)],
          None,
        ),
      ),
      (
        // start
        change(
          1,
          [("file:///a.ts", Closed), ("file:///b.ts", Opened)],
          None,
        ),
        // new
        change(
          2,
          // a gets closed then reopened, b gets opened then closed
          [("file:///a.ts", Opened), ("file:///b.ts", Closed)],
          None,
        ),
        // expected
        change(
          2,
          [("file:///a.ts", Opened), ("file:///b.ts", Closed)],
          None,
        ),
      ),
      (
        change(
          1,
          [("file:///a.ts", Opened), ("file:///b.ts", Modified)],
          None,
        ),
        // new
        change(
          2,
          // a gets opened then modified, b gets modified then closed
          [("file:///a.ts", Opened), ("file:///b.ts", Closed)],
          None,
        ),
        // expected
        change(
          2,
          [("file:///a.ts", Opened), ("file:///b.ts", Closed)],
          None,
        ),
      ),
    ];

    for (start, new, expected) in cases {
      let mut pending = start;
      pending.coalesce(new.project_version, new.modified_scripts, None, None);
      assert_eq!(json!(pending), json!(expected));
    }
  }
}
