Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+501
View File
@@ -0,0 +1,501 @@
//! Overlay menu for the code review diff selector: pinned search input and
//! a filtered list of label-only rows with a left check slot.
use std::cmp;
use fuzzy_match::{match_indices_case_insensitive, FuzzyMatchResult};
use warp_core::ui::theme::Fill;
use warp_editor::editor::NavigationKey;
use warpui::{
color::ColorU,
elements::{
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Dismiss,
DispatchEventResult, DropShadow, Element, Empty, EventHandler, Flex, Highlight,
MainAxisSize, MouseInBehavior, ParentElement, Radius, ScrollStateHandle, Scrollable,
ScrollableElement, ScrollbarWidth, Text, UniformList, UniformListState,
},
fonts::{Properties, Weight},
id,
keymap::FixedBinding,
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Entity, FocusContext, SingletonEntity as _, TypedActionView, View, ViewContext,
ViewHandle,
};
use crate::{
appearance::Appearance,
code_review::{diff_selector::DiffTarget, diff_state::DiffMode},
editor::{
EditorOptions, EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys,
TextOptions,
},
ui_components::icons::Icon,
};
const MENU_WIDTH: f32 = 280.;
const MENU_MAX_LIST_HEIGHT: f32 = 200.;
const MENU_CORNER_RADIUS: f32 = 6.;
const ROW_HORIZONTAL_PADDING: f32 = 14.;
const ROW_VERTICAL_PADDING: f32 = 5.;
const LIST_BOTTOM_PADDING: f32 = 9.;
const SEARCH_INPUT_HORIZONTAL_PADDING: f32 = 8.;
const CHECK_GAP: f32 = 8.;
const CHECK_SLOT_SIZE: f32 = 12.;
#[derive(Clone, Debug)]
pub enum CodeReviewDiffMenuEvent {
Select(DiffMode),
Close,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CodeReviewDiffMenuAction {
ClickRow { index: usize },
HoverRow { index: usize },
SelectUp,
SelectDown,
SelectEnter,
Close,
}
pub fn init(app: &mut AppContext) {
app.register_fixed_bindings([
FixedBinding::new(
"up",
CodeReviewDiffMenuAction::SelectUp,
id!(CodeReviewDiffMenu::ui_name()),
),
FixedBinding::new(
"down",
CodeReviewDiffMenuAction::SelectDown,
id!(CodeReviewDiffMenu::ui_name()),
),
FixedBinding::new(
"enter",
CodeReviewDiffMenuAction::SelectEnter,
id!(CodeReviewDiffMenu::ui_name()),
),
FixedBinding::new(
"escape",
CodeReviewDiffMenuAction::Close,
id!(CodeReviewDiffMenu::ui_name()),
),
]);
}
pub struct CodeReviewDiffMenu {
targets: Vec<DiffTarget>,
/// (target index, optional match result) pairs for rows that pass the
/// current filter. Original target order is preserved; the match result
/// carries indices for bolding matched characters in the label.
filtered: Vec<(usize, Option<FuzzyMatchResult>)>,
/// Index into `filtered` of the keyboard-focused row.
selected_index: Option<usize>,
search_input: ViewHandle<EditorView>,
search_query: String,
list_state: UniformListState,
scroll_state: ScrollStateHandle,
}
impl CodeReviewDiffMenu {
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let search_input = ctx.add_typed_action_view(|ctx| {
let appearance = Appearance::handle(ctx).as_ref(ctx);
let ui_font_family = appearance.ui_font_family();
let mut text_options = TextOptions::ui_font_size(appearance);
text_options.font_family_override = Some(ui_font_family);
let options = EditorOptions {
autogrow: false,
soft_wrap: false,
single_line: true,
text: text_options,
propagate_and_no_op_vertical_navigation_keys:
PropagateAndNoOpNavigationKeys::Always,
..Default::default()
};
let mut editor = EditorView::new(options, ctx);
editor.set_placeholder_text("Search diff sets or branches to compare…", ctx);
editor
});
ctx.subscribe_to_view(&search_input, |menu, _, event, ctx| match event {
EditorEvent::Edited(_) => {
let new_query = menu
.search_input
.read(ctx, |editor, ctx| editor.buffer_text(ctx).to_string());
if new_query != menu.search_query {
menu.set_search_query(new_query, ctx);
}
}
EditorEvent::Escape => menu.emit_close(ctx),
EditorEvent::Navigate(NavigationKey::Up) => menu.select_prev(ctx),
EditorEvent::Navigate(NavigationKey::Down) => menu.select_next(ctx),
EditorEvent::Enter => menu.select_enter(ctx),
_ => {}
});
Self {
targets: Vec::new(),
filtered: Vec::new(),
selected_index: None,
search_input,
search_query: String::new(),
list_state: Default::default(),
scroll_state: Default::default(),
}
}
/// Replace the row set and reset filter/scroll to the top.
pub fn set_targets(&mut self, targets: Vec<DiffTarget>, ctx: &mut ViewContext<Self>) {
self.targets = targets;
self.refresh_filtered();
self.scroll_list_to_top();
ctx.notify();
}
/// Reset the menu to a fresh state: empty query, selection/scroll back
/// to the top, and rows re-computed against the current targets.
pub fn reset(&mut self, ctx: &mut ViewContext<Self>) {
self.search_query.clear();
self.search_input.update(ctx, |editor, ctx| {
editor.clear_buffer(ctx);
});
self.refresh_filtered();
self.scroll_list_to_top();
ctx.notify();
}
fn set_search_query(&mut self, query: String, ctx: &mut ViewContext<Self>) {
self.search_query = query;
self.refresh_filtered();
self.scroll_list_to_top();
ctx.notify();
}
fn scroll_list_to_top(&mut self) {
if !self.filtered.is_empty() {
self.list_state.scroll_to(0);
}
}
fn refresh_filtered(&mut self) {
if self.search_query.is_empty() {
self.filtered = (0..self.targets.len()).map(|i| (i, None)).collect();
} else {
// Use fuzzy matching for membership only; preserve original order
// so the list never reorders under the user as they type.
self.filtered = self
.targets
.iter()
.enumerate()
.filter_map(|(i, target)| {
match_indices_case_insensitive(&target.label, &self.search_query)
.map(|m| (i, Some(m)))
})
.collect();
}
self.selected_index = if self.filtered.is_empty() {
None
} else {
Some(0)
};
}
fn select_prev(&mut self, ctx: &mut ViewContext<Self>) {
let Some(current) = self.selected_index else {
return;
};
if current == 0 {
return;
}
self.selected_index = Some(current - 1);
self.list_state.scroll_to(current - 1);
ctx.notify();
}
fn select_next(&mut self, ctx: &mut ViewContext<Self>) {
if self.filtered.is_empty() {
return;
}
let next = match self.selected_index {
Some(i) if i + 1 < self.filtered.len() => i + 1,
Some(i) => i,
None => 0,
};
self.selected_index = Some(next);
self.list_state.scroll_to(next);
ctx.notify();
}
fn select_enter(&mut self, ctx: &mut ViewContext<Self>) {
let Some(selected) = self.selected_index else {
return;
};
self.select_filtered_index(selected, ctx);
}
fn select_filtered_index(&mut self, filtered_index: usize, ctx: &mut ViewContext<Self>) {
let Some((target_index, _)) = self.filtered.get(filtered_index).cloned() else {
return;
};
let Some(target) = self.targets.get(target_index) else {
return;
};
let mode = target.mode.clone();
ctx.emit(CodeReviewDiffMenuEvent::Select(mode));
ctx.notify();
}
fn emit_close(&mut self, ctx: &mut ViewContext<Self>) {
ctx.emit(CodeReviewDiffMenuEvent::Close);
ctx.notify();
}
fn render_search_input(&self, appearance: &Appearance) -> Box<dyn Element> {
let theme = appearance.theme();
let search_input = appearance
.ui_builder()
.text_input(self.search_input.clone())
.with_style(UiComponentStyles {
background: Some(Fill::Solid(ColorU::new(0, 0, 0, 0)).into()),
border_color: None,
border_width: Some(0.),
border_radius: None,
width: Some(MENU_WIDTH - (SEARCH_INPUT_HORIZONTAL_PADDING * 2.)),
padding: Some(Coords::uniform(4.)),
..Default::default()
})
.build()
.finish();
// Search bar inherits the menu card surface; a hairline bottom
// border separates it from the row list below.
Container::new(search_input)
.with_horizontal_padding(SEARCH_INPUT_HORIZONTAL_PADDING)
.with_vertical_padding(2.)
.with_border(Border::bottom(1.0).with_border_color(theme.outline().into()))
.finish()
}
fn render_empty_state(&self, appearance: &Appearance) -> Box<dyn Element> {
let theme = appearance.theme();
Container::new(
Text::new(
"No matches",
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(theme.sub_text_color(theme.surface_2()).into_solid())
.finish(),
)
.with_horizontal_padding(ROW_HORIZONTAL_PADDING)
.with_vertical_padding(ROW_VERTICAL_PADDING * 2.0)
.finish()
}
fn render_rows(&self, ctx: &AppContext) -> Box<dyn Element> {
if self.filtered.is_empty() {
if !self.search_query.is_empty() {
return self.render_empty_state(Appearance::as_ref(ctx));
}
return Empty::new().finish();
}
let appearance = Appearance::as_ref(ctx);
let theme = appearance.theme();
let selected = self.selected_index;
let filtered_snapshot: Vec<(DiffTarget, Option<FuzzyMatchResult>)> = self
.filtered
.iter()
.filter_map(|(i, m)| self.targets.get(*i).cloned().map(|t| (t, m.clone())))
.collect();
let filtered_len = filtered_snapshot.len();
let list = UniformList::new(
self.list_state.clone(),
filtered_len,
move |mut range, app| {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
range.end = cmp::min(range.end, filtered_len);
range
.map(|row_index| {
let (target, match_result) = &filtered_snapshot[row_index];
let is_focused = selected == Some(row_index);
let font_size = appearance.ui_font_size();
let (text_color, bg) = if is_focused {
let bg = theme.accent();
(theme.main_text_color(bg).into_solid(), Some(bg))
} else {
(theme.main_text_color(theme.surface_2()).into_solid(), None)
};
let check_slot: Box<dyn Element> = if target.is_selected {
ConstrainedBox::new(
Icon::Check.to_warpui_icon(Fill::Solid(text_color)).finish(),
)
.with_width(CHECK_SLOT_SIZE)
.with_height(CHECK_SLOT_SIZE)
.finish()
} else {
ConstrainedBox::new(Empty::new().finish())
.with_width(CHECK_SLOT_SIZE)
.with_height(CHECK_SLOT_SIZE)
.finish()
};
let label_text = Text::new_inline(
target.label.clone(),
appearance.ui_font_family(),
font_size,
)
.with_color(text_color);
let label = match match_result {
Some(m) => label_text.with_single_highlight(
Highlight::new()
.with_properties(Properties::default().weight(Weight::Bold))
.with_foreground_color(text_color),
m.matched_indices.clone(),
),
None => label_text,
}
.finish();
let row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_size(MainAxisSize::Max)
.with_child(
Container::new(check_slot)
.with_margin_right(CHECK_GAP)
.finish(),
)
.with_child(label)
.finish();
let mut container = Container::new(row)
.with_horizontal_padding(ROW_HORIZONTAL_PADDING)
.with_vertical_padding(ROW_VERTICAL_PADDING);
if let Some(bg) = bg {
container = container.with_background(bg);
}
EventHandler::new(container.finish())
.on_left_mouse_down(move |ctx, _, _| {
ctx.dispatch_typed_action(CodeReviewDiffMenuAction::ClickRow {
index: row_index,
});
DispatchEventResult::StopPropagation
})
.on_mouse_in(
move |ctx, _, _| {
ctx.dispatch_typed_action(CodeReviewDiffMenuAction::HoverRow {
index: row_index,
});
ctx.notify();
DispatchEventResult::StopPropagation
},
Some(MouseInBehavior {
fire_on_synthetic_events: false,
fire_when_covered: false,
}),
)
.finish()
})
.collect::<Vec<_>>()
.into_iter()
},
);
let scrollable = Scrollable::vertical(
self.scroll_state.clone(),
list.finish_scrollable(),
ScrollbarWidth::None,
theme.nonactive_ui_detail().into(),
theme.active_ui_detail().into(),
warpui::elements::Fill::None,
)
.with_padding_end(0.)
.with_padding_start(0.);
ConstrainedBox::new(scrollable.finish())
.with_width(MENU_WIDTH)
.with_max_height(MENU_MAX_LIST_HEIGHT)
.finish()
}
}
impl Entity for CodeReviewDiffMenu {
type Event = CodeReviewDiffMenuEvent;
}
impl TypedActionView for CodeReviewDiffMenu {
type Action = CodeReviewDiffMenuAction;
fn handle_action(&mut self, action: &CodeReviewDiffMenuAction, ctx: &mut ViewContext<Self>) {
match action {
CodeReviewDiffMenuAction::ClickRow { index } => {
// Sync keyboard selection with the clicked row.
if *index < self.filtered.len() {
self.selected_index = Some(*index);
self.select_filtered_index(*index, ctx);
}
}
CodeReviewDiffMenuAction::HoverRow { index } => {
if *index < self.filtered.len() && self.selected_index != Some(*index) {
self.selected_index = Some(*index);
ctx.notify();
}
}
CodeReviewDiffMenuAction::SelectUp => self.select_prev(ctx),
CodeReviewDiffMenuAction::SelectDown => self.select_next(ctx),
CodeReviewDiffMenuAction::SelectEnter => self.select_enter(ctx),
CodeReviewDiffMenuAction::Close => self.emit_close(ctx),
}
}
}
impl View for CodeReviewDiffMenu {
fn ui_name() -> &'static str {
"CodeReviewDiffMenu"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
ctx.focus(&self.search_input);
}
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut column = Flex::column().with_child(self.render_search_input(appearance));
if !self.targets.is_empty() || !self.search_query.is_empty() {
column.add_child(
Container::new(self.render_rows(app))
.with_padding_bottom(LIST_BOTTOM_PADDING)
.finish(),
);
}
let menu_card = ConstrainedBox::new(
Container::new(column.finish())
.with_background(theme.surface_2())
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(MENU_CORNER_RADIUS)))
.with_drop_shadow(DropShadow::default())
.finish(),
)
.with_width(MENU_WIDTH)
.finish();
Dismiss::new(menu_card)
.on_dismiss(|ctx, _app| {
ctx.dispatch_typed_action(CodeReviewDiffMenuAction::Close);
})
.prevent_interaction_with_other_elements()
.finish()
}
}