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