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