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