634 lines · 21.2 KB · Rust
RawDownload
mod chunking;mod embedding;mod embedding_index;mod indexing;mod project_index;mod project_index_debug_view;mod summary_backlog;mod summary_index;mod worktree_index; use anyhow::{Context as _, Result};use collections::HashMap;use fs::Fs;use gpui::{App, AppContext as _, AsyncApp, BorrowAppContext, Context, Entity, Global, WeakEntity};use language::LineEnding;use project::{Project, Worktree};use std::{    cmp::Ordering,    path::{Path, PathBuf},    sync::Arc,};use util::ResultExt as _;use workspace::Workspace; pub use embedding::*;pub use project_index::{LoadedSearchResult, ProjectIndex, SearchResult, Status};pub use project_index_debug_view::ProjectIndexDebugView;pub use summary_index::FileSummary; pub struct SemanticDb {    embedding_provider: Arc<dyn EmbeddingProvider>,    db_connection: Option<heed::Env>,    project_indices: HashMap<WeakEntity<Project>, Entity<ProjectIndex>>,} impl Global for SemanticDb {} impl SemanticDb {    pub async fn new(        db_path: PathBuf,        embedding_provider: Arc<dyn EmbeddingProvider>,        cx: &mut AsyncApp,    ) -> Result<Self> {        let db_connection = cx            .background_spawn(async move {                std::fs::create_dir_all(&db_path)?;                unsafe {                    heed::EnvOpenOptions::new()                        .map_size(1024 * 1024 * 1024)                        .max_dbs(3000)                        .open(db_path)                }            })            .await            .context("opening database connection")?;         cx.update(|cx| {            cx.observe_new(                |workspace: &mut Workspace, _window, cx: &mut Context<Workspace>| {                    let project = workspace.project().clone();                     if cx.has_global::<SemanticDb>() {                        cx.update_global::<SemanticDb, _>(|this, cx| {                            this.create_project_index(project, cx);                        })                    } else {                        log::info!("No SemanticDb, skipping project index")                    }                },            )            .detach();        })        .ok();         Ok(SemanticDb {            db_connection: Some(db_connection),            embedding_provider,            project_indices: HashMap::default(),        })    }     pub async fn load_results(        mut results: Vec<SearchResult>,        fs: &Arc<dyn Fs>,        cx: &AsyncApp,    ) -> Result<Vec<LoadedSearchResult>> {        let mut max_scores_by_path = HashMap::<_, (f32, usize)>::default();        for result in &results {            let (score, query_index) = max_scores_by_path                .entry((result.worktree.clone(), result.path.clone()))                .or_default();            if result.score > *score {                *score = result.score;                *query_index = result.query_index;            }        }         results.sort_by(|a, b| {            let max_score_a = max_scores_by_path[&(a.worktree.clone(), a.path.clone())].0;            let max_score_b = max_scores_by_path[&(b.worktree.clone(), b.path.clone())].0;            max_score_b                .partial_cmp(&max_score_a)                .unwrap_or(Ordering::Equal)                .then_with(|| a.worktree.entity_id().cmp(&b.worktree.entity_id()))                .then_with(|| a.path.cmp(&b.path))                .then_with(|| a.range.start.cmp(&b.range.start))        });         let mut last_loaded_file: Option<(Entity<Worktree>, Arc<Path>, PathBuf, String)> = None;        let mut loaded_results = Vec::<LoadedSearchResult>::new();        for result in results {            let full_path;            let file_content;            if let Some(last_loaded_file) =                last_loaded_file                    .as_ref()                    .filter(|(last_worktree, last_path, _, _)| {                        last_worktree == &result.worktree && last_path == &result.path                    })            {                full_path = last_loaded_file.2.clone();                file_content = &last_loaded_file.3;            } else {                let output = result.worktree.read_with(cx, |worktree, _cx| {                    let entry_abs_path = worktree.abs_path().join(&result.path);                    let mut entry_full_path = PathBuf::from(worktree.root_name());                    entry_full_path.push(&result.path);                    let file_content = async {                        let entry_abs_path = entry_abs_path;                        fs.load(&entry_abs_path).await                    };                    (entry_full_path, file_content)                })?;                full_path = output.0;                let Some(content) = output.1.await.log_err() else {                    continue;                };                last_loaded_file = Some((                    result.worktree.clone(),                    result.path.clone(),                    full_path.clone(),                    content,                ));                file_content = &last_loaded_file.as_ref().unwrap().3;            };             let query_index = max_scores_by_path[&(result.worktree.clone(), result.path.clone())].1;             let mut range_start = result.range.start.min(file_content.len());            let mut range_end = result.range.end.min(file_content.len());            while !file_content.is_char_boundary(range_start) {                range_start += 1;            }            while !file_content.is_char_boundary(range_end) {                range_end += 1;            }             let start_row = file_content[0..range_start].matches('\n').count() as u32;            let mut end_row = file_content[0..range_end].matches('\n').count() as u32;            let start_line_byte_offset = file_content[0..range_start]                .rfind('\n')                .map(|pos| pos + 1)                .unwrap_or_default();            let mut end_line_byte_offset = range_end;            if file_content[..end_line_byte_offset].ends_with('\n') {                end_row -= 1;            } else {                end_line_byte_offset = file_content[range_end..]                    .find('\n')                    .map(|pos| range_end + pos + 1)                    .unwrap_or_else(|| file_content.len());            }            let mut excerpt_content =                file_content[start_line_byte_offset..end_line_byte_offset].to_string();            LineEnding::normalize(&mut excerpt_content);             if let Some(prev_result) = loaded_results.last_mut() {                if prev_result.full_path == full_path {                    if *prev_result.row_range.end() + 1 == start_row {                        prev_result.row_range = *prev_result.row_range.start()..=end_row;                        prev_result.excerpt_content.push_str(&excerpt_content);                        continue;                    }                }            }             loaded_results.push(LoadedSearchResult {                path: result.path,                full_path,                excerpt_content,                row_range: start_row..=end_row,                query_index,            });        }         for result in &mut loaded_results {            while result.excerpt_content.ends_with("\n\n") {                result.excerpt_content.pop();                result.row_range =                    *result.row_range.start()..=result.row_range.end().saturating_sub(1)            }        }         Ok(loaded_results)    }     pub fn project_index(        &mut self,        project: Entity<Project>,        _cx: &mut App,    ) -> Option<Entity<ProjectIndex>> {        self.project_indices.get(&project.downgrade()).cloned()    }     pub fn remaining_summaries(        &self,        project: &WeakEntity<Project>,        cx: &mut App,    ) -> Option<usize> {        self.project_indices.get(project).map(|project_index| {            project_index.update(cx, |project_index, cx| {                project_index.remaining_summaries(cx)            })        })    }     pub fn create_project_index(        &mut self,        project: Entity<Project>,        cx: &mut App,    ) -> Entity<ProjectIndex> {        let project_index = cx.new(|cx| {            ProjectIndex::new(                project.clone(),                self.db_connection.clone().unwrap(),                self.embedding_provider.clone(),                cx,            )        });         let project_weak = project.downgrade();        self.project_indices            .insert(project_weak.clone(), project_index.clone());         cx.observe_release(&project, move |_, cx| {            if cx.has_global::<SemanticDb>() {                cx.update_global::<SemanticDb, _>(|this, _| {                    this.project_indices.remove(&project_weak);                })            }        })        .detach();         project_index    }} impl Drop for SemanticDb {    fn drop(&mut self) {        self.db_connection.take().unwrap().prepare_for_closing();    }} #[cfg(test)]mod tests {    use super::*;    use anyhow::anyhow;    use chunking::Chunk;    use embedding_index::{ChunkedFile, EmbeddingIndex};    use feature_flags::FeatureFlagAppExt;    use fs::FakeFs;    use futures::{future::BoxFuture, FutureExt};    use gpui::TestAppContext;    use indexing::IndexingEntrySet;    use language::language_settings::AllLanguageSettings;    use project::{Project, ProjectEntryId};    use serde_json::json;    use settings::SettingsStore;    use smol::channel;    use std::{future, path::Path, sync::Arc};    use util::separator;     fn init_test(cx: &mut TestAppContext) {        env_logger::try_init().ok();         cx.update(|cx| {            let store = SettingsStore::test(cx);            cx.set_global(store);            language::init(cx);            cx.update_flags(false, vec![]);            Project::init_settings(cx);            SettingsStore::update(cx, |store, cx| {                store.update_user_settings::<AllLanguageSettings>(cx, |_| {});            });        });    }     pub struct TestEmbeddingProvider {        batch_size: usize,        compute_embedding: Box<dyn Fn(&str) -> Result<Embedding> + Send + Sync>,    }     impl TestEmbeddingProvider {        pub fn new(            batch_size: usize,            compute_embedding: impl 'static + Fn(&str) -> Result<Embedding> + Send + Sync,        ) -> Self {            Self {                batch_size,                compute_embedding: Box::new(compute_embedding),            }        }    }     impl EmbeddingProvider for TestEmbeddingProvider {        fn embed<'a>(            &'a self,            texts: &'a [TextToEmbed<'a>],        ) -> BoxFuture<'a, Result<Vec<Embedding>>> {            let embeddings = texts                .iter()                .map(|to_embed| (self.compute_embedding)(to_embed.text))                .collect();            future::ready(embeddings).boxed()        }         fn batch_size(&self) -> usize {            self.batch_size        }    }     #[gpui::test]    async fn test_search(cx: &mut TestAppContext) {        cx.executor().allow_parking();         init_test(cx);         cx.update(|cx| {            // This functionality is staff-flagged.            cx.update_flags(true, vec![]);        });         let temp_dir = tempfile::tempdir().unwrap();         let mut semantic_index = SemanticDb::new(            temp_dir.path().into(),            Arc::new(TestEmbeddingProvider::new(16, |text| {                let mut embedding = vec![0f32; 2];                // if the text contains garbage, give it a 1 in the first dimension                if text.contains("garbage in") {                    embedding[0] = 0.9;                } else {                    embedding[0] = -0.9;                }                 if text.contains("garbage out") {                    embedding[1] = 0.9;                } else {                    embedding[1] = -0.9;                }                 Ok(Embedding::new(embedding))            })),            &mut cx.to_async(),        )        .await        .unwrap();         let fs = FakeFs::new(cx.executor());        let project_path = Path::new("/fake_project");         fs.insert_tree(            project_path,            json!({                "fixture": {                    "main.rs": include_str!("../fixture/main.rs"),                    "needle.md": include_str!("../fixture/needle.md"),                }            }),        )        .await;         let project = Project::test(fs, [project_path], cx).await;         let project_index = cx.update(|cx| {            let language_registry = project.read(cx).languages().clone();            let node_runtime = project.read(cx).node_runtime().unwrap().clone();            languages::init(language_registry, node_runtime, cx);            semantic_index.create_project_index(project.clone(), cx)        });         cx.run_until_parked();        while cx            .update(|cx| semantic_index.remaining_summaries(&project.downgrade(), cx))            .unwrap()            > 0        {            cx.run_until_parked();        }         let results = cx            .update(|cx| {                let project_index = project_index.read(cx);                let query = "garbage in, garbage out";                project_index.search(vec![query.into()], 4, cx)            })            .await            .unwrap();         assert!(            results.len() > 1,            "should have found some results, but only found {:?}",            results        );         for result in &results {            println!("result: {:?}", result.path);            println!("score: {:?}", result.score);        }         // Find result that is greater than 0.5        let search_result = results.iter().find(|result| result.score > 0.9).unwrap();         assert_eq!(            search_result.path.to_string_lossy(),            separator!("fixture/needle.md")        );         let content = cx            .update(|cx| {                let worktree = search_result.worktree.read(cx);                let entry_abs_path = worktree.abs_path().join(&search_result.path);                let fs = project.read(cx).fs().clone();                cx.background_spawn(async move { fs.load(&entry_abs_path).await.unwrap() })            })            .await;         let range = search_result.range.clone();        let content = content[range.clone()].to_owned();         assert!(content.contains("garbage in, garbage out"));    }     #[gpui::test]    async fn test_embed_files(cx: &mut TestAppContext) {        cx.executor().allow_parking();         let provider = Arc::new(TestEmbeddingProvider::new(3, |text| {            if text.contains('g') {                Err(anyhow!("cannot embed text containing a 'g' character"))            } else {                Ok(Embedding::new(                    ('a'..='z')                        .map(|char| text.chars().filter(|c| *c == char).count() as f32)                        .collect(),                ))            }        }));         let (indexing_progress_tx, _) = channel::unbounded();        let indexing_entries = Arc::new(IndexingEntrySet::new(indexing_progress_tx));         let (chunked_files_tx, chunked_files_rx) = channel::unbounded::<ChunkedFile>();        chunked_files_tx            .send_blocking(ChunkedFile {                path: Path::new("test1.md").into(),                mtime: None,                handle: indexing_entries.insert(ProjectEntryId::from_proto(0)),                text: "abcdefghijklmnop".to_string(),                chunks: [0..4, 4..8, 8..12, 12..16]                    .into_iter()                    .map(|range| Chunk {                        range,                        digest: Default::default(),                    })                    .collect(),            })            .unwrap();        chunked_files_tx            .send_blocking(ChunkedFile {                path: Path::new("test2.md").into(),                mtime: None,                handle: indexing_entries.insert(ProjectEntryId::from_proto(1)),                text: "qrstuvwxyz".to_string(),                chunks: [0..4, 4..8, 8..10]                    .into_iter()                    .map(|range| Chunk {                        range,                        digest: Default::default(),                    })                    .collect(),            })            .unwrap();        chunked_files_tx.close();         let embed_files_task =            cx.update(|cx| EmbeddingIndex::embed_files(provider.clone(), chunked_files_rx, cx));        embed_files_task.task.await.unwrap();         let embedded_files_rx = embed_files_task.files;

Lines 1–500 of 634.