Files
galaxy/app/src/code_review/comments/batch.rs
T

221 lines
7.5 KiB
Rust

use super::{
AttachedReviewComment, AttachedReviewCommentTarget, CommentId, PendingImportedReviewComment,
};
use crate::{code::editor::EditorReviewComment, code_review::diff_state::DiffMode};
use std::{collections::HashMap, path::Path};
use galaxy_core::features::FeatureFlag;
use galaxy_editor::render::model::LineCount;
use galaxyui::{Entity, ModelContext};
#[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` param should always be the filepath from the root of the repository.
/// This ensures we'll never confuse files in different subdirectories with
/// the same suffix, for example `/my_repo/src/a/file.txt` and `/my_repo/src/b/file.txt`.
pub fn file_comments<'a>(
&'a self,
file: &'a Path,
) -> impl Iterator<Item = &'a AttachedReviewComment> + 'a {
self.comments.iter().filter(move |comment| {
comment
.target
.absolute_file_path()
.is_some_and(|comment_file| comment_file.ends_with(file))
})
}
/// `file` param should always be the filepath from the root of the repository.
/// This ensures we'll never confuse files in different subdirectories with
/// the same suffix, for example `/my_repo/src/a/file.txt` and `/my_repo/src/b/file.txt`.
pub fn comment_line_numbers_for_file<'a>(
&'a self,
file: &'a Path,
) -> 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.ends_with(file) {
line.line_number()
} else {
None
}
} else {
None
}
})
}
pub(crate) fn editor_comments_for_file(&self, file: &Path) -> Vec<EditorReviewComment> {
self.file_comments(file)
.filter(|comment| {
if FeatureFlag::PRCommentsSlashCommand.is_enabled() {
!comment.outdated
} else {
true
}
})
.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;