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