/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/language_server.rs
5 051 строка
154 KB
Nathan Whitaker
fix(resolver): scope sloppy import hints to prepared modules (#36221)
23 июл 2026, 04:23
Не верифицирован
23 июл 2026, 04:23
b23cb76
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::RefCell; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; use std::env; use std::fmt::Write as _; use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use dashmap::DashMap; use deno_ast::MediaType; use deno_cache_dir::GlobalOrLocalHttpCache; use deno_cache_dir::file_fetcher::CacheSetting; use deno_core::ModuleSpecifier; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::parking_lot::Mutex; use deno_core::serde_json; use deno_core::serde_json::Value; use deno_core::serde_json::json; use deno_core::unsync::spawn; use deno_core::url; use deno_core::url::Url; use deno_graph::CheckJsOption; use deno_graph::GraphKind; use deno_graph::Resolution; use deno_lib::args::CaData; use deno_lib::args::get_root_cert_store; use deno_lib::version::DENO_VERSION_INFO; use deno_npm::resolution::NpmVersionResolver; use deno_npm_installer::graph::NpmCachingStrategy; use deno_path_util::url_to_file_path; use deno_resolver::deno_json::CompilerOptionsKey; use deno_resolver::loader::MemoryFilesRc; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_semver::jsr::JsrPackageReqReference; use indexmap::IndexSet; use log::error; use serde::Deserialize; use serde_json::from_value; use tokio::sync::OnceCell; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::unbounded_channel; use tokio_util::sync::CancellationToken; use tower_lsp::jsonrpc::Error as LspError; use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types::request::*; use tower_lsp::lsp_types::*; use super::analysis::CodeActionData; use super::analysis::TsResponseImportMapper; use super::analysis::fix_ts_import_changes; use super::analysis::ts_changes_to_edit; use super::cache::LspCache; use super::capabilities; use super::capabilities::INFERRED_TYPE_CODE_ACTION_KIND; use super::capabilities::semantic_tokens_registration_options; use super::client::Client; use super::code_lens; use super::completions; use super::config::Config; use super::config::SETTINGS_SECTION; use super::config::WorkspaceSettings; use super::diagnostics; use super::diagnostics::DiagnosticDataSpecifier; use super::diagnostics::DiagnosticsServer; use super::diagnostics::DiagnosticsUpdateMessage; use super::documents::Document; use super::documents::DocumentModule; use super::documents::DocumentModules; use super::documents::LanguageId; use super::jsr::CliJsrSearchApi; use super::logging::lsp_log; use super::logging::lsp_warn; use super::lsp_custom; use super::lsp_custom::INFERRED_TYPE_COMMAND; use super::lsp_custom::TaskDefinition; use super::npm::CliNpmSearchApi; use super::parent_process_checker; use super::performance::Performance; use super::refactor; use super::registries::ModuleRegistry; use super::resolver::ImportMapHover; use super::resolver::LspResolver; use super::test_code_actions::collect_test_code_actions; use super::testing; use super::text; use super::ts_server::TsServer; use super::tsc::ChangeKind; use super::urls::uri_to_file_path; use super::urls::uri_to_url; use super::urls::url_to_uri; use crate::args::Flags; use crate::args::InternalFlags; use crate::args::UnstableFmtOptions; use crate::factory::CliFactory; use crate::file_fetcher::CreateCliFileFetcherOptions; use crate::file_fetcher::create_cli_file_fetcher; use crate::graph_util; use crate::http_util::HttpClientProvider; use crate::lsp::compiler_options::LspCompilerOptionsResolver; use crate::lsp::completions::CompletionItemData; use crate::lsp::config::ConfigWatchedFileType; use crate::lsp::diagnostics::DenoDiagnostic; use crate::lsp::diagnostics::generate_import_map_diagnostics; use crate::lsp::diagnostics::generate_module_diagnostics; use crate::lsp::lint::LspLinterResolver; use crate::lsp::lint::get_deno_lint_code_actions; use crate::lsp::logging::init_log_file; use crate::sys::CliSys; use crate::tools::fmt::format_file; use crate::tools::fmt::format_parsed_source; use crate::tools::upgrade::check_for_upgrades_for_lsp; use crate::tools::upgrade::upgrade_check_enabled; use crate::util::env::resolve_cwd; use crate::util::fs::remove_dir_all_if_exists; use crate::util::path::to_percent_decoded_str; use crate::util::sync::AsyncFlag; struct LspRootCertStoreProvider(RootCertStore); impl RootCertStoreProvider for LspRootCertStoreProvider { fn get_or_try_init(&self) -> Result<&RootCertStore, deno_error::JsErrorBox> { Ok(&self.0) } } /// Used as a filtering option for `Inner::get_document()`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Enabled { /// Return Ok(None) if not enabled. Filter, /// Allow even if not enabled. Ignore, } /// Used as a filtering option for `Inner::get_document()`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Exists { /// Return Err(LspError::InvalidParams(...)) if non-existent. Enforce, /// Return Ok(None) if non-existent. Filter, } /// Used as a filtering option for `Inner::get_document()`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Diagnosable { /// Return Ok(None) if not diagnosable. Filter, /// Allow even if not diagnosable. Ignore, } pub fn to_lsp_range(referrer: &deno_graph::Range) -> lsp_types::Range { lsp_types::Range { start: lsp_types::Position { line: referrer.range.start.line as u32, character: referrer.range.start.character as u32, }, end: lsp_types::Position { line: referrer.range.end.line as u32, character: referrer.range.end.character as u32, }, } } /// Render the hover markdown describing how a specifier was resolved through an /// import map. fn import_map_hover_text(hover: &ImportMapHover) -> String { let source = hover .base_url .path_segments() .and_then(|mut s| s.next_back()) .filter(|s| !s.is_empty()) .unwrap_or_else(|| hover.base_url.as_str()); format!( "**Import Map**: `{}` → `{}` _({})_\n", hover.key, hover.value, source, ) } #[derive(Debug)] struct DidChangeBatchQueueEntry { version: i32, content_changes: Vec<TextDocumentContentChangeEvent>, } #[derive(Debug)] struct DidChangeBatchQueue { uri: Uri, entries: Mutex<VecDeque<(DidChangeBatchQueueEntry, CancellationToken)>>, } impl DidChangeBatchQueue { fn new(uri: Uri) -> Self { DidChangeBatchQueue { uri, entries: Default::default(), } } fn enqueue(&self, entry: DidChangeBatchQueueEntry) -> CancellationToken { let token = CancellationToken::new(); self.entries.lock().push_back((entry, token.clone())); token } fn dequeue(&self) -> Option<DidChangeBatchQueueEntry> { let (entry, token) = self.entries.lock().pop_front()?; token.cancel(); Some(entry) } fn clear(&self) { let entries = std::mem::take(&mut *self.entries.lock()); entries.into_iter().for_each(|(_, token)| token.cancel()); } } #[derive(Debug, Clone)] pub struct LanguageServer { client: Client, pub inner: Rc<tokio::sync::RwLock<Inner>>, /// This is used to block out standard request handling until the complete /// user configuration has been fetched. This is done in the `initialized` /// handler which normally may occur concurrently with those other requests. /// TODO(nayeemrmn): This wouldn't be necessary if LSP allowed /// `workspace/configuration` requests in the `initialize` handler. See: /// https://github.com/Microsoft/language-server-protocol/issues/567#issuecomment-2085131917 init_flag: AsyncFlag, did_change_batch_queue: RefCell<Option<Arc<DidChangeBatchQueue>>>, performance: Arc<Performance>, } /// Snapshot of the state used by TSC. #[derive(Clone, Debug, Default)] pub struct StateSnapshot { pub project_version: usize, pub config: Arc<Config>, pub compiler_options_resolver: Arc<LspCompilerOptionsResolver>, pub linter_resolver: Arc<LspLinterResolver>, pub document_modules: DocumentModules, pub resolver: Arc<LspResolver>, pub cache: Arc<LspCache>, pub client_needs_file_uris_for_virtual_documents: bool, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] enum ProjectScopesChange { None, OpenNotebooks, Config, } type LanguageServerTaskFn = Box<dyn FnOnce(LanguageServer) + Send + Sync>; /// Used to queue tasks from inside of the language server lock that must be /// commenced from outside of it. For example, queue a request to cache a module /// after having loaded a config file which references it. #[derive(Debug)] struct LanguageServerTaskQueue { task_tx: UnboundedSender<LanguageServerTaskFn>, /// This is moved out to its own task after initializing. task_rx: Option<UnboundedReceiver<LanguageServerTaskFn>>, } impl Default for LanguageServerTaskQueue { fn default() -> Self { let (task_tx, task_rx) = unbounded_channel(); Self { task_tx, task_rx: Some(task_rx), } } } impl LanguageServerTaskQueue { pub fn queue_task(&self, task_fn: LanguageServerTaskFn) -> bool { self.task_tx.send(task_fn).is_ok() } /// Panics if called more than once. fn start(&mut self, ls: LanguageServer) { let mut task_rx = self.task_rx.take().unwrap(); spawn(async move { while let Some(task_fn) = task_rx.recv().await { task_fn(ls.clone()); } }); } } pub type OnceCellMap<K, V> = DashMap<K, Arc<OnceCell<V>>>; pub struct Inner { /// (_, notebook_uri) -> _ ambient_modules_regex_cache: OnceCellMap<(CompilerOptionsKey, Option<Arc<Uri>>), Option<regex::Regex>>, pub cache: LspCache, /// The LSP client that this LSP server is connected to. pub client: Client, compiler_options_resolver: Arc<LspCompilerOptionsResolver>, /// Configuration information. pub config: Config, diagnostics_cache: OnceCellMap<Arc<Uri>, Arc<Vec<Diagnostic>>>, no_slow_types_cache: diagnostics::NoSlowTypesDiagnosticsCache, diagnostics_server: Option<diagnostics::DiagnosticsServer>, /// The collection of documents that the server is currently handling, either /// on disk or "open" within the client. pub document_modules: DocumentModules, http_client_provider: Arc<HttpClientProvider>, initial_cwd: PathBuf, jsr_search_api: CliJsrSearchApi, linter_resolver: Arc<LspLinterResolver>, /// Handles module registries, which allow discovery of modules module_registry: ModuleRegistry, /// A lazily create "server" for handling test run requests. maybe_testing_server: Option<testing::TestServer>, pub npm_search_api: CliNpmSearchApi, project_version: usize, /// A collection of measurements which instrument that performance of the LSP. performance: Arc<Performance>, client_needs_file_uris_for_virtual_documents: bool, force_push_based_diagnostics: bool, registered_semantic_tokens_capabilities: bool, pub resolver: Arc<LspResolver>, task_queue: LanguageServerTaskQueue, pub ts_server: Arc<TsServer>, workspace_files: Arc<IndexSet<PathBuf>>, /// Set to `self.config.settings.enable_settings_hash()` after /// refreshing `self.workspace_files`. workspace_files_hash: u64, _tracing: Option<super::trace::TracingGuard>, } impl std::fmt::Debug for Inner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Inner") .field("cache", &self.cache) .field("client", &self.client) .field("config", &self.config) .field("diagnostics_server", &self.diagnostics_server) .field("document_modules", &self.document_modules) .field("http_client_provider", &self.http_client_provider) .field("initial_cwd", &self.initial_cwd) .field("jsr_search_api", &self.jsr_search_api) .field("module_registry", &self.module_registry) .field("maybe_testing_server", &self.maybe_testing_server) .field("npm_search_api", &self.npm_search_api) .field("project_version", &self.project_version) .field("performance", &self.performance) .field( "client_needs_file_uris_for_virtual_documents", &self.client_needs_file_uris_for_virtual_documents, ) .field( "registered_semantic_tokens_capabilities", &self.registered_semantic_tokens_capabilities, ) .field("resolver", &self.resolver) .field("task_queue", &self.task_queue) .field("ts_server", &self.ts_server) .field("workspace_files", &self.workspace_files) .field("workspace_files_hash", &self.workspace_files_hash) .field("_tracing", &self._tracing) .finish() } } impl LanguageServer { pub fn new(client: Client) -> Self { let performance = Arc::new(Performance::default()); Self { client: client.clone(), inner: Rc::new(tokio::sync::RwLock::new(Inner::new( client, performance.clone(), ))), init_flag: Default::default(), did_change_batch_queue: Default::default(), performance, } } /// Similar to `deno install --entrypoint` on the command line, where modules will be cached /// in the Deno cache, including any of their dependencies. #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn cache( &self, specifiers: Vec<ModuleSpecifier>, referrer: ModuleSpecifier, force_global_cache: bool, ) -> LspResult<Option<Value>> { async fn create_graph_for_caching( factory: CliFactory, roots: Vec<ModuleSpecifier>, open_modules: Vec<Arc<DocumentModule>>, ) -> Result<(), AnyError> { let open_modules = open_modules .into_iter() .map(|m| (m.specifier.clone(), m)) .collect::<HashMap<_, _>>(); let module_graph_builder = factory.module_graph_builder().await?; let module_graph_creator = factory.module_graph_creator().await?; let mut inner_loader = module_graph_builder.create_graph_loader_with_root_permissions(); let mut loader = crate::lsp::documents::OpenDocumentsGraphLoader { inner_loader: &mut inner_loader, open_modules: &open_modules, }; let graph = module_graph_creator .create_graph_with_loader( GraphKind::All, roots.clone(), &mut loader, NpmCachingStrategy::Eager, ) .await?; let cli_options = factory.cli_options()?; let collect_bare_importable_pkg_names = || { cli_options .workspace() .resolver_jsr_pkgs() .map(|pkg| pkg.name) .collect::<Vec<_>>() }; graph_util::graph_valid( &graph, &CliSys::default(), &roots, graph_util::GraphValidOptions { kind: GraphKind::All, will_type_check: true, check_js: CheckJsOption::False, exit_integrity_errors: false, allow_unknown_media_types: true, allow_unknown_jsr_exports: false, allow_sloppy_imports_hints_for_unreferenced_roots: true, collect_bare_importable_pkg_names: &collect_bare_importable_pkg_names, }, )?; Ok(()) } self.init_flag.wait_raised().await; // prepare the cache inside the lock let mark = self .performance .mark_with_args("lsp.cache", (&specifiers, &referrer)); *self.did_change_batch_queue.borrow_mut() = None; let prepare_cache_result = self.inner.write().await.prepare_cache( specifiers, referrer, force_global_cache, ); match prepare_cache_result { Ok(result) => { // cache outside the lock let cli_factory = result.cli_factory; let roots = result.roots; let open_modules = result.open_modules; let handle = spawn(async move { create_graph_for_caching(cli_factory, roots, open_modules).await }); if let Err(err) = handle.await.unwrap() { lsp_warn!("Error caching: {:#}", err); self.client.show_message(MessageType::WARNING, err); } // now get the lock back to update with the new information *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.post_cache().await; self.performance.measure(mark); } Err(err) => { lsp_warn!("Error preparing caching: {:#}", err); self.client.show_message(MessageType::WARNING, err); return Err(LspError::internal_error()); } } Ok(Some(json!(true))) } pub async fn performance_request( &self, _token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; Ok(Some(self.inner.read().await.get_performance())) } pub async fn inferred_type( &self, params: Option<Value>, token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; match params.map(serde_json::from_value) { Some(Ok(params)) => Ok(Some( serde_json::to_value( self .inner .read() .await .inferred_type(params, &token) .await?, ) .map_err(|err| { error!("Failed to serialize inferred_type response: {:#}", err); LspError::internal_error() })?, )), Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), } } pub async fn task_definitions( &self, _token: CancellationToken, ) -> LspResult<Vec<TaskDefinition>> { self.init_flag.wait_raised().await; self.inner.read().await.task_definitions() } pub async fn test_run_request( &self, params: Option<Value>, _token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; self.inner.read().await.test_run_request(params).await } pub async fn test_run_cancel_request( &self, params: Option<Value>, _token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; self.inner.read().await.test_run_cancel_request(params) } pub async fn virtual_text_document( &self, params: Option<Value>, _token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; match params.map(serde_json::from_value) { Some(Ok(params)) => Ok(Some( serde_json::to_value( self.inner.read().await.virtual_text_document(params)?, ) .map_err(|err| { error!( "Failed to serialize virtual_text_document response: {:#}", err ); LspError::internal_error() })?, )), Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), } } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn refresh_configuration(&self) { let (folders, capable) = { let inner = self.inner.read().await; ( inner.config.workspace_folders.clone(), inner.config.workspace_configuration_capable(), ) }; if capable { let mut scopes = Vec::with_capacity(folders.len() + 1); scopes.push(None); for (_, folder) in folders.as_ref() { scopes.push(Some(folder.uri.clone())); } let configs = self .client .when_outside_lsp_lock() .workspace_configuration(scopes) .await; if let Ok(configs) = configs { if configs.len() != folders.len() + 1 { lsp_warn!("Incorrect number of configurations received."); return; } let mut configs = configs.into_iter(); let unscoped = configs.next().unwrap(); let mut folder_settings = Vec::with_capacity(folders.len()); for (folder_uri, _) in folders.as_ref() { folder_settings.push((folder_uri.clone(), configs.next().unwrap())); } *self.did_change_batch_queue.borrow_mut() = None; self .inner .write() .await .config .set_workspace_settings(unscoped, folder_settings); } } } } impl Inner { fn new(client: Client, performance: Arc<Performance>) -> Self { let cache = LspCache::default(); let http_client_provider = Arc::new(HttpClientProvider::new(None, None)); let module_registry = ModuleRegistry::new( cache.deno_dir().registries_folder_path(), http_client_provider.clone(), ); let jsr_search_api = CliJsrSearchApi::new(module_registry.file_fetcher.clone()); let npm_search_api = CliNpmSearchApi::new( module_registry.file_fetcher.clone(), Arc::new(NpmVersionResolver { link_packages: Default::default(), newest_dependency_date_options: Default::default(), overrides: Default::default(), trust_policy: Default::default(), }), ); let config = Config::default(); let ts_server = Arc::new(TsServer::new(performance.clone())); let initial_cwd = resolve_cwd(None).unwrap().into_owned(); Self { ambient_modules_regex_cache: Default::default(), cache, client, compiler_options_resolver: Default::default(), config, diagnostics_cache: Default::default(), no_slow_types_cache: Default::default(), diagnostics_server: None, document_modules: Default::default(), http_client_provider, initial_cwd: initial_cwd.clone(), jsr_search_api, linter_resolver: Default::default(), project_version: 0, task_queue: Default::default(), maybe_testing_server: None, module_registry, npm_search_api, performance, client_needs_file_uris_for_virtual_documents: false, registered_semantic_tokens_capabilities: false, force_push_based_diagnostics: false, resolver: Default::default(), ts_server, workspace_files: Default::default(), workspace_files_hash: 0, _tracing: Default::default(), } } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn get_document( &self, uri: &Uri, enabled: Enabled, exists: Exists, diagnosable: Diagnosable, ) -> LspResult<Option<Document>> { match enabled { Enabled::Filter => { if !self.config.uri_enabled(uri) { return Ok(None); } } Enabled::Ignore => {} } let Some(document) = self.document_modules.documents.get(uri) else { match exists { Exists::Enforce if !uri.scheme().as_str().eq_ignore_ascii_case("deno") => { return Err(LspError::invalid_params(format!( "Unable to find document for: {}", uri.as_str() ))); } Exists::Filter => { return Ok(None); } _ => return Ok(None), } }; match diagnosable { Diagnosable::Filter => { if !document.is_diagnosable() { return Ok(None); } } Diagnosable::Ignore => {} } Ok(Some(document)) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn get_primary_module( &self, document: &Document, ) -> LspResult<Option<Arc<DocumentModule>>> { let Some(module) = self.document_modules.primary_module(document) else { let url = uri_to_url(document.uri()); if url.scheme() != "file" || self.resolver.in_node_modules(&url) || self.cache.in_cache_directory(&url) { return Ok(None); } lsp_warn!( "Unable to get module for document: {}", document.uri().as_str(), ); return Err(LspError::internal_error()); }; Ok(Some(module)) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn snapshot(&self) -> Arc<StateSnapshot> { Arc::new(StateSnapshot { project_version: self.project_version, config: Arc::new(self.config.clone()), compiler_options_resolver: self.compiler_options_resolver.clone(), linter_resolver: self.linter_resolver.clone(), document_modules: self.document_modules.clone(), resolver: self.resolver.snapshot(), cache: Arc::new(self.cache.clone()), client_needs_file_uris_for_virtual_documents: self .client_needs_file_uris_for_virtual_documents, }) } fn completion_context(&self) -> completions::CompletionContext<'_> { completions::CompletionContext { config: &self.config, client: &self.client, module_registry: &self.module_registry, jsr_search_api: &self.jsr_search_api, npm_search_api: &self.npm_search_api, document_modules: &self.document_modules, resolver: self.resolver.as_ref(), } } pub fn update_tracing(&mut self) { let tracing = self .config .workspace_settings() .tracing .clone() .or_else(|| { std::env::var("DENO_LSP_TRACE").ok().map(|_| { super::trace::TracingConfig { enable: true, ..Default::default() } .into() }) }); { let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server .set_tracing_enabled(tracing.as_ref().is_some_and(|t| t.enabled())); } self._tracing = tracing.and_then(|conf| { if !conf.enabled() { return None; } lsp_log!("Initializing tracing subscriber: {:#?}", conf); let config = conf.into(); super::trace::init_tracing_subscriber(&config) .inspect_err(|e| { lsp_warn!("Error initializing tracing subscriber: {e:#}"); }) .ok() }); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn update_global_cache(&mut self) { let mark = self.performance.mark("lsp.update_global_cache"); let maybe_cache = self.config.workspace_settings().cache.as_ref(); let global_cache_url = maybe_cache.and_then(|cache_str| { if let Ok(url) = Url::from_file_path(cache_str) { Some(url) } else if let Some(root_url) = self.config.root_url() { root_url.join(cache_str).inspect_err(|err| lsp_warn!("Failed to resolve custom cache path: {err}")).ok() } else { lsp_warn!( "The configured cache path \"{cache_str}\" is not resolvable outside of a workspace.", ); None } }); self.cache = LspCache::new(global_cache_url); let deno_dir = self.cache.deno_dir(); let workspace_settings = self.config.workspace_settings(); let maybe_root_path = self .config .root_url() .and_then(|url| url_to_file_path(url).ok()); let root_cert_store = get_root_cert_store( &CliSys::default(), maybe_root_path, workspace_settings.certificate_stores.clone(), workspace_settings.tls_certificate.clone().map(CaData::File), ) .inspect_err(|err| lsp_warn!("Failed to load root cert store: {err}")) .unwrap_or_else(|_| RootCertStore::empty()); let root_cert_store_provider = Arc::new(LspRootCertStoreProvider(root_cert_store)); self.http_client_provider = Arc::new(HttpClientProvider::new( Some(root_cert_store_provider), workspace_settings .unsafely_ignore_certificate_errors .clone(), )); self.module_registry = ModuleRegistry::new( deno_dir.registries_folder_path(), self.http_client_provider.clone(), ); let workspace_settings = self.config.workspace_settings(); for (registry, enabled) in workspace_settings.suggest.imports.hosts.iter() { if *enabled { lsp_log!("Enabling import suggestions for: {}", registry); self.module_registry.enable(registry).await; } else { self.module_registry.disable(registry); } } self.jsr_search_api = CliJsrSearchApi::new(self.module_registry.file_fetcher.clone()); self.npm_search_api = CliNpmSearchApi::new( self.module_registry.file_fetcher.clone(), Arc::new(NpmVersionResolver { // todo(dsherret): the npm_search_api should probably be specific // to each workspace so that the link packages can be properly // hooked up link_packages: Default::default(), newest_dependency_date_options: Default::default(), overrides: Default::default(), trust_policy: Default::default(), }), ); self.performance.measure(mark); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn update_cache(&mut self) { let mark = self.performance.mark("lsp.update_cache"); self.cache.update_config(&self.config); self.performance.measure(mark); } pub fn update_debug_flag(&self) { let internal_debug = self.config.workspace_settings().internal_debug; super::logging::set_lsp_debug_flag(internal_debug) } pub fn check_semantic_tokens_capabilities(&mut self) { if self.registered_semantic_tokens_capabilities { return; } if !self .config .client_capabilities .text_document .as_ref() .and_then(|t| t.semantic_tokens.as_ref()) .and_then(|s| s.dynamic_registration) .unwrap_or_default() { return; } let exists_enabled_document = self .document_modules .documents .open_docs() .any(|doc| doc.is_diagnosable() && self.config.uri_enabled(&doc.uri)); if !exists_enabled_document { return; } self.task_queue.queue_task(Box::new(|ls| { spawn(async move { let register_options = serde_json::to_value(semantic_tokens_registration_options()).unwrap(); ls.client.when_outside_lsp_lock().register_capability(vec![Registration { id: "textDocument/semanticTokens".to_string(), method: "textDocument/semanticTokens".to_string(), register_options: Some(register_options.clone()), }]).await.inspect_err(|err| { lsp_warn!("Couldn't register capability for \"textDocument/semanticTokens\": {err}"); }).ok(); }); })); self.registered_semantic_tokens_capabilities = true; } fn is_using_push_based_diagnostics(&self) -> bool { self.force_push_based_diagnostics || !self.config.diagnostic_capable() } } // lspower::LanguageServer methods. This file's LanguageServer delegates to us. impl Inner { fn initialize( &mut self, params: InitializeParams, ) -> LspResult<InitializeResult> { lsp_log!("Starting Deno language server..."); let mark = self.performance.mark_with_args("lsp.initialize", ¶ms); // exit this process when the parent is lost if let Some(parent_pid) = params.process_id { parent_process_checker::start(parent_pid); } let version = format!( "{} ({}, {})", DENO_VERSION_INFO.deno, env!("PROFILE"), env!("TARGET") ); lsp_log!(" version: {}", version); if let Ok(path) = std::env::current_exe() { lsp_log!(" executable: {}", path.to_string_lossy()); } let server_info = ServerInfo { name: "deno-language-server".to_string(), version: Some(version), }; if let Some(client_info) = params.client_info { let client_name = client_info.name.to_lowercase(); self.client_needs_file_uris_for_virtual_documents = client_name == "neovim" || client_name == "nvim"; lsp_log!( "Connected to \"{}\" {}", client_info.name, client_info.version.unwrap_or_default(), ); } { let mut workspace_folders = vec![]; if let Some(folders) = params.workspace_folders { workspace_folders = folders .into_iter() .map(|folder| { let mut url = uri_to_url(&folder.uri); if !url.path().ends_with('/') && let Ok(mut path_segments) = url.path_segments_mut() { path_segments.push(""); } (Arc::new(url), folder) }) .collect(); } #[allow( deprecated, reason = "rootUri is deprecated by the LSP spec. If it's specified, merge it into workspace_folders." )] if let Some(root_uri) = params.root_uri && !workspace_folders.iter().any(|(_, f)| f.uri == root_uri) { let mut root_url = uri_to_url(&root_uri); let name = root_url .path_segments() .and_then(|mut s| s.next_back()) .unwrap_or_default() .to_string(); if !root_url.path().ends_with('/') && let Ok(mut path_segments) = root_url.path_segments_mut() { path_segments.push(""); } workspace_folders.insert( 0, ( Arc::new(root_url), WorkspaceFolder { uri: root_uri, name, }, ), ); } self.config.set_workspace_folders(workspace_folders); if let Some(options) = params.initialization_options { let settings = WorkspaceSettings::from_initialization_options(options); self.force_push_based_diagnostics = settings.force_push_based_diagnostics; self.config.set_workspace_settings(settings, vec![]); } self.config.set_client_capabilities(params.capabilities); } if self.is_using_push_based_diagnostics() { let mut diagnostics_server = DiagnosticsServer::new( self.client.clone(), self.performance.clone(), self.ts_server.clone(), ); diagnostics_server.start(); self.diagnostics_server = Some(diagnostics_server); } { let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server .set_inspector_server_addr(self.config.internal_inspect().to_address()); } self.update_tracing(); self.update_debug_flag(); let mut capabilities = capabilities::server_capabilities(&self.config.client_capabilities); if self.force_push_based_diagnostics { capabilities.diagnostic_provider = None; } if capabilities.semantic_tokens_provider.is_some() { self.registered_semantic_tokens_capabilities = true; } self.performance.measure(mark); Ok(InitializeResult { capabilities, server_info: Some(server_info), offset_encoding: None, }) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn walk_workspace(config: &Config) -> (IndexSet<PathBuf>, bool) { if !config.workspace_capable() { log::debug!("Skipped workspace walk due to client incapability."); return (Default::default(), false); } let mut workspace_files = IndexSet::default(); let entry_limit = 1000; let mut pending = VecDeque::new(); let mut entry_count = 0; let mut roots = config .workspace_folders .iter() .filter_map(|(_, f)| uri_to_file_path(&f.uri).ok()) .collect::<Vec<_>>(); roots.sort(); let roots = roots .iter() .enumerate() .filter(|(i, root)| *i == 0 || !root.starts_with(&roots[i - 1])) .map(|(_, r)| r.clone()) .collect::<Vec<_>>(); let mut root_ancestors = BTreeSet::new(); for root in roots { for ancestor in root.ancestors().skip(1) { if root_ancestors.insert(ancestor.to_path_buf()) { break; } } if let Ok(read_dir) = std::fs::read_dir(&root) { pending.push_back((root, read_dir)); } } for root_ancestor in root_ancestors { for deno_json in ["deno.json", "deno.jsonc"] { let path = root_ancestor.join(deno_json); if path.exists() { workspace_files.insert(path); } } } while let Some((parent_path, read_dir)) = pending.pop_front() { // Sort entries from each dir for consistency across operating systems. let mut dir_files = BTreeSet::new(); let mut dir_subdirs = BTreeMap::new(); for entry in read_dir { let Ok(entry) = entry else { continue; }; if entry_count >= entry_limit { return (workspace_files, true); } entry_count += 1; let path = parent_path.join(entry.path()); let Ok(file_type) = entry.file_type() else { continue; }; let Some(file_name) = path.file_name() else { continue; }; if config.settings.path_enabled(&path) == Some(false) { continue; } if file_type.is_dir() { let dir_name = file_name.to_string_lossy().to_lowercase(); // We ignore these directories by default because there is a // high likelihood they aren't relevant. Someone can opt-into // them by specifying one of them as an enabled path. if matches!( dir_name.as_str(), "vendor" | "coverage" | "node_modules" | ".git" ) { continue; } // ignore cargo target directories for anyone using Deno with Rust if dir_name == "target" && path .parent() .map(|p| p.join("Cargo.toml").exists()) .unwrap_or(false) { continue; } if let Ok(read_dir) = std::fs::read_dir(&path) { dir_subdirs.insert(path, read_dir); } } else if file_type.is_file() || file_type.is_symlink() && std::fs::metadata(&path) .ok() .map(|m| m.is_file()) .unwrap_or(false) { if file_name.to_string_lossy().contains(".min.") { continue; } let media_type = MediaType::from_path(&path); match media_type { MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs | MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Json | MediaType::Jsonc | MediaType::Tsx => {} MediaType::Wasm | MediaType::SourceMap | MediaType::Css | MediaType::Html | MediaType::Json5 | MediaType::Markdown | MediaType::Sql | MediaType::Unknown => { continue; } } dir_files.insert(path); } } workspace_files.extend(dir_files); pending.extend(dir_subdirs); } (workspace_files, false) } fn refresh_workspace_files(&mut self) { let enable_settings_hash = self.config.settings.enable_settings_hash(); if self.workspace_files_hash == enable_settings_hash { return; } let (workspace_files, hit_limit) = Self::walk_workspace(&self.config); if hit_limit { let document_preload_limit = self.config.workspace_settings().document_preload_limit; if document_preload_limit == 0 { log::debug!("Skipped document preload."); } else { lsp_warn!( concat!( "Hit the language server document preload limit of {} file system entries. ", "You may want to use the \"deno.enablePaths\" configuration setting to only have Deno ", "partially enable a workspace or increase the limit via \"deno.documentPreloadLimit\". ", "In cases where Deno ends up using too much memory, you may want to lower the limit." ), document_preload_limit, ); } } self.workspace_files = Arc::new(workspace_files); self.workspace_files_hash = enable_settings_hash; } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn refresh_config_tree(&mut self, reason: &str) { let file_fetcher = create_cli_file_fetcher( Arc::new(deno_runtime::deno_web::BlobStore::default()) as Arc<dyn deno_runtime::deno_web::BlobStoreTrait>, GlobalOrLocalHttpCache::Global(self.cache.global().clone()), self.http_client_provider.clone(), MemoryFilesRc::default(), CliSys::default(), CreateCliFileFetcherOptions { allow_remote: true, cache_setting: CacheSetting::RespectHeaders, download_log_level: super::logging::lsp_log_level(), progress_bar: None, }, ); let file_fetcher = Arc::new(file_fetcher); self .config .tree .refresh( &self.config.settings, &self.workspace_files, &file_fetcher, &self.http_client_provider, self.cache.deno_dir(), reason, ) .await; self .client .send_did_refresh_deno_configuration_tree_notification( self.config.tree.to_did_refresh_params(), ); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn refresh_compiler_options_resolver(&mut self) { let compiler_options_resolver = LspCompilerOptionsResolver::new( &self.config, &self.resolver, Some(&self.compiler_options_resolver), ); self.compiler_options_resolver = Arc::new(compiler_options_resolver); // TODO(nayeemrmn): This represents a circular dependency between // `LspCompilerOptionsResolver` and `LspResolver` because the former uses // the node resolver to resolve `extends` in tsconfig. Break out the node // resolver from `LspResolver`. self .resolver .set_compiler_options_resolver(&self.compiler_options_resolver.inner); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn refresh_linter_resolver(&mut self) { self.linter_resolver = Arc::new(LspLinterResolver::new( &self.config, &self.compiler_options_resolver, &self.resolver, )); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn refresh_resolver(&mut self) { self.resolver = Arc::new( LspResolver::from_config( &self.config, &self.cache, Some(&self.http_client_provider), ) .await, ); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn refresh_documents_config(&mut self) { self.document_modules.update_config( &self.config, &self.compiler_options_resolver, &self.resolver, &self.cache, &self.workspace_files, ); // refresh the npm specifiers because it might have discovered // a @types/node package and now's a good time to do that anyway self.refresh_dep_info(); self.project_changed(vec![], ProjectScopesChange::Config); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn did_open(&mut self, params: DidOpenTextDocumentParams) { let mark = self.performance.mark_with_args("lsp.did_open", ¶ms); // `deno:` documents are read-only and should only be handled as server // documents. if params .text_document .uri .scheme() .as_str() .eq_ignore_ascii_case("deno") { return; } let language_id = params .text_document .language_id .parse() .unwrap_or_else(|err| { error!("{:#}", err); LanguageId::Unknown }); if language_id == LanguageId::Unknown { lsp_warn!( "Unsupported language id \"{}\" received for document \"{}\".", params.text_document.language_id, params.text_document.uri.as_str() ); } let document = self.document_modules.open_document( params.text_document.uri, params.text_document.version, language_id, params.text_document.text.into(), None, ); if document.is_diagnosable() { self.check_semantic_tokens_capabilities(); self.refresh_dep_info(); self.project_changed( vec![(Document::Open(document), ChangeKind::Opened)], ProjectScopesChange::None, ); self.send_diagnostics_update(); self.send_testing_update(); } self.performance.measure(mark); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn did_change_batched(&mut self, batch_queue: Arc<DidChangeBatchQueue>) { let mark = self .performance .mark_with_args("lsp.did_change_batched", &batch_queue.uri); // `deno:` documents are read-only and should only be handled as server // documents. if batch_queue .uri .scheme() .as_str() .eq_ignore_ascii_case("deno") { batch_queue.clear(); return; } let mut document = None; while let Some(entry) = batch_queue.dequeue() { match self.document_modules.change_document( &batch_queue.uri, entry.version, entry.content_changes, ) { Ok(doc) => { document = Some(doc); } Err(err) => { error!("{:#}", err); return; } } } if let Some(document) = document && document.is_diagnosable() { self.refresh_dep_info(); self.project_changed( vec![(Document::Open(document), ChangeKind::Modified)], ProjectScopesChange::None, ); self.send_diagnostics_update(); self.send_testing_update(); } self.performance.measure(mark); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_save(&self, params: DidSaveTextDocumentParams) { let _mark = self.performance.measure_scope("lsp.did_save"); let Ok(Some(document)) = self .get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, ) .inspect_err(|err| { lsp_warn!("{:#}", err); }) else { return; }; let has_no_cache_diagnostics = async || { if let Some(diagnostics_server) = &self.diagnostics_server { diagnostics_server .state .has_no_cache_diagnostics(document.uri()) } else { let Ok(Some(module)) = self.get_primary_module(&document) else { return false; }; self .get_module_diagnostics(&module, &Default::default()) .await .ok() .iter() .flat_map(|d| d.iter()) .any(|d| { let Some(NumberOrString::String(code)) = &d.code else { return false; }; matches!( code.as_str(), "no-cache" | "not-installed-jsr" | "not-installed-npm" ) }) } }; if !self .config .workspace_settings_for_uri(document.uri()) .cache_on_save || !self.config.uri_enabled(document.uri()) || !has_no_cache_diagnostics().await { return; } let Ok(Some(module)) = self.get_primary_module(&document).inspect_err(|err| { lsp_warn!("{:#}", err); }) else { return; }; if module.specifier.scheme() != "file" || module.specifier.as_str().contains("/node_modules/") { return; } self.task_queue.queue_task(Box::new(|ls: LanguageServer| { spawn(async move { if let Err(err) = ls .cache(vec![], module.specifier.as_ref().clone(), false) .await { lsp_warn!( "Failed to cache \"{}\" on save: {:#}", &module.specifier, err ); } }); })); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn refresh_dep_info(&mut self) { let dep_info_by_scope = self.document_modules.dep_info_by_scope(); self.resolver.set_dep_info_by_scope(&dep_info_by_scope); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn did_close(&mut self, params: DidCloseTextDocumentParams) { let mark = self.performance.mark_with_args("lsp.did_close", ¶ms); // `deno:` documents are read-only and should only be handled as server // documents. if params .text_document .uri .scheme() .as_str() .eq_ignore_ascii_case("deno") { return; } if let Some(diagnostics_server) = &self.diagnostics_server { diagnostics_server.state.clear(¶ms.text_document.uri); } let document = match self .document_modules .close_document(¶ms.text_document.uri) { Ok(doc) => doc, Err(err) => { error!("{:#}", err); return; } }; if document.is_diagnosable() { self.refresh_dep_info(); self.project_changed( vec![(Document::Open(document), ChangeKind::Closed)], ProjectScopesChange::None, ); self.send_diagnostics_update(); self.send_testing_update(); } self.performance.measure(mark); } fn notebook_did_open(&mut self, params: DidOpenNotebookDocumentParams) { let _mark = self.performance.measure_scope("lsp.notebook_did_open"); let documents = self.document_modules.open_notebook_document( params.notebook_document.uri, params.cell_text_documents, ); let diagnosable_documents = documents .into_iter() .filter(|d| d.is_diagnosable()) .collect::<Vec<_>>(); if !diagnosable_documents.is_empty() { self.check_semantic_tokens_capabilities(); self.refresh_dep_info(); self.project_changed( diagnosable_documents .into_iter() .map(|d| (Document::Open(d), ChangeKind::Opened)) .collect(), ProjectScopesChange::OpenNotebooks, ); self.send_diagnostics_update(); } } fn notebook_did_change(&mut self, params: DidChangeNotebookDocumentParams) { let _mark = self.performance.measure_scope("lsp.notebook_did_change"); let Some(cells) = params.change.cells else { return; }; let documents = self.document_modules.change_notebook_document( ¶ms.notebook_document.uri, cells.structure, cells.text_content, ); let diagnosable_documents = documents .into_iter() .filter(|(d, _)| d.is_diagnosable()) .collect::<Vec<_>>(); if !diagnosable_documents.is_empty() { self.refresh_dep_info(); self.project_changed( diagnosable_documents .into_iter() .map(|(d, k)| (Document::Open(d), k)) .collect(), ProjectScopesChange::None, ); self.send_diagnostics_update(); } } async fn notebook_did_save(&self, params: DidSaveNotebookDocumentParams) { let _mark = self.performance.measure_scope("lsp.notebook_did_save"); let Some(cell_uris) = self .document_modules .documents .cells_by_notebook_uri() .get(¶ms.notebook_document.uri) .cloned() else { lsp_warn!( "The URI \"{}\" does not refer to an open notebook document.", params.notebook_document.uri.as_str() ); return; }; for cell_uri in cell_uris { self .did_save(DidSaveTextDocumentParams { text_document: TextDocumentIdentifier { uri: cell_uri.as_ref().clone(), }, text: None, }) .await; } } fn notebook_did_close(&mut self, params: DidCloseNotebookDocumentParams) { let _mark = self.performance.measure_scope("lsp.notebook_did_close"); let documents = self .document_modules .close_notebook_document(¶ms.notebook_document.uri); let diagnosable_documents = documents .into_iter() .filter(|d| d.is_diagnosable()) .collect::<Vec<_>>(); if !diagnosable_documents.is_empty() { self.refresh_dep_info(); self.project_changed( diagnosable_documents .into_iter() .map(|d| (Document::Open(d), ChangeKind::Closed)) .collect(), ProjectScopesChange::OpenNotebooks, ); self.send_diagnostics_update(); } } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_change_configuration( &mut self, params: DidChangeConfigurationParams, ) { if !self.config.workspace_configuration_capable() { let config = params.settings.as_object().map(|settings| { let deno = serde_json::to_value(settings.get(SETTINGS_SECTION)).unwrap(); let javascript = serde_json::to_value(settings.get("javascript")).unwrap(); let typescript = serde_json::to_value(settings.get("typescript")).unwrap(); WorkspaceSettings::from_raw_settings(deno, javascript, typescript) }); if let Some(settings) = config { self.config.set_workspace_settings(settings, vec![]); } }; // TODO(nathanwhit): allow updating after startup, needs work to set thread local collector on tsc thread // self.update_tracing(); self.check_semantic_tokens_capabilities(); self.update_debug_flag(); self.update_global_cache().await; self.refresh_workspace_files(); self.refresh_config_tree("did_change_configuration").await; self.update_cache(); self.refresh_resolver().await; self.refresh_compiler_options_resolver(); self.refresh_linter_resolver(); self.refresh_documents_config(); self.send_diagnostics_update(); self.send_testing_update(); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip(self)))] async fn did_change_watched_files( &mut self, params: DidChangeWatchedFilesParams, ) { let mark = self .performance .mark_with_args("lsp.did_change_watched_files", ¶ms); let changes = params .changes .into_iter() .map(|e| (uri_to_url(&e.uri), e)) .collect::<Vec<_>>(); // Evict cached on-disk documents whose source files were changed or // deleted on disk. Without this, modules importing them would see the // stale content the LSP last read (issue #12813). Open documents are owned // by the editor — leave them alone. let mut server_doc_changes = Vec::new(); for (specifier, event) in &changes { if specifier.scheme() != "file" || specifier.path().contains("/node_modules/") { continue; } let change_kind = match event.typ { FileChangeType::DELETED => ChangeKind::Closed, FileChangeType::CHANGED => ChangeKind::Modified, _ => continue, }; let Some(document) = self.document_modules.documents.inspect(&event.uri) else { continue; }; if document.open().is_some() { continue; } self .document_modules .documents .remove_server_doc(&event.uri); server_doc_changes.push((document, change_kind)); } let has_config_changes = changes.iter().any(|(specifier, _)| { let path = specifier.path(); !path.contains("/node_modules/") && (path.ends_with("/deno.json") || path.ends_with("/deno.jsonc") || path.ends_with("/package.json") || path.ends_with("/tsconfig.json")) || path.ends_with("/node_modules/.package-lock.json") || path.ends_with("/node_modules/.yarn-integrity.json") || path.ends_with("/node_modules/.modules.yaml") || path.ends_with("/node_modules/.deno/.setup-cache.bin") || self.config.tree.is_watched_file(specifier) || self.compiler_options_resolver.is_watched_file(specifier) }); if has_config_changes { let mut deno_config_changes = IndexSet::with_capacity(changes.len()); let mut changed_deno_json = false; deno_config_changes.extend(changes.iter().filter_map(|(s, e)| { self.config.tree.watched_file_type(s).and_then(|t| { let configuration_type = match t.1 { ConfigWatchedFileType::DenoJson => { changed_deno_json = true; lsp_custom::DenoConfigurationType::DenoJson } ConfigWatchedFileType::PackageJson => { lsp_custom::DenoConfigurationType::PackageJson } _ => return None, }; Some(lsp_custom::DenoConfigurationChangeEvent { scope_uri: url_to_uri(t.0).ok()?, file_uri: e.uri.clone(), typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type( e.typ, ), configuration_type, }) }) })); self.workspace_files_hash = 0; self.refresh_workspace_files(); self.refresh_config_tree("did_change_watched_files").await; self.update_cache(); self.refresh_resolver().await; self.refresh_compiler_options_resolver(); self.refresh_linter_resolver(); self.refresh_documents_config(); let mut project_changes = changes .iter() .filter_map(|(_, e)| { let document = self.document_modules.documents.inspect(&e.uri)?; Some((document, ChangeKind::Modified)) }) .collect::<Vec<_>>(); project_changes.extend(server_doc_changes); self.project_changed(project_changes, ProjectScopesChange::None); let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); if !self.is_using_push_based_diagnostics() && self.config.diagnostic_refresh_capable() { self.client.refresh_diagnostics(); } self.send_testing_update(); deno_config_changes.extend(changes.iter().filter_map(|(s, e)| { self.config.tree.watched_file_type(s).and_then(|t| { let configuration_type = match t.1 { ConfigWatchedFileType::DenoJson => { lsp_custom::DenoConfigurationType::DenoJson } ConfigWatchedFileType::PackageJson => { lsp_custom::DenoConfigurationType::PackageJson } _ => return None, }; Some(lsp_custom::DenoConfigurationChangeEvent { scope_uri: url_to_uri(t.0).ok()?, file_uri: e.uri.clone(), typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type( e.typ, ), configuration_type, }) }) })); if !deno_config_changes.is_empty() { self.client.send_did_change_deno_configuration_notification( lsp_custom::DidChangeDenoConfigurationNotificationParams { changes: deno_config_changes.into_iter().collect(), }, ); } } else if !server_doc_changes.is_empty() { // Source files changed/were deleted on disk but no config changes — // bump the project version, refresh diagnostics, and clear stale TSC // script caches so dependents pick up the new state. self.project_changed(server_doc_changes, ProjectScopesChange::None); let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); if !self.is_using_push_based_diagnostics() && self.config.diagnostic_refresh_capable() { self.client.refresh_diagnostics(); } self.send_testing_update(); } self.performance.measure(mark); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn document_symbol( &self, params: DocumentSymbolParams, token: &CancellationToken, ) -> LspResult<Option<DocumentSymbolResponse>> { let mark = self .performance .mark_with_args("lsp.document_symbol", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let response = self .ts_server .provide_document_symbols(&module, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Error getting document symbols for \"{}\" from TypeScript: {:#}", &module.specifier, err, ); LspError::internal_error() } })?; self.performance.measure(mark); Ok(response) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn formatting( &self, params: DocumentFormattingParams, _token: &CancellationToken, ) -> LspResult<Option<Vec<TextEdit>>> { let mark = self.performance.mark_with_args("lsp.formatting", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Ignore, Exists::Enforce, Diagnosable::Ignore, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let fmt_config = self.config.tree.fmt_config_for_specifier(&module.specifier); // Untitled files are exempt from enabled-checks because they tend not to // have meaningful paths, and they won't be auto-formatted on save anyway. let is_untitled = params .text_document .uri .scheme() .as_str() .eq_ignore_ascii_case("untitled"); if !is_untitled && !fmt_config.files.matches_specifier(&module.specifier) { return Ok(None); } let file_path = url_to_file_path(&module.specifier) .or_else(|_| uri_to_file_path(document.uri())) .map_err(|err| { lsp_warn!("{:#}", err); LspError::invalid_request() })?; // spawn a blocking task to allow doing other work while this is occurring let text_edits = deno_core::unsync::spawn_blocking({ let mut fmt_options = fmt_config.options.clone(); let config_data = self.config.tree.data_for_specifier(&module.specifier); #[allow( clippy::nonminimal_bool, reason = "clippy's suggestion is more confusing" )] if !config_data.is_some_and(|d| d.maybe_deno_json().is_some()) { fmt_options.use_tabs = Some(!params.options.insert_spaces); fmt_options.indent_width = Some(params.options.tab_size as u8); } let unstable_options = UnstableFmtOptions { component: config_data .map(|d| d.unstable.contains("fmt-component")) .unwrap_or(false), sql: config_data .map(|d| d.unstable.contains("fmt-sql")) .unwrap_or(false), }; move || { let format_result = match &module .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) { Some(Ok(parsed_source)) => { format_parsed_source(parsed_source, &fmt_options, &unstable_options) } Some(Err(err)) => Err(anyhow!("{:#}", err)), None => { let ext = document.open().and_then(|d| { d.language_id.as_extension().map(|s| s.to_string()) }); format_file( &file_path, &crate::tools::fmt::FileContents { text: Cow::Borrowed(document.text().as_ref()), had_bom: false, }, &fmt_options, &unstable_options, ext, ) } }; match format_result { Ok(Some(new_text)) => Some(text::get_edits( &document.text(), &new_text, &module.line_index, )), Ok(None) => Some(Vec::new()), Err(err) => { lsp_warn!("Format error: {:#}", err); None } } } }) .await .unwrap(); self.performance.measure(mark); if let Some(text_edits) = text_edits { if text_edits.is_empty() { Ok(None) } else { Ok(Some(text_edits)) } } else { Ok(None) } } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn hover( &self, params: HoverParams, token: &CancellationToken, ) -> LspResult<Option<Hover>> { let mark = self.performance.mark_with_args("lsp.hover", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let hover = if let Some((specifier_text, dep, range)) = module .dependency_at_position(¶ms.text_document_position_params.position) { let dep_module = dep.get_code().and_then(|s| { self.document_modules.module_for_specifier( s, module.scope.as_deref(), Some(&module.compiler_options_key), ) }); let dep_types_dependency = dep_module.as_ref().map(|m| { m.types_dependency .as_ref() .map(|d| &d.dependency) .unwrap_or(&Resolution::None) }); let value = match ( dep.maybe_code.is_none(), dep.maybe_type.is_none(), &dep_types_dependency, ) { (false, false, None) => format!( "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", self .resolution_to_hover_text(&dep.maybe_code, module.scope.as_deref()), self .resolution_to_hover_text(&dep.maybe_type, module.scope.as_deref()), ), (false, false, Some(types_dep)) if !types_dep.is_none() => format!( "**Resolved Dependency**\n\n**Code**: {}\n**Types**: {}\n**Import Types**: {}\n", self .resolution_to_hover_text(&dep.maybe_code, module.scope.as_deref()), self .resolution_to_hover_text(&dep.maybe_type, module.scope.as_deref()), self.resolution_to_hover_text(types_dep, module.scope.as_deref()), ), (false, false, Some(_)) => format!( "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", self .resolution_to_hover_text(&dep.maybe_code, module.scope.as_deref()), self .resolution_to_hover_text(&dep.maybe_type, module.scope.as_deref()), ), (false, true, Some(types_dep)) if !types_dep.is_none() => format!( "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", self .resolution_to_hover_text(&dep.maybe_code, module.scope.as_deref()), self.resolution_to_hover_text(types_dep, module.scope.as_deref()), ), (false, true, _) => format!( "**Resolved Dependency**\n\n**Code**: {}\n", self .resolution_to_hover_text(&dep.maybe_code, module.scope.as_deref()), ), (true, false, _) => format!( "**Resolved Dependency**\n\n**Types**: {}\n", self .resolution_to_hover_text(&dep.maybe_type, module.scope.as_deref()), ), (true, true, _) => return Ok(None), }; let value = if let Some(import_map_hover) = dep .get_code() .or_else(|| dep.maybe_type.maybe_specifier()) .and_then(|code| { self .resolver .get_scoped_resolver(module.scope.as_deref()) .import_map_hover(specifier_text, code, &module.specifier) }) { format!("{value}\n{}", import_map_hover_text(&import_map_hover)) } else { value }; let value = if let Some(docs) = self.module_registry.get_hover(dep).await { format!("{value}\n\n---\n\n{docs}") } else { value }; Some(Hover { contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, value, }), range: Some(to_lsp_range(range)), }) } else { self .ts_server .provide_hover( &module, params.text_document_position_params.position, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get hover from TypeScript: {:#}", err); LspError::internal_error() } })? }; self.performance.measure(mark); Ok(hover) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn resolution_to_hover_text( &self, resolution: &Resolution, scope: Option<&Url>, ) -> String { match resolution { Resolution::Ok(resolved) => { let specifier = &resolved.specifier; let format = |scheme: &str, rest: &str| -> String { format!("{}​{}", scheme, rest).replace('@', "​@") }; match specifier.scheme() { "data" => "_(a data url)_".to_string(), "blob" => "_(a blob url)_".to_string(), "file" => format( &specifier[..url::Position::AfterScheme], &to_percent_decoded_str(&specifier[url::Position::AfterScheme..]), ), _ => { let mut result = format( &specifier[..url::Position::AfterScheme], &specifier[url::Position::AfterScheme..], ); if let Ok(jsr_req_ref) = JsrPackageReqReference::from_specifier(specifier) { let scoped_resolver = self.resolver.get_scoped_resolver(scope); if let Some(url) = scoped_resolver.jsr_to_resource_url(&jsr_req_ref) { result = format!("{result} (<{url}>)"); } } result } } } Resolution::Err(_) => "_[errored]_".to_string(), Resolution::None => "_[missing]_".to_string(), } } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn code_action( &self, params: CodeActionParams, token: &CancellationToken, ) -> LspResult<Option<CodeActionResponse>> { let mark = self.performance.mark_with_args("lsp.code_action", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Ignore, )? else { return Ok(None); }; let url = uri_to_url(document.uri()); if matches!( self.config.tree.watched_file_type(&url), Some((_, ConfigWatchedFileType::ImportMap)) ) { let code_action_disabled_capable = self.config.code_action_disabled_capable(); let actions = params .context .diagnostics .iter() .filter(|diagnostic| { diagnostic.source.as_deref() == Some("deno") && diagnostics::DenoDiagnostic::is_fixable(diagnostic) }) .filter_map(|diagnostic| { DenoDiagnostic::get_code_action(document.uri(), &url, diagnostic) .inspect_err(|err| { lsp_warn!( "Error getting deno code action: {:#}\nDiagnostic: {:#?}", err, diagnostic ); }) .ok() }) .map(CodeActionOrCommand::CodeAction) .filter(|a| { code_action_disabled_capable || matches!(a, CodeActionOrCommand::CodeAction(ca) if ca.disabled.is_none()) }) .collect::<Vec<_>>(); self.performance.measure(mark); return Ok(if actions.is_empty() { None } else { Some(actions) }); } let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let mut deno_actions = Vec::new(); let mut deno_lint_actions = Vec::new(); let mut deno_test_actions = Vec::new(); let mut includes_no_cache = false; if params.context.only.as_ref().is_none_or(|only| { only.iter().any(|kind| { INFERRED_TYPE_CODE_ACTION_KIND .as_str() .starts_with(kind.as_str()) }) }) { let snapshot = self.snapshot(); let response = self .ts_server .provide_inferred_type(&module, params.range.start, snapshot, token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to get inferred type code action from TypeScript: {:#}", err ); LspError::internal_error() } })?; if let Some(response) = response { deno_actions.push(CodeAction { title: "Copy inferred type".to_string(), kind: Some(INFERRED_TYPE_CODE_ACTION_KIND.clone()), command: Some(Command { title: "Copy inferred type".to_string(), command: INFERRED_TYPE_COMMAND.to_string(), arguments: Some(vec![ serde_json::to_value(lsp_custom::InferredTypeParams { text_document: TextDocumentIdentifier { uri: params.text_document.uri.clone(), }, position: params.range.start, }) .map_err(|err| { error!( "Failed to serialize inferred type command params: {:#}", err ); LspError::internal_error() })?, serde_json::to_value(response).map_err(|err| { error!( "Failed to serialize inferred type command response: {:#}", err ); LspError::internal_error() })?, ]), }), ..Default::default() }); } } if self.config.specifier_enabled_for_test(&module.specifier) && let Some(Ok(parsed_source)) = &module .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) && params.context.only.as_ref().is_none_or(|only| { only.iter().any(|kind| { CodeActionKind::REFACTOR_REWRITE .as_str() .starts_with(kind.as_str()) }) }) { deno_test_actions.extend(collect_test_code_actions( document.uri(), parsed_source, params.range, )); } let file_diagnostics = async { if let Some(diagnostics_server) = &self.diagnostics_server { diagnostics_server.state.ts_diagnostics(document.uri()) } else { self .get_module_diagnostics(&module, token) .await .unwrap_or_default() } } .shared(); for diagnostic in ¶ms.context.diagnostics { match diagnostic.source.as_deref() { Some("deno") => { if !diagnostics::DenoDiagnostic::is_fixable(diagnostic) { continue; } if diagnostic.code == Some(NumberOrString::String("no-cache".to_string())) || diagnostic.code == Some(NumberOrString::String("not-installed-jsr".to_string())) || diagnostic.code == Some(NumberOrString::String("not-installed-npm".to_string())) { includes_no_cache = true; } match DenoDiagnostic::get_code_action( document.uri(), &module.specifier, diagnostic, ) { Ok(action) => { deno_actions.push(action); } Err(err) => { lsp_warn!( "Error getting deno code action: {:#}\nDiagnostic: {:#?}", err, diagnostic ); } } } Some("deno-lint") => { if diagnostic.code.is_none() { continue; } match get_deno_lint_code_actions(document.uri(), &module, diagnostic) { Ok(actions) => { deno_lint_actions.extend(actions); } Err(err) => { lsp_warn!( "Error getting deno lint code actions: {:#}\nDiagnostic: {:#?}", err, diagnostic ); } } } _ => {} } if includes_no_cache { let no_cache_diagnostics = if let Some(diagnostics_server) = &self.diagnostics_server { diagnostics_server .state .no_cache_diagnostics(document.uri()) } else { Arc::new( file_diagnostics .clone() .await .iter() .filter(|d| { let Some(NumberOrString::String(code)) = &d.code else { return false; }; matches!( code.as_str(), "no-cache" | "not-installed-jsr" | "not-installed-npm" ) }) .cloned() .collect::<Vec<_>>(), ) }; let uncached_deps = no_cache_diagnostics .iter() .filter_map(|d| { let data = serde_json::from_value::<DiagnosticDataSpecifier>( d.data.clone().into(), ) .ok()?; Some(data.specifier) }) .collect::<HashSet<_>>(); if uncached_deps.len() > 1 { deno_actions.push(CodeAction { title: "Cache all dependencies of this module.".to_string(), kind: Some(CodeActionKind::QUICKFIX), diagnostics: Some(no_cache_diagnostics.as_ref().clone()), command: Some(Command { title: "".to_string(), command: "deno.cache".to_string(), arguments: Some(vec![json!([]), json!(&module.specifier)]), }), ..Default::default() }); } } } let ts_actions = self .ts_server .provide_code_actions( &module, params.range, ¶ms.context, file_diagnostics, !deno_actions.is_empty(), self, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Error getting code actions for \"{}\" from TypeScript: {:#}", &module.specifier, err, ); LspError::internal_error() } })? .unwrap_or_default(); let code_action_disabled_capable = self.config.code_action_disabled_capable(); let actions = deno_actions.into_iter().map(CodeActionOrCommand::CodeAction).chain(deno_test_actions.into_iter().map(CodeActionOrCommand::CodeAction)).chain(ts_actions).chain(deno_lint_actions.into_iter().map(CodeActionOrCommand::CodeAction)).filter(|a| code_action_disabled_capable || matches!(a, CodeActionOrCommand::CodeAction(ca) if ca.disabled.is_none())).collect::<Vec<_>>(); let response = if actions.is_empty() { None } else { Some(actions) }; self.performance.measure(mark); Ok(response) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn code_action_resolve( &self, params: CodeAction, token: &CancellationToken, ) -> LspResult<CodeAction> { let mark = self .performance .mark_with_args("lsp.code_action_resolve", ¶ms); let TsServer::Js(ts_server) = self.ts_server.as_ref(); let (Some(kind), Some(data)) = (params.kind.clone(), params.data.clone()) else { return Ok(params); }; let result = if kind.as_str().starts_with(CodeActionKind::QUICKFIX.as_str()) { let code_action_data: CodeActionData = from_value(data).map_err(|err| { error!("Unable to decode code action data: {:#}", err); LspError::invalid_params("The CodeAction's data is invalid.") })?; let Some(document) = self.get_document( &code_action_data.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(params); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(params); }; let snapshot = self.snapshot(); let combined_code_actions = ts_server .get_combined_code_fix( snapshot.clone(), &module, &code_action_data.fix_id, token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get combined fix from TypeScript: {:#}", err); LspError::internal_error() } })?; if combined_code_actions.commands.is_some() { error!("Deno does not support code actions with commands."); return Err(LspError::invalid_request()); } let changes = if code_action_data.fix_id == "fixMissingImport" { fix_ts_import_changes( &combined_code_actions.changes, &module, self, token, ) .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to fix import changes: {:#}", err); LspError::internal_error() } })? } else { combined_code_actions.changes }; let mut code_action = params; code_action.edit = ts_changes_to_edit(&changes, &module, self).map_err(|err| { error!("Unable to convert changes to edits: {:#}", err); LspError::internal_error() })?; code_action } else if let Some(kind_suffix) = kind .as_str() .strip_prefix(CodeActionKind::REFACTOR.as_str()) { let mut code_action = params; let action_data: refactor::RefactorCodeActionData = from_value(data) .map_err(|err| { error!("Unable to decode code action data: {:#}", err); LspError::invalid_params("The CodeAction's data is invalid.") })?; let Some(document) = self.get_document( &action_data.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(code_action); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(code_action); }; let snapshot = self.snapshot(); let refactor_edit_info = ts_server .get_edits_for_refactor( snapshot.clone(), &module, module.line_index.offset_tsc(action_data.range.start)? ..module.line_index.offset_tsc(action_data.range.end)?, action_data.refactor_name.clone(), action_data.action_name.clone(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to get refactor edit info from TypeScript: {:#}", err ); LspError::invalid_request() } }); match refactor_edit_info { Ok(mut refactor_edit_info) => { if kind_suffix == ".rewrite.function.returnType" || kind_suffix == ".move.newFile" { refactor_edit_info.edits = fix_ts_import_changes( &refactor_edit_info.edits, &module, self, token, ) .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to fix import changes: {:#}", err); LspError::internal_error() } })? } code_action.command = refactor_edit_info.to_rename_command(&module, &snapshot); code_action.edit = refactor_edit_info .to_workspace_edit(&module, &snapshot, token) .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { lsp_warn!("Unable to convert refactor edit info: {:#}", err); LspError::internal_error() } })?; } Err(err) => { if token.is_cancelled() { return Err(LspError::request_cancelled()); } else { lsp_warn!( "Unable to get refactor edit info from TypeScript: {:#}\nCode action data: {:#}", err, json!(&action_data) ); } } } code_action } else { // The code action doesn't need to be resolved params }; self.performance.measure(mark); Ok(result) } pub fn get_ts_response_import_mapper( &self, module: &DocumentModule, ) -> TsResponseImportMapper<'_> { TsResponseImportMapper::new( &self.document_modules, module.scope.clone(), &self.resolver, { let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server.specifier_map.clone() }, ) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn code_lens( &self, params: CodeLensParams, token: &CancellationToken, ) -> LspResult<Option<Vec<CodeLens>>> { let mark = self.performance.mark_with_args("lsp.code_lens", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let settings = self .config .workspace_settings_for_specifier(&module.specifier); let mut code_lenses = Vec::new(); if settings.code_lens.test && self.config.specifier_enabled_for_test(&module.specifier) && let Some(Ok(parsed_source)) = &module .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) { code_lenses.extend( code_lens::collect_test(&module.specifier, parsed_source, token) .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Error getting test code lenses for \"{}\": {:#}", &module.specifier, err ); LspError::internal_error() } })?, ); } let ts_code_lenses = self .ts_server .provide_code_lenses(&module, &settings.code_lens, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Error getting TypeScript code lenses for \"{}\": {:#}", &module.specifier, err, ); LspError::internal_error() } })?; if let Some(ts_code_lenses) = ts_code_lenses { code_lenses.extend(ts_code_lenses); } self.performance.measure(mark); if code_lenses.is_empty() { return Ok(None); } Ok(Some(code_lenses)) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn code_lens_resolve( &self, code_lens: CodeLens, token: &CancellationToken, ) -> LspResult<CodeLens> { let mark = self .performance .mark_with_args("lsp.code_lens_resolve", &code_lens); let result = if code_lens.data.is_some() { code_lens::resolve_code_lens(code_lens, self, token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get resolved code lens: {:#}", err); LspError::internal_error() } }) } else { Err(LspError::invalid_params( "Code lens is missing the \"data\" property.", )) }; self.performance.measure(mark); result } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn document_highlight( &self, params: DocumentHighlightParams, token: &CancellationToken, ) -> LspResult<Option<Vec<DocumentHighlight>>> { let mark = self .performance .mark_with_args("lsp.document_highlight", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let document_highlights = self .ts_server .provide_document_highlights( &module, params.text_document_position_params.position, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to get document highlights from TypeScript: {:#}", err ); LspError::internal_error() } })?; self.performance.measure(mark); Ok(document_highlights) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn references( &self, params: ReferenceParams, token: &CancellationToken, ) -> LspResult<Option<Vec<Location>>> { let mark = self.performance.mark_with_args("lsp.references", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let locations = self .ts_server .provide_references( &document, &module, params.text_document_position.position, params.context, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get references from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(locations) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn goto_definition( &self, params: GotoDefinitionParams, token: &CancellationToken, ) -> LspResult<Option<GotoDefinitionResponse>> { let mark = self .performance .mark_with_args("lsp.goto_definition", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let response = self .ts_server .provide_definition( &module, params.text_document_position_params.position, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get definition from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(response) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn goto_type_definition( &self, params: GotoTypeDefinitionParams, token: &CancellationToken, ) -> LspResult<Option<GotoTypeDefinitionResponse>> { let mark = self .performance .mark_with_args("lsp.goto_definition", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let response = self .ts_server .provide_type_definition( &module, params.text_document_position_params.position, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get type definition from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(response) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn completion( &self, params: CompletionParams, token: &CancellationToken, ) -> LspResult<Option<CompletionResponse>> { let mark = self.performance.mark_with_args("lsp.completion", ¶ms); let uri = ¶ms.text_document_position.text_document.uri; // Handle completions inside Deno configuration files (deno.json/jsonc) // separately, since they're not "diagnosable" documents but still benefit // from registry (jsr:/npm:/node:) import-specifier completion in the // `imports` and `scopes` fields. Use `Enabled::Ignore` because a config // file may itself be the only thing identifying its workspace as enabled. if let Some(document) = self.get_document( uri, Enabled::Ignore, Exists::Filter, Diagnosable::Ignore, )? && let Some(open_doc) = document.open() && matches!(open_doc.language_id, LanguageId::Json | LanguageId::JsonC) { let url = uri_to_url(uri); if completions::is_deno_config_url(&url) { let response = completions::get_deno_json_import_completions( &url, &open_doc.text, &open_doc.line_index, ¶ms.text_document_position.position, &self.completion_context(), ) .await; self.performance.measure(mark); return Ok(response); } } let Some(document) = self.get_document( uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let language_settings = self .config .language_settings_for_specifier(&module.specifier); if !language_settings.map(|s| s.suggest.enabled).unwrap_or(true) { return Ok(None); } // Import specifiers are something wholly internal to Deno, so for // completions, we will use internal logic and if there are completions // for imports, we will return those and not send a message into tsc, where // other completions come from. let mut response = None; if language_settings .map(|s| s.suggest.include_completions_for_import_statements) .unwrap_or(true) { response = completions::get_import_completions( &module, ¶ms.text_document_position.position, &self.completion_context(), ) .await; } if response.is_none() { response = self .ts_server .provide_completion( &module, params.text_document_position.position, params.context.as_ref(), self, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get completions from TypeScript: {:#}", err); LspError::internal_error() } })?; } self.performance.measure(mark); Ok(response) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn completion_resolve( &self, params: CompletionItem, token: &CancellationToken, ) -> LspResult<CompletionItem> { let _mark = self.performance.measure_scope("lsp.completion_resolve"); let Some(data) = ¶ms.data else { return Ok(params); }; let data = serde_json::from_value::<completions::CompletionItemData>(data.clone()) .map_err(|err| { error!("Could not decode data field of completion item: {:#}", err); LspError::internal_error() })?; let uri = match &data { CompletionItemData::Documentation(data) => { return Ok(CompletionItem { documentation: self .module_registry .get_documentation(&data.url) .await, data: None, ..params }); } CompletionItemData::TsJs(data) => &data.uri, }; let Some(document) = self.get_document( uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(params); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(params); }; let item = self .ts_server .resolve_completion_item(&module, params, data, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to resolve completion item from TypeScript: {:#}", err ); LspError::internal_error() } })?; Ok(item) } async fn diagnostic( &self, params: DocumentDiagnosticParams, token: &CancellationToken, ) -> LspResult<DocumentDiagnosticReportResult> { let _mark = self.performance.measure_scope("lsp.diagnostic"); if params .previous_result_id .as_ref() .and_then(|i| i.parse::<usize>().ok()) .is_some_and(|i| i >= self.project_version) { return Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Unchanged( RelatedUnchangedDocumentDiagnosticReport { related_documents: None, unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport { result_id: self.project_version.to_string(), }, }, ), )); } let empty_result = || { DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full( RelatedFullDocumentDiagnosticReport { related_documents: None, full_document_diagnostic_report: FullDocumentDiagnosticReport { result_id: Some(self.project_version.to_string()), items: Vec::new(), }, }, )) }; let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Ignore, )? else { return Ok(empty_result()); }; let url = uri_to_url(document.uri()); if matches!( self.config.tree.watched_file_type(&url), Some((_, ConfigWatchedFileType::ImportMap)) ) && let Some(open_doc) = document.open() { let diagnostics = generate_import_map_diagnostics(open_doc, &self.snapshot()); return Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { related_documents: None, full_document_diagnostic_report: FullDocumentDiagnosticReport { result_id: Some(self.project_version.to_string()), items: diagnostics, }, }), )); } let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(empty_result()); }; let module = match self.get_primary_module(&document)? { Some(module) => module, None => { let url = uri_to_url(document.uri()); if url.scheme() == "file" && !self.resolver.in_node_modules(&url) && !self.cache.in_cache_directory(&url) { return Ok(empty_result()); } // If this document represents a non-local module, the module may not be // retrievable until its referrer is known through some other request. // Wait and try one more time. tokio::time::sleep(std::time::Duration::from_millis(200)).await; let Some(module) = self.get_primary_module(&document)? else { return Ok(empty_result()); }; module } }; let diagnostics = self.get_module_diagnostics(&module, token).await?; Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { related_documents: None, full_document_diagnostic_report: FullDocumentDiagnosticReport { result_id: Some(self.project_version.to_string()), items: diagnostics.as_ref().clone(), }, }), )) } async fn get_module_diagnostics( &self, module: &Arc<DocumentModule>, token: &CancellationToken, ) -> Result<Arc<Vec<Diagnostic>>, LspError> { let diagnostics_cell = self .diagnostics_cache .entry(module.uri.clone()) .or_default() .clone(); diagnostics_cell .get_or_try_init(async || { let diagnostics = generate_module_diagnostics( module, &self.snapshot(), &self.ts_server, &self.ambient_modules_regex_cache, &self.no_slow_types_cache, token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to generate diagnostics for \"{}\": {:#}", &module.specifier, err ); LspError::internal_error() } })?; Ok(Arc::new(diagnostics)) }) .await .cloned() } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn goto_implementation( &self, params: GotoImplementationParams, token: &CancellationToken, ) -> LspResult<Option<GotoImplementationResponse>> { let mark = self .performance .mark_with_args("lsp.goto_implementation", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let implementations = self .ts_server .provide_implementations( &document, &module, params.text_document_position_params.position, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get implementations from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(implementations) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn folding_range( &self, params: FoldingRangeParams, token: &CancellationToken, ) -> LspResult<Option<Vec<FoldingRange>>> { let mark = self .performance .mark_with_args("lsp.folding_range", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let folding_ranges = self .ts_server .provide_folding_range(&module, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get folding ranges from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(folding_ranges) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn incoming_calls( &self, params: CallHierarchyIncomingCallsParams, token: &CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> { let mark = self .performance .mark_with_args("lsp.incoming_calls", ¶ms); let Some(document) = self.get_document( ¶ms.item.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let incoming_calls = self .ts_server .provide_call_hierarchy_incoming_calls( &document, &module, ¶ms.item, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get incoming calls from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(incoming_calls) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn outgoing_calls( &self, params: CallHierarchyOutgoingCallsParams, token: &CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> { let mark = self .performance .mark_with_args("lsp.outgoing_calls", ¶ms); let Some(document) = self.get_document( ¶ms.item.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let outgoing_calls = self .ts_server .provide_call_hierarchy_outgoing_calls( &module, ¶ms.item, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get outgoing calls from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(outgoing_calls) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn prepare_call_hierarchy( &self, params: CallHierarchyPrepareParams, token: &CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyItem>>> { let mark = self .performance .mark_with_args("lsp.prepare_call_hierarchy", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let items = self .ts_server .provide_prepare_call_hierarchy( &module, params.text_document_position_params.position, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get call hierarchy from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(items) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn rename( &self, params: RenameParams, token: &CancellationToken, ) -> LspResult<Option<WorkspaceEdit>> { let mark = self.performance.mark_with_args("lsp.rename", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let workspace_edit = self .ts_server .provide_rename( &document, &module, params.text_document_position.position, ¶ms.new_name, self, &self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get rename edit from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(workspace_edit) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn selection_range( &self, params: SelectionRangeParams, token: &CancellationToken, ) -> LspResult<Option<Vec<SelectionRange>>> { let mark = self .performance .mark_with_args("lsp.selection_range", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let selection_ranges = self .ts_server .provide_selection_ranges( &module, ¶ms.positions, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get selection ranges from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(selection_ranges) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn semantic_tokens_full( &self, params: SemanticTokensParams, token: &CancellationToken, ) -> LspResult<Option<SemanticTokensResult>> { let mark = self .performance .mark_with_args("lsp.semantic_tokens_full", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Ignore, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let result = self .ts_server .provide_semantic_tokens_full(&module, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { lsp_warn!("Unable to get semantic tokens from TypeScript: {:#}", err); LspError::invalid_request() } })?; self.performance.measure(mark); Ok(result) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn semantic_tokens_range( &self, params: SemanticTokensRangeParams, token: &CancellationToken, ) -> LspResult<Option<SemanticTokensRangeResult>> { let mark = self .performance .mark_with_args("lsp.semantic_tokens_range", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Ignore, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let result = self .ts_server .provide_semantic_tokens_range( &module, params.range, self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { lsp_warn!( "Unable to get semantic tokens range from TypeScript: {:#}", err ); LspError::invalid_request() } })?; self.performance.measure(mark); Ok(result) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn signature_help( &self, params: SignatureHelpParams, token: &CancellationToken, ) -> LspResult<Option<SignatureHelp>> { let mark = self .performance .mark_with_args("lsp.signature_help", ¶ms); let Some(document) = self.get_document( ¶ms.text_document_position_params.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let signature_help = self .ts_server .provide_signature_help( &module, params.text_document_position_params.position, params.context.as_ref(), self.snapshot(), token, ) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { lsp_warn!("Unable to get signature help from TypeScript: {:#}", err); LspError::invalid_request() } })?; self.performance.measure(mark); Ok(signature_help) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn will_rename_files( &self, params: RenameFilesParams, token: &CancellationToken, ) -> LspResult<Option<WorkspaceEdit>> { if !self.ts_server.is_started() { return Ok(None); } self .ts_server .provide_will_rename_files(¶ms.files, self, self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!( "Unable to get edits for file renames from TypeScript: {:#}", err ); LspError::internal_error() } }) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn symbol( &self, params: WorkspaceSymbolParams, token: &CancellationToken, ) -> LspResult<Option<Vec<SymbolInformation>>> { if !self.ts_server.is_started() { return Ok(None); } let mark = self.performance.mark_with_args("lsp.symbol", ¶ms); let symbol_information = self .ts_server .provide_workspace_symbol(¶ms.query, &self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get workspace symbols from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(symbol_information) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn project_changed( &mut self, documents: Vec<(Document, ChangeKind)>, scopes_change: ProjectScopesChange, ) { self.ambient_modules_regex_cache.clear(); self.diagnostics_cache.clear(); self.no_slow_types_cache.clear(); self.project_version += 1; // increment before getting the snapshot self.ts_server.project_changed( &documents, matches!( scopes_change, ProjectScopesChange::OpenNotebooks | ProjectScopesChange::Config ), self.snapshot(), ); // Invalidate the weak references of `documents` before removing expired // entries. drop(documents); self.document_modules.remove_expired_modules(); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn send_diagnostics_update(&self) { if let Some(diagnostics_server) = &self.diagnostics_server { let snapshot = DiagnosticsUpdateMessage { snapshot: self.snapshot(), }; if let Err(err) = diagnostics_server.update(snapshot) { error!("Cannot update diagnostics: {:#}", err); } } } /// Send a message to the testing server to look for any changes in tests and /// update the client. fn send_testing_update(&self) { if let Some(testing_server) = &self.maybe_testing_server && let Err(err) = testing_server.update(self.snapshot()) { error!("Cannot update testing server: {:#}", err); } } } #[tower_lsp::async_trait(?Send)] impl tower_lsp::LanguageServer for LanguageServer { async fn execute_command( &self, params: ExecuteCommandParams, _token: CancellationToken, ) -> LspResult<Option<Value>> { self.init_flag.wait_raised().await; if params.command == "deno.cache" { #[derive(Default, Deserialize)] #[serde(rename_all = "camelCase")] struct Options { #[serde(default)] force_global_cache: bool, } #[derive(Deserialize)] struct Arguments(Vec<Url>, Url, #[serde(default)] Options); let Arguments(specifiers, referrer, options) = serde_json::from_value(json!(params.arguments)) .map_err(|err| LspError::invalid_params(err.to_string()))?; self .cache(specifiers, referrer, options.force_global_cache) .await } else if params.command == "deno.reloadImportRegistries" { *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.reload_import_registries().await } else { Ok(None) } } async fn initialize( &self, params: InitializeParams, ) -> LspResult<InitializeResult> { self.inner.write().await.initialize(params) } async fn initialized(&self, _: InitializedParams) { self.refresh_configuration().await; let (registrations, http_client) = { let mut inner = self.inner.write().await; let registrations = inner.initialized().await; inner.task_queue.start(self.clone()); (registrations, inner.http_client_provider.clone()) }; self.init_flag.raise(); for registration in registrations { if let Err(err) = self .client .when_outside_lsp_lock() .register_capability(vec![registration]) .await { lsp_warn!("Client errored on capabilities.\n{:#}", err); } } if upgrade_check_enabled() { let client = self.client.clone(); // spawn to avoid lsp send/sync requirement, but also just // to ensure this initialized method returns quickly spawn(async move { match check_for_upgrades_for_lsp(http_client).await { Ok(version_info) => { client.send_did_upgrade_check_notification( lsp_custom::DidUpgradeCheckNotificationParams { upgrade_available: version_info.map(|info| { lsp_custom::UpgradeAvailable { latest_version: info.latest_version, is_canary: info.is_canary, } }), }, ); } Err(err) => lsp_warn!("Failed to check for upgrades: {err}"), } }); } lsp_log!("Server ready."); } async fn shutdown(&self) -> LspResult<()> { Ok(()) } async fn did_open(&self, params: DidOpenTextDocumentParams) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.did_open(params); } async fn did_change(&self, params: DidChangeTextDocumentParams) { self.init_flag.wait_raised().await; let (batch_queue, token) = { let existing = self.did_change_batch_queue.borrow_mut().take(); let batch_queue = existing .filter(|q| q.uri == params.text_document.uri) .unwrap_or_else(|| { Arc::new(DidChangeBatchQueue::new(params.text_document.uri)) }); let token = batch_queue.enqueue(DidChangeBatchQueueEntry { version: params.text_document.version, content_changes: params.content_changes, }); *self.did_change_batch_queue.borrow_mut() = Some(batch_queue.clone()); (batch_queue, token) }; tokio::select! { biased; _ = token.cancelled() => {} mut inner = self.inner.write() => { inner.did_change_batched(batch_queue); } } } async fn did_save(&self, params: DidSaveTextDocumentParams) { self.init_flag.wait_raised().await; self.inner.read().await.did_save(params).await; } async fn did_close(&self, params: DidCloseTextDocumentParams) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.did_close(params); } async fn notebook_did_open(&self, params: DidOpenNotebookDocumentParams) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.notebook_did_open(params) } async fn notebook_did_change(&self, params: DidChangeNotebookDocumentParams) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.notebook_did_change(params) } async fn notebook_did_save(&self, params: DidSaveNotebookDocumentParams) { self.init_flag.wait_raised().await; self.inner.read().await.notebook_did_save(params).await } async fn notebook_did_close(&self, params: DidCloseNotebookDocumentParams) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self.inner.write().await.notebook_did_close(params) } async fn did_change_configuration( &self, params: DidChangeConfigurationParams, ) { self.init_flag.wait_raised().await; let mark = self .performance .mark_with_args("lsp.did_change_configuration", ¶ms); self.refresh_configuration().await; *self.did_change_batch_queue.borrow_mut() = None; self .inner .write() .await .did_change_configuration(params) .await; self.performance.measure(mark); } async fn did_change_watched_files( &self, params: DidChangeWatchedFilesParams, ) { self.init_flag.wait_raised().await; *self.did_change_batch_queue.borrow_mut() = None; self .inner .write() .await .did_change_watched_files(params) .await; } async fn did_change_workspace_folders( &self, params: DidChangeWorkspaceFoldersParams, ) { self.init_flag.wait_raised().await; let mark = self .performance .mark_with_args("lsp.did_change_workspace_folders", ¶ms); *self.did_change_batch_queue.borrow_mut() = None; self .inner .write() .await .pre_did_change_workspace_folders(params); self.refresh_configuration().await; *self.did_change_batch_queue.borrow_mut() = None; self .inner .write() .await .post_did_change_workspace_folders() .await; self.performance.measure(mark); } async fn document_symbol( &self, params: DocumentSymbolParams, token: CancellationToken, ) -> LspResult<Option<DocumentSymbolResponse>> { self.init_flag.wait_raised().await; self .inner .read() .await .document_symbol(params, &token) .await } async fn formatting( &self, params: DocumentFormattingParams, token: CancellationToken, ) -> LspResult<Option<Vec<TextEdit>>> { self.init_flag.wait_raised().await; self.inner.read().await.formatting(params, &token).await } async fn hover( &self, params: HoverParams, token: CancellationToken, ) -> LspResult<Option<Hover>> { self.init_flag.wait_raised().await; self.inner.read().await.hover(params, &token).await } async fn inlay_hint( &self, params: InlayHintParams, token: CancellationToken, ) -> LspResult<Option<Vec<InlayHint>>> { self.init_flag.wait_raised().await; self.inner.read().await.inlay_hint(params, &token).await } async fn code_action( &self, params: CodeActionParams, token: CancellationToken, ) -> LspResult<Option<CodeActionResponse>> { self.init_flag.wait_raised().await; self.inner.read().await.code_action(params, &token).await } async fn code_action_resolve( &self, params: CodeAction, token: CancellationToken, ) -> LspResult<CodeAction> { self.init_flag.wait_raised().await; self .inner .read() .await .code_action_resolve(params, &token) .await } async fn code_lens( &self, params: CodeLensParams, token: CancellationToken, ) -> LspResult<Option<Vec<CodeLens>>> { self.init_flag.wait_raised().await; self.inner.read().await.code_lens(params, &token).await } async fn code_lens_resolve( &self, params: CodeLens, token: CancellationToken, ) -> LspResult<CodeLens> { self.init_flag.wait_raised().await; self .inner .read() .await .code_lens_resolve(params, &token) .await } async fn document_highlight( &self, params: DocumentHighlightParams, token: CancellationToken, ) -> LspResult<Option<Vec<DocumentHighlight>>> { self.init_flag.wait_raised().await; self .inner .read() .await .document_highlight(params, &token) .await } async fn references( &self, params: ReferenceParams, token: CancellationToken, ) -> LspResult<Option<Vec<Location>>> { self.init_flag.wait_raised().await; self.inner.read().await.references(params, &token).await } async fn goto_definition( &self, params: GotoDefinitionParams, token: CancellationToken, ) -> LspResult<Option<GotoDefinitionResponse>> { self.init_flag.wait_raised().await; self .inner .read() .await .goto_definition(params, &token) .await } async fn goto_type_definition( &self, params: GotoTypeDefinitionParams, token: CancellationToken, ) -> LspResult<Option<GotoTypeDefinitionResponse>> { self.init_flag.wait_raised().await; self .inner .read() .await .goto_type_definition(params, &token) .await } async fn completion( &self, params: CompletionParams, token: CancellationToken, ) -> LspResult<Option<CompletionResponse>> { self.init_flag.wait_raised().await; self.inner.read().await.completion(params, &token).await } async fn completion_resolve( &self, params: CompletionItem, token: CancellationToken, ) -> LspResult<CompletionItem> { self.init_flag.wait_raised().await; self .inner .read() .await .completion_resolve(params, &token) .await } async fn diagnostic( &self, params: DocumentDiagnosticParams, token: CancellationToken, ) -> LspResult<DocumentDiagnosticReportResult> { self.init_flag.wait_raised().await; self.inner.read().await.diagnostic(params, &token).await } async fn goto_implementation( &self, params: GotoImplementationParams, token: CancellationToken, ) -> LspResult<Option<GotoImplementationResponse>> { self.init_flag.wait_raised().await; self .inner .read() .await .goto_implementation(params, &token) .await } async fn folding_range( &self, params: FoldingRangeParams, token: CancellationToken, ) -> LspResult<Option<Vec<FoldingRange>>> { self.init_flag.wait_raised().await; self.inner.read().await.folding_range(params, &token).await } async fn incoming_calls( &self, params: CallHierarchyIncomingCallsParams, token: CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyIncomingCall>>> { self.init_flag.wait_raised().await; self.inner.read().await.incoming_calls(params, &token).await } async fn outgoing_calls( &self, params: CallHierarchyOutgoingCallsParams, token: CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyOutgoingCall>>> { self.init_flag.wait_raised().await; self.inner.read().await.outgoing_calls(params, &token).await } async fn prepare_call_hierarchy( &self, params: CallHierarchyPrepareParams, token: CancellationToken, ) -> LspResult<Option<Vec<CallHierarchyItem>>> { self.init_flag.wait_raised().await; self .inner .read() .await .prepare_call_hierarchy(params, &token) .await } async fn rename( &self, params: RenameParams, token: CancellationToken, ) -> LspResult<Option<WorkspaceEdit>> { self.init_flag.wait_raised().await; self.inner.read().await.rename(params, &token).await } async fn selection_range( &self, params: SelectionRangeParams, token: CancellationToken, ) -> LspResult<Option<Vec<SelectionRange>>> { self.init_flag.wait_raised().await; self .inner .read() .await .selection_range(params, &token) .await } async fn semantic_tokens_full( &self, params: SemanticTokensParams, token: CancellationToken, ) -> LspResult<Option<SemanticTokensResult>> { self.init_flag.wait_raised().await; self .inner .read() .await .semantic_tokens_full(params, &token) .await } async fn semantic_tokens_range( &self, params: SemanticTokensRangeParams, token: CancellationToken, ) -> LspResult<Option<SemanticTokensRangeResult>> { self.init_flag.wait_raised().await; self .inner .read() .await .semantic_tokens_range(params, &token) .await } async fn signature_help( &self, params: SignatureHelpParams, token: CancellationToken, ) -> LspResult<Option<SignatureHelp>> { self.init_flag.wait_raised().await; self.inner.read().await.signature_help(params, &token).await } async fn will_rename_files( &self, params: RenameFilesParams, token: CancellationToken, ) -> LspResult<Option<WorkspaceEdit>> { self.init_flag.wait_raised().await; self .inner .read() .await .will_rename_files(params, &token) .await } async fn symbol( &self, params: WorkspaceSymbolParams, token: CancellationToken, ) -> LspResult<Option<Vec<SymbolInformation>>> { self.init_flag.wait_raised().await; self.inner.read().await.symbol(params, &token).await } } struct PrepareCacheResult { cli_factory: CliFactory, roots: Vec<ModuleSpecifier>, open_modules: Vec<Arc<DocumentModule>>, } // These are implementations of custom commands supported by the LSP impl Inner { async fn initialized(&mut self) -> Vec<Registration> { let mut registrations = Vec::with_capacity(2); init_log_file(self.config.log_file(), &self.initial_cwd); self.update_debug_flag(); self.update_global_cache().await; self.refresh_workspace_files(); self.refresh_config_tree("initialized").await; self.update_cache(); self.refresh_resolver().await; self.refresh_compiler_options_resolver(); self.refresh_linter_resolver(); self.refresh_documents_config(); if self.config.did_change_watched_files_capable() { // Watch config files (so we can refresh the resolver/compiler options // when they change) and source files (so we can evict cached entries // when files referenced by on-disk imports change or are deleted — // otherwise the LSP keeps serving stale content from its in-memory // document cache, see issue #12813). let options = DidChangeWatchedFilesRegistrationOptions { watchers: vec![FileSystemWatcher { glob_pattern: GlobPattern::String( "**/*.{json,jsonc,lock,js,mjs,cjs,jsx,ts,mts,cts,tsx,wasm}" .to_string(), ), kind: None, }], }; registrations.push(Registration { id: "workspace/didChangeWatchedFiles".to_string(), method: "workspace/didChangeWatchedFiles".to_string(), register_options: Some(serde_json::to_value(options).unwrap()), }); } if self.config.will_rename_files_capable() { let options = FileOperationRegistrationOptions { filters: vec![FileOperationFilter { scheme: Some("file".to_string()), pattern: FileOperationPattern { glob: "**/*".to_string(), matches: None, options: None, }, }], }; registrations.push(Registration { id: "workspace/willRenameFiles".to_string(), method: "workspace/willRenameFiles".to_string(), register_options: Some(serde_json::to_value(options).unwrap()), }); } if self.config.testing_api_capable() { let test_server = testing::TestServer::new( self.client.clone(), self.performance.clone(), self.config.root_url().cloned(), ); self.maybe_testing_server = Some(test_server); } let mut config_events = vec![]; for (scope_url, config_data) in self.config.tree.data_by_scope().iter() { let Ok(scope_uri) = url_to_uri(scope_url) else { continue; }; if let Some(config_file) = config_data.maybe_deno_json() && let Ok(file_uri) = url_to_uri(&config_file.specifier) { config_events.push(lsp_custom::DenoConfigurationChangeEvent { scope_uri: scope_uri.clone(), file_uri, typ: lsp_custom::DenoConfigurationChangeType::Added, configuration_type: lsp_custom::DenoConfigurationType::DenoJson, }); } if let Some(package_json) = config_data.maybe_pkg_json() && let Ok(file_uri) = url_to_uri(&package_json.specifier()) { config_events.push(lsp_custom::DenoConfigurationChangeEvent { scope_uri, file_uri, typ: lsp_custom::DenoConfigurationChangeType::Added, configuration_type: lsp_custom::DenoConfigurationType::PackageJson, }); } } if !config_events.is_empty() { self.client.send_did_change_deno_configuration_notification( lsp_custom::DidChangeDenoConfigurationNotificationParams { changes: config_events, }, ); } registrations } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn prepare_cache( &mut self, specifiers: Vec<ModuleSpecifier>, referrer: ModuleSpecifier, force_global_cache: bool, ) -> Result<PrepareCacheResult, AnyError> { let config_data = self.config.tree.data_for_specifier(&referrer); let scope = config_data.map(|d| d.scope.clone()); let byonm = config_data.map(|d| d.byonm).unwrap_or(false); let mut roots = if !specifiers.is_empty() { specifiers } else { vec![referrer.clone()] }; let scoped_resolver = self.resolver.get_scoped_resolver(scope.as_deref()); roots.extend(scoped_resolver.configured_auto_import_roots()); if byonm { roots.retain(|s| s.scheme() != "npm"); } else { // always include the npm packages since resolution of one npm package // might affect the resolution of other npm packages roots.extend( scoped_resolver .npm_reqs() .iter() .map(|req| ModuleSpecifier::parse(&format!("npm:{}", req)).unwrap()), ); } let open_modules = self .document_modules .documents .open_docs() .filter(|d| d.is_diagnosable()) .flat_map(|d| { self .document_modules .module(&Document::Open(d.clone()), scope.as_deref()) }) .collect::<Vec<_>>(); for module in &open_modules { roots.extend( module .dependencies .values() .filter_map(|d| d.get_type()) .cloned(), ); roots.extend( module .types_dependency .iter() .flat_map(|d| d.dependency.maybe_specifier()) .cloned(), ); } let workspace_settings = self.config.workspace_settings(); let initial_cwd = config_data .and_then(|d| d.scope.to_file_path().ok()) .unwrap_or_else(|| self.initial_cwd.clone()); let mut cli_factory = CliFactory::from_flags(Arc::new(Flags { internal: InternalFlags { cache_path: Some(self.cache.deno_dir().root.clone()), lockfile_skip_write: true, ..Default::default() }, ca_stores: workspace_settings.certificate_stores.clone(), ca_data: workspace_settings.tls_certificate.clone().map(CaData::File), unsafely_ignore_certificate_errors: workspace_settings .unsafely_ignore_certificate_errors .clone(), import_map_path: config_data.and_then(|d| { d.import_map_from_settings .as_ref() .map(|url| url.to_string()) }), // bit of a hack to force the lsp to cache the @types/node package type_check_mode: crate::args::TypeCheckMode::Local, permissions: crate::args::PermissionFlags { // allow remote import permissions in the lsp for now allow_import: Some(vec![]), ..Default::default() }, vendor: if force_global_cache { Some(false) } else { None }, no_lock: force_global_cache, ..Default::default() })); cli_factory.set_initial_cwd(initial_cwd); if let Some(d) = &config_data { cli_factory.set_workspace_dir(d.member_dir.clone()); }; Ok(PrepareCacheResult { cli_factory, open_modules, roots, }) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn post_cache(&mut self) { self.refresh_config_tree("post_cache").await; self.update_cache(); self.refresh_resolver().await; self.refresh_compiler_options_resolver(); self.refresh_linter_resolver(); self.refresh_documents_config(); self.resolver.did_cache(); self.refresh_dep_info(); self.project_changed(vec![], ProjectScopesChange::Config); let TsServer::Js(ts_server) = self.ts_server.as_ref(); ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); if !self.is_using_push_based_diagnostics() && self.config.diagnostic_refresh_capable() { self.client.refresh_diagnostics(); } self.send_testing_update(); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn pre_did_change_workspace_folders( &mut self, params: DidChangeWorkspaceFoldersParams, ) { let mut workspace_folders = params .event .added .into_iter() .map(|folder| { let mut url = uri_to_url(&folder.uri); if !url.path().ends_with('/') && let Ok(mut path_segments) = url.path_segments_mut() { path_segments.push(""); } (Arc::new(url), folder) }) .collect::<Vec<_>>(); for (specifier, folder) in self.config.workspace_folders.as_ref() { if !params.event.removed.is_empty() && params.event.removed.iter().any(|f| f.uri == folder.uri) { continue; } workspace_folders.push((specifier.clone(), folder.clone())); } self.config.set_workspace_folders(workspace_folders); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn post_did_change_workspace_folders(&mut self) { self.refresh_workspace_files(); self .refresh_config_tree("did_change_workspace_folders") .await; self.update_cache(); self.refresh_resolver().await; self.refresh_compiler_options_resolver(); self.refresh_linter_resolver(); self.refresh_documents_config(); self.send_diagnostics_update(); self.send_testing_update(); } fn get_performance(&self) -> Value { let averages = self.performance.averages(); json!({ "averages": averages }) } async fn test_run_request( &self, params: Option<Value>, ) -> LspResult<Option<Value>> { if let Some(testing_server) = &self.maybe_testing_server { match params.map(serde_json::from_value) { Some(Ok(params)) => { testing_server .run_request( params, self.config.workspace_settings().clone(), &self.config.tree, ) .await } Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), } } else { Err(LspError::invalid_request()) } } fn test_run_cancel_request( &self, params: Option<Value>, ) -> LspResult<Option<Value>> { if let Some(testing_server) = &self.maybe_testing_server { match params.map(serde_json::from_value) { Some(Ok(params)) => testing_server.run_cancel_request(params), Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), } } else { Err(LspError::invalid_request()) } } fn task_definitions(&self) -> LspResult<Vec<TaskDefinition>> { let mut result = vec![]; for config_file in self.config.tree.config_files() { if let Some(tasks) = config_file.to_tasks_config().ok().flatten() { for (name, def) in tasks { result.push(TaskDefinition { name: name.clone(), command: def.command.clone(), source_uri: url_to_uri(&config_file.specifier) .map_err(|_| LspError::internal_error())?, description: def.description.clone(), }); } }; } for package_json in self.config.tree.package_jsons() { if let Some(scripts) = &package_json.scripts { for (name, command) in scripts { result.push(TaskDefinition { name: name.clone(), command: Some(command.clone()), source_uri: url_to_uri(&package_json.specifier()) .map_err(|_| LspError::internal_error())?, description: None, }); } } } result.sort_by_key(|d| d.name.clone()); Ok(result) } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn inlay_hint( &self, params: InlayHintParams, token: &CancellationToken, ) -> LspResult<Option<Vec<InlayHint>>> { let mark = self.performance.mark_with_args("lsp.inlay_hint", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; if !self .config .enabled_inlay_hints_for_specifier(&module.specifier) { return Ok(None); } let inlay_hints = self .ts_server .provide_inlay_hint(&module, params.range, &self.snapshot(), token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get inlay hints from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(inlay_hints) } async fn reload_import_registries(&mut self) -> LspResult<Option<Value>> { remove_dir_all_if_exists(&self.module_registry.location) .await .map_err(|err| { error!("Unable to remove registries cache: {:#}", err); LspError::internal_error() })?; self.module_registry.clear_cache(); self.jsr_search_api.clear_cache(); self.npm_search_api.clear_cache(); Ok(Some(json!(true))) } fn virtual_text_document( &self, params: lsp_custom::VirtualTextDocumentParams, ) -> LspResult<Option<String>> { let mark = self .performance .mark_with_args("lsp.virtual_text_document", ¶ms); let contents = if params .text_document .uri .scheme() .as_str() .eq_ignore_ascii_case("deno") && params.text_document.uri.path().as_str() == "/status.md" { let mut contents = String::new(); let mut open_docs = self .document_modules .documents .open_docs() .collect::<Vec<_>>(); open_docs.sort_by_cached_key(|d| d.uri.to_string()); let mut server_docs = self.document_modules.documents.server_docs(); server_docs.sort_by_cached_key(|d| d.uri.to_string()); let measures = self.performance.to_vec(); write!( contents, r#"# Deno Language Server Status ## Workspace Settings {} ## Documents <details><summary>Open: {}</summary> {} </details> <details><summary>Server: {}</summary> {} </details> ## Performance Measures <details><summary>Measures: {}</summary> {} </details> "#, self.config.settings.by_workspace_folder .iter() .filter_map(|(folder_url, settings)| Some((format!("\"{folder_url}\""), settings.as_ref()?))) .chain(std::iter::once(("Unscoped".to_string(), &self.config.settings.unscoped))) .map(|(title, settings)| format!("<details><summary>{title}</summary>\n\n```json\n{}\n```\n\n</details>", serde_json::to_string_pretty(settings).unwrap())) .collect::<Vec<_>>() .join("\n\n"), open_docs.len(), open_docs.iter().map(|d| format!("- {}", d.uri.as_str())).collect::<Vec<_>>().join("\n"), server_docs.len(), server_docs.iter().map(|d| format!("- {}", d.uri.as_str())).collect::<Vec<_>>().join("\n"), measures.len(), measures .iter() .map(|m| format!("- {m}")) .collect::<Vec<_>>() .join("\n"), ) .unwrap(); contents .push_str("\n## Performance (last 3 000 entries)\n\n|Name|Count|Duration|\n|---|---|---|\n"); let mut averages = self.performance.averages_as_f64(); averages.sort_by(|a, b| a.0.cmp(&b.0)); for (name, count, average_duration) in averages { writeln!(contents, "|{}|{}|{}ms|", name, count, average_duration) .unwrap(); } contents.push_str( "\n## Performance (total)\n\n|Name|Count|Duration|\n|---|---|---|\n", ); let mut measurements_by_type = self.performance.measurements_by_type(); measurements_by_type.sort_by(|a, b| a.0.cmp(&b.0)); for (name, total_count, total_duration) in measurements_by_type { writeln!( contents, "|{}|{}|{:.3}ms|", name, total_count, total_duration ) .unwrap(); } Some(contents) } else { match self.get_document( ¶ms.text_document.uri, Enabled::Ignore, Exists::Filter, Diagnosable::Ignore, )? { Some(document) => Some(document.text().to_string()), _ => { lsp_warn!( "The document was not found: {}", params.text_document.uri.as_str() ); None } } }; self.performance.measure(mark); Ok(contents) } async fn inferred_type( &self, params: lsp_custom::InferredTypeParams, token: &CancellationToken, ) -> LspResult<Option<lsp_custom::InferredTypeResponse>> { let mark = self .performance .mark_with_args("lsp.inferred_type", ¶ms); let Some(document) = self.get_document( ¶ms.text_document.uri, Enabled::Filter, Exists::Enforce, Diagnosable::Filter, )? else { return Ok(None); }; let Some(module) = self.get_primary_module(&document)? else { return Ok(None); }; let snapshot = self.snapshot(); let response = self .ts_server .provide_inferred_type(&module, params.position, snapshot, token) .await .map_err(|err| { if token.is_cancelled() { LspError::request_cancelled() } else { error!("Unable to get inferred type from TypeScript: {:#}", err); LspError::internal_error() } })?; self.performance.measure(mark); Ok(response) } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use test_util::TempDir; use super::*; #[test] fn test_walk_workspace() { let temp_dir = TempDir::new(); temp_dir.create_dir_all("root1/vendor/"); temp_dir.create_dir_all("root1/coverage/"); temp_dir.write("root1/vendor/mod.ts", ""); // no, vendor temp_dir.write("root1/coverage/mod.ts", ""); // no, coverage temp_dir.create_dir_all("root1/node_modules/"); temp_dir.write("root1/node_modules/mod.ts", ""); // no, node_modules temp_dir.create_dir_all("root1/folder"); temp_dir.create_dir_all("root1/target"); temp_dir.create_dir_all("root1/node_modules"); temp_dir.create_dir_all("root1/.git"); temp_dir.create_dir_all("root1/file.ts"); // no, directory temp_dir.write("root1/mod0.ts", ""); // yes temp_dir.write("root1/mod1.js", ""); // yes temp_dir.write("root1/mod2.tsx", ""); // yes temp_dir.write("root1/mod3.d.ts", ""); // yes temp_dir.write("root1/mod4.jsx", ""); // yes temp_dir.write("root1/mod5.mjs", ""); // yes temp_dir.write("root1/mod6.mts", ""); // yes temp_dir.write("root1/mod7.d.mts", ""); // yes temp_dir.write("root1/mod8.json", ""); // yes temp_dir.write("root1/mod9.jsonc", ""); // yes temp_dir.write("root1/other.txt", ""); // no, text file temp_dir.write("root1/other.wasm", ""); // no, don't load wasm temp_dir.write("root1/Cargo.toml", ""); // no temp_dir.write("root1/folder/mod.ts", ""); // yes temp_dir.write("root1/folder/data.min.ts", ""); // no, minified file temp_dir.write("root1/.git/main.ts", ""); // no, .git folder temp_dir.write("root1/node_modules/main.ts", ""); // no, because it's in a node_modules folder temp_dir.write("root1/target/main.ts", ""); // no, because there is a Cargo.toml in the root directory temp_dir.create_dir_all("root2/folder"); temp_dir.create_dir_all("root2/folder2/inner_folder"); temp_dir.create_dir_all("root2/sub_folder"); temp_dir.create_dir_all("root2/root2.1"); temp_dir.write("root2/file1.ts", ""); // yes, enabled temp_dir.write("root2/file2.ts", ""); // no, not enabled temp_dir.write("root2/folder/main.ts", ""); // yes, enabled temp_dir.write("root2/folder/other.ts", ""); // no, disabled temp_dir.write("root2/folder2/inner_folder/main.ts", ""); // yes, enabled (regression test for https://github.com/denoland/vscode_deno/issues/1239) temp_dir.write("root2/sub_folder/a.js", ""); // no, not enabled temp_dir.write("root2/sub_folder/b.ts", ""); // no, not enabled temp_dir.write("root2/sub_folder/c.js", ""); // no, not enabled temp_dir.write("root2/root2.1/main.ts", ""); // yes, enabled as separate root temp_dir.create_dir_all("root3/"); temp_dir.write("root3/mod.ts", ""); // no, not enabled temp_dir.create_dir_all("root4_parent/root4"); temp_dir.write("root4_parent/deno.json", ""); // yes, enabled as deno.json above root temp_dir.write("root4_parent/root4/main.ts", ""); // yes, enabled let mut config = Config::new_with_roots(vec![ temp_dir.url().join("root1/").unwrap(), temp_dir.url().join("root2/").unwrap(), temp_dir.url().join("root2/root2.1/").unwrap(), temp_dir.url().join("root3/").unwrap(), temp_dir.url().join("root4_parent/root4/").unwrap(), ]); config.set_client_capabilities(ClientCapabilities { workspace: Some(Default::default()), ..Default::default() }); config.set_workspace_settings( Default::default(), vec![ ( Arc::new(temp_dir.url().join("root1/").unwrap()), WorkspaceSettings { enable: Some(true), ..Default::default() }, ), ( Arc::new(temp_dir.url().join("root2/").unwrap()), WorkspaceSettings { enable: Some(true), enable_paths: Some(vec![ "file1.ts".to_string(), "folder".to_string(), "folder2/inner_folder".to_string(), ]), disable_paths: vec!["folder/other.ts".to_string()], ..Default::default() }, ), ( Arc::new(temp_dir.url().join("root2/root2.1/").unwrap()), WorkspaceSettings { enable: Some(true), ..Default::default() }, ), ( Arc::new(temp_dir.url().join("root3/").unwrap()), WorkspaceSettings { enable: Some(false), ..Default::default() }, ), ( Arc::new(temp_dir.url().join("root4_parent/root4/").unwrap()), WorkspaceSettings { enable: Some(true), ..Default::default() }, ), ], ); let (workspace_files, hit_limit) = Inner::walk_workspace(&config); let workspace_files = workspace_files .into_iter() .map(|p| Url::from_file_path(p).unwrap()) .collect::<IndexSet<_>>(); assert!(!hit_limit); assert_eq!( json!(workspace_files), json!([ temp_dir.url().join("root4_parent/deno.json").unwrap(), temp_dir.url().join("root1/mod0.ts").unwrap(), temp_dir.url().join("root1/mod1.js").unwrap(), temp_dir.url().join("root1/mod2.tsx").unwrap(), temp_dir.url().join("root1/mod3.d.ts").unwrap(), temp_dir.url().join("root1/mod4.jsx").unwrap(), temp_dir.url().join("root1/mod5.mjs").unwrap(), temp_dir.url().join("root1/mod6.mts").unwrap(), temp_dir.url().join("root1/mod7.d.mts").unwrap(), temp_dir.url().join("root1/mod8.json").unwrap(), temp_dir.url().join("root1/mod9.jsonc").unwrap(), temp_dir.url().join("root2/file1.ts").unwrap(), temp_dir.url().join("root4_parent/root4/main.ts").unwrap(), temp_dir.url().join("root1/folder/mod.ts").unwrap(), temp_dir.url().join("root2/folder/main.ts").unwrap(), temp_dir.url().join("root2/root2.1/main.ts").unwrap(), temp_dir .url() .join("root2/folder2/inner_folder/main.ts") .unwrap(), ]) ); } #[test] fn test_walk_workspace_reaches_enabled_nested_workspace() { let temp_dir = TempDir::new(); temp_dir.write("repo/root.ts", ""); // no, root is disabled temp_dir.write( "repo/apps/backend/deno.json", r#"{ "importMap": "import_map.json" }"#, ); // yes temp_dir.write( "repo/apps/backend/import_map.json", r#"{ "imports": { "core/": "../../shared/ts/core/src/" } }"#, ); // yes temp_dir.write("repo/apps/backend/src/run.ts", ""); // yes temp_dir.write("repo/apps/web/src/main.ts", ""); // no, not enabled temp_dir.write("repo/shared/ts/core/src/index.ts", ""); // no, not enabled let root_url = temp_dir.url().join("repo/").unwrap(); let shared_url = temp_dir.url().join("repo/shared/").unwrap(); let backend_url = temp_dir.url().join("repo/apps/backend/").unwrap(); let web_url = temp_dir.url().join("repo/apps/web/").unwrap(); let mut config = Config::new_with_roots(vec![ shared_url.clone(), backend_url.clone(), web_url.clone(), root_url.clone(), ]); config.set_client_capabilities(ClientCapabilities { workspace: Some(Default::default()), ..Default::default() }); config.set_workspace_settings( WorkspaceSettings { enable: Some(false), ..Default::default() }, vec![ ( Arc::new(shared_url.clone()), WorkspaceSettings { enable: Some(false), ..Default::default() }, ), ( Arc::new(backend_url.clone()), WorkspaceSettings { enable: Some(true), ..Default::default() }, ), ( Arc::new(web_url.clone()), WorkspaceSettings { enable: Some(false), ..Default::default() }, ), ( Arc::new(root_url.clone()), WorkspaceSettings { enable: Some(false), ..Default::default() }, ), ], ); let (workspace_files, hit_limit) = Inner::walk_workspace(&config); let workspace_files = workspace_files .into_iter() .map(|p| Url::from_file_path(p).unwrap()) .collect::<IndexSet<_>>(); assert!(!hit_limit); assert_eq!( json!(workspace_files), json!([ backend_url.join("deno.json").unwrap(), backend_url.join("import_map.json").unwrap(), backend_url.join("src/run.ts").unwrap(), ]) ); } }