diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs index 5654c04a5928..67ee9d111997 100644 --- a/crates/load-cargo/src/lib.rs +++ b/crates/load-cargo/src/lib.rs @@ -94,7 +94,9 @@ pub fn load_workspace( let contents = loader.load_sync(path); let path = vfs::VfsPath::from(path.to_path_buf()); vfs.set_file_contents(path.clone(), contents); - vfs.file_id(&path) + vfs.file_id(&path).and_then(|(file_id, excluded)| { + (excluded == vfs::FileExcluded::No).then_some(file_id) + }) }, extra_env, ); diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 7656c07c9485..758f60dc3e43 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -84,10 +84,10 @@ config_data! { completion_snippets_custom: FxHashMap = Config::completion_snippets_default(), - /// These directories will be ignored by rust-analyzer. They are + /// These paths (file/directories) will be ignored by rust-analyzer. They are /// relative to the workspace root, and globs are not supported. You may /// also need to add the folders to Code's `files.watcherExclude`. - files_excludeDirs: Vec = vec![], + files_exclude | files_excludeDirs: Vec = vec![], @@ -1787,7 +1787,7 @@ impl Config { fn discovered_projects(&self) -> Vec { let exclude_dirs: Vec<_> = - self.files_excludeDirs().iter().map(|p| self.root_path.join(p)).collect(); + self.files_exclude().iter().map(|p| self.root_path.join(p)).collect(); let mut projects = vec![]; for fs_proj in &self.discovered_projects_from_filesystem { @@ -1909,10 +1909,14 @@ impl Config { } _ => FilesWatcher::Server, }, - exclude: self.files_excludeDirs().iter().map(|it| self.root_path.join(it)).collect(), + exclude: self.excluded().collect(), } } + pub fn excluded(&self) -> impl Iterator + use<'_> { + self.files_exclude().iter().map(|it| self.root_path.join(it)) + } + pub fn notifications(&self) -> NotificationsConfig { NotificationsConfig { cargo_toml_not_found: self.notifications_cargoTomlNotFound().to_owned(), diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs index b52f64aaacec..70105cda006b 100644 --- a/crates/rust-analyzer/src/global_state.rs +++ b/crates/rust-analyzer/src/global_state.rs @@ -650,7 +650,8 @@ impl GlobalStateSnapshot { RwLockReadGuard::map(self.vfs.read(), |(it, _)| it) } - pub(crate) fn url_to_file_id(&self, url: &Url) -> anyhow::Result { + /// Returns `None` if the file was excluded. + pub(crate) fn url_to_file_id(&self, url: &Url) -> anyhow::Result> { url_to_file_id(&self.vfs_read(), url) } @@ -658,7 +659,8 @@ impl GlobalStateSnapshot { file_id_to_url(&self.vfs_read(), id) } - pub(crate) fn vfs_path_to_file_id(&self, vfs_path: &VfsPath) -> anyhow::Result { + /// Returns `None` if the file was excluded. + pub(crate) fn vfs_path_to_file_id(&self, vfs_path: &VfsPath) -> anyhow::Result> { vfs_path_to_file_id(&self.vfs_read(), vfs_path) } @@ -750,14 +752,21 @@ pub(crate) fn file_id_to_url(vfs: &vfs::Vfs, id: FileId) -> Url { url_from_abs_path(path) } -pub(crate) fn url_to_file_id(vfs: &vfs::Vfs, url: &Url) -> anyhow::Result { +/// Returns `None` if the file was excluded. +pub(crate) fn url_to_file_id(vfs: &vfs::Vfs, url: &Url) -> anyhow::Result> { let path = from_proto::vfs_path(url)?; - let res = vfs.file_id(&path).ok_or_else(|| anyhow::format_err!("file not found: {path}"))?; - Ok(res) + vfs_path_to_file_id(vfs, &path) } -pub(crate) fn vfs_path_to_file_id(vfs: &vfs::Vfs, vfs_path: &VfsPath) -> anyhow::Result { - let res = +/// Returns `None` if the file was excluded. +pub(crate) fn vfs_path_to_file_id( + vfs: &vfs::Vfs, + vfs_path: &VfsPath, +) -> anyhow::Result> { + let (file_id, excluded) = vfs.file_id(vfs_path).ok_or_else(|| anyhow::format_err!("file not found: {vfs_path}"))?; - Ok(res) + match excluded { + vfs::FileExcluded::Yes => Ok(None), + vfs::FileExcluded::No => Ok(Some(file_id)), + } } diff --git a/crates/rust-analyzer/src/handlers/notification.rs b/crates/rust-analyzer/src/handlers/notification.rs index 48856d19e155..55344a4d6ac6 100644 --- a/crates/rust-analyzer/src/handlers/notification.rs +++ b/crates/rust-analyzer/src/handlers/notification.rs @@ -22,6 +22,7 @@ use crate::{ mem_docs::DocumentData, reload, target_spec::TargetSpec, + try_default, }; pub(crate) fn handle_cancel(state: &mut GlobalState, params: CancelParams) -> anyhow::Result<()> { @@ -74,6 +75,14 @@ pub(crate) fn handle_did_open_text_document( tracing::error!("duplicate DidOpenTextDocument: {}", path); } + if let Some(abs_path) = path.as_path() { + if state.config.excluded().any(|excluded| abs_path.starts_with(&excluded)) { + tracing::trace!("opened excluded file {abs_path}"); + state.vfs.write().0.insert_excluded_file(path); + return Ok(()); + } + } + let contents = params.text_document.text.into_bytes(); state.vfs.write().0.set_file_contents(path, Some(contents)); if state.config.discover_workspace_config().is_some() { @@ -127,7 +136,8 @@ pub(crate) fn handle_did_close_text_document( tracing::error!("orphan DidCloseTextDocument: {}", path); } - if let Some(file_id) = state.vfs.read().0.file_id(&path) { + // Clear diagnostics also for excluded files, just in case. + if let Some((file_id, _)) = state.vfs.read().0.file_id(&path) { state.diagnostics.clear_native_for(file_id); } @@ -146,7 +156,7 @@ pub(crate) fn handle_did_save_text_document( ) -> anyhow::Result<()> { if let Ok(vfs_path) = from_proto::vfs_path(¶ms.text_document.uri) { let snap = state.snapshot(); - let file_id = snap.vfs_path_to_file_id(&vfs_path)?; + let file_id = try_default!(snap.vfs_path_to_file_id(&vfs_path)?); let sr = snap.analysis.source_root_id(file_id)?; if state.config.script_rebuild_on_save(Some(sr)) && state.build_deps_changed { @@ -290,7 +300,7 @@ fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool { let _p = tracing::info_span!("run_flycheck").entered(); let file_id = state.vfs.read().0.file_id(&vfs_path); - if let Some(file_id) = file_id { + if let Some((file_id, vfs::FileExcluded::No)) = file_id { let world = state.snapshot(); let invocation_strategy_once = state.config.flycheck(None).invocation_strategy_once(); let may_flycheck_workspace = state.config.flycheck_workspace(None); diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index ed028f1d37b6..a3cb8c9be61f 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -53,6 +53,7 @@ use crate::{ }, target_spec::{CargoTargetSpec, TargetSpec}, test_runner::{CargoTestHandle, TestTarget}, + try_default, }; pub(crate) fn handle_workspace_reload(state: &mut GlobalState, _: ()) -> anyhow::Result<()> { @@ -83,7 +84,8 @@ pub(crate) fn handle_analyzer_status( let mut file_id = None; if let Some(tdi) = params.text_document { match from_proto::file_id(&snap, &tdi.uri) { - Ok(it) => file_id = Some(it), + Ok(Some(it)) => file_id = Some(it), + Ok(None) => {} Err(_) => format_to!(buf, "file {} not found in vfs", tdi.uri), } } @@ -141,7 +143,7 @@ pub(crate) fn handle_view_syntax_tree( params: lsp_ext::ViewSyntaxTreeParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_view_syntax_tree").entered(); - let id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let res = snap.analysis.view_syntax_tree(id)?; Ok(res) } @@ -151,7 +153,7 @@ pub(crate) fn handle_view_hir( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_view_hir").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let res = snap.analysis.view_hir(position)?; Ok(res) } @@ -161,7 +163,7 @@ pub(crate) fn handle_view_mir( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_view_mir").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let res = snap.analysis.view_mir(position)?; Ok(res) } @@ -171,7 +173,7 @@ pub(crate) fn handle_interpret_function( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_interpret_function").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let res = snap.analysis.interpret_function(position)?; Ok(res) } @@ -180,7 +182,7 @@ pub(crate) fn handle_view_file_text( snap: GlobalStateSnapshot, params: lsp_types::TextDocumentIdentifier, ) -> anyhow::Result { - let file_id = from_proto::file_id(&snap, ¶ms.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.uri)?); Ok(snap.analysis.file_text(file_id)?.to_string()) } @@ -189,7 +191,7 @@ pub(crate) fn handle_view_item_tree( params: lsp_ext::ViewItemTreeParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_view_item_tree").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let res = snap.analysis.view_item_tree(file_id)?; Ok(res) } @@ -315,7 +317,7 @@ pub(crate) fn handle_expand_macro( params: lsp_ext::ExpandMacroParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_expand_macro").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; let offset = from_proto::offset(&line_index, params.position)?; @@ -328,7 +330,7 @@ pub(crate) fn handle_selection_range( params: lsp_types::SelectionRangeParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_selection_range").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; let res: anyhow::Result> = params .positions @@ -371,7 +373,7 @@ pub(crate) fn handle_matching_brace( params: lsp_ext::MatchingBraceParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_matching_brace").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; params .positions @@ -395,7 +397,7 @@ pub(crate) fn handle_join_lines( ) -> anyhow::Result> { let _p = tracing::info_span!("handle_join_lines").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let config = snap.config.join_lines(); let line_index = snap.file_line_index(file_id)?; @@ -419,7 +421,7 @@ pub(crate) fn handle_on_enter( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_on_enter").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let edit = match snap.analysis.on_enter(position)? { None => return Ok(None), Some(it) => it, @@ -439,7 +441,8 @@ pub(crate) fn handle_on_type_formatting( return Ok(None); } - let mut position = from_proto::file_position(&snap, params.text_document_position)?; + let mut position = + try_default!(from_proto::file_position(&snap, params.text_document_position)?); let line_index = snap.file_line_index(position.file_id)?; // in `ide`, the `on_type` invariant is that @@ -465,32 +468,33 @@ pub(crate) fn handle_on_type_formatting( Ok(Some(change)) } +pub(crate) fn empty_diagnostic_report() -> lsp_types::DocumentDiagnosticReportResult { + lsp_types::DocumentDiagnosticReportResult::Report(lsp_types::DocumentDiagnosticReport::Full( + lsp_types::RelatedFullDocumentDiagnosticReport { + related_documents: None, + full_document_diagnostic_report: lsp_types::FullDocumentDiagnosticReport { + result_id: Some("rust-analyzer".to_owned()), + items: vec![], + }, + }, + )) +} + pub(crate) fn handle_document_diagnostics( snap: GlobalStateSnapshot, params: lsp_types::DocumentDiagnosticParams, ) -> anyhow::Result { - let empty = || { - lsp_types::DocumentDiagnosticReportResult::Report( - lsp_types::DocumentDiagnosticReport::Full( - lsp_types::RelatedFullDocumentDiagnosticReport { - related_documents: None, - full_document_diagnostic_report: lsp_types::FullDocumentDiagnosticReport { - result_id: Some("rust-analyzer".to_owned()), - items: vec![], - }, - }, - ), - ) + let file_id = match from_proto::file_id(&snap, ¶ms.text_document.uri)? { + Some(it) => it, + None => return Ok(empty_diagnostic_report()), }; - - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; let source_root = snap.analysis.source_root_id(file_id)?; if !snap.analysis.is_local_source_root(source_root)? { - return Ok(empty()); + return Ok(empty_diagnostic_report()); } let config = snap.config.diagnostics(Some(source_root)); if !config.enabled { - return Ok(empty()); + return Ok(empty_diagnostic_report()); } let line_index = snap.file_line_index(file_id)?; let supports_related = snap.config.text_document_diagnostic_related_document_support(); @@ -546,7 +550,7 @@ pub(crate) fn handle_document_symbol( params: lsp_types::DocumentSymbolParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_document_symbol").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; let mut parents: Vec<(lsp_types::DocumentSymbol, Option)> = Vec::new(); @@ -760,7 +764,7 @@ pub(crate) fn handle_will_rename_files( } }) .filter_map(|(file_id, new_name)| { - snap.analysis.will_rename_file(file_id, &new_name).ok()? + snap.analysis.will_rename_file(file_id?, &new_name).ok()? }) .collect(); @@ -782,7 +786,8 @@ pub(crate) fn handle_goto_definition( params: lsp_types::GotoDefinitionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_goto_definition").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let nav_info = match snap.analysis.goto_definition(position)? { None => return Ok(None), Some(it) => it, @@ -797,7 +802,10 @@ pub(crate) fn handle_goto_declaration( params: lsp_types::request::GotoDeclarationParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_goto_declaration").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params.clone())?; + let position = try_default!(from_proto::file_position( + &snap, + params.text_document_position_params.clone() + )?); let nav_info = match snap.analysis.goto_declaration(position)? { None => return handle_goto_definition(snap, params), Some(it) => it, @@ -812,7 +820,8 @@ pub(crate) fn handle_goto_implementation( params: lsp_types::request::GotoImplementationParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_goto_implementation").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let nav_info = match snap.analysis.goto_implementation(position)? { None => return Ok(None), Some(it) => it, @@ -827,7 +836,8 @@ pub(crate) fn handle_goto_type_definition( params: lsp_types::request::GotoTypeDefinitionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_goto_type_definition").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let nav_info = match snap.analysis.goto_type_definition(position)? { None => return Ok(None), Some(it) => it, @@ -880,7 +890,7 @@ pub(crate) fn handle_parent_module( } // check if invoked at the crate root - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let crate_id = match snap.analysis.crates_for(file_id)?.first() { Some(&crate_id) => crate_id, None => return Ok(None), @@ -904,7 +914,7 @@ pub(crate) fn handle_parent_module( } // locate parent module by semantics - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let navs = snap.analysis.parent_module(position)?; let res = to_proto::goto_definition_response(&snap, None, navs)?; Ok(Some(res)) @@ -915,7 +925,7 @@ pub(crate) fn handle_runnables( params: lsp_ext::RunnablesParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_runnables").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let source_root = snap.analysis.source_root_id(file_id).ok(); let line_index = snap.file_line_index(file_id)?; let offset = params.position.and_then(|it| from_proto::offset(&line_index, it).ok()); @@ -1035,7 +1045,7 @@ pub(crate) fn handle_related_tests( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_related_tests").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let tests = snap.analysis.related_tests(position, None)?; let mut res = Vec::new(); @@ -1053,7 +1063,8 @@ pub(crate) fn handle_completion( lsp_types::CompletionParams { text_document_position, context,.. }: lsp_types::CompletionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_completion").entered(); - let mut position = from_proto::file_position(&snap, text_document_position.clone())?; + let mut position = + try_default!(from_proto::file_position(&snap, text_document_position.clone())?); let line_index = snap.file_line_index(position.file_id)?; let completion_trigger_character = context.and_then(|ctx| ctx.trigger_character).and_then(|s| s.chars().next()); @@ -1102,7 +1113,8 @@ pub(crate) fn handle_completion_resolve( let resolve_data: lsp_ext::CompletionResolveData = serde_json::from_value(data)?; - let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)?; + let file_id = from_proto::file_id(&snap, &resolve_data.position.text_document.uri)? + .expect("we never provide completions for excluded files"); let line_index = snap.file_line_index(file_id)?; // FIXME: We should fix up the position when retrying the cancelled request instead let Ok(offset) = from_proto::offset(&line_index, resolve_data.position.position) else { @@ -1185,7 +1197,7 @@ pub(crate) fn handle_folding_range( params: FoldingRangeParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_folding_range").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let folds = snap.analysis.folding_ranges(file_id)?; let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; @@ -1202,7 +1214,8 @@ pub(crate) fn handle_signature_help( params: lsp_types::SignatureHelpParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_signature_help").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let help = match snap.analysis.signature_help(position)? { Some(it) => it, None => return Ok(None), @@ -1221,7 +1234,7 @@ pub(crate) fn handle_hover( PositionOrRange::Position(position) => Range::new(position, position), PositionOrRange::Range(range) => range, }; - let file_range = from_proto::file_range(&snap, ¶ms.text_document, range)?; + let file_range = try_default!(from_proto::file_range(&snap, ¶ms.text_document, range)?); let hover = snap.config.hover(); let info = match snap.analysis.hover(&hover, file_range)? { @@ -1255,7 +1268,7 @@ pub(crate) fn handle_prepare_rename( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_prepare_rename").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let change = snap.analysis.prepare_rename(position)?.map_err(to_proto::rename_error)?; @@ -1269,7 +1282,7 @@ pub(crate) fn handle_rename( params: RenameParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_rename").entered(); - let position = from_proto::file_position(&snap, params.text_document_position)?; + let position = try_default!(from_proto::file_position(&snap, params.text_document_position)?); let mut change = snap.analysis.rename(position, ¶ms.new_name)?.map_err(to_proto::rename_error)?; @@ -1304,7 +1317,7 @@ pub(crate) fn handle_references( params: lsp_types::ReferenceParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_references").entered(); - let position = from_proto::file_position(&snap, params.text_document_position)?; + let position = try_default!(from_proto::file_position(&snap, params.text_document_position)?); let exclude_imports = snap.config.find_all_refs_exclude_imports(); let exclude_tests = snap.config.find_all_refs_exclude_tests(); @@ -1375,9 +1388,9 @@ pub(crate) fn handle_code_action( return Ok(None); } - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; - let frange = from_proto::file_range(&snap, ¶ms.text_document, params.range)?; + let frange = try_default!(from_proto::file_range(&snap, ¶ms.text_document, params.range)?); let source_root = snap.analysis.source_root_id(file_id)?; let mut assists_config = snap.config.assist(Some(source_root)); @@ -1455,7 +1468,8 @@ pub(crate) fn handle_code_action_resolve( return Err(invalid_params_error("code action without data".to_owned()).into()); }; - let file_id = from_proto::file_id(&snap, ¶ms.code_action_params.text_document.uri)?; + let file_id = from_proto::file_id(&snap, ¶ms.code_action_params.text_document.uri)? + .expect("we never provide code actions for excluded files"); if snap.file_version(file_id) != params.version { return Err(invalid_params_error("stale code action".to_owned()).into()); } @@ -1551,7 +1565,7 @@ pub(crate) fn handle_code_lens( return Ok(Some(Vec::default())); } - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let target_spec = TargetSpec::for_file(&snap, file_id)?; let annotations = snap.analysis.annotations( @@ -1613,7 +1627,8 @@ pub(crate) fn handle_document_highlight( params: lsp_types::DocumentHighlightParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_document_highlight").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let line_index = snap.file_line_index(position.file_id)?; let source_root = snap.analysis.source_root_id(position.file_id)?; @@ -1639,12 +1654,12 @@ pub(crate) fn handle_ssr( params: lsp_ext::SsrParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_ssr").entered(); - let selections = params + let selections = try_default!(params .selections .iter() .map(|range| from_proto::file_range(&snap, ¶ms.position.text_document, *range)) - .collect::, _>>()?; - let position = from_proto::file_position(&snap, params.position)?; + .collect::>, _>>()?); + let position = try_default!(from_proto::file_position(&snap, params.position)?); let source_change = snap.analysis.structural_search_replace( ¶ms.query, params.parse_only, @@ -1660,11 +1675,11 @@ pub(crate) fn handle_inlay_hints( ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_inlay_hints").entered(); let document_uri = ¶ms.text_document.uri; - let FileRange { file_id, range } = from_proto::file_range( + let FileRange { file_id, range } = try_default!(from_proto::file_range( &snap, &TextDocumentIdentifier::new(document_uri.to_owned()), params.range, - )?; + )?); let line_index = snap.file_line_index(file_id)?; let range = TextRange::new( range.start().min(line_index.index.len()), @@ -1744,7 +1759,8 @@ pub(crate) fn handle_call_hierarchy_prepare( params: CallHierarchyPrepareParams, ) -> anyhow::Result>> { let _p = tracing::info_span!("handle_call_hierarchy_prepare").entered(); - let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let position = + try_default!(from_proto::file_position(&snap, params.text_document_position_params)?); let nav_info = match snap.analysis.call_hierarchy(position)? { None => return Ok(None), @@ -1769,7 +1785,7 @@ pub(crate) fn handle_call_hierarchy_incoming( let item = params.item; let doc = TextDocumentIdentifier::new(item.uri); - let frange = from_proto::file_range(&snap, &doc, item.selection_range)?; + let frange = try_default!(from_proto::file_range(&snap, &doc, item.selection_range)?); let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; let config = snap.config.call_hierarchy(); @@ -1807,7 +1823,7 @@ pub(crate) fn handle_call_hierarchy_outgoing( let item = params.item; let doc = TextDocumentIdentifier::new(item.uri); - let frange = from_proto::file_range(&snap, &doc, item.selection_range)?; + let frange = try_default!(from_proto::file_range(&snap, &doc, item.selection_range)?); let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; let line_index = snap.file_line_index(fpos.file_id)?; @@ -1842,7 +1858,7 @@ pub(crate) fn handle_semantic_tokens_full( ) -> anyhow::Result> { let _p = tracing::info_span!("handle_semantic_tokens_full").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; @@ -1872,7 +1888,7 @@ pub(crate) fn handle_semantic_tokens_full_delta( ) -> anyhow::Result> { let _p = tracing::info_span!("handle_semantic_tokens_full_delta").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let text = snap.analysis.file_text(file_id)?; let line_index = snap.file_line_index(file_id)?; @@ -1915,7 +1931,7 @@ pub(crate) fn handle_semantic_tokens_range( ) -> anyhow::Result> { let _p = tracing::info_span!("handle_semantic_tokens_range").entered(); - let frange = from_proto::file_range(&snap, ¶ms.text_document, params.range)?; + let frange = try_default!(from_proto::file_range(&snap, ¶ms.text_document, params.range)?); let text = snap.analysis.file_text(frange.file_id)?; let line_index = snap.file_line_index(frange.file_id)?; @@ -1940,7 +1956,7 @@ pub(crate) fn handle_open_docs( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result { let _p = tracing::info_span!("handle_open_docs").entered(); - let position = from_proto::file_position(&snap, params)?; + let position = try_default!(from_proto::file_position(&snap, params)?); let ws_and_sysroot = snap.workspaces.iter().find_map(|ws| match &ws.kind { ProjectWorkspaceKind::Cargo { cargo, .. } @@ -1982,7 +1998,7 @@ pub(crate) fn handle_open_cargo_toml( params: lsp_ext::OpenCargoTomlParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_open_cargo_toml").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let cargo_spec = match TargetSpec::for_file(&snap, file_id)? { Some(TargetSpec::Cargo(it)) => it, @@ -2000,8 +2016,8 @@ pub(crate) fn handle_move_item( params: lsp_ext::MoveItemParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_move_item").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; - let range = from_proto::file_range(&snap, ¶ms.text_document, params.range)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); + let range = try_default!(from_proto::file_range(&snap, ¶ms.text_document, params.range)?); let direction = match params.direction { lsp_ext::MoveItemDirection::Up => ide::Direction::Up, @@ -2022,7 +2038,7 @@ pub(crate) fn handle_view_recursive_memory_layout( params: lsp_types::TextDocumentPositionParams, ) -> anyhow::Result> { let _p = tracing::info_span!("handle_view_recursive_memory_layout").entered(); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let file_id = try_default!(from_proto::file_id(&snap, ¶ms.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; let offset = from_proto::offset(&line_index, params.position)?; @@ -2210,7 +2226,7 @@ fn run_rustfmt( text_document: TextDocumentIdentifier, range: Option, ) -> anyhow::Result>> { - let file_id = from_proto::file_id(snap, &text_document.uri)?; + let file_id = try_default!(from_proto::file_id(snap, &text_document.uri)?); let file = snap.analysis.file_text(file_id)?; // Determine the edition of the crate the file belongs to (if there's multiple, we pick the @@ -2275,7 +2291,7 @@ fn run_rustfmt( .into()); } - let frange = from_proto::file_range(snap, &text_document, range)?; + let frange = try_default!(from_proto::file_range(snap, &text_document, range)?); let start_line = line_index.index.line_col(frange.range.start()).line; let end_line = line_index.index.line_col(frange.range.end()).line; @@ -2416,15 +2432,15 @@ pub(crate) fn internal_testing_fetch_config( state: GlobalStateSnapshot, params: InternalTestingFetchConfigParams, ) -> anyhow::Result> { - let source_root = params - .text_document - .map(|it| { + let source_root = match params.text_document { + Some(it) => Some( state .analysis - .source_root_id(from_proto::file_id(&state, &it.uri)?) - .map_err(anyhow::Error::from) - }) - .transpose()?; + .source_root_id(try_default!(from_proto::file_id(&state, &it.uri)?)) + .map_err(anyhow::Error::from)?, + ), + None => None, + }; Ok(Some(match params.config { InternalTestingFetchConfigOption::AssistEmitMustUse => { InternalTestingFetchConfigResponse::AssistEmitMustUse( diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs index 5cdc51a1c199..c6aa8ba17077 100644 --- a/crates/rust-analyzer/src/integrated_benchmarks.rs +++ b/crates/rust-analyzer/src/integrated_benchmarks.rs @@ -25,6 +25,14 @@ use vfs::{AbsPathBuf, VfsPath}; use load_cargo::{load_workspace_at, LoadCargoConfig, ProcMacroServerChoice}; +#[track_caller] +fn file_id(vfs: &vfs::Vfs, path: &VfsPath) -> vfs::FileId { + match vfs.file_id(path) { + Some((file_id, vfs::FileExcluded::No)) => file_id, + None | Some((_, vfs::FileExcluded::Yes)) => panic!("can't find virtual file for {path}"), + } +} + #[test] fn integrated_highlighting_benchmark() { if std::env::var("RUN_SLOW_BENCHES").is_err() { @@ -62,7 +70,7 @@ fn integrated_highlighting_benchmark() { let file_id = { let file = workspace_to_load.join(file); let path = VfsPath::from(AbsPathBuf::assert(file)); - vfs.file_id(&path).unwrap_or_else(|| panic!("can't find virtual file for {path}")) + file_id(&vfs, &path) }; { @@ -130,7 +138,7 @@ fn integrated_completion_benchmark() { let file_id = { let file = workspace_to_load.join(file); let path = VfsPath::from(AbsPathBuf::assert(file)); - vfs.file_id(&path).unwrap_or_else(|| panic!("can't find virtual file for {path}")) + file_id(&vfs, &path) }; // kick off parsing and index population @@ -324,7 +332,7 @@ fn integrated_diagnostics_benchmark() { let file_id = { let file = workspace_to_load.join(file); let path = VfsPath::from(AbsPathBuf::assert(file)); - vfs.file_id(&path).unwrap_or_else(|| panic!("can't find virtual file for {path}")) + file_id(&vfs, &path) }; let diagnostics_config = DiagnosticsConfig { diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 1221f7c7012e..27d6225cdb7e 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -173,3 +173,14 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; hasher.finalize() } + +#[doc(hidden)] +macro_rules! try_default_ { + ($it:expr $(,)?) => { + match $it { + Some(it) => it, + None => return Ok(Default::default()), + } + }; +} +pub(crate) use try_default_ as try_default; diff --git a/crates/rust-analyzer/src/lsp/from_proto.rs b/crates/rust-analyzer/src/lsp/from_proto.rs index 47e9961cf13f..6375a1a054b7 100644 --- a/crates/rust-analyzer/src/lsp/from_proto.rs +++ b/crates/rust-analyzer/src/lsp/from_proto.rs @@ -9,7 +9,7 @@ use vfs::AbsPathBuf; use crate::{ global_state::GlobalStateSnapshot, line_index::{LineIndex, PositionEncoding}, - lsp_ext, + lsp_ext, try_default, }; pub(crate) fn abs_path(url: &lsp_types::Url) -> anyhow::Result { @@ -61,37 +61,44 @@ pub(crate) fn text_range( } } -pub(crate) fn file_id(snap: &GlobalStateSnapshot, url: &lsp_types::Url) -> anyhow::Result { +/// Returns `None` if the file was excluded. +pub(crate) fn file_id( + snap: &GlobalStateSnapshot, + url: &lsp_types::Url, +) -> anyhow::Result> { snap.url_to_file_id(url) } +/// Returns `None` if the file was excluded. pub(crate) fn file_position( snap: &GlobalStateSnapshot, tdpp: lsp_types::TextDocumentPositionParams, -) -> anyhow::Result { - let file_id = file_id(snap, &tdpp.text_document.uri)?; +) -> anyhow::Result> { + let file_id = try_default!(file_id(snap, &tdpp.text_document.uri)?); let line_index = snap.file_line_index(file_id)?; let offset = offset(&line_index, tdpp.position)?; - Ok(FilePosition { file_id, offset }) + Ok(Some(FilePosition { file_id, offset })) } +/// Returns `None` if the file was excluded. pub(crate) fn file_range( snap: &GlobalStateSnapshot, text_document_identifier: &lsp_types::TextDocumentIdentifier, range: lsp_types::Range, -) -> anyhow::Result { +) -> anyhow::Result> { file_range_uri(snap, &text_document_identifier.uri, range) } +/// Returns `None` if the file was excluded. pub(crate) fn file_range_uri( snap: &GlobalStateSnapshot, document: &lsp_types::Url, range: lsp_types::Range, -) -> anyhow::Result { - let file_id = file_id(snap, document)?; +) -> anyhow::Result> { + let file_id = try_default!(file_id(snap, document)?); let line_index = snap.file_line_index(file_id)?; let range = text_range(&line_index, range)?; - Ok(FileRange { file_id, range }) + Ok(Some(FileRange { file_id, range })) } pub(crate) fn assist_kind(kind: lsp_types::CodeActionKind) -> Option { @@ -108,6 +115,7 @@ pub(crate) fn assist_kind(kind: lsp_types::CodeActionKind) -> Option Some(assist_kind) } +/// Returns `None` if the file was excluded. pub(crate) fn annotation( snap: &GlobalStateSnapshot, range: lsp_types::Range, @@ -121,7 +129,7 @@ pub(crate) fn annotation( return Ok(None); } let pos @ FilePosition { file_id, .. } = - file_position(snap, params.text_document_position_params)?; + try_default!(file_position(snap, params.text_document_position_params)?); let line_index = snap.file_line_index(file_id)?; Ok(Annotation { @@ -133,7 +141,7 @@ pub(crate) fn annotation( if snap.url_file_version(¶ms.text_document.uri) != Some(data.version) { return Ok(None); } - let pos @ FilePosition { file_id, .. } = file_position(snap, params)?; + let pos @ FilePosition { file_id, .. } = try_default!(file_position(snap, params)?); let line_index = snap.file_line_index(file_id)?; Ok(Annotation { diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index ebc65373b522..f5d9469f2622 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -27,7 +27,10 @@ use crate::{ FetchWorkspaceResponse, GlobalState, }, hack_recover_crate_name, - handlers::dispatch::{NotificationDispatcher, RequestDispatcher}, + handlers::{ + dispatch::{NotificationDispatcher, RequestDispatcher}, + request::empty_diagnostic_report, + }, lsp::{ from_proto, to_proto, utils::{notification_is, Progress}, @@ -548,6 +551,9 @@ impl GlobalState { self.mem_docs .iter() .map(|path| vfs.file_id(path).unwrap()) + .filter_map(|(file_id, excluded)| { + (excluded == vfs::FileExcluded::No).then_some(file_id) + }) .filter(|&file_id| { let source_root = db.file_source_root(file_id); // Only publish diagnostics for files in the workspace, not from crates.io deps @@ -632,6 +638,9 @@ impl GlobalState { .mem_docs .iter() .map(|path| self.vfs.read().0.file_id(path).unwrap()) + .filter_map(|(file_id, excluded)| { + (excluded == vfs::FileExcluded::No).then_some(file_id) + }) .filter(|&file_id| { let source_root = db.file_source_root(file_id); !db.source_root(source_root).is_library @@ -879,7 +888,10 @@ impl GlobalState { self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| { let _p = tracing::info_span!("GlobalState::check_if_indexed").entered(); tracing::debug!(?uri, "handling uri"); - let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId"); + let Some(id) = from_proto::file_id(&snap, &uri).expect("unable to get FileId") + else { + return; + }; if let Ok(crates) = &snap.analysis.crates_for(id) { if crates.is_empty() { if snap.config.discover_workspace_config().is_some() { @@ -987,13 +999,14 @@ impl GlobalState { ); for diag in diagnostics { match url_to_file_id(&self.vfs.read().0, &diag.url) { - Ok(file_id) => self.diagnostics.add_check_diagnostic( + Ok(Some(file_id)) => self.diagnostics.add_check_diagnostic( id, &package_id, file_id, diag.diagnostic, diag.fix, ), + Ok(None) => {} Err(err) => { error!( "flycheck {id}: File with cargo diagnostic not found in VFS: {}", @@ -1115,17 +1128,7 @@ impl GlobalState { .on_latency_sensitive::(handlers::handle_semantic_tokens_range) // FIXME: Some of these NO_RETRY could be retries if the file they are interested didn't change. // All other request handlers - .on_with_vfs_default::(handlers::handle_document_diagnostics, || lsp_types::DocumentDiagnosticReportResult::Report( - lsp_types::DocumentDiagnosticReport::Full( - lsp_types::RelatedFullDocumentDiagnosticReport { - related_documents: None, - full_document_diagnostic_report: lsp_types::FullDocumentDiagnosticReport { - result_id: Some("rust-analyzer".to_owned()), - items: vec![], - }, - }, - ), - ), || lsp_server::ResponseError { + .on_with_vfs_default::(handlers::handle_document_diagnostics, empty_diagnostic_report, || lsp_server::ResponseError { code: lsp_server::ErrorCode::ServerCancelled as i32, message: "server cancelled the request".to_owned(), data: serde_json::to_value(lsp_types::DiagnosticServerCancellationData { diff --git a/crates/rust-analyzer/src/reload.rs b/crates/rust-analyzer/src/reload.rs index d18e57704774..e3c003dbf8b1 100644 --- a/crates/rust-analyzer/src/reload.rs +++ b/crates/rust-analyzer/src/reload.rs @@ -705,7 +705,9 @@ impl GlobalState { let load = |path: &AbsPath| { let vfs_path = vfs::VfsPath::from(path.to_path_buf()); self.crate_graph_file_dependencies.insert(vfs_path.clone()); - vfs.file_id(&vfs_path) + vfs.file_id(&vfs_path).and_then(|(file_id, excluded)| { + (excluded == vfs::FileExcluded::No).then_some(file_id) + }) }; ws_to_crate_graph(&self.workspaces, self.config.extra_env(None), load) diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs index 2b3c0a47a220..5ad28d0b909e 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -1372,6 +1372,40 @@ pub fn foo() {} name = "bar" version = "0.0.0" +[dependencies] +foo = { path = "../foo" } + +//- /bar/src/lib.rs +"#, + ) + .root("foo") + .root("bar") + .root("baz") + .with_config(json!({ + "files": { + "exclude": ["foo"] + } + })) + .server() + .wait_until_workspace_is_loaded(); + + server.request::(Default::default(), json!([])); + + let server = Project::with_fixture( + r#" +//- /foo/Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /foo/src/lib.rs +pub fn foo() {} + +//- /bar/Cargo.toml +[package] +name = "bar" +version = "0.0.0" + //- /bar/src/lib.rs pub fn bar() {} @@ -1388,7 +1422,7 @@ version = "0.0.0" .root("baz") .with_config(json!({ "files": { - "excludeDirs": ["foo", "bar"] + "exclude": ["foo", "bar"] } })) .server() diff --git a/crates/vfs-notify/src/lib.rs b/crates/vfs-notify/src/lib.rs index 0ae8b7baf464..320033417640 100644 --- a/crates/vfs-notify/src/lib.rs +++ b/crates/vfs-notify/src/lib.rs @@ -280,8 +280,9 @@ impl NotifyActor { return false; } - root == path - || dirs.exclude.iter().chain(&dirs.include).all(|it| it != path) + // We want to filter out subdirectories that are roots themselves, because they will be visited separately. + dirs.exclude.iter().all(|it| it != path) + && (root == path || dirs.include.iter().all(|it| it != path)) }); let files = walkdir.filter_map(|it| it.ok()).filter_map(|entry| { diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index a26444e9ea23..3feca512e55a 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -100,6 +100,9 @@ pub enum FileState { Exists(u64), /// The file is deleted. Deleted, + /// The file was specifically excluded by the user. We still include excluded files + /// when they're opened (without their contents). + Excluded, } /// Changed file in the [`Vfs`]. @@ -164,10 +167,22 @@ pub enum ChangeKind { Delete, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileExcluded { + Yes, + No, +} + impl Vfs { /// Id of the given path if it exists in the `Vfs` and is not deleted. - pub fn file_id(&self, path: &VfsPath) -> Option { - self.interner.get(path).filter(|&it| matches!(self.get(it), FileState::Exists(_))) + pub fn file_id(&self, path: &VfsPath) -> Option<(FileId, FileExcluded)> { + let file_id = self.interner.get(path)?; + let file_state = self.get(file_id); + match file_state { + FileState::Exists(_) => Some((file_id, FileExcluded::No)), + FileState::Deleted => None, + FileState::Excluded => Some((file_id, FileExcluded::Yes)), + } } /// File path corresponding to the given `file_id`. @@ -216,6 +231,7 @@ impl Vfs { } Change::Modify(v, new_hash) } + (FileState::Excluded, _) => return false, }; let mut set_data = |change_kind| { @@ -297,6 +313,13 @@ impl Vfs { fn get(&self, file_id: FileId) -> FileState { self.data[file_id.0 as usize] } + + /// We cannot ignore excluded files, because this will lead to errors when the client + /// requests semantic information for them, so we instead mark them specially. + pub fn insert_excluded_file(&mut self, path: VfsPath) { + let file_id = self.alloc_file_id(path); + self.data[file_id.0 as usize] = FileState::Excluded; + } } impl fmt::Debug for Vfs { diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md index 49eb7248898e..a7f54289b964 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -470,9 +470,9 @@ The warnings will be indicated by a blue squiggly underline in code and a blue icon in the `Problems Panel`. -**rust-analyzer.files.excludeDirs** (default: []) +**rust-analyzer.files.exclude** (default: []) - These directories will be ignored by rust-analyzer. They are + These paths (file/directories) will be ignored by rust-analyzer. They are relative to the workspace root, and globs are not supported. You may also need to add the folders to Code's `files.watcherExclude`. diff --git a/editors/code/package.json b/editors/code/package.json index 57f4254b6899..5db8ca52a587 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -1473,8 +1473,8 @@ { "title": "files", "properties": { - "rust-analyzer.files.excludeDirs": { - "markdownDescription": "These directories will be ignored by rust-analyzer. They are\nrelative to the workspace root, and globs are not supported. You may\nalso need to add the folders to Code's `files.watcherExclude`.", + "rust-analyzer.files.exclude": { + "markdownDescription": "These paths (file/directories) will be ignored by rust-analyzer. They are\nrelative to the workspace root, and globs are not supported. You may\nalso need to add the folders to Code's `files.watcherExclude`.", "default": [], "type": "array", "items": {