Detailed changes
@@ -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"
@@ -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" }
@@ -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"
@@ -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"
@@ -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;
@@ -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<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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());
@@ -38,7 +38,6 @@ actions!(
NewThread,
NewPromptEditor,
ToggleContextPicker,
- ToggleModelSelector,
RemoveAllContext,
OpenHistory,
OpenConfiguration,
@@ -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<LanguageModelSelector>,
- menu_handle: PopoverMenuHandle<LanguageModelSelector>,
+ pub selector: Entity<LanguageModelSelector>,
focus_handle: FocusHandle,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
- menu_handle: PopoverMenuHandle<LanguageModelSelector>,
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<Self>) -> 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<Self>) -> impl IntoElement {
+ AssistantLanguageModelSelector::new(self.focus_handle.clone(), self.selector.clone())
+ .render(window, cx)
}
}
@@ -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<T> {
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
- model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -104,7 +102,12 @@ impl<T: 'static> Render for PromptEditor<T> {
.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<T: 'static> PromptEditor<T> {
self.context_picker_menu_handle.toggle(window, cx);
}
- fn toggle_model_selector(
- &mut self,
- _: &ToggleModelSelector,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.model_selector_menu_handle.toggle(window, cx);
- }
-
pub fn remove_all_context(
&mut self,
_: &RemoveAllContext,
@@ -864,7 +858,6 @@ impl PromptEditor<BufferCodegen> {
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<BufferCodegen> {
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<TerminalCodegen> {
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<TerminalCodegen> {
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,
@@ -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<Thread>,
@@ -36,7 +35,6 @@ pub struct MessageEditor {
inline_context_picker: Entity<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
- model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
}
@@ -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>,
- ) {
- self.model_selector_menu_handle.toggle(window, cx)
- }
-
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
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))
@@ -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<Entity<Worktree>>,
language_model_selector: Entity<LanguageModelSelector>,
- language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
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::<Vec<_>>();
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>,
- ) {
- self.language_model_selector_menu_handle.toggle(window, cx);
- }
-
fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
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<Self>) -> 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<Self>) -> Option<AnyElement> {
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()
@@ -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
@@ -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<Workspace>,
) {
- 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<Project>, window: &mut Window, cx: &mut App) -> Entity<BranchList> {
+ 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<Picker<BranchListDelegate>>,
rem_width: f32,
- _subscription: Subscription,
+ popover_handle: PopoverMenuHandle<Self>,
+ default_focus_handle: FocusHandle,
+ project: Entity<Project>,
+ style: BranchListStyle,
+ pub picker: Option<Entity<Picker<BranchListDelegate>>>,
+ _subscription: Option<Subscription>,
+}
+
+impl popover_button::TriggerablePopover for BranchList {
+ fn menu_handle(
+ &mut self,
+ _window: &mut Window,
+ _cx: &mut gpui::Context<Self>,
+ ) -> PopoverMenuHandle<Self> {
+ self.popover_handle.clone()
+ }
}
impl BranchList {
- pub fn new(
- delegate: BranchListDelegate,
- rem_width: f32,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> 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<Project>, 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<Self>) {
+ 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<DismissEvent> 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<Self>) -> 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<BranchEntry>,
all_branches: Vec<Branch>,
- workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ 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<Workspace>,
+ async fn new(
+ project: Entity<Project>,
+ style: BranchListStyle,
branch_name_trailoff_after: usize,
cx: &AsyncApp,
) -> Result<Self> {
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| {
@@ -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<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
@@ -139,9 +143,11 @@ impl CommitModal {
is_open,
active_index,
};
+
+ let project = workspace.project().clone();
workspace.open_panel::<GitPanel>(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<GitPanel>,
restore_dock: RestoreDock,
+ project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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<Self>) -> impl IntoElement {
+ pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> 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(|| "<no branch>".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()
@@ -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<T>(self, option: &Option<T>, 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"))]
@@ -21,3 +21,4 @@ proto.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
+popover_button.workspace = true
@@ -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<dyn Fn(Arc<dyn LanguageModel>, &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<Task<()>>,
+ popover_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
}
@@ -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>,
+ ) {
+ self.popover_menu_handle.toggle(window, cx);
+ }
+
fn handle_language_model_registry_event(
&mut self,
_registry: &Entity<LanguageModelRegistry>,
@@ -181,62 +201,13 @@ impl Render for LanguageModelSelector {
}
}
-#[derive(IntoElement)]
-pub struct LanguageModelSelectorPopoverMenu<T, TT>
-where
- T: PopoverTrigger + ButtonCommon,
- TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
-{
- language_model_selector: Entity<LanguageModelSelector>,
- trigger: T,
- tooltip: TT,
- handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
- anchor: Corner,
-}
-
-impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
-where
- T: PopoverTrigger + ButtonCommon,
- TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
-{
- pub fn new(
- language_model_selector: Entity<LanguageModelSelector>,
- trigger: T,
- tooltip: TT,
- anchor: Corner,
- ) -> Self {
- Self {
- language_model_selector,
- trigger,
- tooltip,
- handle: None,
- anchor,
- }
- }
-
- pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
- self.handle = Some(handle);
- self
- }
-}
-
-impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
-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<Self>,
+ ) -> PopoverMenuHandle<Self> {
+ self.popover_menu_handle.clone()
}
}
@@ -521,3 +492,98 @@ impl PickerDelegate for LanguageModelPickerDelegate {
)
}
}
+
+pub struct InlineLanguageModelSelector {
+ selector: Entity<LanguageModelSelector>,
+}
+
+impl InlineLanguageModelSelector {
+ pub fn new(selector: Entity<LanguageModelSelector>) -> 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<LanguageModelSelector>,
+}
+
+impl AssistantLanguageModelSelector {
+ pub fn new(focus_handle: FocusHandle, selector: Entity<LanguageModelSelector>) -> 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)
+ }
+}
@@ -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
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -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<Self>,
+ ) -> PopoverMenuHandle<Self>;
+}
+
+// We want a button, that tells us what parameters to pass, and that "just works" after that
+pub struct PopoverButton<T, B, F> {
+ selector: Entity<T>,
+ button: B,
+ tooltip: F,
+ corner: Corner,
+}
+
+impl<T, B, F> PopoverButton<T, B, F> {
+ pub fn new(selector: Entity<T>, corner: Corner, button: B, tooltip: F) -> Self
+ where
+ F: Fn(&mut Window, &mut App) -> AnyView + 'static,
+ {
+ Self {
+ selector,
+ button,
+ tooltip,
+ corner,
+ }
+ }
+}
+
+impl<T: TriggerablePopover, B: PopoverTrigger + ButtonCommon, F> RenderOnce
+ for PopoverButton<T, B, F>
+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),
+ })
+ }
+}