/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/tsc.rs
7 887 строк
229 KB
Bartek Iwańczuk
feat: drop the TypeScript fork, run on stock typescript@6.0.3 (#35642)
08 июл 2026, 22:25
Не верифицирован
08 июл 2026, 22:25
34d5929
Код
Авторство
О чём код?
// 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(¶m.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(¬ebook_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 { let _ = 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(); let runtime = create_basic_runtime(); let _guard = runtime.enter(); // 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(); 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(); // 23 with stock TypeScript: `@types/node`'s `console` global exposes the // `Console` constructor property in addition to the 22 log methods (the // forked compiler suppressed it). assert_eq!(info.entries.len(), 23); 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" }, { "text": " ", "kind": "space" }, { "text": "(", "kind": "punctuation" }, { "text": "+", "kind": "operator" }, { "text": "1", "kind": "numericLiteral" }, { "text": " ", "kind": "space" }, { "text": "overload", "kind": "text" }, { "text": ")", "kind": "punctuation" } ], "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)); } } }