mirror of
https://github.com/denoland/deno.git
synced 2025-08-27 22:14:43 +00:00
2114 lines
59 KiB
Rust
2114 lines
59 KiB
Rust
// 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<Uri>,
|
|
pub text: Arc<str>,
|
|
pub line_index: Arc<LineIndex>,
|
|
pub version: i32,
|
|
pub language_id: LanguageId,
|
|
pub fs_version_on_open: Option<String>,
|
|
}
|
|
|
|
impl OpenDocument {
|
|
fn new(
|
|
uri: Uri,
|
|
version: i32,
|
|
language_id: LanguageId,
|
|
text: Arc<str>,
|
|
) -> 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<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,
|
|
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<Uri> {
|
|
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<Uri> {
|
|
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<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 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<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,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Serialize for DocumentText {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
(self as &str).serialize(serializer)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ServerDocumentKind {
|
|
Fs {
|
|
fs_version: String,
|
|
text: Arc<str>,
|
|
},
|
|
RemoteUrl {
|
|
url: Arc<Url>,
|
|
fs_cache_version: String,
|
|
text: Arc<str>,
|
|
},
|
|
DataUrl {
|
|
url: Arc<Url>,
|
|
text: Arc<str>,
|
|
},
|
|
Asset {
|
|
url: Arc<Url>,
|
|
text: &'static str,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ServerDocument {
|
|
pub uri: Arc<Uri>,
|
|
pub media_type: MediaType,
|
|
pub line_index: Arc<LineIndex>,
|
|
pub kind: ServerDocumentKind,
|
|
}
|
|
|
|
impl ServerDocument {
|
|
fn load(uri: &Uri) -> Option<Self> {
|
|
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<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()),
|
|
media_type,
|
|
line_index,
|
|
kind: ServerDocumentKind::Fs { fs_version, text },
|
|
});
|
|
}
|
|
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()),
|
|
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<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()),
|
|
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<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.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>>>,
|
|
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 {
|
|
pub fn open(
|
|
&mut self,
|
|
uri: Uri,
|
|
version: i32,
|
|
language_id: LanguageId,
|
|
text: Arc<str>,
|
|
) -> Arc<OpenDocument> {
|
|
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<lsp::TextDocumentContentChangeEvent>,
|
|
) -> Result<Arc<OpenDocument>, 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<Arc<OpenDocument>, 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<Document> {
|
|
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<Document> {
|
|
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 = 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<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 script_version: String,
|
|
pub specifier: Arc<Url>,
|
|
pub scope: Option<Arc<Url>>,
|
|
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>,
|
|
text_info_cell: once_cell::sync::OnceCell<SourceTextInfo>,
|
|
test_module_fut: Option<TestModuleFut>,
|
|
}
|
|
|
|
impl DocumentModule {
|
|
pub fn new(
|
|
document: &Document,
|
|
specifier: Arc<Url>,
|
|
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 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<Arc<TestModule>> {
|
|
self.test_module_fut.clone()?.await
|
|
}
|
|
}
|
|
|
|
type DepInfoByScope = BTreeMap<Option<Arc<Url>>, Arc<ScopeDepInfo>>;
|
|
|
|
#[derive(Debug, Default)]
|
|
struct WeakDocumentModuleMap {
|
|
open: RwLock<PtrWeakKeyHashMap<Weak<OpenDocument>, Arc<DocumentModule>>>,
|
|
server: RwLock<PtrWeakKeyHashMap<Weak<ServerDocument>, Arc<DocumentModule>>>,
|
|
by_specifier: RwLock<WeakValueHashMap<Arc<Url>, Weak<DocumentModule>>>,
|
|
}
|
|
|
|
impl WeakDocumentModuleMap {
|
|
fn get(&self, document: &Document) -> Option<Arc<DocumentModule>> {
|
|
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<Arc<DocumentModule>> {
|
|
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<Arc<DocumentModule>> {
|
|
self
|
|
.open
|
|
.read()
|
|
.values()
|
|
.cloned()
|
|
.chain(self.server.read().values().cloned())
|
|
.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();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct DocumentModules {
|
|
pub documents: Documents,
|
|
config: Arc<Config>,
|
|
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>>>,
|
|
}
|
|
|
|
impl DocumentModules {
|
|
pub fn update_config(
|
|
&mut self,
|
|
config: &Config,
|
|
resolver: &Arc<LspResolver>,
|
|
cache: &LspCache,
|
|
workspace_files: &Arc<IndexSet<PathBuf>>,
|
|
) {
|
|
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<str>,
|
|
) -> Arc<OpenDocument> {
|
|
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<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().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<Url>>,
|
|
scope: Option<&Url>,
|
|
) -> 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(|| {
|
|
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<Arc<DocumentModule>> {
|
|
self.module_inner(document, None, scope)
|
|
}
|
|
|
|
pub fn module_for_specifier(
|
|
&self,
|
|
specifier: &Url,
|
|
scope: Option<&Url>,
|
|
) -> Option<Arc<DocumentModule>> {
|
|
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<Arc<DocumentModule>> {
|
|
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<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 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<Arc<DocumentModule>> {
|
|
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<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 create any module entries, only retrieve existing entries.
|
|
pub fn inspect_modules_by_scope(
|
|
&self,
|
|
document: &Document,
|
|
) -> BTreeMap<Option<Arc<Url>>, Arc<DocumentModule>> {
|
|
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<Option<Arc<Url>>, Arc<DocumentModule>> {
|
|
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<WeakDocumentModuleMap>> {
|
|
match scope {
|
|
Some(s) => Some(self.modules_by_scope.get(s)?),
|
|
None => Some(&self.modules_unscoped),
|
|
}
|
|
}
|
|
|
|
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 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<Option<Arc<Url>>> {
|
|
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<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 {
|
|
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<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>,
|
|
) -> Vec<Option<(Url, MediaType)>> {
|
|
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<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),
|
|
"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<deno_graph::JsModule, deno_graph::ModuleGraphError>;
|
|
type ParsedSourceResult = Result<ParsedSource, deno_ast::ParseDiagnostic>;
|
|
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)
|
|
}
|
|
|
|
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" {
|
|
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<str>,
|
|
maybe_headers: Option<&HashMap<String, String>>,
|
|
media_type: MediaType,
|
|
file_referrer: Option<&ModuleSpecifier>,
|
|
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,
|
|
file_referrer,
|
|
resolver,
|
|
);
|
|
(
|
|
Some(parsed_source_result),
|
|
Some(module_result),
|
|
resolution_mode,
|
|
)
|
|
}
|
|
|
|
fn parse_source(
|
|
specifier: ModuleSpecifier,
|
|
text: Arc<str>,
|
|
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<String, String>>,
|
|
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<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 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::<IndexSet<_>>(),
|
|
);
|
|
|
|
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),
|
|
);
|
|
}
|
|
}
|
|
}
|