diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index a3d8d2ad62..a87c49108d 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -2,12 +2,11 @@ use super::logging::lsp_log; use crate::args::ConfigFile; +use crate::cache::FastInsecureHasher; use crate::lsp::logging::lsp_warn; use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::path::specifier_to_file_path; use deno_ast::MediaType; -use deno_config::glob::PathOrPattern; -use deno_config::glob::PathOrPatternSet; use deno_config::FmtOptionsConfig; use deno_core::parking_lot::Mutex; use deno_core::serde::de::DeserializeOwned; @@ -733,12 +732,17 @@ impl ConfigSnapshot { /// Determine if the provided specifier is enabled or not. pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { - specifier_enabled( - specifier, - self.config_file.as_ref(), - &self.settings, - &self.workspace_folders, - ) + if let Some(cf) = &self.config_file { + if let Ok(files) = cf.to_files_config() { + if !files.matches_specifier(specifier) { + return false; + } + } + } + self + .settings + .specifier_enabled(specifier) + .unwrap_or_else(|| self.config_file.is_some()) } pub fn specifier_enabled_for_test( @@ -759,10 +763,55 @@ impl ConfigSnapshot { #[derive(Debug, Default, Clone)] pub struct Settings { pub unscoped: WorkspaceSettings, - pub by_workspace_folder: Option>, + pub by_workspace_folder: BTreeMap>, } impl Settings { + pub fn first_root_uri(&self) -> Option<&ModuleSpecifier> { + self.by_workspace_folder.first_key_value().map(|e| e.0) + } + + /// Returns `None` if the value should be deferred to the presence of a + /// `deno.json` file. + pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option { + let Ok(path) = specifier_to_file_path(specifier) else { + // Non-file URLs are not disabled by these settings. + return Some(true); + }; + let (settings, mut folder_uri) = self.get_for_specifier(specifier); + folder_uri = folder_uri.or_else(|| self.first_root_uri()); + let mut disable_paths = vec![]; + let mut enable_paths = None; + if let Some(folder_uri) = folder_uri { + if let Ok(folder_path) = specifier_to_file_path(folder_uri) { + disable_paths = settings + .disable_paths + .iter() + .map(|p| folder_path.join(p)) + .collect::>(); + enable_paths = settings.enable_paths.as_ref().map(|enable_paths| { + enable_paths + .iter() + .map(|p| folder_path.join(p)) + .collect::>() + }); + } + } + + if disable_paths.iter().any(|p| path.starts_with(p)) { + Some(false) + } else if let Some(enable_paths) = &enable_paths { + for enable_path in enable_paths { + if path.starts_with(enable_path) { + return Some(true); + } + } + Some(false) + } else { + settings.enable + } + } + pub fn get_unscoped(&self) -> &WorkspaceSettings { &self.unscoped } @@ -774,8 +823,14 @@ impl Settings { let Ok(path) = specifier_to_file_path(specifier) else { return (&self.unscoped, None); }; - if let Some(by_workspace_folder) = &self.by_workspace_folder { - for (folder_uri, settings) in by_workspace_folder.iter().rev() { + let mut is_first_folder = true; + for (folder_uri, settings) in self.by_workspace_folder.iter().rev() { + let mut settings = settings.as_ref(); + if is_first_folder { + settings = settings.or(Some(&self.unscoped)); + } + is_first_folder = false; + if let Some(settings) = settings { let Ok(folder_path) = specifier_to_file_path(folder_uri) else { continue; }; @@ -786,6 +841,24 @@ impl Settings { } (&self.unscoped, None) } + + pub fn enable_settings_hash(&self) -> u64 { + let mut hasher = FastInsecureHasher::default(); + let unscoped = self.get_unscoped(); + hasher.write_hashable(unscoped.enable); + hasher.write_hashable(&unscoped.enable_paths); + hasher.write_hashable(&unscoped.disable_paths); + hasher.write_hashable(unscoped.document_preload_limit); + for (folder_uri, settings) in &self.by_workspace_folder { + hasher.write_hashable(folder_uri); + hasher.write_hashable( + settings + .as_ref() + .map(|s| (&s.enable, &s.enable_paths, &s.disable_paths)), + ); + } + hasher.finish() + } } #[derive(Debug)] @@ -808,7 +881,7 @@ struct LspConfigFileInfo { #[derive(Debug)] pub struct Config { pub client_capabilities: ClientCapabilities, - settings: Settings, + pub settings: Settings, pub workspace_folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>, /// An optional configuration file which has been specified in the client /// options along with some data that is computed after the config file is set. @@ -827,29 +900,46 @@ impl Config { } #[cfg(test)] - pub fn new_with_root(root_uri: Url) -> Self { + pub fn new_with_roots(root_uris: impl IntoIterator) -> Self { let mut config = Self::new(); - let name = root_uri.path_segments().and_then(|s| s.last()); - let name = name.unwrap_or_default().to_string(); - config.workspace_folders = vec![( - root_uri.clone(), - lsp::WorkspaceFolder { - uri: root_uri, - name, - }, - )]; + let mut folders = vec![]; + for root_uri in root_uris { + let name = root_uri.path_segments().and_then(|s| s.last()); + let name = name.unwrap_or_default().to_string(); + folders.push(( + root_uri.clone(), + lsp::WorkspaceFolder { + uri: root_uri, + name, + }, + )); + } + config.set_workspace_folders(folders); config } + pub fn set_workspace_folders( + &mut self, + folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>, + ) { + self.settings.by_workspace_folder = + folders.iter().map(|(s, _)| (s.clone(), None)).collect(); + self.workspace_folders = folders; + } + pub fn set_workspace_settings( &mut self, unscoped: WorkspaceSettings, - by_workspace_folder: Option>, + folder_settings: Vec<(ModuleSpecifier, WorkspaceSettings)>, ) { - self.settings = Settings { - unscoped, - by_workspace_folder, - }; + self.settings.unscoped = unscoped; + for (folder_uri, settings) in folder_settings.into_iter() { + if let Some(settings_) = + self.settings.by_workspace_folder.get_mut(&folder_uri) + { + *settings_ = Some(settings); + } + } } pub fn workspace_settings(&self) -> &WorkspaceSettings { @@ -1006,12 +1096,18 @@ impl Config { } pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { - specifier_enabled( - specifier, - self.maybe_config_file(), - &self.settings, - &self.workspace_folders, - ) + let config_file = self.maybe_config_file(); + if let Some(cf) = config_file { + if let Ok(files) = cf.to_files_config() { + if !files.matches_specifier(specifier) { + return false; + } + } + } + self + .settings + .specifier_enabled(specifier) + .unwrap_or_else(|| config_file.is_some()) } pub fn specifier_enabled_for_test( @@ -1025,71 +1121,7 @@ impl Config { } } } - if !self.specifier_enabled(specifier) { - return false; - } - true - } - - pub fn get_enabled_paths(&self) -> PathOrPatternSet { - let mut paths = vec![]; - for (workspace_uri, _) in &self.workspace_folders { - let Ok(workspace_path) = specifier_to_file_path(workspace_uri) else { - lsp_log!("Unable to convert uri \"{}\" to path.", workspace_uri); - continue; - }; - let settings = self.workspace_settings_for_specifier(workspace_uri); - if let Some(enable_paths) = &settings.enable_paths { - for path in enable_paths { - match PathOrPattern::from_relative(&workspace_path, path) { - Ok(path_or_pattern) => paths.push(path_or_pattern), - Err(err) => { - lsp_log!("Invalid enable path '{}': {:#}", path, err); - } - } - } - } else { - paths.push(PathOrPattern::Path(workspace_path)); - } - } - paths.sort(); - paths.dedup(); - PathOrPatternSet::new(paths) - } - - pub fn get_disabled_paths(&self) -> PathOrPatternSet { - let mut path_or_patterns = vec![]; - if let Some(cf) = self.maybe_config_file() { - if let Ok(files) = cf.to_files_config() { - for path in files.exclude.into_path_or_patterns() { - path_or_patterns.push(path); - } - } - } - for (workspace_uri, _) in &self.workspace_folders { - let Ok(workspace_path) = specifier_to_file_path(workspace_uri) else { - lsp_log!("Unable to convert uri \"{}\" to path.", workspace_uri); - continue; - }; - let settings = self.workspace_settings_for_specifier(workspace_uri); - let is_enabled = settings - .enable_paths - .as_ref() - .map(|p| !p.is_empty()) - .unwrap_or_else(|| { - settings.enable.unwrap_or_else(|| self.has_config_file()) - }); - if is_enabled { - for path in &settings.disable_paths { - path_or_patterns.push(PathOrPattern::Path(workspace_path.join(path))); - } - } else { - path_or_patterns.push(PathOrPattern::Path(workspace_path)); - } - } - path_or_patterns.sort(); - path_or_patterns.dedup(); - PathOrPatternSet::new(path_or_patterns) + self.specifier_enabled(specifier) } pub fn log_file(&self) -> bool { @@ -1154,57 +1186,6 @@ impl Config { } } -fn specifier_enabled( - specifier: &Url, - config_file: Option<&ConfigFile>, - settings: &Settings, - workspace_folders: &[(Url, lsp::WorkspaceFolder)], -) -> bool { - if let Some(cf) = config_file { - if let Ok(files) = cf.to_files_config() { - if !files.matches_specifier(specifier) { - return false; - } - } - } - let Ok(path) = specifier_to_file_path(specifier) else { - // Non-file URLs are not disabled by these settings. - return true; - }; - let (settings, mut folder_uri) = settings.get_for_specifier(specifier); - folder_uri = folder_uri.or_else(|| workspace_folders.first().map(|f| &f.0)); - let mut disable_paths = vec![]; - let mut enable_paths = None; - if let Some(folder_uri) = folder_uri { - if let Ok(folder_path) = specifier_to_file_path(folder_uri) { - disable_paths = settings - .disable_paths - .iter() - .map(|p| folder_path.join(p)) - .collect::>(); - enable_paths = settings.enable_paths.as_ref().map(|enable_paths| { - enable_paths - .iter() - .map(|p| folder_path.join(p)) - .collect::>() - }); - } - } - if let Some(enable_paths) = &enable_paths { - for enable_path in enable_paths { - if path.starts_with(enable_path) - && !disable_paths.iter().any(|p| path.starts_with(p)) - { - return true; - } - } - false - } else { - settings.enable.unwrap_or_else(|| config_file.is_some()) - && !disable_paths.iter().any(|p| path.starts_with(p)) - } -} - fn resolve_lockfile_from_config(config_file: &ConfigFile) -> Option { let lockfile_path = match config_file.resolve_lockfile_path() { Ok(Some(value)) => value, @@ -1265,7 +1246,7 @@ mod tests { #[test] fn test_config_specifier_enabled() { let root_uri = resolve_url("file:///").unwrap(); - let mut config = Config::new_with_root(root_uri); + let mut config = Config::new_with_roots(vec![root_uri]); let specifier = resolve_url("file:///a.ts").unwrap(); assert!(!config.specifier_enabled(&specifier)); config.set_workspace_settings( @@ -1273,7 +1254,7 @@ mod tests { "enable": true })) .unwrap(), - None, + vec![], ); assert!(config.specifier_enabled(&specifier)); } @@ -1281,7 +1262,7 @@ mod tests { #[test] fn test_config_snapshot_specifier_enabled() { let root_uri = resolve_url("file:///").unwrap(); - let mut config = Config::new_with_root(root_uri); + let mut config = Config::new_with_roots(vec![root_uri]); let specifier = resolve_url("file:///a.ts").unwrap(); assert!(!config.specifier_enabled(&specifier)); config.set_workspace_settings( @@ -1289,7 +1270,7 @@ mod tests { "enable": true })) .unwrap(), - None, + vec![], ); let config_snapshot = config.snapshot(); assert!(config_snapshot.specifier_enabled(&specifier)); @@ -1298,14 +1279,14 @@ mod tests { #[test] fn test_config_specifier_enabled_path() { let root_uri = resolve_url("file:///project/").unwrap(); - let mut config = Config::new_with_root(root_uri); + let mut config = Config::new_with_roots(vec![root_uri]); let specifier_a = resolve_url("file:///project/worker/a.ts").unwrap(); let specifier_b = resolve_url("file:///project/other/b.ts").unwrap(); assert!(!config.specifier_enabled(&specifier_a)); assert!(!config.specifier_enabled(&specifier_b)); let workspace_settings = serde_json::from_str(r#"{ "enablePaths": ["worker"] }"#).unwrap(); - config.set_workspace_settings(workspace_settings, None); + config.set_workspace_settings(workspace_settings, vec![]); assert!(config.specifier_enabled(&specifier_a)); assert!(!config.specifier_enabled(&specifier_b)); let config_snapshot = config.snapshot(); @@ -1316,7 +1297,7 @@ mod tests { #[test] fn test_config_specifier_disabled_path() { let root_uri = resolve_url("file:///root/").unwrap(); - let mut config = Config::new_with_root(root_uri.clone()); + let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.settings.unscoped.enable = Some(true); config.settings.unscoped.enable_paths = Some(vec!["mod1.ts".to_string(), "mod2.ts".to_string()]); @@ -1330,8 +1311,10 @@ mod tests { #[test] fn test_set_workspace_settings_defaults() { let mut config = Config::new(); - config - .set_workspace_settings(serde_json::from_value(json!({})).unwrap(), None); + config.set_workspace_settings( + serde_json::from_value(json!({})).unwrap(), + vec![], + ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings { @@ -1465,7 +1448,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "cache": "" })).unwrap(), - None, + vec![], ); assert_eq!( config.workspace_settings().clone(), @@ -1478,7 +1461,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "import_map": "" })).unwrap(), - None, + vec![], ); assert_eq!( config.workspace_settings().clone(), @@ -1491,7 +1474,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "tls_certificate": "" })).unwrap(), - None, + vec![], ); assert_eq!( config.workspace_settings().clone(), @@ -1504,7 +1487,7 @@ mod tests { let mut config = Config::new(); config.set_workspace_settings( serde_json::from_value(json!({ "config": "" })).unwrap(), - None, + vec![], ); assert_eq!( config.workspace_settings().clone(), @@ -1512,83 +1495,10 @@ mod tests { ); } - #[test] - fn config_get_enabled_paths() { - let mut config = Config::new(); - config.workspace_folders = vec![ - ( - Url::parse("file:///root1/").unwrap(), - lsp::WorkspaceFolder { - uri: Url::parse("file:///root1/").unwrap(), - name: "1".to_string(), - }, - ), - ( - Url::parse("file:///root2/").unwrap(), - lsp::WorkspaceFolder { - uri: Url::parse("file:///root2/").unwrap(), - name: "2".to_string(), - }, - ), - ( - Url::parse("file:///root3/").unwrap(), - lsp::WorkspaceFolder { - uri: Url::parse("file:///root3/").unwrap(), - name: "3".to_string(), - }, - ), - ]; - config.set_workspace_settings( - Default::default(), - Some( - vec![ - ( - Url::parse("file:///root1/").unwrap(), - WorkspaceSettings { - enable_paths: Some(vec![ - "sub_dir".to_string(), - "sub_dir/other".to_string(), - "test.ts".to_string(), - ]), - ..Default::default() - }, - ), - ( - Url::parse("file:///root2/").unwrap(), - WorkspaceSettings { - enable_paths: Some(vec!["other.ts".to_string()]), - ..Default::default() - }, - ), - ( - Url::parse("file:///root3/").unwrap(), - WorkspaceSettings { - enable: Some(true), - ..Default::default() - }, - ), - ] - .into_iter() - .collect(), - ), - ); - - assert_eq!( - config.get_enabled_paths(), - PathOrPatternSet::new(vec![ - PathOrPattern::Path(PathBuf::from("/root1/sub_dir")), - PathOrPattern::Path(PathBuf::from("/root1/sub_dir/other")), - PathOrPattern::Path(PathBuf::from("/root1/test.ts")), - PathOrPattern::Path(PathBuf::from("/root2/other.ts")), - PathOrPattern::Path(PathBuf::from("/root3/")), - ]) - ); - } - #[test] fn config_enable_via_config_file_detection() { let root_uri = resolve_url("file:///root/").unwrap(); - let mut config = Config::new_with_root(root_uri.clone()); + let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.settings.unscoped.enable = None; assert!(!config.specifier_enabled(&root_uri)); @@ -1602,7 +1512,7 @@ mod tests { #[test] fn config_specifier_enabled_matches_by_path_component() { let root_uri = resolve_url("file:///root/").unwrap(); - let mut config = Config::new_with_root(root_uri.clone()); + let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.settings.unscoped.enable_paths = Some(vec!["mo".to_string()]); assert!(!config.specifier_enabled(&root_uri.join("mod.ts").unwrap())); } @@ -1610,7 +1520,7 @@ mod tests { #[test] fn config_specifier_enabled_for_test() { let root_uri = resolve_url("file:///root/").unwrap(); - let mut config = Config::new_with_root(root_uri.clone()); + let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.settings.unscoped.enable = Some(true); config.settings.unscoped.enable_paths = @@ -1693,7 +1603,7 @@ mod tests { #[test] fn config_snapshot_specifier_enabled_for_test() { let root_uri = resolve_url("file:///root/").unwrap(); - let mut config = Config::new_with_root(root_uri.clone()); + let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.settings.unscoped.enable = Some(true); config.set_config_file( ConfigFile::new( diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 7912dad78b..9aa862dbc9 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -3,19 +3,15 @@ use super::cache::calculate_fs_version; use super::cache::calculate_fs_version_at_path; use super::cache::LSP_DISALLOW_GLOBAL_TO_LOCAL_COPY; +use super::config::Config; use super::language_server::StateNpmSnapshot; use super::text::LineIndex; use super::tsc; use super::tsc::AssetDocument; use crate::args::package_json; -use crate::args::package_json::PackageJsonDeps; -use crate::args::ConfigFile; -use crate::args::JsxImportSourceConfig; -use crate::cache::FastInsecureHasher; use crate::cache::HttpCache; use crate::jsr::JsrCacheResolver; -use crate::lsp::logging::lsp_warn; use crate::npm::CliNpmResolver; use crate::resolver::CliGraphResolver; use crate::resolver::CliGraphResolverOptions; @@ -28,9 +24,6 @@ use crate::util::path::specifier_to_file_path; use deno_ast::MediaType; use deno_ast::ParsedSource; use deno_ast::SourceTextInfo; -use deno_config::glob::FilePatterns; -use deno_config::glob::PathOrPattern; -use deno_config::glob::PathOrPatternSet; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::futures::future; @@ -53,15 +46,12 @@ use indexmap::IndexMap; use once_cell::sync::Lazy; use package_json::PackageJsonDepsProvider; use std::borrow::Cow; +use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; use std::fs; -use std::fs::ReadDir; use std::ops::Range; -use std::path::Path; -use std::path::PathBuf; -use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use tower_lsp::lsp_types as lsp; @@ -828,14 +818,12 @@ impl FileSystemDocuments { } pub struct UpdateDocumentConfigOptions<'a> { - pub file_patterns: FilePatterns, - pub document_preload_limit: usize, + pub config: &'a Config, pub maybe_import_map: Option>, - pub maybe_config_file: Option<&'a ConfigFile>, pub maybe_package_json: Option<&'a PackageJson>, - pub maybe_lockfile: Option>>, pub node_resolver: Option>, pub npm_resolver: Option>, + pub workspace_files: &'a BTreeSet, } /// Specify the documents to include on a `documents.documents(...)` call. @@ -863,9 +851,6 @@ pub struct Documents { open_docs: HashMap, /// Documents stored on the file system. file_system_docs: Arc>, - /// Hash of the config used for resolution. When the hash changes we update - /// dependencies. - resolver_config_hash: u64, /// Any imports to the context supplied by configuration files. This is like /// the imports into the a module graph in CLI. imports: Arc>, @@ -893,7 +878,6 @@ impl Documents { dependents_map: Default::default(), open_docs: HashMap::default(), file_system_docs: Default::default(), - resolver_config_hash: 0, imports: Default::default(), resolver: Arc::new(CliGraphResolver::new(CliGraphResolverOptions { node_resolver: None, @@ -1326,84 +1310,13 @@ impl Documents { } pub fn update_config(&mut self, options: UpdateDocumentConfigOptions) { - #[allow(clippy::too_many_arguments)] - fn calculate_resolver_config_hash( - file_patterns: &FilePatterns, - document_preload_limit: usize, - maybe_import_map: Option<&import_map::ImportMap>, - maybe_jsx_config: Option<&JsxImportSourceConfig>, - maybe_vendor_dir: Option, - maybe_package_json_deps: Option<&PackageJsonDeps>, - maybe_unstable_flags: Option<&Vec>, - ) -> u64 { - fn get_pattern_set_vec(set: &PathOrPatternSet) -> Vec> { - let mut paths = set - .inner() - .iter() - .map(|p| match p { - PathOrPattern::Path(p) => p.to_string_lossy(), - PathOrPattern::NegatedPath(p) => { - Cow::Owned(format!("!{}", p.to_string_lossy())) - } - PathOrPattern::RemoteUrl(p) => Cow::Borrowed(p.as_str()), - PathOrPattern::Pattern(p) => p.as_str(), - }) - .collect::>(); - // ensure these are sorted so the hashing is deterministic - paths.sort_unstable(); - paths - } - - let mut hasher = FastInsecureHasher::default(); - hasher.write_hashable(document_preload_limit); - hasher.write_hashable( - &file_patterns.include.as_ref().map(get_pattern_set_vec), - ); - hasher.write_hashable(&get_pattern_set_vec(&file_patterns.exclude)); - if let Some(import_map) = maybe_import_map { - hasher.write_str(&import_map.to_json()); - hasher.write_str(import_map.base_url().as_str()); - } - hasher.write_hashable(maybe_vendor_dir); - hasher.write_hashable(maybe_jsx_config); - hasher.write_hashable(maybe_unstable_flags); - if let Some(package_json_deps) = &maybe_package_json_deps { - // We need to ensure the hashing is deterministic so explicitly type - // this in order to catch if the type of package_json_deps ever changes - // from a deterministic IndexMap to something else. - let package_json_deps: &IndexMap<_, _> = *package_json_deps; - for (key, value) in package_json_deps { - hasher.write_hashable(key); - match value { - Ok(value) => { - hasher.write_hashable(value); - } - Err(err) => { - hasher.write_str(&err.to_string()); - } - } - } - } - - hasher.finish() - } - + let maybe_config_file = options.config.maybe_config_file(); let maybe_package_json_deps = options.maybe_package_json.map(|package_json| { package_json::get_local_package_json_version_reqs(package_json) }); - let maybe_jsx_config = options - .maybe_config_file + let maybe_jsx_config = maybe_config_file .and_then(|cf| cf.to_maybe_jsx_import_source_config().ok().flatten()); - let new_resolver_config_hash = calculate_resolver_config_hash( - &options.file_patterns, - options.document_preload_limit, - options.maybe_import_map.as_deref(), - maybe_jsx_config.as_ref(), - options.maybe_config_file.and_then(|c| c.json.vendor), - maybe_package_json_deps.as_ref(), - options.maybe_config_file.map(|c| &c.json.unstable), - ); let deps_provider = Arc::new(PackageJsonDepsProvider::new(maybe_package_json_deps)); self.resolver = Arc::new(CliGraphResolver::new(CliGraphResolverOptions { @@ -1412,12 +1325,10 @@ impl Documents { package_json_deps_provider: deps_provider, maybe_jsx_import_source_config: maybe_jsx_config, maybe_import_map: options.maybe_import_map, - maybe_vendor_dir: options - .maybe_config_file + maybe_vendor_dir: maybe_config_file .and_then(|c| c.vendor_dir_path()) .as_ref(), - bare_node_builtins_enabled: options - .maybe_config_file + bare_node_builtins_enabled: maybe_config_file .map(|config| config.has_unstable("bare-node-builtins")) .unwrap_or(false), // Don't set this for the LSP because instead we'll use the OpenDocumentsLoader @@ -1427,13 +1338,15 @@ impl Documents { })); self.jsr_resolver = Arc::new(JsrCacheResolver::new( self.cache.clone(), - options.maybe_lockfile, + options.config.maybe_lockfile().cloned(), )); self.redirect_resolver = Arc::new(RedirectResolver::new(self.cache.clone())); + let resolver = self.resolver.as_graph_resolver(); + let npm_resolver = self.resolver.as_graph_npm_resolver(); self.imports = Arc::new( if let Some(Ok(imports)) = - options.maybe_config_file.map(|cf| cf.to_maybe_imports()) + maybe_config_file.map(|cf| cf.to_maybe_imports()) { imports .into_iter() @@ -1441,8 +1354,8 @@ impl Documents { let graph_import = GraphImport::new( &referrer, imports, - Some(self.get_resolver()), - Some(self.get_npm_resolver()), + Some(resolver), + Some(npm_resolver), ); (referrer, graph_import) }) @@ -1451,121 +1364,56 @@ impl Documents { IndexMap::new() }, ); - self.unstable_sloppy_imports = options - .maybe_config_file + self.unstable_sloppy_imports = maybe_config_file .map(|c| c.has_unstable("sloppy-imports")) .unwrap_or(false); - - // only refresh the dependencies if the underlying configuration has changed - if self.resolver_config_hash != new_resolver_config_hash { - self.refresh_dependencies( - options.file_patterns, - options.document_preload_limit, - ); - self.resolver_config_hash = new_resolver_config_hash; - - self.increment_project_version(); - self.dirty = true; - self.calculate_dependents_if_dirty(); - } - } - - fn refresh_dependencies( - &mut self, - file_patterns: FilePatterns, - document_preload_limit: usize, - ) { - let resolver = self.resolver.as_graph_resolver(); - let npm_resolver = self.resolver.as_graph_npm_resolver(); - for doc in self.open_docs.values_mut() { - if let Some(new_doc) = doc.maybe_with_new_resolver(resolver, npm_resolver) - { - *doc = new_doc; + { + let mut fs_docs = self.file_system_docs.lock(); + // Clean up non-existent documents. + fs_docs.docs.retain(|specifier, _| { + let Ok(path) = specifier_to_file_path(specifier) else { + // Remove non-file schemed docs (deps). They may not be dependencies + // anymore after updating resolvers. + return false; + }; + if !options.config.specifier_enabled(specifier) { + return false; + } + path.is_file() + }); + let mut open_docs = std::mem::take(&mut self.open_docs); + for docs in [&mut open_docs, &mut fs_docs.docs] { + for doc in docs.values_mut() { + if !options.config.specifier_enabled(doc.specifier()) { + continue; + } + if let Some(new_doc) = + doc.maybe_with_new_resolver(resolver, npm_resolver) + { + *doc = new_doc; + } + } } - } - - // update the file system documents - let mut fs_docs = self.file_system_docs.lock(); - if document_preload_limit > 0 { - let mut not_found_docs = - fs_docs.docs.keys().cloned().collect::>(); - let open_docs = &mut self.open_docs; - - log::debug!("Preloading documents from enabled urls..."); - let mut finder = - PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns, - limit: document_preload_limit, - }); - for specifier in finder.by_ref() { - // mark this document as having been found - not_found_docs.remove(&specifier); - - if !open_docs.contains_key(&specifier) - && !fs_docs.docs.contains_key(&specifier) + self.open_docs = open_docs; + for specifier in options.workspace_files { + if !options.config.specifier_enabled(specifier) { + continue; + } + if !self.open_docs.contains_key(specifier) + && !fs_docs.docs.contains_key(specifier) { fs_docs.refresh_document( &self.cache, resolver, - &specifier, + specifier, npm_resolver, ); - } else { - // update the existing entry to have the new resolver - if let Some(doc) = fs_docs.docs.get_mut(&specifier) { - if let Some(new_doc) = - doc.maybe_with_new_resolver(resolver, npm_resolver) - { - *doc = new_doc; - } - } - } - } - - if finder.hit_limit() { - lsp_warn!( - concat!( - "Hit the language server document preload limit of {} file system entries. ", - "You may want to use the \"deno.enablePaths\" configuration setting to only have Deno ", - "partially enable a workspace or increase the limit via \"deno.documentPreloadLimit\". ", - "In cases where Deno ends up using too much memory, you may want to lower the limit." - ), - document_preload_limit, - ); - - // since we hit the limit, just update everything to use the new resolver - for uri in not_found_docs { - if let Some(doc) = fs_docs.docs.get_mut(&uri) { - if let Some(new_doc) = - doc.maybe_with_new_resolver(resolver, npm_resolver) - { - *doc = new_doc; - } - } - } - } else { - // clean up and remove any documents that weren't found - for uri in not_found_docs { - fs_docs.docs.remove(&uri); - } - } - } else { - // This log statement is used in the tests to ensure preloading doesn't - // happen, which is not useful in the repl and could be very expensive - // if the repl is launched from a directory with a lot of descendants. - log::debug!("Skipping document preload."); - - // just update to use the new resolver - for doc in fs_docs.docs.values_mut() { - if let Some(new_doc) = - doc.maybe_with_new_resolver(resolver, npm_resolver) - { - *doc = new_doc; } } + fs_docs.dirty = true; } - - fs_docs.dirty = true; + self.dirty = true; + self.calculate_dependents_if_dirty(); } /// Iterate through the documents, building a map where the key is a unique @@ -1884,230 +1732,13 @@ fn analyze_module( } } -#[derive(Debug)] -enum PendingEntry { - /// File specified as a root url. - SpecifiedRootFile(PathBuf), - /// Directory that is queued to read. - Dir(PathBuf, Rc), - /// The current directory being read. - ReadDir(Box, Rc), -} - -struct PreloadDocumentFinderOptions { - file_patterns: FilePatterns, - limit: usize, -} - -/// Iterator that finds documents that can be preloaded into -/// the LSP on startup. -struct PreloadDocumentFinder { - limit: usize, - entry_count: usize, - pending_entries: VecDeque, - root_dir_entries: Vec, - visited_paths: HashSet, -} - -impl PreloadDocumentFinder { - pub fn new(options: PreloadDocumentFinderOptions) -> Self { - fn is_allowed_root_dir(dir_path: &Path) -> bool { - if dir_path.parent().is_none() { - // never search the root directory of a drive - return false; - } - true - } - - let mut finder = PreloadDocumentFinder { - limit: options.limit, - entry_count: 0, - pending_entries: Default::default(), - root_dir_entries: Default::default(), - visited_paths: Default::default(), - }; - - let file_patterns_by_base = options.file_patterns.split_by_base(); - - // initialize the finder with the initial paths - for file_patterns in file_patterns_by_base { - let path = &file_patterns.base; - if path.is_dir() { - if is_allowed_root_dir(path) { - finder - .root_dir_entries - .push(PendingEntry::Dir(path.clone(), Rc::new(file_patterns))); - } - } else { - finder - .pending_entries - .push_back(PendingEntry::SpecifiedRootFile(path.clone())); - } - } - finder - } - - pub fn hit_limit(&self) -> bool { - self.entry_count >= self.limit - } - - fn get_valid_specifier(path: &Path) -> Option { - fn is_allowed_media_type(media_type: MediaType) -> bool { - match media_type { - MediaType::JavaScript - | MediaType::Jsx - | MediaType::Mjs - | MediaType::Cjs - | MediaType::TypeScript - | MediaType::Mts - | MediaType::Cts - | MediaType::Dts - | MediaType::Dmts - | MediaType::Dcts - | MediaType::Tsx => true, - MediaType::Json // ignore because json never depends on other files - | MediaType::Wasm - | MediaType::SourceMap - | MediaType::TsBuildInfo - | MediaType::Unknown => false, - } - } - - let media_type = MediaType::from_path(path); - if is_allowed_media_type(media_type) { - if let Ok(specifier) = ModuleSpecifier::from_file_path(path) { - return Some(specifier); - } - } - None - } -} - -impl Iterator for PreloadDocumentFinder { - type Item = ModuleSpecifier; - - fn next(&mut self) -> Option { - fn is_discoverable_dir(dir_path: &Path) -> bool { - if let Some(dir_name) = dir_path.file_name() { - let dir_name = dir_name.to_string_lossy().to_lowercase(); - // We ignore these directories by default because there is a - // high likelihood they aren't relevant. Someone can opt-into - // them by specifying one of them as an enabled path. - if matches!(dir_name.as_str(), "node_modules" | ".git") { - return false; - } - - // ignore cargo target directories for anyone using Deno with Rust - if dir_name == "target" - && dir_path - .parent() - .map(|p| p.join("Cargo.toml").exists()) - .unwrap_or(false) - { - return false; - } - - true - } else { - false - } - } - - fn is_discoverable_file(file_path: &Path) -> bool { - // Don't auto-discover minified files as they are likely to be very large - // and likely not to have dependencies on code outside them that would - // be useful in the LSP - if let Some(file_name) = file_path.file_name() { - let file_name = file_name.to_string_lossy().to_lowercase(); - !file_name.as_str().contains(".min.") - } else { - false - } - } - - // This first drains all the pending entries then adds the root dir entries - // one at a time to the pending entries before draining them. This is because - // we're traversing based on directory depth, so we want to search deeper - // directories first - while !self.pending_entries.is_empty() || !self.root_dir_entries.is_empty() - { - while let Some(entry) = self.pending_entries.pop_front() { - match entry { - PendingEntry::SpecifiedRootFile(file) => { - // since it was a file that was specified as a root url, only - // verify that it's valid - if let Some(specifier) = Self::get_valid_specifier(&file) { - return Some(specifier); - } - } - PendingEntry::Dir(dir_path, file_patterns) => { - if self.visited_paths.insert(dir_path.clone()) { - if let Ok(read_dir) = fs::read_dir(&dir_path) { - self.pending_entries.push_back(PendingEntry::ReadDir( - Box::new(read_dir), - file_patterns, - )); - } - } - } - PendingEntry::ReadDir(mut entries, file_patterns) => { - while let Some(entry) = entries.next() { - self.entry_count += 1; - - if self.hit_limit() { - self.pending_entries.clear(); // stop searching - return None; - } - - if let Ok(entry) = entry { - let path = entry.path(); - if let Ok(file_type) = entry.file_type() { - let is_dir = file_type.is_dir(); - let path_kind = match is_dir { - true => deno_config::glob::PathKind::Directory, - false => deno_config::glob::PathKind::File, - }; - if file_patterns.matches_path(&path, path_kind) { - if is_dir && is_discoverable_dir(&path) { - self.pending_entries.push_back(PendingEntry::Dir( - path.to_path_buf(), - file_patterns.clone(), - )); - } else if file_type.is_file() && is_discoverable_file(&path) - { - if let Some(specifier) = Self::get_valid_specifier(&path) - { - // restore the next entries for next time - self.pending_entries.push_front(PendingEntry::ReadDir( - entries, - file_patterns.clone(), - )); - return Some(specifier); - } - } - } - } - } - } - } - } - } - - if let Some(entry) = self.root_dir_entries.pop() { - self.pending_entries.push_back(entry); - } - } - - None - } -} - #[cfg(test)] mod tests { use crate::cache::GlobalHttpCache; use crate::cache::RealDenoCacheEnv; use super::*; + use deno_core::serde_json; use import_map::ImportMap; use pretty_assertions::assert_eq; use test_util::PathRef; @@ -2231,6 +1862,20 @@ console.log(b, "hello deno"); let file3_specifier = ModuleSpecifier::from_file_path(&file3_path).unwrap(); fs::write(&file3_path, "").unwrap(); + let mut config = + Config::new_with_roots(vec![ModuleSpecifier::from_directory_path( + &documents_path, + ) + .unwrap()]); + let workspace_settings = + serde_json::from_str(r#"{ "enable": true }"#).unwrap(); + config.set_workspace_settings(workspace_settings, vec![]); + let workspace_files = + [&file1_specifier, &file2_specifier, &file3_specifier] + .into_iter() + .cloned() + .collect::>(); + // set the initial import map and point to file 2 { let mut import_map = ImportMap::new( @@ -2243,16 +1888,12 @@ console.log(b, "hello deno"); .unwrap(); documents.update_config(UpdateDocumentConfigOptions { - file_patterns: FilePatterns::new_with_base( - documents_path.to_path_buf(), - ), - document_preload_limit: 1_000, + config: &config, maybe_import_map: Some(Arc::new(import_map)), - maybe_config_file: None, maybe_package_json: None, - maybe_lockfile: None, node_resolver: None, npm_resolver: None, + workspace_files: &workspace_files, }); // open the document @@ -2287,16 +1928,12 @@ console.log(b, "hello deno"); .unwrap(); documents.update_config(UpdateDocumentConfigOptions { - file_patterns: FilePatterns::new_with_base( - documents_path.to_path_buf(), - ), - document_preload_limit: 1_000, + config: &config, maybe_import_map: Some(Arc::new(import_map)), - maybe_config_file: None, maybe_package_json: None, - maybe_lockfile: None, node_resolver: None, npm_resolver: None, + workspace_files: &workspace_files, }); // check the document's dependencies @@ -2313,163 +1950,4 @@ console.log(b, "hello deno"); ); } } - - #[test] - pub fn test_pre_load_document_finder() { - let temp_dir = TempDir::new(); - temp_dir.create_dir_all("root1/node_modules/"); - temp_dir.write("root1/node_modules/mod.ts", ""); // no, node_modules - - temp_dir.create_dir_all("root1/sub_dir"); - temp_dir.create_dir_all("root1/target"); - temp_dir.create_dir_all("root1/node_modules"); - temp_dir.create_dir_all("root1/.git"); - temp_dir.create_dir_all("root1/file.ts"); // no, directory - temp_dir.write("root1/mod1.ts", ""); // yes - temp_dir.write("root1/mod2.js", ""); // yes - temp_dir.write("root1/mod3.tsx", ""); // yes - temp_dir.write("root1/mod4.d.ts", ""); // yes - temp_dir.write("root1/mod5.jsx", ""); // yes - temp_dir.write("root1/mod6.mjs", ""); // yes - temp_dir.write("root1/mod7.mts", ""); // yes - temp_dir.write("root1/mod8.d.mts", ""); // yes - temp_dir.write("root1/other.json", ""); // no, json - temp_dir.write("root1/other.txt", ""); // no, text file - temp_dir.write("root1/other.wasm", ""); // no, don't load wasm - temp_dir.write("root1/Cargo.toml", ""); // no - temp_dir.write("root1/sub_dir/mod.ts", ""); // yes - temp_dir.write("root1/sub_dir/data.min.ts", ""); // no, minified file - temp_dir.write("root1/.git/main.ts", ""); // no, .git folder - temp_dir.write("root1/node_modules/main.ts", ""); // no, because it's in a node_modules folder - temp_dir.write("root1/target/main.ts", ""); // no, because there is a Cargo.toml in the root directory - - temp_dir.create_dir_all("root2/folder"); - temp_dir.create_dir_all("root2/sub_folder"); - temp_dir.write("root2/file1.ts", ""); // yes, provided - temp_dir.write("root2/file2.ts", ""); // no, not provided - temp_dir.write("root2/main.min.ts", ""); // yes, provided - temp_dir.write("root2/folder/main.ts", ""); // yes, provided - temp_dir.write("root2/sub_folder/a.js", ""); // no, not provided - temp_dir.write("root2/sub_folder/b.ts", ""); // no, not provided - temp_dir.write("root2/sub_folder/c.js", ""); // no, not provided - - temp_dir.create_dir_all("root3/"); - temp_dir.write("root3/mod.ts", ""); // no, not provided - - let mut urls = PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns: FilePatterns { - base: temp_dir.path().to_path_buf(), - include: Some( - PathOrPatternSet::from_include_relative_path_or_patterns( - temp_dir.path().as_path(), - &[ - "root1".to_string(), - "root2/file1.ts".to_string(), - "root2/main.min.ts".to_string(), - "root2/folder".to_string(), - ], - ) - .unwrap(), - ), - exclude: Default::default(), - }, - limit: 1_000, - }) - .collect::>(); - - // Ideally we would test for order here, which should be BFS, but - // different file systems have different directory iteration - // so we sort the results - urls.sort(); - - assert_eq!( - urls, - vec![ - temp_dir.uri().join("root1/mod1.ts").unwrap(), - temp_dir.uri().join("root1/mod2.js").unwrap(), - temp_dir.uri().join("root1/mod3.tsx").unwrap(), - temp_dir.uri().join("root1/mod4.d.ts").unwrap(), - temp_dir.uri().join("root1/mod5.jsx").unwrap(), - temp_dir.uri().join("root1/mod6.mjs").unwrap(), - temp_dir.uri().join("root1/mod7.mts").unwrap(), - temp_dir.uri().join("root1/mod8.d.mts").unwrap(), - temp_dir.uri().join("root1/sub_dir/mod.ts").unwrap(), - temp_dir.uri().join("root2/file1.ts").unwrap(), - temp_dir.uri().join("root2/folder/main.ts").unwrap(), - temp_dir.uri().join("root2/main.min.ts").unwrap(), - ] - ); - - // now try iterating with a low limit - let urls = PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns: FilePatterns { - base: temp_dir.path().to_path_buf(), - include: Default::default(), - exclude: Default::default(), - }, - limit: 10, // entries and not results - }) - .collect::>(); - - // since different file system have different iteration - // order, the number here may vary, so just assert it's below - // a certain amount - assert!(urls.len() < 5, "Actual length: {}", urls.len()); - - // now try with certain directories and files disabled - let mut urls = PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns: FilePatterns { - base: temp_dir.path().to_path_buf(), - include: Default::default(), - exclude: PathOrPatternSet::from_exclude_relative_path_or_patterns( - temp_dir.path().as_path(), - &[ - "root1".to_string(), - "root2/file1.ts".to_string(), - "**/*.js".to_string(), // ignore js files - ], - ) - .unwrap(), - }, - limit: 1_000, - }) - .collect::>(); - urls.sort(); - assert_eq!( - urls, - vec![ - temp_dir.uri().join("root2/file2.ts").unwrap(), - temp_dir.uri().join("root2/folder/main.ts").unwrap(), - temp_dir.uri().join("root2/sub_folder/b.ts").unwrap(), // won't have the javascript files - temp_dir.uri().join("root3/mod.ts").unwrap(), - ] - ); - } - - #[test] - pub fn test_pre_load_document_finder_disallowed_dirs() { - if cfg!(windows) { - let paths = PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns: FilePatterns { - base: PathBuf::from("C:\\"), - include: Default::default(), - exclude: Default::default(), - }, - limit: 1_000, - }) - .collect::>(); - assert_eq!(paths, vec![]); - } else { - let paths = PreloadDocumentFinder::new(PreloadDocumentFinderOptions { - file_patterns: FilePatterns { - base: PathBuf::from("/"), - include: Default::default(), - exclude: Default::default(), - }, - limit: 1_000, - }) - .collect::>(); - assert_eq!(paths, vec![]); - } - } } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 08bb37c7c6..7d9c4318b5 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -2,7 +2,6 @@ use base64::Engine; use deno_ast::MediaType; -use deno_config::glob::FilePatterns; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; @@ -28,9 +27,10 @@ use indexmap::IndexSet; use log::error; use serde::Deserialize; use serde_json::from_value; -use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; +use std::collections::VecDeque; use std::env; use std::fmt::Write as _; use std::path::Path; @@ -274,6 +274,10 @@ pub struct Inner { pub ts_server: Arc, /// A map of specifiers and URLs used to translate over the LSP. pub url_map: urls::LspUrlMap, + workspace_files: BTreeSet, + /// Set to `self.config.settings.enable_settings_hash()` after + /// refreshing `self.workspace_files`. + workspace_files_hash: u64, } impl LanguageServer { @@ -486,14 +490,12 @@ impl LanguageServer { } let mut configs = configs.into_iter(); let unscoped = configs.next().unwrap(); - let mut by_workspace_folder = BTreeMap::new(); + let mut folder_settings = Vec::with_capacity(folders.len()); for (folder_uri, _) in &folders { - by_workspace_folder - .insert(folder_uri.clone(), configs.next().unwrap()); + folder_settings.push((folder_uri.clone(), configs.next().unwrap())); } let mut ls = self.0.write().await; - ls.config - .set_workspace_settings(unscoped, Some(by_workspace_folder)); + ls.config.set_workspace_settings(unscoped, folder_settings); } } } @@ -574,6 +576,8 @@ impl Inner { ts_fixable_diagnostics: Default::default(), ts_server, url_map: Default::default(), + workspace_files: Default::default(), + workspace_files_hash: 0, } } @@ -1226,11 +1230,12 @@ impl Inner { if let Some(options) = params.initialization_options { self.config.set_workspace_settings( WorkspaceSettings::from_initialization_options(options), - None, + vec![], ); } + let mut workspace_folders = vec![]; if let Some(folders) = params.workspace_folders { - self.config.workspace_folders = folders + workspace_folders = folders .into_iter() .map(|folder| { ( @@ -1243,15 +1248,10 @@ impl Inner { // rootUri is deprecated by the LSP spec. If it's specified, merge it into // workspace_folders. if let Some(root_uri) = params.root_uri { - if !self - .config - .workspace_folders - .iter() - .any(|(_, f)| f.uri == root_uri) - { + if !workspace_folders.iter().any(|(_, f)| f.uri == root_uri) { let name = root_uri.path_segments().and_then(|s| s.last()); let name = name.unwrap_or_default().to_string(); - self.config.workspace_folders.insert( + workspace_folders.insert( 0, ( self.url_map.normalize_url(&root_uri, LspUrlKind::Folder), @@ -1263,6 +1263,7 @@ impl Inner { ); } } + self.config.set_workspace_folders(workspace_folders); self.config.update_capabilities(¶ms.capabilities); } @@ -1319,23 +1320,144 @@ impl Inner { }) } + fn walk_workspace(config: &Config) -> (BTreeSet, bool) { + let mut workspace_files = Default::default(); + let document_preload_limit = + config.workspace_settings().document_preload_limit; + let mut pending = VecDeque::new(); + let mut entry_count = 0; + let mut roots = config + .workspace_folders + .iter() + .filter_map(|p| specifier_to_file_path(&p.0).ok()) + .collect::>(); + roots.sort(); + for i in 0..roots.len() { + if i == 0 || !roots[i].starts_with(&roots[i - 1]) { + if let Ok(read_dir) = std::fs::read_dir(&roots[i]) { + pending.push_back((roots[i].clone(), read_dir)); + } + } + } + while let Some((parent_path, read_dir)) = pending.pop_front() { + for entry in read_dir { + let Ok(entry) = entry else { + continue; + }; + if entry_count >= document_preload_limit { + return (workspace_files, true); + } + entry_count += 1; + let path = parent_path.join(entry.path()); + let Ok(specifier) = ModuleSpecifier::from_file_path(&path) else { + continue; + }; + // TODO(nayeemrmn): Don't walk folders that are `None` here and aren't + // in a `deno.json` scope. + if config.settings.specifier_enabled(&specifier) == Some(false) { + continue; + } + let Ok(file_type) = entry.file_type() else { + continue; + }; + let Some(file_name) = path.file_name() else { + continue; + }; + if file_type.is_dir() { + let dir_name = file_name.to_string_lossy().to_lowercase(); + // We ignore these directories by default because there is a + // high likelihood they aren't relevant. Someone can opt-into + // them by specifying one of them as an enabled path. + if matches!(dir_name.as_str(), "node_modules" | ".git") { + continue; + } + // ignore cargo target directories for anyone using Deno with Rust + if dir_name == "target" + && path + .parent() + .map(|p| p.join("Cargo.toml").exists()) + .unwrap_or(false) + { + continue; + } + if let Ok(read_dir) = std::fs::read_dir(&path) { + pending.push_back((path, read_dir)); + } + } else if file_type.is_file() + || file_type.is_symlink() + && std::fs::metadata(&path) + .ok() + .map(|m| m.is_file()) + .unwrap_or(false) + { + if file_name.to_string_lossy().contains(".min.") { + continue; + } + let media_type = MediaType::from_specifier(&specifier); + match media_type { + MediaType::JavaScript + | MediaType::Jsx + | MediaType::Mjs + | MediaType::Cjs + | MediaType::TypeScript + | MediaType::Mts + | MediaType::Cts + | MediaType::Dts + | MediaType::Dmts + | MediaType::Dcts + | MediaType::Json + | MediaType::Tsx => {} + MediaType::Wasm + | MediaType::SourceMap + | MediaType::TsBuildInfo + | MediaType::Unknown => { + if path.extension().and_then(|s| s.to_str()) != Some("jsonc") { + continue; + } + } + } + workspace_files.insert(specifier); + } + } + } + (workspace_files, false) + } + + fn refresh_workspace_files(&mut self) { + let enable_settings_hash = self.config.settings.enable_settings_hash(); + if self.workspace_files_hash == enable_settings_hash { + return; + } + let (workspace_files, hit_limit) = Self::walk_workspace(&self.config); + if hit_limit { + let document_preload_limit = + self.config.workspace_settings().document_preload_limit; + if document_preload_limit == 0 { + log::debug!("Skipped document preload."); + } else { + lsp_warn!( + concat!( + "Hit the language server document preload limit of {} file system entries. ", + "You may want to use the \"deno.enablePaths\" configuration setting to only have Deno ", + "partially enable a workspace or increase the limit via \"deno.documentPreloadLimit\". ", + "In cases where Deno ends up using too much memory, you may want to lower the limit." + ), + document_preload_limit, + ); + } + } + self.workspace_files = workspace_files; + self.workspace_files_hash = enable_settings_hash; + } + async fn refresh_documents_config(&mut self) { self.documents.update_config(UpdateDocumentConfigOptions { - file_patterns: FilePatterns { - base: self.initial_cwd.clone(), - include: Some(self.config.get_enabled_paths()), - exclude: self.config.get_disabled_paths(), - }, - document_preload_limit: self - .config - .workspace_settings() - .document_preload_limit, + config: &self.config, maybe_import_map: self.maybe_import_map.clone(), - maybe_config_file: self.config.maybe_config_file(), maybe_package_json: self.maybe_package_json.as_ref(), - maybe_lockfile: self.config.maybe_lockfile().cloned(), node_resolver: self.npm.node_resolver.clone(), npm_resolver: self.npm.resolver.clone(), + workspace_files: &self.workspace_files, }); // refresh the npm specifiers because it might have discovered @@ -1464,7 +1586,7 @@ impl Inner { WorkspaceSettings::from_raw_settings(deno, javascript, typescript) }); if let Some(settings) = config { - self.config.set_workspace_settings(settings, None); + self.config.set_workspace_settings(settings, vec![]); } }; @@ -1495,6 +1617,7 @@ impl Inner { } self.recreate_npm_services_if_necessary().await; + self.refresh_workspace_files(); self.refresh_documents_config().await; self.diagnostics_server.invalidate_all(); @@ -1693,6 +1816,7 @@ impl Inner { if touched { self.recreate_npm_services_if_necessary().await; + self.refresh_workspace_files(); self.refresh_documents_config().await; self.diagnostics_server.invalidate_all(); self.ts_server.restart(self.snapshot()).await; @@ -1725,8 +1849,7 @@ impl Inner { } workspace_folders.push((specifier.clone(), folder.clone())); } - - self.config.workspace_folders = workspace_folders; + self.config.set_workspace_folders(workspace_folders); } async fn document_symbol( @@ -3385,6 +3508,7 @@ impl tower_lsp::LanguageServer for LanguageServer { lsp_warn!("Error updating tsconfig: {:#}", err); ls.client.show_message(MessageType::WARNING, err); } + ls.refresh_workspace_files(); ls.refresh_documents_config().await; ls.diagnostics_server.invalidate_all(); ls.send_diagnostics_update(); @@ -3518,6 +3642,7 @@ impl tower_lsp::LanguageServer for LanguageServer { self.refresh_configuration().await; { let mut ls = self.0.write().await; + ls.refresh_workspace_files(); ls.refresh_documents_config().await; ls.diagnostics_server.invalidate_all(); ls.send_diagnostics_update(); @@ -3973,3 +4098,112 @@ impl Inner { Ok(contents) } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use test_util::TempDir; + + #[test] + fn test_walk_workspace() { + let temp_dir = TempDir::new(); + temp_dir.create_dir_all("root1/node_modules/"); + temp_dir.write("root1/node_modules/mod.ts", ""); // no, node_modules + + temp_dir.create_dir_all("root1/sub_dir"); + temp_dir.create_dir_all("root1/target"); + temp_dir.create_dir_all("root1/node_modules"); + temp_dir.create_dir_all("root1/.git"); + temp_dir.create_dir_all("root1/file.ts"); // no, directory + temp_dir.write("root1/mod0.ts", ""); // yes + temp_dir.write("root1/mod1.js", ""); // yes + temp_dir.write("root1/mod2.tsx", ""); // yes + temp_dir.write("root1/mod3.d.ts", ""); // yes + temp_dir.write("root1/mod4.jsx", ""); // yes + temp_dir.write("root1/mod5.mjs", ""); // yes + temp_dir.write("root1/mod6.mts", ""); // yes + temp_dir.write("root1/mod7.d.mts", ""); // yes + temp_dir.write("root1/mod8.json", ""); // yes + temp_dir.write("root1/mod9.jsonc", ""); // yes + temp_dir.write("root1/other.txt", ""); // no, text file + temp_dir.write("root1/other.wasm", ""); // no, don't load wasm + temp_dir.write("root1/Cargo.toml", ""); // no + temp_dir.write("root1/sub_dir/mod.ts", ""); // yes + temp_dir.write("root1/sub_dir/data.min.ts", ""); // no, minified file + temp_dir.write("root1/.git/main.ts", ""); // no, .git folder + temp_dir.write("root1/node_modules/main.ts", ""); // no, because it's in a node_modules folder + temp_dir.write("root1/target/main.ts", ""); // no, because there is a Cargo.toml in the root directory + + temp_dir.create_dir_all("root2/folder"); + temp_dir.create_dir_all("root2/sub_folder"); + temp_dir.write("root2/file1.ts", ""); // yes, enabled + temp_dir.write("root2/file2.ts", ""); // no, not enabled + temp_dir.write("root2/folder/main.ts", ""); // yes, enabled + temp_dir.write("root2/folder/other.ts", ""); // no, disabled + temp_dir.write("root2/sub_folder/a.js", ""); // no, not enabled + temp_dir.write("root2/sub_folder/b.ts", ""); // no, not enabled + temp_dir.write("root2/sub_folder/c.js", ""); // no, not enabled + + temp_dir.create_dir_all("root3/"); + temp_dir.write("root3/mod.ts", ""); // no, not enabled + + let mut config = Config::new_with_roots(vec![ + temp_dir.uri().join("root1/").unwrap(), + temp_dir.uri().join("root2/").unwrap(), + temp_dir.uri().join("root3/").unwrap(), + ]); + config.set_workspace_settings( + Default::default(), + vec![ + ( + temp_dir.uri().join("root1/").unwrap(), + WorkspaceSettings { + enable: Some(true), + ..Default::default() + }, + ), + ( + temp_dir.uri().join("root2/").unwrap(), + WorkspaceSettings { + enable: Some(true), + enable_paths: Some(vec![ + "file1.ts".to_string(), + "folder".to_string(), + ]), + disable_paths: vec!["folder/other.ts".to_string()], + ..Default::default() + }, + ), + ( + temp_dir.uri().join("root3/").unwrap(), + WorkspaceSettings { + enable: Some(false), + ..Default::default() + }, + ), + ], + ); + + let (workspace_files, hit_limit) = Inner::walk_workspace(&config); + assert!(!hit_limit); + assert_eq!( + json!(workspace_files), + json!([ + temp_dir.uri().join("root1/mod0.ts").unwrap(), + temp_dir.uri().join("root1/mod1.js").unwrap(), + temp_dir.uri().join("root1/mod2.tsx").unwrap(), + temp_dir.uri().join("root1/mod3.d.ts").unwrap(), + temp_dir.uri().join("root1/mod4.jsx").unwrap(), + temp_dir.uri().join("root1/mod5.mjs").unwrap(), + temp_dir.uri().join("root1/mod6.mts").unwrap(), + temp_dir.uri().join("root1/mod7.d.mts").unwrap(), + temp_dir.uri().join("root1/mod8.json").unwrap(), + temp_dir.uri().join("root1/mod9.jsonc").unwrap(), + temp_dir.uri().join("root1/sub_dir/mod.ts").unwrap(), + temp_dir.uri().join("root2/file1.ts").unwrap(), + temp_dir.uri().join("root2/folder/main.ts").unwrap(), + ]) + ); + } +} diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 3fc78a20f2..e88aba07be 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -5469,7 +5469,7 @@ mod tests { .variable_types .suppress_when_type_matches_name = true; let mut config = config::Config::new(); - config.set_workspace_settings(settings, None); + config.set_workspace_settings(settings, vec![]); let user_preferences = UserPreferences::from_config_for_specifier( &config, &Default::default(), diff --git a/tests/integration/repl_tests.rs b/tests/integration/repl_tests.rs index 7a09f904e8..26a92492ce 100644 --- a/tests/integration/repl_tests.rs +++ b/tests/integration/repl_tests.rs @@ -1085,7 +1085,7 @@ fn closed_file_pre_load_does_not_occur() { .new_command() .args_vec(["repl", "-A", "--log-level=debug"]) .with_pty(|console| { - assert_contains!(console.all_output(), "Skipping document preload.",); + assert_contains!(console.all_output(), "Skipped document preload.",); }); }