405 lines · 13.9 KB · Rust
RawDownload
use crate::{    EditPredictionExampleCaptureFeatureFlag, StoredEvent,    cursor_excerpt::editable_and_context_ranges_for_cursor_position, example_spec::ExampleSpec,};use anyhow::Result;use buffer_diff::BufferDiffSnapshot;use collections::HashMap;use feature_flags::FeatureFlagAppExt as _;use gpui::{App, Entity, Task};use language::{Buffer, ToPoint as _};use project::{Project, WorktreeId};use std::{collections::hash_map, fmt::Write as _, path::Path, sync::Arc};use text::BufferSnapshot as TextBufferSnapshot; pub(crate) const DEFAULT_EXAMPLE_CAPTURE_RATE_PER_10K_PREDICTIONS: u16 = 10; pub fn capture_example(    project: Entity<Project>,    buffer: Entity<Buffer>,    cursor_anchor: language::Anchor,    mut events: Vec<StoredEvent>,    cx: &mut App,) -> Option<Task<Result<ExampleSpec>>> {    let snapshot = buffer.read(cx).snapshot();    let file = snapshot.file()?;    let worktree_id = file.worktree_id(cx);    let repository = project.read(cx).active_repository(cx)?;    let repository_snapshot = repository.read(cx).snapshot();    let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;    let cursor_path = worktree.read(cx).root_name().join(file.path());    if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {        return None;    }     let repository_url = repository_snapshot        .remote_origin_url        .clone()        .or_else(|| repository_snapshot.remote_upstream_url.clone())?;    let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();     let git_store = project.read(cx).git_store().clone();     Some(cx.spawn(async move |mut cx| {        let snapshots_by_path =            collect_snapshots(&project, &git_store, worktree_id, &events, &mut cx).await?;         events.retain(|stored_event| {            match stored_event.event.as_ref() {                zeta_prompt::Event::BufferChange { path, .. } => {                    if !snapshots_by_path.contains_key(path) {                        return false;                    }                }            }            true        });         let line_comment_prefix = snapshot            .language()            .and_then(|lang| lang.config().line_comments.first())            .map(|s| s.to_string())            .unwrap_or_default();        let (cursor_excerpt, cursor_offset) = cx            .background_executor()            .spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })            .await;        let uncommitted_diff = cx            .background_executor()            .spawn(async move { compute_uncommitted_diff(snapshots_by_path) })            .await;         let mut edit_history = String::new();        for stored_event in &events {            zeta_prompt::write_event(&mut edit_history, &stored_event.event);            if !edit_history.ends_with('\n') {                edit_history.push('\n');            }        }         let mut spec = ExampleSpec {            name: generate_timestamp_name(),            repository_url,            revision,            tags: Vec::new(),            reasoning: None,            uncommitted_diff,            cursor_path: cursor_path.as_std_path().into(),            cursor_position: String::new(),            edit_history,            expected_patches: Vec::new(),        };        spec.set_cursor_excerpt(&cursor_excerpt, cursor_offset, &line_comment_prefix);        Ok(spec)    }))} fn compute_cursor_excerpt(    snapshot: &language::BufferSnapshot,    cursor_anchor: language::Anchor,) -> (String, usize) {    use text::ToOffset as _;     let cursor_point = cursor_anchor.to_point(snapshot);    let (_editable_range, context_range) =        editable_and_context_ranges_for_cursor_position(cursor_point, snapshot, 100, 50);    let context_start_offset = context_range.start.to_offset(snapshot);    let cursor_offset = cursor_anchor.to_offset(snapshot);    let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);    let excerpt = snapshot.text_for_range(context_range).collect::<String>();    (excerpt, cursor_offset_in_excerpt)} async fn collect_snapshots(    project: &Entity<Project>,    git_store: &Entity<project::git_store::GitStore>,    worktree_id: WorktreeId,    events: &[StoredEvent],    cx: &mut gpui::AsyncApp,) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {    let mut snapshots_by_path = HashMap::default();    let root_name = project.read_with(cx, |project, cx| {        project            .worktree_for_id(worktree_id, cx)            .unwrap()            .read(cx)            .root_name()            .to_owned()    })?;    for stored_event in events {        let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();        if let Some((project_path, full_path)) = project.read_with(cx, |project, cx| {            let project_path = project                .find_project_path(path, cx)                .filter(|path| path.worktree_id == worktree_id)?;            let full_path = root_name.join(&project_path.path).as_std_path().into();            Some((project_path, full_path))        })? {            if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(full_path) {                let buffer = project                    .update(cx, |project, cx| {                        project.open_buffer(project_path.clone(), cx)                    })?                    .await?;                let diff = git_store                    .update(cx, |git_store, cx| {                        git_store.open_uncommitted_diff(buffer.clone(), cx)                    })?                    .await?;                let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;                entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));            }        }    }    Ok(snapshots_by_path)} fn compute_uncommitted_diff(    snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,) -> String {    let mut uncommitted_diff = String::new();    for (full_path, (before_text, diff_snapshot)) in snapshots_by_path {        if let Some(head_text) = &diff_snapshot.base_text_string() {            let file_diff = language::unified_diff(head_text, &before_text.text());            if !file_diff.is_empty() {                let path_str = full_path.to_string_lossy();                writeln!(uncommitted_diff, "--- a/{path_str}").ok();                writeln!(uncommitted_diff, "+++ b/{path_str}").ok();                uncommitted_diff.push_str(&file_diff);                if !uncommitted_diff.ends_with('\n') {                    uncommitted_diff.push('\n');                }            }        }    }    uncommitted_diff} fn generate_timestamp_name() -> String {    let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");    match format {        Ok(format) => {            let now = time::OffsetDateTime::now_local()                .unwrap_or_else(|_| time::OffsetDateTime::now_utc());            now.format(&format)                .unwrap_or_else(|_| "unknown-time".to_string())        }        Err(_) => "unknown-time".to_string(),    }} pub(crate) fn should_sample_edit_prediction_example_capture(cx: &App) -> bool {    let capture_rate = language::language_settings::all_language_settings(None, cx)        .edit_predictions        .example_capture_rate        .unwrap_or(DEFAULT_EXAMPLE_CAPTURE_RATE_PER_10K_PREDICTIONS);    cx.has_flag::<EditPredictionExampleCaptureFeatureFlag>()        && rand::random::<u16>() % 10_000 < capture_rate} #[cfg(test)]mod tests {    use super::*;    use crate::EditPredictionStore;    use client::{Client, UserStore};    use clock::FakeSystemClock;    use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};    use indoc::indoc;    use language::{Anchor, Point};    use project::{FakeFs, Project};    use serde_json::json;    use settings::SettingsStore;    use std::path::Path;     #[gpui::test]    async fn test_capture_example(cx: &mut TestAppContext) {        init_test(cx);        let fs = FakeFs::new(cx.executor());         let committed_contents = indoc! {"            fn main() {                one();                two();                three();                four();                five();                six();                seven();                eight();                nine();            }        "};         let disk_contents = indoc! {"            fn main() {                // comment 1                one();                two();                three();                four();                five();                six();                seven();                eight();                // comment 2                nine();            }        "};         fs.insert_tree(            "/project",            json!({                ".git": {},                "src": {                    "main.rs": disk_contents,                }            }),        )        .await;         fs.set_head_for_repo(            Path::new("/project/.git"),            &[("src/main.rs", committed_contents.to_string())],            "abc123def456",        );        fs.set_remote_for_repo(            Path::new("/project/.git"),            "origin",            "https://github.com/test/repo.git",        );         let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;         let buffer = project            .update(cx, |project, cx| {                project.open_local_buffer("/project/src/main.rs", cx)            })            .await            .unwrap();         let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());        ep_store.update(cx, |ep_store, cx| {            ep_store.register_buffer(&buffer, &project, cx)        });        cx.run_until_parked();         buffer.update(cx, |buffer, cx| {            let point = Point::new(6, 0);            buffer.edit([(point..point, "    // comment 3\n")], None, cx);            let point = Point::new(4, 0);            buffer.edit([(point..point, "    // comment 4\n")], None, cx);             pretty_assertions::assert_eq!(                buffer.text(),                indoc! {"                    fn main() {                        // comment 1                        one();                        two();                        // comment 4                        three();                        four();                        // comment 3                        five();                        six();                        seven();                        eight();                        // comment 2                        nine();                    }                "}            );        });        cx.run_until_parked();         let events = ep_store.update(cx, |store, cx| {            store.edit_history_for_project_with_pause_split_last_event(&project, cx)        });        let mut example = cx            .update(|cx| {                capture_example(project.clone(), buffer.clone(), Anchor::MIN, events, cx).unwrap()            })            .await            .unwrap();        example.name = "test".to_string();         pretty_assertions::assert_eq!(            example,            ExampleSpec {                name: "test".to_string(),                repository_url: "https://github.com/test/repo.git".to_string(),                revision: "abc123def456".to_string(),                tags: Vec::new(),                reasoning: None,                uncommitted_diff: indoc! {"                    --- a/project/src/main.rs                    +++ b/project/src/main.rs                    @@ -1,4 +1,5 @@                     fn main() {                    +    // comment 1                         one();                         two();                         three();                    @@ -7,5 +8,6 @@                         six();                         seven();                         eight();                    +    // comment 2                         nine();                     }                "}                .to_string(),                cursor_path: Path::new("project/src/main.rs").into(),                cursor_position: indoc! {"                    fn main() {                    ^[CURSOR_POSITION]                        // comment 1                        one();                        two();                        // comment 4                        three();                        four();                        // comment 3                        five();                        six();                        seven();                        eight();                        // comment 2                        nine();                    }                "}                .to_string(),                edit_history: indoc! {"                    --- a/project/src/main.rs                    +++ b/project/src/main.rs                    @@ -2,8 +2,10 @@                         // comment 1                         one();                         two();                    +    // comment 4                         three();                         four();                    +    // comment 3                         five();                         six();                         seven();                "}                .to_string(),                expected_patches: Vec::new()            }        );    }     fn init_test(cx: &mut TestAppContext) {        cx.update(|cx| {            let settings_store = SettingsStore::test(cx);            cx.set_global(settings_store);            zlog::init_test();            let http_client = FakeHttpClient::with_404_response();            let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);            language_model::init(client.clone(), cx);            let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));            EditPredictionStore::global(&client, &user_store, cx);        })    }}