// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use deno_ast::MediaType; use deno_config::deno_json::DenoJsonCache; use deno_config::deno_json::FmtConfig; use deno_config::deno_json::FmtOptionsConfig; use deno_config::deno_json::LintConfig; use deno_config::deno_json::TestConfig; use deno_config::deno_json::TsConfig; use deno_config::fs::DenoConfigFs; use deno_config::fs::RealDenoConfigFs; use deno_config::glob::FilePatterns; use deno_config::glob::PathOrPatternSet; use deno_config::package_json::PackageJsonCache; use deno_config::workspace::CreateResolverOptions; use deno_config::workspace::PackageJsonDepResolution; use deno_config::workspace::SpecifiedImportMap; use deno_config::workspace::VendorEnablement; use deno_config::workspace::Workspace; use deno_config::workspace::WorkspaceCache; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDirectoryEmptyOptions; use deno_config::workspace::WorkspaceDiscoverOptions; use deno_config::workspace::WorkspaceResolver; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::serde::de::DeserializeOwned; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::ModuleSpecifier; use deno_lint::linter::LintConfig as DenoLintConfig; use deno_npm::npm_rc::ResolvedNpmRc; use deno_runtime::deno_node::PackageJson; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::fs_util::specifier_to_file_path; use indexmap::IndexSet; use lsp::Url; use lsp_types::ClientCapabilities; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tower_lsp::lsp_types as lsp; use super::logging::lsp_log; use crate::args::discover_npmrc_from_workspace; use crate::args::has_flag_env_var; use crate::args::CliLockfile; use crate::args::ConfigFile; use crate::args::DENO_FUTURE; use crate::cache::FastInsecureHasher; use crate::file_fetcher::FileFetcher; use crate::lsp::logging::lsp_warn; use crate::tools::lint::get_configured_rules; use crate::tools::lint::ConfiguredRules; use crate::util::fs::canonicalize_path_maybe_not_exists; pub const SETTINGS_SECTION: &str = "deno"; fn is_true() -> bool { true } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CodeLensSettings { /// Flag for providing implementation code lenses. #[serde(default)] pub implementations: bool, /// Flag for providing reference code lenses. #[serde(default)] pub references: bool, /// Flag for providing reference code lens on all functions. For this to have /// an impact, the `references` flag needs to be `true`. #[serde(default)] pub references_all_functions: bool, /// Flag for providing test code lens on `Deno.test` statements. There is /// also the `test_args` setting, but this is not used by the server. #[serde(default = "is_true")] pub test: bool, } impl Default for CodeLensSettings { fn default() -> Self { Self { implementations: false, references: false, references_all_functions: false, test: true, } } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DenoCompletionSettings { #[serde(default)] pub imports: ImportCompletionSettings, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ClassMemberSnippets { #[serde(default = "is_true")] pub enabled: bool, } impl Default for ClassMemberSnippets { fn default() -> Self { Self { enabled: true } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ObjectLiteralMethodSnippets { #[serde(default = "is_true")] pub enabled: bool, } impl Default for ObjectLiteralMethodSnippets { fn default() -> Self { Self { enabled: true } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CompletionSettings { #[serde(default)] pub complete_function_calls: bool, #[serde(default = "is_true")] pub include_automatic_optional_chain_completions: bool, #[serde(default = "is_true")] pub include_completions_for_import_statements: bool, #[serde(default = "is_true")] pub names: bool, #[serde(default = "is_true")] pub paths: bool, #[serde(default = "is_true")] pub auto_imports: bool, #[serde(default = "is_true")] pub enabled: bool, #[serde(default)] pub class_member_snippets: ClassMemberSnippets, #[serde(default)] pub object_literal_method_snippets: ObjectLiteralMethodSnippets, } impl Default for CompletionSettings { fn default() -> Self { Self { complete_function_calls: false, include_automatic_optional_chain_completions: true, include_completions_for_import_statements: true, names: true, paths: true, auto_imports: true, enabled: true, class_member_snippets: Default::default(), object_literal_method_snippets: Default::default(), } } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsSettings { #[serde(default)] pub parameter_names: InlayHintsParamNamesOptions, #[serde(default)] pub parameter_types: InlayHintsParamTypesOptions, #[serde(default)] pub variable_types: InlayHintsVarTypesOptions, #[serde(default)] pub property_declaration_types: InlayHintsPropDeclTypesOptions, #[serde(default)] pub function_like_return_types: InlayHintsFuncLikeReturnTypesOptions, #[serde(default)] pub enum_member_values: InlayHintsEnumMemberValuesOptions, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsParamNamesOptions { #[serde(default)] pub enabled: InlayHintsParamNamesEnabled, #[serde(default = "is_true")] pub suppress_when_argument_matches_name: bool, } impl Default for InlayHintsParamNamesOptions { fn default() -> Self { Self { enabled: InlayHintsParamNamesEnabled::None, suppress_when_argument_matches_name: true, } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum InlayHintsParamNamesEnabled { None, Literals, All, } impl Default for InlayHintsParamNamesEnabled { fn default() -> Self { Self::None } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsParamTypesOptions { #[serde(default)] pub enabled: bool, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsVarTypesOptions { #[serde(default)] pub enabled: bool, #[serde(default = "is_true")] pub suppress_when_type_matches_name: bool, } impl Default for InlayHintsVarTypesOptions { fn default() -> Self { Self { enabled: false, suppress_when_type_matches_name: true, } } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsPropDeclTypesOptions { #[serde(default)] pub enabled: bool, } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsFuncLikeReturnTypesOptions { #[serde(default)] pub enabled: bool, } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct InlayHintsEnumMemberValuesOptions { #[serde(default)] pub enabled: bool, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ImportCompletionSettings { /// A flag that indicates if non-explicitly set origins should be checked for /// supporting import suggestions. #[serde(default = "is_true")] pub auto_discover: bool, /// A map of origins which have had explicitly set if import suggestions are /// enabled. #[serde(default)] pub hosts: HashMap, } impl Default for ImportCompletionSettings { fn default() -> Self { Self { auto_discover: true, hosts: HashMap::default(), } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct TestingSettings { /// A vector of arguments which should be used when running the tests for /// a workspace. #[serde(default)] pub args: Vec, } impl Default for TestingSettings { fn default() -> Self { Self { args: vec!["--allow-all".to_string(), "--no-check".to_string()], } } } fn default_to_true() -> bool { true } fn default_document_preload_limit() -> usize { 1000 } fn empty_string_none<'de, D: serde::Deserializer<'de>>( d: D, ) -> Result, D::Error> { let o: Option = Option::deserialize(d)?; Ok(o.filter(|s| !s.is_empty())) } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum ImportModuleSpecifier { NonRelative, ProjectRelative, Relative, Shortest, } impl Default for ImportModuleSpecifier { fn default() -> Self { Self::Shortest } } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum JsxAttributeCompletionStyle { Auto, Braces, None, } impl Default for JsxAttributeCompletionStyle { fn default() -> Self { Self::Auto } } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum QuoteStyle { Auto, Double, Single, } impl Default for QuoteStyle { fn default() -> Self { Self::Auto } } impl From<&FmtOptionsConfig> for QuoteStyle { fn from(config: &FmtOptionsConfig) -> Self { match config.single_quote { Some(true) => QuoteStyle::Single, _ => QuoteStyle::Double, } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct LanguagePreferences { #[serde(default)] pub import_module_specifier: ImportModuleSpecifier, #[serde(default)] pub jsx_attribute_completion_style: JsxAttributeCompletionStyle, #[serde(default)] pub auto_import_file_exclude_patterns: Vec, #[serde(default = "is_true")] pub use_aliases_for_renames: bool, #[serde(default)] pub quote_style: QuoteStyle, } impl Default for LanguagePreferences { fn default() -> Self { LanguagePreferences { import_module_specifier: Default::default(), jsx_attribute_completion_style: Default::default(), auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: Default::default(), } } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UpdateImportsOnFileMoveOptions { #[serde(default)] pub enabled: UpdateImportsOnFileMoveEnabled, } #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum UpdateImportsOnFileMoveEnabled { Always, Prompt, Never, } impl Default for UpdateImportsOnFileMoveEnabled { fn default() -> Self { Self::Prompt } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct LanguageWorkspaceSettings { #[serde(default)] pub inlay_hints: InlayHintsSettings, #[serde(default)] pub preferences: LanguagePreferences, #[serde(default)] pub suggest: CompletionSettings, #[serde(default)] pub update_imports_on_file_move: UpdateImportsOnFileMoveOptions, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[serde(untagged)] pub enum InspectSetting { Bool(bool), String(String), } impl Default for InspectSetting { fn default() -> Self { InspectSetting::Bool(false) } } impl InspectSetting { pub fn to_address(&self) -> Option { match self { InspectSetting::Bool(false) => None, InspectSetting::Bool(true) => Some("127.0.0.1:9222".to_string()), InspectSetting::String(s) => Some(s.clone()), } } } /// Deno language server specific settings that are applied to a workspace. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WorkspaceSettings { /// A flag that indicates if Deno is enabled for the workspace. pub enable: Option, /// A list of paths, using the root_uri as a base that should be Deno /// disabled. #[serde(default)] pub disable_paths: Vec, /// A list of paths, using the root_uri as a base that should be Deno enabled. pub enable_paths: Option>, /// An option that points to a path string of the path to utilise as the /// cache/DENO_DIR for the language server. #[serde(default, deserialize_with = "empty_string_none")] pub cache: Option, /// Cache local modules and their dependencies on `textDocument/didSave` /// notifications corresponding to them. #[serde(default)] pub cache_on_save: bool, /// Override the default stores used to validate certificates. This overrides /// the environment variable `DENO_TLS_CA_STORE` if present. pub certificate_stores: Option>, /// An option that points to a path string of the config file to apply to /// code within the workspace. #[serde(default, deserialize_with = "empty_string_none")] pub config: Option, /// An option that points to a path string of the import map to apply to the /// code within the workspace. #[serde(default, deserialize_with = "empty_string_none")] pub import_map: Option, /// Code lens specific settings for the workspace. #[serde(default)] pub code_lens: CodeLensSettings, /// A flag that indicates if internal debug logging should be made available. #[serde(default)] pub internal_debug: bool, #[serde(default)] pub internal_inspect: InspectSetting, /// Write logs to a file in a project-local directory. #[serde(default)] pub log_file: bool, /// A flag that indicates if linting is enabled for the workspace. #[serde(default = "default_to_true")] pub lint: bool, /// Limits the number of files that can be preloaded by the language server. #[serde(default = "default_document_preload_limit")] pub document_preload_limit: usize, #[serde(default)] pub suggest: DenoCompletionSettings, /// Testing settings for the workspace. #[serde(default)] pub testing: TestingSettings, /// An option which sets the cert file to use when attempting to fetch remote /// resources. This overrides `DENO_CERT` if present. #[serde(default, deserialize_with = "empty_string_none")] pub tls_certificate: Option, /// An option, if set, will unsafely ignore certificate errors when fetching /// remote resources. #[serde(default)] pub unsafely_ignore_certificate_errors: Option>, #[serde(default)] pub unstable: bool, #[serde(default)] pub javascript: LanguageWorkspaceSettings, #[serde(default)] pub typescript: LanguageWorkspaceSettings, } impl Default for WorkspaceSettings { fn default() -> Self { WorkspaceSettings { enable: None, disable_paths: vec![], enable_paths: None, cache: None, cache_on_save: false, certificate_stores: None, config: None, import_map: None, code_lens: Default::default(), internal_debug: false, internal_inspect: Default::default(), log_file: false, lint: true, document_preload_limit: default_document_preload_limit(), suggest: Default::default(), testing: Default::default(), tls_certificate: None, unsafely_ignore_certificate_errors: None, unstable: false, javascript: Default::default(), typescript: Default::default(), } } } impl WorkspaceSettings { pub fn from_raw_settings( deno: Value, javascript: Value, typescript: Value, ) -> Self { fn parse_or_default( value: Value, description: &str, ) -> T { if value.is_null() { return T::default(); } match serde_json::from_value(value) { Ok(v) => v, Err(err) => { lsp_warn!("Couldn't parse {description}: {err}"); T::default() } } } let deno_inlay_hints = deno.as_object().and_then(|o| o.get("inlayHints").cloned()); let deno_suggest = deno.as_object().and_then(|o| o.get("suggest").cloned()); let mut settings: Self = parse_or_default(deno, "settings under \"deno\""); settings.javascript = parse_or_default(javascript, "settings under \"javascript\""); settings.typescript = parse_or_default(typescript, "settings under \"typescript\""); if let Some(inlay_hints) = deno_inlay_hints { let inlay_hints: InlayHintsSettings = parse_or_default(inlay_hints, "settings under \"deno.inlayHints\""); if inlay_hints.parameter_names.enabled != Default::default() { lsp_warn!("\"deno.inlayHints.parameterNames.enabled\" is deprecated. Instead use \"javascript.inlayHints.parameterNames.enabled\" and \"typescript.inlayHints.parameterNames.enabled\"."); settings.javascript.inlay_hints.parameter_names.enabled = inlay_hints.parameter_names.enabled.clone(); settings.typescript.inlay_hints.parameter_names.enabled = inlay_hints.parameter_names.enabled; } if !inlay_hints .parameter_names .suppress_when_argument_matches_name { lsp_warn!("\"deno.inlayHints.parameterNames.suppressWhenArgumentMatchesName\" is deprecated. Instead use \"javascript.inlayHints.parameterNames.suppressWhenArgumentMatchesName\" and \"typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName\"."); settings .javascript .inlay_hints .parameter_names .suppress_when_argument_matches_name = inlay_hints .parameter_names .suppress_when_argument_matches_name; settings .typescript .inlay_hints .parameter_names .suppress_when_argument_matches_name = inlay_hints .parameter_names .suppress_when_argument_matches_name; } if inlay_hints.parameter_types.enabled { lsp_warn!("\"deno.inlayHints.parameterTypes.enabled\" is deprecated. Instead use \"javascript.inlayHints.parameterTypes.enabled\" and \"typescript.inlayHints.parameterTypes.enabled\"."); settings.javascript.inlay_hints.parameter_types.enabled = inlay_hints.parameter_types.enabled; settings.typescript.inlay_hints.parameter_types.enabled = inlay_hints.parameter_types.enabled; } if inlay_hints.variable_types.enabled { lsp_warn!("\"deno.inlayHints.variableTypes.enabled\" is deprecated. Instead use \"javascript.inlayHints.variableTypes.enabled\" and \"typescript.inlayHints.variableTypes.enabled\"."); settings.javascript.inlay_hints.variable_types.enabled = inlay_hints.variable_types.enabled; settings.typescript.inlay_hints.variable_types.enabled = inlay_hints.variable_types.enabled; } if !inlay_hints.variable_types.suppress_when_type_matches_name { lsp_warn!("\"deno.inlayHints.variableTypes.suppressWhenTypeMatchesName\" is deprecated. Instead use \"javascript.inlayHints.variableTypes.suppressWhenTypeMatchesName\" and \"typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName\"."); settings .javascript .inlay_hints .variable_types .suppress_when_type_matches_name = inlay_hints.variable_types.suppress_when_type_matches_name; settings .typescript .inlay_hints .variable_types .suppress_when_type_matches_name = inlay_hints.variable_types.suppress_when_type_matches_name; } if inlay_hints.property_declaration_types.enabled { lsp_warn!("\"deno.inlayHints.propertyDeclarationTypes.enabled\" is deprecated. Instead use \"javascript.inlayHints.propertyDeclarationTypes.enabled\" and \"typescript.inlayHints.propertyDeclarationTypes.enabled\"."); settings .javascript .inlay_hints .property_declaration_types .enabled = inlay_hints.property_declaration_types.enabled; settings .typescript .inlay_hints .property_declaration_types .enabled = inlay_hints.property_declaration_types.enabled; } if inlay_hints.function_like_return_types.enabled { lsp_warn!("\"deno.inlayHints.functionLikeReturnTypes.enabled\" is deprecated. Instead use \"javascript.inlayHints.functionLikeReturnTypes.enabled\" and \"typescript.inlayHints.functionLikeReturnTypes.enabled\"."); settings .javascript .inlay_hints .function_like_return_types .enabled = inlay_hints.function_like_return_types.enabled; settings .typescript .inlay_hints .function_like_return_types .enabled = inlay_hints.function_like_return_types.enabled; } if inlay_hints.enum_member_values.enabled { lsp_warn!("\"deno.inlayHints.enumMemberValues.enabled\" is deprecated. Instead use \"javascript.inlayHints.enumMemberValues.enabled\" and \"typescript.inlayHints.enumMemberValues.enabled\"."); settings.javascript.inlay_hints.enum_member_values.enabled = inlay_hints.enum_member_values.enabled; settings.typescript.inlay_hints.enum_member_values.enabled = inlay_hints.enum_member_values.enabled; } } if let Some(suggest) = deno_suggest { let suggest: CompletionSettings = parse_or_default(suggest, "settings under \"deno.suggest\""); if suggest.complete_function_calls { lsp_warn!("\"deno.suggest.completeFunctionCalls\" is deprecated. Instead use \"javascript.suggest.completeFunctionCalls\" and \"typescript.suggest.completeFunctionCalls\"."); settings.javascript.suggest.complete_function_calls = suggest.complete_function_calls; settings.typescript.suggest.complete_function_calls = suggest.complete_function_calls; } if !suggest.names { lsp_warn!("\"deno.suggest.names\" is deprecated. Instead use \"javascript.suggest.names\" and \"typescript.suggest.names\"."); settings.javascript.suggest.names = suggest.names; settings.typescript.suggest.names = suggest.names; } if !suggest.paths { lsp_warn!("\"deno.suggest.paths\" is deprecated. Instead use \"javascript.suggest.paths\" and \"typescript.suggest.paths\"."); settings.javascript.suggest.paths = suggest.paths; settings.typescript.suggest.paths = suggest.paths; } if !suggest.auto_imports { lsp_warn!("\"deno.suggest.autoImports\" is deprecated. Instead use \"javascript.suggest.autoImports\" and \"typescript.suggest.autoImports\"."); settings.javascript.suggest.auto_imports = suggest.auto_imports; settings.typescript.suggest.auto_imports = suggest.auto_imports; } } settings } pub fn from_initialization_options(options: Value) -> Self { let deno = options; let javascript = deno .as_object() .and_then(|o| o.get("javascript").cloned()) .unwrap_or_default(); let typescript = deno .as_object() .and_then(|o| o.get("typescript").cloned()) .unwrap_or_default(); Self::from_raw_settings(deno, javascript, typescript) } } #[derive(Debug, Default, Clone)] pub struct Settings { pub unscoped: Arc, pub by_workspace_folder: BTreeMap>>, pub first_folder: Option, } impl Settings { /// 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(self.first_folder.as_ref()); 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 } pub fn get_for_specifier( &self, specifier: &ModuleSpecifier, ) -> (&WorkspaceSettings, Option<&ModuleSpecifier>) { let Ok(path) = specifier_to_file_path(specifier) else { return (&self.unscoped, self.first_folder.as_ref()); }; for (folder_uri, settings) in self.by_workspace_folder.iter().rev() { if let Some(settings) = settings { let Ok(folder_path) = specifier_to_file_path(folder_uri) else { continue; }; if path.starts_with(folder_path) { return (settings, Some(folder_uri)); } } } (&self.unscoped, self.first_folder.as_ref()) } pub fn enable_settings_hash(&self) -> u64 { let mut hasher = FastInsecureHasher::new_without_deno_version(); 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.write_hashable(&self.first_folder); hasher.finish() } } #[derive(Clone, Debug, Default)] pub struct Config { pub client_capabilities: Arc, pub settings: Arc, pub workspace_folders: Arc>, pub tree: ConfigTree, } impl Config { #[cfg(test)] pub fn new_with_roots(root_uris: impl IntoIterator) -> Self { let mut config = Self::default(); 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 = Arc::new(Settings { unscoped: self.settings.unscoped.clone(), by_workspace_folder: folders .iter() .map(|(s, _)| (s.clone(), None)) .collect(), first_folder: folders.first().map(|(s, _)| s.clone()), }); self.workspace_folders = Arc::new(folders); } pub fn set_workspace_settings( &mut self, unscoped: WorkspaceSettings, folder_settings: Vec<(ModuleSpecifier, WorkspaceSettings)>, ) { let mut by_folder = folder_settings.into_iter().collect::>(); self.settings = Arc::new(Settings { unscoped: Arc::new(unscoped), by_workspace_folder: self .settings .by_workspace_folder .keys() .map(|s| (s.clone(), by_folder.remove(s).map(Arc::new))) .collect(), first_folder: self.settings.first_folder.clone(), }); } pub fn workspace_settings(&self) -> &WorkspaceSettings { self.settings.get_unscoped() } pub fn workspace_settings_for_specifier( &self, specifier: &ModuleSpecifier, ) -> &WorkspaceSettings { self.settings.get_for_specifier(specifier).0 } pub fn language_settings_for_specifier( &self, specifier: &ModuleSpecifier, ) -> Option<&LanguageWorkspaceSettings> { let workspace_settings = self.workspace_settings_for_specifier(specifier); match MediaType::from_specifier(specifier) { MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs => Some(&workspace_settings.javascript), MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Tsx => Some(&workspace_settings.typescript), MediaType::Json | MediaType::Wasm | MediaType::TsBuildInfo | MediaType::SourceMap | MediaType::Unknown => None, } } /// Determine if any inlay hints are enabled. This allows short circuiting /// when there are no inlay hints enabled. pub fn enabled_inlay_hints_for_specifier( &self, specifier: &ModuleSpecifier, ) -> bool { let Some(settings) = self.language_settings_for_specifier(specifier) else { return false; }; !matches!( settings.inlay_hints.parameter_names.enabled, InlayHintsParamNamesEnabled::None ) || settings.inlay_hints.parameter_types.enabled || settings.inlay_hints.variable_types.enabled || settings.inlay_hints.property_declaration_types.enabled || settings.inlay_hints.function_like_return_types.enabled || settings.inlay_hints.enum_member_values.enabled } pub fn root_uri(&self) -> Option<&Url> { self.workspace_folders.first().map(|p| &p.0) } pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool { let data = self.tree.data_for_specifier(specifier); if let Some(data) = &data { if let Ok(path) = specifier.to_file_path() { if data.exclude_files.matches_path(&path) { return false; } } } self .settings .specifier_enabled(specifier) .unwrap_or_else(|| data.and_then(|d| d.maybe_deno_json()).is_some()) } pub fn specifier_enabled_for_test( &self, specifier: &ModuleSpecifier, ) -> bool { if let Some(data) = self.tree.data_for_specifier(specifier) { if !data.test_config.files.matches_specifier(specifier) { return false; } } self.specifier_enabled(specifier) } pub fn log_file(&self) -> bool { self.settings.unscoped.log_file } pub fn internal_inspect(&self) -> &InspectSetting { &self.settings.unscoped.internal_inspect } pub fn set_client_capabilities( &mut self, client_capabilities: ClientCapabilities, ) { self.client_capabilities = Arc::new(client_capabilities); } pub fn workspace_capable(&self) -> bool { self.client_capabilities.workspace.is_some() } pub fn workspace_configuration_capable(&self) -> bool { (|| self.client_capabilities.workspace.as_ref()?.configuration)() .unwrap_or(false) } pub fn did_change_watched_files_capable(&self) -> bool { (|| { let workspace = self.client_capabilities.workspace.as_ref()?; let did_change_watched_files = workspace.did_change_watched_files.as_ref()?; did_change_watched_files.dynamic_registration })() .unwrap_or(false) } pub fn will_rename_files_capable(&self) -> bool { (|| { let workspace = self.client_capabilities.workspace.as_ref()?; let file_operations = workspace.file_operations.as_ref()?; file_operations.dynamic_registration.filter(|d| *d)?; file_operations.will_rename })() .unwrap_or(false) } pub fn line_folding_only_capable(&self) -> bool { (|| { let text_document = self.client_capabilities.text_document.as_ref()?; text_document.folding_range.as_ref()?.line_folding_only })() .unwrap_or(false) } pub fn code_action_disabled_capable(&self) -> bool { (|| { let text_document = self.client_capabilities.text_document.as_ref()?; text_document.code_action.as_ref()?.disabled_support })() .unwrap_or(false) } pub fn snippet_support_capable(&self) -> bool { (|| { let text_document = self.client_capabilities.text_document.as_ref()?; let completion = text_document.completion.as_ref()?; completion.completion_item.as_ref()?.snippet_support })() .unwrap_or(false) } pub fn testing_api_capable(&self) -> bool { (|| { let experimental = self.client_capabilities.experimental.as_ref()?; experimental.get("testingApi")?.as_bool() })() .unwrap_or(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, "lib": ["deno.ns", "deno.window", "deno.unstable"], "module": "esnext", "moduleDetection": "force", "noEmit": true, "resolveJsonModule": true, "strict": true, "target": "esnext", "useDefineForClassFields": true, "useUnknownInCatchVariables": false, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", })), } } } impl LspTsConfig { pub fn new(config_file: Option<&ConfigFile>) -> Self { let mut ts_config = Self::default(); match ts_config.inner.merge_tsconfig_from_config_file(config_file) { Ok(Some(ignored_options)) => lsp_warn!("{}", ignored_options), Err(err) => lsp_warn!("{}", err), _ => {} } ts_config } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConfigWatchedFileType { DenoJson, Lockfile, NpmRc, PackageJson, ImportMap, } /// Contains the config file and dependent information. #[derive(Debug, Clone)] pub struct ConfigData { pub scope: Arc, pub member_dir: Arc, pub fmt_config: Arc, pub lint_config: Arc, pub test_config: Arc, pub exclude_files: Arc, pub deno_lint_config: DenoLintConfig, pub lint_rules: Arc, pub ts_config: Arc, pub byonm: bool, pub node_modules_dir: Option, pub vendor_dir: Option, pub lockfile: Option>, pub npmrc: Option>, pub resolver: Arc, pub import_map_from_settings: Option, watched_files: HashMap, } impl ConfigData { #[allow(clippy::too_many_arguments)] async fn load( specified_config: Option<&Path>, scope: &ModuleSpecifier, settings: &Settings, file_fetcher: &Arc, // sync requirement is because the lsp requires sync cached_deno_config_fs: &(dyn DenoConfigFs + Sync), deno_json_cache: &(dyn DenoJsonCache + Sync), pkg_json_cache: &(dyn PackageJsonCache + Sync), workspace_cache: &(dyn WorkspaceCache + Sync), ) -> Self { let scope = Arc::new(scope.clone()); let discover_result = match scope.to_file_path() { Ok(scope_dir_path) => { let paths = [scope_dir_path]; WorkspaceDirectory::discover( match specified_config { Some(config_path) => { deno_config::workspace::WorkspaceDiscoverStart::ConfigFile( config_path, ) } None => { deno_config::workspace::WorkspaceDiscoverStart::Paths(&paths) } }, &WorkspaceDiscoverOptions { fs: cached_deno_config_fs, additional_config_file_names: &[], deno_json_cache: Some(deno_json_cache), pkg_json_cache: Some(pkg_json_cache), workspace_cache: Some(workspace_cache), discover_pkg_json: !has_flag_env_var("DENO_NO_PACKAGE_JSON"), config_parse_options: Default::default(), maybe_vendor_override: None, }, ) .map(Arc::new) .map_err(AnyError::from) } Err(()) => Err(anyhow!("Scope '{}' was not a directory path.", scope)), }; match discover_result { Ok(member_dir) => { Self::load_inner(member_dir, scope, settings, Some(file_fetcher)).await } Err(err) => { lsp_warn!(" Couldn't open workspace \"{}\": {}", scope.as_str(), err); let member_dir = Arc::new(WorkspaceDirectory::empty(WorkspaceDirectoryEmptyOptions { root_dir: scope.clone(), use_vendor_dir: VendorEnablement::Disable, })); let mut data = Self::load_inner( member_dir, scope.clone(), settings, Some(file_fetcher), ) .await; // check if any of these need to be added to the workspace let files = [ ( scope.join("deno.json").unwrap(), ConfigWatchedFileType::DenoJson, ), ( scope.join("deno.jsonc").unwrap(), ConfigWatchedFileType::DenoJson, ), ( scope.join("package.json").unwrap(), ConfigWatchedFileType::PackageJson, ), ]; for (url, file_type) in files { let Some(file_path) = url.to_file_path().ok() else { continue; }; if file_path.exists() { data.watched_files.insert(url.clone(), file_type); let canonicalized_specifier = canonicalize_path_maybe_not_exists(&file_path) .ok() .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); if let Some(specifier) = canonicalized_specifier { data.watched_files.insert(specifier, file_type); } } } data } } } async fn load_inner( member_dir: Arc, scope: Arc, settings: &Settings, file_fetcher: Option<&Arc>, ) -> Self { let (settings, workspace_folder) = settings.get_for_specifier(&scope); let mut watched_files = HashMap::with_capacity(10); let mut add_watched_file = |specifier: ModuleSpecifier, file_type: ConfigWatchedFileType| { let maybe_canonicalized = specifier .to_file_path() .ok() .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok()) .and_then(|p| ModuleSpecifier::from_file_path(p).ok()); if let Some(canonicalized) = maybe_canonicalized { if canonicalized != specifier { watched_files.entry(canonicalized).or_insert(file_type); } } watched_files.entry(specifier).or_insert(file_type); }; if let Some(deno_json) = member_dir.maybe_deno_json() { lsp_log!( " Resolved Deno configuration file: \"{}\"", deno_json.specifier ); add_watched_file( deno_json.specifier.clone(), ConfigWatchedFileType::DenoJson, ); } if let Some(pkg_json) = member_dir.maybe_pkg_json() { lsp_log!(" Resolved package.json: \"{}\"", pkg_json.specifier()); add_watched_file( pkg_json.specifier(), ConfigWatchedFileType::PackageJson, ); } // todo(dsherret): cache this so we don't load this so many times let npmrc = discover_npmrc_from_workspace(&member_dir.workspace) .inspect(|(_, path)| { if let Some(path) = path { lsp_log!(" Resolved .npmrc: \"{}\"", path.display()); if let Ok(specifier) = ModuleSpecifier::from_file_path(path) { add_watched_file(specifier, ConfigWatchedFileType::NpmRc); } } }) .inspect_err(|err| { lsp_warn!(" Couldn't read .npmrc for \"{scope}\": {err}"); }) .map(|(r, _)| r) .ok(); let default_file_pattern_base = scope.to_file_path().unwrap_or_else(|_| PathBuf::from("/")); let fmt_config = Arc::new( member_dir .to_fmt_config(FilePatterns::new_with_base(member_dir.dir_path())) .inspect_err(|err| { lsp_warn!(" Couldn't read formatter configuration: {}", err) }) .ok() .unwrap_or_else(|| { FmtConfig::new_with_base(default_file_pattern_base.clone()) }), ); let lint_config = Arc::new( member_dir .to_lint_config(FilePatterns::new_with_base(member_dir.dir_path())) .inspect_err(|err| { lsp_warn!(" Couldn't read lint configuration: {}", err) }) .ok() .unwrap_or_else(|| { LintConfig::new_with_base(default_file_pattern_base.clone()) }), ); let lint_rules = Arc::new(get_configured_rules( lint_config.options.rules.clone(), member_dir.maybe_deno_json().map(|c| c.as_ref()), )); let test_config = Arc::new( member_dir .to_test_config(FilePatterns::new_with_base(member_dir.dir_path())) .inspect_err(|err| { lsp_warn!(" Couldn't read test configuration: {}", err) }) .ok() .unwrap_or_else(|| { TestConfig::new_with_base(default_file_pattern_base.clone()) }), ); let exclude_files = Arc::new( member_dir .workspace .resolve_config_excludes() .inspect_err(|err| { lsp_warn!(" Couldn't read config excludes: {}", err) }) .ok() .unwrap_or_default(), ); let ts_config = LspTsConfig::new( member_dir.workspace.root_deno_json().map(|c| c.as_ref()), ); let deno_lint_config = if ts_config.inner.0.get("jsx").and_then(|v| v.as_str()) == Some("react") { let default_jsx_factory = ts_config.inner.0.get("jsxFactory").and_then(|v| v.as_str()); let default_jsx_fragment_factory = ts_config .inner .0 .get("jsxFragmentFactory") .and_then(|v| v.as_str()); DenoLintConfig { default_jsx_factory: default_jsx_factory.map(String::from), default_jsx_fragment_factory: default_jsx_fragment_factory .map(String::from), } } else { DenoLintConfig { default_jsx_factory: None, default_jsx_fragment_factory: None, } }; let vendor_dir = member_dir.workspace.vendor_dir_path().cloned(); // todo(dsherret): add caching so we don't load this so many times let lockfile = resolve_lockfile_from_workspace(&member_dir).map(Arc::new); if let Some(lockfile) = &lockfile { if let Ok(specifier) = ModuleSpecifier::from_file_path(&lockfile.filename) { add_watched_file(specifier, ConfigWatchedFileType::Lockfile); } } let byonm = std::env::var("DENO_UNSTABLE_BYONM").is_ok() || member_dir.workspace.has_unstable("byonm") || (*DENO_FUTURE && member_dir.workspace.package_jsons().next().is_some() && member_dir.workspace.node_modules_dir().is_none()); if byonm { lsp_log!(" Enabled 'bring your own node_modules'."); } let node_modules_dir = resolve_node_modules_dir(&member_dir.workspace, byonm); // Mark the import map as a watched file if let Some(import_map_specifier) = member_dir .workspace .to_import_map_specifier() .ok() .flatten() { add_watched_file( import_map_specifier.clone(), ConfigWatchedFileType::ImportMap, ); } // attempt to create a resolver for the workspace let pkg_json_dep_resolution = if byonm { PackageJsonDepResolution::Disabled } else { // todo(dsherret): this should be false for nodeModulesDir: true PackageJsonDepResolution::Enabled }; let mut import_map_from_settings = { let is_config_import_map = member_dir .maybe_deno_json() .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) .or_else(|| { member_dir .workspace .root_deno_json() .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) }) .unwrap_or(false); if is_config_import_map { None } else { settings.import_map.as_ref().and_then(|import_map_str| { Url::parse(import_map_str) .ok() .or_else(|| workspace_folder?.join(import_map_str).ok()) }) } }; let specified_import_map = { let is_config_import_map = member_dir .maybe_deno_json() .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) .or_else(|| { member_dir .workspace .root_deno_json() .map(|c| c.is_an_import_map() || c.json.import_map.is_some()) }) .unwrap_or(false); if is_config_import_map { import_map_from_settings = None; } if let Some(import_map_url) = &import_map_from_settings { add_watched_file( import_map_url.clone(), ConfigWatchedFileType::ImportMap, ); // spawn due to the lsp's `Send` requirement let fetch_result = deno_core::unsync::spawn({ let file_fetcher = file_fetcher.cloned().unwrap(); let import_map_url = import_map_url.clone(); async move { file_fetcher .fetch(&import_map_url, &PermissionsContainer::allow_all()) .await } }) .await .unwrap(); let value_result = fetch_result.and_then(|f| { serde_json::from_slice::(&f.source).map_err(|e| e.into()) }); match value_result { Ok(value) => Some(SpecifiedImportMap { base_url: import_map_url.clone(), value, }), Err(err) => { lsp_warn!( " Couldn't read import map \"{}\": {}", import_map_url.as_str(), err ); import_map_from_settings = None; None } } } else { None } }; let resolver = deno_core::unsync::spawn({ let workspace = member_dir.clone(); let file_fetcher = file_fetcher.cloned(); async move { workspace .create_resolver( CreateResolverOptions { pkg_json_dep_resolution, specified_import_map, }, move |specifier| { let specifier = specifier.clone(); let file_fetcher = file_fetcher.clone().unwrap(); async move { let file = file_fetcher .fetch(&specifier, &PermissionsContainer::allow_all()) .await? .into_text_decoded()?; Ok(file.source.to_string()) } }, ) .await .inspect_err(|err| { lsp_warn!( " Failed to load resolver: {}", err // will contain the specifier ); }) .ok() } }) .await .unwrap() .unwrap_or_else(|| { // create a dummy resolver WorkspaceResolver::new_raw( scope.clone(), None, member_dir.workspace.package_jsons().cloned().collect(), pkg_json_dep_resolution, ) }); if !resolver.diagnostics().is_empty() { lsp_warn!( " Import map diagnostics:\n{}", resolver .diagnostics() .iter() .map(|d| format!(" - {d}")) .collect::>() .join("\n") ); } ConfigData { scope, member_dir, resolver: Arc::new(resolver), fmt_config, lint_config, test_config, deno_lint_config, lint_rules, exclude_files, ts_config: Arc::new(ts_config), byonm, node_modules_dir, vendor_dir, lockfile, npmrc, import_map_from_settings, watched_files, } } pub fn maybe_deno_json( &self, ) -> Option<&Arc> { self.member_dir.maybe_deno_json() } pub fn maybe_pkg_json( &self, ) -> Option<&Arc> { self.member_dir.maybe_pkg_json() } } #[derive(Clone, Debug, Default)] pub struct ConfigTree { scopes: Arc>>, } impl ConfigTree { pub fn scope_for_specifier( &self, specifier: &ModuleSpecifier, ) -> Option<&ModuleSpecifier> { self .scopes .keys() .rfind(|s| specifier.as_str().starts_with(s.as_str())) } pub fn data_for_specifier( &self, specifier: &ModuleSpecifier, ) -> Option<&Arc> { self .scope_for_specifier(specifier) .and_then(|s| self.scopes.get(s)) } pub fn data_by_scope( &self, ) -> &Arc>> { &self.scopes } pub fn workspace_dir_for_specifier( &self, specifier: &ModuleSpecifier, ) -> Option<&WorkspaceDirectory> { self .data_for_specifier(specifier) .map(|d| d.member_dir.as_ref()) } pub fn config_files(&self) -> Vec<&Arc> { self .scopes .iter() .filter_map(|(_, d)| d.maybe_deno_json()) .collect() } pub fn package_jsons(&self) -> Vec<&Arc> { self .scopes .iter() .filter_map(|(_, d)| d.maybe_pkg_json()) .collect() } pub fn fmt_config_for_specifier( &self, specifier: &ModuleSpecifier, ) -> Arc { self .data_for_specifier(specifier) .map(|d| d.fmt_config.clone()) .unwrap_or_else(|| Arc::new(FmtConfig::new_with_base(PathBuf::from("/")))) } /// Returns (scope_uri, type). pub fn watched_file_type( &self, 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 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 .scopes .values() .any(|data| data.watched_files.contains_key(specifier)) } pub async fn refresh( &mut self, settings: &Settings, workspace_files: &IndexSet, file_fetcher: &Arc, ) { lsp_log!("Refreshing configuration tree..."); // since we're resolving a workspace multiple times in different // folders, we want to cache all the lookups and config files across // ConfigData::load calls let cached_fs = CachedDenoConfigFs::default(); let deno_json_cache = DenoJsonMemCache::default(); let pkg_json_cache = PackageJsonMemCache::default(); let workspace_cache = WorkspaceMemCache::default(); 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) { if let Ok(config_file_path) = config_uri.to_file_path() { scopes.insert( folder_uri.clone(), Arc::new( ConfigData::load( Some(&config_file_path), folder_uri, settings, file_fetcher, &cached_fs, &deno_json_cache, &pkg_json_cache, &workspace_cache, ) .await, ), ); } } } } } for specifier in workspace_files { if !(specifier.path().ends_with("/deno.json") || specifier.path().ends_with("/deno.jsonc") || specifier.path().ends_with("/package.json")) { continue; } let Ok(scope) = specifier.join(".") else { continue; }; if scopes.contains_key(&scope) { continue; } let data = Arc::new( ConfigData::load( None, &scope, settings, file_fetcher, &cached_fs, &deno_json_cache, &pkg_json_cache, &workspace_cache, ) .await, ); scopes.insert(scope, data.clone()); for (member_scope, _) in data.member_dir.workspace.config_folders() { if scopes.contains_key(member_scope) { continue; } let member_data = ConfigData::load( None, member_scope, settings, file_fetcher, &cached_fs, &deno_json_cache, &pkg_json_cache, &workspace_cache, ) .await; scopes.insert(member_scope.as_ref().clone(), Arc::new(member_data)); } } 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(), Arc::new( ConfigData::load( None, folder_uri, settings, file_fetcher, &cached_fs, &deno_json_cache, &pkg_json_cache, &workspace_cache, ) .await, ), ); } } self.scopes = Arc::new(scopes); } #[cfg(test)] pub async fn inject_config_file(&mut self, config_file: ConfigFile) { let scope = config_file.specifier.join(".").unwrap(); let json_text = serde_json::to_string(&config_file.json).unwrap(); let test_fs = deno_runtime::deno_fs::InMemoryFs::default(); let config_path = specifier_to_file_path(&config_file.specifier).unwrap(); test_fs.setup_text_files(vec![( config_path.to_string_lossy().to_string(), json_text, )]); let workspace_dir = Arc::new( WorkspaceDirectory::discover( deno_config::workspace::WorkspaceDiscoverStart::ConfigFile( &config_path, ), &deno_config::workspace::WorkspaceDiscoverOptions { fs: &deno_runtime::deno_fs::DenoConfigFsAdapter::new(&test_fs), ..Default::default() }, ) .unwrap(), ); let data = Arc::new( ConfigData::load_inner( workspace_dir, Arc::new(scope.clone()), &Default::default(), None, ) .await, ); assert!(data.maybe_deno_json().is_some()); self.scopes = Arc::new([(scope, data)].into_iter().collect()); } } fn resolve_lockfile_from_workspace( workspace: &WorkspaceDirectory, ) -> Option { let lockfile_path = match workspace.workspace.resolve_lockfile_path() { Ok(Some(value)) => value, Ok(None) => return None, Err(err) => { lsp_warn!("Error resolving lockfile: {:#}", err); return None; } }; resolve_lockfile_from_path(lockfile_path) } fn resolve_node_modules_dir( workspace: &Workspace, byonm: bool, ) -> Option { // For the language server, require an explicit opt-in via the // `nodeModulesDir: true` setting in the deno.json file. This is to // reduce the chance of modifying someone's node_modules directory // without them having asked us to do so. let explicitly_disabled = workspace.node_modules_dir() == Some(false); if explicitly_disabled { return None; } let enabled = byonm || workspace.node_modules_dir() == Some(true) || workspace.vendor_dir_path().is_some(); if !enabled { return None; } let node_modules_dir = workspace .root_dir() .to_file_path() .ok()? .join("node_modules"); canonicalize_path_maybe_not_exists(&node_modules_dir).ok() } fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option { match CliLockfile::read_from_path(lockfile_path, false) { Ok(value) => { if value.filename.exists() { if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename) { lsp_log!(" Resolved lockfile: \"{}\"", specifier); } } Some(value) } Err(err) => { lsp_warn!("Error loading lockfile: {:#}", err); None } } } // todo(dsherret): switch to RefCell once the lsp no longer requires Sync #[derive(Default)] struct DenoJsonMemCache(Mutex>>); impl deno_config::deno_json::DenoJsonCache for DenoJsonMemCache { fn get(&self, path: &Path) -> Option> { self.0.lock().get(path).cloned() } fn set(&self, path: PathBuf, data: Arc) { self.0.lock().insert(path, data); } } #[derive(Default)] struct PackageJsonMemCache(Mutex>>); impl deno_config::package_json::PackageJsonCache for PackageJsonMemCache { fn get(&self, path: &Path) -> Option> { self.0.lock().get(path).cloned() } fn set(&self, path: PathBuf, data: Arc) { self.0.lock().insert(path, data); } } #[derive(Default)] struct WorkspaceMemCache(Mutex>>); impl deno_config::workspace::WorkspaceCache for WorkspaceMemCache { fn get(&self, dir_path: &Path) -> Option> { self.0.lock().get(dir_path).cloned() } fn set(&self, dir_path: PathBuf, workspace: Arc) { self.0.lock().insert(dir_path, workspace); } } #[derive(Default)] struct CachedFsItems { items: HashMap>, } impl CachedFsItems { pub fn get( &mut self, path: &Path, action: impl FnOnce(&Path) -> Result, ) -> Result { let value = if let Some(value) = self.items.get(path) { value } else { let value = action(path); // just in case this gets really large for some reason if self.items.len() == 16_384 { return value; } self.items.insert(path.to_owned(), value); self.items.get(path).unwrap() }; value .as_ref() .map(|v| (*v).clone()) .map_err(|e| std::io::Error::new(e.kind(), e.to_string())) } } #[derive(Default)] struct InnerData { stat_calls: CachedFsItems, read_to_string_calls: CachedFsItems, } #[derive(Default)] struct CachedDenoConfigFs(Mutex); impl DenoConfigFs for CachedDenoConfigFs { fn stat_sync( &self, path: &Path, ) -> Result { self .0 .lock() .stat_calls .get(path, |path| RealDenoConfigFs.stat_sync(path)) } fn read_to_string_lossy( &self, path: &Path, ) -> Result { self .0 .lock() .read_to_string_calls .get(path, |path| RealDenoConfigFs.read_to_string_lossy(path)) } fn read_dir( &self, path: &Path, ) -> Result, std::io::Error> { // no need to cache these because the workspace cache will ensure // we only do read_dir calls once (read_dirs are only used for // npm workspace resolution) RealDenoConfigFs.read_dir(path) } } #[cfg(test)] mod tests { use deno_config::deno_json::ConfigParseOptions; use deno_core::resolve_url; use deno_core::serde_json; use deno_core::serde_json::json; use pretty_assertions::assert_eq; use super::*; #[test] fn test_config_specifier_enabled() { let root_uri = resolve_url("file:///").unwrap(); 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( serde_json::from_value(json!({ "enable": true })) .unwrap(), vec![], ); assert!(config.specifier_enabled(&specifier)); } #[test] fn test_config_snapshot_specifier_enabled() { let root_uri = resolve_url("file:///").unwrap(); 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( serde_json::from_value(json!({ "enable": true })) .unwrap(), vec![], ); assert!(config.specifier_enabled(&specifier)); } #[test] fn test_config_specifier_enabled_path() { let root_uri = resolve_url("file:///project/").unwrap(); 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, vec![]); assert!(config.specifier_enabled(&specifier_a)); assert!(!config.specifier_enabled(&specifier_b)); } #[test] fn test_config_specifier_disabled_path() { let root_uri = resolve_url("file:///root/").unwrap(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.set_workspace_settings( WorkspaceSettings { enable: Some(true), enable_paths: Some(vec!["mod1.ts".to_string(), "mod2.ts".to_string()]), disable_paths: vec!["mod2.ts".to_string()], ..Default::default() }, vec![], ); assert!(config.specifier_enabled(&root_uri.join("mod1.ts").unwrap())); assert!(!config.specifier_enabled(&root_uri.join("mod2.ts").unwrap())); assert!(!config.specifier_enabled(&root_uri.join("mod3.ts").unwrap())); } #[test] fn test_set_workspace_settings_defaults() { let mut config = Config::default(); config.set_workspace_settings( serde_json::from_value(json!({})).unwrap(), vec![], ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings { enable: None, disable_paths: vec![], enable_paths: None, cache: None, cache_on_save: false, certificate_stores: None, config: None, import_map: None, code_lens: CodeLensSettings { implementations: false, references: false, references_all_functions: false, test: true, }, internal_debug: false, internal_inspect: InspectSetting::Bool(false), log_file: false, lint: true, document_preload_limit: 1_000, suggest: DenoCompletionSettings { imports: ImportCompletionSettings { auto_discover: true, hosts: HashMap::new(), } }, testing: TestingSettings { args: vec!["--allow-all".to_string(), "--no-check".to_string()], }, tls_certificate: None, unsafely_ignore_certificate_errors: None, unstable: false, javascript: LanguageWorkspaceSettings { inlay_hints: InlayHintsSettings { parameter_names: InlayHintsParamNamesOptions { enabled: InlayHintsParamNamesEnabled::None, suppress_when_argument_matches_name: true }, parameter_types: InlayHintsParamTypesOptions { enabled: false }, variable_types: InlayHintsVarTypesOptions { enabled: false, suppress_when_type_matches_name: true }, property_declaration_types: InlayHintsPropDeclTypesOptions { enabled: false }, function_like_return_types: InlayHintsFuncLikeReturnTypesOptions { enabled: false }, enum_member_values: InlayHintsEnumMemberValuesOptions { enabled: false }, }, preferences: LanguagePreferences { import_module_specifier: ImportModuleSpecifier::Shortest, jsx_attribute_completion_style: JsxAttributeCompletionStyle::Auto, auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: QuoteStyle::Auto, }, suggest: CompletionSettings { complete_function_calls: false, include_automatic_optional_chain_completions: true, include_completions_for_import_statements: true, names: true, paths: true, auto_imports: true, enabled: true, class_member_snippets: ClassMemberSnippets { enabled: true }, object_literal_method_snippets: ObjectLiteralMethodSnippets { enabled: true, }, }, update_imports_on_file_move: UpdateImportsOnFileMoveOptions { enabled: UpdateImportsOnFileMoveEnabled::Prompt } }, typescript: LanguageWorkspaceSettings { inlay_hints: InlayHintsSettings { parameter_names: InlayHintsParamNamesOptions { enabled: InlayHintsParamNamesEnabled::None, suppress_when_argument_matches_name: true }, parameter_types: InlayHintsParamTypesOptions { enabled: false }, variable_types: InlayHintsVarTypesOptions { enabled: false, suppress_when_type_matches_name: true }, property_declaration_types: InlayHintsPropDeclTypesOptions { enabled: false }, function_like_return_types: InlayHintsFuncLikeReturnTypesOptions { enabled: false }, enum_member_values: InlayHintsEnumMemberValuesOptions { enabled: false }, }, preferences: LanguagePreferences { import_module_specifier: ImportModuleSpecifier::Shortest, jsx_attribute_completion_style: JsxAttributeCompletionStyle::Auto, auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, quote_style: QuoteStyle::Auto, }, suggest: CompletionSettings { complete_function_calls: false, include_automatic_optional_chain_completions: true, include_completions_for_import_statements: true, names: true, paths: true, auto_imports: true, enabled: true, class_member_snippets: ClassMemberSnippets { enabled: true }, object_literal_method_snippets: ObjectLiteralMethodSnippets { enabled: true, }, }, update_imports_on_file_move: UpdateImportsOnFileMoveOptions { enabled: UpdateImportsOnFileMoveEnabled::Prompt } }, } ); } #[test] fn test_empty_cache() { let mut config = Config::default(); config.set_workspace_settings( serde_json::from_value(json!({ "cache": "" })).unwrap(), vec![], ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings::default() ); } #[test] fn test_empty_import_map() { let mut config = Config::default(); config.set_workspace_settings( serde_json::from_value(json!({ "import_map": "" })).unwrap(), vec![], ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings::default() ); } #[test] fn test_empty_tls_certificate() { let mut config = Config::default(); config.set_workspace_settings( serde_json::from_value(json!({ "tls_certificate": "" })).unwrap(), vec![], ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings::default() ); } #[test] fn test_empty_config() { let mut config = Config::default(); config.set_workspace_settings( serde_json::from_value(json!({ "config": "" })).unwrap(), vec![], ); assert_eq!( config.workspace_settings().clone(), WorkspaceSettings::default() ); } #[tokio::test] async fn config_enable_via_config_file_detection() { let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); assert!(!config.specifier_enabled(&root_uri)); config .tree .inject_config_file( ConfigFile::new( "{}", root_uri.join("deno.json").unwrap(), &ConfigParseOptions::default(), ) .unwrap(), ) .await; assert!(config.specifier_enabled(&root_uri)); } // Regression test for https://github.com/denoland/vscode_deno/issues/917. #[test] fn config_specifier_enabled_matches_by_path_component() { let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); config.set_workspace_settings( WorkspaceSettings { enable_paths: Some(vec!["mo".to_string()]), ..Default::default() }, vec![], ); assert!(!config.specifier_enabled(&root_uri.join("mod.ts").unwrap())); } #[tokio::test] async fn config_specifier_enabled_for_test() { let root_uri = root_dir(); let mut config = Config::new_with_roots(vec![root_uri.clone()]); let mut settings = WorkspaceSettings { enable: Some(true), enable_paths: Some(vec!["mod1.ts".to_string(), "mod2.ts".to_string()]), disable_paths: vec!["mod2.ts".to_string()], ..Default::default() }; config.set_workspace_settings(settings.clone(), vec![]); assert!( config.specifier_enabled_for_test(&root_uri.join("mod1.ts").unwrap()) ); assert!( !config.specifier_enabled_for_test(&root_uri.join("mod2.ts").unwrap()) ); assert!( !config.specifier_enabled_for_test(&root_uri.join("mod3.ts").unwrap()) ); settings.enable_paths = None; config.set_workspace_settings(settings, vec![]); config .tree .inject_config_file( ConfigFile::new( &json!({ "exclude": ["mod2.ts"], "test": { "exclude": ["mod3.ts"], }, }) .to_string(), root_uri.join("deno.json").unwrap(), &ConfigParseOptions::default(), ) .unwrap(), ) .await; assert!( config.specifier_enabled_for_test(&root_uri.join("mod1.ts").unwrap()) ); assert!( !config.specifier_enabled_for_test(&root_uri.join("mod2.ts").unwrap()) ); assert!( !config.specifier_enabled_for_test(&root_uri.join("mod3.ts").unwrap()) ); config .tree .inject_config_file( ConfigFile::new( &json!({ "test": { "include": ["mod1.ts"], }, }) .to_string(), root_uri.join("deno.json").unwrap(), &ConfigParseOptions::default(), ) .unwrap(), ) .await; config .tree .inject_config_file( ConfigFile::new( &json!({ "test": { "exclude": ["mod2.ts"], "include": ["mod2.ts"], }, }) .to_string(), root_uri.join("deno.json").unwrap(), &ConfigParseOptions::default(), ) .unwrap(), ) .await; assert!( !config.specifier_enabled_for_test(&root_uri.join("mod1.ts").unwrap()) ); assert!( !config.specifier_enabled_for_test(&root_uri.join("mod2.ts").unwrap()) ); } fn root_dir() -> Url { if cfg!(windows) { Url::parse("file://C:/root/").unwrap() } else { Url::parse("file:///root/").unwrap() } } }