diff options
-rw-r--r-- | cli/lsp/analysis.rs | 13 | ||||
-rw-r--r-- | cli/lsp/completions.rs | 10 | ||||
-rw-r--r-- | cli/lsp/config.rs | 347 | ||||
-rw-r--r-- | cli/lsp/diagnostics.rs | 15 | ||||
-rw-r--r-- | cli/lsp/documents.rs | 12 | ||||
-rw-r--r-- | cli/lsp/language_server.rs | 110 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 72 | ||||
-rw-r--r-- | tests/integration/lsp_tests.rs | 206 | ||||
-rw-r--r-- | tests/util/server/src/builders.rs | 3 |
9 files changed, 476 insertions, 312 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index cd7d45928..ce5d0c7f4 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -43,7 +43,6 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::collections::HashSet; use std::path::Path; -use std::sync::Arc; use tower_lsp::lsp_types as lsp; use tower_lsp::lsp_types::Position; use tower_lsp::lsp_types::Range; @@ -217,7 +216,7 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String { /// Rewrites imports in quick fixes and code changes to be Deno specific. pub struct TsResponseImportMapper<'a> { documents: &'a Documents, - maybe_import_map: Option<Arc<ImportMap>>, + maybe_import_map: Option<&'a ImportMap>, node_resolver: Option<&'a CliNodeResolver>, npm_resolver: Option<&'a dyn CliNpmResolver>, } @@ -225,7 +224,7 @@ pub struct TsResponseImportMapper<'a> { impl<'a> TsResponseImportMapper<'a> { pub fn new( documents: &'a Documents, - maybe_import_map: Option<Arc<ImportMap>>, + maybe_import_map: Option<&'a ImportMap>, node_resolver: Option<&'a CliNodeResolver>, npm_resolver: Option<&'a dyn CliNpmResolver>, ) -> Self { @@ -270,7 +269,7 @@ impl<'a> TsResponseImportMapper<'a> { let sub_path = (export != ".").then_some(export); let mut req = None; req = req.or_else(|| { - let import_map = self.maybe_import_map.as_ref()?; + let import_map = self.maybe_import_map?; for entry in import_map.entries_for_referrer(referrer) { let Some(value) = entry.raw_value else { continue; @@ -297,7 +296,7 @@ impl<'a> TsResponseImportMapper<'a> { JsrPackageNvReference::new(nv_ref).to_string() }; let specifier = ModuleSpecifier::parse(&spec_str).ok()?; - if let Some(import_map) = &self.maybe_import_map { + if let Some(import_map) = self.maybe_import_map { if let Some(result) = import_map.lookup(&specifier, referrer) { return Some(result); } @@ -316,7 +315,7 @@ impl<'a> TsResponseImportMapper<'a> { // check if any pkg reqs match what is found in an import map if !pkg_reqs.is_empty() { let sub_path = self.resolve_package_path(specifier); - if let Some(import_map) = &self.maybe_import_map { + if let Some(import_map) = self.maybe_import_map { let pkg_reqs = pkg_reqs.iter().collect::<HashSet<_>>(); let mut matches = Vec::new(); for entry in import_map.entries_for_referrer(referrer) { @@ -358,7 +357,7 @@ impl<'a> TsResponseImportMapper<'a> { } // check if the import map has this specifier - if let Some(import_map) = &self.maybe_import_map { + if let Some(import_map) = self.maybe_import_map { if let Some(result) = import_map.lookup(specifier, referrer) { return Some(result); } diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 2186949fb..164b3b8c3 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -32,7 +32,6 @@ use deno_semver::package::PackageNv; use import_map::ImportMap; use once_cell::sync::Lazy; use regex::Regex; -use std::sync::Arc; use tower_lsp::lsp_types as lsp; static FILE_PROTO_RE: Lazy<Regex> = @@ -155,7 +154,7 @@ pub async fn get_import_completions( jsr_search_api: &CliJsrSearchApi, npm_search_api: &CliNpmSearchApi, documents: &Documents, - maybe_import_map: Option<Arc<ImportMap>>, + maybe_import_map: Option<&ImportMap>, ) -> Option<lsp::CompletionResponse> { let document = documents.get(specifier)?; let (text, _, range) = document.get_maybe_dependency(position)?; @@ -164,7 +163,7 @@ pub async fn get_import_completions( specifier, &text, &range, - maybe_import_map.clone(), + maybe_import_map, documents, ) { // completions for import map specifiers @@ -238,7 +237,7 @@ pub async fn get_import_completions( .collect(); let mut is_incomplete = false; if let Some(import_map) = maybe_import_map { - items.extend(get_base_import_map_completions(import_map.as_ref())); + items.extend(get_base_import_map_completions(import_map)); } if let Some(origin_items) = module_registries.get_origin_completions(&text, &range) @@ -301,7 +300,7 @@ fn get_import_map_completions( specifier: &ModuleSpecifier, text: &str, range: &lsp::Range, - maybe_import_map: Option<Arc<ImportMap>>, + maybe_import_map: Option<&ImportMap>, documents: &Documents, ) -> Option<lsp::CompletionList> { if !text.is_empty() { @@ -809,6 +808,7 @@ mod tests { use deno_graph::Range; use std::collections::HashMap; use std::path::Path; + use std::sync::Arc; use test_util::TempDir; fn mock_documents( diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index e80851429..1a707c44c 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -731,7 +731,7 @@ pub struct ConfigSnapshot { pub client_capabilities: ClientCapabilities, pub settings: Settings, pub workspace_folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>, - pub tree: Arc<ConfigTree>, + pub tree: ConfigTree, } impl ConfigSnapshot { @@ -745,7 +745,7 @@ impl ConfigSnapshot { /// Determine if the provided specifier is enabled or not. pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { let config_file = self.tree.config_file_for_specifier(specifier); - if let Some(cf) = &config_file { + if let Some(cf) = config_file { if let Ok(files) = cf.to_files_config() { if !files.matches_specifier(specifier) { return false; @@ -781,10 +781,6 @@ pub struct Settings { } impl Settings { - pub fn first_root_uri(&self) -> Option<&ModuleSpecifier> { - self.first_folder.as_ref() - } - /// Returns `None` if the value should be deferred to the presence of a /// `deno.json` file. pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option<bool> { @@ -793,7 +789,7 @@ impl Settings { return Some(true); }; let (settings, mut folder_uri) = self.get_for_specifier(specifier); - folder_uri = folder_uri.or_else(|| self.first_root_uri()); + folder_uri = folder_uri.or(self.first_folder.as_ref()); let mut disable_paths = vec![]; let mut enable_paths = None; if let Some(folder_uri) = folder_uri { @@ -879,7 +875,7 @@ pub struct Config { pub client_capabilities: ClientCapabilities, pub settings: Settings, pub workspace_folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>, - pub tree: Arc<ConfigTree>, + pub tree: ConfigTree, } impl Config { @@ -997,7 +993,7 @@ impl Config { pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { let config_file = self.tree.config_file_for_specifier(specifier); - if let Some(cf) = &config_file { + if let Some(cf) = config_file { if let Ok(files) = cf.to_files_config() { if !files.matches_specifier(specifier) { return false; @@ -1086,23 +1082,51 @@ impl Config { } } -pub fn default_ts_config() -> TsConfig { - TsConfig::new(json!({ - "allowJs": true, - "esModuleInterop": true, - "experimentalDecorators": false, - "isolatedModules": true, - "jsx": "react", - "lib": ["deno.ns", "deno.window", "deno.unstable"], - "module": "esnext", - "moduleDetection": "force", - "noEmit": true, - "resolveJsonModule": true, - "strict": true, - "target": "esnext", - "useDefineForClassFields": true, - "useUnknownInCatchVariables": false, - })) +#[derive(Debug, Serialize)] +pub struct LspTsConfig { + #[serde(flatten)] + inner: TsConfig, +} + +impl Default for LspTsConfig { + fn default() -> Self { + Self { + inner: TsConfig::new(json!({ + "allowJs": true, + "esModuleInterop": true, + "experimentalDecorators": false, + "isolatedModules": true, + "jsx": "react", + "lib": ["deno.ns", "deno.window", "deno.unstable"], + "module": "esnext", + "moduleDetection": "force", + "noEmit": true, + "resolveJsonModule": true, + "strict": true, + "target": "esnext", + "useDefineForClassFields": true, + "useUnknownInCatchVariables": false, + })), + } + } +} + +impl LspTsConfig { + pub fn new(config_file: Option<&ConfigFile>) -> Self { + let mut ts_config = Self::default(); + if let Some(config_file) = config_file { + match config_file.to_compiler_options() { + Ok((value, maybe_ignored_options)) => { + ts_config.inner.merge(&value); + if let Some(ignored_options) = maybe_ignored_options { + lsp_warn!("{}", ignored_options); + } + } + Err(err) => lsp_warn!("{}", err), + } + } + ts_config + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1120,7 +1144,7 @@ pub struct ConfigData { pub fmt_options: Arc<FmtOptions>, pub lint_options: Arc<LintOptions>, pub lint_rules: Arc<ConfiguredRules>, - pub ts_config: Arc<TsConfig>, + pub ts_config: Arc<LspTsConfig>, pub node_modules_dir: Option<PathBuf>, pub vendor_dir: Option<PathBuf>, pub lockfile: Option<Arc<Mutex<Lockfile>>>, @@ -1242,18 +1266,7 @@ impl ConfigData { .unwrap_or_default(); let lint_rules = get_configured_rules(lint_options.rules.clone(), config_file.as_ref()); - let mut ts_config = default_ts_config(); - if let Some(config_file) = &config_file { - match config_file.to_compiler_options() { - Ok((value, maybe_ignored_options)) => { - ts_config.merge(&value); - if let Some(ignored_options) = maybe_ignored_options { - lsp_warn!("{}", ignored_options); - } - } - Err(err) => lsp_warn!("{}", err), - } - } + let ts_config = LspTsConfig::new(config_file.as_ref()); let node_modules_dir = config_file.as_ref().and_then(resolve_node_modules_dir); let vendor_dir = config_file.as_ref().and_then(|c| c.vendor_dir_path()); @@ -1425,206 +1438,191 @@ impl ConfigData { } } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct ConfigTree { - root: Mutex<Option<(ModuleSpecifier, Arc<ConfigData>)>>, + first_folder: Option<ModuleSpecifier>, + scopes: Arc<BTreeMap<ModuleSpecifier, ConfigData>>, } impl ConfigTree { - pub fn root_data(&self) -> Option<Arc<ConfigData>> { - self.root.lock().as_ref().map(|(_, d)| d.clone()) + pub fn root_data(&self) -> Option<&ConfigData> { + self.first_folder.as_ref().and_then(|s| self.scopes.get(s)) } - pub fn root_config_file(&self) -> Option<Arc<ConfigFile>> { + pub fn root_ts_config(&self) -> Arc<LspTsConfig> { self - .root - .lock() - .as_ref() - .and_then(|(_, d)| d.config_file.clone()) + .root_data() + .map(|d| d.ts_config.clone()) + .unwrap_or_default() } - pub fn root_ts_config(&self) -> Arc<TsConfig> { - self - .root - .lock() - .as_ref() - .map(|(_, d)| d.ts_config.clone()) - .unwrap_or_else(|| Arc::new(default_ts_config())) + pub fn root_vendor_dir(&self) -> Option<&PathBuf> { + self.root_data().and_then(|d| d.vendor_dir.as_ref()) } - pub fn root_vendor_dir(&self) -> Option<PathBuf> { - self - .root - .lock() - .as_ref() - .and_then(|(_, d)| d.vendor_dir.clone()) + pub fn root_lockfile(&self) -> Option<&Arc<Mutex<Lockfile>>> { + self.root_data().and_then(|d| d.lockfile.as_ref()) } - pub fn root_lockfile(&self) -> Option<Arc<Mutex<Lockfile>>> { - self - .root - .lock() - .as_ref() - .and_then(|(_, d)| d.lockfile.clone()) + pub fn root_import_map(&self) -> Option<&Arc<ImportMap>> { + self.root_data().and_then(|d| d.import_map.as_ref()) } pub fn scope_for_specifier( &self, - _specifier: &ModuleSpecifier, - ) -> Option<ModuleSpecifier> { - self.root.lock().as_ref().map(|r| r.0.clone()) + specifier: &ModuleSpecifier, + ) -> Option<&ModuleSpecifier> { + self + .scopes + .keys() + .rfind(|s| specifier.as_str().starts_with(s.as_str())) + .or(self.first_folder.as_ref()) } pub fn data_for_specifier( &self, - _specifier: &ModuleSpecifier, - ) -> Option<Arc<ConfigData>> { - self.root_data() + specifier: &ModuleSpecifier, + ) -> Option<&ConfigData> { + self + .scope_for_specifier(specifier) + .and_then(|s| self.scopes.get(s)) } - pub fn data_by_scope(&self) -> BTreeMap<ModuleSpecifier, Arc<ConfigData>> { - self.root.lock().iter().cloned().collect() + pub fn data_by_scope(&self) -> &Arc<BTreeMap<ModuleSpecifier, ConfigData>> { + &self.scopes } pub fn config_file_for_specifier( &self, - _specifier: &ModuleSpecifier, - ) -> Option<Arc<ConfigFile>> { - self.root_config_file() - } - - pub fn has_config_file_for_specifier( - &self, - _specifier: &ModuleSpecifier, - ) -> bool { + specifier: &ModuleSpecifier, + ) -> Option<&Arc<ConfigFile>> { self - .root - .lock() - .as_ref() - .map(|(_, d)| d.config_file.is_some()) - .unwrap_or(false) + .data_for_specifier(specifier) + .and_then(|d| d.config_file.as_ref()) } - pub fn config_files(&self) -> Vec<Arc<ConfigFile>> { - self.root_config_file().into_iter().collect() + pub fn config_files(&self) -> Vec<&Arc<ConfigFile>> { + self + .scopes + .iter() + .filter_map(|(_, d)| d.config_file.as_ref()) + .collect() } - pub fn package_jsons(&self) -> Vec<Arc<PackageJson>> { + pub fn package_jsons(&self) -> Vec<&Arc<PackageJson>> { self - .root - .lock() - .as_ref() - .and_then(|(_, d)| d.package_json.clone()) - .into_iter() + .scopes + .iter() + .filter_map(|(_, d)| d.package_json.as_ref()) .collect() } pub fn fmt_options_for_specifier( &self, - _specifier: &ModuleSpecifier, + specifier: &ModuleSpecifier, ) -> Arc<FmtOptions> { self - .root - .lock() - .as_ref() - .map(|(_, d)| d.fmt_options.clone()) + .data_for_specifier(specifier) + .map(|d| d.fmt_options.clone()) .unwrap_or_default() } - pub fn lockfile_for_specifier( + /// Returns (scope_uri, type). + pub fn watched_file_type( &self, - _specifier: &ModuleSpecifier, - ) -> Option<Arc<Mutex<Lockfile>>> { - self.root_lockfile() + specifier: &ModuleSpecifier, + ) -> Option<(&ModuleSpecifier, ConfigWatchedFileType)> { + for (scope_uri, data) in self.scopes.iter() { + if let Some(typ) = data.watched_files.get(specifier) { + return Some((scope_uri, *typ)); + } + } + None } - pub fn import_map_for_specifier( - &self, - _specifier: &ModuleSpecifier, - ) -> Option<Arc<ImportMap>> { + pub fn is_watched_file(&self, specifier: &ModuleSpecifier) -> bool { + if specifier.path().ends_with("/deno.json") + || specifier.path().ends_with("/deno.jsonc") + || specifier.path().ends_with("/package.json") + { + return true; + } self - .root - .lock() - .as_ref() - .and_then(|(_, d)| d.import_map.clone()) + .scopes + .values() + .any(|data| data.watched_files.contains_key(specifier)) } pub async fn refresh( - &self, + &mut self, settings: &Settings, - root_uri: &ModuleSpecifier, workspace_files: &BTreeSet<ModuleSpecifier>, file_fetcher: &FileFetcher, ) { lsp_log!("Refreshing configuration tree..."); - let mut root = None; - if let Some(config_path) = &settings.unscoped.config { - if let Ok(config_uri) = root_uri.join(config_path) { - root = Some(( - root_uri.clone(), - Arc::new( - ConfigData::load( - Some(&config_uri), - root_uri, + let mut scopes = BTreeMap::new(); + for (folder_uri, ws_settings) in &settings.by_workspace_folder { + let mut ws_settings = ws_settings.as_ref(); + if Some(folder_uri) == settings.first_folder.as_ref() { + ws_settings = ws_settings.or(Some(&settings.unscoped)); + } + if let Some(ws_settings) = ws_settings { + if let Some(config_path) = &ws_settings.config { + if let Ok(config_uri) = folder_uri.join(config_path) { + scopes.insert( + folder_uri.clone(), + ConfigData::load( + Some(&config_uri), + folder_uri, + settings, + Some(file_fetcher), + ) + .await, + ); + } + } + } + } + + for specifier in workspace_files { + if specifier.path().ends_with("/deno.json") + || specifier.path().ends_with("/deno.jsonc") + { + if let Ok(scope) = specifier.join(".") { + let entry = scopes.entry(scope.clone()); + #[allow(clippy::map_entry)] + if matches!(entry, std::collections::btree_map::Entry::Vacant(_)) { + let data = ConfigData::load( + Some(specifier), + &scope, settings, Some(file_fetcher), ) - .await, - ), - )); + .await; + entry.or_insert(data); + } + } } - } else { - let get_uri_if_exists = |name| { - let uri = root_uri.join(name).ok(); - uri.filter(|s| workspace_files.contains(s)) - }; - let config_uri = get_uri_if_exists("deno.jsonc") - .or_else(|| get_uri_if_exists("deno.json")); - root = Some(( - root_uri.clone(), - Arc::new( - ConfigData::load( - config_uri.as_ref(), - root_uri, - settings, - Some(file_fetcher), - ) - .await, - ), - )); } - *self.root.lock() = root; - } - /// Returns (scope_uri, type). - pub fn watched_file_type( - &self, - specifier: &ModuleSpecifier, - ) -> Option<(ModuleSpecifier, ConfigWatchedFileType)> { - if let Some((scope_uri, data)) = &*self.root.lock() { - if let Some(typ) = data.watched_files.get(specifier) { - return Some((scope_uri.clone(), *typ)); + for folder_uri in settings.by_workspace_folder.keys() { + if !scopes + .keys() + .any(|s| folder_uri.as_str().starts_with(s.as_str())) + { + scopes.insert( + folder_uri.clone(), + ConfigData::load(None, folder_uri, settings, Some(file_fetcher)) + .await, + ); } } - None - } - - pub fn is_watched_file(&self, specifier: &ModuleSpecifier) -> bool { - if specifier.path().ends_with("/deno.json") - || specifier.path().ends_with("/deno.jsonc") - || specifier.path().ends_with("/package.json") - { - return true; - } - self - .root - .lock() - .as_ref() - .is_some_and(|(_, d)| d.watched_files.contains_key(specifier)) + self.first_folder = settings.first_folder.clone(); + self.scopes = Arc::new(scopes); } #[cfg(test)] - pub async fn inject_config_file(&self, config_file: ConfigFile) { + pub async fn inject_config_file(&mut self, config_file: ConfigFile) { let scope = config_file.specifier.join(".").unwrap(); let data = ConfigData::load_inner( Some(config_file), @@ -1633,7 +1631,8 @@ impl ConfigTree { None, ) .await; - *self.root.lock() = Some((scope, Arc::new(data))); + self.first_folder = Some(scope.clone()); + self.scopes = Arc::new([(scope, data)].into_iter().collect()); } } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index e518f4c6e..ac1f522d7 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -811,7 +811,7 @@ fn generate_lint_diagnostics( let (lint_options, lint_rules) = config .tree .scope_for_specifier(document.specifier()) - .and_then(|s| config_data_by_scope.get(&s)) + .and_then(|s| config_data_by_scope.get(s)) .map(|d| (d.lint_options.clone(), d.lint_rules.clone())) .unwrap_or_default(); diagnostics_vec.push(DiagnosticRecord { @@ -1452,8 +1452,8 @@ fn diagnose_dependency( } } - let import_map = snapshot.config.tree.import_map_for_specifier(referrer); - if let Some(import_map) = &import_map { + let import_map = snapshot.config.tree.root_import_map(); + if let Some(import_map) = import_map { if let Resolution::Ok(resolved) = &dependency.maybe_code { if let Some(to) = import_map.lookup(&resolved.specifier, referrer) { if dependency_key != to { @@ -1502,7 +1502,7 @@ fn diagnose_dependency( }, dependency.is_dynamic, dependency.maybe_attribute_type.as_deref(), - import_map.as_deref(), + import_map.map(|i| i.as_ref()), ) .iter() .flat_map(|diag| { @@ -1525,7 +1525,7 @@ fn diagnose_dependency( &dependency.maybe_type, dependency.is_dynamic, dependency.maybe_attribute_type.as_deref(), - import_map.as_deref(), + import_map.map(|i| i.as_ref()), ) .iter() .map(|diag| diag.to_lsp_diagnostic(&range)), @@ -1614,7 +1614,7 @@ mod tests { (*source).into(), ); } - let config = Config::new_with_roots([resolve_url("file:///").unwrap()]); + let mut config = Config::new_with_roots([resolve_url("file:///").unwrap()]); if let Some((base_url, json_string)) = maybe_import_map { let base_url = resolve_url(base_url).unwrap(); let config_file = ConfigFile::new( @@ -1689,8 +1689,7 @@ let c: number = "a"; let snapshot = Arc::new(snapshot); let cache = Arc::new(GlobalHttpCache::new(cache_location, RealDenoCacheEnv)); - let ts_server = - TsServer::new(Default::default(), cache, Default::default()); + let ts_server = TsServer::new(Default::default(), cache); ts_server.start(None); // test enabled diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index d207330ad..0e9b6c3c3 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -1309,14 +1309,12 @@ impl Documents { workspace_files: &BTreeSet<ModuleSpecifier>, ) { let config_data = config.tree.root_data(); - let config_file = - config_data.as_ref().and_then(|d| d.config_file.as_deref()); + let config_file = config_data.and_then(|d| d.config_file.as_deref()); self.resolver = Arc::new(CliGraphResolver::new(CliGraphResolverOptions { node_resolver, npm_resolver, package_json_deps_provider: Arc::new(PackageJsonDepsProvider::new( config_data - .as_ref() .and_then(|d| d.package_json.as_ref()) .map(|package_json| { package_json::get_local_package_json_version_reqs(package_json) @@ -1324,10 +1322,8 @@ impl Documents { )), maybe_jsx_import_source_config: config_file .and_then(|cf| cf.to_maybe_jsx_import_source_config().ok().flatten()), - maybe_import_map: config_data.as_ref().and_then(|d| d.import_map.clone()), - maybe_vendor_dir: config_data - .as_ref() - .and_then(|d| d.vendor_dir.as_ref()), + maybe_import_map: config_data.and_then(|d| d.import_map.clone()), + maybe_vendor_dir: config_data.and_then(|d| d.vendor_dir.as_ref()), bare_node_builtins_enabled: config_file .map(|config| config.has_unstable("bare-node-builtins")) .unwrap_or(false), @@ -1338,7 +1334,7 @@ impl Documents { })); self.jsr_resolver = Arc::new(JsrCacheResolver::new( self.cache.clone(), - config.tree.root_lockfile(), + config.tree.root_lockfile().cloned(), )); self.redirect_resolver = Arc::new(RedirectResolver::new(self.cache.clone())); diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index a5923a84a..8348bd95f 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -334,11 +334,7 @@ impl LanguageServer { // do as much as possible in a read, then do a write outside let maybe_prepare_cache_result = { let inner = self.0.read().await; // ensure dropped - match inner.prepare_cache( - specifiers, - referrer.clone(), - force_global_cache, - ) { + match inner.prepare_cache(specifiers, referrer, force_global_cache) { Ok(maybe_cache_result) => maybe_cache_result, Err(err) => { lsp_warn!("Error preparing caching: {:#}", err); @@ -370,7 +366,7 @@ impl LanguageServer { } { let mut inner = self.0.write().await; - let lockfile = inner.config.tree.lockfile_for_specifier(&referrer); + let lockfile = inner.config.tree.root_lockfile().cloned(); inner.documents.refresh_jsr_resolver(lockfile); inner.refresh_npm_specifiers().await; } @@ -516,11 +512,8 @@ impl Inner { let cache_metadata = cache::CacheMetadata::new(deps_http_cache.clone()); let performance = Arc::new(Performance::default()); let config = Config::default(); - let ts_server = Arc::new(TsServer::new( - performance.clone(), - deps_http_cache.clone(), - config.tree.clone(), - )); + let ts_server = + Arc::new(TsServer::new(performance.clone(), deps_http_cache.clone())); let diagnostics_state = Arc::new(DiagnosticsState::default()); let diagnostics_server = DiagnosticsServer::new( client.clone(), @@ -765,7 +758,10 @@ impl Inner { )); let maybe_local_cache = self.config.tree.root_vendor_dir().map(|local_path| { - Arc::new(LocalLspHttpCache::new(local_path, global_cache.clone())) + Arc::new(LocalLspHttpCache::new( + local_path.clone(), + global_cache.clone(), + )) }); let cache: Arc<dyn HttpCache> = maybe_local_cache .clone() @@ -1154,42 +1150,33 @@ impl Inner { async fn refresh_config_tree(&mut self) { let file_fetcher = self.create_file_fetcher(CacheSetting::RespectHeaders); - if let Some(root_uri) = self.config.root_uri() { - self - .config - .tree - .refresh( - &self.config.settings, - root_uri, - &self.workspace_files, - &file_fetcher, - ) - .await; - for config_file in self.config.tree.config_files() { - if let Ok((compiler_options, _)) = config_file.to_compiler_options() { - if let Some(compiler_options_obj) = compiler_options.as_object() { - if let Some(jsx_import_source) = - compiler_options_obj.get("jsxImportSource") - { - if let Some(jsx_import_source) = jsx_import_source.as_str() { - let specifiers = vec![Url::parse(&format!( - "data:application/typescript;base64,{}", - base64::engine::general_purpose::STANDARD.encode(format!( - "import '{jsx_import_source}/jsx-runtime';" - )) - )) - .unwrap()]; - let referrer = config_file.specifier.clone(); - self.task_queue.queue_task(Box::new(|ls: LanguageServer| { - spawn(async move { - if let Err(err) = - ls.cache(specifiers, referrer, false).await - { - lsp_warn!("{:#}", err); - } - }); - })); - } + self + .config + .tree + .refresh(&self.config.settings, &self.workspace_files, &file_fetcher) + .await; + for config_file in self.config.tree.config_files() { + if let Ok((compiler_options, _)) = config_file.to_compiler_options() { + if let Some(compiler_options_obj) = compiler_options.as_object() { + if let Some(jsx_import_source) = + compiler_options_obj.get("jsxImportSource") + { + if let Some(jsx_import_source) = jsx_import_source.as_str() { + let specifiers = vec![Url::parse(&format!( + "data:application/typescript;base64,{}", + base64::engine::general_purpose::STANDARD + .encode(format!("import '{jsx_import_source}/jsx-runtime';")) + )) + .unwrap()]; + let referrer = config_file.specifier.clone(); + self.task_queue.queue_task(Box::new(|ls: LanguageServer| { + spawn(async move { + if let Err(err) = ls.cache(specifiers, referrer, false).await + { + lsp_warn!("{:#}", err); + } + }); + })); } } } @@ -1383,7 +1370,7 @@ impl Inner { _ => return None, }; Some(lsp_custom::DenoConfigurationChangeEvent { - scope_uri: t.0, + scope_uri: t.0.clone(), file_uri: e.uri.clone(), typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type( e.typ, @@ -1407,7 +1394,7 @@ impl Inner { _ => return None, }; Some(lsp_custom::DenoConfigurationChangeEvent { - scope_uri: t.0, + scope_uri: t.0.clone(), file_uri: e.uri.clone(), typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type( e.typ, @@ -2010,11 +1997,11 @@ impl Inner { pub fn get_ts_response_import_mapper( &self, - referrer: &ModuleSpecifier, + _referrer: &ModuleSpecifier, ) -> TsResponseImportMapper { TsResponseImportMapper::new( &self.documents, - self.config.tree.import_map_for_specifier(referrer), + self.config.tree.root_import_map().map(|i| i.as_ref()), self.npm.node_resolver.as_deref(), self.npm.resolver.as_deref(), ) @@ -2327,7 +2314,7 @@ impl Inner { &self.jsr_search_api, &self.npm.search_api, &self.documents, - self.config.tree.import_map_for_specifier(&specifier), + self.config.tree.root_import_map().map(|i| i.as_ref()), ) .await; } @@ -3112,7 +3099,7 @@ impl tower_lsp::LanguageServer for LanguageServer { } let mut config_events = vec![]; - for (scope_uri, config_data) in ls.config.tree.data_by_scope() { + for (scope_uri, config_data) in ls.config.tree.data_by_scope().iter() { if let Some(config_file) = &config_data.config_file { config_events.push(lsp_custom::DenoConfigurationChangeEvent { scope_uri: scope_uri.clone(), @@ -3493,7 +3480,7 @@ impl Inner { let mark = self .performance .mark_with_args("lsp.cache", (&specifiers, &referrer)); - let config_data = self.config.tree.data_for_specifier(&referrer); + let config_data = self.config.tree.root_data(); let roots = if !specifiers.is_empty() { specifiers } else { @@ -3508,7 +3495,7 @@ impl Inner { unsafely_ignore_certificate_errors: workspace_settings .unsafely_ignore_certificate_errors .clone(), - import_map_path: config_data.as_ref().and_then(|d| { + import_map_path: config_data.and_then(|d| { if d.import_map_from_settings { return Some(d.import_map.as_ref()?.base_url().to_string()); } @@ -3516,7 +3503,6 @@ impl Inner { }), node_modules_dir: Some( config_data - .as_ref() .and_then(|d| d.node_modules_dir.as_ref()) .is_some(), ), @@ -3525,13 +3511,9 @@ impl Inner { ..Default::default() }, self.initial_cwd.clone(), - config_data - .as_ref() - .and_then(|d| d.config_file.as_deref().cloned()), - config_data.as_ref().and_then(|d| d.lockfile.clone()), - config_data - .as_ref() - .and_then(|d| d.package_json.as_deref().cloned()), + config_data.and_then(|d| d.config_file.as_deref().cloned()), + config_data.and_then(|d| d.lockfile.clone()), + config_data.and_then(|d| d.package_json.as_deref().cloned()), force_global_cache, )?; diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 63c08331d..fbc712a56 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -3,7 +3,6 @@ use super::analysis::CodeActionData; use super::code_lens; use super::config; -use super::config::ConfigTree; use super::documents::AssetOrDocument; use super::documents::DocumentsFilter; use super::language_server; @@ -222,7 +221,6 @@ pub struct TsServer { sender: mpsc::UnboundedSender<Request>, receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>, specifier_map: Arc<TscSpecifierMap>, - config_tree: Arc<ConfigTree>, inspector_server: Mutex<Option<Arc<InspectorServer>>>, } @@ -240,11 +238,7 @@ impl std::fmt::Debug for TsServer { } impl TsServer { - pub fn new( - performance: Arc<Performance>, - cache: Arc<dyn HttpCache>, - config_tree: Arc<ConfigTree>, - ) -> Self { + pub fn new(performance: Arc<Performance>, cache: Arc<dyn HttpCache>) -> Self { let (tx, request_rx) = mpsc::unbounded_channel::<Request>(); Self { performance, @@ -252,7 +246,6 @@ impl TsServer { sender: tx, receiver: Mutex::new(Some(request_rx)), specifier_map: Arc::new(TscSpecifierMap::new()), - config_tree, inspector_server: Mutex::new(None), } } @@ -275,7 +268,6 @@ impl TsServer { let performance = self.performance.clone(); let cache = self.cache.clone(); let specifier_map = self.specifier_map.clone(); - let config_tree = self.config_tree.clone(); let _join_handle = thread::spawn(move || { run_tsc_thread( receiver, @@ -283,7 +275,6 @@ impl TsServer { cache.clone(), specifier_map.clone(), maybe_inspector_server, - config_tree, ) }); } @@ -3884,7 +3875,6 @@ struct State { response: Option<Response>, state_snapshot: Arc<StateSnapshot>, specifier_map: Arc<TscSpecifierMap>, - config_tree: Arc<ConfigTree>, token: CancellationToken, } @@ -3892,7 +3882,6 @@ impl State { fn new( state_snapshot: Arc<StateSnapshot>, specifier_map: Arc<TscSpecifierMap>, - config_tree: Arc<ConfigTree>, performance: Arc<Performance>, ) -> Self { Self { @@ -3901,7 +3890,6 @@ impl State { response: None, state_snapshot, specifier_map, - config_tree, token: Default::default(), } } @@ -4120,7 +4108,7 @@ fn op_script_version( fn op_ts_config(state: &mut OpState) -> serde_json::Value { let state = state.borrow_mut::<State>(); let mark = state.performance.mark("tsc.op.op_ts_config"); - let r = json!(state.config_tree.root_ts_config()); + let r = json!(state.state_snapshot.config.tree.root_ts_config()); state.performance.measure(mark); r } @@ -4141,19 +4129,13 @@ fn run_tsc_thread( cache: Arc<dyn HttpCache>, specifier_map: Arc<TscSpecifierMap>, maybe_inspector_server: Option<Arc<InspectorServer>>, - config_tree: Arc<ConfigTree>, ) { let has_inspector_server = maybe_inspector_server.is_some(); // Create and setup a JsRuntime based on a snapshot. It is expected that the // supplied snapshot is an isolate that contains the TypeScript language // server. let mut tsc_runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![deno_tsc::init_ops( - performance, - cache, - specifier_map, - config_tree, - )], + extensions: vec![deno_tsc::init_ops(performance, cache, specifier_map)], startup_snapshot: Some(tsc::compiler_snapshot()), inspector: maybe_inspector_server.is_some(), ..Default::default() @@ -4227,7 +4209,6 @@ deno_core::extension!(deno_tsc, performance: Arc<Performance>, cache: Arc<dyn HttpCache>, specifier_map: Arc<TscSpecifierMap>, - config_tree: Arc<ConfigTree>, }, state = |state, options| { state.put(State::new( @@ -4239,7 +4220,6 @@ deno_core::extension!(deno_tsc, npm: None, }), options.specifier_map, - options.config_tree, options.performance, )); }, @@ -4507,7 +4487,10 @@ impl UserPreferences { language_settings.preferences.use_aliases_for_renames, ), // Only use workspace settings for quote style if there's no `deno.json`. - quote_preference: if config.tree.has_config_file_for_specifier(specifier) + quote_preference: if config + .tree + .config_file_for_specifier(specifier) + .is_some() { base_preferences.quote_preference } else { @@ -4650,12 +4633,14 @@ fn request( #[cfg(test)] mod tests { + use super::*; use crate::cache::GlobalHttpCache; use crate::cache::HttpCache; use crate::cache::RealDenoCacheEnv; use crate::http_util::HeadersMap; use crate::lsp::cache::CacheMetadata; + use crate::lsp::config::ConfigSnapshot; use crate::lsp::config::WorkspaceSettings; use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; @@ -4664,9 +4649,10 @@ mod tests { use std::path::Path; use test_util::TempDir; - fn mock_state_snapshot( + async fn mock_state_snapshot( fixtures: &[(&str, &str, i32, LanguageId)], location: &Path, + ts_config: Value, ) -> StateSnapshot { let cache = Arc::new(GlobalHttpCache::new( location.to_path_buf(), @@ -4683,11 +4669,26 @@ mod tests { (*source).into(), ); } + let mut config = ConfigSnapshot::default(); + config + .tree + .inject_config_file( + deno_config::ConfigFile::new( + &json!({ + "compilerOptions": ts_config, + }) + .to_string(), + resolve_url("file:///deno.json").unwrap(), + &deno_config::ParseOptions::default(), + ) + .unwrap(), + ) + .await; StateSnapshot { documents, assets: Default::default(), cache_metadata: CacheMetadata::new(cache), - config: Default::default(), + config: Arc::new(config), npm: None, } } @@ -4700,23 +4701,10 @@ mod tests { let location = temp_dir.path().join("deps").to_path_buf(); let cache = Arc::new(GlobalHttpCache::new(location.clone(), RealDenoCacheEnv)); - let snapshot = Arc::new(mock_state_snapshot(sources, &location)); + let snapshot = + Arc::new(mock_state_snapshot(sources, &location, config).await); let performance = Arc::new(Performance::default()); - let config_tree = Arc::new(ConfigTree::default()); - config_tree - .inject_config_file( - deno_config::ConfigFile::new( - &json!({ - "compilerOptions": config, - }) - .to_string(), - resolve_url("file:///deno.json").unwrap(), - &deno_config::ParseOptions::default(), - ) - .unwrap(), - ) - .await; - let ts_server = TsServer::new(performance, cache.clone(), config_tree); + let ts_server = TsServer::new(performance, cache.clone()); ts_server.start(None); (ts_server, snapshot, cache) } diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 91608d53c..9348d625c 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -1061,6 +1061,7 @@ fn lsp_did_change_deno_configuration_notification() { }], })) ); + client.shutdown(); } #[test] @@ -1098,6 +1099,7 @@ fn lsp_deno_task() { } ]) ); + client.shutdown(); } #[test] @@ -1110,6 +1112,7 @@ fn lsp_reload_import_registries_command() { json!({ "command": "deno.reloadImportRegistries" }), ); assert_eq!(res, json!(true)); + client.shutdown(); } #[test] @@ -1598,6 +1601,7 @@ fn lsp_inlay_hints() { } ]) ); + client.shutdown(); } #[test] @@ -1645,6 +1649,7 @@ fn lsp_inlay_hints_not_enabled() { }), ); assert_eq!(res, json!(null)); + client.shutdown(); } #[test] @@ -2409,6 +2414,7 @@ fn lsp_hover_dependency() { } }) ); + client.shutdown(); } // This tests for a regression covered by denoland/deno#12753 where the lsp was @@ -4822,6 +4828,7 @@ fn test_lsp_code_actions_ordering() { }, ]) ); + client.shutdown(); } #[test] @@ -4851,6 +4858,7 @@ fn lsp_status_file() { ); let res = res.as_str().unwrap().to_string(); assert!(res.starts_with("# Deno Language Server Status")); + client.shutdown(); } #[test] @@ -5598,6 +5606,7 @@ fn lsp_cache_then_definition() { }, }]), ); + client.shutdown(); } #[test] @@ -6355,6 +6364,7 @@ fn lsp_quote_style_from_workspace_settings() { }, }]), ); + client.shutdown(); } #[test] @@ -6812,6 +6822,7 @@ fn lsp_completions_auto_import() { ] }) ); + client.shutdown(); } #[test] @@ -7063,6 +7074,7 @@ fn lsp_npm_completions_auto_import_and_quick_fix_no_import_map() { } }]) ); + client.shutdown(); } #[test] @@ -7102,6 +7114,7 @@ fn lsp_semantic_tokens_for_disabled_module() { "data": [0, 6, 9, 7, 9, 0, 15, 9, 7, 8], }) ); + client.shutdown(); } #[test] @@ -7538,6 +7551,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() { ] }) ); + client.shutdown(); } #[test] @@ -7633,6 +7647,7 @@ fn lsp_completions_snippet() { "insertTextFormat": 2 }) ); + client.shutdown(); } #[test] @@ -7688,6 +7703,7 @@ fn lsp_completions_no_snippet() { ] }) ); + client.shutdown(); } #[test] @@ -7919,6 +7935,7 @@ fn lsp_npm_specifier_unopened_file() { assert!(!list.is_incomplete); assert_eq!(list.items.len(), 63); assert!(list.items.iter().any(|i| i.label == "ansi256")); + client.shutdown(); } #[test] @@ -11432,6 +11449,193 @@ fn lsp_vendor_dir() { client.shutdown(); } +#[test] +fn lsp_deno_json_scopes_fmt_config() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.write( + "project1/deno.json", + json!({ + "fmt": { + "semiColons": false, + }, + }) + .to_string(), + ); + temp_dir.create_dir_all("project2"); + temp_dir.write( + "project2/deno.json", + json!({ + "fmt": { + "singleQuote": true, + }, + }) + .to_string(), + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(\"\");\n", + }, + })); + let res = client.write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + }, + "options": { + "tabSize": 2, + "insertSpaces": true, + }, + }), + ); + assert_eq!( + res, + json!([{ + "range": { + "start": { "line": 0, "character": 15 }, + "end": { "line": 0, "character": 16 }, + }, + "newText": "", + }]) + ); + client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": "console.log(\"\");\n", + }, + })); + let res = client.write_request( + "textDocument/formatting", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + }, + "options": { + "tabSize": 2, + "insertSpaces": true, + }, + }), + ); + assert_eq!( + res, + json!([{ + "range": { + "start": { "line": 0, "character": 12 }, + "end": { "line": 0, "character": 14 }, + }, + "newText": "''", + }]) + ); + client.shutdown(); +} + +#[test] +fn lsp_deno_json_scopes_lint_config() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.create_dir_all("project1"); + temp_dir.write( + "project1/deno.json", + json!({ + "lint": { + "rules": { + "include": ["camelcase"], + }, + }, + }) + .to_string(), + ); + temp_dir.create_dir_all("project2"); + temp_dir.write( + "project2/deno.json", + json!({ + "lint": { + "rules": { + "include": ["ban-untagged-todo"], + }, + }, + }) + .to_string(), + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": r#" + // TODO: Unused var + const snake_case_var = 1; + console.log(snake_case_var); + "#, + }, + })); + assert_eq!( + json!(diagnostics.messages_with_source("deno-lint")), + json!({ + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + "diagnostics": [{ + "range": { + "start": { "line": 2, "character": 14 }, + "end": { "line": 2, "character": 28 }, + }, + "severity": 2, + "code": "camelcase", + "source": "deno-lint", + "message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`", + }], + "version": 1, + }) + ); + client.write_notification( + "textDocument/didClose", + json!({ + "textDocument": { + "uri": temp_dir.uri().join("project1/file.ts").unwrap(), + }, + }), + ); + let diagnostics = client.did_open(json!({ + "textDocument": { + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "languageId": "typescript", + "version": 1, + "text": r#" + // TODO: Unused var + const snake_case_var = 1; + console.log(snake_case_var); + "#, + }, + })); + assert_eq!( + json!(diagnostics.messages_with_source("deno-lint")), + json!({ + "uri": temp_dir.uri().join("project2/file.ts").unwrap(), + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 8 }, + "end": { "line": 1, "character": 27 }, + }, + "severity": 2, + "code": "ban-untagged-todo", + "source": "deno-lint", + "message": "TODO should be tagged with (@username) or (#issue)\nAdd a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123)", + }], + "version": 1, + }) + ); + client.shutdown(); +} #[test] fn lsp_import_unstable_bare_node_builtins_auto_discovered() { @@ -11995,7 +12199,7 @@ C.test(); })) .all(); - assert_eq!(diagnostics.len(), 0); + assert_eq!(json!(diagnostics), json!([])); client.shutdown(); } diff --git a/tests/util/server/src/builders.rs b/tests/util/server/src/builders.rs index 6a57548a3..8c93ceeb0 100644 --- a/tests/util/server/src/builders.rs +++ b/tests/util/server/src/builders.rs @@ -225,9 +225,6 @@ impl TestContextBuilder { } let deno_exe = deno_exe_path(); - self - .diagnostic_logger - .writeln(format!("deno_exe path {}", deno_exe)); let http_server_guard = if self.use_http_server { Some(Rc::new(http_server())) |