first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,21 +1,55 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use warpui::elements::{
|
||||
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Empty, Flex, Hoverable, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Radius, Stack, Text, DEFAULT_UI_LINE_HEIGHT_RATIO,
|
||||
};
|
||||
use warpui::fonts::{Cache, FamilyId, Properties, Weight};
|
||||
use warpui::keymap::Keystroke;
|
||||
use warpui::platform::Cursor;
|
||||
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use warpui::{
|
||||
AppContext, Element, Entity, EntityId, Gradient, ModelHandle, SingletonEntity, TypedActionView,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use super::directory_fetcher::{
|
||||
DirectoryFetcher, DirectoryFetcherEvent, DirectoryItem, DirectoryType,
|
||||
};
|
||||
use super::display_menu::{
|
||||
ChipMenuType, DisplayChipMenu, FixedFooter, GenericMenuItem, PromptDisplayMenuEvent,
|
||||
};
|
||||
use super::{
|
||||
agent_view_chip_color, github_pr_display_text_from_url, render_text_from_kind, ChipResult,
|
||||
ChipValue, ContextChipKind,
|
||||
};
|
||||
use crate::ai::blocklist::agent_view::AgentViewController;
|
||||
use crate::ai::blocklist::prompt::plan_and_todo_list::{PlanAndTodoListEvent, PlanAndTodoListView};
|
||||
use crate::ai::{
|
||||
blocklist::{BlocklistAIContextModel, BlocklistAIInputModel},
|
||||
document::ai_document_model::{AIDocumentId, AIDocumentVersion},
|
||||
};
|
||||
use crate::ai::blocklist::{BlocklistAIContextModel, BlocklistAIInputModel};
|
||||
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::code::editor::{add_color, remove_color};
|
||||
use crate::code_review::code_review_view::CODE_REVIEW_TOOLTIP_TEXT;
|
||||
use crate::code_review::diff_state::DiffStats;
|
||||
use crate::completer::SessionContext;
|
||||
use crate::context_chips::git_branch_on_click::{
|
||||
is_plausible_new_branch_name, GitBranchOnClickValue,
|
||||
};
|
||||
use crate::context_chips::node_version_popup::{NodeVersionPopupEvent, NodeVersionPopupView};
|
||||
use crate::context_chips::spacing;
|
||||
use crate::settings::{AISettings, AISettingsChangedEvent, InputSettings};
|
||||
use crate::settings_view::keybindings::{KeybindingChangedEvent, KeybindingChangedNotifier};
|
||||
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
|
||||
use crate::terminal::input::{MenuPositioning, MenuPositioningProvider};
|
||||
use crate::terminal::model::session::SessionType;
|
||||
use crate::terminal::model_events::ModelEventDispatcher;
|
||||
use crate::terminal::view::ambient_agent::AmbientAgentViewModel;
|
||||
use crate::ui_components::blended_colors;
|
||||
@@ -24,40 +58,8 @@ use crate::util::bindings::keybinding_name_to_display_string;
|
||||
use crate::util::truncation::truncate_from_beginning;
|
||||
use crate::view_components::action_button::{ActionButtonTheme, NakedTheme};
|
||||
use crate::view_components::{FeaturePopup, NewFeaturePopupEvent, NewFeaturePopupLabel};
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::{features::FeatureFlag, ui::theme::color::internal_colors};
|
||||
use galaxyui::elements::Empty;
|
||||
use galaxyui::keymap::Keystroke;
|
||||
use galaxyui::platform::Cursor;
|
||||
use galaxyui::ui_components::components::UiComponentStyles;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Flex, Hoverable, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Radius, Stack, Text, DEFAULT_UI_LINE_HEIGHT_RATIO,
|
||||
},
|
||||
fonts::{Cache, FamilyId, Properties, Weight},
|
||||
AppContext, Element, Entity, EntityId, Gradient, ModelHandle, SingletonEntity, TypedActionView,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::appearance::Appearance;
|
||||
use crate::completer::SessionContext;
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
|
||||
use super::{
|
||||
agent_view_chip_color,
|
||||
directory_fetcher::{DirectoryFetcher, DirectoryFetcherEvent, DirectoryItem, DirectoryType},
|
||||
display_menu::{
|
||||
ChipMenuType, DisplayChipMenu, FixedFooter, GenericMenuItem, PromptDisplayMenuEvent,
|
||||
},
|
||||
github_pr_display_text_from_url, render_text_from_kind, ChipResult, ContextChipKind,
|
||||
};
|
||||
use crate::workspace::view::TOGGLE_RIGHT_PANEL_BINDING_NAME;
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
|
||||
/// Helper function to render git diff stats content (file icon or +- icons, file count, bullet, +/- counts)
|
||||
/// Used by both the context chips and the AI control panel
|
||||
@@ -164,6 +166,62 @@ pub fn render_git_diff_stats_content(
|
||||
git_content.finish()
|
||||
}
|
||||
|
||||
fn git_branch_status_icon(icon: Icon, color: ColorU, icon_size: f32) -> Box<dyn Element> {
|
||||
Container::new(
|
||||
ConstrainedBox::new(icon.to_warpui_icon(Fill::Solid(color)).finish())
|
||||
.with_height(icon_size)
|
||||
.with_width(icon_size)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn git_branch_status_text(
|
||||
text: String,
|
||||
color: ColorU,
|
||||
font_family: FamilyId,
|
||||
font_size: f32,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
Text::new_inline(text, font_family, font_size)
|
||||
.with_color(Fill::Solid(color).into())
|
||||
.with_line_height_ratio(appearance.line_height_ratio())
|
||||
.with_style(Properties::default().weight(Weight::Semibold))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn git_branch_status_count(
|
||||
icon: Icon,
|
||||
count: String,
|
||||
color: ColorU,
|
||||
font_family: FamilyId,
|
||||
font_size: f32,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let mut content = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
add_git_branch_status_child(
|
||||
&mut content,
|
||||
git_branch_status_icon(icon, color, GIT_BRANCH_STATUS_STATUS_ICON_SIZE),
|
||||
GIT_BRANCH_STATUS_COUNT_GAP,
|
||||
);
|
||||
content.add_child(git_branch_status_text(
|
||||
count,
|
||||
color,
|
||||
font_family,
|
||||
font_size,
|
||||
appearance,
|
||||
));
|
||||
content.finish()
|
||||
}
|
||||
|
||||
fn add_git_branch_status_child(content: &mut Flex, child: Box<dyn Element>, margin_right: f32) {
|
||||
content.add_child(
|
||||
Container::new(child)
|
||||
.with_margin_right(margin_right)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
const PROMPT_CHIP_DISPLAY_ID: &str = "PromptChipDisplay";
|
||||
const DROP_SHADOW_COLOR: ColorU = ColorU {
|
||||
r: 0,
|
||||
@@ -176,6 +234,12 @@ const CHIP_MARGIN_RIGHT: f32 = 8.;
|
||||
const UDI_CHIP_MAX_NUM_CHARACTERS: usize = 40;
|
||||
|
||||
const CHIP_CORNER_RADIUS: f32 = 4.0;
|
||||
const GIT_BRANCH_STATUS_CHIP_HORIZONTAL_PADDING: f32 = 8.0;
|
||||
const GIT_BRANCH_STATUS_CHIP_VERTICAL_PADDING: f32 = 4.0;
|
||||
const GIT_BRANCH_STATUS_MAIN_GAP: f32 = 4.0;
|
||||
const GIT_BRANCH_STATUS_COUNT_GAP: f32 = 2.0;
|
||||
const GIT_BRANCH_STATUS_BRANCH_ICON_SIZE: f32 = 14.0;
|
||||
const GIT_BRANCH_STATUS_STATUS_ICON_SIZE: f32 = 12.0;
|
||||
pub(crate) const CHIP_BORDER_WIDTH: f32 = 1.0;
|
||||
/// Inner rounded corners are 1px smaller than the outer border radius
|
||||
const CHIP_INNER_CORNER_RADIUS: f32 = CHIP_CORNER_RADIUS - CHIP_BORDER_WIDTH;
|
||||
@@ -284,10 +348,11 @@ pub struct DisplayChip {
|
||||
mouse_state: MouseStateHandle,
|
||||
diff_stats_mouse_state: MouseStateHandle,
|
||||
text: String,
|
||||
value: Option<ChipValue>,
|
||||
chip_kind: ContextChipKind,
|
||||
display_chip_kind: DisplayChipKind,
|
||||
next_chip_kind: Option<ContextChipKind>,
|
||||
first_on_click_value: Option<String>,
|
||||
on_click_values: Vec<String>,
|
||||
quota_reset_popup: ViewHandle<FeaturePopup>,
|
||||
session_context: Option<SessionContext>,
|
||||
menu_positioning_provider: Arc<dyn MenuPositioningProvider>,
|
||||
@@ -355,6 +420,198 @@ impl GitLineChanges {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GitBranchTrackingStatus {
|
||||
pub branch: String,
|
||||
pub upstream: Option<String>,
|
||||
pub ahead: u32,
|
||||
pub behind: u32,
|
||||
pub counts_available: bool,
|
||||
#[serde(default)]
|
||||
pub rebased: bool,
|
||||
}
|
||||
|
||||
impl GitBranchTrackingStatus {
|
||||
pub fn new(branch: String, upstream: Option<String>, ahead: u32, behind: u32) -> Self {
|
||||
let counts_available = upstream.is_some();
|
||||
Self {
|
||||
branch,
|
||||
upstream,
|
||||
ahead,
|
||||
behind,
|
||||
counts_available,
|
||||
rebased: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_counts(branch: String, upstream: Option<String>) -> Self {
|
||||
Self {
|
||||
branch,
|
||||
upstream,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
counts_available: false,
|
||||
rebased: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rebased(branch: String, upstream: String) -> Self {
|
||||
Self {
|
||||
branch,
|
||||
upstream: Some(upstream),
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
counts_available: true,
|
||||
rebased: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_display_text(text: &str) -> Option<Self> {
|
||||
let text = text.trim();
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some((branch, status_text)) = text.rsplit_once(" • ") else {
|
||||
return Some(Self {
|
||||
branch: text.to_string(),
|
||||
upstream: None,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
counts_available: false,
|
||||
rebased: false,
|
||||
});
|
||||
};
|
||||
|
||||
let Some((ahead, behind, rebased)) = Self::parse_display_status(status_text) else {
|
||||
return Some(Self {
|
||||
branch: text.to_string(),
|
||||
upstream: None,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
counts_available: false,
|
||||
rebased: false,
|
||||
});
|
||||
};
|
||||
|
||||
let branch = branch.trim();
|
||||
if branch.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
branch: branch.to_string(),
|
||||
upstream: None,
|
||||
ahead,
|
||||
behind,
|
||||
counts_available: true,
|
||||
rebased,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn display_text(&self) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if self.is_rebased() {
|
||||
parts.push("⇅".to_string());
|
||||
} else {
|
||||
if let Some(ahead) = self.ahead_display_count() {
|
||||
parts.push(format!("↑{ahead}"));
|
||||
}
|
||||
if let Some(behind) = self.behind_display_count() {
|
||||
parts.push(format!("↓{behind}"));
|
||||
}
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
self.branch.clone()
|
||||
} else {
|
||||
format!("{} • {}", self.branch, parts.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_rebased(&self) -> bool {
|
||||
self.counts_available && self.rebased
|
||||
}
|
||||
|
||||
pub fn ahead_display_count(&self) -> Option<String> {
|
||||
(!self.is_rebased() && self.counts_available && self.ahead > 0)
|
||||
.then(|| Self::format_display_count(self.ahead))
|
||||
}
|
||||
|
||||
pub fn behind_display_count(&self) -> Option<String> {
|
||||
(!self.is_rebased() && self.counts_available && self.behind > 0)
|
||||
.then(|| Self::format_display_count(self.behind))
|
||||
}
|
||||
|
||||
fn format_display_count(count: u32) -> String {
|
||||
const MAX_DISPLAY_COUNT: u32 = 999;
|
||||
if count > MAX_DISPLAY_COUNT {
|
||||
format!("{MAX_DISPLAY_COUNT}+")
|
||||
} else {
|
||||
count.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_display_count(count: &str) -> Option<u32> {
|
||||
if let Some(capped_count) = count.strip_suffix('+') {
|
||||
capped_count.parse::<u32>().ok()?.checked_add(1)
|
||||
} else {
|
||||
count.parse::<u32>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_display_status(status_text: &str) -> Option<(u32, u32, bool)> {
|
||||
let mut ahead = 0;
|
||||
let mut behind = 0;
|
||||
let mut rebased = false;
|
||||
let mut saw_status_token = false;
|
||||
|
||||
for part in status_text.split_whitespace() {
|
||||
saw_status_token = true;
|
||||
if part == "⇅" {
|
||||
rebased = true;
|
||||
} else if let Some(ahead_count) = part.strip_prefix('↑') {
|
||||
ahead = Self::parse_display_count(ahead_count)?;
|
||||
} else if let Some(behind_count) = part.strip_prefix('↓') {
|
||||
behind = Self::parse_display_count(behind_count)?;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
saw_status_token.then_some((ahead, behind, rebased))
|
||||
}
|
||||
|
||||
fn tooltip_text(&self) -> String {
|
||||
match &self.upstream {
|
||||
Some(upstream) if self.is_rebased() => {
|
||||
format!("Tracking {upstream} • branch was rebased")
|
||||
}
|
||||
Some(upstream) if self.counts_available => format!(
|
||||
"Tracking {upstream} • ahead {}, behind {}",
|
||||
self.ahead, self.behind
|
||||
),
|
||||
Some(upstream) => {
|
||||
format!("Tracking {upstream}; ahead/behind counts are unavailable")
|
||||
}
|
||||
None if self.is_rebased() => {
|
||||
"Branch was rebased; upstream name is unavailable".to_string()
|
||||
}
|
||||
None if self.counts_available => format!(
|
||||
"Ahead {}, behind {}; upstream name is unavailable",
|
||||
self.ahead, self.behind
|
||||
),
|
||||
None => "No upstream configured".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GitBranchTrackingStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.display_text())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DisplayChipKind {
|
||||
Text,
|
||||
@@ -379,6 +636,9 @@ pub enum DisplayChipKind {
|
||||
menu_open: bool,
|
||||
menu: ViewHandle<DisplayChipMenu>,
|
||||
},
|
||||
GitBranchStatus {
|
||||
tracking_status: Option<GitBranchTrackingStatus>,
|
||||
},
|
||||
GithubPullRequest,
|
||||
GitDiffStats {
|
||||
line_changes_info: Option<GitLineChanges>,
|
||||
@@ -392,6 +652,7 @@ impl DisplayChipKind {
|
||||
DisplayChipKind::NodeVersion { popup_open, .. } => *popup_open,
|
||||
DisplayChipKind::GitBranch { menu_open, .. } => *menu_open,
|
||||
DisplayChipKind::GithubPullRequest
|
||||
| DisplayChipKind::GitBranchStatus { .. }
|
||||
| DisplayChipKind::GitDiffStats { .. }
|
||||
| DisplayChipKind::Text
|
||||
| DisplayChipKind::Ssh
|
||||
@@ -443,17 +704,88 @@ pub struct DisplayChipConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitBranch(String);
|
||||
|
||||
impl GitBranch {
|
||||
fn prompt_chip_command(&self) -> PromptChipShellCommand {
|
||||
let branch = GitBranchOnClickValue::decode(&self.0);
|
||||
if let Some(worktree_path) = branch.worktree_path {
|
||||
return PromptChipShellCommand::ChangeDirectory {
|
||||
dir_name: worktree_path,
|
||||
};
|
||||
}
|
||||
|
||||
if branch.is_linked_worktree {
|
||||
return PromptChipShellCommand::Echo {
|
||||
message: "The branch is already checked out in another worktree, but Warp couldn't find its path.",
|
||||
};
|
||||
}
|
||||
|
||||
PromptChipShellCommand::GitCheckout {
|
||||
branch_name: branch.branch_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_for_menu(&self) -> Icon {
|
||||
let branch = GitBranchOnClickValue::decode(&self.0);
|
||||
if branch.is_linked_worktree {
|
||||
Icon::Dataflow02
|
||||
} else {
|
||||
Icon::GitBranch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericMenuItem for GitBranch {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
self.0.clone()
|
||||
GitBranchOnClickValue::decode(&self.0).branch_name
|
||||
}
|
||||
|
||||
fn icon(&self, _app: &AppContext) -> Option<Icon> {
|
||||
Some(Icon::GitBranch)
|
||||
Some(self.icon_for_menu())
|
||||
}
|
||||
|
||||
fn action_data(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetic menu entry shown in the branch switcher when the user types a
|
||||
/// query that does not match any existing branch. Selecting it runs
|
||||
/// `git checkout -b <branch>` so the user can create the branch they were
|
||||
/// about to switch to without leaving the picker.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CreateGitBranch(String);
|
||||
|
||||
impl CreateGitBranch {
|
||||
pub(crate) fn new(branch_name: String) -> Self {
|
||||
Self(branch_name.trim().to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn branch_name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn prompt_chip_command(&self) -> PromptChipShellCommand {
|
||||
PromptChipShellCommand::GitCreateAndCheckoutBranch {
|
||||
branch_name: self.0.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericMenuItem for CreateGitBranch {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn name(&self) -> String {
|
||||
format!("Create new branch \"{}\"", self.0)
|
||||
}
|
||||
|
||||
fn icon(&self, _app: &AppContext) -> Option<Icon> {
|
||||
Some(Icon::Plus)
|
||||
}
|
||||
|
||||
fn action_data(&self) -> String {
|
||||
@@ -544,21 +876,34 @@ impl DisplayChip {
|
||||
ChipMenuType::Branches,
|
||||
ctx,
|
||||
)
|
||||
.with_create_item_from_query(Arc::new(|query: &str| {
|
||||
if !is_plausible_new_branch_name(query) {
|
||||
return None;
|
||||
}
|
||||
let create_branch = CreateGitBranch::new(query.to_string());
|
||||
let arc: Arc<dyn GenericMenuItem> = Arc::new(create_branch);
|
||||
Some(arc)
|
||||
}))
|
||||
});
|
||||
ctx.subscribe_to_view(&menu_view, |me, _, event, ctx| match event {
|
||||
PromptDisplayMenuEvent::MenuAction(generic_event) => {
|
||||
let Some(git_branch) = generic_event
|
||||
.action_item
|
||||
.as_any()
|
||||
.downcast_ref::<GitBranch>()
|
||||
else {
|
||||
log::warn!("MenuAction event should contain ActionItem action item");
|
||||
return;
|
||||
};
|
||||
let action_item = generic_event.action_item.as_any();
|
||||
let command =
|
||||
if let Some(git_branch) = action_item.downcast_ref::<GitBranch>() {
|
||||
git_branch.prompt_chip_command()
|
||||
} else if let Some(create_branch) =
|
||||
action_item.downcast_ref::<CreateGitBranch>()
|
||||
{
|
||||
create_branch.prompt_chip_command()
|
||||
} else {
|
||||
log::warn!(
|
||||
"MenuAction event should contain a GitBranch or CreateGitBranch \
|
||||
action item"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(
|
||||
format_git_branch_command(&git_branch.name()),
|
||||
));
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(command));
|
||||
me.close_git_branch_menu(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
@@ -577,6 +922,13 @@ impl DisplayChip {
|
||||
ContextChipKind::GitDiffStats => DisplayChipKind::GitDiffStats {
|
||||
line_changes_info: None,
|
||||
},
|
||||
ContextChipKind::GitBranchStatus => DisplayChipKind::GitBranchStatus {
|
||||
tracking_status: chip_result
|
||||
.value
|
||||
.as_ref()
|
||||
.and_then(|value| value.as_git_branch_tracking_status())
|
||||
.cloned(),
|
||||
},
|
||||
ContextChipKind::GithubPullRequest => DisplayChipKind::GithubPullRequest,
|
||||
ContextChipKind::WorkingDirectory => {
|
||||
let dir_path = chip_result
|
||||
@@ -602,17 +954,16 @@ impl DisplayChip {
|
||||
});
|
||||
|
||||
// Subscribe to DirectoryFetcher events to update menu
|
||||
let directory_fetcher_clone = directory_fetcher.clone();
|
||||
ctx.subscribe_to_model(
|
||||
&directory_fetcher,
|
||||
move |display_chip, _model, event, ctx| {
|
||||
move |display_chip, model, event, ctx| {
|
||||
match event {
|
||||
DirectoryFetcherEvent::DirectoryContentsUpdated => {
|
||||
// Update the existing menu with new directory contents
|
||||
if let DisplayChipKind::WorkingDirectory { menu, .. } =
|
||||
&mut display_chip.display_chip_kind
|
||||
{
|
||||
let new_files = directory_fetcher_clone
|
||||
let new_files = model
|
||||
.read(ctx, |fetcher, _| fetcher.cached_files().to_vec());
|
||||
// Update the existing menu with new content instead of recreating it
|
||||
menu.update(ctx, |menu_view, menu_ctx| {
|
||||
@@ -645,7 +996,9 @@ impl DisplayChip {
|
||||
DirectoryType::Directory => {
|
||||
// For directories, navigate action is change directory
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(
|
||||
format_change_directory_command(&directory_item.name),
|
||||
PromptChipShellCommand::ChangeDirectory {
|
||||
dir_name: directory_item.name.clone(),
|
||||
},
|
||||
));
|
||||
me.close_working_directory_menu(ctx);
|
||||
ctx.notify();
|
||||
@@ -668,7 +1021,9 @@ impl DisplayChip {
|
||||
}
|
||||
DirectoryType::NavigateToParent => {
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(
|
||||
format_change_directory_command(".."),
|
||||
PromptChipShellCommand::ChangeDirectory {
|
||||
dir_name: "..".to_string(),
|
||||
},
|
||||
));
|
||||
me.close_working_directory_menu(ctx);
|
||||
ctx.notify();
|
||||
@@ -706,9 +1061,11 @@ impl DisplayChip {
|
||||
ctx.focus_self();
|
||||
}
|
||||
NodeVersionPopupEvent::SelectVersion { version } => {
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(format!(
|
||||
"nvm use {version}"
|
||||
)));
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(
|
||||
PromptChipShellCommand::NvmUse {
|
||||
version: version.clone(),
|
||||
},
|
||||
));
|
||||
me.close_node_version_popup(ctx);
|
||||
ctx.focus_self();
|
||||
}
|
||||
@@ -726,7 +1083,7 @@ impl DisplayChip {
|
||||
}
|
||||
NodeVersionPopupEvent::InstallLatestNodeVersion => {
|
||||
ctx.emit(PromptDisplayChipEvent::TryExecuteCommand(
|
||||
"nvm install node".to_string(),
|
||||
PromptChipShellCommand::NvmInstallLatestNode,
|
||||
));
|
||||
me.close_node_version_popup(ctx);
|
||||
}
|
||||
@@ -792,11 +1149,16 @@ impl DisplayChip {
|
||||
Self {
|
||||
mouse_state: Default::default(),
|
||||
diff_stats_mouse_state: Default::default(),
|
||||
text: chip_result.value.map(|v| v.to_string()).unwrap_or_default(),
|
||||
text: chip_result
|
||||
.value
|
||||
.as_ref()
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default(),
|
||||
value: chip_result.value,
|
||||
chip_kind: chip_result.kind,
|
||||
display_chip_kind,
|
||||
next_chip_kind,
|
||||
first_on_click_value: chip_result.on_click_values.first().cloned(),
|
||||
on_click_values: chip_result.on_click_values,
|
||||
quota_reset_popup,
|
||||
session_context: config.session_context,
|
||||
menu_positioning_provider: config.menu_positioning_provider,
|
||||
@@ -828,6 +1190,10 @@ impl DisplayChip {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Option<&ChipValue> {
|
||||
self.value.as_ref()
|
||||
}
|
||||
|
||||
pub fn chip_kind(&self) -> &ContextChipKind {
|
||||
&self.chip_kind
|
||||
}
|
||||
@@ -836,8 +1202,8 @@ impl DisplayChip {
|
||||
&self.display_chip_kind
|
||||
}
|
||||
|
||||
pub fn first_on_click_value(&self) -> Option<&String> {
|
||||
self.first_on_click_value.as_ref()
|
||||
pub(crate) fn on_click_values(&self) -> &[String] {
|
||||
&self.on_click_values
|
||||
}
|
||||
|
||||
pub fn close_git_branch_menu(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
@@ -880,6 +1246,7 @@ impl DisplayChip {
|
||||
}
|
||||
}
|
||||
DisplayChipKind::GitDiffStats { .. }
|
||||
| DisplayChipKind::GitBranchStatus { .. }
|
||||
| DisplayChipKind::Text
|
||||
| DisplayChipKind::Ssh
|
||||
| DisplayChipKind::Subshell
|
||||
@@ -1110,6 +1477,127 @@ impl DisplayChip {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn git_branch_status_chip(
|
||||
&self,
|
||||
tracking_status: &Option<GitBranchTrackingStatus>,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let font_color = internal_colors::neutral_6(theme);
|
||||
let font_family = appearance.ui_font_family();
|
||||
let font_size = udi_font_size(appearance);
|
||||
let fallback_branch = self.text.clone();
|
||||
let tracking_status = tracking_status
|
||||
.clone()
|
||||
.or_else(|| GitBranchTrackingStatus::from_display_text(&self.text));
|
||||
let tooltip_text = tracking_status
|
||||
.as_ref()
|
||||
.map(GitBranchTrackingStatus::tooltip_text);
|
||||
|
||||
Hoverable::new(self.mouse_state.clone(), move |state| {
|
||||
let branch = tracking_status
|
||||
.as_ref()
|
||||
.map(|status| status.branch.clone())
|
||||
.unwrap_or_else(|| fallback_branch.clone());
|
||||
|
||||
let mut content = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
add_git_branch_status_child(
|
||||
&mut content,
|
||||
git_branch_status_icon(
|
||||
Icon::GitBranch,
|
||||
font_color,
|
||||
GIT_BRANCH_STATUS_BRANCH_ICON_SIZE,
|
||||
),
|
||||
GIT_BRANCH_STATUS_MAIN_GAP,
|
||||
);
|
||||
add_git_branch_status_child(
|
||||
&mut content,
|
||||
git_branch_status_text(branch, font_color, font_family, font_size, appearance),
|
||||
GIT_BRANCH_STATUS_MAIN_GAP,
|
||||
);
|
||||
|
||||
if let Some(status) = tracking_status.as_ref() {
|
||||
let show_rebased = status.is_rebased();
|
||||
let ahead = status.ahead_display_count();
|
||||
let behind = status.behind_display_count();
|
||||
if show_rebased || ahead.is_some() || behind.is_some() {
|
||||
add_git_branch_status_child(
|
||||
&mut content,
|
||||
git_branch_status_text(
|
||||
"•".to_string(),
|
||||
font_color,
|
||||
font_family,
|
||||
font_size,
|
||||
appearance,
|
||||
),
|
||||
GIT_BRANCH_STATUS_MAIN_GAP,
|
||||
);
|
||||
}
|
||||
|
||||
if show_rebased {
|
||||
content.add_child(git_branch_status_icon(
|
||||
Icon::SwitchVertical02,
|
||||
font_color,
|
||||
GIT_BRANCH_STATUS_STATUS_ICON_SIZE,
|
||||
));
|
||||
} else {
|
||||
if let Some(ahead) = ahead {
|
||||
add_git_branch_status_child(
|
||||
&mut content,
|
||||
git_branch_status_count(
|
||||
Icon::ArrowUp,
|
||||
ahead,
|
||||
font_color,
|
||||
font_family,
|
||||
font_size,
|
||||
appearance,
|
||||
),
|
||||
GIT_BRANCH_STATUS_MAIN_GAP,
|
||||
);
|
||||
}
|
||||
if let Some(behind) = behind {
|
||||
content.add_child(git_branch_status_count(
|
||||
Icon::ArrowDown,
|
||||
behind,
|
||||
font_color,
|
||||
font_family,
|
||||
font_size,
|
||||
appearance,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut chip_element = Container::new(content.finish())
|
||||
.with_background(theme.surface_1())
|
||||
.with_border(
|
||||
Border::all(CHIP_BORDER_WIDTH)
|
||||
.with_border_color(internal_colors::neutral_3(theme)),
|
||||
)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(CHIP_CORNER_RADIUS)))
|
||||
.with_vertical_padding(GIT_BRANCH_STATUS_CHIP_VERTICAL_PADDING)
|
||||
.with_horizontal_padding(GIT_BRANCH_STATUS_CHIP_HORIZONTAL_PADDING);
|
||||
if state.is_hovered() {
|
||||
chip_element = chip_element.with_background(theme.surface_2());
|
||||
}
|
||||
|
||||
let mut stack = Stack::new().with_child(chip_element.finish());
|
||||
if state.is_hovered() {
|
||||
if let Some(tooltip_text) = tooltip_text.clone() {
|
||||
let tool_tip = appearance
|
||||
.ui_builder()
|
||||
.tool_tip(tooltip_text)
|
||||
.build()
|
||||
.finish();
|
||||
stack.add_positioned_overlay_child(tool_tip, udi_tooltip_positioning());
|
||||
}
|
||||
}
|
||||
stack.finish()
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn git_diff_stats_chip(
|
||||
&self,
|
||||
line_changes_info: &Option<GitLineChanges>,
|
||||
@@ -1143,13 +1631,21 @@ impl DisplayChip {
|
||||
appearance,
|
||||
);
|
||||
|
||||
let is_local_session = self
|
||||
// Code review is only supported on local sessions and
|
||||
// on remote sessions with a connected host ID.
|
||||
let supports_code_review = self
|
||||
.session_context
|
||||
.as_ref()
|
||||
.map(|ctx| ctx.session.is_local())
|
||||
.unwrap_or(true);
|
||||
.map(|ctx| match ctx.session.session_type() {
|
||||
SessionType::Local => true,
|
||||
SessionType::WarpifiedRemote { host_id: Some(_) } => {
|
||||
FeatureFlag::RemoteCodeReview.is_enabled()
|
||||
}
|
||||
SessionType::WarpifiedRemote { host_id: None } => false,
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let diff_stats_display = if is_local_session {
|
||||
let diff_stats_display = if supports_code_review {
|
||||
// Get the keybinding for the tooltip
|
||||
let code_review_keybinding = self.code_review_keybinding.clone().unwrap_or_default();
|
||||
|
||||
@@ -1189,7 +1685,6 @@ impl DisplayChip {
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.finish()
|
||||
} else {
|
||||
// Remote session: chip is non-interactive (no tooltip, no click handler)
|
||||
Container::new(git_diff_stats_content)
|
||||
.with_vertical_padding(2.)
|
||||
.with_horizontal_padding(4.)
|
||||
@@ -1487,6 +1982,9 @@ impl DisplayChip {
|
||||
DisplayChipKind::GitBranch { menu_open, menu } => {
|
||||
Some(self.git_branch_chip(*menu_open, menu, app))
|
||||
}
|
||||
DisplayChipKind::GitBranchStatus { tracking_status } => {
|
||||
Some(self.git_branch_status_chip(tracking_status, app))
|
||||
}
|
||||
DisplayChipKind::GithubPullRequest => Some(self.github_pull_request_chip(app)),
|
||||
DisplayChipKind::GitDiffStats { line_changes_info } => {
|
||||
self.git_diff_stats_chip(line_changes_info, app)
|
||||
@@ -1535,6 +2033,30 @@ impl View for DisplayChip {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PromptChipShellCommand {
|
||||
GitCheckout {
|
||||
branch_name: String,
|
||||
},
|
||||
GitCreateAndCheckoutBranch {
|
||||
branch_name: String,
|
||||
},
|
||||
ChangeDirectory {
|
||||
dir_name: String,
|
||||
},
|
||||
NvmUse {
|
||||
version: String,
|
||||
},
|
||||
NvmInstallLatestNode,
|
||||
Echo {
|
||||
/// The message to echo.
|
||||
///
|
||||
/// This is very intentionally a `&'static str` to ensure that the message is a compile-time constant.
|
||||
/// This is to prevent accidental injection of user input into the message.
|
||||
message: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum PromptDisplayChipEvent {
|
||||
OpenFile(String),
|
||||
OpenTextFileInCodeEditor(String),
|
||||
@@ -1544,7 +2066,7 @@ pub enum PromptDisplayChipEvent {
|
||||
OpenCodeReview,
|
||||
OpenConversationHistory,
|
||||
OpenCommandPaletteFiles,
|
||||
TryExecuteCommand(String),
|
||||
TryExecuteCommand(PromptChipShellCommand),
|
||||
RunAgentQuery(String),
|
||||
OpenAIDocument {
|
||||
document_id: AIDocumentId,
|
||||
@@ -1581,6 +2103,7 @@ impl TypedActionView for DisplayChip {
|
||||
| DisplayChipKind::AgentPlanAndTodoList { .. }
|
||||
| DisplayChipKind::Text
|
||||
| DisplayChipKind::GithubPullRequest
|
||||
| DisplayChipKind::GitBranchStatus { .. }
|
||||
| DisplayChipKind::GitDiffStats { .. } => {}
|
||||
DisplayChipKind::NodeVersion { popup_open, .. } => {
|
||||
*popup_open = false;
|
||||
@@ -1774,14 +2297,6 @@ impl ActionButtonTheme for EnterAgentViewButton {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_change_directory_command(dir_name: &str) -> String {
|
||||
format!("cd '{}'", dir_name.replace("'", "'\\'''"))
|
||||
}
|
||||
|
||||
pub fn format_git_branch_command(branch_name: &str) -> String {
|
||||
format!("git checkout {branch_name}")
|
||||
}
|
||||
|
||||
pub(crate) fn chip_container(
|
||||
content: Box<dyn Element>,
|
||||
border_override: Option<Border>,
|
||||
@@ -1881,5 +2396,5 @@ pub fn udi_icon_size(appearance: &Appearance, app: &AppContext) -> f32 {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "display_chip_test.rs"]
|
||||
#[path = "display_chip_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user