216 lines · 7.0 KB · Rust
RawDownload
use std::collections::HashMap; use warp_editor::render::model::LineCount;use warpui::{Entity, ModelContext}; use super::{    AttachedReviewComment, AttachedReviewCommentTarget, CommentId, PendingImportedReviewComment,};use crate::code::buffer_location::LocalOrRemotePath;use crate::code::editor::EditorReviewComment;use crate::code_review::diff_state::DiffMode; #[derive(Clone, Debug, PartialEq, Eq)]pub enum ReviewCommentBatchEvent {    Changed { should_reposition_comments: bool },} #[derive(Clone, Debug, Default)]pub struct ReviewCommentBatch {    /// Comments that are attached to local editors and visible to the user.    pub comments: Vec<AttachedReviewComment>,    /// Imported comments waiting for editors and diffs to load before they can be displayed to the user.    /// Comments are grouped by base branch.    pending_imported_comments: HashMap<DiffMode, Vec<PendingImportedReviewComment>>,} impl Entity for ReviewCommentBatch {    type Event = ReviewCommentBatchEvent;} impl ReviewCommentBatch {    pub fn from_comments(comments: Vec<AttachedReviewComment>) -> Self {        Self {            comments,            pending_imported_comments: HashMap::new(),        }    }     pub(crate) fn get_review_comment_by_id(&self, id: CommentId) -> Option<&AttachedReviewComment> {        self.comments.iter().find(|comment| comment.id == id)    }     pub(super) fn get_mut_review_comment_by_id(        &mut self,        id: CommentId,    ) -> Option<&mut AttachedReviewComment> {        self.comments.iter_mut().find(|comment| comment.id == id)    }     pub(crate) fn diffset_comment(&self) -> Option<&AttachedReviewComment> {        self.comments            .iter()            .find(|comment| matches!(comment.target, AttachedReviewCommentTarget::General))    }     pub(crate) fn has_only_outdated_comments(&self) -> bool {        self.comments.iter().all(|comment| comment.outdated)    }     /// `file` should be the host-aware absolute path for the editor file.    pub fn file_comments<'a>(        &'a self,        file: &'a LocalOrRemotePath,    ) -> impl Iterator<Item = &'a AttachedReviewComment> + 'a {        self.comments.iter().filter(move |comment| {            comment                .target                .absolute_file_path()                .is_some_and(|comment_file| comment_file == file)        })    }     /// `file` should be the host-aware absolute path for the editor file.    pub fn comment_line_numbers_for_file<'a>(        &'a self,        file: &'a LocalOrRemotePath,    ) -> impl Iterator<Item = LineCount> + 'a {        self.file_comments(file).filter_map(move |comment| {            if let AttachedReviewCommentTarget::Line {                absolute_file_path: comment_file_path,                line,                ..            } = &comment.target            {                if comment_file_path == file {                    line.line_number()                } else {                    None                }            } else {                None            }        })    }     pub(crate) fn editor_comments_for_file(        &self,        file: &LocalOrRemotePath,    ) -> Vec<EditorReviewComment> {        self.file_comments(file)            .filter(|comment| !comment.outdated)            .filter_map(|comment| EditorReviewComment::try_from(comment.clone()).ok())            .collect()    }     pub(crate) fn upsert_comment(        &mut self,        comment: AttachedReviewComment,        ctx: &mut ModelContext<Self>,    ) {        self.upsert_comments_inner(vec![comment]);        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: false,        });    }     #[cfg(feature = "local_fs")]    pub(crate) fn upsert_imported_comments(        &mut self,        comments: Vec<AttachedReviewComment>,        ctx: &mut ModelContext<Self>,    ) {        if comments.is_empty() {            return;        }        self.upsert_comments_inner(comments);        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: true,        });    }     /// Comments with existing IDs are updated.    /// New comments are inserted into the batch.    pub fn upsert_comments(        &mut self,        comments: Vec<AttachedReviewComment>,        ctx: &mut ModelContext<Self>,    ) {        self.upsert_comments_inner(comments);        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: false,        });    }     fn upsert_comments_inner(&mut self, comments: Vec<AttachedReviewComment>) {        let (existing_comments, new_comments): (            Vec<AttachedReviewComment>,            Vec<AttachedReviewComment>,        ) = comments            .into_iter()            .partition(|c| self.get_review_comment_by_id(c.id).is_some());         self.comments.extend(new_comments);        for c in existing_comments {            if let Some(existing_entry) = self.get_mut_review_comment_by_id(c.id) {                *existing_entry = c;            }        }    }     pub(crate) fn take_comments(&mut self) -> Vec<AttachedReviewComment> {        std::mem::take(&mut self.comments)    }     /// Deleting a comment does NOT remove the associated diff hunk from the batch's    /// diff set because that hunk may be referenced by another comment.    /// In the future, we may investigate a cleaner way to do this.    pub(crate) fn delete_comment(&mut self, id: CommentId, ctx: &mut ModelContext<Self>) {        self.comments.retain(|comment| comment.id != id);        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: false,        });    }     pub(crate) fn clear_all(&mut self, ctx: &mut ModelContext<Self>) {        self.comments.clear();        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: false,        });    }     /// Stores imported comments that are waiting for diffs and editors to load before they can be flattened,    /// relocated, and inserted into `comments`.    #[cfg(feature = "local_fs")]    pub(crate) fn add_pending_imported_comments(        &mut self,        comments: Vec<PendingImportedReviewComment>,        base_branch: DiffMode,        ctx: &mut ModelContext<Self>,    ) {        self.pending_imported_comments            .entry(base_branch)            .or_default()            .extend(comments);        ctx.emit(ReviewCommentBatchEvent::Changed {            should_reposition_comments: true,        });    }     /// Takes all pending imported comments for the given diff mode, leaving the pending list empty.    /// Used when diffs have loaded and comments can be relocated.    pub(crate) fn take_pending_imported_comments_for_branch(        &mut self,        branch: &DiffMode,    ) -> Vec<PendingImportedReviewComment> {        if let Some(pending_comments) = self.pending_imported_comments.get_mut(branch) {            std::mem::take(pending_comments)        } else {            Vec::new()        }    }} #[cfg(test)]#[path = "batch_tests.rs"]mod tests;