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
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
110 .new(|cx| AssistantModelSelector::new(fs, editor.focus_handle(cx), window, cx)),
111 use_tools: false,
112 _subscriptions: subscriptions,
113 }
114 }
115
116 fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
117 self.use_tools = !self.use_tools;
118 cx.notify();
119 }
120
121 fn toggle_context_picker(
122 &mut self,
123 _: &ToggleContextPicker,
124 window: &mut Window,
125 cx: &mut Context<Self>,
126 ) {
127 self.context_picker_menu_handle.toggle(window, cx);
128 }
129
130 pub fn remove_all_context(
131 &mut self,
132 _: &RemoveAllContext,
133 _window: &mut Window,
134 cx: &mut Context<Self>,
135 ) {
136 self.context_store.update(cx, |store, _cx| store.clear());
137 cx.notify();
138 }
139
140 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
141 self.send_to_model(RequestKind::Chat, window, cx);
142 }
143
144 fn is_editor_empty(&self, cx: &App) -> bool {
145 self.editor.read(cx).text(cx).is_empty()
146 }
147
148 fn is_model_selected(&self, cx: &App) -> bool {
149 LanguageModelRegistry::read_global(cx)
150 .active_model()
151 .is_some()
152 }
153
154 fn send_to_model(
155 &mut self,
156 request_kind: RequestKind,
157 window: &mut Window,
158 cx: &mut Context<Self>,
159 ) {
160 let provider = LanguageModelRegistry::read_global(cx).active_provider();
161 if provider
162 .as_ref()
163 .map_or(false, |provider| provider.must_accept_terms(cx))
164 {
165 cx.notify();
166 return;
167 }
168
169 let model_registry = LanguageModelRegistry::read_global(cx);
170 let Some(model) = model_registry.active_model() else {
171 return;
172 };
173
174 let user_message = self.editor.update(cx, |editor, cx| {
175 let text = editor.text(cx);
176 editor.clear(window, cx);
177 text
178 });
179
180 let refresh_task = refresh_context_store_text(self.context_store.clone(), cx);
181
182 let thread = self.thread.clone();
183 let context_store = self.context_store.clone();
184 let use_tools = self.use_tools;
185 cx.spawn(move |_, mut cx| async move {
186 refresh_task.await;
187 thread
188 .update(&mut cx, |thread, cx| {
189 let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
190 thread.insert_user_message(user_message, context, cx);
191 thread.send_to_model(model, request_kind, use_tools, cx);
192 })
193 .ok();
194 })
195 .detach();
196 }
197
198 fn handle_editor_event(
199 &mut self,
200 editor: &Entity<Editor>,
201 event: &EditorEvent,
202 window: &mut Window,
203 cx: &mut Context<Self>,
204 ) {
205 match event {
206 EditorEvent::SelectionsChanged { .. } => {
207 editor.update(cx, |editor, cx| {
208 let snapshot = editor.buffer().read(cx).snapshot(cx);
209 let newest_cursor = editor.selections.newest::<Point>(cx).head();
210 if newest_cursor.column > 0 {
211 let behind_cursor = snapshot.clip_point(
212 Point::new(newest_cursor.row, newest_cursor.column - 1),
213 Bias::Left,
214 );
215 let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
216 if char_behind_cursor == Some('@') {
217 self.inline_context_picker_menu_handle.show(window, cx);
218 }
219 }
220 });
221 }
222 _ => {}
223 }
224 }
225
226 fn handle_inline_context_picker_event(
227 &mut self,
228 _inline_context_picker: &Entity<ContextPicker>,
229 _event: &DismissEvent,
230 window: &mut Window,
231 cx: &mut Context<Self>,
232 ) {
233 let editor_focus_handle = self.editor.focus_handle(cx);
234 window.focus(&editor_focus_handle);
235 }
236
237 fn handle_context_strip_event(
238 &mut self,
239 _context_strip: &Entity<ContextStrip>,
240 event: &ContextStripEvent,
241 window: &mut Window,
242 cx: &mut Context<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 window.focus(&editor_focus_handle);
250 }
251 ContextStripEvent::BlurredUp => {}
252 }
253 }
254
255 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<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 self.context_strip.focus_handle(cx).focus(window);
262 }
263 }
264}
265
266impl Focusable for MessageEditor {
267 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
268 self.editor.focus_handle(cx)
269 }
270}
271
272impl Render for MessageEditor {
273 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
274 let font_size = TextSize::Default.rems(cx);
275 let line_height = font_size.to_pixels(window.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 is_model_selected = self.is_model_selected(cx);
281 let is_editor_empty = self.is_editor_empty(cx);
282 let submit_label_color = if is_editor_empty {
283 Color::Muted
284 } else {
285 Color::Default
286 };
287
288 let vim_mode_enabled = VimModeSetting::get_global(cx).0;
289 let platform = PlatformStyle::platform();
290 let linux = platform == PlatformStyle::Linux;
291 let windows = platform == PlatformStyle::Windows;
292 let button_width = if linux || windows || vim_mode_enabled {
293 px(92.)
294 } else {
295 px(64.)
296 };
297
298 v_flex()
299 .key_context("MessageEditor")
300 .on_action(cx.listener(Self::chat))
301 .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
302 this.model_selector
303 .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
304 }))
305 .on_action(cx.listener(Self::toggle_context_picker))
306 .on_action(cx.listener(Self::remove_all_context))
307 .on_action(cx.listener(Self::move_up))
308 .on_action(cx.listener(Self::toggle_chat_mode))
309 .size_full()
310 .gap_2()
311 .p_2()
312 .bg(bg_color)
313 .child(self.context_strip.clone())
314 .child(
315 v_flex()
316 .gap_5()
317 .child({
318 let settings = ThemeSettings::get_global(cx);
319 let text_style = TextStyle {
320 color: cx.theme().colors().text,
321 font_family: settings.ui_font.family.clone(),
322 font_features: settings.ui_font.features.clone(),
323 font_size: font_size.into(),
324 font_weight: settings.ui_font.weight,
325 line_height: line_height.into(),
326 ..Default::default()
327 };
328
329 EditorElement::new(
330 &self.editor,
331 EditorStyle {
332 background: bg_color,
333 local_player: cx.theme().players().local(),
334 text: text_style,
335 ..Default::default()
336 },
337 )
338 })
339 .child(
340 PopoverMenu::new("inline-context-picker")
341 .menu(move |window, cx| {
342 inline_context_picker.update(cx, |this, cx| {
343 this.init(window, cx);
344 });
345
346 Some(inline_context_picker.clone())
347 })
348 .attach(gpui::Corner::TopLeft)
349 .anchor(gpui::Corner::BottomLeft)
350 .offset(gpui::Point {
351 x: px(0.0),
352 y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
353 })
354 .with_handle(self.inline_context_picker_menu_handle.clone()),
355 )
356 .child(
357 h_flex()
358 .justify_between()
359 .child(
360 Switch::new("use-tools", self.use_tools.into())
361 .label("Tools")
362 .on_click(cx.listener(|this, selection, _window, _cx| {
363 this.use_tools = match selection {
364 ToggleState::Selected => true,
365 ToggleState::Unselected
366 | ToggleState::Indeterminate => false,
367 };
368 }))
369 .key_binding(KeyBinding::for_action_in(
370 &ChatMode,
371 &focus_handle,
372 window,
373 cx,
374 )),
375 )
376 .child(h_flex().gap_1().child(self.model_selector.clone()).child(
377 if is_streaming_completion {
378 ButtonLike::new("cancel-generation")
379 .width(button_width.into())
380 .style(ButtonStyle::Tinted(TintColor::Accent))
381 .child(
382 h_flex()
383 .w_full()
384 .justify_between()
385 .child(
386 Label::new("Cancel")
387 .size(LabelSize::Small)
388 .with_animation(
389 "pulsating-label",
390 Animation::new(Duration::from_secs(2))
391 .repeat()
392 .with_easing(pulsating_between(
393 0.4, 0.8,
394 )),
395 |label, delta| label.alpha(delta),
396 ),
397 )
398 .children(
399 KeyBinding::for_action_in(
400 &editor::actions::Cancel,
401 &focus_handle,
402 window,
403 cx,
404 )
405 .map(|binding| binding.into_any_element()),
406 ),
407 )
408 .on_click(move |_event, window, cx| {
409 focus_handle.dispatch_action(
410 &editor::actions::Cancel,
411 window,
412 cx,
413 );
414 })
415 } else {
416 ButtonLike::new("submit-message")
417 .width(button_width.into())
418 .style(ButtonStyle::Filled)
419 .disabled(is_editor_empty || !is_model_selected)
420 .child(
421 h_flex()
422 .w_full()
423 .justify_between()
424 .child(
425 Label::new("Submit")
426 .size(LabelSize::Small)
427 .color(submit_label_color),
428 )
429 .children(
430 KeyBinding::for_action_in(
431 &Chat,
432 &focus_handle,
433 window,
434 cx,
435 )
436 .map(|binding| binding.into_any_element()),
437 ),
438 )
439 .on_click(move |_event, window, cx| {
440 focus_handle.dispatch_action(&Chat, window, cx);
441 })
442 .when(is_editor_empty, |button| {
443 button
444 .tooltip(Tooltip::text("Type a message to submit"))
445 })
446 .when(!is_model_selected, |button| {
447 button.tooltip(Tooltip::text(
448 "Select a model to continue",
449 ))
450 })
451 },
452 )),
453 ),
454 )
455 }
456}