diff --git a/Cargo.lock b/Cargo.lock index c39a474bfd..2a3b4d5593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5412,6 +5412,7 @@ dependencies = [ "multi_buffer", "panel", "picker", + "popover_button", "postage", "project", "schemars", @@ -7046,6 +7047,7 @@ dependencies = [ "language_model", "log", "picker", + "popover_button", "proto", "ui", "workspace", @@ -10010,6 +10012,14 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" +[[package]] +name = "popover_button" +version = "0.1.0" +dependencies = [ + "gpui", + "ui", +] + [[package]] name = "postage" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 58066a253f..1424a41fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "crates/collab", "crates/collab_ui", "crates/collections", + "crates/popover_button", "crates/command_palette", "crates/command_palette_hooks", "crates/component", @@ -231,6 +232,7 @@ clock = { path = "crates/clock" } collab = { path = "crates/collab" } collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } +popover_button = { path = "crates/popover_button" } command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } component = { path = "crates/component" } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index fc58fb1cd9..37d356aa98 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -606,7 +606,7 @@ "ctrl-n": "assistant2::NewThread", "new": "assistant2::NewThread", "ctrl-shift-h": "assistant2::OpenHistory", - "ctrl-alt-/": "assistant2::ToggleModelSelector", + "ctrl-alt-/": "assistant::ToggleModelSelector", "ctrl-shift-a": "assistant2::ToggleContextPicker", "ctrl-e": "assistant2::ChatMode", "ctrl-alt-e": "assistant2::RemoveAllContext" diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index fe0667f740..a3687e364e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -238,7 +238,7 @@ "cmd-n": "assistant2::NewThread", "cmd-alt-p": "assistant2::NewPromptEditor", "cmd-shift-h": "assistant2::OpenHistory", - "cmd-alt-/": "assistant2::ToggleModelSelector", + "cmd-alt-/": "assistant::ToggleModelSelector", "cmd-shift-a": "assistant2::ToggleContextPicker", "cmd-e": "assistant2::ChatMode", "cmd-alt-e": "assistant2::RemoveAllContext" @@ -658,7 +658,7 @@ "use_key_equivalents": true, "bindings": { "cmd-shift-a": "assistant2::ToggleContextPicker", - "cmd-alt-/": "assistant2::ToggleModelSelector", + "cmd-alt-/": "assistant::ToggleModelSelector", "cmd-alt-e": "assistant2::RemoveAllContext", "ctrl-[": "assistant::CyclePreviousInlineAssist", "ctrl-]": "assistant::CycleNextInlineAssist" diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 02ae9a2333..e37ef141a4 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -35,7 +35,7 @@ use language_model::{ report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelTextStream, Role, }; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector}; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, ProjectTransaction}; @@ -1589,29 +1589,10 @@ impl Render for PromptEditor { .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)) .justify_center() .gap_2() - .child(LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted), - move |window, cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - window, - cx, - ) - }, - gpui::Corner::TopRight, - )) + .child( + InlineLanguageModelSelector::new(self.language_model_selector.clone()) + .render(window, cx), + ) .map(|el| { let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else { return el; diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 87b5f4bbda..a4f27db6ca 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -19,7 +19,7 @@ use language_model::{ report_assistant_event, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use language_model_selector::{InlineLanguageModelSelector, LanguageModelSelector}; use prompt_library::PromptBuilder; use settings::{update_settings_file, Settings}; use std::{ @@ -506,7 +506,7 @@ struct PromptEditor { impl EventEmitter for PromptEditor {} impl Render for PromptEditor { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let status = &self.codegen.read(cx).status; let buttons = match status { CodegenStatus::Idle => { @@ -641,29 +641,10 @@ impl Render for PromptEditor { .w_12() .justify_center() .gap_2() - .child(LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted), - move |window, cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - window, - cx, - ) - }, - gpui::Corner::TopRight, - )) + .child( + InlineLanguageModelSelector::new(self.language_model_selector.clone()) + .render(window, cx), + ) .children( if let CodegenStatus::Error(error) = &self.codegen.read(cx).status { let error_message = SharedString::from(error.to_string()); diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 70d4d9d100..7a3a82c9f7 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -38,7 +38,6 @@ actions!( NewThread, NewPromptEditor, ToggleContextPicker, - ToggleModelSelector, RemoveAllContext, OpenHistory, OpenConfiguration, diff --git a/crates/assistant2/src/assistant_model_selector.rs b/crates/assistant2/src/assistant_model_selector.rs index 2769a9e5ca..82be03734c 100644 --- a/crates/assistant2/src/assistant_model_selector.rs +++ b/crates/assistant2/src/assistant_model_selector.rs @@ -1,24 +1,19 @@ use assistant_settings::AssistantSettings; use fs::Fs; -use gpui::{Entity, FocusHandle, SharedString}; -use language_model::LanguageModelRegistry; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use gpui::{Entity, FocusHandle}; +use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector}; use settings::update_settings_file; use std::sync::Arc; -use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip}; - -use crate::ToggleModelSelector; +use ui::prelude::*; pub struct AssistantModelSelector { - selector: Entity, - menu_handle: PopoverMenuHandle, + pub selector: Entity, focus_handle: FocusHandle, } impl AssistantModelSelector { pub(crate) fn new( fs: Arc, - menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, window: &mut Window, cx: &mut App, @@ -38,50 +33,14 @@ impl AssistantModelSelector { cx, ) }), - menu_handle, focus_handle, } } } impl Render for AssistantModelSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let active_model = LanguageModelRegistry::read_global(cx).active_model(); - let focus_handle = self.focus_handle.clone(); - let model_name = match active_model { - Some(model) => model.name().0, - _ => SharedString::from("No model selected"), - }; - - LanguageModelSelectorPopoverMenu::new( - self.selector.clone(), - ButtonLike::new("active-model") - .style(ButtonStyle::Subtle) - .child( - h_flex() - .gap_0p5() - .child( - Label::new(model_name) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) - }, - gpui::Corner::BottomRight, - ) - .with_handle(self.menu_handle.clone()) + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone()) + .render(window, cx) } } diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index 3323246903..bf00b2b458 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -6,7 +6,7 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::terminal_codegen::TerminalCodegen; use crate::thread_store::ThreadStore; use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist}; -use crate::{RemoveAllContext, ToggleContextPicker, ToggleModelSelector}; +use crate::{RemoveAllContext, ToggleContextPicker}; use client::ErrorExt; use collections::VecDeque; use editor::{ @@ -20,7 +20,6 @@ use gpui::{ EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, }; use language_model::{LanguageModel, LanguageModelRegistry}; -use language_model_selector::LanguageModelSelector; use parking_lot::Mutex; use settings::Settings; use std::cmp; @@ -40,7 +39,6 @@ pub struct PromptEditor { context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, model_selector: Entity, - model_selector_menu_handle: PopoverMenuHandle, edited_since_done: bool, prompt_history: VecDeque, prompt_history_ix: Option, @@ -104,7 +102,12 @@ impl Render for PromptEditor { .items_start() .cursor(CursorStyle::Arrow) .on_action(cx.listener(Self::toggle_context_picker)) - .on_action(cx.listener(Self::toggle_model_selector)) + .on_action(cx.listener(|this, action, window, cx| { + let selector = this.model_selector.read(cx).selector.clone(); + selector.update(cx, |selector, cx| { + selector.toggle_model_selector(action, window, cx); + }) + })) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) @@ -347,15 +350,6 @@ impl PromptEditor { self.context_picker_menu_handle.toggle(window, cx); } - fn toggle_model_selector( - &mut self, - _: &ToggleModelSelector, - window: &mut Window, - cx: &mut Context, - ) { - self.model_selector_menu_handle.toggle(window, cx); - } - pub fn remove_all_context( &mut self, _: &RemoveAllContext, @@ -864,7 +858,6 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); - let model_selector_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { ContextStrip::new( @@ -888,15 +881,8 @@ impl PromptEditor { context_strip, context_picker_menu_handle, model_selector: cx.new(|cx| { - AssistantModelSelector::new( - fs, - model_selector_menu_handle.clone(), - prompt_editor.focus_handle(cx), - window, - cx, - ) + AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx) }), - model_selector_menu_handle, edited_since_done: false, prompt_history, prompt_history_ix: None, @@ -1020,7 +1006,6 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); - let model_selector_menu_handle = PopoverMenuHandle::default(); let context_strip = cx.new(|cx| { ContextStrip::new( @@ -1044,15 +1029,8 @@ impl PromptEditor { context_strip, context_picker_menu_handle, model_selector: cx.new(|cx| { - AssistantModelSelector::new( - fs, - model_selector_menu_handle.clone(), - prompt_editor.focus_handle(cx), - window, - cx, - ) + AssistantModelSelector::new(fs, prompt_editor.focus_handle(cx), window, cx) }), - model_selector_menu_handle, edited_since_done: false, prompt_history, prompt_history_ix: None, diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 2c749ba7f0..1051b3ea67 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -8,7 +8,6 @@ use gpui::{ TextStyle, WeakEntity, }; use language_model::LanguageModelRegistry; -use language_model_selector::LanguageModelSelector; use rope::Point; use settings::Settings; use std::time::Duration; @@ -25,7 +24,7 @@ use crate::context_store::{refresh_context_store_text, ContextStore}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::thread::{RequestKind, Thread}; use crate::thread_store::ThreadStore; -use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector}; +use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker}; pub struct MessageEditor { thread: Entity, @@ -36,7 +35,6 @@ pub struct MessageEditor { inline_context_picker: Entity, inline_context_picker_menu_handle: PopoverMenuHandle, model_selector: Entity, - model_selector_menu_handle: PopoverMenuHandle, use_tools: bool, _subscriptions: Vec, } @@ -53,7 +51,6 @@ impl MessageEditor { let context_store = cx.new(|_cx| ContextStore::new(workspace.clone())); let context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default(); - let model_selector_menu_handle = PopoverMenuHandle::default(); let editor = cx.new(|cx| { let mut editor = Editor::auto_height(10, window, cx); @@ -106,30 +103,13 @@ impl MessageEditor { context_picker_menu_handle, inline_context_picker, inline_context_picker_menu_handle, - model_selector: cx.new(|cx| { - AssistantModelSelector::new( - fs, - model_selector_menu_handle.clone(), - editor.focus_handle(cx), - window, - cx, - ) - }), - model_selector_menu_handle, + model_selector: cx + .new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)), use_tools: false, _subscriptions: subscriptions, } } - fn toggle_model_selector( - &mut self, - _: &ToggleModelSelector, - window: &mut Window, - cx: &mut Context, - ) { - self.model_selector_menu_handle.toggle(window, cx) - } - fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context) { self.use_tools = !self.use_tools; cx.notify(); @@ -306,7 +286,12 @@ impl Render for MessageEditor { v_flex() .key_context("MessageEditor") .on_action(cx.listener(Self::chat)) - .on_action(cx.listener(Self::toggle_model_selector)) + .on_action(cx.listener(|this, action, window, cx| { + let selector = this.model_selector.read(cx).selector.clone(); + selector.update(cx, |this, cx| { + this.toggle_model_selector(action, window, cx); + }) + })) .on_action(cx.listener(Self::toggle_context_picker)) .on_action(cx.listener(Self::remove_all_context)) .on_action(cx.listener(Self::move_up)) diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 5ecebd3e8d..7180635103 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -34,7 +34,7 @@ use language_model::{ LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry, Role, }; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector}; use multi_buffer::MultiBufferRow; use picker::Picker; use project::lsp_store::LocalLspAdapterDelegate; @@ -77,7 +77,6 @@ actions!( InsertIntoEditor, QuoteSelection, Split, - ToggleModelSelector, ] ); @@ -194,7 +193,6 @@ pub struct ContextEditor { // context editor, we keep a reference here. dragged_file_worktrees: Vec>, language_model_selector: Entity, - language_model_selector_menu_handle: PopoverMenuHandle, } pub const DEFAULT_TAB_TITLE: &str = "New Chat"; @@ -255,7 +253,6 @@ impl ContextEditor { ) }); - let language_model_selector_menu_handle = PopoverMenuHandle::default(); let sections = context.read(cx).slash_command_output_sections().to_vec(); let patch_ranges = context.read(cx).patch_ranges().collect::>(); let slash_commands = context.read(cx).slash_commands().clone(); @@ -281,7 +278,6 @@ impl ContextEditor { slash_menu_handle: Default::default(), dragged_file_worktrees: Vec::new(), language_model_selector, - language_model_selector_menu_handle, }; this.update_message_headers(cx); this.update_image_blocks(cx); @@ -2024,15 +2020,6 @@ impl ContextEditor { }); } - fn toggle_model_selector( - &mut self, - _: &ToggleModelSelector, - window: &mut Window, - cx: &mut Context, - ) { - self.language_model_selector_menu_handle.toggle(window, cx); - } - fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context) { self.context.update(cx, |context, cx| { context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) @@ -2380,46 +2367,6 @@ impl ContextEditor { ) } - fn render_language_model_selector(&self, cx: &mut Context) -> impl IntoElement { - let active_model = LanguageModelRegistry::read_global(cx).active_model(); - let focus_handle = self.editor().focus_handle(cx).clone(); - let model_name = match active_model { - Some(model) => model.name().0, - None => SharedString::from("No model selected"), - }; - - LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - ButtonLike::new("active-model") - .style(ButtonStyle::Subtle) - .child( - h_flex() - .gap_0p5() - .child( - Label::new(model_name) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) - }, - gpui::Corner::BottomLeft, - ) - .with_handle(self.language_model_selector_menu_handle.clone()) - } - fn render_last_error(&self, cx: &mut Context) -> Option { let last_error = self.last_error.as_ref()?; @@ -2864,6 +2811,7 @@ impl Render for ContextEditor { None }; + let language_model_selector = self.language_model_selector.clone(); v_flex() .key_context("ContextEditor") .capture_action(cx.listener(ContextEditor::cancel)) @@ -2876,7 +2824,11 @@ impl Render for ContextEditor { .on_action(cx.listener(ContextEditor::edit)) .on_action(cx.listener(ContextEditor::assist)) .on_action(cx.listener(ContextEditor::split)) - .on_action(cx.listener(ContextEditor::toggle_model_selector)) + .on_action(move |action, window, cx| { + language_model_selector.update(cx, |this, cx| { + this.toggle_model_selector(action, window, cx); + }) + }) .size_full() .children(self.render_notice(cx)) .child( @@ -2914,11 +2866,14 @@ impl Render for ContextEditor { .gap_1() .child(self.render_inject_context_menu(cx)) .child(ui::Divider::vertical()) - .child( - div() - .pl_0p5() - .child(self.render_language_model_selector(cx)), - ), + .child(div().pl_0p5().child({ + let focus_handle = self.editor().focus_handle(cx).clone(); + AssistantLanguageModelSelector::new( + focus_handle, + self.language_model_selector.clone(), + ) + .render(window, cx) + })), ) .child( h_flex() diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 18f44122b4..9c0da064b1 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -16,6 +16,7 @@ path = "src/git_ui.rs" anyhow.workspace = true buffer_diff.workspace = true collections.workspace = true +popover_button.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 33febb7af5..c1e5f3b627 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1,16 +1,16 @@ -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{Context as _, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; use git::repository::Branch; use gpui::{ rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, - Task, WeakEntity, Window, + Task, Window, }; use picker::{Picker, PickerDelegate}; -use project::ProjectPath; +use project::{Project, ProjectPath}; use std::sync::Arc; -use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -23,19 +23,29 @@ pub fn init(cx: &mut App) { } pub fn open( - _: &mut Workspace, + workspace: &mut Workspace, _: &zed_actions::git::Branch, window: &mut Window, cx: &mut Context, ) { - let this = cx.entity().clone(); + let project = workspace.project().clone(); + let this = cx.entity(); + let style = BranchListStyle::Modal; cx.spawn_in(window, |_, mut cx| async move { // Modal branch picker has a longer trailoff than a popover one. - let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?; + let delegate = BranchListDelegate::new(project.clone(), style, 70, &cx).await?; - this.update_in(&mut cx, |workspace, window, cx| { + this.update_in(&mut cx, move |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { - BranchList::new(delegate, 34., window, cx) + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + + let mut list = BranchList::new(project, style, 34., cx); + list._subscription = Some(_subscription); + list.picker = Some(picker); + list }) })?; @@ -44,34 +54,86 @@ pub fn open( .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None) } +pub fn popover(project: Entity, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| { + let mut list = BranchList::new(project, BranchListStyle::Popover, 15., cx); + list.reload_branches(window, cx); + list + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum BranchListStyle { + Modal, + Popover, +} + pub struct BranchList { - pub picker: Entity>, rem_width: f32, - _subscription: Subscription, + popover_handle: PopoverMenuHandle, + default_focus_handle: FocusHandle, + project: Entity, + style: BranchListStyle, + pub picker: Option>>, + _subscription: Option, +} + +impl popover_button::TriggerablePopover for BranchList { + fn menu_handle( + &mut self, + _window: &mut Window, + _cx: &mut gpui::Context, + ) -> PopoverMenuHandle { + self.popover_handle.clone() + } } impl BranchList { - pub fn new( - delegate: BranchListDelegate, - rem_width: f32, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); + fn new(project: Entity, style: BranchListStyle, rem_width: f32, cx: &mut App) -> Self { + let popover_handle = PopoverMenuHandle::default(); Self { - picker, + project, + picker: None, rem_width, - _subscription, + popover_handle, + default_focus_handle: cx.focus_handle(), + style, + _subscription: None, } } + + fn reload_branches(&mut self, window: &mut Window, cx: &mut Context) { + let project = self.project.clone(); + let style = self.style; + cx.spawn_in(window, |this, mut cx| async move { + let delegate = BranchListDelegate::new(project, style, 20, &cx).await?; + let picker = + cx.new_window_entity(|window, cx| Picker::uniform_list(delegate, window, cx))?; + + this.update(&mut cx, |branch_list, cx| { + let subscription = + cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| cx.emit(DismissEvent)); + + branch_list.picker = Some(picker); + branch_list._subscription = Some(subscription); + + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } impl ModalView for BranchList {} impl EventEmitter for BranchList {} impl Focusable for BranchList { fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) + self.picker + .as_ref() + .map(|picker| picker.focus_handle(cx)) + .unwrap_or_else(|| self.default_focus_handle.clone()) } } @@ -79,12 +141,27 @@ impl Render for BranchList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .w(rems(self.rem_width)) - .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); + .when_some(self.picker.clone(), |div, picker| { + div.child(picker.clone()).on_mouse_down_out({ + let picker = picker.clone(); + cx.listener(move |_, _, window, cx| { + picker.update(cx, |this, cx| { + this.cancel(&Default::default(), window, cx); + }) + }) }) - })) + }) + .when_none(&self.picker, |div| { + div.child( + h_flex() + .id("branch-picker-error") + .on_click( + cx.listener(|this, _, window, cx| this.reload_branches(window, cx)), + ) + .child("Could not load branches.") + .child("Click to retry"), + ) + }) } } @@ -108,7 +185,8 @@ impl BranchEntry { pub struct BranchListDelegate { matches: Vec, all_branches: Vec, - workspace: WeakEntity, + project: Entity, + style: BranchListStyle, selected_index: usize, last_query: String, /// Max length of branch name before we truncate it and add a trailing `...`. @@ -116,13 +194,14 @@ pub struct BranchListDelegate { } impl BranchListDelegate { - pub async fn new( - workspace: Entity, + async fn new( + project: Entity, + style: BranchListStyle, branch_name_trailoff_after: usize, cx: &AsyncApp, ) -> Result { let all_branches_request = cx.update(|cx| { - let project = workspace.read(cx).project().read(cx); + let project = project.read(cx); let first_worktree = project .visible_worktrees(cx) .next() @@ -135,7 +214,8 @@ impl BranchListDelegate { Ok(Self { matches: vec![], - workspace: workspace.downgrade(), + project, + style, all_branches, selected_index: 0, last_query: Default::default(), @@ -254,18 +334,12 @@ impl PickerDelegate for BranchListDelegate { return; }; - let current_branch = self - .workspace - .update(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .active_repository(cx) - .and_then(|repo| repo.read(cx).current_branch()) - .map(|branch| branch.name.to_string()) - }) - .ok() - .flatten(); + let current_branch = self.project.update(cx, |project, cx| { + project + .active_repository(cx) + .and_then(|repo| repo.read(cx).current_branch()) + .map(|branch| branch.name.to_string()) + }); if current_branch == Some(branch.name().to_string()) { cx.emit(DismissEvent); @@ -276,13 +350,7 @@ impl PickerDelegate for BranchListDelegate { let branch = branch.clone(); |picker, mut cx| async move { let branch_change_task = picker.update(&mut cx, |this, cx| { - let workspace = this - .delegate - .workspace - .upgrade() - .ok_or_else(|| anyhow!("workspace was dropped"))?; - - let project = workspace.read(cx).project().read(cx); + let project = this.delegate.project.read(cx); let branch_to_checkout = match branch { BranchEntry::Branch(branch) => branch.string, BranchEntry::History(string) => string, @@ -327,6 +395,10 @@ impl PickerDelegate for BranchListDelegate { Some( ListItem::new(SharedString::from(format!("vcs-menu-{ix}"))) .inset(true) + .spacing(match self.style { + BranchListStyle::Modal => ListItemSpacing::default(), + BranchListStyle::Popover => ListItemSpacing::ExtraDense, + }) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .when(matches!(hit, BranchEntry::History(_)), |el| { diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 69b03ccc96..c81ad62ebe 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,8 +1,11 @@ // #![allow(unused, dead_code)] +use crate::branch_picker::{self, BranchList}; use crate::git_panel::{commit_message_editor, GitPanel}; use git::Commit; use panel::{panel_button, panel_editor_style, panel_filled_button}; +use popover_button::TriggerablePopover; +use project::Project; use ui::{prelude::*, KeybindingHint, Tooltip}; use editor::{Editor, EditorElement}; @@ -64,6 +67,7 @@ pub fn init(cx: &mut App) { } pub struct CommitModal { + branch_list: Entity, git_panel: Entity, commit_editor: Entity, restore_dock: RestoreDock, @@ -139,9 +143,11 @@ impl CommitModal { is_open, active_index, }; + + let project = workspace.project().clone(); workspace.open_panel::(window, cx); workspace.toggle_modal(window, cx, move |window, cx| { - CommitModal::new(git_panel, restore_dock_position, window, cx) + CommitModal::new(git_panel, restore_dock_position, project, window, cx) }) }); } @@ -149,6 +155,7 @@ impl CommitModal { fn new( git_panel: Entity, restore_dock: RestoreDock, + project: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -182,14 +189,21 @@ impl CommitModal { let focus_handle = commit_editor.focus_handle(cx); - cx.on_focus_out(&focus_handle, window, |_, _, _, cx| { - cx.emit(DismissEvent); + cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { + if !this + .branch_list + .focus_handle(cx) + .contains_focused(window, cx) + { + cx.emit(DismissEvent); + } }) .detach(); let properties = ModalContainerProperties::new(window, 50); Self { + branch_list: branch_picker::popover(project.clone(), window, cx), git_panel, commit_editor, restore_dock, @@ -230,7 +244,7 @@ impl CommitModal { ) } - fn render_footer(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + pub fn render_footer(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let git_panel = self.git_panel.clone(); let (branch, tooltip, commit_label, co_authors) = @@ -238,7 +252,12 @@ impl CommitModal { let branch = git_panel .active_repository .as_ref() - .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone())) + .and_then(|repo| { + repo.read(cx) + .repository_entry + .branch() + .map(|b| b.name.clone()) + }) .unwrap_or_else(|| "".into()); let tooltip = if git_panel.has_staged_changes() { "Commit staged changes" @@ -248,13 +267,13 @@ impl CommitModal { let title = if git_panel.has_staged_changes() { "Commit" } else { - "Commit Tracked" + "Commit All" }; let co_authors = git_panel.render_co_authors(cx); (branch, tooltip, title, co_authors) }); - let branch_selector = panel_button(branch) + let branch_picker_button = panel_button(branch) .icon(IconName::GitBranch) .icon_size(IconSize::Small) .icon_color(Color::Placeholder) @@ -269,6 +288,13 @@ impl CommitModal { })) .style(ButtonStyle::Transparent); + let branch_picker = popover_button::PopoverButton::new( + self.branch_list.clone(), + Corner::BottomLeft, + branch_picker_button, + Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch), + ); + let close_kb_hint = if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) { Some( @@ -303,7 +329,12 @@ impl CommitModal { .w_full() .h(px(self.properties.footer_height)) .gap_1() - .child(h_flex().gap_1().child(branch_selector).children(co_authors)) + .child( + h_flex() + .gap_1() + .child(branch_picker.render(window, cx)) + .children(co_authors), + ) .child(div().flex_1()) .child( h_flex() @@ -340,6 +371,13 @@ impl Render for CommitModal { .key_context("GitCommit") .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::commit)) + .on_action( + cx.listener(|this, _: &zed_actions::git::Branch, window, cx| { + this.branch_list.update(cx, |branch_list, cx| { + branch_list.menu_handle(window, cx).toggle(window, cx); + }) + }), + ) .elevation_3(cx) .overflow_hidden() .flex_none() diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index 53eb544bd0..01ef384e7c 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -40,6 +40,19 @@ pub trait FluentBuilder { } }) } + /// Conditionally unwrap and modify self with the given closure, if the given option is Some. + fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(_) = option { + this + } else { + then(this) + } + }) + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/language_model_selector/Cargo.toml b/crates/language_model_selector/Cargo.toml index e4b8b7256e..77a98e55ff 100644 --- a/crates/language_model_selector/Cargo.toml +++ b/crates/language_model_selector/Cargo.toml @@ -21,3 +21,4 @@ proto.workspace = true ui.workspace = true workspace.workspace = true zed_actions.workspace = true +popover_button.workspace = true diff --git a/crates/language_model_selector/src/language_model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs index cd03a6b1f5..cb4dcb4e1c 100644 --- a/crates/language_model_selector/src/language_model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -2,17 +2,26 @@ use std::sync::Arc; use feature_flags::ZedPro; use gpui::{ - Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, Subscription, Task, WeakEntity, + action_with_deprecated_aliases, Action, AnyElement, App, Corner, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; use language_model::{ AuthenticateError, LanguageModel, LanguageModelAvailability, LanguageModelRegistry, }; use picker::{Picker, PickerDelegate}; +use popover_button::{PopoverButton, TriggerablePopover}; use proto::Plan; -use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; +use ui::{ + prelude::*, ButtonLike, IconButtonShape, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, +}; use workspace::ShowConfiguration; +action_with_deprecated_aliases!( + assistant, + ToggleModelSelector, + ["assistant2::ToggleModelSelector"] +); + const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; type OnModelChanged = Arc, &App) + 'static>; @@ -22,6 +31,7 @@ pub struct LanguageModelSelector { /// The task used to update the picker's matches when there is a change to /// the language model registry. update_matches_task: Option>, + popover_menu_handle: PopoverMenuHandle, _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -53,6 +63,7 @@ impl LanguageModelSelector { LanguageModelSelector { picker, update_matches_task: None, + popover_menu_handle: PopoverMenuHandle::default(), _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), @@ -62,6 +73,15 @@ impl LanguageModelSelector { } } + pub fn toggle_model_selector( + &mut self, + _: &ToggleModelSelector, + window: &mut Window, + cx: &mut Context, + ) { + self.popover_menu_handle.toggle(window, cx); + } + fn handle_language_model_registry_event( &mut self, _registry: &Entity, @@ -181,62 +201,13 @@ impl Render for LanguageModelSelector { } } -#[derive(IntoElement)] -pub struct LanguageModelSelectorPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - language_model_selector: Entity, - trigger: T, - tooltip: TT, - handle: Option>, - anchor: Corner, -} - -impl LanguageModelSelectorPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - pub fn new( - language_model_selector: Entity, - trigger: T, - tooltip: TT, - anchor: Corner, - ) -> Self { - Self { - language_model_selector, - trigger, - tooltip, - handle: None, - anchor, - } - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle) -> Self { - self.handle = Some(handle); - self - } -} - -impl RenderOnce for LanguageModelSelectorPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let language_model_selector = self.language_model_selector.clone(); - - PopoverMenu::new("model-switcher") - .menu(move |_window, _cx| Some(language_model_selector.clone())) - .trigger_with_tooltip(self.trigger, self.tooltip) - .anchor(self.anchor) - .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle)) - .offset(gpui::Point { - x: px(0.0), - y: px(-2.0), - }) +impl TriggerablePopover for LanguageModelSelector { + fn menu_handle( + &mut self, + _window: &mut Window, + _cx: &mut gpui::Context, + ) -> PopoverMenuHandle { + self.popover_menu_handle.clone() } } @@ -521,3 +492,98 @@ impl PickerDelegate for LanguageModelPickerDelegate { ) } } + +pub struct InlineLanguageModelSelector { + selector: Entity, +} + +impl InlineLanguageModelSelector { + pub fn new(selector: Entity) -> Self { + Self { selector } + } +} + +impl RenderOnce for InlineLanguageModelSelector { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + PopoverButton::new( + self.selector, + gpui::Corner::TopRight, + IconButton::new("context", IconName::SettingsAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(Color::Muted), + move |window, cx| { + Tooltip::with_meta( + format!( + "Using {}", + LanguageModelRegistry::read_global(cx) + .active_model() + .map(|model| model.name().0) + .unwrap_or_else(|| "No model selected".into()), + ), + None, + "Change Model", + window, + cx, + ) + }, + ) + .render(window, cx) + } +} + +pub struct AssistantLanguageModelSelector { + focus_handle: FocusHandle, + selector: Entity, +} + +impl AssistantLanguageModelSelector { + pub fn new(focus_handle: FocusHandle, selector: Entity) -> Self { + Self { + focus_handle, + selector, + } + } +} + +impl RenderOnce for AssistantLanguageModelSelector { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + let focus_handle = self.focus_handle.clone(); + let model_name = match active_model { + Some(model) => model.name().0, + _ => SharedString::from("No model selected"), + }; + + popover_button::PopoverButton::new( + self.selector.clone(), + Corner::BottomRight, + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .gap_0p5() + .child( + Label::new(model_name) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ), + move |window, cx| { + Tooltip::for_action_in( + "Change Model", + &ToggleModelSelector, + &focus_handle, + window, + cx, + ) + }, + ) + .render(window, cx) + } +} diff --git a/crates/popover_button/Cargo.toml b/crates/popover_button/Cargo.toml new file mode 100644 index 0000000000..a138470ad5 --- /dev/null +++ b/crates/popover_button/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "popover_button" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/popover_button.rs" + +[features] +default = [] + +[dependencies] +gpui.workspace = true +ui.workspace = true diff --git a/crates/popover_button/LICENSE-GPL b/crates/popover_button/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/popover_button/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/popover_button/src/popover_button.rs b/crates/popover_button/src/popover_button.rs new file mode 100644 index 0000000000..348c9889e9 --- /dev/null +++ b/crates/popover_button/src/popover_button.rs @@ -0,0 +1,60 @@ +use gpui::{AnyView, Corner, Entity, ManagedView}; +use ui::{ + px, App, ButtonCommon, IntoElement, PopoverMenu, PopoverMenuHandle, PopoverTrigger, RenderOnce, + Window, +}; + +pub trait TriggerablePopover: ManagedView { + fn menu_handle( + &mut self, + window: &mut Window, + cx: &mut gpui::Context, + ) -> PopoverMenuHandle; +} + +// We want a button, that tells us what parameters to pass, and that "just works" after that +pub struct PopoverButton { + selector: Entity, + button: B, + tooltip: F, + corner: Corner, +} + +impl PopoverButton { + pub fn new(selector: Entity, corner: Corner, button: B, tooltip: F) -> Self + where + F: Fn(&mut Window, &mut App) -> AnyView + 'static, + { + Self { + selector, + button, + tooltip, + corner, + } + } +} + +impl RenderOnce + for PopoverButton +where + F: Fn(&mut Window, &mut App) -> AnyView + 'static, +{ + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let menu_handle = self + .selector + .update(cx, |selector, cx| selector.menu_handle(window, cx)); + + PopoverMenu::new("popover-button") + .menu({ + let selector = self.selector.clone(); + move |_window, _cx| Some(selector.clone()) + }) + .trigger_with_tooltip(self.button, self.tooltip) + .anchor(self.corner) + .with_handle(menu_handle) + .offset(gpui::Point { + x: px(0.0), + y: px(-2.0), + }) + } +}