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