/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/documents.rs
2 631 строка
75 KB
Fibi
fix(lsp): include tsconfig roots in script names (#34136)
17 июн 2026, 10:36
Не верифицирован
17 июн 2026, 10:36
792bea4
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::fs; use std::future::Future; use std::ops::Range; use std::panic::AssertUnwindSafe; 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::MediaType; use deno_ast::ParsedSource; use deno_ast::SourceTextInfo; use deno_ast::swc::ecma_visit::VisitWith; use deno_core::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::futures::future; use deno_core::futures::future::Shared; use deno_core::parking_lot::RwLock; use deno_core::resolve_url; use deno_core::url::Position; use deno_core::url::Url; use deno_error::JsErrorBox; use deno_graph::TypesDependency; use deno_path_util::url_to_file_path; use deno_resolver::deno_json::CompilerOptionsKey; use deno_runtime::deno_node; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use indexmap::IndexMap; use indexmap::IndexSet; use lsp_types::Uri; use node_resolver::NodeResolutionKind; use node_resolver::ResolutionMode; use node_resolver::cache::NodeResolutionThreadLocalCache; use once_cell::sync::Lazy; use tower_lsp::lsp_types as lsp; use super::cache::LspCache; use super::cache::calculate_fs_version_at_path; 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::ChangeKind; use super::tsc::NavigationTree; use super::urls::normalize_uri; use super::urls::uri_is_file_like; use super::urls::uri_is_in_memory; use super::urls::uri_to_file_path; use super::urls::uri_to_url; use super::urls::url_to_uri; use crate::graph_util::CliJsrUrlProvider; use crate::lsp::compiler_options::LspCompilerOptionsData; use crate::lsp::compiler_options::LspCompilerOptionsResolver; use crate::tsc::BYTES_IMPORT_SOURCE; use crate::tsc::CSS_IMPORT_SOURCE; use crate::tsc::RawImportKind; use crate::tsc::TEXT_IMPORT_SOURCE; #[derive(Debug)] pub struct OpenDocument { pub uri: Arc<Uri>, pub text: Arc<str>, pub line_index: Arc<LineIndex>, pub version: i32, pub language_id: LanguageId, pub notebook_uri: Option<Arc<Uri>>, pub fs_version_on_open: Option<String>, } impl OpenDocument { fn new( uri: Uri, version: i32, language_id: LanguageId, text: Arc<str>, notebook_uri: Option<Arc<Uri>>, ) -> 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, notebook_uri, fs_version_on_open, } } fn with_change( &self, version: i32, changes: Vec<lsp::TextDocumentContentChangeEvent>, ) -> Result<Self, AnyError> { 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::<usize>::from(range), &change.text); } else { text = change.text; index_valid = IndexValid::UpTo(0); } } let text: Arc<str> = 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, notebook_uri: self.notebook_uri.clone(), 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_or_asset_url_to_uri(url: &Url) -> Option<Uri> { if !matches!(url.scheme(), "http" | "https" | "asset") { return None; } let mut encoded_path = fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new(); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(url.scheme()); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>( &percent_encoding::percent_decode_str( url[Position::BeforeUsername..].trim_start_matches('/'), ) .decode_utf8_lossy(), ); let uri = fluent_uri::Uri::builder() .scheme(fluent_uri::component::Scheme::new_or_panic("deno")) .path(encoded_path.as_ref()) .build() .expect("component constraints should be met by the above") .into(); Some(uri) } fn data_url_to_uri(url: &Url) -> Option<Uri> { 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 hash = deno_lib::util::checksum::r#gen(&[url.as_str().as_bytes()]); let mut encoded_path = fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new(); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("data-url"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(&hash); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(extension); let uri = fluent_uri::Uri::builder() .scheme(fluent_uri::component::Scheme::new_or_panic("deno")) .path(encoded_path.as_ref()) .build() .expect("component constraints should be met by the above") .into(); Some(uri) } #[derive(Debug, Clone, deno_core::ToV8)] #[to_v8(untagged)] pub enum DocumentText { Static(&'static str), Arc(Arc<str>), } impl DocumentText { /// Will clone the string if static. pub fn to_arc(&self) -> Arc<str> { 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, } } } #[derive(Debug, Clone)] pub enum ServerDocumentKind { Fs { fs_version: String, media_type: MediaType, text: Arc<str>, line_index: Arc<LineIndex>, }, RemoteUrl { url: Arc<Url>, fs_cache_version: String, media_type: MediaType, text: Arc<str>, line_index: Arc<LineIndex>, }, DataUrl { url: Arc<Url>, media_type: MediaType, text: Arc<str>, line_index: Arc<LineIndex>, }, Asset { url: Arc<Url>, media_type: MediaType, text: &'static str, line_index: Arc<LineIndex>, }, RawImportTypes { kind: RawImportKind, }, } static TEXT_IMPORT_SOURCE_LINE_INDEX: Lazy<Arc<LineIndex>> = Lazy::new(|| Arc::new(LineIndex::new(TEXT_IMPORT_SOURCE))); static BYTES_IMPORT_SOURCE_LINE_INDEX: Lazy<Arc<LineIndex>> = Lazy::new(|| Arc::new(LineIndex::new(BYTES_IMPORT_SOURCE))); static CSS_IMPORT_SOURCE_LINE_INDEX: Lazy<Arc<LineIndex>> = Lazy::new(|| Arc::new(LineIndex::new(CSS_IMPORT_SOURCE))); #[derive(Debug)] pub struct ServerDocument { pub uri: Arc<Uri>, pub kind: ServerDocumentKind, } impl ServerDocument { fn load(uri: &Uri) -> Option<Self> { if uri.scheme().as_str().eq_ignore_ascii_case("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<str> = 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()), kind: ServerDocumentKind::Fs { fs_version, media_type, text, line_index, }, }); } None } fn remote_url( uri: &Uri, url: Arc<Url>, scope: Option<&Url>, cache: &LspCache, ) -> Option<Self> { 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<str> = 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()), kind: ServerDocumentKind::RemoteUrl { url, fs_cache_version, media_type, text, line_index, }, }) } fn asset(name: &str, text: &'static str) -> Self { let url = Arc::new(Url::parse(&format!("asset:///{name}")).unwrap()); let uri = remote_or_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), kind: ServerDocumentKind::Asset { url, media_type, text, line_index, }, } } fn data_url(uri: &Uri, url: Arc<Url>) -> Option<Self> { let raw_data_url = deno_media_type::data_url::RawDataUrl::parse(&url).ok()?; let media_type = raw_data_url.media_type(); let text: Arc<str> = raw_data_url.decode().ok()?.into(); let line_index = Arc::new(LineIndex::new(&text)); Some(Self { uri: Arc::new(uri.clone()), kind: ServerDocumentKind::DataUrl { url, media_type, text, line_index, }, }) } fn raw_import_types(uri: &Uri) -> Option<Self> { if uri .as_str() .strip_prefix("deno:/text-import-types/") .and_then(|s| s.strip_suffix(".ts")) .is_some() { return Some(ServerDocument { uri: Arc::new(uri.clone()), kind: ServerDocumentKind::RawImportTypes { kind: RawImportKind::Text, }, }); } if uri .as_str() .strip_prefix("deno:/bytes-import-types/") .and_then(|s| s.strip_suffix(".ts")) .is_some() { return Some(ServerDocument { uri: Arc::new(uri.clone()), kind: ServerDocumentKind::RawImportTypes { kind: RawImportKind::Bytes, }, }); } if uri .as_str() .strip_prefix("deno:/css-import-types/") .and_then(|s| s.strip_suffix(".ts")) .is_some() { return Some(ServerDocument { uri: Arc::new(uri.clone()), kind: ServerDocumentKind::RawImportTypes { kind: RawImportKind::Css, }, }); } None } pub fn media_type(&self) -> MediaType { match &self.kind { ServerDocumentKind::Fs { media_type, .. } => *media_type, ServerDocumentKind::RemoteUrl { media_type, .. } => *media_type, ServerDocumentKind::DataUrl { media_type, .. } => *media_type, ServerDocumentKind::Asset { media_type, .. } => *media_type, ServerDocumentKind::RawImportTypes { .. } => MediaType::TypeScript, } } 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), ServerDocumentKind::RawImportTypes { kind: RawImportKind::Text, .. } => DocumentText::Static(TEXT_IMPORT_SOURCE), ServerDocumentKind::RawImportTypes { kind: RawImportKind::Bytes, .. } => DocumentText::Static(BYTES_IMPORT_SOURCE), ServerDocumentKind::RawImportTypes { kind: RawImportKind::Css, .. } => DocumentText::Static(CSS_IMPORT_SOURCE), } } pub fn line_index(&self) -> &Arc<LineIndex> { match &self.kind { ServerDocumentKind::Fs { line_index, .. } => line_index, ServerDocumentKind::RemoteUrl { line_index, .. } => line_index, ServerDocumentKind::DataUrl { line_index, .. } => line_index, ServerDocumentKind::Asset { line_index, .. } => line_index, ServerDocumentKind::RawImportTypes { kind: RawImportKind::Text, .. } => &TEXT_IMPORT_SOURCE_LINE_INDEX, ServerDocumentKind::RawImportTypes { kind: RawImportKind::Bytes, .. } => &BYTES_IMPORT_SOURCE_LINE_INDEX, ServerDocumentKind::RawImportTypes { kind: RawImportKind::Css, .. } => &CSS_IMPORT_SOURCE_LINE_INDEX, } } 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(), ServerDocumentKind::RawImportTypes { .. } => "1".to_string(), } } } #[derive(Debug)] pub struct AssetDocuments { inner: HashMap<Arc<Uri>, Arc<ServerDocument>>, } impl AssetDocuments { pub fn get(&self, k: &Uri) -> Option<&Arc<ServerDocument>> { self.inner.get(k) } } pub static ASSET_DOCUMENTS: Lazy<AssetDocuments> = Lazy::new(|| AssetDocuments { inner: crate::tsc::LAZILY_LOADED_STATIC_ASSETS .iter() .map(|(k, v)| { let doc = Arc::new(ServerDocument::asset(k, v.source.as_str())); let uri = doc.uri.clone(); (uri, doc) }) .collect(), }); #[derive(Debug, Clone)] pub enum Document { Open(Arc<OpenDocument>), Server(Arc<ServerDocument>), } impl Document { pub fn open(&self) -> Option<&Arc<OpenDocument>> { match self { Self::Open(d) => Some(d), Self::Server(_) => None, } } pub fn server(&self) -> Option<&Arc<ServerDocument>> { match self { Self::Open(_) => None, Self::Server(d) => Some(d), } } pub fn uri(&self) -> &Arc<Uri> { 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<LineIndex> { 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<Uri, Arc<OpenDocument>>, server: Arc<DashMap<Uri, Arc<ServerDocument>>>, cells_by_notebook_uri: BTreeMap<Arc<Uri>, Vec<Arc<Uri>>>, file_like_uris_by_url: Arc<DashMap<Url, Arc<Uri>>>, /// 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<DashMap<Uri, Arc<Url>>>, remote_urls_by_uri: Arc<DashMap<Uri, Arc<Url>>>, } impl Documents { fn open( &mut self, uri: Uri, version: i32, language_id: LanguageId, text: Arc<str>, notebook_uri: Option<Arc<Uri>>, ) -> Arc<OpenDocument> { let uri = normalize_uri(&uri); self.server.remove(&uri); let doc = Arc::new(OpenDocument::new( uri.clone(), version, language_id, text, notebook_uri, )); self.open.insert(uri, doc.clone()); if !doc.uri.scheme().as_str().eq_ignore_ascii_case("file") { let url = uri_to_url(&doc.uri); if url.scheme() == "file" { self.file_like_uris_by_url.insert(url, doc.uri.clone()); } } doc } fn change( &mut self, uri: &Uri, version: i32, changes: Vec<lsp::TextDocumentContentChangeEvent>, ) -> Result<Arc<OpenDocument>, AnyError> { let uri = normalize_uri(uri); 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) } fn close(&mut self, uri: &Uri) -> Result<Arc<OpenDocument>, AnyError> { let uri = normalize_uri(uri); self.file_like_uris_by_url.retain(|_, u| u.as_ref() != &uri); let doc = self.open.shift_remove(&uri).ok_or_else(|| { JsErrorBox::new( "NotFound", format!( "The URI \"{}\" does not refer to an open document.", uri.as_str() ), ) })?; Ok(doc) } fn open_notebook( &mut self, uri: Uri, cells: Vec<lsp::TextDocumentItem>, ) -> Vec<Arc<OpenDocument>> { let uri = Arc::new(normalize_uri(&uri)); let mut documents = Vec::with_capacity(cells.len()); for cell in cells { let language_id = cell.language_id.parse().unwrap_or_else(|err| { lsp_warn!("{:#}", err); LanguageId::Unknown }); if language_id == LanguageId::Unknown { lsp_warn!( "Unsupported language id \"{}\" received for document \"{}\".", cell.language_id, cell.uri.as_str() ); } let document = self.open( cell.uri.clone(), cell.version, language_id, cell.text.into(), Some(uri.clone()), ); documents.push(document); } self .cells_by_notebook_uri .insert(uri, documents.iter().map(|d| d.uri.clone()).collect()); documents } pub fn change_notebook( &mut self, uri: &Uri, structure: Option<lsp::NotebookDocumentCellChangeStructure>, content: Option<Vec<lsp::NotebookDocumentChangeTextContent>>, ) -> Vec<(Arc<OpenDocument>, ChangeKind)> { let uri = Arc::new(normalize_uri(uri)); let mut documents_with_change_kinds = Vec::new(); if let Some(structure) = structure { if let Some(cells) = self.cells_by_notebook_uri.get_mut(&uri) { cells.splice( structure.array.start as usize ..(structure.array.start + structure.array.delete_count) as usize, structure .array .cells .into_iter() .flatten() .map(|c| Arc::new(normalize_uri(&c.document))), ); } for closed in structure.did_close.into_iter().flatten() { let document = match self.close(&closed.uri) { Ok(d) => d, Err(err) => { lsp_warn!("{:#}", err); continue; } }; documents_with_change_kinds.push((document, ChangeKind::Closed)); } for opened in structure.did_open.into_iter().flatten() { let language_id = opened.language_id.parse().unwrap_or_else(|err| { lsp_warn!("{:#}", err); LanguageId::Unknown }); if language_id == LanguageId::Unknown { lsp_warn!( "Unsupported language id \"{}\" received for document \"{}\".", opened.language_id, opened.uri.as_str() ); } let document = self.open( opened.uri, opened.version, language_id, opened.text.into(), Some(uri.clone()), ); documents_with_change_kinds.push((document, ChangeKind::Opened)); } } for changed in content.into_iter().flatten() { let document = match self.change( &changed.document.uri, changed.document.version, changed.changes, ) { Ok(d) => d, Err(err) => { lsp_warn!("{:#}", err); continue; } }; documents_with_change_kinds.push((document, ChangeKind::Modified)); } documents_with_change_kinds } pub fn close_notebook(&mut self, uri: &Uri) -> Vec<Arc<OpenDocument>> { let uri = normalize_uri(uri); let Some(cell_uris) = self.cells_by_notebook_uri.remove(&uri) else { lsp_warn!( "The URI \"{}\" does not refer to an open notebook document.", uri.as_str(), ); return Default::default(); }; let mut documents = Vec::with_capacity(cell_uris.len()); for cell_uri in cell_uris { let document = match self.close(&cell_uri) { Ok(d) => d, Err(err) => { lsp_warn!("{:#}", err); continue; } }; documents.push(document); } documents } pub fn get(&self, uri: &Uri) -> Option<Document> { let uri = normalize_uri(uri); 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) = ServerDocument::raw_import_types(&uri) { return Some(Document::Server(Arc::new(doc))); } if let Some(doc) = self.server.get(&uri) { return Some(Document::Server(doc.clone())); } let doc = match ServerDocument::load(&uri) { Some(doc) => doc, _ => match self.data_urls_by_uri.get(&uri) { Some(data_url) => { ServerDocument::data_url(&uri, data_url.value().clone())? } _ => { return None; } }, }; let doc = Arc::new(doc); self.server.insert(uri, doc.clone()); Some(Document::Server(doc)) } /// This will not create any server entries, only retrieve existing entries. pub fn inspect(&self, uri: &Uri) -> Option<Document> { let uri = normalize_uri(uri); 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<Document> { 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 = remote_or_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_or_asset_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 cells_by_notebook_uri(&self) -> &BTreeMap<Arc<Uri>, Vec<Arc<Uri>>> { &self.cells_by_notebook_uri } pub fn open_docs(&self) -> impl Iterator<Item = &Arc<OpenDocument>> { self.open.values() } pub fn server_docs(&self) -> Vec<Arc<ServerDocument>> { self.server.iter().map(|e| e.value().clone()).collect() } pub fn docs(&self) -> Vec<Document> { 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<Document> { 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<ParsedSourceResult>, } #[derive(Debug)] pub struct DocumentModule { pub uri: Arc<Uri>, pub open_data: Option<DocumentModuleOpenData>, pub notebook_uri: Option<Arc<Uri>>, pub script_version: String, pub specifier: Arc<Url>, pub scope: Option<Arc<Url>>, pub compiler_options_key: CompilerOptionsKey, pub media_type: MediaType, pub headers: Option<HashMap<String, String>>, pub text: DocumentText, pub line_index: Arc<LineIndex>, pub resolution_mode: ResolutionMode, pub dependencies: Arc<IndexMap<String, deno_graph::Dependency>>, pub types_dependency: Option<Arc<TypesDependency>>, pub navigation_tree: tokio::sync::OnceCell<Arc<NavigationTree>>, pub semantic_tokens_full: tokio::sync::OnceCell<lsp::SemanticTokens>, /// Cached lint diagnostics. These only depend on this module's own contents /// (not on other files), so they can be computed once per module instance /// and reused until the document changes. A new `DocumentModule` is created /// whenever the document content or the lint configuration changes, which /// naturally invalidates this cache. pub lint_diagnostics: once_cell::sync::OnceCell<Arc<Vec<lsp::Diagnostic>>>, /// Cached `deno doc --lint` diagnostics. Like lint diagnostics, these are /// derived solely from this module's own contents (a single-module graph), /// so they're cached per module instance. pub doc_diagnostics: once_cell::sync::OnceCell<Arc<Vec<lsp::Diagnostic>>>, text_info_cell: once_cell::sync::OnceCell<SourceTextInfo>, test_module_fut: Option<TestModuleFut>, } impl DocumentModule { #[allow(clippy::too_many_arguments, reason = "construction")] pub fn new( document: &Document, specifier: Arc<Url>, compiler_options_key: CompilerOptionsKey, compiler_options_data: &LspCompilerOptionsData, scope: Option<Arc<Url>>, 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 open_document = document.open(); let media_type = resolve_media_type( &specifier, headers.as_ref(), open_document.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(), compiler_options_data, 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: open_document.map(|d| DocumentModuleOpenData { version: d.version, parsed_source, }), notebook_uri: open_document.and_then(|d| d.notebook_uri.clone()), script_version: document.script_version(), specifier, scope, compiler_options_key, media_type, headers, text, line_index: document.line_index().clone(), resolution_mode, dependencies, types_dependency, navigation_tree: Default::default(), semantic_tokens_full: Default::default(), lint_diagnostics: Default::default(), doc_diagnostics: 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<Arc<TestModule>> { self.test_module_fut.clone()?.await } } type DepInfoByScope = BTreeMap<Option<Arc<Url>>, Arc<ScopeDepInfo>>; struct WeakKeyDocumentMap<T> { entries: HashMap<usize, (Weak<T>, Arc<DocumentModule>)>, } impl<T> Default for WeakKeyDocumentMap<T> { fn default() -> Self { Self { entries: Default::default(), } } } impl<T> fmt::Debug for WeakKeyDocumentMap<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WeakKeyDocumentMap") .field("len", &self.entries.len()) .finish() } } impl<T> WeakKeyDocumentMap<T> { fn get(&self, document: &Arc<T>) -> Option<Arc<DocumentModule>> { let (document_weak, module) = self.entries.get(&Self::key(document))?; let document_strong = document_weak.upgrade()?; Arc::ptr_eq(&document_strong, document).then(|| module.clone()) } fn values(&self) -> impl Iterator<Item = Arc<DocumentModule>> + '_ { self.entries.values().filter_map(|(document, module)| { document.upgrade().map(|_| module.clone()) }) } fn insert( &mut self, document: Arc<T>, module: Arc<DocumentModule>, ) -> Option<Arc<DocumentModule>> { self.remove_expired(); self .entries .insert(Self::key(&document), (Arc::downgrade(&document), module)) .map(|(_, module)| module) } fn remove_expired(&mut self) { self .entries .retain(|_, (document, _)| document.strong_count() > 0); } fn key(document: &Arc<T>) -> usize { Arc::as_ptr(document) as usize } } #[derive(Default)] struct WeakValueDocumentMap { entries: HashMap<Arc<Url>, Weak<DocumentModule>>, } impl fmt::Debug for WeakValueDocumentMap { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("WeakValueDocumentMap") .field("len", &self.entries.len()) .finish() } } impl WeakValueDocumentMap { fn contains_key(&self, specifier: &Url) -> bool { self .entries .get(specifier) .and_then(|module| module.upgrade()) .is_some() } fn insert( &mut self, specifier: Arc<Url>, module: Arc<DocumentModule>, ) -> Option<Arc<DocumentModule>> { self.remove_expired(); self .entries .insert(specifier, Arc::downgrade(&module)) .and_then(|module| module.upgrade()) } fn remove_expired(&mut self) { self.entries.retain(|_, module| module.strong_count() > 0); } } #[derive(Debug, Default)] struct WeakDocumentModuleMap { open: RwLock<WeakKeyDocumentMap<OpenDocument>>, server: RwLock<WeakKeyDocumentMap<ServerDocument>>, by_specifier: RwLock<WeakValueDocumentMap>, } impl WeakDocumentModuleMap { fn get(&self, document: &Document) -> Option<Arc<DocumentModule>> { match document { Document::Open(d) => self.open.read().get(d), Document::Server(d) => self.server.read().get(d), } } fn contains_specifier(&self, specifier: &Url) -> bool { self.by_specifier.read().contains_key(specifier) } fn inspect_values(&self) -> Vec<Arc<DocumentModule>> { self .open .read() .values() .chain(self.server.read().values()) .collect() } fn insert( &self, document: &Document, module: Arc<DocumentModule>, ) -> Option<Arc<DocumentModule>> { 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(); } } type ScopeInfo = (Option<Arc<Url>>, CompilerOptionsKey); #[derive(Debug, Default, Clone)] pub struct DocumentModules { pub documents: Documents, config: Arc<Config>, compiler_options_resolver: Arc<LspCompilerOptionsResolver>, resolver: Arc<LspResolver>, cache: Arc<LspCache>, workspace_files: Arc<IndexSet<PathBuf>>, dep_info_by_scope: once_cell::sync::OnceCell<Arc<DepInfoByScope>>, modules_unscoped: Arc<WeakDocumentModuleMap>, modules_by_scope: Arc<BTreeMap<Arc<Url>, Arc<WeakDocumentModuleMap>>>, assigned_scopes: Arc<RwLock<HashMap<Arc<Uri>, ScopeInfo>>>, } impl DocumentModules { pub fn update_config( &mut self, config: &Config, compiler_options_resolver: &Arc<LspCompilerOptionsResolver>, resolver: &Arc<LspResolver>, cache: &LspCache, workspace_files: &Arc<IndexSet<PathBuf>>, ) { self.config = Arc::new(config.clone()); self.compiler_options_resolver = compiler_options_resolver.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(); self.assigned_scopes = 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<str>, notebook_uri: Option<Arc<Uri>>, ) -> Arc<OpenDocument> { self.dep_info_by_scope = Default::default(); self .documents .open(uri, version, language_id, text, notebook_uri) } pub fn change_document( &mut self, uri: &Uri, version: i32, changes: Vec<lsp::TextDocumentContentChangeEvent>, ) -> Result<Arc<OpenDocument>, 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<Arc<OpenDocument>, 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().as_str().eq_ignore_ascii_case("file") && self.config.uri_enabled(uri) { self.documents.get(uri); } Ok(document) } pub fn open_notebook_document( &mut self, uri: Uri, cells: Vec<lsp::TextDocumentItem>, ) -> Vec<Arc<OpenDocument>> { self.dep_info_by_scope = Default::default(); self.documents.open_notebook(uri, cells) } pub fn change_notebook_document( &mut self, uri: &Uri, structure: Option<lsp::NotebookDocumentCellChangeStructure>, content: Option<Vec<lsp::NotebookDocumentChangeTextContent>>, ) -> Vec<(Arc<OpenDocument>, ChangeKind)> { self.dep_info_by_scope = Default::default(); self.documents.change_notebook(uri, structure, content) } pub fn close_notebook_document( &mut self, uri: &Uri, ) -> Vec<Arc<OpenDocument>> { self.dep_info_by_scope = Default::default(); self.documents.close_notebook(uri) } pub fn release( &self, specifier: &Url, scope: Option<&Url>, compiler_options_key: Option<&CompilerOptionsKey>, ) { let Some(module) = self.module_for_specifier(specifier, scope, compiler_options_key) else { return; }; self.documents.remove_server_doc(&module.uri); } fn infer_specifier(&self, document: &Document) -> Option<Arc<Url>> { 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()), ServerDocumentKind::RawImportTypes { .. } => return None, } } let uri = document.uri(); let url = uri_to_url(uri); if url.scheme() != "file" { return None; } if uri.scheme().as_str().eq_ignore_ascii_case("file") && let Some(remote_specifier) = self.cache.unvendored_specifier(&url) { return Some(Arc::new(remote_specifier)); } Some(Arc::new(url)) } fn module_inner( &self, document: &Document, specifier: Option<&Arc<Url>>, scope: Option<&Url>, compiler_options_key: Option<&CompilerOptionsKey>, ) -> Option<Arc<DocumentModule>> { let modules = self.modules_for_scope(scope)?; if let Some(module) = modules.get(document) { return Some(module); } let specifier = specifier .cloned() .or_else(|| self.infer_specifier(document))?; let scheme = specifier.scheme(); let (compiler_options_key, compiler_options_data) = if scheme != "file" || self.resolver.in_node_modules(&specifier) || self.cache.in_global_cache_directory(&specifier) { // Dependency files get their compiler options key from their referrer, // passed in here when reached through resolution. If we don't have one // (e.g. the document was opened directly in the editor), use the key // previously assigned to it if any, or infer one from its path. See // https://github.com/denoland/deno/issues/35170. let key = compiler_options_key.cloned().or_else(|| { let (assigned_scope, assigned_key) = self.assigned_scopes.read().get(document.uri())?.clone(); (assigned_scope.as_deref() == scope).then_some(assigned_key) }); match key { Some(key) => { let value = self .compiler_options_resolver .for_key(&key) .expect("Key should be in sync with resolver."); (key, value) } None if scheme == "file" => { let (key, value) = self .compiler_options_resolver .entry_for_specifier(&specifier); (key.clone(), value) } None => return None, } } else { let (key, value) = self.compiler_options_resolver.entry_for_specifier( if scheme != "file" && let Some(scope) = scope { scope } else { &specifier }, ); (key.clone(), value) }; let module = Arc::new(DocumentModule::new( document, specifier, compiler_options_key, compiler_options_data, scope.cloned().map(Arc::new), &self.resolver, &self.config, &self.cache, )); self.resolver.did_create_module(&module); modules.insert(document, module.clone()); Some(module) } pub fn module( &self, document: &Document, scope: Option<&Url>, ) -> Option<Arc<DocumentModule>> { self.module_inner(document, None, scope, None) } pub fn module_for_specifier( &self, specifier: &Url, scope: Option<&Url>, compiler_options_key: Option<&CompilerOptionsKey>, ) -> Option<Arc<DocumentModule>> { let scoped_resolver = self.resolver.get_scoped_resolver(scope); let specifier = match JsrPackageReqReference::from_specifier(specifier) { Ok(jsr_req_ref) => { Cow::Owned(scoped_resolver.jsr_to_resource_url(&jsr_req_ref)?) } _ => Cow::Borrowed(specifier), }; let specifier = scoped_resolver.resolve_redirects(&specifier)?; let document = self .documents .get_for_specifier(&specifier, scope, &self.cache)?; let module = self.module_inner( &document, Some(&Arc::new(specifier)), scope, compiler_options_key, ); if let Some(module) = &module { self.assigned_scopes.write().insert( document.uri().clone(), (module.scope.clone(), module.compiler_options_key.clone()), ); } module } pub fn primary_module( &self, document: &Document, ) -> Option<Arc<DocumentModule>> { if let Some(scope) = self.primary_scope(document.uri()) { return self.module(document, scope.map(|s| s.as_ref())); } let assigned_scope = self.assigned_scopes.read().get(document.uri()).cloned(); if let Some((scope, compiler_options_key)) = assigned_scope { return self.module_inner( document, None, scope.as_deref(), Some(&compiler_options_key), ); } 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<Option<Arc<Url>>, Vec<Arc<DocumentModule>>> { 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 open_document = document.open(); if open_document.is_some_and(|d| d.notebook_uri.is_some()) { continue; } let url = uri_to_url(uri); if open_document.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_primary_module( &self, document: &Document, ) -> Option<Arc<DocumentModule>> { 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 store any module entries, only retrieve existing entries or /// create temporary entries for other keys. // TODO(nayeemrmn): Support notebook scopes here. pub fn get_or_temp_modules_by_compiler_options_key( &self, document: &Document, ) -> BTreeMap<CompilerOptionsKey, Arc<DocumentModule>> { let Some(primary_module) = self.primary_module(document) else { return Default::default(); }; let mut result = BTreeMap::new(); for (compiler_options_key, compiler_options_data) in self.compiler_options_resolver.entries() { if compiler_options_key == &primary_module.compiler_options_key { continue; } result.insert( compiler_options_key.clone(), Arc::new(DocumentModule::new( document, primary_module.specifier.clone(), compiler_options_key.clone(), compiler_options_data, primary_module.scope.clone(), &self.resolver, &self.config, &self.cache, )), ); } result.insert(primary_module.compiler_options_key.clone(), primary_module); result } fn modules_for_scope( &self, scope: Option<&Url>, ) -> Option<&Arc<WeakDocumentModuleMap>> { match scope { Some(s) => Some(self.modules_by_scope.get(s)?), None => Some(&self.modules_unscoped), } } pub fn primary_scope(&self, uri: &Uri) -> Option<Option<&Arc<Url>>> { let url = uri_to_url(uri); if url.scheme() == "file" && !self.cache.in_global_cache_directory(&url) { let mut scope = self.config.tree.scope_for_specifier(&url); // In-memory documents (untitled files and unsaved notebook cells) convert // to a `file:` URL at the filesystem root, which matches no workspace // scope. Associate them with the workspace root so its config (import // map, compiler options) applies — matching the behavior once the // document is saved into the workspace. See denoland/deno#22628. if scope.is_none() && uri_is_in_memory(uri) && let Some(root_url) = self.config.root_url() { scope = self.config.tree.scope_for_specifier(root_url); } return Some(scope); } None } pub fn primary_specifier( &self, document: &Document, ) -> Option<(Arc<Url>, MediaType)> { self .primary_module(document) .map(|m| (m.specifier.clone(), m.media_type)) .or_else(|| { let specifier = self.infer_specifier(document)?; let media_type = MediaType::from_specifier(&specifier); Some((specifier, media_type)) }) } pub fn remove_expired_modules(&self) { self.modules_unscoped.remove_expired(); for modules in self.modules_by_scope.values() { modules.remove_expired(); } } pub fn specifier_exists(&self, specifier: &Url, scope: Option<&Url>) -> bool { if let Some(modules) = self.modules_for_scope(scope) && 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<BTreeMap<Option<Arc<Url>>, Arc<ScopeDepInfo>>> { type ScopeEntry<'a> = (Option<&'a Arc<Url>>, &'a Arc<WeakDocumentModuleMap>); 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 && dep.scheme() == "node" { dep_info.has_node_specifier = true; } if dependency.maybe_deno_types_specifier.is_some() && let (Some(code_specifier), Some(type_specifier)) = (code_specifier, type_specifier) && MediaType::from_specifier(type_specifier).is_declaration() { dep_info .deno_types_to_code_resolutions .insert(type_specifier.clone(), code_specifier.clone()); } } }; 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 compiler_options_data = self .compiler_options_resolver .for_specifier(&config_data.scope); let import_source_types = compiler_options_data .jsx_import_source_config .as_ref()? .import_source_types .as_ref()?; let import_source = compiler_options_data .jsx_import_source_config .as_ref()? .import_source .as_ref()?; let scoped_resolver = self.resolver.get_scoped_resolver(scope.map(|s| s.as_ref())); let cli_resolver = scoped_resolver.as_cli_resolver(); 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(()) })(); } (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<Option<Arc<Url>>> { self .dep_info_by_scope() .iter() .filter(|(_, i)| i.has_node_specifier) .map(|(s, _)| s.clone()) .collect::<HashSet<_>>() } #[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>, compiler_options_key: Option<&CompilerOptionsKey>, ) -> Vec<Option<(Url, MediaType)>> { let referrer_module = self.module_for_specifier(referrer, scope, compiler_options_key); let dependencies = referrer_module.as_ref().map(|d| &d.dependencies); let mut results = Vec::new(); let scoped_resolver = self.resolver.get_scoped_resolver(scope); for (is_cjs, raw_specifier) in raw_specifiers { let resolution_mode = match is_cjs { true => ResolutionMode::Require, false => ResolutionMode::Import, }; let result = if raw_specifier.starts_with("asset:") { if let Ok(specifier) = resolve_url(raw_specifier) { let media_type = MediaType::from_specifier(&specifier); Some((specifier, media_type)) } else { 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() { self .resolve_dependency( specifier, referrer, resolution_mode, scope, compiler_options_key, ) .map(|(s, t, _)| (s, t)) } else if let Some(specifier) = dep.maybe_code.maybe_specifier() { self .resolve_dependency( specifier, referrer, resolution_mode, scope, compiler_options_key, ) .map(|(s, t, _)| (s, t)) } else { None } } else { match scoped_resolver.as_cli_resolver().resolve( raw_specifier, referrer, deno_graph::Position::zeroed(), resolution_mode, NodeResolutionKind::Types, ) { Ok(specifier) => self .resolve_dependency( &specifier, referrer, resolution_mode, scope, compiler_options_key, ) .map(|(s, t, _)| (s, t)), _ => None, } }; results.push(result); } 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>, compiler_options_key: Option<&CompilerOptionsKey>, ) -> Option<(Url, MediaType, Option<Arc<Uri>>)> { if let Some(module_name) = specifier.as_str().strip_prefix("node:") && deno_node::is_builtin_node_module(module_name) { // Don't resolve node: specifiers because during type checking we resolve // to the ambient modules in the @types/node package. return None; } let mut specifier = specifier.clone(); let mut media_type = None; if let Ok(npm_ref) = NpmPackageReqReference::from_specifier(&specifier) { let scoped_resolver = self.resolver.get_scoped_resolver(scope); let (s, mt) = scoped_resolver.npm_to_file_url( &npm_ref, referrer, NodeResolutionKind::Types, resolution_mode, )?; specifier = s; media_type = Some(mt); } let Some(module) = self.module_for_specifier(&specifier, scope, compiler_options_key) else { let media_type = media_type.unwrap_or_else(|| MediaType::from_specifier(&specifier)); return Some((specifier, media_type, None)); }; 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, compiler_options_key, ) } else { Some(( module.specifier.as_ref().clone(), module.media_type, Some(module.uri.clone()), )) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LanguageId { JavaScript, Jsx, TypeScript, Tsx, Json, JsonC, Markdown, Html, Css, Scss, 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::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::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<Self, Self::Err> { 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), "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<deno_graph::JsModule, deno_graph::ModuleGraphError>; type ParsedSourceResult = Result<ParsedSource, Arc<JsErrorBox>>; type TestModuleFut = Shared<Pin<Box<dyn Future<Output = Option<Arc<TestModule>>> + 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<TestModuleFut> { 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) } pub fn resolve_media_type( specifier: &ModuleSpecifier, maybe_headers: Option<&HashMap<String, String>>, maybe_language_id: Option<LanguageId>, ) -> 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<Url>, Arc<DocumentModule>>, } impl OpenDocumentsGraphLoader<'_> { fn load_from_docs( &self, specifier: &ModuleSpecifier, ) -> Option<deno_graph::source::LoadFuture> { if specifier.scheme() == "file" && 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()), mtime: None, 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 parse_and_analyze_module( specifier: ModuleSpecifier, text: Arc<str>, maybe_headers: Option<&HashMap<String, String>>, media_type: MediaType, scope: Option<&Url>, compiler_options_data: &LspCompilerOptionsData, resolver: &LspResolver, ) -> ( Option<ParsedSourceResult>, Option<ModuleResult>, 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, scope, compiler_options_data, resolver, ); ( Some(parsed_source_result), Some(module_result), resolution_mode, ) } fn parse_source( specifier: ModuleSpecifier, text: Arc<str>, media_type: MediaType, ) -> ParsedSourceResult { let specifier_for_error = specifier.clone(); let result = std::panic::catch_unwind(AssertUnwindSafe(|| { deno_ast::parse_program(deno_ast::ParseParams { specifier, text, media_type, capture_tokens: true, scope_analysis: true, maybe_syntax: None, }) })); match result { Ok(result) => { result.map_err(|diagnostic| Arc::new(JsErrorBox::from_err(diagnostic))) } Err(error) => { let error = panic_payload_to_string(&error); lsp_warn!( "Failed to parse \"{}\" due to parser panic: {}", specifier_for_error, error, ); Err(Arc::new(JsErrorBox::generic(format!( "The parser panicked while parsing this module: {error}" )))) } } } fn panic_payload_to_string(error: &(dyn std::any::Any + Send)) -> String { if let Some(message) = error.downcast_ref::<String>() { message.clone() } else if let Some(message) = error.downcast_ref::<&'static str>() { message.to_string() } else { "unknown parser panic".to_string() } } fn analyze_module( specifier: ModuleSpecifier, parsed_source_result: &ParsedSourceResult, maybe_headers: Option<&HashMap<String, String>>, scope: Option<&Url>, compiler_options_data: &LspCompilerOptionsData, resolver: &LspResolver, ) -> (ModuleResult, ResolutionMode) { match parsed_source_result { Ok(parsed_source) => { let scoped_resolver = resolver.get_scoped_resolver(scope); let cli_resolver = scoped_resolver.as_cli_resolver(); let is_cjs_resolver = scoped_resolver.as_is_cjs_resolver(); let valid_referrer = specifier.clone(); 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: compiler_options_data .jsx_import_source_config .as_deref(), }; ( Ok(deno_graph::parse_module_from_ast( deno_graph::ParseModuleFromAstOptions { graph_kind: deno_graph::GraphKind::TypesOnly, specifier, maybe_headers, mtime: None, 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), }, )), module_resolution_mode, ) } Err(diagnostic) => ( Err(deno_graph::ModuleGraphError::ModuleError( deno_graph::ModuleErrorKind::Parse { specifier, mtime: None, diagnostic: diagnostic.clone(), } .into_box(), )), ResolutionMode::Import, ), } } fn bytes_to_content( specifier: &ModuleSpecifier, media_type: MediaType, bytes: Vec<u8>, maybe_charset: Option<&str>, ) -> Result<String, AnyError> { 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 compiler_options_resolver = Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None)); resolver.set_compiler_options_resolver(&compiler_options_resolver.inner); let mut document_modules = DocumentModules::default(); document_modules.update_config( &config, &compiler_options_resolver, &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(), None, ); 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(), None, ); 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"); "# ); } #[test] fn test_parse_invalid_tsx_with_jsx_pragmas_does_not_panic() { let specifier = ModuleSpecifier::parse("file:///index.tsx").unwrap(); let result = parse_source( specifier, r#"/** @jsxRuntime automatic */ /** @jsxImportSource npm:preact */ const Counter = () => { const [count <button>{count}</button> } "# .into(), MediaType::Tsx, ); assert!(result.is_err()); } #[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::<IndexSet<_>>(), ); let document = document_modules.open_document( url_to_uri(&file1_specifier).unwrap(), 1, LanguageId::TypeScript, "import {} from 'test';".into(), None, ); // 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); let compiler_options_resolver = Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None)); resolver.set_compiler_options_resolver(&compiler_options_resolver.inner); document_modules.update_config( &config, &compiler_options_resolver, &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); let compiler_options_resolver = Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None)); resolver.set_compiler_options_resolver(&compiler_options_resolver.inner); document_modules.update_config( &config, &compiler_options_resolver, &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), ); } } }