There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
1213 lines
45 KiB
Rust
1213 lines
45 KiB
Rust
use crate::assistant_model_selector::AssistantModelSelector;
|
|
use crate::buffer_codegen::BufferCodegen;
|
|
use crate::context_picker::ContextPicker;
|
|
use crate::context_store::ContextStore;
|
|
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 client::ErrorExt;
|
|
use collections::VecDeque;
|
|
use editor::{
|
|
actions::{MoveDown, MoveUp},
|
|
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
|
|
};
|
|
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
|
use fs::Fs;
|
|
use gpui::{
|
|
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
|
|
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;
|
|
use std::sync::Arc;
|
|
use theme::ThemeSettings;
|
|
use ui::utils::WithRemSize;
|
|
use ui::{
|
|
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
|
|
};
|
|
use util::ResultExt;
|
|
use workspace::Workspace;
|
|
|
|
pub struct PromptEditor<T> {
|
|
pub editor: Entity<Editor>,
|
|
mode: PromptEditorMode,
|
|
context_store: Entity<ContextStore>,
|
|
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>,
|
|
pending_prompt: String,
|
|
_codegen_subscription: Subscription,
|
|
editor_subscriptions: Vec<Subscription>,
|
|
_context_strip_subscription: Subscription,
|
|
show_rate_limit_notice: bool,
|
|
_phantom: std::marker::PhantomData<T>,
|
|
}
|
|
|
|
impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
|
|
|
|
impl<T: 'static> Render for PromptEditor<T> {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
|
|
let mut buttons = Vec::new();
|
|
|
|
let left_gutter_width = match &self.mode {
|
|
PromptEditorMode::Buffer {
|
|
id: _,
|
|
codegen,
|
|
gutter_dimensions,
|
|
} => {
|
|
let codegen = codegen.read(cx);
|
|
|
|
if codegen.alternative_count(cx) > 1 {
|
|
buttons.push(self.render_cycle_controls(&codegen, cx));
|
|
}
|
|
|
|
let gutter_dimensions = gutter_dimensions.lock();
|
|
|
|
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
|
|
}
|
|
PromptEditorMode::Terminal { .. } => {
|
|
// Give the equivalent of the same left-padding that we're using on the right
|
|
Pixels::from(40.0)
|
|
}
|
|
};
|
|
|
|
let bottom_padding = match &self.mode {
|
|
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
|
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
|
};
|
|
|
|
buttons.extend(self.render_buttons(window, cx));
|
|
|
|
v_flex()
|
|
.key_context("PromptEditor")
|
|
.bg(cx.theme().colors().editor_background)
|
|
.block_mouse_down()
|
|
.gap_0p5()
|
|
.border_y_1()
|
|
.border_color(cx.theme().status().info_border)
|
|
.size_full()
|
|
.pt_0p5()
|
|
.pb(bottom_padding)
|
|
.pr_6()
|
|
.child(
|
|
h_flex()
|
|
.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(Self::confirm))
|
|
.on_action(cx.listener(Self::cancel))
|
|
.on_action(cx.listener(Self::move_up))
|
|
.on_action(cx.listener(Self::move_down))
|
|
.on_action(cx.listener(Self::remove_all_context))
|
|
.capture_action(cx.listener(Self::cycle_prev))
|
|
.capture_action(cx.listener(Self::cycle_next))
|
|
.child(
|
|
WithRemSize::new(ui_font_size)
|
|
.flex()
|
|
.flex_row()
|
|
.flex_shrink_0()
|
|
.items_center()
|
|
.h_full()
|
|
.w(left_gutter_width)
|
|
.justify_center()
|
|
.gap_2()
|
|
.child(self.render_close_button(cx))
|
|
.map(|el| {
|
|
let CodegenStatus::Error(error) = self.codegen_status(cx) else {
|
|
return el;
|
|
};
|
|
|
|
let error_message = SharedString::from(error.to_string());
|
|
if error.error_code() == proto::ErrorCode::RateLimitExceeded
|
|
&& cx.has_flag::<ZedPro>()
|
|
{
|
|
el.child(
|
|
v_flex()
|
|
.child(
|
|
IconButton::new(
|
|
"rate-limit-error",
|
|
IconName::XCircle,
|
|
)
|
|
.toggle_state(self.show_rate_limit_notice)
|
|
.shape(IconButtonShape::Square)
|
|
.icon_size(IconSize::Small)
|
|
.on_click(
|
|
cx.listener(Self::toggle_rate_limit_notice),
|
|
),
|
|
)
|
|
.children(self.show_rate_limit_notice.then(|| {
|
|
deferred(
|
|
anchored()
|
|
.position_mode(
|
|
gpui::AnchoredPositionMode::Local,
|
|
)
|
|
.position(point(px(0.), px(24.)))
|
|
.anchor(gpui::Corner::TopLeft)
|
|
.child(self.render_rate_limit_notice(cx)),
|
|
)
|
|
})),
|
|
)
|
|
} else {
|
|
el.child(
|
|
div()
|
|
.id("error")
|
|
.tooltip(Tooltip::text(error_message))
|
|
.child(
|
|
Icon::new(IconName::XCircle)
|
|
.size(IconSize::Small)
|
|
.color(Color::Error),
|
|
),
|
|
)
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.justify_between()
|
|
.child(div().flex_1().child(self.render_editor(window, cx)))
|
|
.child(
|
|
WithRemSize::new(ui_font_size)
|
|
.flex()
|
|
.flex_row()
|
|
.items_center()
|
|
.gap_1()
|
|
.children(buttons),
|
|
),
|
|
),
|
|
)
|
|
.child(
|
|
WithRemSize::new(ui_font_size)
|
|
.flex()
|
|
.flex_row()
|
|
.items_center()
|
|
.child(h_flex().flex_shrink_0().w(left_gutter_width))
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.pl_1()
|
|
.items_start()
|
|
.justify_between()
|
|
.child(self.context_strip.clone())
|
|
.child(self.model_selector.clone()),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<T: 'static> Focusable for PromptEditor<T> {
|
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
self.editor.focus_handle(cx)
|
|
}
|
|
}
|
|
|
|
impl<T: 'static> PromptEditor<T> {
|
|
const MAX_LINES: u8 = 8;
|
|
|
|
fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
|
|
PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
|
|
}
|
|
}
|
|
|
|
fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.editor_subscriptions.clear();
|
|
self.editor_subscriptions.push(cx.subscribe_in(
|
|
&self.editor,
|
|
window,
|
|
Self::handle_prompt_editor_events,
|
|
));
|
|
}
|
|
|
|
pub fn set_show_cursor_when_unfocused(
|
|
&mut self,
|
|
show_cursor_when_unfocused: bool,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
|
|
});
|
|
}
|
|
|
|
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let prompt = self.prompt(cx);
|
|
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
|
|
self.editor = cx.new(|cx| {
|
|
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);
|
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
|
editor.set_placeholder_text(Self::placeholder_text(&self.mode, window, cx), cx);
|
|
editor.set_placeholder_text("Add a prompt…", cx);
|
|
editor.set_text(prompt, window, cx);
|
|
if focus {
|
|
window.focus(&editor.focus_handle(cx));
|
|
}
|
|
editor
|
|
});
|
|
self.subscribe_to_editor(window, cx);
|
|
}
|
|
|
|
pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
|
|
let action = match mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => {
|
|
if codegen.read(cx).is_insertion {
|
|
"Generate"
|
|
} else {
|
|
"Transform"
|
|
}
|
|
}
|
|
PromptEditorMode::Terminal { .. } => "Generate",
|
|
};
|
|
|
|
let assistant_panel_keybinding =
|
|
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window)
|
|
.map(|keybinding| format!("{keybinding} to chat ― "))
|
|
.unwrap_or_default();
|
|
|
|
format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
|
|
}
|
|
|
|
pub fn prompt(&self, cx: &App) -> String {
|
|
self.editor.read(cx).text(cx)
|
|
}
|
|
|
|
fn toggle_rate_limit_notice(
|
|
&mut self,
|
|
_: &ClickEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.show_rate_limit_notice = !self.show_rate_limit_notice;
|
|
if self.show_rate_limit_notice {
|
|
window.focus(&self.editor.focus_handle(cx));
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
fn handle_prompt_editor_events(
|
|
&mut self,
|
|
_: &Entity<Editor>,
|
|
event: &EditorEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
EditorEvent::Edited { .. } => {
|
|
if let Some(workspace) = window.window_handle().downcast::<Workspace>() {
|
|
workspace
|
|
.update(cx, |workspace, _, cx| {
|
|
let is_via_ssh = workspace
|
|
.project()
|
|
.update(cx, |project, _| project.is_via_ssh());
|
|
|
|
workspace
|
|
.client()
|
|
.telemetry()
|
|
.log_edit_event("inline assist", is_via_ssh);
|
|
})
|
|
.log_err();
|
|
}
|
|
let prompt = self.editor.read(cx).text(cx);
|
|
if self
|
|
.prompt_history_ix
|
|
.map_or(true, |ix| self.prompt_history[ix] != prompt)
|
|
{
|
|
self.prompt_history_ix.take();
|
|
self.pending_prompt = prompt;
|
|
}
|
|
|
|
self.edited_since_done = true;
|
|
cx.notify();
|
|
}
|
|
EditorEvent::Blurred => {
|
|
if self.show_rate_limit_notice {
|
|
self.show_rate_limit_notice = false;
|
|
cx.notify();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn toggle_context_picker(
|
|
&mut self,
|
|
_: &ToggleContextPicker,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
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,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.context_store.update(cx, |store, _cx| store.clear());
|
|
cx.notify();
|
|
}
|
|
|
|
fn cancel(
|
|
&mut self,
|
|
_: &editor::actions::Cancel,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match self.codegen_status(cx) {
|
|
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
|
cx.emit(PromptEditorEvent::CancelRequested);
|
|
}
|
|
CodegenStatus::Pending => {
|
|
cx.emit(PromptEditorEvent::StopRequested);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
|
match self.codegen_status(cx) {
|
|
CodegenStatus::Idle => {
|
|
cx.emit(PromptEditorEvent::StartRequested);
|
|
}
|
|
CodegenStatus::Pending => {
|
|
cx.emit(PromptEditorEvent::DismissRequested);
|
|
}
|
|
CodegenStatus::Done => {
|
|
if self.edited_since_done {
|
|
cx.emit(PromptEditorEvent::StartRequested);
|
|
} else {
|
|
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
|
}
|
|
}
|
|
CodegenStatus::Error(_) => {
|
|
cx.emit(PromptEditorEvent::StartRequested);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(ix) = self.prompt_history_ix {
|
|
if ix > 0 {
|
|
self.prompt_history_ix = Some(ix - 1);
|
|
let prompt = self.prompt_history[ix - 1].as_str();
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.set_text(prompt, window, cx);
|
|
editor.move_to_beginning(&Default::default(), window, cx);
|
|
});
|
|
}
|
|
} else if !self.prompt_history.is_empty() {
|
|
self.prompt_history_ix = Some(self.prompt_history.len() - 1);
|
|
let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.set_text(prompt, window, cx);
|
|
editor.move_to_beginning(&Default::default(), window, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(ix) = self.prompt_history_ix {
|
|
if ix < self.prompt_history.len() - 1 {
|
|
self.prompt_history_ix = Some(ix + 1);
|
|
let prompt = self.prompt_history[ix + 1].as_str();
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.set_text(prompt, window, cx);
|
|
editor.move_to_end(&Default::default(), window, cx)
|
|
});
|
|
} else {
|
|
self.prompt_history_ix = None;
|
|
let prompt = self.pending_prompt.as_str();
|
|
self.editor.update(cx, |editor, cx| {
|
|
editor.set_text(prompt, window, cx);
|
|
editor.move_to_end(&Default::default(), window, cx)
|
|
});
|
|
}
|
|
} else {
|
|
self.context_strip.focus_handle(cx).focus(window);
|
|
}
|
|
}
|
|
|
|
fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
|
let mode = match &self.mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => {
|
|
let codegen = codegen.read(cx);
|
|
if codegen.is_insertion {
|
|
GenerationMode::Generate
|
|
} else {
|
|
GenerationMode::Transform
|
|
}
|
|
}
|
|
PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
|
|
};
|
|
|
|
let codegen_status = self.codegen_status(cx);
|
|
|
|
match codegen_status {
|
|
CodegenStatus::Idle => {
|
|
vec![Button::new("start", mode.start_label())
|
|
.label_size(LabelSize::Small)
|
|
.icon(IconName::Return)
|
|
.icon_size(IconSize::XSmall)
|
|
.icon_color(Color::Muted)
|
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
|
.into_any_element()]
|
|
}
|
|
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
|
|
.icon_color(Color::Error)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::with_meta(
|
|
mode.tooltip_interrupt(),
|
|
Some(&menu::Cancel),
|
|
"Changes won't be discarded",
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
|
.into_any_element()],
|
|
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
|
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
|
|
if has_error || self.edited_since_done {
|
|
vec![IconButton::new("restart", IconName::RotateCw)
|
|
.icon_color(Color::Info)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::with_meta(
|
|
mode.tooltip_restart(),
|
|
Some(&menu::Confirm),
|
|
"Changes will be discarded",
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.on_click(cx.listener(|_, _, _, cx| {
|
|
cx.emit(PromptEditorEvent::StartRequested);
|
|
}))
|
|
.into_any_element()]
|
|
} else {
|
|
let accept = IconButton::new("accept", IconName::Check)
|
|
.icon_color(Color::Info)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(move |window, cx| {
|
|
Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
|
|
})
|
|
.on_click(cx.listener(|_, _, _, cx| {
|
|
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
|
}))
|
|
.into_any_element();
|
|
|
|
match &self.mode {
|
|
PromptEditorMode::Terminal { .. } => vec![
|
|
accept,
|
|
IconButton::new("confirm", IconName::Play)
|
|
.icon_color(Color::Info)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(|window, cx| {
|
|
Tooltip::for_action(
|
|
"Execute Generated Command",
|
|
&menu::SecondaryConfirm,
|
|
window,
|
|
cx,
|
|
)
|
|
})
|
|
.on_click(cx.listener(|_, _, _, cx| {
|
|
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
|
}))
|
|
.into_any_element(),
|
|
],
|
|
PromptEditorMode::Buffer { .. } => vec![accept],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cycle_prev(
|
|
&mut self,
|
|
_: &CyclePreviousInlineAssist,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => {
|
|
codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
|
|
}
|
|
PromptEditorMode::Terminal { .. } => {
|
|
// no cycle buttons in terminal mode
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => {
|
|
codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
|
|
}
|
|
PromptEditorMode::Terminal { .. } => {
|
|
// no cycle buttons in terminal mode
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
|
IconButton::new("cancel", IconName::Close)
|
|
.icon_color(Color::Muted)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip(Tooltip::text("Close Assistant"))
|
|
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
|
|
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
|
|
|
|
let model_registry = LanguageModelRegistry::read_global(cx);
|
|
let default_model = model_registry.active_model();
|
|
let alternative_models = model_registry.inline_alternative_models();
|
|
|
|
let get_model_name = |index: usize| -> String {
|
|
let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
|
|
|
|
match index {
|
|
0 => default_model.as_ref().map_or_else(String::new, name),
|
|
index if index <= alternative_models.len() => alternative_models
|
|
.get(index - 1)
|
|
.map_or_else(String::new, name),
|
|
_ => String::new(),
|
|
}
|
|
};
|
|
|
|
let total_models = alternative_models.len() + 1;
|
|
|
|
if total_models <= 1 {
|
|
return div().into_any_element();
|
|
}
|
|
|
|
let current_index = codegen.active_alternative;
|
|
let prev_index = (current_index + total_models - 1) % total_models;
|
|
let next_index = (current_index + 1) % total_models;
|
|
|
|
let prev_model_name = get_model_name(prev_index);
|
|
let next_model_name = get_model_name(next_index);
|
|
|
|
h_flex()
|
|
.child(
|
|
IconButton::new("previous", IconName::ChevronLeft)
|
|
.icon_color(Color::Muted)
|
|
.disabled(disabled || current_index == 0)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip({
|
|
let focus_handle = self.editor.focus_handle(cx);
|
|
move |window, cx| {
|
|
cx.new(|_| {
|
|
let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
|
|
KeyBinding::for_action_in(
|
|
&CyclePreviousInlineAssist,
|
|
&focus_handle,
|
|
window,
|
|
),
|
|
);
|
|
if !disabled && current_index != 0 {
|
|
tooltip = tooltip.meta(prev_model_name.clone());
|
|
}
|
|
tooltip
|
|
})
|
|
.into()
|
|
}
|
|
})
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
|
|
})),
|
|
)
|
|
.child(
|
|
Label::new(format!(
|
|
"{}/{}",
|
|
codegen.active_alternative + 1,
|
|
codegen.alternative_count(cx)
|
|
))
|
|
.size(LabelSize::Small)
|
|
.color(if disabled {
|
|
Color::Disabled
|
|
} else {
|
|
Color::Muted
|
|
}),
|
|
)
|
|
.child(
|
|
IconButton::new("next", IconName::ChevronRight)
|
|
.icon_color(Color::Muted)
|
|
.disabled(disabled || current_index == total_models - 1)
|
|
.shape(IconButtonShape::Square)
|
|
.tooltip({
|
|
let focus_handle = self.editor.focus_handle(cx);
|
|
move |window, cx| {
|
|
cx.new(|_| {
|
|
let mut tooltip = Tooltip::new("Next Alternative").key_binding(
|
|
KeyBinding::for_action_in(
|
|
&CycleNextInlineAssist,
|
|
&focus_handle,
|
|
window,
|
|
),
|
|
);
|
|
if !disabled && current_index != total_models - 1 {
|
|
tooltip = tooltip.meta(next_model_name.clone());
|
|
}
|
|
tooltip
|
|
})
|
|
.into()
|
|
}
|
|
})
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.cycle_next(&CycleNextInlineAssist, window, cx)
|
|
})),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
Popover::new().child(
|
|
v_flex()
|
|
.occlude()
|
|
.p_2()
|
|
.child(
|
|
Label::new("Out of Tokens")
|
|
.size(LabelSize::Small)
|
|
.weight(FontWeight::BOLD),
|
|
)
|
|
.child(Label::new(
|
|
"Try Zed Pro for higher limits, a wider range of models, and more.",
|
|
))
|
|
.child(
|
|
h_flex()
|
|
.justify_between()
|
|
.child(CheckboxWithLabel::new(
|
|
"dont-show-again",
|
|
Label::new("Don't show again"),
|
|
if dismissed_rate_limit_notice() {
|
|
ui::ToggleState::Selected
|
|
} else {
|
|
ui::ToggleState::Unselected
|
|
},
|
|
|selection, _, cx| {
|
|
let is_dismissed = match selection {
|
|
ui::ToggleState::Unselected => false,
|
|
ui::ToggleState::Indeterminate => return,
|
|
ui::ToggleState::Selected => true,
|
|
};
|
|
|
|
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
|
},
|
|
))
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.child(
|
|
Button::new("dismiss", "Dismiss")
|
|
.style(ButtonStyle::Transparent)
|
|
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
|
|
)
|
|
.child(Button::new("more-info", "More Info").on_click(
|
|
|_event, window, cx| {
|
|
window.dispatch_action(
|
|
Box::new(zed_actions::OpenAccountSettings),
|
|
cx,
|
|
)
|
|
},
|
|
)),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
|
let font_size = TextSize::Default.rems(cx);
|
|
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
|
|
|
|
div()
|
|
.key_context("InlineAssistEditor")
|
|
.size_full()
|
|
.p_2()
|
|
.pl_1()
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child({
|
|
let settings = ThemeSettings::get_global(cx);
|
|
let text_style = TextStyle {
|
|
color: cx.theme().colors().editor_foreground,
|
|
font_family: settings.buffer_font.family.clone(),
|
|
font_features: settings.buffer_font.features.clone(),
|
|
font_size: font_size.into(),
|
|
line_height: line_height.into(),
|
|
..Default::default()
|
|
};
|
|
|
|
EditorElement::new(
|
|
&self.editor,
|
|
EditorStyle {
|
|
background: cx.theme().colors().editor_background,
|
|
local_player: cx.theme().players().local(),
|
|
text: text_style,
|
|
..Default::default()
|
|
},
|
|
)
|
|
})
|
|
.into_any_element()
|
|
}
|
|
|
|
fn handle_context_strip_event(
|
|
&mut self,
|
|
_context_strip: &Entity<ContextStrip>,
|
|
event: &ContextStripEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
match event {
|
|
ContextStripEvent::PickerDismissed
|
|
| ContextStripEvent::BlurredEmpty
|
|
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
|
|
ContextStripEvent::BlurredDown => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum PromptEditorMode {
|
|
Buffer {
|
|
id: InlineAssistId,
|
|
codegen: Entity<BufferCodegen>,
|
|
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
|
},
|
|
Terminal {
|
|
id: TerminalInlineAssistId,
|
|
codegen: Entity<TerminalCodegen>,
|
|
height_in_lines: u8,
|
|
},
|
|
}
|
|
|
|
pub enum PromptEditorEvent {
|
|
StartRequested,
|
|
StopRequested,
|
|
ConfirmRequested { execute: bool },
|
|
CancelRequested,
|
|
DismissRequested,
|
|
Resized { height_in_lines: u8 },
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
|
pub struct InlineAssistId(pub usize);
|
|
|
|
impl InlineAssistId {
|
|
pub fn post_inc(&mut self) -> InlineAssistId {
|
|
let id = *self;
|
|
self.0 += 1;
|
|
id
|
|
}
|
|
}
|
|
|
|
impl PromptEditor<BufferCodegen> {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new_buffer(
|
|
id: InlineAssistId,
|
|
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
|
prompt_history: VecDeque<String>,
|
|
prompt_buffer: Entity<MultiBuffer>,
|
|
codegen: Entity<BufferCodegen>,
|
|
fs: Arc<dyn Fs>,
|
|
context_store: Entity<ContextStore>,
|
|
workspace: WeakEntity<Workspace>,
|
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
|
) -> PromptEditor<BufferCodegen> {
|
|
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
|
let mode = PromptEditorMode::Buffer {
|
|
id,
|
|
codegen,
|
|
gutter_dimensions,
|
|
};
|
|
|
|
let prompt_editor = cx.new(|cx| {
|
|
let mut editor = Editor::new(
|
|
EditorMode::AutoHeight {
|
|
max_lines: Self::MAX_LINES as usize,
|
|
},
|
|
prompt_buffer,
|
|
None,
|
|
false,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
|
// Since the prompt editors for all inline assistants are linked,
|
|
// always show the cursor (even when it isn't focused) because
|
|
// typing in one will make what you typed appear in all of them.
|
|
editor.set_show_cursor_when_unfocused(true, cx);
|
|
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
|
|
editor
|
|
});
|
|
let context_picker_menu_handle = PopoverMenuHandle::default();
|
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
|
|
|
let context_strip = cx.new(|cx| {
|
|
ContextStrip::new(
|
|
context_store.clone(),
|
|
workspace.clone(),
|
|
prompt_editor.downgrade(),
|
|
thread_store.clone(),
|
|
context_picker_menu_handle.clone(),
|
|
SuggestContextKind::Thread,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let context_strip_subscription =
|
|
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
|
|
|
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
|
|
editor: prompt_editor.clone(),
|
|
context_store,
|
|
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,
|
|
)
|
|
}),
|
|
model_selector_menu_handle,
|
|
edited_since_done: false,
|
|
prompt_history,
|
|
prompt_history_ix: None,
|
|
pending_prompt: String::new(),
|
|
_codegen_subscription: codegen_subscription,
|
|
editor_subscriptions: Vec::new(),
|
|
_context_strip_subscription: context_strip_subscription,
|
|
show_rate_limit_notice: false,
|
|
mode,
|
|
_phantom: Default::default(),
|
|
};
|
|
|
|
this.subscribe_to_editor(window, cx);
|
|
this
|
|
}
|
|
|
|
fn handle_codegen_changed(
|
|
&mut self,
|
|
_: Entity<BufferCodegen>,
|
|
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
|
) {
|
|
match self.codegen_status(cx) {
|
|
CodegenStatus::Idle => {
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(false));
|
|
}
|
|
CodegenStatus::Pending => {
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(true));
|
|
}
|
|
CodegenStatus::Done => {
|
|
self.edited_since_done = false;
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(false));
|
|
}
|
|
CodegenStatus::Error(error) => {
|
|
if cx.has_flag::<ZedPro>()
|
|
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
|
&& !dismissed_rate_limit_notice()
|
|
{
|
|
self.show_rate_limit_notice = true;
|
|
cx.notify();
|
|
}
|
|
|
|
self.edited_since_done = false;
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> InlineAssistId {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { id, .. } => *id,
|
|
PromptEditorMode::Terminal { .. } => unreachable!(),
|
|
}
|
|
}
|
|
|
|
pub fn codegen(&self) -> &Entity<BufferCodegen> {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { codegen, .. } => codegen,
|
|
PromptEditorMode::Terminal { .. } => unreachable!(),
|
|
}
|
|
}
|
|
|
|
pub fn gutter_dimensions(&self) -> &Arc<Mutex<GutterDimensions>> {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer {
|
|
gutter_dimensions, ..
|
|
} => gutter_dimensions,
|
|
PromptEditorMode::Terminal { .. } => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
|
pub struct TerminalInlineAssistId(pub usize);
|
|
|
|
impl TerminalInlineAssistId {
|
|
pub fn post_inc(&mut self) -> TerminalInlineAssistId {
|
|
let id = *self;
|
|
self.0 += 1;
|
|
id
|
|
}
|
|
}
|
|
|
|
impl PromptEditor<TerminalCodegen> {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new_terminal(
|
|
id: TerminalInlineAssistId,
|
|
prompt_history: VecDeque<String>,
|
|
prompt_buffer: Entity<MultiBuffer>,
|
|
codegen: Entity<TerminalCodegen>,
|
|
fs: Arc<dyn Fs>,
|
|
context_store: Entity<ContextStore>,
|
|
workspace: WeakEntity<Workspace>,
|
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
|
|
let mode = PromptEditorMode::Terminal {
|
|
id,
|
|
codegen,
|
|
height_in_lines: 1,
|
|
};
|
|
|
|
let prompt_editor = cx.new(|cx| {
|
|
let mut editor = Editor::new(
|
|
EditorMode::AutoHeight {
|
|
max_lines: Self::MAX_LINES as usize,
|
|
},
|
|
prompt_buffer,
|
|
None,
|
|
false,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
|
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
|
|
editor
|
|
});
|
|
let context_picker_menu_handle = PopoverMenuHandle::default();
|
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
|
|
|
let context_strip = cx.new(|cx| {
|
|
ContextStrip::new(
|
|
context_store.clone(),
|
|
workspace.clone(),
|
|
prompt_editor.downgrade(),
|
|
thread_store.clone(),
|
|
context_picker_menu_handle.clone(),
|
|
SuggestContextKind::Thread,
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let context_strip_subscription =
|
|
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
|
|
|
|
let mut this = Self {
|
|
editor: prompt_editor.clone(),
|
|
context_store,
|
|
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,
|
|
)
|
|
}),
|
|
model_selector_menu_handle,
|
|
edited_since_done: false,
|
|
prompt_history,
|
|
prompt_history_ix: None,
|
|
pending_prompt: String::new(),
|
|
_codegen_subscription: codegen_subscription,
|
|
editor_subscriptions: Vec::new(),
|
|
_context_strip_subscription: context_strip_subscription,
|
|
mode,
|
|
show_rate_limit_notice: false,
|
|
_phantom: Default::default(),
|
|
};
|
|
this.count_lines(cx);
|
|
this.subscribe_to_editor(window, cx);
|
|
this
|
|
}
|
|
|
|
fn count_lines(&mut self, cx: &mut Context<Self>) {
|
|
let height_in_lines = cmp::max(
|
|
2, // Make the editor at least two lines tall, to account for padding and buttons.
|
|
cmp::min(
|
|
self.editor
|
|
.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
|
|
Self::MAX_LINES as u32,
|
|
),
|
|
) as u8;
|
|
|
|
match &mut self.mode {
|
|
PromptEditorMode::Terminal {
|
|
height_in_lines: current_height,
|
|
..
|
|
} => {
|
|
if height_in_lines != *current_height {
|
|
*current_height = height_in_lines;
|
|
cx.emit(PromptEditorEvent::Resized { height_in_lines });
|
|
}
|
|
}
|
|
PromptEditorMode::Buffer { .. } => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
|
match &self.codegen().read(cx).status {
|
|
CodegenStatus::Idle => {
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(false));
|
|
}
|
|
CodegenStatus::Pending => {
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(true));
|
|
}
|
|
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
|
self.edited_since_done = false;
|
|
self.editor
|
|
.update(cx, |editor, _| editor.set_read_only(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { .. } => unreachable!(),
|
|
PromptEditorMode::Terminal { codegen, .. } => codegen,
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> TerminalInlineAssistId {
|
|
match &self.mode {
|
|
PromptEditorMode::Buffer { .. } => unreachable!(),
|
|
PromptEditorMode::Terminal { id, .. } => *id,
|
|
}
|
|
}
|
|
}
|
|
|
|
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
|
|
|
|
fn dismissed_rate_limit_notice() -> bool {
|
|
db::kvp::KEY_VALUE_STORE
|
|
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
|
|
.log_err()
|
|
.map_or(false, |s| s.is_some())
|
|
}
|
|
|
|
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
|
|
db::write_and_log(cx, move || async move {
|
|
if is_dismissed {
|
|
db::kvp::KEY_VALUE_STORE
|
|
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
|
|
.await
|
|
} else {
|
|
db::kvp::KEY_VALUE_STORE
|
|
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
|
|
.await
|
|
}
|
|
})
|
|
}
|
|
|
|
pub enum CodegenStatus {
|
|
Idle,
|
|
Pending,
|
|
Done,
|
|
Error(anyhow::Error),
|
|
}
|
|
|
|
/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
|
|
#[derive(Copy, Clone)]
|
|
pub enum CancelButtonState {
|
|
Idle,
|
|
Pending,
|
|
Done,
|
|
Error,
|
|
}
|
|
|
|
impl Into<CancelButtonState> for &CodegenStatus {
|
|
fn into(self) -> CancelButtonState {
|
|
match self {
|
|
CodegenStatus::Idle => CancelButtonState::Idle,
|
|
CodegenStatus::Pending => CancelButtonState::Pending,
|
|
CodegenStatus::Done => CancelButtonState::Done,
|
|
CodegenStatus::Error(_) => CancelButtonState::Error,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum GenerationMode {
|
|
Generate,
|
|
Transform,
|
|
}
|
|
|
|
impl GenerationMode {
|
|
fn start_label(self) -> &'static str {
|
|
match self {
|
|
GenerationMode::Generate { .. } => "Generate",
|
|
GenerationMode::Transform => "Transform",
|
|
}
|
|
}
|
|
fn tooltip_interrupt(self) -> &'static str {
|
|
match self {
|
|
GenerationMode::Generate { .. } => "Interrupt Generation",
|
|
GenerationMode::Transform => "Interrupt Transform",
|
|
}
|
|
}
|
|
|
|
fn tooltip_restart(self) -> &'static str {
|
|
match self {
|
|
GenerationMode::Generate { .. } => "Restart Generation",
|
|
GenerationMode::Transform => "Restart Transform",
|
|
}
|
|
}
|
|
|
|
fn tooltip_accept(self) -> &'static str {
|
|
match self {
|
|
GenerationMode::Generate { .. } => "Accept Generation",
|
|
GenerationMode::Transform => "Accept Transform",
|
|
}
|
|
}
|
|
}
|