957 lines · 29.2 KB · Rust
RawDownload
use anyhow::{Context as _, Result, anyhow};use collections::HashMap;use editor::ProposedChangesEditor;use futures::{TryFutureExt as _, future};use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};use language::{AutoindentMode, Buffer, BufferSnapshot};use project::{Project, ProjectPath};use std::{cmp, ops::Range, path::Path, sync::Arc};use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point}; #[derive(Clone, Debug)]pub struct AssistantPatch {    pub range: Range<language::Anchor>,    pub title: SharedString,    pub edits: Arc<[Result<AssistantEdit>]>,    pub status: AssistantPatchStatus,} #[derive(Copy, Clone, Debug, PartialEq, Eq)]pub enum AssistantPatchStatus {    Pending,    Ready,} #[derive(Clone, Debug, PartialEq, Eq)]pub struct AssistantEdit {    pub path: String,    pub kind: AssistantEditKind,} #[derive(Clone, Debug, PartialEq, Eq)]pub enum AssistantEditKind {    Update {        old_text: String,        new_text: String,        description: Option<String>,    },    Create {        new_text: String,        description: Option<String>,    },    InsertBefore {        old_text: String,        new_text: String,        description: Option<String>,    },    InsertAfter {        old_text: String,        new_text: String,        description: Option<String>,    },    Delete {        old_text: String,    },} #[derive(Clone, Debug, Eq, PartialEq)]pub struct ResolvedPatch {    pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,    pub errors: Vec<AssistantPatchResolutionError>,} #[derive(Clone, Debug, Eq, PartialEq)]pub struct ResolvedEditGroup {    pub context_range: Range<language::Anchor>,    pub edits: Vec<ResolvedEdit>,} #[derive(Clone, Debug, Eq, PartialEq)]pub struct ResolvedEdit {    range: Range<language::Anchor>,    new_text: String,    description: Option<String>,} #[derive(Clone, Debug, Eq, PartialEq)]pub struct AssistantPatchResolutionError {    pub edit_ix: usize,    pub message: String,} #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]enum SearchDirection {    Up,    Left,    Diagonal,} #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]struct SearchState {    cost: u32,    direction: SearchDirection,} impl SearchState {    fn new(cost: u32, direction: SearchDirection) -> Self {        Self { cost, direction }    }} struct SearchMatrix {    cols: usize,    data: Vec<SearchState>,} impl SearchMatrix {    fn new(rows: usize, cols: usize) -> Self {        SearchMatrix {            cols,            data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],        }    }     fn get(&self, row: usize, col: usize) -> SearchState {        self.data[row * self.cols + col]    }     fn set(&mut self, row: usize, col: usize, cost: SearchState) {        self.data[row * self.cols + col] = cost;    }} impl ResolvedPatch {    pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {        for (buffer, groups) in &self.edit_groups {            let branch = editor.branch_buffer_for_base(buffer).unwrap();            Self::apply_edit_groups(groups, &branch, cx);        }        editor.recalculate_all_buffer_diffs();    }     fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {        let mut edits = Vec::new();        for group in groups {            for suggestion in &group.edits {                edits.push((suggestion.range.clone(), suggestion.new_text.clone()));            }        }        buffer.update(cx, |buffer, cx| {            buffer.edit(                edits,                Some(AutoindentMode::Block {                    original_indent_columns: Vec::new(),                }),                cx,            );        });    }} impl ResolvedEdit {    pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {        let range = &self.range;        let other_range = &other.range;         // Don't merge if we don't contain the other suggestion.        if range.start.cmp(&other_range.start, buffer).is_gt()            || range.end.cmp(&other_range.end, buffer).is_lt()        {            return false;        }         let other_offset_range = other_range.to_offset(buffer);        let offset_range = range.to_offset(buffer);         // If the other range is empty at the start of this edit's range, combine the new text        if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {            self.new_text = format!("{}\n{}", other.new_text, self.new_text);            self.range.start = other_range.start;             if let Some((description, other_description)) =                self.description.as_mut().zip(other.description.as_ref())            {                *description = format!("{}\n{}", other_description, description)            }        } else {            if let Some((description, other_description)) =                self.description.as_mut().zip(other.description.as_ref())            {                description.push('\n');                description.push_str(other_description);            }        }         true    }} impl AssistantEdit {    pub fn new(        path: Option<String>,        operation: Option<String>,        old_text: Option<String>,        new_text: Option<String>,        description: Option<String>,    ) -> Result<Self> {        let path = path.ok_or_else(|| anyhow!("missing path"))?;        let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;         let kind = match operation.as_str() {            "update" => AssistantEditKind::Update {                old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,                new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,                description,            },            "insert_before" => AssistantEditKind::InsertBefore {                old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,                new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,                description,            },            "insert_after" => AssistantEditKind::InsertAfter {                old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,                new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,                description,            },            "delete" => AssistantEditKind::Delete {                old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,            },            "create" => AssistantEditKind::Create {                description,                new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,            },            _ => Err(anyhow!("unknown operation {operation:?}"))?,        };         Ok(Self { path, kind })    }     pub async fn resolve(        &self,        project: Entity<Project>,        mut cx: AsyncApp,    ) -> Result<(Entity<Buffer>, ResolvedEdit)> {        let path = self.path.clone();        let kind = self.kind.clone();        let buffer = project            .update(&mut cx, |project, cx| {                let project_path = project                    .find_project_path(Path::new(&path), cx)                    .or_else(|| {                        // If we couldn't find a project path for it, put it in the active worktree                        // so that when we create the buffer, it can be saved.                        let worktree = project                            .active_entry()                            .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))                            .or_else(|| project.worktrees(cx).next())?;                        let worktree = worktree.read(cx);                         Some(ProjectPath {                            worktree_id: worktree.id(),                            path: Arc::from(Path::new(&path)),                        })                    })                    .with_context(|| format!("worktree not found for {:?}", path))?;                anyhow::Ok(project.open_buffer(project_path, cx))            })??            .await?;         let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;        let suggestion = cx            .background_spawn(async move { kind.resolve(&snapshot) })            .await;         Ok((buffer, suggestion))    }} impl AssistantEditKind {    fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {        match self {            Self::Update {                old_text,                new_text,                description,            } => {                let range = Self::resolve_location(&snapshot, &old_text);                ResolvedEdit {                    range,                    new_text,                    description,                }            }            Self::Create {                new_text,                description,            } => ResolvedEdit {                range: text::Anchor::MIN..text::Anchor::MAX,                description,                new_text,            },            Self::InsertBefore {                old_text,                mut new_text,                description,            } => {                let range = Self::resolve_location(&snapshot, &old_text);                new_text.push('\n');                ResolvedEdit {                    range: range.start..range.start,                    new_text,                    description,                }            }            Self::InsertAfter {                old_text,                mut new_text,                description,            } => {                let range = Self::resolve_location(&snapshot, &old_text);                new_text.insert(0, '\n');                ResolvedEdit {                    range: range.end..range.end,                    new_text,                    description,                }            }            Self::Delete { old_text } => {                let range = Self::resolve_location(&snapshot, &old_text);                ResolvedEdit {                    range,                    new_text: String::new(),                    description: None,                }            }        }    }     fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {        const INSERTION_COST: u32 = 3;        const DELETION_COST: u32 = 10;        const WHITESPACE_INSERTION_COST: u32 = 1;        const WHITESPACE_DELETION_COST: u32 = 1;         let buffer_len = buffer.len();        let query_len = search_query.len();        let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);        let mut leading_deletion_cost = 0_u32;        for (row, query_byte) in search_query.bytes().enumerate() {            let deletion_cost = if query_byte.is_ascii_whitespace() {                WHITESPACE_DELETION_COST            } else {                DELETION_COST            };             leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);            matrix.set(                row + 1,                0,                SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),            );             for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {                let insertion_cost = if buffer_byte.is_ascii_whitespace() {                    WHITESPACE_INSERTION_COST                } else {                    INSERTION_COST                };                 let up = SearchState::new(                    matrix.get(row, col + 1).cost.saturating_add(deletion_cost),                    SearchDirection::Up,                );                let left = SearchState::new(                    matrix.get(row + 1, col).cost.saturating_add(insertion_cost),                    SearchDirection::Left,                );                let diagonal = SearchState::new(                    if query_byte == *buffer_byte {                        matrix.get(row, col).cost                    } else {                        matrix                            .get(row, col)                            .cost                            .saturating_add(deletion_cost + insertion_cost)                    },                    SearchDirection::Diagonal,                );                matrix.set(row + 1, col + 1, up.min(left).min(diagonal));            }        }         // Traceback to find the best match        let mut best_buffer_end = buffer_len;        let mut best_cost = u32::MAX;        for col in 1..=buffer_len {            let cost = matrix.get(query_len, col).cost;            if cost < best_cost {                best_cost = cost;                best_buffer_end = col;            }        }         let mut query_ix = query_len;        let mut buffer_ix = best_buffer_end;        while query_ix > 0 && buffer_ix > 0 {            let current = matrix.get(query_ix, buffer_ix);            match current.direction {                SearchDirection::Diagonal => {                    query_ix -= 1;                    buffer_ix -= 1;                }                SearchDirection::Up => {                    query_ix -= 1;                }                SearchDirection::Left => {                    buffer_ix -= 1;                }            }        }         let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));        start.column = 0;        let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));        if end.column > 0 {            end.column = buffer.line_len(end.row);        }         buffer.anchor_after(start)..buffer.anchor_before(end)    }} impl AssistantPatch {    pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {        let mut resolve_tasks = Vec::new();        for (ix, edit) in self.edits.iter().enumerate() {            if let Ok(edit) = edit.as_ref() {                resolve_tasks.push(                    edit.resolve(project.clone(), cx.clone())                        .map_err(move |error| (ix, error)),                );            }        }         let edits = future::join_all(resolve_tasks).await;        let mut errors = Vec::new();        let mut edits_by_buffer = HashMap::default();        for entry in edits {            match entry {                Ok((buffer, edit)) => {                    edits_by_buffer                        .entry(buffer)                        .or_insert_with(Vec::new)                        .push(edit);                }                Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {                    edit_ix,                    message: error.to_string(),                }),            }        }         // Expand the context ranges of each edit and group edits with overlapping context ranges.        let mut edit_groups_by_buffer = HashMap::default();        for (buffer, edits) in edits_by_buffer {            if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {                edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));            }        }         ResolvedPatch {            edit_groups: edit_groups_by_buffer,            errors,        }    }     fn group_edits(        mut edits: Vec<ResolvedEdit>,        snapshot: &text::BufferSnapshot,    ) -> Vec<ResolvedEditGroup> {        let mut edit_groups = Vec::<ResolvedEditGroup>::new();        // Sort edits by their range so that earlier, larger ranges come first        edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));         // Merge overlapping edits        edits.dedup_by(|a, b| b.try_merge(a, &snapshot));         // Create context ranges for each edit        for edit in edits {            let context_range = {                let edit_point_range = edit.range.to_point(&snapshot);                let start_row = edit_point_range.start.row.saturating_sub(5);                let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);                let start = snapshot.anchor_before(Point::new(start_row, 0));                let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));                start..end            };             if let Some(last_group) = edit_groups.last_mut() {                if last_group                    .context_range                    .end                    .cmp(&context_range.start, &snapshot)                    .is_ge()                {                    // Merge with the previous group if context ranges overlap                    last_group.context_range.end = context_range.end;                    last_group.edits.push(edit);                } else {                    // Create a new group                    edit_groups.push(ResolvedEditGroup {

Lines 1–500 of 957.