// Copyright 2018-2025 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::fs; use std::future::Future; use std::ops::Range; use std::path::PathBuf; use std::pin::Pin; use std::str::FromStr; use std::sync::Arc; use std::sync::Weak; use std::time::SystemTime; use dashmap::DashMap; use deno_ast::swc::ecma_visit::VisitWith; use deno_ast::MediaType; use deno_ast::ParsedSource; use deno_ast::SourceTextInfo; use deno_core::error::AnyError; use deno_core::futures::future; use deno_core::futures::future::Shared; use deno_core::futures::FutureExt; use deno_core::parking_lot::RwLock; use deno_core::resolve_url; use deno_core::url::Position; use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_error::JsErrorBox; use deno_graph::TypesDependency; use deno_path_util::url_to_file_path; use deno_runtime::deno_node; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use indexmap::IndexMap; use indexmap::IndexSet; use lsp_types::Uri; use node_resolver::cache::NodeResolutionThreadLocalCache; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; use once_cell::sync::Lazy; use serde::Serialize; use tower_lsp::lsp_types as lsp; use weak_table::PtrWeakKeyHashMap; use weak_table::WeakValueHashMap; use super::cache::calculate_fs_version_at_path; use super::cache::LspCache; use super::config::Config; use super::logging::lsp_warn; use super::resolver::LspResolver; use super::resolver::ScopeDepInfo; use super::resolver::SingleReferrerGraphResolver; use super::testing::TestCollector; use super::testing::TestModule; use super::text::LineIndex; use super::tsc::NavigationTree; use super::urls::uri_is_file_like; use super::urls::uri_to_file_path; use super::urls::uri_to_url; use super::urls::url_to_uri; use super::urls::COMPONENT; use crate::graph_util::CliJsrUrlProvider; #[derive(Debug)] pub struct OpenDocument { pub uri: Arc, pub text: Arc, pub line_index: Arc, pub version: i32, pub language_id: LanguageId, pub fs_version_on_open: Option, } impl OpenDocument { fn new( uri: Uri, version: i32, language_id: LanguageId, text: Arc, ) -> Self { let line_index = Arc::new(LineIndex::new(&text)); let fs_version_on_open = uri_to_file_path(&uri) .ok() .and_then(calculate_fs_version_at_path); OpenDocument { uri: Arc::new(uri), text, line_index, version, language_id, fs_version_on_open, } } fn with_change( &self, version: i32, changes: Vec, ) -> Result { let mut text = self.text.to_string(); let mut line_index = self.line_index.clone(); let mut index_valid = IndexValid::All; for change in changes { if let Some(range) = change.range { if !index_valid.covers(range.start.line) { line_index = Arc::new(LineIndex::new(&text)); } index_valid = IndexValid::UpTo(range.start.line); let range = line_index.get_text_range(range)?; text.replace_range(Range::::from(range), &change.text); } else { text = change.text; index_valid = IndexValid::UpTo(0); } } let text: Arc = text.into(); let line_index = if index_valid == IndexValid::All { line_index } else { Arc::new(LineIndex::new(&text)) }; Ok(OpenDocument { uri: self.uri.clone(), text, line_index, version, language_id: self.language_id, fs_version_on_open: self.fs_version_on_open.clone(), }) } pub fn is_diagnosable(&self) -> bool { self.language_id.is_diagnosable() } pub fn is_file_like(&self) -> bool { uri_is_file_like(&self.uri) } pub fn script_version(&self) -> String { let fs_version = self.fs_version_on_open.as_deref().unwrap_or("1"); format!("{fs_version}+{}", self.version) } } fn remote_url_to_uri(url: &Url) -> Option { if !matches!(url.scheme(), "http" | "https") { return None; } let mut string = String::with_capacity(url.as_str().len() + 6); string.push_str("deno:/"); string.push_str(url.scheme()); for p in url[Position::BeforeHost..].split('/') { string.push('/'); string.push_str( &percent_encoding::utf8_percent_encode(p, COMPONENT).to_string(), ); } Uri::from_str(&string) .inspect_err(|err| { lsp_warn!("Couldn't convert remote URL \"{url}\" to URI: {err}") }) .ok() } fn asset_url_to_uri(url: &Url) -> Option { if url.scheme() != "asset" { return None; } Uri::from_str(&format!("deno:/asset{}", url.path())) .inspect_err(|err| { lsp_warn!("Couldn't convert asset URL \"{url}\" to URI: {err}") }) .ok() } fn data_url_to_uri(url: &Url) -> Option { let data_url = deno_media_type::data_url::RawDataUrl::parse(url).ok()?; let media_type = data_url.media_type(); let extension = if media_type == MediaType::Unknown { "" } else { media_type.as_ts_extension() }; let mut file_name_str = url.path().to_string(); if let Some(query) = url.query() { file_name_str.push('?'); file_name_str.push_str(query); } let hash = deno_lib::util::checksum::gen(&[file_name_str.as_bytes()]); Uri::from_str(&format!("deno:/data_url/{hash}{extension}",)) .inspect_err(|err| { lsp_warn!("Couldn't convert data url \"{url}\" to URI: {err}") }) .ok() } #[derive(Debug, Clone)] pub enum DocumentText { Static(&'static str), Arc(Arc), } impl DocumentText { /// Will clone the string if static. pub fn to_arc(&self) -> Arc { match self { Self::Static(s) => (*s).into(), Self::Arc(s) => s.clone(), } } } impl std::ops::Deref for DocumentText { type Target = str; fn deref(&self) -> &Self::Target { match self { Self::Static(s) => s, Self::Arc(s) => s, } } } impl Serialize for DocumentText { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { (self as &str).serialize(serializer) } } #[derive(Debug, Clone)] pub enum ServerDocumentKind { Fs { fs_version: String, text: Arc, }, RemoteUrl { url: Arc, fs_cache_version: String, text: Arc, }, DataUrl { url: Arc, text: Arc, }, Asset { url: Arc, text: &'static str, }, } #[derive(Debug)] pub struct ServerDocument { pub uri: Arc, pub media_type: MediaType, pub line_index: Arc, pub kind: ServerDocumentKind, } impl ServerDocument { fn load(uri: &Uri) -> Option { let scheme = uri.scheme()?; if scheme.eq_lowercase("file") { let url = uri_to_url(uri); let path = url_to_file_path(&url).ok()?; let bytes = fs::read(&path).ok()?; let media_type = MediaType::from_specifier(&url); let text: Arc = bytes_to_content(&url, media_type, bytes, None).ok()?.into(); let fs_version = calculate_fs_version_at_path(&path)?; let line_index = Arc::new(LineIndex::new(&text)); return Some(Self { uri: Arc::new(uri.clone()), media_type, line_index, kind: ServerDocumentKind::Fs { fs_version, text }, }); } None } fn remote_url( uri: &Uri, url: Arc, scope: Option<&Url>, cache: &LspCache, ) -> Option { let media_type = MediaType::from_specifier(&url); let http_cache = cache.for_specifier(scope); let cache_key = http_cache.cache_item_key(&url).ok()?; let cache_entry = http_cache.get(&cache_key, None).ok()??; let (_, maybe_charset) = deno_graph::source::resolve_media_type_and_charset_from_headers( &url, Some(&cache_entry.metadata.headers), ); let fs_cache_version = (|| { let modified = http_cache.read_modified_time(&cache_key).ok()??; let duration = modified.duration_since(SystemTime::UNIX_EPOCH).ok()?; Some(duration.as_millis().to_string()) })() .unwrap_or_else(|| "1".to_string()); let text: Arc = bytes_to_content( &url, media_type, cache_entry.content.into_owned(), maybe_charset, ) .ok()? .into(); let line_index = Arc::new(LineIndex::new(&text)); Some(Self { uri: Arc::new(uri.clone()), media_type, line_index, kind: ServerDocumentKind::RemoteUrl { url, fs_cache_version, text, }, }) } fn asset(name: &str, text: &'static str) -> Self { let url = Arc::new(Url::parse(&format!("asset:///{name}")).unwrap()); let uri = asset_url_to_uri(&url).unwrap(); let media_type = MediaType::from_specifier(&url); let line_index = Arc::new(LineIndex::new(text)); Self { uri: Arc::new(uri), media_type, line_index, kind: ServerDocumentKind::Asset { url, text }, } } fn data_url(uri: &Uri, url: Arc) -> Option { let raw_data_url = deno_media_type::data_url::RawDataUrl::parse(&url).ok()?; let media_type = raw_data_url.media_type(); let text: Arc = raw_data_url.decode().ok()?.into(); let line_index = Arc::new(LineIndex::new(&text)); Some(Self { uri: Arc::new(uri.clone()), media_type, line_index, kind: ServerDocumentKind::DataUrl { url, text }, }) } pub fn text(&self) -> DocumentText { match &self.kind { ServerDocumentKind::Fs { text, .. } => DocumentText::Arc(text.clone()), ServerDocumentKind::RemoteUrl { text, .. } => { DocumentText::Arc(text.clone()) } ServerDocumentKind::DataUrl { text, .. } => { DocumentText::Arc(text.clone()) } ServerDocumentKind::Asset { text, .. } => DocumentText::Static(text), } } pub fn is_diagnosable(&self) -> bool { media_type_is_diagnosable(self.media_type) } pub fn is_file_like(&self) -> bool { uri_is_file_like(&self.uri) } pub fn script_version(&self) -> String { match &self.kind { ServerDocumentKind::Fs { fs_version, .. } => fs_version.clone(), ServerDocumentKind::RemoteUrl { fs_cache_version, .. } => fs_cache_version.clone(), ServerDocumentKind::DataUrl { .. } => "1".to_string(), ServerDocumentKind::Asset { .. } => "1".to_string(), } } } #[derive(Debug)] pub struct AssetDocuments { inner: HashMap, Arc>, } impl AssetDocuments { pub fn get(&self, k: &Uri) -> Option<&Arc> { self.inner.get(k) } } pub static ASSET_DOCUMENTS: Lazy = Lazy::new(|| AssetDocuments { inner: crate::tsc::LAZILY_LOADED_STATIC_ASSETS .iter() .map(|(k, v)| { let doc = Arc::new(ServerDocument::asset(k, v.as_str())); let uri = doc.uri.clone(); (uri, doc) }) .collect(), }); #[derive(Debug, Clone)] pub enum Document { Open(Arc), Server(Arc), } impl Document { pub fn open(&self) -> Option<&Arc> { match self { Self::Open(d) => Some(d), Self::Server(_) => None, } } pub fn server(&self) -> Option<&Arc> { match self { Self::Open(_) => None, Self::Server(d) => Some(d), } } pub fn uri(&self) -> &Arc { match self { Self::Open(d) => &d.uri, Self::Server(d) => &d.uri, } } pub fn text(&self) -> DocumentText { match self { Self::Open(d) => DocumentText::Arc(d.text.clone()), Self::Server(d) => d.text(), } } pub fn line_index(&self) -> &Arc { match self { Self::Open(d) => &d.line_index, Self::Server(d) => &d.line_index, } } pub fn script_version(&self) -> String { match self { Self::Open(d) => d.script_version(), Self::Server(d) => d.script_version(), } } pub fn is_diagnosable(&self) -> bool { match self { Self::Open(d) => d.is_diagnosable(), Self::Server(d) => d.is_diagnosable(), } } pub fn is_file_like(&self) -> bool { match self { Self::Open(d) => d.is_file_like(), Self::Server(d) => d.is_file_like(), } } } #[derive(Debug, Default, Clone)] pub struct Documents { open: IndexMap>, server: Arc>>, file_like_uris_by_url: Arc>>, /// These URLs can not be recovered from the URIs we assign them without these /// maps. We want to be able to discard old documents from here but keep these /// mappings. data_urls_by_uri: Arc>>, remote_urls_by_uri: Arc>>, } impl Documents { pub fn open( &mut self, uri: Uri, version: i32, language_id: LanguageId, text: Arc, ) -> Arc { self.server.remove(&uri); let doc = Arc::new(OpenDocument::new(uri.clone(), version, language_id, text)); self.open.insert(uri, doc.clone()); if !doc.uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { let url = uri_to_url(&doc.uri); if url.scheme() == "file" { self.file_like_uris_by_url.insert(url, doc.uri.clone()); } } doc } pub fn change( &mut self, uri: &Uri, version: i32, changes: Vec, ) -> Result, AnyError> { let Some((uri, doc)) = self.open.shift_remove_entry(uri) else { return Err( JsErrorBox::new( "NotFound", format!( "The URI \"{}\" does not refer to an open document.", uri.as_str() ), ) .into(), ); }; let doc = Arc::new(doc.with_change(version, changes)?); self.open.insert(uri, doc.clone()); Ok(doc) } pub fn close(&mut self, uri: &Uri) -> Result, AnyError> { self.file_like_uris_by_url.retain(|_, u| u.as_ref() != uri); self.open.shift_remove(uri).ok_or_else(|| { JsErrorBox::new( "NotFound", format!( "The URI \"{}\" does not refer to an open document.", uri.as_str() ), ) .into() }) } pub fn get(&self, uri: &Uri) -> Option { if let Some(doc) = self.open.get(uri) { return Some(Document::Open(doc.clone())); } if let Some(doc) = ASSET_DOCUMENTS.get(uri) { return Some(Document::Server(doc.clone())); } if let Some(doc) = self.server.get(uri) { return Some(Document::Server(doc.clone())); } let doc = if let Some(doc) = ServerDocument::load(uri) { doc } else if let Some(data_url) = self.data_urls_by_uri.get(uri) { ServerDocument::data_url(uri, data_url.value().clone())? } else { return None; }; let doc = Arc::new(doc); self.server.insert(uri.clone(), doc.clone()); Some(Document::Server(doc)) } /// This will not create any server entries, only retrieve existing entries. pub fn inspect(&self, uri: &Uri) -> Option { if let Some(doc) = self.open.get(uri) { return Some(Document::Open(doc.clone())); } if let Some(doc) = self.server.get(uri) { return Some(Document::Server(doc.clone())); } None } pub fn get_for_specifier( &self, specifier: &Url, scope: Option<&Url>, cache: &LspCache, ) -> Option { let scheme = specifier.scheme(); if scheme == "file" { let uri = self .file_like_uris_by_url .get(specifier) .map(|e| e.value().clone()) .or_else(|| url_to_uri(specifier).ok().map(Arc::new))?; self.get(&uri) } else if scheme == "asset" { let uri = asset_url_to_uri(specifier)?; self.get(&uri) } else if scheme == "http" || scheme == "https" { if let Some(vendored_specifier) = cache.vendored_specifier(specifier, scope) { let uri = url_to_uri(&vendored_specifier).ok()?; self.get(&uri) } else { let uri = remote_url_to_uri(specifier)?; if let Some(doc) = self.server.get(&uri) { return Some(Document::Server(doc.clone())); } let url = Arc::new(specifier.clone()); self.remote_urls_by_uri.insert(uri.clone(), url.clone()); let doc = Arc::new(ServerDocument::remote_url(&uri, url, scope, cache)?); self.server.insert(uri, doc.clone()); Some(Document::Server(doc)) } } else if scheme == "data" { let uri = data_url_to_uri(specifier)?; if let Some(doc) = self.server.get(&uri) { return Some(Document::Server(doc.clone())); } let url = Arc::new(specifier.clone()); self.data_urls_by_uri.insert(uri.clone(), url.clone()); let doc = Arc::new(ServerDocument::data_url(&uri, url)?); self.server.insert(uri, doc.clone()); Some(Document::Server(doc)) } else { None } } pub fn open_docs(&self) -> impl Iterator> { self.open.values() } pub fn server_docs(&self) -> Vec> { self.server.iter().map(|e| e.value().clone()).collect() } pub fn docs(&self) -> Vec { self .open .values() .map(|d| Document::Open(d.clone())) .chain( self .server .iter() .map(|e| Document::Server(e.value().clone())), ) .collect() } pub fn filtered_docs( &self, predicate: impl FnMut(&Document) -> bool, ) -> Vec { self .open .values() .map(|d| Document::Open(d.clone())) .chain( self .server .iter() .map(|e| Document::Server(e.value().clone())), ) .filter(predicate) .collect() } pub fn remove_server_doc(&self, uri: &Uri) { self.server.remove(uri); } } #[derive(Debug)] pub struct DocumentModuleOpenData { pub version: i32, pub parsed_source: Option, } #[derive(Debug)] pub struct DocumentModule { pub uri: Arc, pub open_data: Option, pub script_version: String, pub specifier: Arc, pub scope: Option>, pub media_type: MediaType, pub headers: Option>, pub text: DocumentText, pub line_index: Arc, pub resolution_mode: ResolutionMode, pub dependencies: Arc>, pub types_dependency: Option>, pub navigation_tree: tokio::sync::OnceCell>, pub semantic_tokens_full: tokio::sync::OnceCell, text_info_cell: once_cell::sync::OnceCell, test_module_fut: Option, } impl DocumentModule { pub fn new( document: &Document, specifier: Arc, scope: Option>, resolver: &LspResolver, config: &Config, cache: &LspCache, ) -> Self { let text = document.text(); let headers = matches!(specifier.scheme(), "http" | "https") .then(|| { let http_cache = cache.for_specifier(scope.as_deref()); let cache_key = http_cache.cache_item_key(&specifier).ok()?; let cache_entry = http_cache.get(&cache_key, None).ok()??; Some(cache_entry.metadata.headers) }) .flatten(); let media_type = resolve_media_type( &specifier, headers.as_ref(), document.open().map(|d| d.language_id), ); let (parsed_source, maybe_module, resolution_mode) = if media_type_is_diagnosable(media_type) { parse_and_analyze_module( specifier.as_ref().clone(), text.to_arc(), headers.as_ref(), media_type, scope.as_deref(), resolver, ) } else { (None, None, ResolutionMode::Import) }; let maybe_module = maybe_module.and_then(Result::ok); let dependencies = maybe_module .as_ref() .map(|m| Arc::new(m.dependencies.clone())) .unwrap_or_default(); let types_dependency = maybe_module .as_ref() .and_then(|m| Some(Arc::new(m.maybe_types_dependency.clone()?))); let test_module_fut = get_maybe_test_module_fut(parsed_source.as_ref(), config); DocumentModule { uri: document.uri().clone(), open_data: document.open().map(|d| DocumentModuleOpenData { version: d.version, parsed_source, }), script_version: document.script_version(), specifier, scope, media_type, headers, text, line_index: document.line_index().clone(), resolution_mode, dependencies, types_dependency, navigation_tree: Default::default(), semantic_tokens_full: Default::default(), text_info_cell: Default::default(), test_module_fut, } } pub fn is_diagnosable(&self) -> bool { media_type_is_diagnosable(self.media_type) } pub fn dependency_at_position( &self, position: &lsp::Position, ) -> Option<(&str, &deno_graph::Dependency, &deno_graph::Range)> { let position = deno_graph::Position { line: position.line as usize, character: position.character as usize, }; self .dependencies .iter() .find_map(|(s, dep)| dep.includes(position).map(|r| (s.as_str(), dep, r))) } pub fn text_info(&self) -> &SourceTextInfo { // try to get the text info from the parsed source and if // not then create one in the cell self .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) .and_then(|p| p.as_ref().ok()) .map(|p| p.text_info_lazy()) .unwrap_or_else(|| { self .text_info_cell .get_or_init(|| SourceTextInfo::new(self.text.to_arc())) }) } pub async fn test_module(&self) -> Option> { self.test_module_fut.clone()?.await } } type DepInfoByScope = BTreeMap>, Arc>; #[derive(Debug, Default)] struct WeakDocumentModuleMap { open: RwLock, Arc>>, server: RwLock, Arc>>, by_specifier: RwLock, Weak>>, } impl WeakDocumentModuleMap { fn get(&self, document: &Document) -> Option> { match document { Document::Open(d) => self.open.read().get(d).cloned(), Document::Server(d) => self.server.read().get(d).cloned(), } } fn get_for_specifier(&self, specifier: &Url) -> Option> { self.by_specifier.read().get(specifier) } fn contains_specifier(&self, specifier: &Url) -> bool { self.by_specifier.read().contains_key(specifier) } fn inspect_values(&self) -> Vec> { self .open .read() .values() .cloned() .chain(self.server.read().values().cloned()) .collect() } fn insert( &self, document: &Document, module: Arc, ) -> Option> { match document { Document::Open(d) => { self.open.write().insert(d.clone(), module.clone()); } Document::Server(d) => { self.server.write().insert(d.clone(), module.clone()); } } self .by_specifier .write() .insert(module.specifier.clone(), module.clone()); Some(module) } fn remove_expired(&self) { // IMPORTANT: Maintain this order based on weak ref relations. self.open.write().remove_expired(); self.server.write().remove_expired(); self.by_specifier.write().remove_expired(); } } #[derive(Debug, Default, Clone)] pub struct DocumentModules { pub documents: Documents, config: Arc, resolver: Arc, cache: Arc, workspace_files: Arc>, dep_info_by_scope: once_cell::sync::OnceCell>, modules_unscoped: Arc, modules_by_scope: Arc, Arc>>, } impl DocumentModules { pub fn update_config( &mut self, config: &Config, resolver: &Arc, cache: &LspCache, workspace_files: &Arc>, ) { self.config = Arc::new(config.clone()); self.cache = Arc::new(cache.clone()); self.resolver = resolver.clone(); self.workspace_files = workspace_files.clone(); self.modules_unscoped = Default::default(); self.modules_by_scope = Arc::new( self .config .tree .data_by_scope() .keys() .map(|s| (s.clone(), Default::default())) .collect(), ); self.dep_info_by_scope = Default::default(); node_resolver::PackageJsonThreadLocalCache::clear(); NodeResolutionThreadLocalCache::clear(); // Clean up non-existent documents. self.documents.server.retain(|_, d| { let Some(module) = self.inspect_primary_module(&Document::Server(d.clone())) else { return false; }; let Ok(path) = url_to_file_path(&module.specifier) else { // Remove non-file schemed docs (deps). They may not be dependencies // anymore after updating resolvers. return false; }; if !config.specifier_enabled(&module.specifier) { return false; } path.is_file() }); } pub fn open_document( &mut self, uri: Uri, version: i32, language_id: LanguageId, text: Arc, ) -> Arc { self.dep_info_by_scope = Default::default(); self.documents.open(uri, version, language_id, text) } pub fn change_document( &mut self, uri: &Uri, version: i32, changes: Vec, ) -> Result, AnyError> { self.dep_info_by_scope = Default::default(); let document = self.documents.change(uri, version, changes)?; Ok(document) } /// Returns if the document is diagnosable. pub fn close_document( &mut self, uri: &Uri, ) -> Result, AnyError> { self.dep_info_by_scope = Default::default(); let document = self.documents.close(uri)?; // If applicable, try to load the closed document as a server document so // it's still included as a ts root etc.. if uri.scheme().is_some_and(|s| s.eq_lowercase("file")) && self.config.uri_enabled(uri) { self.documents.get(uri); } Ok(document) } pub fn release(&self, specifier: &Url, scope: Option<&Url>) { let Some(module) = self.module_for_specifier(specifier, scope) else { return; }; self.documents.remove_server_doc(&module.uri); } fn module_inner( &self, document: &Document, specifier: Option<&Arc>, scope: Option<&Url>, ) -> Option> { let modules = self.modules_for_scope(scope)?; if let Some(module) = modules.get(document) { return Some(module); } let specifier = specifier .cloned() .or_else(|| { if let Some(document) = document.server() { match &document.kind { ServerDocumentKind::Fs { .. } => {} ServerDocumentKind::RemoteUrl { url, .. } => { return Some(url.clone()) } ServerDocumentKind::DataUrl { url, .. } => { return Some(url.clone()) } ServerDocumentKind::Asset { url, .. } => return Some(url.clone()), } } None }) .or_else(|| { let uri = document.uri(); let url = uri_to_url(uri); if url.scheme() != "file" { return None; } if uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { if let Some(remote_specifier) = self.cache.unvendored_specifier(&url) { return Some(Arc::new(remote_specifier)); } } Some(Arc::new(url)) })?; let module = Arc::new(DocumentModule::new( document, specifier, scope.cloned().map(Arc::new), &self.resolver, &self.config, &self.cache, )); modules.insert(document, module.clone()); Some(module) } pub fn module( &self, document: &Document, scope: Option<&Url>, ) -> Option> { self.module_inner(document, None, scope) } pub fn module_for_specifier( &self, specifier: &Url, scope: Option<&Url>, ) -> Option> { let specifier = if let Ok(jsr_req_ref) = JsrPackageReqReference::from_specifier(specifier) { Cow::Owned(self.resolver.jsr_to_resource_url(&jsr_req_ref, scope)?) } else { Cow::Borrowed(specifier) }; let specifier = self.resolver.resolve_redirects(&specifier, scope)?; let document = self .documents .get_for_specifier(&specifier, scope, &self.cache)?; self.module_inner(&document, Some(&Arc::new(specifier)), scope) } pub fn primary_module( &self, document: &Document, ) -> Option> { if let Some(scope) = self.primary_scope(document.uri()) { return self.module(document, scope.map(|s| s.as_ref())); } for modules in self.modules_by_scope.values() { if let Some(module) = modules.get(document) { return Some(module); } } self.modules_unscoped.get(document) } pub fn workspace_file_modules_by_scope( &self, ) -> BTreeMap>, Vec>> { let mut modules_with_scopes = BTreeMap::new(); for path in self .workspace_files .iter() .take(self.config.settings.unscoped.document_preload_limit) { let Ok(url) = Url::from_file_path(path) else { continue; }; let scope = self.config.tree.scope_for_specifier(&url).cloned(); let Some(document) = self .documents .get_for_specifier(&url, scope.as_deref(), &self.cache) else { continue; }; if document.open().is_none() && (!self.config.specifier_enabled(&url) || self.resolver.in_node_modules(&url) || self.cache.in_cache_directory(&url)) { continue; } let Some(module) = self.module(&document, scope.as_deref()) else { continue; }; modules_with_scopes.insert(document.uri().clone(), (module, scope)); } // Include files that aren't in `self.workspace_files` for whatever reason. for document in self.documents.docs() { let uri = document.uri(); if modules_with_scopes.contains_key(uri) { continue; } let url = uri_to_url(uri); if document.open().is_none() && (url.scheme() != "file" || !self.config.specifier_enabled(&url) || self.resolver.in_node_modules(&url) || self.cache.in_cache_directory(&url)) { continue; } let scope = self.config.tree.scope_for_specifier(&url).cloned(); let Some(module) = self.module(&document, scope.as_deref()) else { continue; }; modules_with_scopes.insert(document.uri().clone(), (module, scope)); } let mut result = BTreeMap::new(); for (module, scope) in modules_with_scopes.into_values() { (result.entry(scope).or_default() as &mut Vec<_>).push(module); } result } /// This will not create any module entries, only retrieve existing entries. pub fn inspect_module_for_specifier( &self, specifier: &Url, scope: Option<&Url>, ) -> Option> { let specifier = if let Ok(jsr_req_ref) = JsrPackageReqReference::from_specifier(specifier) { Cow::Owned(self.resolver.jsr_to_resource_url(&jsr_req_ref, scope)?) } else { Cow::Borrowed(specifier) }; let specifier = self.resolver.resolve_redirects(&specifier, scope)?; let modules = self.modules_for_scope(scope)?; modules.get_for_specifier(&specifier) } /// This will not create any module entries, only retrieve existing entries. pub fn inspect_primary_module( &self, document: &Document, ) -> Option> { if let Some(scope) = self.primary_scope(document.uri()) { return self .modules_for_scope(scope.map(|s| s.as_ref()))? .get(document); } for modules in self.modules_by_scope.values() { if let Some(module) = modules.get(document) { return Some(module); } } self.modules_unscoped.get(document) } /// This will not create any module entries, only retrieve existing entries. pub fn inspect_modules_by_scope( &self, document: &Document, ) -> BTreeMap>, Arc> { let mut result = BTreeMap::new(); for (scope, modules) in self.modules_by_scope.iter() { if let Some(module) = modules.get(document) { result.insert(Some(scope.clone()), module); } } if let Some(module) = self.modules_unscoped.get(document) { result.insert(None, module); } result } /// This will not store any module entries, only retrieve existing entries or /// create temporary entries for scopes where one doesn't exist. pub fn inspect_or_temp_modules_by_scope( &self, document: &Document, ) -> BTreeMap>, Arc> { let mut result = BTreeMap::new(); for (scope, modules) in self.modules_by_scope.iter() { let module = modules.get(document).unwrap_or_else(|| { Arc::new(DocumentModule::new( document, Arc::new(uri_to_url(document.uri())), Some(scope.clone()), &self.resolver, &self.config, &self.cache, )) }); result.insert(Some(scope.clone()), module); } let module = self.modules_unscoped.get(document).unwrap_or_else(|| { Arc::new(DocumentModule::new( document, Arc::new(uri_to_url(document.uri())), None, &self.resolver, &self.config, &self.cache, )) }); result.insert(None, module); result } fn modules_for_scope( &self, scope: Option<&Url>, ) -> Option<&Arc> { match scope { Some(s) => Some(self.modules_by_scope.get(s)?), None => Some(&self.modules_unscoped), } } fn primary_scope(&self, uri: &Uri) -> Option>> { let url = uri_to_url(uri); if url.scheme() == "file" && !self.cache.in_global_cache_directory(&url) { let scope = self.config.tree.scope_for_specifier(&url); return Some(scope); } None } pub fn remove_expired_modules(&self) { self.modules_unscoped.remove_expired(); for modules in self.modules_by_scope.values() { modules.remove_expired(); } } pub fn scopes(&self) -> BTreeSet>> { self .modules_by_scope .keys() .cloned() .map(Some) .chain([None]) .collect() } pub fn specifier_exists(&self, specifier: &Url, scope: Option<&Url>) -> bool { if let Some(modules) = self.modules_for_scope(scope) { if modules.contains_specifier(specifier) { return true; } } if specifier.scheme() == "file" { return url_to_file_path(specifier) .map(|p| p.is_file()) .unwrap_or(false); } if specifier.scheme() == "data" { return true; } if self.cache.for_specifier(scope).contains(specifier) { return true; } false } pub fn dep_info_by_scope( &self, ) -> Arc>, Arc>> { type ScopeEntry<'a> = (Option<&'a Arc>, &'a Arc); let dep_info_from_scope_entry = |(scope, modules): ScopeEntry<'_>| { let mut dep_info = ScopeDepInfo::default(); let mut visit_module = |module: &DocumentModule| { for dependency in module.dependencies.values() { let code_specifier = dependency.get_code(); let type_specifier = dependency.get_type(); if let Some(dep) = code_specifier { if dep.scheme() == "node" { dep_info.has_node_specifier = true; } if let Ok(reference) = NpmPackageReqReference::from_specifier(dep) { dep_info.npm_reqs.insert(reference.into_inner().req); } } if let Some(dep) = type_specifier { if let Ok(reference) = NpmPackageReqReference::from_specifier(dep) { dep_info.npm_reqs.insert(reference.into_inner().req); } } if dependency.maybe_deno_types_specifier.is_some() { if let (Some(code_specifier), Some(type_specifier)) = (code_specifier, type_specifier) { if MediaType::from_specifier(type_specifier).is_declaration() { dep_info .deno_types_to_code_resolutions .insert(type_specifier.clone(), code_specifier.clone()); } } } } if let Some(dep) = module .types_dependency .as_ref() .and_then(|d| d.dependency.maybe_specifier()) { if let Ok(reference) = NpmPackageReqReference::from_specifier(dep) { dep_info.npm_reqs.insert(reference.into_inner().req); } } }; for module in modules.inspect_values() { visit_module(&module); } let config_data = scope.and_then(|s| self.config.tree.data_by_scope().get(s)); if let Some(config_data) = config_data { (|| { let member_dir = &config_data.member_dir; let jsx_config = member_dir.to_maybe_jsx_import_source_config().ok()??; let import_source_types = jsx_config.import_source_types.as_ref()?; let import_source = jsx_config.import_source.as_ref()?; let cli_resolver = self.resolver.as_cli_resolver(scope.map(|s| s.as_ref())); let type_specifier = cli_resolver .resolve( &import_source_types.specifier, &import_source_types.base, deno_graph::Position::zeroed(), // todo(dsherret): this is wrong because it doesn't consider CJS referrers ResolutionMode::Import, NodeResolutionKind::Types, ) .ok()?; let code_specifier = cli_resolver .resolve( &import_source.specifier, &import_source.base, deno_graph::Position::zeroed(), // todo(dsherret): this is wrong because it doesn't consider CJS referrers ResolutionMode::Import, NodeResolutionKind::Execution, ) .ok()?; dep_info .deno_types_to_code_resolutions .insert(type_specifier, code_specifier); Some(()) })(); // fill the reqs from the lockfile if let Some(lockfile) = config_data.lockfile.as_ref() { let lockfile = lockfile.lock(); for dep_req in lockfile.content.packages.specifiers.keys() { if dep_req.kind == deno_semver::package::PackageKind::Npm { dep_info.npm_reqs.insert(dep_req.req.clone()); } } } } if dep_info.has_node_specifier && !dep_info.npm_reqs.iter().any(|r| r.name == "@types/node") { dep_info .npm_reqs .insert(PackageReq::from_str("@types/node").unwrap()); } (scope.cloned(), Arc::new(dep_info)) }; self .dep_info_by_scope .get_or_init(|| { NodeResolutionThreadLocalCache::clear(); // Ensure at least module entries for workspace files are initialized. self.workspace_file_modules_by_scope(); Arc::new( self .modules_by_scope .iter() .map(|(s, m)| (Some(s), m)) .chain([(None, &self.modules_unscoped)]) .map(dep_info_from_scope_entry) .collect(), ) }) .clone() } pub fn scopes_with_node_specifier(&self) -> HashSet>> { self .dep_info_by_scope() .iter() .filter(|(_, i)| i.has_node_specifier) .map(|(s, _)| s.clone()) .collect::>() } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn resolve( &self, // (is_cjs: bool, raw_specifier: String) raw_specifiers: &[(bool, String)], referrer: &Url, scope: Option<&Url>, ) -> Vec> { let referrer_module = self.module_for_specifier(referrer, scope); let dependencies = referrer_module.as_ref().map(|d| &d.dependencies); let mut results = Vec::new(); for (is_cjs, raw_specifier) in raw_specifiers { let resolution_mode = match is_cjs { true => ResolutionMode::Require, false => ResolutionMode::Import, }; if raw_specifier.starts_with("asset:") { if let Ok(specifier) = resolve_url(raw_specifier) { let media_type = MediaType::from_specifier(&specifier); results.push(Some((specifier, media_type))); } else { results.push(None); } } else if let Some(dep) = dependencies.as_ref().and_then(|d| d.get(raw_specifier)) { if let Some(specifier) = dep.maybe_type.maybe_specifier() { results.push(self.resolve_dependency( specifier, referrer, resolution_mode, scope, )); } else if let Some(specifier) = dep.maybe_code.maybe_specifier() { results.push(self.resolve_dependency( specifier, referrer, resolution_mode, scope, )); } else { results.push(None); } } else if let Ok(specifier) = self.resolver.as_cli_resolver(scope).resolve( raw_specifier, referrer, deno_graph::Position::zeroed(), resolution_mode, NodeResolutionKind::Types, ) { results.push(self.resolve_dependency( &specifier, referrer, resolution_mode, scope, )); } else { results.push(None); } } results } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub fn resolve_dependency( &self, specifier: &Url, referrer: &Url, resolution_mode: ResolutionMode, scope: Option<&Url>, ) -> Option<(Url, MediaType)> { if let Some(module_name) = specifier.as_str().strip_prefix("node:") { if deno_node::is_builtin_node_module(module_name) { // return itself for node: specifiers because during type checking // we resolve to the ambient modules in the @types/node package // rather than deno_std/node return Some((specifier.clone(), MediaType::Dts)); } } let mut specifier = specifier.clone(); let mut media_type = None; if let Ok(npm_ref) = NpmPackageReqReference::from_specifier(&specifier) { let (s, mt) = self.resolver.npm_to_file_url( &npm_ref, referrer, resolution_mode, scope, )?; specifier = s; media_type = Some(mt); } let Some(module) = self.module_for_specifier(&specifier, scope) else { let media_type = media_type.unwrap_or_else(|| MediaType::from_specifier(&specifier)); return Some((specifier, media_type)); }; if let Some(types) = module .types_dependency .as_ref() .and_then(|d| d.dependency.maybe_specifier()) { self.resolve_dependency(types, &specifier, module.resolution_mode, scope) } else { Some((module.specifier.as_ref().clone(), module.media_type)) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LanguageId { JavaScript, Jsx, TypeScript, Tsx, Json, JsonC, Markdown, Html, Css, Scss, Sass, Less, Yaml, Sql, Svelte, Vue, Astro, Vento, Nunjucks, Unknown, } impl LanguageId { pub fn as_extension(&self) -> Option<&'static str> { match self { LanguageId::JavaScript => Some("js"), LanguageId::Jsx => Some("jsx"), LanguageId::TypeScript => Some("ts"), LanguageId::Tsx => Some("tsx"), LanguageId::Json => Some("json"), LanguageId::JsonC => Some("jsonc"), LanguageId::Markdown => Some("md"), LanguageId::Html => Some("html"), LanguageId::Css => Some("css"), LanguageId::Scss => Some("scss"), LanguageId::Sass => Some("sass"), LanguageId::Less => Some("less"), LanguageId::Yaml => Some("yaml"), LanguageId::Sql => Some("sql"), LanguageId::Svelte => Some("svelte"), LanguageId::Vue => Some("vue"), LanguageId::Astro => Some("astro"), LanguageId::Vento => Some("vto"), LanguageId::Nunjucks => Some("njk"), LanguageId::Unknown => None, } } pub fn as_content_type(&self) -> Option<&'static str> { match self { LanguageId::JavaScript => Some("application/javascript"), LanguageId::Jsx => Some("text/jsx"), LanguageId::TypeScript => Some("application/typescript"), LanguageId::Tsx => Some("text/tsx"), LanguageId::Json | LanguageId::JsonC => Some("application/json"), LanguageId::Markdown => Some("text/markdown"), LanguageId::Html => Some("text/html"), LanguageId::Css => Some("text/css"), LanguageId::Scss => None, LanguageId::Sass => None, LanguageId::Less => None, LanguageId::Yaml => Some("application/yaml"), LanguageId::Sql => None, LanguageId::Svelte => None, LanguageId::Vue => None, LanguageId::Astro => None, LanguageId::Vento => None, LanguageId::Nunjucks => None, LanguageId::Unknown => None, } } fn is_diagnosable(&self) -> bool { matches!( self, Self::JavaScript | Self::Jsx | Self::TypeScript | Self::Tsx ) } } impl FromStr for LanguageId { type Err = AnyError; fn from_str(s: &str) -> Result { match s { "javascript" => Ok(Self::JavaScript), "javascriptreact" | "jsx" => Ok(Self::Jsx), "typescript" => Ok(Self::TypeScript), "typescriptreact" | "tsx" => Ok(Self::Tsx), "json" => Ok(Self::Json), "jsonc" => Ok(Self::JsonC), "markdown" => Ok(Self::Markdown), "html" => Ok(Self::Html), "css" => Ok(Self::Css), "scss" => Ok(Self::Scss), "sass" => Ok(Self::Sass), "less" => Ok(Self::Less), "yaml" => Ok(Self::Yaml), "sql" => Ok(Self::Sql), "svelte" => Ok(Self::Svelte), "vue" => Ok(Self::Vue), "astro" => Ok(Self::Astro), "vento" => Ok(Self::Vento), "nunjucks" => Ok(Self::Nunjucks), _ => Ok(Self::Unknown), } } } #[derive(Debug, PartialEq, Eq)] enum IndexValid { All, UpTo(u32), } impl IndexValid { fn covers(&self, line: u32) -> bool { match *self { IndexValid::UpTo(to) => to > line, IndexValid::All => true, } } } type ModuleResult = Result; type ParsedSourceResult = Result; type TestModuleFut = Shared>> + Send>>>; fn media_type_is_diagnosable(media_type: MediaType) -> bool { matches!( media_type, MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs | MediaType::TypeScript | MediaType::Tsx | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts ) } fn get_maybe_test_module_fut( maybe_parsed_source: Option<&ParsedSourceResult>, config: &Config, ) -> Option { if !config.testing_api_capable() { return None; } let parsed_source = maybe_parsed_source?.as_ref().ok()?.clone(); let specifier = parsed_source.specifier(); if specifier.scheme() != "file" || specifier.as_str().contains("/node_modules/") { return None; } if !media_type_is_diagnosable(parsed_source.media_type()) { return None; } if !config.specifier_enabled_for_test(specifier) { return None; } let handle = tokio::task::spawn_blocking(move || { let mut collector = TestCollector::new( parsed_source.specifier().clone(), parsed_source.text_info_lazy().clone(), ); parsed_source.program().visit_with(&mut collector); Arc::new(collector.take()) }) .map(Result::ok) .boxed() .shared(); Some(handle) } fn resolve_media_type( specifier: &ModuleSpecifier, maybe_headers: Option<&HashMap>, maybe_language_id: Option, ) -> MediaType { if let Some(language_id) = maybe_language_id { return MediaType::from_specifier_and_content_type( specifier, language_id.as_content_type(), ); } if maybe_headers.is_some() { return MediaType::from_specifier_and_headers(specifier, maybe_headers); } MediaType::from_specifier(specifier) } /// Loader that will look at the open documents. pub struct OpenDocumentsGraphLoader<'a> { pub inner_loader: &'a mut dyn deno_graph::source::Loader, pub open_modules: &'a HashMap, Arc>, } impl OpenDocumentsGraphLoader<'_> { fn load_from_docs( &self, specifier: &ModuleSpecifier, ) -> Option { if specifier.scheme() == "file" { if let Some(doc) = self.open_modules.get(specifier) { return Some( future::ready(Ok(Some(deno_graph::source::LoadResponse::Module { content: Arc::from(doc.text.as_bytes().to_owned()), specifier: doc.specifier.as_ref().clone(), maybe_headers: None, }))) .boxed_local(), ); } } None } } impl deno_graph::source::Loader for OpenDocumentsGraphLoader<'_> { fn load( &self, specifier: &ModuleSpecifier, options: deno_graph::source::LoadOptions, ) -> deno_graph::source::LoadFuture { match self.load_from_docs(specifier) { Some(fut) => fut, None => self.inner_loader.load(specifier, options), } } fn cache_module_info( &self, specifier: &deno_ast::ModuleSpecifier, media_type: MediaType, source: &Arc<[u8]>, module_info: &deno_graph::ModuleInfo, ) { self.inner_loader.cache_module_info( specifier, media_type, source, module_info, ) } } fn parse_and_analyze_module( specifier: ModuleSpecifier, text: Arc, maybe_headers: Option<&HashMap>, media_type: MediaType, file_referrer: Option<&ModuleSpecifier>, resolver: &LspResolver, ) -> ( Option, Option, ResolutionMode, ) { let parsed_source_result = parse_source(specifier.clone(), text, media_type); let (module_result, resolution_mode) = analyze_module( specifier, &parsed_source_result, maybe_headers, file_referrer, resolver, ); ( Some(parsed_source_result), Some(module_result), resolution_mode, ) } fn parse_source( specifier: ModuleSpecifier, text: Arc, media_type: MediaType, ) -> ParsedSourceResult { deno_ast::parse_program(deno_ast::ParseParams { specifier, text, media_type, capture_tokens: true, scope_analysis: true, maybe_syntax: None, }) } fn analyze_module( specifier: ModuleSpecifier, parsed_source_result: &ParsedSourceResult, maybe_headers: Option<&HashMap>, file_referrer: Option<&ModuleSpecifier>, resolver: &LspResolver, ) -> (ModuleResult, ResolutionMode) { match parsed_source_result { Ok(parsed_source) => { let npm_resolver = resolver.as_graph_npm_resolver(file_referrer); let cli_resolver = resolver.as_cli_resolver(file_referrer); let is_cjs_resolver = resolver.as_is_cjs_resolver(file_referrer); let config_data = resolver.as_config_data(file_referrer); let valid_referrer = specifier.clone(); let jsx_import_source_config = config_data.and_then(|d| d.maybe_jsx_import_source_config()); let module_resolution_mode = is_cjs_resolver.get_lsp_resolution_mode( &specifier, Some(parsed_source.compute_is_script()), ); let resolver = SingleReferrerGraphResolver { valid_referrer: &valid_referrer, module_resolution_mode, cli_resolver, jsx_import_source_config: jsx_import_source_config.as_ref(), }; ( Ok(deno_graph::parse_module_from_ast( deno_graph::ParseModuleFromAstOptions { graph_kind: deno_graph::GraphKind::TypesOnly, specifier, maybe_headers, parsed_source, // use a null file system because there's no need to bother resolving // dynamic imports like import(`./dir/${something}`) in the LSP file_system: &deno_graph::source::NullFileSystem, jsr_url_provider: &CliJsrUrlProvider, maybe_resolver: Some(&resolver), maybe_npm_resolver: Some(npm_resolver.as_ref()), }, )), module_resolution_mode, ) } Err(err) => ( Err(deno_graph::ModuleGraphError::ModuleError( deno_graph::ModuleError::ParseErr(specifier, err.clone()), )), ResolutionMode::Import, ), } } fn bytes_to_content( specifier: &ModuleSpecifier, media_type: MediaType, bytes: Vec, maybe_charset: Option<&str>, ) -> Result { if media_type == MediaType::Wasm { // we use the dts representation for Wasm modules Ok(deno_graph::source::wasm::wasm_module_to_dts(&bytes)?) } else { let charset = maybe_charset.unwrap_or_else(|| { deno_media_type::encoding::detect_charset(specifier, &bytes) }); Ok(deno_media_type::encoding::decode_owned_source( charset, bytes, )?) } } #[cfg(test)] mod tests { use deno_config::deno_json::ConfigFile; use deno_core::serde_json; use deno_core::serde_json::json; use pretty_assertions::assert_eq; use test_util::TempDir; use super::*; use crate::lsp::cache::LspCache; async fn setup() -> (DocumentModules, LspCache, TempDir) { let temp_dir = TempDir::new(); temp_dir.create_dir_all(".deno_dir"); let cache = LspCache::new(Some(temp_dir.url().join(".deno_dir").unwrap())); let config = Config::default(); let resolver = Arc::new(LspResolver::from_config(&config, &cache, None).await); let mut document_modules = DocumentModules::default(); document_modules.update_config( &config, &resolver, &cache, &Default::default(), ); (document_modules, cache, temp_dir) } #[tokio::test] async fn test_documents_open_close() { let (mut document_modules, _, _) = setup().await; let uri = Uri::from_str("file:///a.ts").unwrap(); let content = r#"import * as b from "./b.ts"; console.log(b); "#; document_modules.open_document( uri.clone(), 1, "javascript".parse().unwrap(), content.into(), ); let document = document_modules .documents .get(&uri) .unwrap() .open() .cloned() .unwrap(); assert_eq!(document.uri.as_ref(), &uri); assert_eq!(document.text.as_ref(), content); assert_eq!(document.version, 1); assert_eq!(document.language_id, LanguageId::JavaScript); assert!(document.is_diagnosable()); assert!(document.is_file_like()); document_modules.close_document(&uri).unwrap(); assert!(document_modules.documents.get(&uri).is_none()); } #[tokio::test] async fn test_documents_change() { let (mut document_modules, _, _) = setup().await; let uri = Uri::from_str("file:///a.ts").unwrap(); let content = r#"import * as b from "./b.ts"; console.log(b); "#; document_modules.open_document( uri.clone(), 1, "javascript".parse().unwrap(), content.into(), ); document_modules .change_document( &uri, 2, vec![lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range { start: lsp::Position { line: 1, character: 13, }, end: lsp::Position { line: 1, character: 13, }, }), range_length: None, text: r#", "hello deno""#.to_string(), }], ) .unwrap(); assert_eq!( document_modules .documents .get(&uri) .unwrap() .text() .as_ref() as &str, r#"import * as b from "./b.ts"; console.log(b, "hello deno"); "# ); } #[tokio::test] async fn test_documents_refresh_dependencies_config_change() { // it should never happen that a user of this API causes this to happen, // but we'll guard against it anyway let (mut document_modules, cache, temp_dir) = setup().await; let file1_path = temp_dir.path().join("file1.ts"); let file1_specifier = temp_dir.url().join("file1.ts").unwrap(); fs::write(&file1_path, "").unwrap(); let file2_path = temp_dir.path().join("file2.ts"); let file2_specifier = temp_dir.url().join("file2.ts").unwrap(); fs::write(&file2_path, "").unwrap(); let file3_path = temp_dir.path().join("file3.ts"); let file3_specifier = temp_dir.url().join("file3.ts").unwrap(); fs::write(&file3_path, "").unwrap(); let mut config = Config::new_with_roots([temp_dir.url()]); let workspace_settings = serde_json::from_str(r#"{ "enable": true }"#).unwrap(); config.set_workspace_settings(workspace_settings, vec![]); let workspace_files = Arc::new( [&file1_specifier, &file2_specifier, &file3_specifier] .into_iter() .map(|s| s.to_file_path().unwrap()) .collect::>(), ); let document = document_modules.open_document( url_to_uri(&file1_specifier).unwrap(), 1, LanguageId::TypeScript, "import {} from 'test';".into(), ); // set the initial import map and point to file 2 { config .tree .inject_config_file( ConfigFile::new( &json!({ "imports": { "test": "./file2.ts", }, }) .to_string(), config.root_url().unwrap().join("deno.json").unwrap(), ) .unwrap(), ) .await; let resolver = Arc::new(LspResolver::from_config(&config, &cache, None).await); document_modules.update_config( &config, &resolver, &cache, &workspace_files, ); let module = document_modules .primary_module(&Document::Open(document.clone())) .unwrap(); assert_eq!( module .dependencies .get("test") .unwrap() .maybe_code .maybe_specifier() .map(ToOwned::to_owned), Some(file2_specifier), ); } // now point at file 3 { config .tree .inject_config_file( ConfigFile::new( &json!({ "imports": { "test": "./file3.ts", }, }) .to_string(), config.root_url().unwrap().join("deno.json").unwrap(), ) .unwrap(), ) .await; let resolver = Arc::new(LspResolver::from_config(&config, &cache, None).await); document_modules.update_config( &config, &resolver, &cache, &workspace_files, ); // check the document's dependencies let module = document_modules .primary_module(&Document::Open(document.clone())) .unwrap(); assert_eq!( module .dependencies .get("test") .unwrap() .maybe_code .maybe_specifier() .map(ToOwned::to_owned), Some(file3_specifier), ); } } }