message_editor.rs

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