message_editor.rs

  1use std::sync::Arc;
  2
  3use editor::actions::MoveUp;
  4use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
  5use fs::Fs;
  6use gpui::{
  7    pulsating_between, Animation, AnimationExt, AppContext, DismissEvent, FocusableView, Model,
  8    Subscription, TextStyle, View, WeakModel, WeakView,
  9};
 10use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
 11use language_model_selector::LanguageModelSelector;
 12use rope::Point;
 13use settings::Settings;
 14use std::time::Duration;
 15use theme::ThemeSettings;
 16use ui::{
 17    prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
 18};
 19use workspace::Workspace;
 20
 21use crate::assistant_model_selector::AssistantModelSelector;
 22use crate::context_picker::{ConfirmBehavior, ContextPicker};
 23use crate::context_store::{refresh_context_store_text, ContextStore};
 24use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
 25use crate::thread::{RequestKind, Thread};
 26use crate::thread_store::ThreadStore;
 27use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
 28
 29pub struct MessageEditor {
 30    thread: Model<Thread>,
 31    editor: View<Editor>,
 32    context_store: Model<ContextStore>,
 33    context_strip: View<ContextStrip>,
 34    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 35    inline_context_picker: View<ContextPicker>,
 36    inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 37    model_selector: View<AssistantModelSelector>,
 38    model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 39    use_tools: bool,
 40    _subscriptions: Vec<Subscription>,
 41}
 42
 43impl MessageEditor {
 44    pub fn new(
 45        fs: Arc<dyn Fs>,
 46        workspace: WeakView<Workspace>,
 47        thread_store: WeakModel<ThreadStore>,
 48        thread: Model<Thread>,
 49        cx: &mut ViewContext<Self>,
 50    ) -> Self {
 51        let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
 52        let context_picker_menu_handle = PopoverMenuHandle::default();
 53        let inline_context_picker_menu_handle = PopoverMenuHandle::default();
 54        let model_selector_menu_handle = PopoverMenuHandle::default();
 55
 56        let editor = cx.new_view(|cx| {
 57            let mut editor = Editor::auto_height(10, cx);
 58            editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
 59            editor.set_show_indent_guides(false, cx);
 60
 61            editor
 62        });
 63
 64        let inline_context_picker = cx.new_view(|cx| {
 65            ContextPicker::new(
 66                workspace.clone(),
 67                Some(thread_store.clone()),
 68                context_store.downgrade(),
 69                editor.downgrade(),
 70                ConfirmBehavior::Close,
 71                cx,
 72            )
 73        });
 74
 75        let context_strip = cx.new_view(|cx| {
 76            ContextStrip::new(
 77                context_store.clone(),
 78                workspace.clone(),
 79                editor.downgrade(),
 80                Some(thread_store.clone()),
 81                context_picker_menu_handle.clone(),
 82                SuggestContextKind::File,
 83                cx,
 84            )
 85        });
 86
 87        let subscriptions = vec![
 88            cx.subscribe(&editor, Self::handle_editor_event),
 89            cx.subscribe(
 90                &inline_context_picker,
 91                Self::handle_inline_context_picker_event,
 92            ),
 93            cx.subscribe(&context_strip, Self::handle_context_strip_event),
 94        ];
 95
 96        Self {
 97            thread,
 98            editor: editor.clone(),
 99            context_store,
100            context_strip,
101            context_picker_menu_handle,
102            inline_context_picker,
103            inline_context_picker_menu_handle,
104            model_selector: cx.new_view(|cx| {
105                AssistantModelSelector::new(
106                    fs,
107                    model_selector_menu_handle.clone(),
108                    editor.focus_handle(cx),
109                    cx,
110                )
111            }),
112            model_selector_menu_handle,
113            use_tools: false,
114            _subscriptions: subscriptions,
115        }
116    }
117
118    fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
119        self.model_selector_menu_handle.toggle(cx)
120    }
121
122    fn toggle_chat_mode(&mut self, _: &ChatMode, cx: &mut ViewContext<Self>) {
123        self.use_tools = !self.use_tools;
124        cx.notify();
125    }
126
127    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
128        self.context_picker_menu_handle.toggle(cx);
129    }
130
131    pub fn remove_all_context(&mut self, _: &RemoveAllContext, cx: &mut ViewContext<Self>) {
132        self.context_store.update(cx, |store, _cx| store.clear());
133        cx.notify();
134    }
135
136    fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
137        self.send_to_model(RequestKind::Chat, cx);
138    }
139
140    fn is_editor_empty(&self, cx: &AppContext) -> bool {
141        self.editor.read(cx).text(cx).is_empty()
142    }
143
144    fn is_model_selected(&self, cx: &AppContext) -> bool {
145        LanguageModelRegistry::read_global(cx)
146            .active_model()
147            .is_some()
148    }
149
150    fn send_to_model(&mut self, request_kind: RequestKind, cx: &mut ViewContext<Self>) {
151        let provider = LanguageModelRegistry::read_global(cx).active_provider();
152        if provider
153            .as_ref()
154            .map_or(false, |provider| provider.must_accept_terms(cx))
155        {
156            cx.notify();
157            return;
158        }
159
160        let model_registry = LanguageModelRegistry::read_global(cx);
161        let Some(model) = model_registry.active_model() else {
162            return;
163        };
164
165        let user_message = self.editor.update(cx, |editor, cx| {
166            let text = editor.text(cx);
167            editor.clear(cx);
168            text
169        });
170
171        let refresh_task = refresh_context_store_text(self.context_store.clone(), cx);
172
173        let thread = self.thread.clone();
174        let context_store = self.context_store.clone();
175        let use_tools = self.use_tools;
176        cx.spawn(move |_, mut cx| async move {
177            refresh_task.await;
178            thread
179                .update(&mut cx, |thread, cx| {
180                    let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
181                    thread.insert_user_message(user_message, context, cx);
182                    let mut request = thread.to_completion_request(request_kind, cx);
183
184                    if use_tools {
185                        request.tools = thread
186                            .tools()
187                            .tools(cx)
188                            .into_iter()
189                            .map(|tool| LanguageModelRequestTool {
190                                name: tool.name(),
191                                description: tool.description(),
192                                input_schema: tool.input_schema(),
193                            })
194                            .collect();
195                    }
196
197                    thread.stream_completion(request, model, cx)
198                })
199                .ok();
200        })
201        .detach();
202    }
203
204    fn handle_editor_event(
205        &mut self,
206        editor: View<Editor>,
207        event: &EditorEvent,
208        cx: &mut ViewContext<Self>,
209    ) {
210        match event {
211            EditorEvent::SelectionsChanged { .. } => {
212                editor.update(cx, |editor, cx| {
213                    let snapshot = editor.buffer().read(cx).snapshot(cx);
214                    let newest_cursor = editor.selections.newest::<Point>(cx).head();
215                    if newest_cursor.column > 0 {
216                        let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
217                        let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
218                        if char_behind_cursor == Some('@') {
219                            self.inline_context_picker_menu_handle.show(cx);
220                        }
221                    }
222                });
223            }
224            _ => {}
225        }
226    }
227
228    fn handle_inline_context_picker_event(
229        &mut self,
230        _inline_context_picker: View<ContextPicker>,
231        _event: &DismissEvent,
232        cx: &mut ViewContext<Self>,
233    ) {
234        let editor_focus_handle = self.editor.focus_handle(cx);
235        cx.focus(&editor_focus_handle);
236    }
237
238    fn handle_context_strip_event(
239        &mut self,
240        _context_strip: View<ContextStrip>,
241        event: &ContextStripEvent,
242        cx: &mut ViewContext<Self>,
243    ) {
244        match event {
245            ContextStripEvent::PickerDismissed
246            | ContextStripEvent::BlurredEmpty
247            | ContextStripEvent::BlurredDown => {
248                let editor_focus_handle = self.editor.focus_handle(cx);
249                cx.focus(&editor_focus_handle);
250            }
251            ContextStripEvent::BlurredUp => {}
252        }
253    }
254
255    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
256        if self.context_picker_menu_handle.is_deployed()
257            || self.inline_context_picker_menu_handle.is_deployed()
258        {
259            cx.propagate();
260        } else {
261            cx.focus_view(&self.context_strip);
262        }
263    }
264}
265
266impl FocusableView for MessageEditor {
267    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
268        self.editor.focus_handle(cx)
269    }
270}
271
272impl Render for MessageEditor {
273    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
274        let font_size = TextSize::Default.rems(cx);
275        let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
276        let focus_handle = self.editor.focus_handle(cx);
277        let inline_context_picker = self.inline_context_picker.clone();
278        let bg_color = cx.theme().colors().editor_background;
279        let is_streaming_completion = self.thread.read(cx).is_streaming();
280        let button_width = px(64.);
281        let is_model_selected = self.is_model_selected(cx);
282        let is_editor_empty = self.is_editor_empty(cx);
283        let submit_label_color = if is_editor_empty {
284            Color::Muted
285        } else {
286            Color::Default
287        };
288
289        v_flex()
290            .key_context("MessageEditor")
291            .on_action(cx.listener(Self::chat))
292            .on_action(cx.listener(Self::toggle_model_selector))
293            .on_action(cx.listener(Self::toggle_context_picker))
294            .on_action(cx.listener(Self::remove_all_context))
295            .on_action(cx.listener(Self::move_up))
296            .on_action(cx.listener(Self::toggle_chat_mode))
297            .size_full()
298            .gap_2()
299            .p_2()
300            .bg(bg_color)
301            .child(self.context_strip.clone())
302            .child(
303                v_flex()
304                    .gap_4()
305                    .child({
306                        let settings = ThemeSettings::get_global(cx);
307                        let text_style = TextStyle {
308                            color: cx.theme().colors().text,
309                            font_family: settings.ui_font.family.clone(),
310                            font_features: settings.ui_font.features.clone(),
311                            font_size: font_size.into(),
312                            font_weight: settings.ui_font.weight,
313                            line_height: line_height.into(),
314                            ..Default::default()
315                        };
316
317                        EditorElement::new(
318                            &self.editor,
319                            EditorStyle {
320                                background: bg_color,
321                                local_player: cx.theme().players().local(),
322                                text: text_style,
323                                ..Default::default()
324                            },
325                        )
326                    })
327                    .child(
328                        PopoverMenu::new("inline-context-picker")
329                            .menu(move |cx| {
330                                inline_context_picker.update(cx, |this, cx| {
331                                    this.init(cx);
332                                });
333
334                                Some(inline_context_picker.clone())
335                            })
336                            .attach(gpui::Corner::TopLeft)
337                            .anchor(gpui::Corner::BottomLeft)
338                            .offset(gpui::Point {
339                                x: px(0.0),
340                                y: px(-ThemeSettings::clamp_font_size(
341                                    ThemeSettings::get_global(cx).ui_font_size,
342                                )
343                                .0 * 2.0)
344                                    - px(4.0),
345                            })
346                            .with_handle(self.inline_context_picker_menu_handle.clone()),
347                    )
348                    .child(
349                        h_flex()
350                            .justify_between()
351                            .child(
352                                Switch::new("use-tools", self.use_tools.into())
353                                    .label("Tools")
354                                    .on_click(cx.listener(|this, selection, _cx| {
355                                        this.use_tools = match selection {
356                                            ToggleState::Selected => true,
357                                            ToggleState::Unselected
358                                            | ToggleState::Indeterminate => false,
359                                        };
360                                    }))
361                                    .key_binding(KeyBinding::for_action_in(
362                                        &ChatMode,
363                                        &focus_handle,
364                                        cx,
365                                    )),
366                            )
367                            .child(h_flex().gap_1().child(self.model_selector.clone()).child(
368                                if is_streaming_completion {
369                                    ButtonLike::new("cancel-generation")
370                                        .width(button_width.into())
371                                        .style(ButtonStyle::Tinted(TintColor::Accent))
372                                        .child(
373                                            h_flex()
374                                                .w_full()
375                                                .justify_between()
376                                                .child(
377                                                    Label::new("Cancel")
378                                                        .size(LabelSize::Small)
379                                                        .with_animation(
380                                                            "pulsating-label",
381                                                            Animation::new(Duration::from_secs(2))
382                                                                .repeat()
383                                                                .with_easing(pulsating_between(
384                                                                    0.4, 0.8,
385                                                                )),
386                                                            |label, delta| label.alpha(delta),
387                                                        ),
388                                                )
389                                                .children(
390                                                    KeyBinding::for_action_in(
391                                                        &editor::actions::Cancel,
392                                                        &focus_handle,
393                                                        cx,
394                                                    )
395                                                    .map(|binding| binding.into_any_element()),
396                                                ),
397                                        )
398                                        .on_click(move |_event, cx| {
399                                            focus_handle
400                                                .dispatch_action(&editor::actions::Cancel, cx);
401                                        })
402                                } else {
403                                    ButtonLike::new("submit-message")
404                                        .width(button_width.into())
405                                        .style(ButtonStyle::Filled)
406                                        .disabled(is_editor_empty || !is_model_selected)
407                                        .child(
408                                            h_flex()
409                                                .w_full()
410                                                .justify_between()
411                                                .child(
412                                                    Label::new("Submit")
413                                                        .size(LabelSize::Small)
414                                                        .color(submit_label_color),
415                                                )
416                                                .children(
417                                                    KeyBinding::for_action_in(
418                                                        &Chat,
419                                                        &focus_handle,
420                                                        cx,
421                                                    )
422                                                    .map(|binding| binding.into_any_element()),
423                                                ),
424                                        )
425                                        .on_click(move |_event, cx| {
426                                            focus_handle.dispatch_action(&Chat, cx);
427                                        })
428                                        .when(is_editor_empty, |button| {
429                                            button.tooltip(|cx| {
430                                                Tooltip::text("Type a message to submit", cx)
431                                            })
432                                        })
433                                        .when(!is_model_selected, |button| {
434                                            button.tooltip(|cx| {
435                                                Tooltip::text("Select a model to continue", cx)
436                                            })
437                                        })
438                                },
439                            )),
440                    ),
441            )
442    }
443}