use std::collections::{HashMap, HashSet};use std::mem;use std::ops::Range;use std::path::{Path, PathBuf};use std::sync::Arc;use std::time::Duration; use ai::project_context::model::ProjectContextModel;use indexmap::IndexMap;use itertools::Itertools;#[cfg(feature = "local_fs")]use num_traits::SaturatingSub;use pathfinder_geometry::rect::RectF;use pathfinder_geometry::vector::{Vector2F, vec2f};use rand::Rng;use rand::distributions::Alphanumeric;use string_offset::CharOffset;use vec1::Vec1;use warp_core::channel::{Channel, ChannelState};use warp_core::features::FeatureFlag;use warp_core::ui::theme::color::internal_colors;use warp_core::{SessionId, safe_error, safe_info};use warp_editor::content::buffer::{AutoScrollBehavior, InitialBufferState, SelectionOffsets};use warp_editor::model::CoreEditorModel;use warp_editor::render::element::VerticalExpansionBehavior;#[cfg(not(target_family = "wasm"))]use warp_editor::render::model::AutoScrollMode;use warp_editor::render::model::LineCount;use warp_util::content_version::ContentVersion;use warp_util::path::LineAndColumnArg;use warp_util::standardized_path::StandardizedPath;use warpui::clipboard::ClipboardContent;use warpui::elements::new_scrollable::{ NewScrollable, NewScrollableElement, ScrollableAppearance, SingleAxisConfig,};use warpui::elements::{ Align, Border, ChildAnchor, ChildView, Clipped, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DEFAULT_UI_LINE_HEIGHT_RATIO, DispatchEventResult, DragBarSide, Element, Empty, EventHandler, Flex, Hoverable, List, ListState, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Percentage, PositionedElementAnchor, PositionedElementOffsetBounds, Radius, Rect, Resizable, ResizableStateHandle, SavePosition, ScrollOffset, ScrollStateHandle, ScrollbarWidth, Shrinkable, Stack, Text, resizable_state_handle,};use warpui::fonts::{Properties, Weight};use warpui::keymap::Keystroke;use warpui::platform::Cursor;use warpui::text_layout::{ClipConfig, default_compute_baseline_position};use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};use warpui::units::Pixels;use warpui::{ AppContext, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle, WindowId,}; use super::code_review_header::CodeReviewHeader;use super::comment_list_view::{CommentListDebugState, CommentListEvent, CommentListView};use super::comments::{AttachedReviewComment, CommentOrigin, attach_pending_imported_comments};use super::diff_size_limits::DiffSize;use super::git_dialog::{GitDialog, GitDialogEvent, GitDialogKind};use super::{GlobalCodeReviewEvent, GlobalCodeReviewModel};#[cfg(feature = "local_fs")]use crate::TelemetryEvent;use crate::ai::agent::{ AIAgentAttachment, AgentReviewCommentBatch, CurrentHead, DiffBase, DiffSetHunk,};use crate::ai::blocklist::agent_view::AgentViewEntryOrigin;use crate::appearance::Appearance;use crate::code::ShowCommentEditorProvider;#[cfg(not(target_family = "wasm"))]use crate::code::ShowFindReferencesCard;use crate::code::buffer_location::LocalOrRemotePath;use crate::code::editor::comment_editor::DEFAULT_COMMENT_MAX_WIDTH;use crate::code::editor::line::EditorLineLocation;use crate::code::editor::view::{CodeEditorEvent, CodeEditorRenderOptions, CodeEditorView};use crate::code::editor::{ CommentEditor, CommentEditorEvent, EditorCommentsModel, EditorReviewComment, GutterHoverTarget, add_color, remove_color,};use crate::code::editor_management::CodeEditorStatus;use crate::code::footer::{CodeFooterView, CodeFooterViewEvent};use crate::code::global_buffer_model::GlobalBufferModel;use crate::code::local_code_editor::{ LocalCodeEditorEvent, LocalCodeEditorView, render_unsaved_circle_with_tooltip,};use crate::code::view::PendingSaveIntent;use crate::code_review::DiffSetScope;use crate::code_review::comments::{ AttachedReviewCommentTarget, CommentId, ReviewCommentBatch, ReviewCommentBatchEvent,};use crate::code_review::context::convert_file_diffs_to_diffset_hunks;#[cfg(feature = "local_fs")]use crate::code_review::context::{ create_attachment_reference_and_key, register_diffset_attachment,};use crate::code_review::diff_selector::{DiffSelector, DiffSelectorEvent, DiffTarget};use crate::code_review::diff_state::{ DiffHunk, DiffLineType, DiffMode, DiffState, DiffStateModel, DiffStateModelEvent, DiffStats, FileDiff, FileDiffAndContent, FileStatusInfo, GitDiffWithBaseContent, GitFileStatus,};use crate::code_review::editor_state::CodeReviewEditorState;use crate::code_review::find_model::CodeReviewFindModel;use crate::code_review::git_repo_model::{GitRepoModels, GitRepoStatusEvent, GitRepoStatusModel};use crate::code_review::github_repo_model::{GitHubRepoEvent, GitHubRepoModel};use crate::code_review::hidden_lines::calculate_hidden_lines;#[cfg(feature = "local_fs")]use crate::code_review::telemetry_event::DiffSetContextScope;use crate::code_review::telemetry_event::{ AddToContextOrigin, CodeReviewContextDestination, CodeReviewTelemetryEvent, GitButtonKind, PaneStateChange,};use crate::coding_panel_enablement_state::CodingPanelEnablementState;use crate::editor::InteractionState;use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields};use crate::pane_group::PaneId;use crate::pane_group::focus_state::{PaneFocusHandle, PaneGroupFocusEvent};use crate::pane_group::pane::{BackingView, PaneEvent, view};use crate::quit_warning::UnsavedStateSummary;use crate::send_telemetry_from_ctx;#[cfg(feature = "local_fs")]use crate::server::telemetry::CodePanelsFileOpenEntrypoint;use crate::settings::{AISettings, CodeSettings};use crate::settings_view::SettingsSection;use crate::terminal::cli_agent::{ build_selection_line_range_prompt, build_selection_substring_prompt,};use crate::terminal::input::MenuPositioning;use crate::terminal::view::{CliAgentRouting, InitProjectModel, TerminalAction, TerminalView};use crate::themes::theme::WarpTheme;use crate::ui_components::blended_colors::{neutral_2, neutral_3};use crate::ui_components::buttons::icon_button_with_color;use crate::ui_components::dialog::{Dialog, dialog_styles};use crate::ui_components::icons::Icon;use crate::ui_components::render_file_search_row::{FileSearchRowOptions, render_file_search_row};use crate::util::bindings::{ CustomAction, custom_tag_to_keystroke, keybinding_name_to_display_string,};#[cfg(feature = "local_fs")]use crate::util::file::external_editor::EditorSettings;use crate::util::git::{BranchEntry, PrInfo};#[cfg(feature = "local_fs")]use crate::util::openable_file_type::FileTarget;#[cfg(feature = "local_fs")]use crate::util::openable_file_type::resolve_file_target_with_editor_choice;use crate::view_components::DismissibleToast;use crate::view_components::action_button::{ ActionButton, ActionButtonTheme, AdjoinedSide, ButtonSize, DangerPrimaryTheme, KeystrokeSource, NakedTheme, PaneHeaderTheme, SecondaryTheme, TooltipAlignment,};use crate::view_components::find::{Event as FindViewEvent, Find, FindEvent, FindWithinBlockState};use crate::workspace::view::right_panel::{ReviewDestination, ReviewSubmissionResult};use crate::workspace::{ToastStack, Workspace, WorkspaceAction}; pub struct CodeReviewHeaderFields { pub is_in_split_pane: bool, pub diff_state_model: ModelHandle<DiffStateModel>, pub maximize_button: ViewHandle<ActionButton>, pub diff_selector: ViewHandle<DiffSelector>, pub header_menu: ViewHandle<Menu<CodeReviewAction>>, pub header_menu_open: bool, pub header_dropdown_button: ViewHandle<ActionButton>, pub has_header_menu_items: bool, pub file_nav_button: Option<ViewHandle<ActionButton>>, pub primary_git_action_mode: PrimaryGitActionMode, pub git_primary_action_button: ViewHandle<ActionButton>, pub git_operations_chevron: ViewHandle<ActionButton>, pub git_operations_menu: ViewHandle<Menu<CodeReviewAction>>, pub git_operations_menu_open: bool,} #[derive(Clone, Debug, PartialEq)]pub struct CodeReviewCommentDebugState { pub repo_path: Option<LocalOrRemotePath>, pub has_active_comment_model: bool, pub comment_list: CommentListDebugState,} /// Renders a file navigation button (sidebar toggle) that can be reused across views.pub fn render_file_navigation_button<F>( appearance: &Appearance, is_sidebar_expanded: bool, mouse_state: MouseStateHandle, on_click: F,) -> Box<dyn Element>where F: Fn(&mut warpui::EventContext<'_>) + 'static,{ let ui_builder = appearance.ui_builder().clone(); let icon_color = appearance .theme() .sub_text_color(appearance.theme().background()); let button = icon_button_with_color( appearance, if is_sidebar_expanded { Icon::LeftSidebarClose } else { Icon::LeftSidebarOpen }, false, mouse_state, icon_color, ) .with_tooltip(move || { ui_builder .tool_tip(if is_sidebar_expanded { "Hide file navigation".to_owned() } else { "Show file navigation".to_owned() }) .build() .finish() }) .with_tooltip_position(warpui::ui_components::button::ButtonTooltipPosition::BelowLeft) .build() .on_click(move |ctx: &mut warpui::EventContext<'_>, _, _| { on_click(ctx); }); Container::new( ConstrainedBox::new(button.finish()) .with_height(24.) .with_width(24.) .finish(), ) .with_margin_right(4.) .finish()} /// Determines which primary git action the code review header should present.#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(crate) enum PrimaryGitActionMode { /// There are uncommitted changes. Primary = Commit, dropdown shows /// Commit / Push-or-Publish / Create PR with per-item disabled states. Commit, /// Branch has an upstream and unpushed local commits. Primary = Push, /// dropdown shows Commit (greyed) / Push / Create PR. Push, /// Nothing to commit or push, and no existing PR. Primary = Create PR, chevron hidden. CreatePr, /// Nothing to commit or push, and a PR exists for this branch. Primary = PR #N, chevron hidden. ViewPr, /// No upstream tracking branch, but local commits exist. Primary = Publish, chevron hidden. Publish,} const DEFAULT_FILE_SIDEBAR_WIDTH: f32 = 250.;const FILE_SIDEBAR_MIN_WIDTH: f32 = 150.;const FILE_SIDEBAR_MAX_WIDTH: f32 = 800.;/// This is a best guess effort at getting the height of the file header./// If the file header is changed, we should update this value.const FILE_HEADER_HEIGHT: f32 = 41.;/// The gap between editors in the viewported list.const EDITOR_GAP: f32 = 12.;const FILE_SIDEBAR_PANE_WIDTH_PERCENTAGE: f32 = 0.25;/// Vertical gap between the right panel header row and the code review content below it/// (sub-header in loaded state, loading text in loading state).pub(super) const CONTENT_TOP_MARGIN: f32 = 4.;/// Horizontal margins for the code review content area. The right panel header/// uses these same values so its edges align with the content below.pub(crate) const CONTENT_LEFT_MARGIN: f32 = 16.;pub(crate) const CONTENT_RIGHT_MARGIN: f32 = 4.;const CODE_REVIEW_EDITOR_LINE_HEIGHT_RATIO: f32 = 1.4;/// Extra scroll buffer (in pixels) added when scrolling to a line that has a comment editor below it.const COMMENT_EDITOR_SCROLL_BUFFER: f32 = 200.0; pub const CODE_REVIEW_TOOLTIP_TEXT: &str = "View changes";const REMOTE_TEXT: &str = "Diffs only work for local workspaces.";const DISABLED_TEXT: &str = "Diffs only work for git repositories.";const WSL_TEXT: &str = "Diffs don't currently work in WSL."; pub fn get_discard_button_disabled_tooltip(git_operation_blocked: bool) -> String { if git_operation_blocked { "Cannot discard changes while a git operation (merge, rebase, etc.) is in progress" .to_string() } else { "No changes to discard".to_string() }} /// Builds the tooltip text for the file navigation toggle button, including the/// live shortcut for `code_review:toggle_file_navigation` when one is bound.fn file_nav_button_tooltip(is_sidebar_expanded: bool, app: &AppContext) -> String { let label = if is_sidebar_expanded { "Hide file navigation" } else { "Show file navigation" }; match keybinding_name_to_display_string("code_review:toggle_file_navigation", app) { Some(shortcut) => format!("{label} ({shortcut})"), None => label.to_string(), }} /// Returns true if the file status changed between Deleted and non-Deleted states,/// which requires rebuilding the editor state because we can't use global buffer/// for files that don't exist on the file system.#[cfg(not(target_family = "wasm"))]fn file_status_changed_deleted_state( current_status: &GitFileStatus, new_status: &GitFileStatus,) -> bool { matches!(current_status, GitFileStatus::Deleted) != matches!(new_status, GitFileStatus::Deleted)} #[cfg(target_family = "wasm")]fn file_status_changed_deleted_state( _current_status: &GitFileStatus, _new_status: &GitFileStatus,) -> bool { false} #[derive(Clone, Debug, PartialEq)]pub enum CodeReviewAction { OpenInNewTab { path: String, line_and_column: Option<LineAndColumnArg>, }, ToggleFileExpanded(String), OpenHeaderMenu, SetDiffMode(DiffMode), ToggleFileSidebar, FileSelected(usize), ToggleMaximize, SaveAllUnsavedFiles, SaveAllFiles { paths: Vec<String>, }, RefreshGitState, UndoRevert, Close, EmitPaneEvent(PaneEvent), ShowDiscardConfirmDialog(Option<String>), ConfirmDiscardFile, CancelDiscardFile, ToggleStashChanges, ToggleFileSelection(String), AddDiffSetAsContext(DiffSetScope), CopyFilePath(String), OpenCommentComposerFromHeader, ShowFindBar, FocusView, InitProjectForCurrentDirectory, OpenRepository, OpenCommitDialog, ToggleGitOperationsMenu, OpenPushDialog, OpenCreatePrDialog, ViewPr(String), PublishBranch, SubmitReviewComments,} pub struct FileState { pub file_diff: FileDiff, pub editor_state: Option<CodeReviewEditorState>, pub is_expanded: bool, sidebar_mouse_state: MouseStateHandle, header_mouse_state: MouseStateHandle, chevron_button: ViewHandle<ActionButton>, open_in_tab_button: ViewHandle<ActionButton>, discard_button: ViewHandle<ActionButton>, add_context_button: ViewHandle<ActionButton>, copy_path_button: ViewHandle<ActionButton>,} pub(crate) struct LoadedState { /// Repo-relative file paths keyed as Strings; absolute file identities use /// StandardizedPath or LocalOrRemotePath at API boundaries. pub(crate) file_states: IndexMap<String, FileState>, pub(crate) total_additions: usize, pub(crate) total_deletions: usize, pub(crate) files_changed: usize,} impl LoadedState { pub(crate) fn to_diff_stats(&self) -> DiffStats { DiffStats { files_changed: self.files_changed, total_additions: self.total_additions, total_deletions: self.total_deletions, } } /// Returns a list of pairs of code editor views and the host-aware /// absolute paths of their underlying files. fn editor_absolute_file_paths( &self, repo_path: &LocalOrRemotePath, ) -> Vec<(ViewHandle<LocalCodeEditorView>, LocalOrRemotePath)> { self.file_states .values() .filter_map(|file_state| { let editor = file_state.editor_state.as_ref()?.editor().clone(); let file_location = repo_path.join(&file_state.file_diff.file_path); Some((editor, file_location)) }) .collect() }} /// State of the code review viewenum CodeReviewViewState { None, Loaded(LoadedState), Error(String), NoRepoFound,} struct UiStateHandles { sidebar_scroll_state: ClippedScrollStateHandle, sidebar_resizable_state: ResizableStateHandle, retry_button_mouse_state: MouseStateHandle,} impl Default for UiStateHandles { fn default() -> Self { Self { sidebar_scroll_state: Default::default(), sidebar_resizable_state: resizable_state_handle(DEFAULT_FILE_SIDEBAR_WIDTH), retry_button_mouse_state: Default::default(), } }} #[cfg_attr(target_family = "wasm", allow(dead_code))]struct GitSessionState { enablement: CodingPanelEnablementState,} #[derive(Clone, Debug)]pub enum CodeReviewViewEvent { Pane(PaneEvent), #[cfg(feature = "local_fs")] OpenFileWithTarget { path: PathBuf, target: FileTarget, line_col: Option<LineAndColumnArg>, }, ReviewSubmitted, /// Emitted when review comments are ready to be submitted. /// A higher-level view (RightPanelView) handles routing to an available terminal. SubmitReviewComments { comments: AgentReviewCommentBatch, repo_path: LocalOrRemotePath, }, /// Request to open a file in a new tab (e.g. goto-definition). OpenFileInNewTab { path: LocalOrRemotePath, line_and_column: Option<LineAndColumnArg>, }, /// Request to open LSP logs for the given log file path. #[cfg(not(target_family = "wasm"))] OpenLspLogs { log_path: PathBuf, },} #[derive(Clone, Debug, PartialEq)]pub enum DiscardOperationType { AllUncommittedChanges, FileUncommittedChanges, AllChangesAgainstBranch(Option<String>), FileChangesAgainstBranch(Option<String>),} impl DiscardOperationType { pub fn title(&self) -> String { match self { DiscardOperationType::AllUncommittedChanges => { "Discard uncommitted changes?".to_string() } DiscardOperationType::FileUncommittedChanges => { "Discard all uncommitted changes to file?".to_string() } DiscardOperationType::AllChangesAgainstBranch(_) => "Discard all changes?".to_string(), DiscardOperationType::FileChangesAgainstBranch(_) => { "Discard all changes to file?".to_string() } } } pub fn description(&self) -> Option<String> { match self { DiscardOperationType::AllUncommittedChanges => Some("You're about to discard all local changes that haven't been committed.".to_string()), DiscardOperationType::FileUncommittedChanges => Some("This will restore this file to the last committed version and discard local edits.".to_string()), DiscardOperationType::AllChangesAgainstBranch(None) => Some("You're about to discard all committed and uncommitted changes.".to_string()), DiscardOperationType::FileChangesAgainstBranch(None) => Some("This will restore this file to the main branch version and discard all committed and uncommitted edits.".to_string()), DiscardOperationType::AllChangesAgainstBranch(Some(_)) => Some("You're about to discard all committed and uncommitted changes.".to_string()), DiscardOperationType::FileChangesAgainstBranch(Some(branch)) => Some(format!("This will reset this file to the {branch} branch version and discard all committed and uncommitted edits.")), } } fn is_uncommitted_changes(&self) -> bool { matches!( self, DiscardOperationType::AllUncommittedChanges | DiscardOperationType::FileUncommittedChanges )Lines 1–500 of 7758. Load more
use std::collections::{HashMap, HashSet}; use std::mem; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use ai::project_context::model::ProjectContextModel; use indexmap::IndexMap; use itertools::Itertools; #[cfg(feature = "local_fs")] use num_traits::SaturatingSub; use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::{Vector2F, vec2f}; use rand::Rng; use rand::distributions::Alphanumeric; use string_offset::CharOffset; use vec1::Vec1; use warp_core::channel::{Channel, ChannelState}; use warp_core::features::FeatureFlag; use warp_core::ui::theme::color::internal_colors; use warp_core::{SessionId, safe_error, safe_info}; use warp_editor::content::buffer::{AutoScrollBehavior, InitialBufferState, SelectionOffsets}; use warp_editor::model::CoreEditorModel; use warp_editor::render::element::VerticalExpansionBehavior; #[cfg(not(target_family = "wasm"))] use warp_editor::render::model::AutoScrollMode; use warp_editor::render::model::LineCount; use warp_util::content_version::ContentVersion; use warp_util::path::LineAndColumnArg; use warp_util::standardized_path::StandardizedPath; use warpui::clipboard::ClipboardContent; use warpui::elements::new_scrollable::{ NewScrollable, NewScrollableElement, ScrollableAppearance, SingleAxisConfig, }; use warpui::elements::{ Align, Border, ChildAnchor, ChildView, Clipped, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DEFAULT_UI_LINE_HEIGHT_RATIO, DispatchEventResult, DragBarSide, Element, Empty, EventHandler, Flex, Hoverable, List, ListState, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Percentage, PositionedElementAnchor, PositionedElementOffsetBounds, Radius, Rect, Resizable, ResizableStateHandle, SavePosition, ScrollOffset, ScrollStateHandle, ScrollbarWidth, Shrinkable, Stack, Text, resizable_state_handle, }; use warpui::fonts::{Properties, Weight}; use warpui::keymap::Keystroke; use warpui::platform::Cursor; use warpui::text_layout::{ClipConfig, default_compute_baseline_position}; use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment}; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use warpui::units::Pixels; use warpui::{ AppContext, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle, WindowId, }; use super::code_review_header::CodeReviewHeader; use super::comment_list_view::{CommentListDebugState, CommentListEvent, CommentListView}; use super::comments::{AttachedReviewComment, CommentOrigin, attach_pending_imported_comments}; use super::diff_size_limits::DiffSize; use super::git_dialog::{GitDialog, GitDialogEvent, GitDialogKind}; use super::{GlobalCodeReviewEvent, GlobalCodeReviewModel}; #[cfg(feature = "local_fs")] use crate::TelemetryEvent; use crate::ai::agent::{ AIAgentAttachment, AgentReviewCommentBatch, CurrentHead, DiffBase, DiffSetHunk, }; use crate::ai::blocklist::agent_view::AgentViewEntryOrigin; use crate::appearance::Appearance; use crate::code::ShowCommentEditorProvider; #[cfg(not(target_family = "wasm"))] use crate::code::ShowFindReferencesCard; use crate::code::buffer_location::LocalOrRemotePath; use crate::code::editor::comment_editor::DEFAULT_COMMENT_MAX_WIDTH; use crate::code::editor::line::EditorLineLocation; use crate::code::editor::view::{CodeEditorEvent, CodeEditorRenderOptions, CodeEditorView}; use crate::code::editor::{ CommentEditor, CommentEditorEvent, EditorCommentsModel, EditorReviewComment, GutterHoverTarget, add_color, remove_color, }; use crate::code::editor_management::CodeEditorStatus; use crate::code::footer::{CodeFooterView, CodeFooterViewEvent}; use crate::code::global_buffer_model::GlobalBufferModel; use crate::code::local_code_editor::{ LocalCodeEditorEvent, LocalCodeEditorView, render_unsaved_circle_with_tooltip, }; use crate::code::view::PendingSaveIntent; use crate::code_review::DiffSetScope; use crate::code_review::comments::{ AttachedReviewCommentTarget, CommentId, ReviewCommentBatch, ReviewCommentBatchEvent, }; use crate::code_review::context::convert_file_diffs_to_diffset_hunks; #[cfg(feature = "local_fs")] use crate::code_review::context::{ create_attachment_reference_and_key, register_diffset_attachment, }; use crate::code_review::diff_selector::{DiffSelector, DiffSelectorEvent, DiffTarget}; use crate::code_review::diff_state::{ DiffHunk, DiffLineType, DiffMode, DiffState, DiffStateModel, DiffStateModelEvent, DiffStats, FileDiff, FileDiffAndContent, FileStatusInfo, GitDiffWithBaseContent, GitFileStatus, }; use crate::code_review::editor_state::CodeReviewEditorState; use crate::code_review::find_model::CodeReviewFindModel; use crate::code_review::git_repo_model::{GitRepoModels, GitRepoStatusEvent, GitRepoStatusModel}; use crate::code_review::github_repo_model::{GitHubRepoEvent, GitHubRepoModel}; use crate::code_review::hidden_lines::calculate_hidden_lines; #[cfg(feature = "local_fs")] use crate::code_review::telemetry_event::DiffSetContextScope; use crate::code_review::telemetry_event::{ AddToContextOrigin, CodeReviewContextDestination, CodeReviewTelemetryEvent, GitButtonKind, PaneStateChange, }; use crate::coding_panel_enablement_state::CodingPanelEnablementState; use crate::editor::InteractionState; use crate::menu::{Event as MenuEvent, Menu, MenuItem, MenuItemFields}; use crate::pane_group::PaneId; use crate::pane_group::focus_state::{PaneFocusHandle, PaneGroupFocusEvent}; use crate::pane_group::pane::{BackingView, PaneEvent, view}; use crate::quit_warning::UnsavedStateSummary; use crate::send_telemetry_from_ctx; #[cfg(feature = "local_fs")] use crate::server::telemetry::CodePanelsFileOpenEntrypoint; use crate::settings::{AISettings, CodeSettings}; use crate::settings_view::SettingsSection; use crate::terminal::cli_agent::{ build_selection_line_range_prompt, build_selection_substring_prompt, }; use crate::terminal::input::MenuPositioning; use crate::terminal::view::{CliAgentRouting, InitProjectModel, TerminalAction, TerminalView}; use crate::themes::theme::WarpTheme; use crate::ui_components::blended_colors::{neutral_2, neutral_3}; use crate::ui_components::buttons::icon_button_with_color; use crate::ui_components::dialog::{Dialog, dialog_styles}; use crate::ui_components::icons::Icon; use crate::ui_components::render_file_search_row::{FileSearchRowOptions, render_file_search_row}; use crate::util::bindings::{ CustomAction, custom_tag_to_keystroke, keybinding_name_to_display_string, }; #[cfg(feature = "local_fs")] use crate::util::file::external_editor::EditorSettings; use crate::util::git::{BranchEntry, PrInfo}; #[cfg(feature = "local_fs")] use crate::util::openable_file_type::FileTarget; #[cfg(feature = "local_fs")] use crate::util::openable_file_type::resolve_file_target_with_editor_choice; use crate::view_components::DismissibleToast; use crate::view_components::action_button::{ ActionButton, ActionButtonTheme, AdjoinedSide, ButtonSize, DangerPrimaryTheme, KeystrokeSource, NakedTheme, PaneHeaderTheme, SecondaryTheme, TooltipAlignment, }; use crate::view_components::find::{Event as FindViewEvent, Find, FindEvent, FindWithinBlockState}; use crate::workspace::view::right_panel::{ReviewDestination, ReviewSubmissionResult}; use crate::workspace::{ToastStack, Workspace, WorkspaceAction}; pub struct CodeReviewHeaderFields { pub is_in_split_pane: bool, pub diff_state_model: ModelHandle<DiffStateModel>, pub maximize_button: ViewHandle<ActionButton>, pub diff_selector: ViewHandle<DiffSelector>, pub header_menu: ViewHandle<Menu<CodeReviewAction>>, pub header_menu_open: bool, pub header_dropdown_button: ViewHandle<ActionButton>, pub has_header_menu_items: bool, pub file_nav_button: Option<ViewHandle<ActionButton>>, pub primary_git_action_mode: PrimaryGitActionMode, pub git_primary_action_button: ViewHandle<ActionButton>, pub git_operations_chevron: ViewHandle<ActionButton>, pub git_operations_menu: ViewHandle<Menu<CodeReviewAction>>, pub git_operations_menu_open: bool, } #[derive(Clone, Debug, PartialEq)] pub struct CodeReviewCommentDebugState { pub repo_path: Option<LocalOrRemotePath>, pub has_active_comment_model: bool, pub comment_list: CommentListDebugState, } /// Renders a file navigation button (sidebar toggle) that can be reused across views. pub fn render_file_navigation_button<F>( appearance: &Appearance, is_sidebar_expanded: bool, mouse_state: MouseStateHandle, on_click: F, ) -> Box<dyn Element> where F: Fn(&mut warpui::EventContext<'_>) + 'static, { let ui_builder = appearance.ui_builder().clone(); let icon_color = appearance .theme() .sub_text_color(appearance.theme().background()); let button = icon_button_with_color( appearance, if is_sidebar_expanded { Icon::LeftSidebarClose } else { Icon::LeftSidebarOpen }, false, mouse_state, icon_color, ) .with_tooltip(move || { ui_builder .tool_tip(if is_sidebar_expanded { "Hide file navigation".to_owned() } else { "Show file navigation".to_owned() }) .build() .finish() }) .with_tooltip_position(warpui::ui_components::button::ButtonTooltipPosition::BelowLeft) .build() .on_click(move |ctx: &mut warpui::EventContext<'_>, _, _| { on_click(ctx); }); Container::new( ConstrainedBox::new(button.finish()) .with_height(24.) .with_width(24.) .finish(), ) .with_margin_right(4.) .finish() } /// Determines which primary git action the code review header should present. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum PrimaryGitActionMode { /// There are uncommitted changes. Primary = Commit, dropdown shows /// Commit / Push-or-Publish / Create PR with per-item disabled states. Commit, /// Branch has an upstream and unpushed local commits. Primary = Push, /// dropdown shows Commit (greyed) / Push / Create PR. Push, /// Nothing to commit or push, and no existing PR. Primary = Create PR, chevron hidden. CreatePr, /// Nothing to commit or push, and a PR exists for this branch. Primary = PR #N, chevron hidden. ViewPr, /// No upstream tracking branch, but local commits exist. Primary = Publish, chevron hidden. Publish, } const DEFAULT_FILE_SIDEBAR_WIDTH: f32 = 250.; const FILE_SIDEBAR_MIN_WIDTH: f32 = 150.; const FILE_SIDEBAR_MAX_WIDTH: f32 = 800.; /// This is a best guess effort at getting the height of the file header. /// If the file header is changed, we should update this value. const FILE_HEADER_HEIGHT: f32 = 41.; /// The gap between editors in the viewported list. const EDITOR_GAP: f32 = 12.; const FILE_SIDEBAR_PANE_WIDTH_PERCENTAGE: f32 = 0.25; /// Vertical gap between the right panel header row and the code review content below it /// (sub-header in loaded state, loading text in loading state). pub(super) const CONTENT_TOP_MARGIN: f32 = 4.; /// Horizontal margins for the code review content area. The right panel header /// uses these same values so its edges align with the content below. pub(crate) const CONTENT_LEFT_MARGIN: f32 = 16.; pub(crate) const CONTENT_RIGHT_MARGIN: f32 = 4.; const CODE_REVIEW_EDITOR_LINE_HEIGHT_RATIO: f32 = 1.4; /// Extra scroll buffer (in pixels) added when scrolling to a line that has a comment editor below it. const COMMENT_EDITOR_SCROLL_BUFFER: f32 = 200.0; pub const CODE_REVIEW_TOOLTIP_TEXT: &str = "View changes"; const REMOTE_TEXT: &str = "Diffs only work for local workspaces."; const DISABLED_TEXT: &str = "Diffs only work for git repositories."; const WSL_TEXT: &str = "Diffs don't currently work in WSL."; pub fn get_discard_button_disabled_tooltip(git_operation_blocked: bool) -> String { if git_operation_blocked { "Cannot discard changes while a git operation (merge, rebase, etc.) is in progress" .to_string() } else { "No changes to discard".to_string() } } /// Builds the tooltip text for the file navigation toggle button, including the /// live shortcut for `code_review:toggle_file_navigation` when one is bound. fn file_nav_button_tooltip(is_sidebar_expanded: bool, app: &AppContext) -> String { let label = if is_sidebar_expanded { "Hide file navigation" } else { "Show file navigation" }; match keybinding_name_to_display_string("code_review:toggle_file_navigation", app) { Some(shortcut) => format!("{label} ({shortcut})"), None => label.to_string(), } } /// Returns true if the file status changed between Deleted and non-Deleted states, /// which requires rebuilding the editor state because we can't use global buffer /// for files that don't exist on the file system. #[cfg(not(target_family = "wasm"))] fn file_status_changed_deleted_state( current_status: &GitFileStatus, new_status: &GitFileStatus, ) -> bool { matches!(current_status, GitFileStatus::Deleted) != matches!(new_status, GitFileStatus::Deleted) } #[cfg(target_family = "wasm")] fn file_status_changed_deleted_state( _current_status: &GitFileStatus, _new_status: &GitFileStatus, ) -> bool { false } #[derive(Clone, Debug, PartialEq)] pub enum CodeReviewAction { OpenInNewTab { path: String, line_and_column: Option<LineAndColumnArg>, }, ToggleFileExpanded(String), OpenHeaderMenu, SetDiffMode(DiffMode), ToggleFileSidebar, FileSelected(usize), ToggleMaximize, SaveAllUnsavedFiles, SaveAllFiles { paths: Vec<String>, }, RefreshGitState, UndoRevert, Close, EmitPaneEvent(PaneEvent), ShowDiscardConfirmDialog(Option<String>), ConfirmDiscardFile, CancelDiscardFile, ToggleStashChanges, ToggleFileSelection(String), AddDiffSetAsContext(DiffSetScope), CopyFilePath(String), OpenCommentComposerFromHeader, ShowFindBar, FocusView, InitProjectForCurrentDirectory, OpenRepository, OpenCommitDialog, ToggleGitOperationsMenu, OpenPushDialog, OpenCreatePrDialog, ViewPr(String), PublishBranch, SubmitReviewComments, } pub struct FileState { pub file_diff: FileDiff, pub editor_state: Option<CodeReviewEditorState>, pub is_expanded: bool, sidebar_mouse_state: MouseStateHandle, header_mouse_state: MouseStateHandle, chevron_button: ViewHandle<ActionButton>, open_in_tab_button: ViewHandle<ActionButton>, discard_button: ViewHandle<ActionButton>, add_context_button: ViewHandle<ActionButton>, copy_path_button: ViewHandle<ActionButton>, } pub(crate) struct LoadedState { /// Repo-relative file paths keyed as Strings; absolute file identities use /// StandardizedPath or LocalOrRemotePath at API boundaries. pub(crate) file_states: IndexMap<String, FileState>, pub(crate) total_additions: usize, pub(crate) total_deletions: usize, pub(crate) files_changed: usize, } impl LoadedState { pub(crate) fn to_diff_stats(&self) -> DiffStats { DiffStats { files_changed: self.files_changed, total_additions: self.total_additions, total_deletions: self.total_deletions, } } /// Returns a list of pairs of code editor views and the host-aware /// absolute paths of their underlying files. fn editor_absolute_file_paths( &self, repo_path: &LocalOrRemotePath, ) -> Vec<(ViewHandle<LocalCodeEditorView>, LocalOrRemotePath)> { self.file_states .values() .filter_map(|file_state| { let editor = file_state.editor_state.as_ref()?.editor().clone(); let file_location = repo_path.join(&file_state.file_diff.file_path); Some((editor, file_location)) }) .collect() } } /// State of the code review view enum CodeReviewViewState { None, Loaded(LoadedState), Error(String), NoRepoFound, } struct UiStateHandles { sidebar_scroll_state: ClippedScrollStateHandle, sidebar_resizable_state: ResizableStateHandle, retry_button_mouse_state: MouseStateHandle, } impl Default for UiStateHandles { fn default() -> Self { Self { sidebar_scroll_state: Default::default(), sidebar_resizable_state: resizable_state_handle(DEFAULT_FILE_SIDEBAR_WIDTH), retry_button_mouse_state: Default::default(), } } } #[cfg_attr(target_family = "wasm", allow(dead_code))] struct GitSessionState { enablement: CodingPanelEnablementState, } #[derive(Clone, Debug)] pub enum CodeReviewViewEvent { Pane(PaneEvent), #[cfg(feature = "local_fs")] OpenFileWithTarget { path: PathBuf, target: FileTarget, line_col: Option<LineAndColumnArg>, }, ReviewSubmitted, /// Emitted when review comments are ready to be submitted. /// A higher-level view (RightPanelView) handles routing to an available terminal. SubmitReviewComments { comments: AgentReviewCommentBatch, repo_path: LocalOrRemotePath, }, /// Request to open a file in a new tab (e.g. goto-definition). OpenFileInNewTab { path: LocalOrRemotePath, line_and_column: Option<LineAndColumnArg>, }, /// Request to open LSP logs for the given log file path. #[cfg(not(target_family = "wasm"))] OpenLspLogs { log_path: PathBuf, }, } #[derive(Clone, Debug, PartialEq)] pub enum DiscardOperationType { AllUncommittedChanges, FileUncommittedChanges, AllChangesAgainstBranch(Option<String>), FileChangesAgainstBranch(Option<String>), } impl DiscardOperationType { pub fn title(&self) -> String { match self { DiscardOperationType::AllUncommittedChanges => { "Discard uncommitted changes?".to_string() } DiscardOperationType::FileUncommittedChanges => { "Discard all uncommitted changes to file?".to_string() } DiscardOperationType::AllChangesAgainstBranch(_) => "Discard all changes?".to_string(), DiscardOperationType::FileChangesAgainstBranch(_) => { "Discard all changes to file?".to_string() } } } pub fn description(&self) -> Option<String> { match self { DiscardOperationType::AllUncommittedChanges => Some("You're about to discard all local changes that haven't been committed.".to_string()), DiscardOperationType::FileUncommittedChanges => Some("This will restore this file to the last committed version and discard local edits.".to_string()), DiscardOperationType::AllChangesAgainstBranch(None) => Some("You're about to discard all committed and uncommitted changes.".to_string()), DiscardOperationType::FileChangesAgainstBranch(None) => Some("This will restore this file to the main branch version and discard all committed and uncommitted edits.".to_string()), DiscardOperationType::AllChangesAgainstBranch(Some(_)) => Some("You're about to discard all committed and uncommitted changes.".to_string()), DiscardOperationType::FileChangesAgainstBranch(Some(branch)) => Some(format!("This will reset this file to the {branch} branch version and discard all committed and uncommitted edits.")), } } fn is_uncommitted_changes(&self) -> bool { matches!( self, DiscardOperationType::AllUncommittedChanges | DiscardOperationType::FileUncommittedChanges )