1157 lines · 41.4 KB · Rust
RawDownload
use std::path::PathBuf;use std::sync::Arc; use ai::agent::action::InsertReviewComment;use chrono::Local;use lsp::LspManagerModel;use repo_metadata::repositories::DetectedRepositories;use warp_core::ui::appearance::Appearance;use warp_editor::content::buffer::InitialBufferState;use warp_editor::render::element::VerticalExpansionBehavior;use warp_editor::render::model::LineCount;use warpui::elements::{Empty, MouseStateHandle};use warpui::platform::WindowStyle;use warpui::{App, ViewHandle}; use super::*;use crate::NotebookKeybindings;use crate::ai::persisted_workspace::PersistedWorkspace;use crate::ai::request_usage_model::AIRequestUsageModel;use crate::auth::AuthStateProvider;use crate::cloud_object::model::persistence::CloudModel;use crate::code::buffer_location::LocalOrRemotePath;use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView};use crate::code::local_code_editor::LocalCodeEditorView;use crate::code_review::GlobalCodeReviewModel;use crate::code_review::comments::{    AttachedReviewComment, AttachedReviewCommentTarget, CommentId, CommentOrigin,    ImportedCommentDetails, LineDiffContent, PendingImportedReviewComment,    PendingImportedReviewCommentTarget, attach_pending_imported_comments,};use crate::code_review::diff_size_limits::DiffSize;use crate::code_review::diff_state::{DiffStateModel, FileDiff, GitFileStatus};use crate::code_review::editor_state::CodeReviewEditorState;use crate::code_review::git_repo_model::GitRepoModels;use crate::pane_group::WorkingDirectoriesModel;use crate::server::server_api::ServerApiProvider;use crate::server::server_api::team::MockTeamClient;use crate::server::server_api::workspace::MockWorkspaceClient;use crate::server::telemetry::context_provider::AppTelemetryContextProvider;use crate::settings_view::keybindings::KeybindingChangedNotifier;use crate::terminal::local_shell::LocalShellState;use crate::test_util::settings::initialize_settings_for_tests;use crate::vim_registers::VimRegisters;use crate::workspace::ActiveSession;use crate::workspace::sync_inputs::SyncedInputState;use crate::workspaces::user_workspaces::UserWorkspaces; #[derive(Default)]struct TestView; impl warpui::Entity for TestView {    type Event = ();} impl warpui::View for TestView {    fn render(&self, _: &warpui::AppContext) -> Box<dyn warpui::Element> {        Empty::new().finish()    }     fn ui_name() -> &'static str {        "TestView"    }} impl warpui::TypedActionView for TestView {    type Action = ();} /// Initialize required singletons for testingfn initialize_test_app(app: &mut App) {    initialize_settings_for_tests(app);    app.add_singleton_model(|_| AuthStateProvider::new_for_test());    app.add_singleton_model(AppTelemetryContextProvider::new_context_provider);    app.add_singleton_model(|_| Appearance::mock());    app.add_singleton_model(|_| SyncedInputState::mock());    app.add_singleton_model(|_| VimRegisters::new());    app.add_singleton_model(|_| KeybindingChangedNotifier::mock());    app.add_singleton_model(|_| DetectedRepositories::default());    app.add_singleton_model(|_| GitRepoModels::new());    app.add_singleton_model(|_| LspManagerModel::new());    app.add_singleton_model(|_| LocalShellState::NotLoaded);    app.add_singleton_model(PersistedWorkspace::new_for_test);    app.add_singleton_model(|_| GlobalCodeReviewModel);    app.add_singleton_model(|ctx| {        UserWorkspaces::mock(            Arc::new(MockTeamClient::new()),            Arc::new(MockWorkspaceClient::new()),            vec![],            ctx,        )    });     // Add mocks required by rich text editor (used in the CommentEditor)    app.add_singleton_model(CloudModel::mock);    app.add_singleton_model(|_| ActiveSession::default());    app.add_singleton_model(NotebookKeybindings::new);    app.add_singleton_model(|_| ServerApiProvider::new_for_test());    app.add_singleton_model(|ctx| {        AIRequestUsageModel::new_for_test(ServerApiProvider::as_ref(ctx).get_ai_client(), ctx)    });} /// Creates a LocalCodeEditorView with the given contentfn create_editor_with_content(app: &mut App, content: &str) -> ViewHandle<LocalCodeEditorView> {    let content = content.to_string();    let (_, local_editor) = app.add_window(WindowStyle::NotStealFocus, move |ctx| {        let code_editor_view = ctx.add_typed_action_view(|ctx| {            CodeEditorView::new(                None,                None,                CodeEditorRenderOptions::new(VerticalExpansionBehavior::GrowToMaxHeight),                ctx,            )        });         code_editor_view.update(ctx, |editor, ctx| {            editor.reset(InitialBufferState::plain_text(&content), ctx);        });         LocalCodeEditorView::new(code_editor_view, None, false, None, ctx)    });     local_editor} /// Creates a LocalCodeEditorView with base and current content for diff testing#[allow(dead_code)]fn create_editor_with_diff(    app: &mut App,    base_content: &str,    current_content: &str,) -> ViewHandle<LocalCodeEditorView> {    let current = current_content.to_string();    let base = base_content.to_string();    let (_, local_editor) = app.add_window(WindowStyle::NotStealFocus, move |ctx| {        let code_editor_view = ctx.add_typed_action_view(|ctx| {            CodeEditorView::new(                None,                None,                CodeEditorRenderOptions::new(VerticalExpansionBehavior::GrowToMaxHeight),                ctx,            )        });         code_editor_view.update(ctx, |editor, ctx| {            editor.reset(InitialBufferState::plain_text(&current), ctx);            editor.set_base(&base, true, ctx);        });         LocalCodeEditorView::new(code_editor_view, None, false, None, ctx)    });     local_editor} /// Creates an attached review comment with a Line targetfn create_line_comment(    file_path: impl Into<PathBuf>,    line_number: usize,    line_text: &str,    comment_content: &str,) -> AttachedReviewComment {    let line_count = LineCount::from(line_number);    AttachedReviewComment {        id: CommentId::new(),        content: comment_content.to_string(),        target: AttachedReviewCommentTarget::Line {            absolute_file_path: LocalOrRemotePath::Local(file_path.into()),            line: EditorLineLocation::Current {                line_number: line_count,                line_range: line_count..LineCount::from(line_number + 1),            },            content: LineDiffContent {                content: format!("+{line_text}"),                lines_added: LineCount::from(1),                lines_removed: LineCount::from(0),            },        },        last_update_time: Local::now(),        base: None,        head: None,        outdated: false,        origin: CommentOrigin::Native,    }} /// Creates an imported (from GitHub) line comment whose stored `content` is the/// raw unified-diff line, including its one-char marker. `raw_diff_content`/// should therefore carry the leading `+`/`-`/space (e.g. `" line 2"` for a/// context line, `"+added"` for an addition).fn create_imported_line_comment(    file_path: impl Into<PathBuf>,    line_number: usize,    raw_diff_content: &str,    comment_content: &str,) -> AttachedReviewComment {    let line_count = LineCount::from(line_number);    AttachedReviewComment {        id: CommentId::new(),        content: comment_content.to_string(),        target: AttachedReviewCommentTarget::Line {            absolute_file_path: LocalOrRemotePath::Local(file_path.into()),            line: EditorLineLocation::Current {                line_number: line_count,                line_range: line_count..LineCount::from(line_number + 1),            },            content: LineDiffContent {                content: raw_diff_content.to_string(),                lines_added: LineCount::from(0),                lines_removed: LineCount::from(0),            },        },        last_update_time: Local::now(),        base: None,        head: None,        outdated: false,        origin: CommentOrigin::ImportedFromGitHub(ImportedCommentDetails {            author: "reviewer".to_string(),            github_comment_id: "1".to_string(),            github_parent_id: None,            html_url: None,        }),    }} /// Creates an attached review comment with a File targetfn create_file_comment(    file_path: impl Into<PathBuf>,    comment_content: &str,) -> AttachedReviewComment {    AttachedReviewComment {        id: CommentId::new(),        content: comment_content.to_string(),        target: AttachedReviewCommentTarget::File {            absolute_file_path: LocalOrRemotePath::Local(file_path.into()),        },        last_update_time: Local::now(),        base: None,        head: None,        outdated: false,        origin: CommentOrigin::Native,    }} /// Creates an attached review comment with a General targetfn create_general_comment(comment_content: &str) -> AttachedReviewComment {    AttachedReviewComment {        id: CommentId::new(),        content: comment_content.to_string(),        target: AttachedReviewCommentTarget::General,        last_update_time: Local::now(),        base: None,        head: None,        outdated: false,        origin: CommentOrigin::Native,    }} fn make_pending_comment(    id: &str,    author: &str,    body: &str,    parent_id: Option<&str>,    timestamp: &str,    target: PendingImportedReviewCommentTarget,) -> PendingImportedReviewComment {    let mut pending = PendingImportedReviewComment::try_from(InsertReviewComment {        comment_id: id.to_string(),        author: author.to_string(),        comment_body: body.to_string(),        parent_comment_id: parent_id.map(|s| s.to_string()),        last_modified_timestamp: timestamp.to_string(),        comment_location: None,        html_url: None,    })    .expect("valid pending import conversion");     // Override the location target since we intentionally use `comment_location: None` above.    pending.target = target;     pending} use crate::view_components::action_button::{ActionButton, NakedTheme}; /// Test context that holds all common test statestruct TestContext {    repo_path: PathBuf,    repo_location: LocalOrRemotePath,    #[allow(dead_code)]    window_id: warpui::WindowId,    state: LoadedState,    code_review_view: ViewHandle<CodeReviewView>,} impl TestContext {    /// Initialize common test state with a single file editor    fn new(app: &mut App, file_path: impl Into<String>, editor_content: &str) -> Self {        initialize_test_app(app);         let editor = create_editor_with_content(app, editor_content);        let repo_path = PathBuf::from("/repo");         let (window_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| TestView);        let state =            create_loaded_state_with_editors(app, window_id, vec![(file_path.into(), editor)]);         let diff_state_model = app.add_model(DiffStateModel::new_for_test);         let working_directories_model = app.add_model(|_| WorkingDirectoriesModel::new());        let repo_key = LocalOrRemotePath::Local(repo_path.clone());        let code_review_comment_batch =            working_directories_model.update(app, |working_directories, ctx| {                working_directories.get_or_create_code_review_comments(&repo_key, ctx)            });         let code_review_view = app.add_view(window_id, |ctx| {            CodeReviewView::new(                Some(repo_key.clone()),                diff_state_model,                code_review_comment_batch,                None,                ctx,            )        });         Self {            repo_path: repo_path.clone(),            repo_location: LocalOrRemotePath::Local(repo_path),            window_id,            state,            code_review_view,        }    }} /// Creates a minimal LoadedState with file states containing editors./// Must be called within an App context.fn create_loaded_state_with_editors(    app: &mut App,    window_id: warpui::WindowId,    file_editors: Vec<(String, ViewHandle<LocalCodeEditorView>)>,) -> LoadedState {    let file_states = file_editors        .into_iter()        .map(|(file_path, editor)| {            let chevron_button = app.add_view(window_id, |_| ActionButton::new("", NakedTheme));            let open_in_tab_button = app.add_view(window_id, |_| ActionButton::new("", NakedTheme));            let discard_button = app.add_view(window_id, |_| ActionButton::new("", NakedTheme));            let add_context_button = app.add_view(window_id, |_| ActionButton::new("", NakedTheme));            let copy_path_button = app.add_view(window_id, |_| ActionButton::new("", NakedTheme));             let state = FileState {                file_diff: FileDiff {                    file_path: file_path.clone(),                    status: GitFileStatus::Modified,                    hunks: Arc::new(vec![]),                    is_binary: false,                    is_autogenerated: false,                    max_line_number: 0,                    has_hidden_bidi_chars: false,                    size: DiffSize::Normal,                },                editor_state: Some(CodeReviewEditorState::new_loaded(editor)),                is_expanded: true,                sidebar_mouse_state: MouseStateHandle::default(),                header_mouse_state: MouseStateHandle::default(),                chevron_button,                open_in_tab_button,                discard_button,                add_context_button,                copy_path_button,            };            (file_path, state)        })        .collect();     LoadedState {        file_states,        total_additions: 0,        total_deletions: 0,        files_changed: 0,    }} #[test]fn test_relocate_comments_empty_input() {    App::test((), |mut app| async move {        let ctx = TestContext::new(&mut app, "test.txt", "line 1\nline 2\nline 3");         ctx.code_review_view.update(&mut app, |_view, view_ctx| {            let RelocateCommentsResult {                comments: relocated,                fallback_count: fallbacks,            } = CodeReviewView::relocate_comments(vec![], &ctx.state, &ctx.repo_location, view_ctx);             assert!(                relocated.is_empty(),                "Empty input should return empty output"            );            assert_eq!(fallbacks, 0, "Empty input should have no fallbacks");        });    });} #[test]fn test_relocate_comments_general_comment_passes_through() {    App::test((), |mut app| async move {        let ctx = TestContext::new(&mut app, "test.txt", "line 1\nline 2\nline 3");         let general_comment = create_general_comment("This is a general comment");        let original_id = general_comment.id;         ctx.code_review_view.update(&mut app, |_view, view_ctx| {            let RelocateCommentsResult {                comments: relocated,                fallback_count: fallbacks,            } = CodeReviewView::relocate_comments(                vec![general_comment],                &ctx.state,                &ctx.repo_location,                view_ctx,            );             assert_eq!(relocated.len(), 1, "Should return the comment");            assert_eq!(relocated[0].id, original_id, "Should preserve comment ID");            assert!(                matches!(relocated[0].target, AttachedReviewCommentTarget::General),                "General comment should remain General"            );            assert_eq!(                fallbacks, 0,                "General comments should not count as fallbacks"            );        });    });} #[test]fn test_relocate_comments_file_comment_passes_through() {    App::test((), |mut app| async move {        let file_path = "test.txt";        let ctx = TestContext::new(&mut app, file_path, "line 1\nline 2\nline 3");         let file_comment =            create_file_comment(ctx.repo_path.join(file_path), "This is a file comment");        let original_id = file_comment.id;         ctx.code_review_view.update(&mut app, |_view, view_ctx| {            let RelocateCommentsResult {                comments: relocated,                fallback_count: fallbacks,            } = CodeReviewView::relocate_comments(                vec![file_comment],                &ctx.state,                &ctx.repo_location,                view_ctx,            );             assert_eq!(relocated.len(), 1, "Should return the comment");            assert_eq!(relocated[0].id, original_id, "Should preserve comment ID");            assert!(                matches!(                    relocated[0].target,                    AttachedReviewCommentTarget::File { .. }                ),                "File comment should remain File"            );            assert_eq!(fallbacks, 0, "File comments should not count as fallbacks");        });    });} #[test]fn test_relocate_comments_line_comment_no_matching_editor_marked_outdated() {    App::test((), |mut app| async move {        // Editor is for "test.txt" but comment is for "other.txt"        let ctx = TestContext::new(&mut app, "test.txt", "line 1\nline 2\nline 3");         let line_comment =            create_line_comment("/repo/other.txt", 1, "line 1", "Comment on other file");        let original_id = line_comment.id;         ctx.code_review_view.update(&mut app, |_view, view_ctx| {            let RelocateCommentsResult {                comments: relocated,                fallback_count: fallbacks,            } = CodeReviewView::relocate_comments(                vec![line_comment],                &ctx.state,                &ctx.repo_location,                view_ctx,            );             assert_eq!(                relocated.len(),                1,                "Comment with no matching editor should be kept but marked outdated"            );            assert_eq!(relocated[0].id, original_id, "Should preserve comment ID");

Lines 1–500 of 1157.