use anyhow::{Result, anyhow}; use collections::{HashMap, HashSet}; use futures::channel::mpsc; use futures::lock::Mutex; use futures::{FutureExt as _, StreamExt, future}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::{Buffer, BufferEvent}; use postage::stream::Stream as _; use project::buffer_store::{BufferStore, BufferStoreEvent}; use project::worktree_store::{WorktreeStore, WorktreeStoreEvent}; use project::{PathChange, Project, ProjectEntryId, ProjectPath}; use slotmap::SlotMap; use std::iter; use std::ops::{DerefMut, Range}; use std::sync::Arc; use text::BufferId; use util::{RangeExt as _, debug_panic, some_or_debug_panic}; use crate::CachedDeclarationPath; use crate::declaration::{ BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier, }; use crate::outline::declarations_in_buffer; // TODO // // * Also queue / debounce buffer changes. A challenge for this is that use of // `buffer_declarations_containing_range` assumes that the index is always immediately up to date. // // * Add a per language configuration for skipping indexing. // // * Handle tsx / ts / js referencing each-other // Potential future improvements: // // * Prevent indexing of a large file from blocking the queue. // // * Send multiple selected excerpt ranges. Challenge is that excerpt ranges influence which // references are present and their scores. // // * Include single-file worktrees / non visible worktrees? E.g. go to definition that resolves to a // file in a build dependency. Should not be editable in that case - but how to distinguish the case // where it should be editable? // Potential future optimizations: // // * Index files on multiple threads in Zed (currently only parallel for the CLI). Adding some kind // of priority system to the background executor could help - it's single threaded for now to avoid // interfering with other work. // // * Parse files directly instead of loading into a Rope. // // - This would allow the task handling dirty_files to be done entirely on the background executor. // // - Make SyntaxMap generic to handle embedded languages? Will also need to find line boundaries, // but that can be done by scanning characters in the flat representation. // // * Use something similar to slotmap without key versions. // // * Concurrent slotmap pub struct SyntaxIndex { state: Arc>, project: WeakEntity, initial_file_indexing_done_rx: postage::watch::Receiver, _file_indexing_task: Option>, } pub struct SyntaxIndexState { declarations: SlotMap, identifiers: HashMap>, files: HashMap, buffers: HashMap, dirty_files: HashMap, dirty_files_tx: mpsc::Sender<()>, } #[derive(Debug, Default)] struct FileState { declarations: Vec, } #[derive(Default)] struct BufferState { declarations: Vec, task: Option>, } impl SyntaxIndex { pub fn new( project: &Entity, file_indexing_parallelism: usize, cx: &mut Context, ) -> Self { assert!(file_indexing_parallelism > 0); let (dirty_files_tx, mut dirty_files_rx) = mpsc::channel::<()>(1); let (mut initial_file_indexing_done_tx, initial_file_indexing_done_rx) = postage::watch::channel(); let initial_state = SyntaxIndexState { declarations: SlotMap::default(), identifiers: HashMap::default(), files: HashMap::default(), buffers: HashMap::default(), dirty_files: HashMap::default(), dirty_files_tx, }; let mut this = Self { project: project.downgrade(), state: Arc::new(Mutex::new(initial_state)), initial_file_indexing_done_rx, _file_indexing_task: None, }; let worktree_store = project.read(cx).worktree_store(); let initial_worktree_snapshots = worktree_store .read(cx) .worktrees() .map(|w| w.read(cx).snapshot()) .collect::>(); this._file_indexing_task = Some(cx.spawn(async move |this, cx| { let snapshots_file_count = initial_worktree_snapshots .iter() .map(|worktree| worktree.file_count()) .sum::(); if snapshots_file_count > 0 { let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism); let chunk_count = snapshots_file_count.div_ceil(chunk_size); let file_chunks = initial_worktree_snapshots .iter() .flat_map(|worktree| { let worktree_id = worktree.id(); worktree.files(false, 0).map(move |entry| { ( entry.id, ProjectPath { worktree_id, path: entry.path.clone(), }, ) }) }) .chunks(chunk_size); let mut tasks = Vec::with_capacity(chunk_count); for chunk in file_chunks.into_iter() { tasks.push(Self::update_dirty_files( &this, chunk.into_iter().collect(), cx.clone(), )); } futures::future::join_all(tasks).await; log::info!("Finished initial file indexing"); } *initial_file_indexing_done_tx.borrow_mut() = true; let Ok(state) = this.read_with(cx, |this, _cx| Arc::downgrade(&this.state)) else { return; }; while dirty_files_rx.next().await.is_some() { let Some(state) = state.upgrade() else { return; }; let mut state = state.lock().await; let was_underused = state.dirty_files.capacity() > 255 && state.dirty_files.len() * 8 < state.dirty_files.capacity(); let dirty_files = state.dirty_files.drain().collect::>(); if was_underused { state.dirty_files.shrink_to_fit(); } drop(state); if dirty_files.is_empty() { continue; } let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism); let chunk_count = dirty_files.len().div_ceil(chunk_size); let mut tasks = Vec::with_capacity(chunk_count); let chunks = dirty_files.into_iter().chunks(chunk_size); for chunk in chunks.into_iter() { tasks.push(Self::update_dirty_files( &this, chunk.into_iter().collect(), cx.clone(), )); } futures::future::join_all(tasks).await; } })); cx.subscribe(&worktree_store, Self::handle_worktree_store_event) .detach(); let buffer_store = project.read(cx).buffer_store().clone(); for buffer in buffer_store.read(cx).buffers().collect::>() { this.register_buffer(&buffer, cx); } cx.subscribe(&buffer_store, Self::handle_buffer_store_event) .detach(); this } async fn update_dirty_files( this: &WeakEntity, dirty_files: Vec<(ProjectEntryId, ProjectPath)>, mut cx: AsyncApp, ) { for (entry_id, project_path) in dirty_files { let Ok(task) = this.update(&mut cx, |this, cx| { this.update_file(entry_id, project_path, cx) }) else { return; }; task.await; } } pub fn wait_for_initial_file_indexing(&self, cx: &App) -> Task> { if *self.initial_file_indexing_done_rx.borrow() { Task::ready(Ok(())) } else { let mut rx = self.initial_file_indexing_done_rx.clone(); cx.background_spawn(async move { loop { match rx.recv().await { Some(true) => return Ok(()), Some(false) => {} None => { return Err(anyhow!( "SyntaxIndex dropped while waiting for initial file indexing" )); } } } }) } } pub fn indexed_file_paths(&self, cx: &App) -> Task> { let state = self.state.clone(); let project = self.project.clone(); cx.spawn(async move |cx| { let state = state.lock().await; let Some(project) = project.upgrade() else { return vec![]; }; project .read_with(cx, |project, cx| { state .files .keys() .filter_map(|entry_id| project.path_for_entry(*entry_id, cx)) .collect() }) .unwrap_or_default() }) } fn handle_worktree_store_event( &mut self, _worktree_store: Entity, event: &WorktreeStoreEvent, cx: &mut Context, ) { use WorktreeStoreEvent::*; match event { WorktreeUpdatedEntries(worktree_id, updated_entries_set) => { let state = Arc::downgrade(&self.state); let worktree_id = *worktree_id; let updated_entries_set = updated_entries_set.clone(); cx.background_spawn(async move { let Some(state) = state.upgrade() else { return }; let mut state = state.lock().await; for (path, entry_id, path_change) in updated_entries_set.iter() { if let PathChange::Removed = path_change { state.files.remove(entry_id); state.dirty_files.remove(entry_id); } else { let project_path = ProjectPath { worktree_id, path: path.clone(), }; state.dirty_files.insert(*entry_id, project_path); } } match state.dirty_files_tx.try_send(()) { Err(err) if err.is_disconnected() => { log::error!("bug: syntax indexing queue is disconnected"); } _ => {} } }) .detach(); } WorktreeDeletedEntry(_worktree_id, project_entry_id) => { let project_entry_id = *project_entry_id; self.with_state(cx, move |state| { state.files.remove(&project_entry_id); }) } _ => {} } } fn handle_buffer_store_event( &mut self, _buffer_store: Entity, event: &BufferStoreEvent, cx: &mut Context, ) { use BufferStoreEvent::*; match event { BufferAdded(buffer) => self.register_buffer(buffer, cx), BufferOpened { .. } | BufferChangedFilePath { .. } | BufferDropped { .. } | SharedBufferClosed { .. } => {} } } pub fn state(&self) -> &Arc> { &self.state } fn with_state(&self, cx: &mut App, f: impl FnOnce(&mut SyntaxIndexState) + Send + 'static) { if let Some(mut state) = self.state.try_lock() { f(&mut state); return; } let state = Arc::downgrade(&self.state); cx.background_spawn(async move { let Some(state) = state.upgrade() else { return; }; let mut state = state.lock().await; f(&mut state) }) .detach(); } fn register_buffer(&self, buffer: &Entity, cx: &mut Context) { let buffer_id = buffer.read(cx).remote_id(); cx.observe_release(buffer, move |this, _buffer, cx| { this.with_state(cx, move |state| { if let Some(buffer_state) = state.buffers.remove(&buffer_id) { SyntaxIndexState::remove_buffer_declarations( &buffer_state.declarations, &mut state.declarations, &mut state.identifiers, ); } }) }) .detach(); cx.subscribe(buffer, Self::handle_buffer_event).detach(); self.update_buffer(buffer.clone(), cx); } fn handle_buffer_event( &mut self, buffer: Entity, event: &BufferEvent, cx: &mut Context, ) { match event { BufferEvent::Edited | // paths are cached and so should be updated BufferEvent::FileHandleChanged => self.update_buffer(buffer, cx), _ => {} } } fn update_buffer(&self, buffer_entity: Entity, cx: &mut Context) { let buffer = buffer_entity.read(cx); if buffer.language().is_none() { return; } let Some((project_entry_id, cached_path)) = project::File::from_dyn(buffer.file()) .and_then(|f| { let project_entry_id = f.project_entry_id()?; let cached_path = CachedDeclarationPath::new( f.worktree.read(cx).abs_path(), &f.path, buffer.language(), ); Some((project_entry_id, cached_path)) }) else { return; }; let buffer_id = buffer.remote_id(); let mut parse_status = buffer.parse_status(); let snapshot_task = cx.spawn({ let weak_buffer = buffer_entity.downgrade(); async move |_, cx| { while *parse_status.borrow() != language::ParseStatus::Idle { parse_status.changed().await?; } weak_buffer.read_with(cx, |buffer, _cx| buffer.snapshot()) } }); let state = Arc::downgrade(&self.state); let task = cx.background_spawn(async move { // TODO: How to handle errors? let Ok(snapshot) = snapshot_task.await else { return; }; let rope = snapshot.text.as_rope(); let declarations = declarations_in_buffer(&snapshot) .into_iter() .map(|item| { ( item.parent_index, BufferDeclaration::from_outline(item, &rope), ) }) .collect::>(); let Some(state) = state.upgrade() else { return; }; let mut state = state.lock().await; let state = state.deref_mut(); let buffer_state = state .buffers .entry(buffer_id) .or_insert_with(Default::default); SyntaxIndexState::remove_buffer_declarations( &buffer_state.declarations, &mut state.declarations, &mut state.identifiers, ); let mut new_ids = Vec::with_capacity(declarations.len()); state.declarations.reserve(declarations.len()); for (parent_index, mut declaration) in declarations { declaration.parent = parent_index.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied())); let identifier = declaration.identifier.clone(); let declaration_id = state.declarations.insert(Declaration::Buffer { rope: rope.clone(), buffer_id, declaration, project_entry_id, cached_path: cached_path.clone(), }); new_ids.push(declaration_id); state .identifiers .entry(identifier) .or_default() .insert(declaration_id); } buffer_state.declarations = new_ids; }); self.with_state(cx, move |state| { state .buffers .entry(buffer_id) .or_insert_with(Default::default) .task = Some(task) }); } fn update_file( &mut self, entry_id: ProjectEntryId, project_path: ProjectPath, cx: &mut Context, ) -> Task<()> { let Some(project) = self.project.upgrade() else { return Task::ready(()); }; let project = project.read(cx); let language_registry = project.languages(); let Some(available_language) = language_registry.language_for_file_path(project_path.path.as_std_path()) else { return Task::ready(()); }; let language = if let Some(Ok(Ok(language))) = language_registry .load_language(&available_language) .now_or_never() { if language .grammar() .is_none_or(|grammar| grammar.outline_config.is_none()) { return Task::ready(()); } future::Either::Left(async { Ok(language) }) } else { let language_registry = language_registry.clone(); future::Either::Right(async move { anyhow::Ok( language_registry .load_language(&available_language) .await??, ) }) }; let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) else { return Task::ready(()); }; let snapshot_task = worktree.update(cx, |worktree, cx| { let load_task = worktree.load_file(&project_path.path, cx); let worktree_abs_path = worktree.abs_path(); cx.spawn(async move |_this, cx| { let loaded_file = load_task.await?; let language = language.await?; let buffer = cx.new(|cx| { let mut buffer = Buffer::local(loaded_file.text, cx); buffer.set_language(Some(language.clone()), cx); buffer })?; let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?; while *parse_status.borrow() != language::ParseStatus::Idle { parse_status.changed().await?; } let cached_path = CachedDeclarationPath::new( worktree_abs_path, &project_path.path, Some(&language), ); let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; anyhow::Ok((snapshot, cached_path)) }) }); let state = Arc::downgrade(&self.state); cx.background_spawn(async move { // TODO: How to handle errors? let Ok((snapshot, cached_path)) = snapshot_task.await else { return; }; let rope = snapshot.as_rope(); let declarations = declarations_in_buffer(&snapshot) .into_iter() .map(|item| (item.parent_index, FileDeclaration::from_outline(item, rope))) .collect::>(); let Some(state) = state.upgrade() else { return; }; let mut state = state.lock().await; let state = state.deref_mut(); let file_state = state.files.entry(entry_id).or_insert_with(Default::default); for old_declaration_id in &file_state.declarations { let Some(declaration) = state.declarations.remove(*old_declaration_id) else { debug_panic!("declaration not found"); continue; }; if let Some(identifier_declarations) = state.identifiers.get_mut(declaration.identifier()) { identifier_declarations.remove(old_declaration_id); } } let mut new_ids = Vec::with_capacity(declarations.len()); state.declarations.reserve(declarations.len()); for (parent_index, mut declaration) in declarations { declaration.parent = parent_index.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied())); let identifier = declaration.identifier.clone(); let declaration_id = state.declarations.insert(Declaration::File { project_entry_id: entry_id, declaration, cached_path: cached_path.clone(), }); new_ids.push(declaration_id); state .identifiers .entry(identifier) .or_default() .insert(declaration_id); } file_state.declarations = new_ids; }) } } impl SyntaxIndexState { pub fn declaration(&self, id: DeclarationId) -> Option<&Declaration> { self.declarations.get(id) } /// Returns declarations for the identifier. If the limit is exceeded, returns an empty vector. /// /// TODO: Consider doing some pre-ranking and instead truncating when N is exceeded. pub fn declarations_for_identifier( &self, identifier: &Identifier, ) -> Vec<(DeclarationId, &Declaration)> { // make sure to not have a large stack allocation assert!(N < 32); let Some(declaration_ids) = self.identifiers.get(&identifier) else { return vec![]; }; let mut result = Vec::with_capacity(N); let mut included_buffer_entry_ids = arrayvec::ArrayVec::<_, N>::new(); let mut file_declarations = Vec::new(); for declaration_id in declaration_ids { let declaration = self.declarations.get(*declaration_id); let Some(declaration) = some_or_debug_panic(declaration) else { continue; }; match declaration { Declaration::Buffer { project_entry_id, .. } => { included_buffer_entry_ids.push(*project_entry_id); result.push((*declaration_id, declaration)); if result.len() == N { return Vec::new(); } } Declaration::File { project_entry_id, .. } => { if !included_buffer_entry_ids.contains(&project_entry_id) { file_declarations.push((*declaration_id, declaration)); } } } } for (declaration_id, declaration) in file_declarations { match declaration { Declaration::File { project_entry_id, .. } => { if !included_buffer_entry_ids.contains(&project_entry_id) { result.push((declaration_id, declaration)); if result.len() == N { return Vec::new(); } } } Declaration::Buffer { .. } => {} } } result } pub fn buffer_declarations_containing_range( &self, buffer_id: BufferId, range: Range, ) -> impl Iterator { let Some(buffer_state) = self.buffers.get(&buffer_id) else { return itertools::Either::Left(iter::empty()); }; let iter = buffer_state .declarations .iter() .filter_map(move |declaration_id| { let Some(declaration) = self .declarations .get(*declaration_id) .and_then(|d| d.as_buffer()) else { log::error!("bug: missing buffer outline declaration"); return None; }; if declaration.item_range.contains_inclusive(&range) { return Some((*declaration_id, declaration)); } return None; }); itertools::Either::Right(iter) } pub fn file_declaration_count(&self, declaration: &Declaration) -> usize { match declaration { Declaration::File { project_entry_id, .. } => self .files .get(project_entry_id) .map(|file_state| file_state.declarations.len()) .unwrap_or_default(), Declaration::Buffer { buffer_id, .. } => self .buffers .get(buffer_id) .map(|buffer_state| buffer_state.declarations.len()) .unwrap_or_default(), } } fn remove_buffer_declarations( old_declaration_ids: &[DeclarationId], declarations: &mut SlotMap, identifiers: &mut HashMap>, ) { for old_declaration_id in old_declaration_ids { let Some(declaration) = declarations.remove(*old_declaration_id) else { debug_panic!("declaration not found"); continue; }; if let Some(identifier_declarations) = identifiers.get_mut(declaration.identifier()) { identifier_declarations.remove(old_declaration_id); } } } } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; use gpui::TestAppContext; use indoc::indoc; use language::{Language, LanguageConfig, LanguageId, LanguageMatcher, tree_sitter_rust}; use project::{FakeFs, Project}; use serde_json::json; use settings::SettingsStore; use text::OffsetRangeExt as _; use util::{path, rel_path::rel_path}; use crate::syntax_index::SyntaxIndex; #[gpui::test] async fn test_unopen_indexed_files(cx: &mut TestAppContext) { let (project, index, rust_lang_id) = init_test(cx).await; let main = Identifier { name: "main".into(), language_id: rust_lang_id, }; let index_state = index.read_with(cx, |index, _cx| index.state().clone()); let index_state = index_state.lock().await; cx.update(|cx| { let decls = index_state.declarations_for_identifier::<8>(&main); assert_eq!(decls.len(), 2); let decl = expect_file_decl("a.rs", &decls[0].1, &project, cx); assert_eq!(decl.identifier, main); assert_eq!(decl.item_range, 0..98); let decl = expect_file_decl("c.rs", &decls[1].1, &project, cx); assert_eq!(decl.identifier, main.clone()); assert_eq!(decl.item_range, 32..280); }); } #[gpui::test] async fn test_parents_in_file(cx: &mut TestAppContext) { let (project, index, rust_lang_id) = init_test(cx).await; let test_process_data = Identifier { name: "test_process_data".into(), language_id: rust_lang_id, }; let index_state = index.read_with(cx, |index, _cx| index.state().clone()); let index_state = index_state.lock().await; cx.update(|cx| { let decls = index_state.declarations_for_identifier::<8>(&test_process_data); assert_eq!(decls.len(), 1); let decl = expect_file_decl("c.rs", &decls[0].1, &project, cx); assert_eq!(decl.identifier, test_process_data); let parent_id = decl.parent.unwrap(); let parent = index_state.declaration(parent_id).unwrap(); let parent_decl = expect_file_decl("c.rs", &parent, &project, cx); assert_eq!( parent_decl.identifier, Identifier { name: "tests".into(), language_id: rust_lang_id } ); assert_eq!(parent_decl.parent, None); }); } #[gpui::test] async fn test_parents_in_buffer(cx: &mut TestAppContext) { let (project, index, rust_lang_id) = init_test(cx).await; let test_process_data = Identifier { name: "test_process_data".into(), language_id: rust_lang_id, }; let buffer = project .update(cx, |project, cx| { let project_path = project.find_project_path("c.rs", cx).unwrap(); project.open_buffer(project_path, cx) }) .await .unwrap(); cx.run_until_parked(); let index_state = index.read_with(cx, |index, _cx| index.state().clone()); let index_state = index_state.lock().await; cx.update(|cx| { let decls = index_state.declarations_for_identifier::<8>(&test_process_data); assert_eq!(decls.len(), 1); let decl = expect_buffer_decl("c.rs", &decls[0].1, &project, cx); assert_eq!(decl.identifier, test_process_data); let parent_id = decl.parent.unwrap(); let parent = index_state.declaration(parent_id).unwrap(); let parent_decl = expect_buffer_decl("c.rs", &parent, &project, cx); assert_eq!( parent_decl.identifier, Identifier { name: "tests".into(), language_id: rust_lang_id } ); assert_eq!(parent_decl.parent, None); }); drop(buffer); } #[gpui::test] async fn test_declarations_limt(cx: &mut TestAppContext) { let (_, index, rust_lang_id) = init_test(cx).await; let index_state = index.read_with(cx, |index, _cx| index.state().clone()); let index_state = index_state.lock().await; let decls = index_state.declarations_for_identifier::<1>(&Identifier { name: "main".into(), language_id: rust_lang_id, }); assert_eq!(decls.len(), 0); } #[gpui::test] async fn test_buffer_shadow(cx: &mut TestAppContext) { let (project, index, rust_lang_id) = init_test(cx).await; let main = Identifier { name: "main".into(), language_id: rust_lang_id, }; let buffer = project .update(cx, |project, cx| { let project_path = project.find_project_path("c.rs", cx).unwrap(); project.open_buffer(project_path, cx) }) .await .unwrap(); cx.run_until_parked(); let index_state_arc = index.read_with(cx, |index, _cx| index.state().clone()); { let index_state = index_state_arc.lock().await; cx.update(|cx| { let decls = index_state.declarations_for_identifier::<8>(&main); assert_eq!(decls.len(), 2); let decl = expect_buffer_decl("c.rs", &decls[0].1, &project, cx); assert_eq!(decl.identifier, main); assert_eq!(decl.item_range.to_offset(&buffer.read(cx)), 32..280); expect_file_decl("a.rs", &decls[1].1, &project, cx); }); } // Drop the buffer and wait for release cx.update(|_| { drop(buffer); }); cx.run_until_parked(); let index_state = index_state_arc.lock().await; cx.update(|cx| { let decls = index_state.declarations_for_identifier::<8>(&main); assert_eq!(decls.len(), 2); expect_file_decl("a.rs", &decls[0].1, &project, cx); expect_file_decl("c.rs", &decls[1].1, &project, cx); }); } fn expect_buffer_decl<'a>( path: &str, declaration: &'a Declaration, project: &Entity, cx: &App, ) -> &'a BufferDeclaration { if let Declaration::Buffer { declaration, project_entry_id, .. } = declaration { let project_path = project .read(cx) .path_for_entry(*project_entry_id, cx) .unwrap(); assert_eq!(project_path.path.as_ref(), rel_path(path),); declaration } else { panic!("Expected a buffer declaration, found {:?}", declaration); } } fn expect_file_decl<'a>( path: &str, declaration: &'a Declaration, project: &Entity, cx: &App, ) -> &'a FileDeclaration { if let Declaration::File { declaration, project_entry_id: file, .. } = declaration { assert_eq!( project .read(cx) .path_for_entry(*file, cx) .unwrap() .path .as_ref(), rel_path(path), ); declaration } else { panic!("Expected a file declaration, found {:?}", declaration); } } async fn init_test( cx: &mut TestAppContext, ) -> (Entity, Entity, LanguageId) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); language::init(cx); Project::init_settings(cx); }); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ "a.rs": indoc! {r#" fn main() { let x = 1; let y = 2; let z = add(x, y); println!("Result: {}", z); } fn add(a: i32, b: i32) -> i32 { a + b } "#}, "b.rs": indoc! {" pub struct Config { pub name: String, pub value: i32, } impl Config { pub fn new(name: String, value: i32) -> Self { Config { name, value } } } "}, "c.rs": indoc! {r#" use std::collections::HashMap; fn main() { let args: Vec = std::env::args().collect(); let data: Vec = args[1..] .iter() .filter_map(|s| s.parse().ok()) .collect(); let result = process_data(data); println!("{:?}", result); } fn process_data(data: Vec) -> HashMap { let mut counts = HashMap::new(); for value in data { *counts.entry(value).or_insert(0) += 1; } counts } #[cfg(test)] mod tests { use super::*; #[test] fn test_process_data() { let data = vec![1, 2, 2, 3]; let result = process_data(data); assert_eq!(result.get(&2), Some(&2)); } } "#} }), ) .await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); let lang = rust_lang(); let lang_id = lang.id(); language_registry.add(Arc::new(lang)); let file_indexing_parallelism = 2; let index = cx.new(|cx| SyntaxIndex::new(&project, file_indexing_parallelism, cx)); cx.run_until_parked(); (project, index, lang_id) } fn rust_lang() -> Language { Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ) .with_outline_query(include_str!("../../languages/src/rust/outline.scm")) .unwrap() } }