1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 prompt_library::open_prompt_library,
4 search::*,
5 slash_command::{
6 default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
7 SlashCommandRegistry,
8 },
9 ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
10 InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
11 MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role, SavedConversation,
12 SavedConversationMetadata, SavedMessage, Split, ToggleFocus, ToggleHistory,
13 ToggleModelSelector,
14};
15use anyhow::{anyhow, Result};
16use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
17use client::telemetry::Telemetry;
18use collections::{BTreeSet, HashMap, HashSet};
19use editor::actions::ShowCompletions;
20use editor::{
21 actions::{FoldAt, MoveToEndOfLine, Newline, UnfoldAt},
22 display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint},
23 scroll::{Autoscroll, AutoscrollStrategy},
24 Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint,
25};
26use editor::{display_map::FlapId, FoldPlaceholder};
27use file_icons::FileIcons;
28use fs::Fs;
29use futures::future::Shared;
30use futures::{FutureExt, StreamExt};
31use gpui::{
32 div, point, rems, uniform_list, Action, AnyElement, AnyView, AppContext, AsyncAppContext,
33 AsyncWindowContext, ClipboardItem, Context, Empty, EventEmitter, FocusHandle, FocusableView,
34 InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
35 SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle,
36 UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
37};
38use language::{
39 language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
40 LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
41};
42use multi_buffer::MultiBufferRow;
43use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
44use search::{buffer_search::DivRegistrar, BufferSearchBar};
45use settings::Settings;
46use std::{
47 cmp::{self, Ordering},
48 fmt::Write,
49 iter,
50 ops::Range,
51 path::PathBuf,
52 sync::Arc,
53 time::{Duration, Instant},
54};
55use telemetry_events::AssistantKind;
56use ui::{
57 popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
58 PopoverMenuHandle, Tab, TabBar, Tooltip,
59};
60use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
61use uuid::Uuid;
62use workspace::NewFile;
63use workspace::{
64 dock::{DockPosition, Panel, PanelEvent},
65 searchable::Direction,
66 Save, ToggleZoom, Toolbar, Workspace,
67};
68
69pub fn init(cx: &mut AppContext) {
70 cx.observe_new_views(
71 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
72 workspace
73 .register_action(|workspace, _: &ToggleFocus, cx| {
74 let settings = AssistantSettings::get_global(cx);
75 if !settings.enabled {
76 return;
77 }
78
79 workspace.toggle_panel_focus::<AssistantPanel>(cx);
80 })
81 .register_action(AssistantPanel::inline_assist)
82 .register_action(AssistantPanel::cancel_last_inline_assist)
83 // .register_action(ConversationEditor::insert_active_prompt)
84 .register_action(ConversationEditor::quote_selection);
85 },
86 )
87 .detach();
88}
89
90pub struct AssistantPanel {
91 workspace: WeakView<Workspace>,
92 width: Option<Pixels>,
93 height: Option<Pixels>,
94 active_conversation_editor: Option<ActiveConversationEditor>,
95 show_saved_conversations: bool,
96 saved_conversations: Vec<SavedConversationMetadata>,
97 saved_conversations_scroll_handle: UniformListScrollHandle,
98 zoomed: bool,
99 focus_handle: FocusHandle,
100 toolbar: View<Toolbar>,
101 languages: Arc<LanguageRegistry>,
102 slash_commands: Arc<SlashCommandRegistry>,
103 fs: Arc<dyn Fs>,
104 telemetry: Arc<Telemetry>,
105 _subscriptions: Vec<Subscription>,
106 _watch_saved_conversations: Task<Result<()>>,
107 authentication_prompt: Option<AnyView>,
108 model_menu_handle: PopoverMenuHandle<ContextMenu>,
109}
110
111struct ActiveConversationEditor {
112 editor: View<ConversationEditor>,
113 _subscriptions: Vec<Subscription>,
114}
115
116impl AssistantPanel {
117 pub fn load(
118 workspace: WeakView<Workspace>,
119 cx: AsyncWindowContext,
120 ) -> Task<Result<View<Self>>> {
121 cx.spawn(|mut cx| async move {
122 let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
123 let saved_conversations = SavedConversationMetadata::list(fs.clone())
124 .await
125 .log_err()
126 .unwrap_or_default();
127
128 // TODO: deserialize state.
129 let workspace_handle = workspace.clone();
130 workspace.update(&mut cx, |workspace, cx| {
131 cx.new_view::<Self>(|cx| {
132 const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
133 let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
134 let (mut events, _) = fs
135 .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
136 .await;
137 while events.next().await.is_some() {
138 let saved_conversations = SavedConversationMetadata::list(fs.clone())
139 .await
140 .log_err()
141 .unwrap_or_default();
142 this.update(&mut cx, |this, cx| {
143 this.saved_conversations = saved_conversations;
144 cx.notify();
145 })
146 .ok();
147 }
148
149 anyhow::Ok(())
150 });
151
152 let toolbar = cx.new_view(|cx| {
153 let mut toolbar = Toolbar::new();
154 toolbar.set_can_navigate(false, cx);
155 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx);
156 toolbar
157 });
158
159 let focus_handle = cx.focus_handle();
160 let subscriptions = vec![
161 cx.on_focus_in(&focus_handle, Self::focus_in),
162 cx.on_focus_out(&focus_handle, Self::focus_out),
163 cx.observe_global::<CompletionProvider>({
164 let mut prev_settings_version =
165 CompletionProvider::global(cx).settings_version();
166 move |this, cx| {
167 this.completion_provider_changed(prev_settings_version, cx);
168 prev_settings_version =
169 CompletionProvider::global(cx).settings_version();
170 }
171 }),
172 ];
173
174 cx.observe_global::<FileIcons>(|_, cx| {
175 cx.notify();
176 })
177 .detach();
178
179 Self {
180 workspace: workspace_handle,
181 active_conversation_editor: None,
182 show_saved_conversations: false,
183 saved_conversations,
184 saved_conversations_scroll_handle: Default::default(),
185 zoomed: false,
186 focus_handle,
187 toolbar,
188 languages: workspace.app_state().languages.clone(),
189 slash_commands: SlashCommandRegistry::global(cx),
190 fs: workspace.app_state().fs.clone(),
191 telemetry: workspace.client().telemetry().clone(),
192 width: None,
193 height: None,
194 _subscriptions: subscriptions,
195 _watch_saved_conversations,
196 authentication_prompt: None,
197 model_menu_handle: PopoverMenuHandle::default(),
198 }
199 })
200 })
201 })
202 }
203
204 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
205 self.toolbar
206 .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
207 cx.notify();
208 if self.focus_handle.is_focused(cx) {
209 if let Some(editor) = self.active_conversation_editor() {
210 cx.focus_view(editor);
211 }
212 }
213 }
214
215 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
216 self.toolbar
217 .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
218 cx.notify();
219 }
220
221 fn completion_provider_changed(
222 &mut self,
223 prev_settings_version: usize,
224 cx: &mut ViewContext<Self>,
225 ) {
226 if self.is_authenticated(cx) {
227 self.authentication_prompt = None;
228
229 if let Some(editor) = self.active_conversation_editor() {
230 editor.update(cx, |active_conversation, cx| {
231 active_conversation
232 .conversation
233 .update(cx, |conversation, cx| {
234 conversation.completion_provider_changed(cx)
235 })
236 })
237 }
238
239 if self.active_conversation_editor().is_none() {
240 self.new_conversation(cx);
241 }
242 cx.notify();
243 } else if self.authentication_prompt.is_none()
244 || prev_settings_version != CompletionProvider::global(cx).settings_version()
245 {
246 self.authentication_prompt =
247 Some(cx.update_global::<CompletionProvider, _>(|provider, cx| {
248 provider.authentication_prompt(cx)
249 }));
250 cx.notify();
251 }
252 }
253
254 pub fn inline_assist(
255 workspace: &mut Workspace,
256 _: &InlineAssist,
257 cx: &mut ViewContext<Workspace>,
258 ) {
259 let settings = AssistantSettings::get_global(cx);
260 if !settings.enabled {
261 return;
262 }
263
264 let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
265 return;
266 };
267
268 let conversation_editor =
269 assistant
270 .read(cx)
271 .active_conversation_editor()
272 .and_then(|editor| {
273 let editor = &editor.read(cx).editor;
274 if editor.read(cx).is_focused(cx) {
275 Some(editor.clone())
276 } else {
277 None
278 }
279 });
280
281 let include_conversation;
282 let active_editor;
283 if let Some(conversation_editor) = conversation_editor {
284 active_editor = conversation_editor;
285 include_conversation = false;
286 } else if let Some(workspace_editor) = workspace
287 .active_item(cx)
288 .and_then(|item| item.act_as::<Editor>(cx))
289 {
290 active_editor = workspace_editor;
291 include_conversation = true;
292 } else {
293 return;
294 };
295
296 if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
297 InlineAssistant::update_global(cx, |assistant, cx| {
298 assistant.assist(
299 &active_editor,
300 Some(cx.view().downgrade()),
301 include_conversation,
302 cx,
303 )
304 })
305 } else {
306 let assistant = assistant.downgrade();
307 cx.spawn(|workspace, mut cx| async move {
308 assistant
309 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
310 .await?;
311 if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
312 cx.update(|cx| {
313 InlineAssistant::update_global(cx, |assistant, cx| {
314 assistant.assist(
315 &active_editor,
316 Some(workspace),
317 include_conversation,
318 cx,
319 )
320 })
321 })?
322 } else {
323 workspace.update(&mut cx, |workspace, cx| {
324 workspace.focus_panel::<AssistantPanel>(cx)
325 })?;
326 }
327
328 anyhow::Ok(())
329 })
330 .detach_and_log_err(cx)
331 }
332 }
333
334 fn cancel_last_inline_assist(
335 _workspace: &mut Workspace,
336 _: &editor::actions::Cancel,
337 cx: &mut ViewContext<Workspace>,
338 ) {
339 let canceled = InlineAssistant::update_global(cx, |assistant, cx| {
340 assistant.cancel_last_inline_assist(cx)
341 });
342 if !canceled {
343 cx.propagate();
344 }
345 }
346
347 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ConversationEditor>> {
348 let workspace = self.workspace.upgrade()?;
349
350 let editor = cx.new_view(|cx| {
351 ConversationEditor::new(
352 self.languages.clone(),
353 self.slash_commands.clone(),
354 self.fs.clone(),
355 workspace,
356 cx,
357 )
358 });
359
360 self.show_conversation(editor.clone(), cx);
361 Some(editor)
362 }
363
364 fn show_conversation(
365 &mut self,
366 conversation_editor: View<ConversationEditor>,
367 cx: &mut ViewContext<Self>,
368 ) {
369 let mut subscriptions = Vec::new();
370 subscriptions
371 .push(cx.subscribe(&conversation_editor, Self::handle_conversation_editor_event));
372
373 let conversation = conversation_editor.read(cx).conversation.clone();
374 subscriptions.push(cx.observe(&conversation, |_, _, cx| cx.notify()));
375
376 let editor = conversation_editor.read(cx).editor.clone();
377 self.toolbar.update(cx, |toolbar, cx| {
378 toolbar.set_active_item(Some(&editor), cx);
379 });
380 if self.focus_handle.contains_focused(cx) {
381 cx.focus_view(&editor);
382 }
383 self.active_conversation_editor = Some(ActiveConversationEditor {
384 editor: conversation_editor,
385 _subscriptions: subscriptions,
386 });
387 self.show_saved_conversations = false;
388
389 cx.notify();
390 }
391
392 fn handle_conversation_editor_event(
393 &mut self,
394 _: View<ConversationEditor>,
395 event: &ConversationEditorEvent,
396 cx: &mut ViewContext<Self>,
397 ) {
398 match event {
399 ConversationEditorEvent::TabContentChanged => cx.notify(),
400 }
401 }
402
403 fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
404 if self.zoomed {
405 cx.emit(PanelEvent::ZoomOut)
406 } else {
407 cx.emit(PanelEvent::ZoomIn)
408 }
409 }
410
411 fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext<Self>) {
412 self.show_saved_conversations = !self.show_saved_conversations;
413 cx.notify();
414 }
415
416 fn show_history(&mut self, cx: &mut ViewContext<Self>) {
417 if !self.show_saved_conversations {
418 self.show_saved_conversations = true;
419 cx.notify();
420 }
421 }
422
423 fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
424 let mut propagate = true;
425 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
426 search_bar.update(cx, |search_bar, cx| {
427 if search_bar.show(cx) {
428 search_bar.search_suggested(cx);
429 if action.focus {
430 let focus_handle = search_bar.focus_handle(cx);
431 search_bar.select_query(cx);
432 cx.focus(&focus_handle);
433 }
434 propagate = false
435 }
436 });
437 }
438 if propagate {
439 cx.propagate();
440 }
441 }
442
443 fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
444 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
445 if !search_bar.read(cx).is_dismissed() {
446 search_bar.update(cx, |search_bar, cx| {
447 search_bar.dismiss(&Default::default(), cx)
448 });
449 return;
450 }
451 }
452 cx.propagate();
453 }
454
455 fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
456 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
457 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
458 }
459 }
460
461 fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
462 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
463 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
464 }
465 }
466
467 fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
468 CompletionProvider::global(cx)
469 .reset_credentials(cx)
470 .detach_and_log_err(cx);
471 }
472
473 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
474 self.model_menu_handle.toggle(cx);
475 }
476
477 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
478 if let Some(conversation_editor) = self.active_conversation_editor() {
479 conversation_editor.update(cx, |conversation_editor, cx| {
480 conversation_editor.insert_command(name, cx)
481 });
482 }
483 }
484
485 fn active_conversation_editor(&self) -> Option<&View<ConversationEditor>> {
486 Some(&self.active_conversation_editor.as_ref()?.editor)
487 }
488
489 pub fn active_conversation(&self, cx: &AppContext) -> Option<Model<Conversation>> {
490 Some(
491 self.active_conversation_editor()?
492 .read(cx)
493 .conversation
494 .clone(),
495 )
496 }
497
498 fn render_popover_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
499 let assistant = cx.view().clone();
500 let zoomed = self.zoomed;
501 popover_menu("assistant-popover")
502 .trigger(IconButton::new("trigger", IconName::Menu))
503 .menu(move |cx| {
504 let assistant = assistant.clone();
505 ContextMenu::build(cx, |menu, _cx| {
506 menu.entry(
507 if zoomed { "Zoom Out" } else { "Zoom In" },
508 Some(Box::new(ToggleZoom)),
509 {
510 let assistant = assistant.clone();
511 move |cx| {
512 assistant.focus_handle(cx).dispatch_action(&ToggleZoom, cx);
513 }
514 },
515 )
516 .entry("New Context", Some(Box::new(NewFile)), {
517 let assistant = assistant.clone();
518 move |cx| {
519 assistant.focus_handle(cx).dispatch_action(&NewFile, cx);
520 }
521 })
522 .entry("History", Some(Box::new(ToggleHistory)), {
523 let assistant = assistant.clone();
524 move |cx| assistant.update(cx, |assistant, cx| assistant.show_history(cx))
525 })
526 })
527 .into()
528 })
529 }
530
531 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
532 let commands = self.slash_commands.clone();
533 let assistant_panel = cx.view().downgrade();
534 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
535 Some(
536 workspace
537 .read(cx)
538 .active_item_as::<Editor>(cx)?
539 .focus_handle(cx),
540 )
541 });
542
543 popover_menu("inject-context-menu")
544 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
545 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
546 }))
547 .menu(move |cx| {
548 ContextMenu::build(cx, |mut menu, _cx| {
549 for command_name in commands.featured_command_names() {
550 if let Some(command) = commands.command(&command_name) {
551 let menu_text = SharedString::from(Arc::from(command.menu_text()));
552 menu = menu.custom_entry(
553 {
554 let command_name = command_name.clone();
555 move |_cx| {
556 h_flex()
557 .w_full()
558 .justify_between()
559 .child(Label::new(menu_text.clone()))
560 .child(
561 div().ml_4().child(
562 Label::new(format!("/{command_name}"))
563 .color(Color::Muted),
564 ),
565 )
566 .into_any()
567 }
568 },
569 {
570 let assistant_panel = assistant_panel.clone();
571 move |cx| {
572 assistant_panel
573 .update(cx, |assistant_panel, cx| {
574 assistant_panel.insert_command(&command_name, cx)
575 })
576 .ok();
577 }
578 },
579 )
580 }
581 }
582
583 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
584 menu = menu
585 .context(active_editor_focus_handle)
586 .action("Quote Selection", Box::new(QuoteSelection));
587 }
588
589 menu
590 })
591 .into()
592 })
593 }
594
595 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
596 self.active_conversation_editor
597 .as_ref()
598 .map(|conversation| {
599 let focus_handle = conversation.editor.focus_handle(cx);
600 ButtonLike::new("send_button")
601 .style(ButtonStyle::Filled)
602 .layer(ElevationIndex::ModalSurface)
603 .children(
604 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
605 .map(|binding| binding.into_any_element()),
606 )
607 .child(Label::new("Send"))
608 .on_click(cx.listener(|this, _event, cx| {
609 if let Some(active_editor) = this.active_conversation_editor() {
610 active_editor.update(cx, |editor, cx| editor.assist(&Assist, cx));
611 }
612 }))
613 })
614 }
615
616 fn render_saved_conversation(
617 &mut self,
618 index: usize,
619 cx: &mut ViewContext<Self>,
620 ) -> impl IntoElement {
621 let conversation = &self.saved_conversations[index];
622 let path = conversation.path.clone();
623
624 ButtonLike::new(index)
625 .on_click(cx.listener(move |this, _, cx| {
626 this.open_conversation(path.clone(), cx)
627 .detach_and_log_err(cx)
628 }))
629 .full_width()
630 .child(
631 div()
632 .flex()
633 .w_full()
634 .gap_2()
635 .child(
636 Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
637 .color(Color::Muted)
638 .size(LabelSize::Small),
639 )
640 .child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
641 )
642 }
643
644 fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
645 cx.focus(&self.focus_handle);
646
647 let fs = self.fs.clone();
648 let workspace = self.workspace.clone();
649 let slash_commands = self.slash_commands.clone();
650 let languages = self.languages.clone();
651 let telemetry = self.telemetry.clone();
652
653 let lsp_adapter_delegate = workspace
654 .update(cx, |workspace, cx| {
655 make_lsp_adapter_delegate(workspace.project(), cx).log_err()
656 })
657 .log_err()
658 .flatten();
659
660 cx.spawn(|this, mut cx| async move {
661 let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
662 let conversation = Conversation::deserialize(
663 saved_conversation,
664 path.clone(),
665 languages,
666 slash_commands,
667 Some(telemetry),
668 &mut cx,
669 )
670 .await?;
671
672 this.update(&mut cx, |this, cx| {
673 let workspace = workspace
674 .upgrade()
675 .ok_or_else(|| anyhow!("workspace dropped"))?;
676 let editor = cx.new_view(|cx| {
677 ConversationEditor::for_conversation(
678 conversation,
679 fs,
680 workspace,
681 lsp_adapter_delegate,
682 cx,
683 )
684 });
685 this.show_conversation(editor, cx);
686 anyhow::Ok(())
687 })??;
688 Ok(())
689 })
690 }
691
692 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
693 CompletionProvider::global(cx).is_authenticated()
694 }
695
696 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
697 cx.update_global::<CompletionProvider, _>(|provider, cx| provider.authenticate(cx))
698 }
699
700 fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
701 let header = TabBar::new("assistant_header")
702 .start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
703 .children(self.active_conversation_editor().map(|editor| {
704 h_flex()
705 .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
706 .flex_1()
707 .px_2()
708 .child(Label::new(editor.read(cx).title(cx)).into_element())
709 }))
710 .end_child(
711 h_flex()
712 .gap_2()
713 .when_some(self.active_conversation_editor(), |this, editor| {
714 let conversation = editor.read(cx).conversation.clone();
715 this.child(
716 h_flex()
717 .gap_1()
718 .child(ModelSelector::new(
719 self.model_menu_handle.clone(),
720 self.fs.clone(),
721 ))
722 .children(self.render_remaining_tokens(&conversation, cx)),
723 )
724 .child(
725 ui::Divider::vertical()
726 .inset()
727 .color(ui::DividerColor::Border),
728 )
729 })
730 .child(
731 h_flex()
732 .gap_1()
733 .child(self.render_inject_context_menu(cx))
734 .child(
735 IconButton::new("show-prompt-library", IconName::Library)
736 .icon_size(IconSize::Small)
737 .on_click({
738 let language_registry = self.languages.clone();
739 cx.listener(move |_this, _event, cx| {
740 open_prompt_library(language_registry.clone(), cx)
741 .detach_and_log_err(cx);
742 })
743 })
744 .tooltip(|cx| Tooltip::text("Prompt Library…", cx)),
745 ),
746 ),
747 );
748
749 let contents = if self.active_conversation_editor().is_some() {
750 let mut registrar = DivRegistrar::new(
751 |panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
752 cx,
753 );
754 BufferSearchBar::register(&mut registrar);
755 registrar.into_div()
756 } else {
757 div()
758 };
759
760 v_flex()
761 .key_context("AssistantPanel")
762 .size_full()
763 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
764 this.new_conversation(cx);
765 }))
766 .on_action(cx.listener(AssistantPanel::toggle_zoom))
767 .on_action(cx.listener(AssistantPanel::toggle_history))
768 .on_action(cx.listener(AssistantPanel::deploy))
769 .on_action(cx.listener(AssistantPanel::select_next_match))
770 .on_action(cx.listener(AssistantPanel::select_prev_match))
771 .on_action(cx.listener(AssistantPanel::handle_editor_cancel))
772 .on_action(cx.listener(AssistantPanel::reset_credentials))
773 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
774 .track_focus(&self.focus_handle)
775 .child(header)
776 .children(if self.toolbar.read(cx).hidden() {
777 None
778 } else {
779 Some(self.toolbar.clone())
780 })
781 .child(contents.flex_1().child(
782 if self.show_saved_conversations || self.active_conversation_editor().is_none() {
783 let view = cx.view().clone();
784 let scroll_handle = self.saved_conversations_scroll_handle.clone();
785 let conversation_count = self.saved_conversations.len();
786 uniform_list(
787 view,
788 "saved_conversations",
789 conversation_count,
790 |this, range, cx| {
791 range
792 .map(|ix| this.render_saved_conversation(ix, cx))
793 .collect()
794 },
795 )
796 .size_full()
797 .track_scroll(scroll_handle)
798 .into_any_element()
799 } else if let Some(editor) = self.active_conversation_editor() {
800 let editor = editor.clone();
801 div()
802 .size_full()
803 .child(editor.clone())
804 .child(
805 h_flex()
806 .w_full()
807 .absolute()
808 .bottom_0()
809 .p_4()
810 .justify_end()
811 .children(self.render_send_button(cx)),
812 )
813 .into_any_element()
814 } else {
815 div().into_any_element()
816 },
817 ))
818 }
819
820 fn render_remaining_tokens(
821 &self,
822 conversation: &Model<Conversation>,
823 cx: &mut ViewContext<Self>,
824 ) -> Option<impl IntoElement> {
825 let remaining_tokens = conversation.read(cx).remaining_tokens(cx)?;
826 let remaining_tokens_color = if remaining_tokens <= 0 {
827 Color::Error
828 } else if remaining_tokens <= 500 {
829 Color::Warning
830 } else {
831 Color::Muted
832 };
833 Some(
834 Label::new(remaining_tokens.to_string())
835 .size(LabelSize::Small)
836 .color(remaining_tokens_color),
837 )
838 }
839}
840
841impl Render for AssistantPanel {
842 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
843 if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
844 authentication_prompt.clone().into_any()
845 } else {
846 self.render_signed_in(cx).into_any_element()
847 }
848 }
849}
850
851impl Panel for AssistantPanel {
852 fn persistent_name() -> &'static str {
853 "AssistantPanel"
854 }
855
856 fn position(&self, cx: &WindowContext) -> DockPosition {
857 match AssistantSettings::get_global(cx).dock {
858 AssistantDockPosition::Left => DockPosition::Left,
859 AssistantDockPosition::Bottom => DockPosition::Bottom,
860 AssistantDockPosition::Right => DockPosition::Right,
861 }
862 }
863
864 fn position_is_valid(&self, _: DockPosition) -> bool {
865 true
866 }
867
868 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
869 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
870 let dock = match position {
871 DockPosition::Left => AssistantDockPosition::Left,
872 DockPosition::Bottom => AssistantDockPosition::Bottom,
873 DockPosition::Right => AssistantDockPosition::Right,
874 };
875 settings.set_dock(dock);
876 });
877 }
878
879 fn size(&self, cx: &WindowContext) -> Pixels {
880 let settings = AssistantSettings::get_global(cx);
881 match self.position(cx) {
882 DockPosition::Left | DockPosition::Right => {
883 self.width.unwrap_or(settings.default_width)
884 }
885 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
886 }
887 }
888
889 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
890 match self.position(cx) {
891 DockPosition::Left | DockPosition::Right => self.width = size,
892 DockPosition::Bottom => self.height = size,
893 }
894 cx.notify();
895 }
896
897 fn is_zoomed(&self, _: &WindowContext) -> bool {
898 self.zoomed
899 }
900
901 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
902 self.zoomed = zoomed;
903 cx.notify();
904 }
905
906 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
907 if active {
908 let load_credentials = self.authenticate(cx);
909 cx.spawn(|this, mut cx| async move {
910 load_credentials.await?;
911 this.update(&mut cx, |this, cx| {
912 if this.is_authenticated(cx) && this.active_conversation_editor().is_none() {
913 this.new_conversation(cx);
914 }
915 })
916 })
917 .detach_and_log_err(cx);
918 }
919 }
920
921 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
922 let settings = AssistantSettings::get_global(cx);
923 if !settings.enabled || !settings.button {
924 return None;
925 }
926
927 Some(IconName::ZedAssistant)
928 }
929
930 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
931 Some("Assistant Panel")
932 }
933
934 fn toggle_action(&self) -> Box<dyn Action> {
935 Box::new(ToggleFocus)
936 }
937}
938
939impl EventEmitter<PanelEvent> for AssistantPanel {}
940
941impl FocusableView for AssistantPanel {
942 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
943 self.focus_handle.clone()
944 }
945}
946
947#[derive(Clone)]
948enum ConversationEvent {
949 MessagesEdited,
950 SummaryChanged,
951 EditSuggestionsChanged,
952 StreamedCompletion,
953 PendingSlashCommandsUpdated {
954 removed: Vec<Range<language::Anchor>>,
955 updated: Vec<PendingSlashCommand>,
956 },
957 SlashCommandFinished {
958 output_range: Range<language::Anchor>,
959 sections: Vec<SlashCommandOutputSection<language::Anchor>>,
960 run_commands_in_output: bool,
961 },
962}
963
964#[derive(Default)]
965struct Summary {
966 text: String,
967 done: bool,
968}
969
970pub struct Conversation {
971 id: Option<String>,
972 buffer: Model<Buffer>,
973 edit_suggestions: Vec<EditSuggestion>,
974 pending_slash_commands: Vec<PendingSlashCommand>,
975 edits_since_last_slash_command_parse: language::Subscription,
976 message_anchors: Vec<MessageAnchor>,
977 messages_metadata: HashMap<MessageId, MessageMetadata>,
978 next_message_id: MessageId,
979 summary: Option<Summary>,
980 pending_summary: Task<Option<()>>,
981 completion_count: usize,
982 pending_completions: Vec<PendingCompletion>,
983 token_count: Option<usize>,
984 pending_token_count: Task<Option<()>>,
985 pending_edit_suggestion_parse: Option<Task<()>>,
986 pending_save: Task<Result<()>>,
987 path: Option<PathBuf>,
988 _subscriptions: Vec<Subscription>,
989 telemetry: Option<Arc<Telemetry>>,
990 slash_command_registry: Arc<SlashCommandRegistry>,
991 language_registry: Arc<LanguageRegistry>,
992}
993
994impl EventEmitter<ConversationEvent> for Conversation {}
995
996impl Conversation {
997 fn new(
998 language_registry: Arc<LanguageRegistry>,
999 slash_command_registry: Arc<SlashCommandRegistry>,
1000 telemetry: Option<Arc<Telemetry>>,
1001 cx: &mut ModelContext<Self>,
1002 ) -> Self {
1003 let buffer = cx.new_model(|cx| {
1004 let mut buffer = Buffer::local("", cx);
1005 buffer.set_language_registry(language_registry.clone());
1006 buffer
1007 });
1008 let edits_since_last_slash_command_parse =
1009 buffer.update(cx, |buffer, _| buffer.subscribe());
1010 let mut this = Self {
1011 id: Some(Uuid::new_v4().to_string()),
1012 message_anchors: Default::default(),
1013 messages_metadata: Default::default(),
1014 next_message_id: Default::default(),
1015 edit_suggestions: Vec::new(),
1016 pending_slash_commands: Vec::new(),
1017 edits_since_last_slash_command_parse,
1018 summary: None,
1019 pending_summary: Task::ready(None),
1020 completion_count: Default::default(),
1021 pending_completions: Default::default(),
1022 token_count: None,
1023 pending_token_count: Task::ready(None),
1024 pending_edit_suggestion_parse: None,
1025 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1026 pending_save: Task::ready(Ok(())),
1027 path: None,
1028 buffer,
1029 telemetry,
1030 language_registry,
1031 slash_command_registry,
1032 };
1033
1034 let message = MessageAnchor {
1035 id: MessageId(post_inc(&mut this.next_message_id.0)),
1036 start: language::Anchor::MIN,
1037 };
1038 this.message_anchors.push(message.clone());
1039 this.messages_metadata.insert(
1040 message.id,
1041 MessageMetadata {
1042 role: Role::User,
1043 status: MessageStatus::Done,
1044 },
1045 );
1046
1047 this.set_language(cx);
1048 this.count_remaining_tokens(cx);
1049 this
1050 }
1051
1052 fn serialize(&self, cx: &AppContext) -> SavedConversation {
1053 SavedConversation {
1054 id: self.id.clone(),
1055 zed: "conversation".into(),
1056 version: SavedConversation::VERSION.into(),
1057 text: self.buffer.read(cx).text(),
1058 message_metadata: self.messages_metadata.clone(),
1059 messages: self
1060 .messages(cx)
1061 .map(|message| SavedMessage {
1062 id: message.id,
1063 start: message.offset_range.start,
1064 })
1065 .collect(),
1066 summary: self
1067 .summary
1068 .as_ref()
1069 .map(|summary| summary.text.clone())
1070 .unwrap_or_default(),
1071 }
1072 }
1073
1074 #[allow(clippy::too_many_arguments)]
1075 async fn deserialize(
1076 saved_conversation: SavedConversation,
1077 path: PathBuf,
1078 language_registry: Arc<LanguageRegistry>,
1079 slash_command_registry: Arc<SlashCommandRegistry>,
1080 telemetry: Option<Arc<Telemetry>>,
1081 cx: &mut AsyncAppContext,
1082 ) -> Result<Model<Self>> {
1083 let id = match saved_conversation.id {
1084 Some(id) => Some(id),
1085 None => Some(Uuid::new_v4().to_string()),
1086 };
1087
1088 let markdown = language_registry.language_for_name("Markdown");
1089 let mut message_anchors = Vec::new();
1090 let mut next_message_id = MessageId(0);
1091 let buffer = cx.new_model(|cx| {
1092 let mut buffer = Buffer::local(saved_conversation.text, cx);
1093 for message in saved_conversation.messages {
1094 message_anchors.push(MessageAnchor {
1095 id: message.id,
1096 start: buffer.anchor_before(message.start),
1097 });
1098 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1099 }
1100 buffer.set_language_registry(language_registry.clone());
1101 cx.spawn(|buffer, mut cx| async move {
1102 let markdown = markdown.await?;
1103 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1104 buffer.set_language(Some(markdown), cx)
1105 })?;
1106 anyhow::Ok(())
1107 })
1108 .detach_and_log_err(cx);
1109 buffer
1110 })?;
1111
1112 cx.new_model(move |cx| {
1113 let edits_since_last_slash_command_parse =
1114 buffer.update(cx, |buffer, _| buffer.subscribe());
1115 let mut this = Self {
1116 id,
1117 message_anchors,
1118 messages_metadata: saved_conversation.message_metadata,
1119 next_message_id,
1120 edit_suggestions: Vec::new(),
1121 pending_slash_commands: Vec::new(),
1122 edits_since_last_slash_command_parse,
1123 summary: Some(Summary {
1124 text: saved_conversation.summary,
1125 done: true,
1126 }),
1127 pending_summary: Task::ready(None),
1128 completion_count: Default::default(),
1129 pending_completions: Default::default(),
1130 token_count: None,
1131 pending_edit_suggestion_parse: None,
1132 pending_token_count: Task::ready(None),
1133 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1134 pending_save: Task::ready(Ok(())),
1135 path: Some(path),
1136 buffer,
1137 telemetry,
1138 language_registry,
1139 slash_command_registry,
1140 };
1141 this.set_language(cx);
1142 this.reparse_edit_suggestions(cx);
1143 this.count_remaining_tokens(cx);
1144 this
1145 })
1146 }
1147
1148 fn set_language(&mut self, cx: &mut ModelContext<Self>) {
1149 let markdown = self.language_registry.language_for_name("Markdown");
1150 cx.spawn(|this, mut cx| async move {
1151 let markdown = markdown.await?;
1152 this.update(&mut cx, |this, cx| {
1153 this.buffer
1154 .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx));
1155 })
1156 })
1157 .detach_and_log_err(cx);
1158 }
1159
1160 fn handle_buffer_event(
1161 &mut self,
1162 _: Model<Buffer>,
1163 event: &language::Event,
1164 cx: &mut ModelContext<Self>,
1165 ) {
1166 if *event == language::Event::Edited {
1167 self.count_remaining_tokens(cx);
1168 self.reparse_edit_suggestions(cx);
1169 self.reparse_slash_commands(cx);
1170 cx.emit(ConversationEvent::MessagesEdited);
1171 }
1172 }
1173
1174 pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1175 let request = self.to_completion_request(cx);
1176 self.pending_token_count = cx.spawn(|this, mut cx| {
1177 async move {
1178 cx.background_executor()
1179 .timer(Duration::from_millis(200))
1180 .await;
1181
1182 let token_count = cx
1183 .update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
1184 .await?;
1185
1186 this.update(&mut cx, |this, cx| {
1187 this.token_count = Some(token_count);
1188 cx.notify()
1189 })?;
1190 anyhow::Ok(())
1191 }
1192 .log_err()
1193 });
1194 }
1195
1196 fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
1197 let buffer = self.buffer.read(cx);
1198 let mut row_ranges = self
1199 .edits_since_last_slash_command_parse
1200 .consume()
1201 .into_iter()
1202 .map(|edit| {
1203 let start_row = buffer.offset_to_point(edit.new.start).row;
1204 let end_row = buffer.offset_to_point(edit.new.end).row + 1;
1205 start_row..end_row
1206 })
1207 .peekable();
1208
1209 let mut removed = Vec::new();
1210 let mut updated = Vec::new();
1211 while let Some(mut row_range) = row_ranges.next() {
1212 while let Some(next_row_range) = row_ranges.peek() {
1213 if row_range.end >= next_row_range.start {
1214 row_range.end = next_row_range.end;
1215 row_ranges.next();
1216 } else {
1217 break;
1218 }
1219 }
1220
1221 let start = buffer.anchor_before(Point::new(row_range.start, 0));
1222 let end = buffer.anchor_after(Point::new(
1223 row_range.end - 1,
1224 buffer.line_len(row_range.end - 1),
1225 ));
1226
1227 let old_range = self.pending_command_indices_for_range(start..end, cx);
1228
1229 let mut new_commands = Vec::new();
1230 let mut lines = buffer.text_for_range(start..end).lines();
1231 let mut offset = lines.offset();
1232 while let Some(line) = lines.next() {
1233 if let Some(command_line) = SlashCommandLine::parse(line) {
1234 let name = &line[command_line.name.clone()];
1235 let argument = command_line.argument.as_ref().and_then(|argument| {
1236 (!argument.is_empty()).then_some(&line[argument.clone()])
1237 });
1238 if let Some(command) = self.slash_command_registry.command(name) {
1239 if !command.requires_argument() || argument.is_some() {
1240 let start_ix = offset + command_line.name.start - 1;
1241 let end_ix = offset
1242 + command_line
1243 .argument
1244 .map_or(command_line.name.end, |argument| argument.end);
1245 let source_range =
1246 buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
1247 let pending_command = PendingSlashCommand {
1248 name: name.to_string(),
1249 argument: argument.map(ToString::to_string),
1250 source_range,
1251 status: PendingSlashCommandStatus::Idle,
1252 };
1253 updated.push(pending_command.clone());
1254 new_commands.push(pending_command);
1255 }
1256 }
1257 }
1258
1259 offset = lines.offset();
1260 }
1261
1262 let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
1263 removed.extend(removed_commands.map(|command| command.source_range));
1264 }
1265
1266 if !updated.is_empty() || !removed.is_empty() {
1267 cx.emit(ConversationEvent::PendingSlashCommandsUpdated { removed, updated });
1268 }
1269 }
1270
1271 fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
1272 self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
1273 cx.background_executor()
1274 .timer(Duration::from_millis(200))
1275 .await;
1276
1277 this.update(&mut cx, |this, cx| {
1278 this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx);
1279 })
1280 .ok();
1281 }));
1282 }
1283
1284 fn reparse_edit_suggestions_in_range(
1285 &mut self,
1286 range: Range<usize>,
1287 cx: &mut ModelContext<Self>,
1288 ) {
1289 self.buffer.update(cx, |buffer, _| {
1290 let range_start = buffer.anchor_before(range.start);
1291 let range_end = buffer.anchor_after(range.end);
1292 let start_ix = self
1293 .edit_suggestions
1294 .binary_search_by(|probe| {
1295 probe
1296 .source_range
1297 .end
1298 .cmp(&range_start, buffer)
1299 .then(Ordering::Greater)
1300 })
1301 .unwrap_err();
1302 let end_ix = self
1303 .edit_suggestions
1304 .binary_search_by(|probe| {
1305 probe
1306 .source_range
1307 .start
1308 .cmp(&range_end, buffer)
1309 .then(Ordering::Less)
1310 })
1311 .unwrap_err();
1312
1313 let mut new_edit_suggestions = Vec::new();
1314 let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
1315 while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) {
1316 let start_anchor = buffer.anchor_after(suggestion.outer_range.start);
1317 let end_anchor = buffer.anchor_before(suggestion.outer_range.end);
1318 new_edit_suggestions.push(EditSuggestion {
1319 source_range: start_anchor..end_anchor,
1320 full_path: suggestion.path,
1321 });
1322 }
1323 self.edit_suggestions
1324 .splice(start_ix..end_ix, new_edit_suggestions);
1325 });
1326 cx.emit(ConversationEvent::EditSuggestionsChanged);
1327 cx.notify();
1328 }
1329
1330 fn pending_command_for_position(
1331 &mut self,
1332 position: language::Anchor,
1333 cx: &mut ModelContext<Self>,
1334 ) -> Option<&mut PendingSlashCommand> {
1335 let buffer = self.buffer.read(cx);
1336 match self
1337 .pending_slash_commands
1338 .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
1339 {
1340 Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
1341 Err(ix) => {
1342 let cmd = self.pending_slash_commands.get_mut(ix)?;
1343 if position.cmp(&cmd.source_range.start, buffer).is_ge()
1344 && position.cmp(&cmd.source_range.end, buffer).is_le()
1345 {
1346 Some(cmd)
1347 } else {
1348 None
1349 }
1350 }
1351 }
1352 }
1353
1354 fn pending_commands_for_range(
1355 &self,
1356 range: Range<language::Anchor>,
1357 cx: &AppContext,
1358 ) -> &[PendingSlashCommand] {
1359 let range = self.pending_command_indices_for_range(range, cx);
1360 &self.pending_slash_commands[range]
1361 }
1362
1363 fn pending_command_indices_for_range(
1364 &self,
1365 range: Range<language::Anchor>,
1366 cx: &AppContext,
1367 ) -> Range<usize> {
1368 let buffer = self.buffer.read(cx);
1369 let start_ix = match self
1370 .pending_slash_commands
1371 .binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
1372 {
1373 Ok(ix) | Err(ix) => ix,
1374 };
1375 let end_ix = match self
1376 .pending_slash_commands
1377 .binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
1378 {
1379 Ok(ix) => ix + 1,
1380 Err(ix) => ix,
1381 };
1382 start_ix..end_ix
1383 }
1384
1385 fn insert_command_output(
1386 &mut self,
1387 command_range: Range<language::Anchor>,
1388 output: Task<Result<SlashCommandOutput>>,
1389 insert_trailing_newline: bool,
1390 cx: &mut ModelContext<Self>,
1391 ) {
1392 self.reparse_slash_commands(cx);
1393
1394 let insert_output_task = cx.spawn(|this, mut cx| {
1395 let command_range = command_range.clone();
1396 async move {
1397 let output = output.await;
1398 this.update(&mut cx, |this, cx| match output {
1399 Ok(mut output) => {
1400 if insert_trailing_newline {
1401 output.text.push('\n');
1402 }
1403
1404 let event = this.buffer.update(cx, |buffer, cx| {
1405 let start = command_range.start.to_offset(buffer);
1406 let old_end = command_range.end.to_offset(buffer);
1407 let new_end = start + output.text.len();
1408 buffer.edit([(start..old_end, output.text)], None, cx);
1409
1410 let mut sections = output
1411 .sections
1412 .into_iter()
1413 .map(|section| SlashCommandOutputSection {
1414 range: buffer.anchor_after(start + section.range.start)
1415 ..buffer.anchor_before(start + section.range.end),
1416 render_placeholder: section.render_placeholder,
1417 })
1418 .collect::<Vec<_>>();
1419 sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
1420 ConversationEvent::SlashCommandFinished {
1421 output_range: buffer.anchor_after(start)
1422 ..buffer.anchor_before(new_end),
1423 sections,
1424 run_commands_in_output: output.run_commands_in_text,
1425 }
1426 });
1427 cx.emit(event);
1428 }
1429 Err(error) => {
1430 if let Some(pending_command) =
1431 this.pending_command_for_position(command_range.start, cx)
1432 {
1433 pending_command.status =
1434 PendingSlashCommandStatus::Error(error.to_string());
1435 cx.emit(ConversationEvent::PendingSlashCommandsUpdated {
1436 removed: vec![pending_command.source_range.clone()],
1437 updated: vec![pending_command.clone()],
1438 });
1439 }
1440 }
1441 })
1442 .ok();
1443 }
1444 });
1445
1446 if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) {
1447 pending_command.status = PendingSlashCommandStatus::Running {
1448 _task: insert_output_task.shared(),
1449 };
1450 cx.emit(ConversationEvent::PendingSlashCommandsUpdated {
1451 removed: vec![pending_command.source_range.clone()],
1452 updated: vec![pending_command.clone()],
1453 });
1454 }
1455 }
1456
1457 fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
1458 let model = CompletionProvider::global(cx).model();
1459 Some(model.max_token_count() as isize - self.token_count? as isize)
1460 }
1461
1462 fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
1463 self.count_remaining_tokens(cx);
1464 }
1465
1466 fn assist(
1467 &mut self,
1468 selected_messages: HashSet<MessageId>,
1469 cx: &mut ModelContext<Self>,
1470 ) -> Vec<MessageAnchor> {
1471 let mut user_messages = Vec::new();
1472
1473 let last_message_id = if let Some(last_message_id) =
1474 self.message_anchors.iter().rev().find_map(|message| {
1475 message
1476 .start
1477 .is_valid(self.buffer.read(cx))
1478 .then_some(message.id)
1479 }) {
1480 last_message_id
1481 } else {
1482 return Default::default();
1483 };
1484
1485 let mut should_assist = false;
1486 for selected_message_id in selected_messages {
1487 let selected_message_role =
1488 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1489 metadata.role
1490 } else {
1491 continue;
1492 };
1493
1494 if selected_message_role == Role::Assistant {
1495 if let Some(user_message) = self.insert_message_after(
1496 selected_message_id,
1497 Role::User,
1498 MessageStatus::Done,
1499 cx,
1500 ) {
1501 user_messages.push(user_message);
1502 }
1503 } else {
1504 should_assist = true;
1505 }
1506 }
1507
1508 if should_assist {
1509 if !CompletionProvider::global(cx).is_authenticated() {
1510 log::info!("completion provider has no credentials");
1511 return Default::default();
1512 }
1513
1514 let request = self.to_completion_request(cx);
1515 let stream = CompletionProvider::global(cx).complete(request);
1516 let assistant_message = self
1517 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1518 .unwrap();
1519
1520 // Queue up the user's next reply.
1521 let user_message = self
1522 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1523 .unwrap();
1524 user_messages.push(user_message);
1525
1526 let task = cx.spawn({
1527 |this, mut cx| async move {
1528 let assistant_message_id = assistant_message.id;
1529 let mut response_latency = None;
1530 let stream_completion = async {
1531 let request_start = Instant::now();
1532 let mut messages = stream.await?;
1533
1534 while let Some(message) = messages.next().await {
1535 if response_latency.is_none() {
1536 response_latency = Some(request_start.elapsed());
1537 }
1538 let text = message?;
1539
1540 this.update(&mut cx, |this, cx| {
1541 let message_ix = this
1542 .message_anchors
1543 .iter()
1544 .position(|message| message.id == assistant_message_id)?;
1545 let message_range = this.buffer.update(cx, |buffer, cx| {
1546 let message_start_offset =
1547 this.message_anchors[message_ix].start.to_offset(buffer);
1548 let message_old_end_offset = this.message_anchors
1549 [message_ix + 1..]
1550 .iter()
1551 .find(|message| message.start.is_valid(buffer))
1552 .map_or(buffer.len(), |message| {
1553 message.start.to_offset(buffer).saturating_sub(1)
1554 });
1555 let message_new_end_offset =
1556 message_old_end_offset + text.len();
1557 buffer.edit(
1558 [(message_old_end_offset..message_old_end_offset, text)],
1559 None,
1560 cx,
1561 );
1562 message_start_offset..message_new_end_offset
1563 });
1564 this.reparse_edit_suggestions_in_range(message_range, cx);
1565 cx.emit(ConversationEvent::StreamedCompletion);
1566
1567 Some(())
1568 })?;
1569 smol::future::yield_now().await;
1570 }
1571
1572 this.update(&mut cx, |this, cx| {
1573 this.pending_completions
1574 .retain(|completion| completion.id != this.completion_count);
1575 this.summarize(cx);
1576 })?;
1577
1578 anyhow::Ok(())
1579 };
1580
1581 let result = stream_completion.await;
1582
1583 this.update(&mut cx, |this, cx| {
1584 if let Some(metadata) =
1585 this.messages_metadata.get_mut(&assistant_message.id)
1586 {
1587 let error_message = result
1588 .err()
1589 .map(|error| error.to_string().trim().to_string());
1590 if let Some(error_message) = error_message.as_ref() {
1591 metadata.status =
1592 MessageStatus::Error(SharedString::from(error_message.clone()));
1593 } else {
1594 metadata.status = MessageStatus::Done;
1595 }
1596
1597 if let Some(telemetry) = this.telemetry.as_ref() {
1598 let model = CompletionProvider::global(cx).model();
1599 telemetry.report_assistant_event(
1600 this.id.clone(),
1601 AssistantKind::Panel,
1602 model.telemetry_id(),
1603 response_latency,
1604 error_message,
1605 );
1606 }
1607
1608 cx.emit(ConversationEvent::MessagesEdited);
1609 }
1610 })
1611 .ok();
1612 }
1613 });
1614
1615 self.pending_completions.push(PendingCompletion {
1616 id: post_inc(&mut self.completion_count),
1617 _task: task,
1618 });
1619 }
1620
1621 user_messages
1622 }
1623
1624 pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
1625 let messages = self
1626 .messages(cx)
1627 .filter(|message| matches!(message.status, MessageStatus::Done))
1628 .map(|message| message.to_request_message(self.buffer.read(cx)));
1629
1630 LanguageModelRequest {
1631 model: CompletionProvider::global(cx).model(),
1632 messages: messages.collect(),
1633 stop: vec![],
1634 temperature: 1.0,
1635 }
1636 }
1637
1638 fn cancel_last_assist(&mut self) -> bool {
1639 self.pending_completions.pop().is_some()
1640 }
1641
1642 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1643 for id in ids {
1644 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1645 metadata.role.cycle();
1646 cx.emit(ConversationEvent::MessagesEdited);
1647 cx.notify();
1648 }
1649 }
1650 }
1651
1652 fn insert_message_after(
1653 &mut self,
1654 message_id: MessageId,
1655 role: Role,
1656 status: MessageStatus,
1657 cx: &mut ModelContext<Self>,
1658 ) -> Option<MessageAnchor> {
1659 if let Some(prev_message_ix) = self
1660 .message_anchors
1661 .iter()
1662 .position(|message| message.id == message_id)
1663 {
1664 // Find the next valid message after the one we were given.
1665 let mut next_message_ix = prev_message_ix + 1;
1666 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1667 if next_message.start.is_valid(self.buffer.read(cx)) {
1668 break;
1669 }
1670 next_message_ix += 1;
1671 }
1672
1673 let start = self.buffer.update(cx, |buffer, cx| {
1674 let offset = self
1675 .message_anchors
1676 .get(next_message_ix)
1677 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1678 buffer.edit([(offset..offset, "\n")], None, cx);
1679 buffer.anchor_before(offset + 1)
1680 });
1681 let message = MessageAnchor {
1682 id: MessageId(post_inc(&mut self.next_message_id.0)),
1683 start,
1684 };
1685 self.message_anchors
1686 .insert(next_message_ix, message.clone());
1687 self.messages_metadata
1688 .insert(message.id, MessageMetadata { role, status });
1689 cx.emit(ConversationEvent::MessagesEdited);
1690 Some(message)
1691 } else {
1692 None
1693 }
1694 }
1695
1696 fn split_message(
1697 &mut self,
1698 range: Range<usize>,
1699 cx: &mut ModelContext<Self>,
1700 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1701 let start_message = self.message_for_offset(range.start, cx);
1702 let end_message = self.message_for_offset(range.end, cx);
1703 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1704 // Prevent splitting when range spans multiple messages.
1705 if start_message.id != end_message.id {
1706 return (None, None);
1707 }
1708
1709 let message = start_message;
1710 let role = message.role;
1711 let mut edited_buffer = false;
1712
1713 let mut suffix_start = None;
1714 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1715 {
1716 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1717 suffix_start = Some(range.end + 1);
1718 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1719 suffix_start = Some(range.end);
1720 }
1721 }
1722
1723 let suffix = if let Some(suffix_start) = suffix_start {
1724 MessageAnchor {
1725 id: MessageId(post_inc(&mut self.next_message_id.0)),
1726 start: self.buffer.read(cx).anchor_before(suffix_start),
1727 }
1728 } else {
1729 self.buffer.update(cx, |buffer, cx| {
1730 buffer.edit([(range.end..range.end, "\n")], None, cx);
1731 });
1732 edited_buffer = true;
1733 MessageAnchor {
1734 id: MessageId(post_inc(&mut self.next_message_id.0)),
1735 start: self.buffer.read(cx).anchor_before(range.end + 1),
1736 }
1737 };
1738
1739 self.message_anchors
1740 .insert(message.index_range.end + 1, suffix.clone());
1741 self.messages_metadata.insert(
1742 suffix.id,
1743 MessageMetadata {
1744 role,
1745 status: MessageStatus::Done,
1746 },
1747 );
1748
1749 let new_messages =
1750 if range.start == range.end || range.start == message.offset_range.start {
1751 (None, Some(suffix))
1752 } else {
1753 let mut prefix_end = None;
1754 if range.start > message.offset_range.start
1755 && range.end < message.offset_range.end - 1
1756 {
1757 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1758 prefix_end = Some(range.start + 1);
1759 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1760 == Some('\n')
1761 {
1762 prefix_end = Some(range.start);
1763 }
1764 }
1765
1766 let selection = if let Some(prefix_end) = prefix_end {
1767 cx.emit(ConversationEvent::MessagesEdited);
1768 MessageAnchor {
1769 id: MessageId(post_inc(&mut self.next_message_id.0)),
1770 start: self.buffer.read(cx).anchor_before(prefix_end),
1771 }
1772 } else {
1773 self.buffer.update(cx, |buffer, cx| {
1774 buffer.edit([(range.start..range.start, "\n")], None, cx)
1775 });
1776 edited_buffer = true;
1777 MessageAnchor {
1778 id: MessageId(post_inc(&mut self.next_message_id.0)),
1779 start: self.buffer.read(cx).anchor_before(range.end + 1),
1780 }
1781 };
1782
1783 self.message_anchors
1784 .insert(message.index_range.end + 1, selection.clone());
1785 self.messages_metadata.insert(
1786 selection.id,
1787 MessageMetadata {
1788 role,
1789 status: MessageStatus::Done,
1790 },
1791 );
1792 (Some(selection), Some(suffix))
1793 };
1794
1795 if !edited_buffer {
1796 cx.emit(ConversationEvent::MessagesEdited);
1797 }
1798 new_messages
1799 } else {
1800 (None, None)
1801 }
1802 }
1803
1804 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1805 if self.message_anchors.len() >= 2 && self.summary.is_none() {
1806 if !CompletionProvider::global(cx).is_authenticated() {
1807 return;
1808 }
1809
1810 let messages = self
1811 .messages(cx)
1812 .take(2)
1813 .map(|message| message.to_request_message(self.buffer.read(cx)))
1814 .chain(Some(LanguageModelRequestMessage {
1815 role: Role::User,
1816 content: "Summarize the conversation into a short title without punctuation"
1817 .into(),
1818 }));
1819 let request = LanguageModelRequest {
1820 model: CompletionProvider::global(cx).model(),
1821 messages: messages.collect(),
1822 stop: vec![],
1823 temperature: 1.0,
1824 };
1825
1826 let stream = CompletionProvider::global(cx).complete(request);
1827 self.pending_summary = cx.spawn(|this, mut cx| {
1828 async move {
1829 let mut messages = stream.await?;
1830
1831 while let Some(message) = messages.next().await {
1832 let text = message?;
1833 this.update(&mut cx, |this, cx| {
1834 this.summary
1835 .get_or_insert(Default::default())
1836 .text
1837 .push_str(&text);
1838 cx.emit(ConversationEvent::SummaryChanged);
1839 })?;
1840 }
1841
1842 this.update(&mut cx, |this, cx| {
1843 if let Some(summary) = this.summary.as_mut() {
1844 summary.done = true;
1845 cx.emit(ConversationEvent::SummaryChanged);
1846 }
1847 })?;
1848
1849 anyhow::Ok(())
1850 }
1851 .log_err()
1852 });
1853 }
1854 }
1855
1856 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
1857 self.messages_for_offsets([offset], cx).pop()
1858 }
1859
1860 fn messages_for_offsets(
1861 &self,
1862 offsets: impl IntoIterator<Item = usize>,
1863 cx: &AppContext,
1864 ) -> Vec<Message> {
1865 let mut result = Vec::new();
1866
1867 let mut messages = self.messages(cx).peekable();
1868 let mut offsets = offsets.into_iter().peekable();
1869 let mut current_message = messages.next();
1870 while let Some(offset) = offsets.next() {
1871 // Locate the message that contains the offset.
1872 while current_message.as_ref().map_or(false, |message| {
1873 !message.offset_range.contains(&offset) && messages.peek().is_some()
1874 }) {
1875 current_message = messages.next();
1876 }
1877 let Some(message) = current_message.as_ref() else {
1878 break;
1879 };
1880
1881 // Skip offsets that are in the same message.
1882 while offsets.peek().map_or(false, |offset| {
1883 message.offset_range.contains(offset) || messages.peek().is_none()
1884 }) {
1885 offsets.next();
1886 }
1887
1888 result.push(message.clone());
1889 }
1890 result
1891 }
1892
1893 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1894 let buffer = self.buffer.read(cx);
1895 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1896 iter::from_fn(move || {
1897 if let Some((start_ix, message_anchor)) = message_anchors.next() {
1898 let metadata = self.messages_metadata.get(&message_anchor.id)?;
1899 let message_start = message_anchor.start.to_offset(buffer);
1900 let mut message_end = None;
1901 let mut end_ix = start_ix;
1902 while let Some((_, next_message)) = message_anchors.peek() {
1903 if next_message.start.is_valid(buffer) {
1904 message_end = Some(next_message.start);
1905 break;
1906 } else {
1907 end_ix += 1;
1908 message_anchors.next();
1909 }
1910 }
1911 let message_end = message_end
1912 .unwrap_or(language::Anchor::MAX)
1913 .to_offset(buffer);
1914
1915 return Some(Message {
1916 index_range: start_ix..end_ix,
1917 offset_range: message_start..message_end,
1918 id: message_anchor.id,
1919 anchor: message_anchor.start,
1920 role: metadata.role,
1921 status: metadata.status.clone(),
1922 });
1923 }
1924 None
1925 })
1926 }
1927
1928 fn save(
1929 &mut self,
1930 debounce: Option<Duration>,
1931 fs: Arc<dyn Fs>,
1932 cx: &mut ModelContext<Conversation>,
1933 ) {
1934 self.pending_save = cx.spawn(|this, mut cx| async move {
1935 if let Some(debounce) = debounce {
1936 cx.background_executor().timer(debounce).await;
1937 }
1938
1939 let (old_path, summary) = this.read_with(&cx, |this, _| {
1940 let path = this.path.clone();
1941 let summary = if let Some(summary) = this.summary.as_ref() {
1942 if summary.done {
1943 Some(summary.text.clone())
1944 } else {
1945 None
1946 }
1947 } else {
1948 None
1949 };
1950 (path, summary)
1951 })?;
1952
1953 if let Some(summary) = summary {
1954 let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
1955 let path = if let Some(old_path) = old_path {
1956 old_path
1957 } else {
1958 let mut discriminant = 1;
1959 let mut new_path;
1960 loop {
1961 new_path = CONVERSATIONS_DIR.join(&format!(
1962 "{} - {}.zed.json",
1963 summary.trim(),
1964 discriminant
1965 ));
1966 if fs.is_file(&new_path).await {
1967 discriminant += 1;
1968 } else {
1969 break;
1970 }
1971 }
1972 new_path
1973 };
1974
1975 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
1976 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
1977 .await?;
1978 this.update(&mut cx, |this, _| this.path = Some(path))?;
1979 }
1980
1981 Ok(())
1982 });
1983 }
1984}
1985
1986#[derive(Debug)]
1987enum EditParsingState {
1988 None,
1989 InOldText {
1990 path: PathBuf,
1991 start_offset: usize,
1992 old_text_start_offset: usize,
1993 },
1994 InNewText {
1995 path: PathBuf,
1996 start_offset: usize,
1997 old_text_range: Range<usize>,
1998 new_text_start_offset: usize,
1999 },
2000}
2001
2002#[derive(Clone, Debug, PartialEq)]
2003struct EditSuggestion {
2004 source_range: Range<language::Anchor>,
2005 full_path: PathBuf,
2006}
2007
2008struct ParsedEditSuggestion {
2009 path: PathBuf,
2010 outer_range: Range<usize>,
2011 old_text_range: Range<usize>,
2012 new_text_range: Range<usize>,
2013}
2014
2015fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSuggestion> {
2016 let mut state = EditParsingState::None;
2017 loop {
2018 let offset = lines.offset();
2019 let message_line = lines.next()?;
2020 match state {
2021 EditParsingState::None => {
2022 if let Some(rest) = message_line.strip_prefix("```edit ") {
2023 let path = rest.trim();
2024 if !path.is_empty() {
2025 state = EditParsingState::InOldText {
2026 path: PathBuf::from(path),
2027 start_offset: offset,
2028 old_text_start_offset: lines.offset(),
2029 };
2030 }
2031 }
2032 }
2033 EditParsingState::InOldText {
2034 path,
2035 start_offset,
2036 old_text_start_offset,
2037 } => {
2038 if message_line == "---" {
2039 state = EditParsingState::InNewText {
2040 path,
2041 start_offset,
2042 old_text_range: old_text_start_offset..offset,
2043 new_text_start_offset: lines.offset(),
2044 };
2045 } else {
2046 state = EditParsingState::InOldText {
2047 path,
2048 start_offset,
2049 old_text_start_offset,
2050 };
2051 }
2052 }
2053 EditParsingState::InNewText {
2054 path,
2055 start_offset,
2056 old_text_range,
2057 new_text_start_offset,
2058 } => {
2059 if message_line == "```" {
2060 return Some(ParsedEditSuggestion {
2061 path,
2062 outer_range: start_offset..offset + "```".len(),
2063 old_text_range,
2064 new_text_range: new_text_start_offset..offset,
2065 });
2066 } else {
2067 state = EditParsingState::InNewText {
2068 path,
2069 start_offset,
2070 old_text_range,
2071 new_text_start_offset,
2072 };
2073 }
2074 }
2075 }
2076 }
2077}
2078
2079#[derive(Clone)]
2080struct PendingSlashCommand {
2081 name: String,
2082 argument: Option<String>,
2083 status: PendingSlashCommandStatus,
2084 source_range: Range<language::Anchor>,
2085}
2086
2087#[derive(Clone)]
2088enum PendingSlashCommandStatus {
2089 Idle,
2090 Running { _task: Shared<Task<()>> },
2091 Error(String),
2092}
2093
2094struct PendingCompletion {
2095 id: usize,
2096 _task: Task<()>,
2097}
2098
2099enum ConversationEditorEvent {
2100 TabContentChanged,
2101}
2102
2103#[derive(Copy, Clone, Debug, PartialEq)]
2104struct ScrollPosition {
2105 offset_before_cursor: gpui::Point<f32>,
2106 cursor: Anchor,
2107}
2108
2109pub struct ConversationEditor {
2110 conversation: Model<Conversation>,
2111 fs: Arc<dyn Fs>,
2112 workspace: WeakView<Workspace>,
2113 slash_command_registry: Arc<SlashCommandRegistry>,
2114 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2115 editor: View<Editor>,
2116 blocks: HashSet<BlockId>,
2117 scroll_position: Option<ScrollPosition>,
2118 pending_slash_command_flaps: HashMap<Range<language::Anchor>, FlapId>,
2119 _subscriptions: Vec<Subscription>,
2120}
2121
2122impl ConversationEditor {
2123 fn new(
2124 language_registry: Arc<LanguageRegistry>,
2125 slash_command_registry: Arc<SlashCommandRegistry>,
2126 fs: Arc<dyn Fs>,
2127 workspace: View<Workspace>,
2128 cx: &mut ViewContext<Self>,
2129 ) -> Self {
2130 let telemetry = workspace.read(cx).client().telemetry().clone();
2131 let project = workspace.read(cx).project().clone();
2132 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
2133
2134 let conversation = cx.new_model(|cx| {
2135 Conversation::new(
2136 language_registry,
2137 slash_command_registry,
2138 Some(telemetry),
2139 cx,
2140 )
2141 });
2142
2143 let mut this =
2144 Self::for_conversation(conversation, fs, workspace, lsp_adapter_delegate, cx);
2145 this.insert_default_prompt(cx);
2146 this
2147 }
2148
2149 fn for_conversation(
2150 conversation: Model<Conversation>,
2151 fs: Arc<dyn Fs>,
2152 workspace: View<Workspace>,
2153 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2154 cx: &mut ViewContext<Self>,
2155 ) -> Self {
2156 let slash_command_registry = conversation.read(cx).slash_command_registry.clone();
2157
2158 let completion_provider = SlashCommandCompletionProvider::new(
2159 slash_command_registry.clone(),
2160 Some(cx.view().downgrade()),
2161 Some(workspace.downgrade()),
2162 );
2163
2164 let editor = cx.new_view(|cx| {
2165 let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2166 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2167 editor.set_show_line_numbers(false, cx);
2168 editor.set_show_git_diff_gutter(false, cx);
2169 editor.set_show_code_actions(false, cx);
2170 editor.set_show_wrap_guides(false, cx);
2171 editor.set_show_indent_guides(false, cx);
2172 editor.set_completion_provider(Box::new(completion_provider));
2173 editor
2174 });
2175
2176 let _subscriptions = vec![
2177 cx.observe(&conversation, |_, _, cx| cx.notify()),
2178 cx.subscribe(&conversation, Self::handle_conversation_event),
2179 cx.subscribe(&editor, Self::handle_editor_event),
2180 ];
2181
2182 let mut this = Self {
2183 conversation,
2184 editor,
2185 slash_command_registry,
2186 lsp_adapter_delegate,
2187 blocks: Default::default(),
2188 scroll_position: None,
2189 fs,
2190 workspace: workspace.downgrade(),
2191 pending_slash_command_flaps: HashMap::default(),
2192 _subscriptions,
2193 };
2194 this.update_message_headers(cx);
2195 this
2196 }
2197
2198 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
2199 let command_name = DefaultSlashCommand.name();
2200 self.editor.update(cx, |editor, cx| {
2201 editor.insert(&format!("/{command_name}"), cx)
2202 });
2203 self.split(&Split, cx);
2204 let command = self.conversation.update(cx, |conversation, cx| {
2205 conversation
2206 .messages_metadata
2207 .get_mut(&MessageId::default())
2208 .unwrap()
2209 .role = Role::System;
2210 conversation.reparse_slash_commands(cx);
2211 conversation.pending_slash_commands[0].clone()
2212 });
2213
2214 self.run_command(
2215 command.source_range,
2216 &command.name,
2217 command.argument.as_deref(),
2218 false,
2219 self.workspace.clone(),
2220 cx,
2221 );
2222 }
2223
2224 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2225 let cursors = self.cursors(cx);
2226
2227 let user_messages = self.conversation.update(cx, |conversation, cx| {
2228 let selected_messages = conversation
2229 .messages_for_offsets(cursors, cx)
2230 .into_iter()
2231 .map(|message| message.id)
2232 .collect();
2233 conversation.assist(selected_messages, cx)
2234 });
2235 let new_selections = user_messages
2236 .iter()
2237 .map(|message| {
2238 let cursor = message
2239 .start
2240 .to_offset(self.conversation.read(cx).buffer.read(cx));
2241 cursor..cursor
2242 })
2243 .collect::<Vec<_>>();
2244 if !new_selections.is_empty() {
2245 self.editor.update(cx, |editor, cx| {
2246 editor.change_selections(
2247 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2248 cx,
2249 |selections| selections.select_ranges(new_selections),
2250 );
2251 });
2252 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2253 cx.defer(|this, _| this.scroll_position = None);
2254 }
2255 }
2256
2257 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2258 if !self
2259 .conversation
2260 .update(cx, |conversation, _| conversation.cancel_last_assist())
2261 {
2262 cx.propagate();
2263 }
2264 }
2265
2266 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2267 let cursors = self.cursors(cx);
2268 self.conversation.update(cx, |conversation, cx| {
2269 let messages = conversation
2270 .messages_for_offsets(cursors, cx)
2271 .into_iter()
2272 .map(|message| message.id)
2273 .collect();
2274 conversation.cycle_message_roles(messages, cx)
2275 });
2276 }
2277
2278 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2279 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2280 selections
2281 .into_iter()
2282 .map(|selection| selection.head())
2283 .collect()
2284 }
2285
2286 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2287 if let Some(command) = self.slash_command_registry.command(name) {
2288 self.editor.update(cx, |editor, cx| {
2289 editor.transact(cx, |editor, cx| {
2290 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2291 let snapshot = editor.buffer().read(cx).snapshot(cx);
2292 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2293 if newest_cursor.column > 0
2294 || snapshot
2295 .chars_at(newest_cursor)
2296 .next()
2297 .map_or(false, |ch| ch != '\n')
2298 {
2299 editor.move_to_end_of_line(
2300 &MoveToEndOfLine {
2301 stop_at_soft_wraps: false,
2302 },
2303 cx,
2304 );
2305 editor.newline(&Newline, cx);
2306 }
2307
2308 editor.insert(&format!("/{name}"), cx);
2309 if command.requires_argument() {
2310 editor.insert(" ", cx);
2311 editor.show_completions(&ShowCompletions, cx);
2312 }
2313 });
2314 });
2315 if !command.requires_argument() {
2316 self.confirm_command(&ConfirmCommand, cx);
2317 }
2318 }
2319 }
2320
2321 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2322 let selections = self.editor.read(cx).selections.disjoint_anchors();
2323 let mut commands_by_range = HashMap::default();
2324 let workspace = self.workspace.clone();
2325 self.conversation.update(cx, |conversation, cx| {
2326 conversation.reparse_slash_commands(cx);
2327 for selection in selections.iter() {
2328 if let Some(command) =
2329 conversation.pending_command_for_position(selection.head().text_anchor, cx)
2330 {
2331 commands_by_range
2332 .entry(command.source_range.clone())
2333 .or_insert_with(|| command.clone());
2334 }
2335 }
2336 });
2337
2338 if commands_by_range.is_empty() {
2339 cx.propagate();
2340 } else {
2341 for command in commands_by_range.into_values() {
2342 self.run_command(
2343 command.source_range,
2344 &command.name,
2345 command.argument.as_deref(),
2346 true,
2347 workspace.clone(),
2348 cx,
2349 );
2350 }
2351 cx.stop_propagation();
2352 }
2353 }
2354
2355 pub fn run_command(
2356 &mut self,
2357 command_range: Range<language::Anchor>,
2358 name: &str,
2359 argument: Option<&str>,
2360 insert_trailing_newline: bool,
2361 workspace: WeakView<Workspace>,
2362 cx: &mut ViewContext<Self>,
2363 ) {
2364 if let Some(command) = self.slash_command_registry.command(name) {
2365 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
2366 let argument = argument.map(ToString::to_string);
2367 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
2368 self.conversation.update(cx, |conversation, cx| {
2369 conversation.insert_command_output(
2370 command_range,
2371 output,
2372 insert_trailing_newline,
2373 cx,
2374 )
2375 });
2376 }
2377 }
2378 }
2379
2380 fn handle_conversation_event(
2381 &mut self,
2382 _: Model<Conversation>,
2383 event: &ConversationEvent,
2384 cx: &mut ViewContext<Self>,
2385 ) {
2386 let conversation_editor = cx.view().downgrade();
2387
2388 match event {
2389 ConversationEvent::MessagesEdited => {
2390 self.update_message_headers(cx);
2391 self.conversation.update(cx, |conversation, cx| {
2392 conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2393 });
2394 }
2395 ConversationEvent::EditSuggestionsChanged => {
2396 self.editor.update(cx, |editor, cx| {
2397 let buffer = editor.buffer().read(cx).snapshot(cx);
2398 let excerpt_id = *buffer.as_singleton().unwrap().0;
2399 let conversation = self.conversation.read(cx);
2400 let highlighted_rows = conversation
2401 .edit_suggestions
2402 .iter()
2403 .map(|suggestion| {
2404 let start = buffer
2405 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2406 .unwrap();
2407 let end = buffer
2408 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2409 .unwrap();
2410 start..=end
2411 })
2412 .collect::<Vec<_>>();
2413
2414 editor.clear_row_highlights::<EditSuggestion>();
2415 for range in highlighted_rows {
2416 editor.highlight_rows::<EditSuggestion>(
2417 range,
2418 Some(
2419 cx.theme()
2420 .colors()
2421 .editor_document_highlight_read_background,
2422 ),
2423 false,
2424 cx,
2425 );
2426 }
2427 });
2428 }
2429 ConversationEvent::SummaryChanged => {
2430 cx.emit(ConversationEditorEvent::TabContentChanged);
2431 self.conversation.update(cx, |conversation, cx| {
2432 conversation.save(None, self.fs.clone(), cx);
2433 });
2434 }
2435 ConversationEvent::StreamedCompletion => {
2436 self.editor.update(cx, |editor, cx| {
2437 if let Some(scroll_position) = self.scroll_position {
2438 let snapshot = editor.snapshot(cx);
2439 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2440 let scroll_top =
2441 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2442 editor.set_scroll_position(
2443 point(scroll_position.offset_before_cursor.x, scroll_top),
2444 cx,
2445 );
2446 }
2447 });
2448 }
2449 ConversationEvent::PendingSlashCommandsUpdated { removed, updated } => {
2450 self.editor.update(cx, |editor, cx| {
2451 let buffer = editor.buffer().read(cx).snapshot(cx);
2452 let excerpt_id = *buffer.as_singleton().unwrap().0;
2453
2454 editor.remove_flaps(
2455 removed
2456 .iter()
2457 .filter_map(|range| self.pending_slash_command_flaps.remove(range)),
2458 cx,
2459 );
2460
2461 let flap_ids = editor.insert_flaps(
2462 updated.iter().map(|command| {
2463 let workspace = self.workspace.clone();
2464 let confirm_command = Arc::new({
2465 let conversation_editor = conversation_editor.clone();
2466 let command = command.clone();
2467 move |cx: &mut WindowContext| {
2468 conversation_editor
2469 .update(cx, |conversation_editor, cx| {
2470 conversation_editor.run_command(
2471 command.source_range.clone(),
2472 &command.name,
2473 command.argument.as_deref(),
2474 false,
2475 workspace.clone(),
2476 cx,
2477 );
2478 })
2479 .ok();
2480 }
2481 });
2482 let placeholder = FoldPlaceholder {
2483 render: Arc::new(move |_, _, _| Empty.into_any()),
2484 constrain_width: false,
2485 merge_adjacent: false,
2486 };
2487 let render_toggle = {
2488 let confirm_command = confirm_command.clone();
2489 let command = command.clone();
2490 move |row, _, _, _cx: &mut WindowContext| {
2491 render_pending_slash_command_gutter_decoration(
2492 row,
2493 command.status.clone(),
2494 confirm_command.clone(),
2495 )
2496 }
2497 };
2498 let render_trailer =
2499 |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2500
2501 let start = buffer
2502 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2503 .unwrap();
2504 let end = buffer
2505 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2506 .unwrap();
2507 Flap::new(start..end, placeholder, render_toggle, render_trailer)
2508 }),
2509 cx,
2510 );
2511
2512 self.pending_slash_command_flaps.extend(
2513 updated
2514 .iter()
2515 .map(|command| command.source_range.clone())
2516 .zip(flap_ids),
2517 );
2518 })
2519 }
2520 ConversationEvent::SlashCommandFinished {
2521 output_range,
2522 sections,
2523 run_commands_in_output,
2524 } => {
2525 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2526
2527 if *run_commands_in_output {
2528 let commands = self.conversation.update(cx, |conversation, cx| {
2529 conversation.reparse_slash_commands(cx);
2530 conversation
2531 .pending_commands_for_range(output_range.clone(), cx)
2532 .to_vec()
2533 });
2534
2535 for command in commands {
2536 self.run_command(
2537 command.source_range,
2538 &command.name,
2539 command.argument.as_deref(),
2540 false,
2541 self.workspace.clone(),
2542 cx,
2543 );
2544 }
2545 }
2546 }
2547 }
2548 }
2549
2550 fn insert_slash_command_output_sections(
2551 &mut self,
2552 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2553 cx: &mut ViewContext<Self>,
2554 ) {
2555 self.editor.update(cx, |editor, cx| {
2556 let buffer = editor.buffer().read(cx).snapshot(cx);
2557 let excerpt_id = *buffer.as_singleton().unwrap().0;
2558 let mut buffer_rows_to_fold = BTreeSet::new();
2559 let mut flaps = Vec::new();
2560 for section in sections {
2561 let start = buffer
2562 .anchor_in_excerpt(excerpt_id, section.range.start)
2563 .unwrap();
2564 let end = buffer
2565 .anchor_in_excerpt(excerpt_id, section.range.end)
2566 .unwrap();
2567 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2568 buffer_rows_to_fold.insert(buffer_row);
2569 flaps.push(Flap::new(
2570 start..end,
2571 FoldPlaceholder {
2572 render: Arc::new({
2573 let editor = cx.view().downgrade();
2574 let render_placeholder = section.render_placeholder.clone();
2575 move |fold_id, fold_range, cx| {
2576 let editor = editor.clone();
2577 let unfold = Arc::new(move |cx: &mut WindowContext| {
2578 editor
2579 .update(cx, |editor, cx| {
2580 let buffer_start = fold_range
2581 .start
2582 .to_point(&editor.buffer().read(cx).read(cx));
2583 let buffer_row = MultiBufferRow(buffer_start.row);
2584 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2585 })
2586 .ok();
2587 });
2588 render_placeholder(fold_id.into(), unfold, cx)
2589 }
2590 }),
2591 constrain_width: false,
2592 merge_adjacent: false,
2593 },
2594 render_slash_command_output_toggle,
2595 |_, _, _| Empty.into_any_element(),
2596 ));
2597 }
2598
2599 editor.insert_flaps(flaps, cx);
2600
2601 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2602 editor.fold_at(&FoldAt { buffer_row }, cx);
2603 }
2604 });
2605 }
2606
2607 fn handle_editor_event(
2608 &mut self,
2609 _: View<Editor>,
2610 event: &EditorEvent,
2611 cx: &mut ViewContext<Self>,
2612 ) {
2613 match event {
2614 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2615 let cursor_scroll_position = self.cursor_scroll_position(cx);
2616 if *autoscroll {
2617 self.scroll_position = cursor_scroll_position;
2618 } else if self.scroll_position != cursor_scroll_position {
2619 self.scroll_position = None;
2620 }
2621 }
2622 EditorEvent::SelectionsChanged { .. } => {
2623 self.scroll_position = self.cursor_scroll_position(cx);
2624 }
2625 _ => {}
2626 }
2627 }
2628
2629 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2630 self.editor.update(cx, |editor, cx| {
2631 let snapshot = editor.snapshot(cx);
2632 let cursor = editor.selections.newest_anchor().head();
2633 let cursor_row = cursor
2634 .to_display_point(&snapshot.display_snapshot)
2635 .row()
2636 .as_f32();
2637 let scroll_position = editor
2638 .scroll_manager
2639 .anchor()
2640 .scroll_position(&snapshot.display_snapshot);
2641
2642 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2643 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2644 Some(ScrollPosition {
2645 cursor,
2646 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2647 })
2648 } else {
2649 None
2650 }
2651 })
2652 }
2653
2654 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2655 self.editor.update(cx, |editor, cx| {
2656 let buffer = editor.buffer().read(cx).snapshot(cx);
2657 let excerpt_id = *buffer.as_singleton().unwrap().0;
2658 let old_blocks = std::mem::take(&mut self.blocks);
2659 let new_blocks = self
2660 .conversation
2661 .read(cx)
2662 .messages(cx)
2663 .map(|message| BlockProperties {
2664 position: buffer
2665 .anchor_in_excerpt(excerpt_id, message.anchor)
2666 .unwrap(),
2667 height: 2,
2668 style: BlockStyle::Sticky,
2669 render: Box::new({
2670 let conversation = self.conversation.clone();
2671 move |cx| {
2672 let message_id = message.id;
2673 let sender = ButtonLike::new("role")
2674 .style(ButtonStyle::Filled)
2675 .child(match message.role {
2676 Role::User => Label::new("You").color(Color::Default),
2677 Role::Assistant => Label::new("Assistant").color(Color::Info),
2678 Role::System => Label::new("System").color(Color::Warning),
2679 })
2680 .tooltip(|cx| {
2681 Tooltip::with_meta(
2682 "Toggle message role",
2683 None,
2684 "Available roles: You (User), Assistant, System",
2685 cx,
2686 )
2687 })
2688 .on_click({
2689 let conversation = conversation.clone();
2690 move |_, cx| {
2691 conversation.update(cx, |conversation, cx| {
2692 conversation.cycle_message_roles(
2693 HashSet::from_iter(Some(message_id)),
2694 cx,
2695 )
2696 })
2697 }
2698 });
2699
2700 h_flex()
2701 .id(("message_header", message_id.0))
2702 .pl(cx.gutter_dimensions.full_width())
2703 .h_11()
2704 .w_full()
2705 .relative()
2706 .gap_1()
2707 .child(sender)
2708 .children(
2709 if let MessageStatus::Error(error) = message.status.clone() {
2710 Some(
2711 div()
2712 .id("error")
2713 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2714 .child(Icon::new(IconName::XCircle)),
2715 )
2716 } else {
2717 None
2718 },
2719 )
2720 .into_any_element()
2721 }
2722 }),
2723 disposition: BlockDisposition::Above,
2724 })
2725 .collect::<Vec<_>>();
2726
2727 editor.remove_blocks(old_blocks, None, cx);
2728 let ids = editor.insert_blocks(new_blocks, None, cx);
2729 self.blocks = HashSet::from_iter(ids);
2730 });
2731 }
2732
2733 fn quote_selection(
2734 workspace: &mut Workspace,
2735 _: &QuoteSelection,
2736 cx: &mut ViewContext<Workspace>,
2737 ) {
2738 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2739 return;
2740 };
2741 let Some(editor) = workspace
2742 .active_item(cx)
2743 .and_then(|item| item.act_as::<Editor>(cx))
2744 else {
2745 return;
2746 };
2747
2748 let editor = editor.read(cx);
2749 let range = editor.selections.newest::<usize>(cx).range();
2750 let buffer = editor.buffer().read(cx).snapshot(cx);
2751 let start_language = buffer.language_at(range.start);
2752 let end_language = buffer.language_at(range.end);
2753 let language_name = if start_language == end_language {
2754 start_language.map(|language| language.code_fence_block_name())
2755 } else {
2756 None
2757 };
2758 let language_name = language_name.as_deref().unwrap_or("");
2759
2760 let selected_text = buffer.text_for_range(range).collect::<String>();
2761 let text = if selected_text.is_empty() {
2762 None
2763 } else {
2764 Some(if language_name == "markdown" {
2765 selected_text
2766 .lines()
2767 .map(|line| format!("> {}", line))
2768 .collect::<Vec<_>>()
2769 .join("\n")
2770 } else {
2771 format!("```{language_name}\n{selected_text}\n```")
2772 })
2773 };
2774
2775 // Activate the panel
2776 if !panel.focus_handle(cx).contains_focused(cx) {
2777 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2778 }
2779
2780 if let Some(text) = text {
2781 panel.update(cx, |_, cx| {
2782 // Wait to create a new conversation until the workspace is no longer
2783 // being updated.
2784 cx.defer(move |panel, cx| {
2785 if let Some(conversation) = panel
2786 .active_conversation_editor()
2787 .cloned()
2788 .or_else(|| panel.new_conversation(cx))
2789 {
2790 conversation.update(cx, |conversation, cx| {
2791 conversation
2792 .editor
2793 .update(cx, |editor, cx| editor.insert(&text, cx))
2794 });
2795 };
2796 });
2797 });
2798 }
2799 }
2800
2801 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2802 let editor = self.editor.read(cx);
2803 let conversation = self.conversation.read(cx);
2804 if editor.selections.count() == 1 {
2805 let selection = editor.selections.newest::<usize>(cx);
2806 let mut copied_text = String::new();
2807 let mut spanned_messages = 0;
2808 for message in conversation.messages(cx) {
2809 if message.offset_range.start >= selection.range().end {
2810 break;
2811 } else if message.offset_range.end >= selection.range().start {
2812 let range = cmp::max(message.offset_range.start, selection.range().start)
2813 ..cmp::min(message.offset_range.end, selection.range().end);
2814 if !range.is_empty() {
2815 spanned_messages += 1;
2816 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2817 for chunk in conversation.buffer.read(cx).text_for_range(range) {
2818 copied_text.push_str(chunk);
2819 }
2820 copied_text.push('\n');
2821 }
2822 }
2823 }
2824
2825 if spanned_messages > 1 {
2826 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2827 return;
2828 }
2829 }
2830
2831 cx.propagate();
2832 }
2833
2834 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2835 self.conversation.update(cx, |conversation, cx| {
2836 let selections = self.editor.read(cx).selections.disjoint_anchors();
2837 for selection in selections.as_ref() {
2838 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2839 let range = selection
2840 .map(|endpoint| endpoint.to_offset(&buffer))
2841 .range();
2842 conversation.split_message(range, cx);
2843 }
2844 });
2845 }
2846
2847 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
2848 let Some(workspace) = self.workspace.upgrade() else {
2849 return;
2850 };
2851 let project = workspace.read(cx).project().clone();
2852
2853 struct Edit {
2854 old_text: String,
2855 new_text: String,
2856 }
2857
2858 let conversation = self.conversation.read(cx);
2859 let conversation_buffer = conversation.buffer.read(cx);
2860 let conversation_buffer_snapshot = conversation_buffer.snapshot();
2861
2862 let selections = self.editor.read(cx).selections.disjoint_anchors();
2863 let mut selections = selections.iter().peekable();
2864 let selected_suggestions = conversation
2865 .edit_suggestions
2866 .iter()
2867 .filter(|suggestion| {
2868 while let Some(selection) = selections.peek() {
2869 if selection
2870 .end
2871 .text_anchor
2872 .cmp(&suggestion.source_range.start, conversation_buffer)
2873 .is_lt()
2874 {
2875 selections.next();
2876 continue;
2877 }
2878 if selection
2879 .start
2880 .text_anchor
2881 .cmp(&suggestion.source_range.end, conversation_buffer)
2882 .is_gt()
2883 {
2884 break;
2885 }
2886 return true;
2887 }
2888 false
2889 })
2890 .cloned()
2891 .collect::<Vec<_>>();
2892
2893 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
2894 project.update(cx, |project, cx| {
2895 for suggestion in &selected_suggestions {
2896 opened_buffers
2897 .entry(suggestion.full_path.clone())
2898 .or_insert_with(|| {
2899 project.open_buffer_for_full_path(&suggestion.full_path, cx)
2900 });
2901 }
2902 });
2903
2904 cx.spawn(|this, mut cx| async move {
2905 let mut buffers_by_full_path = HashMap::default();
2906 for (full_path, buffer) in opened_buffers {
2907 if let Some(buffer) = buffer.await.log_err() {
2908 buffers_by_full_path.insert(full_path, buffer);
2909 }
2910 }
2911
2912 let mut suggestions_by_buffer = HashMap::default();
2913 cx.update(|cx| {
2914 for suggestion in selected_suggestions {
2915 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
2916 let (_, edits) = suggestions_by_buffer
2917 .entry(buffer.clone())
2918 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
2919
2920 let mut lines = conversation_buffer_snapshot
2921 .as_rope()
2922 .chunks_in_range(
2923 suggestion
2924 .source_range
2925 .to_offset(&conversation_buffer_snapshot),
2926 )
2927 .lines();
2928 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
2929 let old_text = conversation_buffer_snapshot
2930 .text_for_range(suggestion.old_text_range)
2931 .collect();
2932 let new_text = conversation_buffer_snapshot
2933 .text_for_range(suggestion.new_text_range)
2934 .collect();
2935 edits.push(Edit { old_text, new_text });
2936 }
2937 }
2938 }
2939 })?;
2940
2941 let edits_by_buffer = cx
2942 .background_executor()
2943 .spawn(async move {
2944 let mut result = HashMap::default();
2945 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
2946 let edits =
2947 result
2948 .entry(buffer)
2949 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
2950 for suggestion in suggestions {
2951 if let Some(range) =
2952 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
2953 {
2954 let edit_start = snapshot.anchor_after(range.start);
2955 let edit_end = snapshot.anchor_before(range.end);
2956 if let Err(ix) = edits.binary_search_by(|(range, _)| {
2957 range.start.cmp(&edit_start, &snapshot)
2958 }) {
2959 edits.insert(
2960 ix,
2961 (edit_start..edit_end, suggestion.new_text.clone()),
2962 );
2963 }
2964 } else {
2965 log::info!(
2966 "assistant edit did not match any text in buffer {:?}",
2967 &suggestion.old_text
2968 );
2969 }
2970 }
2971 }
2972 result
2973 })
2974 .await;
2975
2976 let mut project_transaction = ProjectTransaction::default();
2977 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
2978 for (buffer_handle, edits) in edits_by_buffer {
2979 buffer_handle.update(cx, |buffer, cx| {
2980 buffer.start_transaction();
2981 buffer.edit(
2982 edits,
2983 Some(AutoindentMode::Block {
2984 original_indent_columns: Vec::new(),
2985 }),
2986 cx,
2987 );
2988 buffer.end_transaction(cx);
2989 if let Some(transaction) = buffer.finalize_last_transaction() {
2990 project_transaction
2991 .0
2992 .insert(buffer_handle.clone(), transaction.clone());
2993 }
2994 });
2995 }
2996
2997 (
2998 this.editor.downgrade(),
2999 this.workspace.clone(),
3000 this.title(cx),
3001 )
3002 })?;
3003
3004 Editor::open_project_transaction(
3005 &editor,
3006 workspace,
3007 project_transaction,
3008 format!("Edits from {}", title),
3009 cx,
3010 )
3011 .await
3012 })
3013 .detach_and_log_err(cx);
3014 }
3015
3016 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3017 self.conversation.update(cx, |conversation, cx| {
3018 conversation.save(None, self.fs.clone(), cx)
3019 });
3020 }
3021
3022 fn title(&self, cx: &AppContext) -> String {
3023 self.conversation
3024 .read(cx)
3025 .summary
3026 .as_ref()
3027 .map(|summary| summary.text.clone())
3028 .unwrap_or_else(|| "New Context".into())
3029 }
3030}
3031
3032impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
3033
3034impl Render for ConversationEditor {
3035 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3036 div()
3037 .key_context("ConversationEditor")
3038 .capture_action(cx.listener(ConversationEditor::cancel_last_assist))
3039 .capture_action(cx.listener(ConversationEditor::save))
3040 .capture_action(cx.listener(ConversationEditor::copy))
3041 .capture_action(cx.listener(ConversationEditor::cycle_message_role))
3042 .capture_action(cx.listener(ConversationEditor::confirm_command))
3043 .on_action(cx.listener(ConversationEditor::assist))
3044 .on_action(cx.listener(ConversationEditor::split))
3045 .on_action(cx.listener(ConversationEditor::apply_edit))
3046 .size_full()
3047 .v_flex()
3048 .child(
3049 div()
3050 .flex_grow()
3051 .bg(cx.theme().colors().editor_background)
3052 .child(self.editor.clone()),
3053 )
3054 }
3055}
3056
3057impl FocusableView for ConversationEditor {
3058 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3059 self.editor.focus_handle(cx)
3060 }
3061}
3062
3063#[derive(Clone, Debug)]
3064struct MessageAnchor {
3065 id: MessageId,
3066 start: language::Anchor,
3067}
3068
3069#[derive(Clone, Debug)]
3070pub struct Message {
3071 offset_range: Range<usize>,
3072 index_range: Range<usize>,
3073 id: MessageId,
3074 anchor: language::Anchor,
3075 role: Role,
3076 status: MessageStatus,
3077}
3078
3079impl Message {
3080 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
3081 LanguageModelRequestMessage {
3082 role: self.role,
3083 content: buffer.text_for_range(self.offset_range.clone()).collect(),
3084 }
3085 }
3086}
3087
3088type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3089
3090fn render_slash_command_output_toggle(
3091 row: MultiBufferRow,
3092 is_folded: bool,
3093 fold: ToggleFold,
3094 _cx: &mut WindowContext,
3095) -> AnyElement {
3096 IconButton::new(
3097 ("slash-command-output-fold-indicator", row.0),
3098 ui::IconName::ChevronDown,
3099 )
3100 .on_click(move |_e, cx| fold(!is_folded, cx))
3101 .icon_color(ui::Color::Muted)
3102 .icon_size(ui::IconSize::Small)
3103 .selected(is_folded)
3104 .selected_icon(ui::IconName::ChevronRight)
3105 .size(ui::ButtonSize::None)
3106 .into_any_element()
3107}
3108
3109fn render_pending_slash_command_gutter_decoration(
3110 row: MultiBufferRow,
3111 status: PendingSlashCommandStatus,
3112 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3113) -> AnyElement {
3114 let mut icon = IconButton::new(
3115 ("slash-command-gutter-decoration", row.0),
3116 ui::IconName::TriangleRight,
3117 )
3118 .on_click(move |_e, cx| confirm_command(cx))
3119 .icon_size(ui::IconSize::Small)
3120 .size(ui::ButtonSize::None);
3121
3122 match status {
3123 PendingSlashCommandStatus::Idle => {
3124 icon = icon.icon_color(Color::Muted);
3125 }
3126 PendingSlashCommandStatus::Running { .. } => {
3127 icon = icon.selected(true);
3128 }
3129 PendingSlashCommandStatus::Error(error) => {
3130 icon = icon
3131 .icon_color(Color::Error)
3132 .tooltip(move |cx| Tooltip::text(format!("error: {error}"), cx));
3133 }
3134 }
3135
3136 icon.into_any_element()
3137}
3138
3139fn make_lsp_adapter_delegate(
3140 project: &Model<Project>,
3141 cx: &mut AppContext,
3142) -> Result<Arc<dyn LspAdapterDelegate>> {
3143 project.update(cx, |project, cx| {
3144 // TODO: Find the right worktree.
3145 let worktree = project
3146 .worktrees()
3147 .next()
3148 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3149 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3150 })
3151}
3152
3153#[cfg(test)]
3154mod tests {
3155 use super::*;
3156 use crate::{
3157 slash_command::{active_command, file_command},
3158 FakeCompletionProvider, MessageId,
3159 };
3160 use fs::FakeFs;
3161 use gpui::{AppContext, TestAppContext};
3162 use rope::Rope;
3163 use serde_json::json;
3164 use settings::SettingsStore;
3165 use std::{cell::RefCell, path::Path, rc::Rc};
3166 use unindent::Unindent;
3167 use util::test::marked_text_ranges;
3168
3169 #[gpui::test]
3170 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3171 let settings_store = SettingsStore::test(cx);
3172 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3173 cx.set_global(settings_store);
3174 init(cx);
3175 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3176
3177 let conversation =
3178 cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
3179 let buffer = conversation.read(cx).buffer.clone();
3180
3181 let message_1 = conversation.read(cx).message_anchors[0].clone();
3182 assert_eq!(
3183 messages(&conversation, cx),
3184 vec![(message_1.id, Role::User, 0..0)]
3185 );
3186
3187 let message_2 = conversation.update(cx, |conversation, cx| {
3188 conversation
3189 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3190 .unwrap()
3191 });
3192 assert_eq!(
3193 messages(&conversation, cx),
3194 vec![
3195 (message_1.id, Role::User, 0..1),
3196 (message_2.id, Role::Assistant, 1..1)
3197 ]
3198 );
3199
3200 buffer.update(cx, |buffer, cx| {
3201 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3202 });
3203 assert_eq!(
3204 messages(&conversation, cx),
3205 vec![
3206 (message_1.id, Role::User, 0..2),
3207 (message_2.id, Role::Assistant, 2..3)
3208 ]
3209 );
3210
3211 let message_3 = conversation.update(cx, |conversation, cx| {
3212 conversation
3213 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3214 .unwrap()
3215 });
3216 assert_eq!(
3217 messages(&conversation, cx),
3218 vec![
3219 (message_1.id, Role::User, 0..2),
3220 (message_2.id, Role::Assistant, 2..4),
3221 (message_3.id, Role::User, 4..4)
3222 ]
3223 );
3224
3225 let message_4 = conversation.update(cx, |conversation, cx| {
3226 conversation
3227 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3228 .unwrap()
3229 });
3230 assert_eq!(
3231 messages(&conversation, cx),
3232 vec![
3233 (message_1.id, Role::User, 0..2),
3234 (message_2.id, Role::Assistant, 2..4),
3235 (message_4.id, Role::User, 4..5),
3236 (message_3.id, Role::User, 5..5),
3237 ]
3238 );
3239
3240 buffer.update(cx, |buffer, cx| {
3241 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3242 });
3243 assert_eq!(
3244 messages(&conversation, cx),
3245 vec![
3246 (message_1.id, Role::User, 0..2),
3247 (message_2.id, Role::Assistant, 2..4),
3248 (message_4.id, Role::User, 4..6),
3249 (message_3.id, Role::User, 6..7),
3250 ]
3251 );
3252
3253 // Deleting across message boundaries merges the messages.
3254 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3255 assert_eq!(
3256 messages(&conversation, cx),
3257 vec![
3258 (message_1.id, Role::User, 0..3),
3259 (message_3.id, Role::User, 3..4),
3260 ]
3261 );
3262
3263 // Undoing the deletion should also undo the merge.
3264 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3265 assert_eq!(
3266 messages(&conversation, cx),
3267 vec![
3268 (message_1.id, Role::User, 0..2),
3269 (message_2.id, Role::Assistant, 2..4),
3270 (message_4.id, Role::User, 4..6),
3271 (message_3.id, Role::User, 6..7),
3272 ]
3273 );
3274
3275 // Redoing the deletion should also redo the merge.
3276 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3277 assert_eq!(
3278 messages(&conversation, cx),
3279 vec![
3280 (message_1.id, Role::User, 0..3),
3281 (message_3.id, Role::User, 3..4),
3282 ]
3283 );
3284
3285 // Ensure we can still insert after a merged message.
3286 let message_5 = conversation.update(cx, |conversation, cx| {
3287 conversation
3288 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3289 .unwrap()
3290 });
3291 assert_eq!(
3292 messages(&conversation, cx),
3293 vec![
3294 (message_1.id, Role::User, 0..3),
3295 (message_5.id, Role::System, 3..4),
3296 (message_3.id, Role::User, 4..5)
3297 ]
3298 );
3299 }
3300
3301 #[gpui::test]
3302 fn test_message_splitting(cx: &mut AppContext) {
3303 let settings_store = SettingsStore::test(cx);
3304 cx.set_global(settings_store);
3305 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3306 init(cx);
3307 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3308
3309 let conversation =
3310 cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
3311 let buffer = conversation.read(cx).buffer.clone();
3312
3313 let message_1 = conversation.read(cx).message_anchors[0].clone();
3314 assert_eq!(
3315 messages(&conversation, cx),
3316 vec![(message_1.id, Role::User, 0..0)]
3317 );
3318
3319 buffer.update(cx, |buffer, cx| {
3320 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3321 });
3322
3323 let (_, message_2) =
3324 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3325 let message_2 = message_2.unwrap();
3326
3327 // We recycle newlines in the middle of a split message
3328 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3329 assert_eq!(
3330 messages(&conversation, cx),
3331 vec![
3332 (message_1.id, Role::User, 0..4),
3333 (message_2.id, Role::User, 4..16),
3334 ]
3335 );
3336
3337 let (_, message_3) =
3338 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3339 let message_3 = message_3.unwrap();
3340
3341 // We don't recycle newlines at the end of a split message
3342 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3343 assert_eq!(
3344 messages(&conversation, cx),
3345 vec![
3346 (message_1.id, Role::User, 0..4),
3347 (message_3.id, Role::User, 4..5),
3348 (message_2.id, Role::User, 5..17),
3349 ]
3350 );
3351
3352 let (_, message_4) =
3353 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3354 let message_4 = message_4.unwrap();
3355 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3356 assert_eq!(
3357 messages(&conversation, cx),
3358 vec![
3359 (message_1.id, Role::User, 0..4),
3360 (message_3.id, Role::User, 4..5),
3361 (message_2.id, Role::User, 5..9),
3362 (message_4.id, Role::User, 9..17),
3363 ]
3364 );
3365
3366 let (_, message_5) =
3367 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3368 let message_5 = message_5.unwrap();
3369 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3370 assert_eq!(
3371 messages(&conversation, cx),
3372 vec![
3373 (message_1.id, Role::User, 0..4),
3374 (message_3.id, Role::User, 4..5),
3375 (message_2.id, Role::User, 5..9),
3376 (message_4.id, Role::User, 9..10),
3377 (message_5.id, Role::User, 10..18),
3378 ]
3379 );
3380
3381 let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
3382 conversation.split_message(14..16, cx)
3383 });
3384 let message_6 = message_6.unwrap();
3385 let message_7 = message_7.unwrap();
3386 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3387 assert_eq!(
3388 messages(&conversation, cx),
3389 vec![
3390 (message_1.id, Role::User, 0..4),
3391 (message_3.id, Role::User, 4..5),
3392 (message_2.id, Role::User, 5..9),
3393 (message_4.id, Role::User, 9..10),
3394 (message_5.id, Role::User, 10..14),
3395 (message_6.id, Role::User, 14..17),
3396 (message_7.id, Role::User, 17..19),
3397 ]
3398 );
3399 }
3400
3401 #[gpui::test]
3402 fn test_messages_for_offsets(cx: &mut AppContext) {
3403 let settings_store = SettingsStore::test(cx);
3404 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3405 cx.set_global(settings_store);
3406 init(cx);
3407 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3408 let conversation =
3409 cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
3410 let buffer = conversation.read(cx).buffer.clone();
3411
3412 let message_1 = conversation.read(cx).message_anchors[0].clone();
3413 assert_eq!(
3414 messages(&conversation, cx),
3415 vec![(message_1.id, Role::User, 0..0)]
3416 );
3417
3418 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3419 let message_2 = conversation
3420 .update(cx, |conversation, cx| {
3421 conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3422 })
3423 .unwrap();
3424 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3425
3426 let message_3 = conversation
3427 .update(cx, |conversation, cx| {
3428 conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3429 })
3430 .unwrap();
3431 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3432
3433 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3434 assert_eq!(
3435 messages(&conversation, cx),
3436 vec![
3437 (message_1.id, Role::User, 0..4),
3438 (message_2.id, Role::User, 4..8),
3439 (message_3.id, Role::User, 8..11)
3440 ]
3441 );
3442
3443 assert_eq!(
3444 message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
3445 [message_1.id, message_2.id, message_3.id]
3446 );
3447 assert_eq!(
3448 message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
3449 [message_1.id, message_3.id]
3450 );
3451
3452 let message_4 = conversation
3453 .update(cx, |conversation, cx| {
3454 conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3455 })
3456 .unwrap();
3457 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3458 assert_eq!(
3459 messages(&conversation, cx),
3460 vec![
3461 (message_1.id, Role::User, 0..4),
3462 (message_2.id, Role::User, 4..8),
3463 (message_3.id, Role::User, 8..12),
3464 (message_4.id, Role::User, 12..12)
3465 ]
3466 );
3467 assert_eq!(
3468 message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
3469 [message_1.id, message_2.id, message_3.id, message_4.id]
3470 );
3471
3472 fn message_ids_for_offsets(
3473 conversation: &Model<Conversation>,
3474 offsets: &[usize],
3475 cx: &AppContext,
3476 ) -> Vec<MessageId> {
3477 conversation
3478 .read(cx)
3479 .messages_for_offsets(offsets.iter().copied(), cx)
3480 .into_iter()
3481 .map(|message| message.id)
3482 .collect()
3483 }
3484 }
3485
3486 #[gpui::test]
3487 async fn test_slash_commands(cx: &mut TestAppContext) {
3488 let settings_store = cx.update(SettingsStore::test);
3489 cx.set_global(settings_store);
3490 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3491 cx.update(Project::init_settings);
3492 cx.update(init);
3493 let fs = FakeFs::new(cx.background_executor.clone());
3494
3495 fs.insert_tree(
3496 "/test",
3497 json!({
3498 "src": {
3499 "lib.rs": "fn one() -> usize { 1 }",
3500 "main.rs": "
3501 use crate::one;
3502 fn main() { one(); }
3503 ".unindent(),
3504 }
3505 }),
3506 )
3507 .await;
3508
3509 let slash_command_registry = SlashCommandRegistry::new();
3510 slash_command_registry.register_command(file_command::FileSlashCommand, false);
3511 slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
3512
3513 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3514 let conversation = cx
3515 .new_model(|cx| Conversation::new(registry.clone(), slash_command_registry, None, cx));
3516
3517 let output_ranges = Rc::new(RefCell::new(HashSet::default()));
3518 conversation.update(cx, |_, cx| {
3519 cx.subscribe(&conversation, {
3520 let ranges = output_ranges.clone();
3521 move |_, _, event, _| match event {
3522 ConversationEvent::PendingSlashCommandsUpdated { removed, updated } => {
3523 for range in removed {
3524 ranges.borrow_mut().remove(range);
3525 }
3526 for command in updated {
3527 ranges.borrow_mut().insert(command.source_range.clone());
3528 }
3529 }
3530 _ => {}
3531 }
3532 })
3533 .detach();
3534 });
3535
3536 let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3537
3538 // Insert a slash command
3539 buffer.update(cx, |buffer, cx| {
3540 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
3541 });
3542 assert_text_and_output_ranges(
3543 &buffer,
3544 &output_ranges.borrow(),
3545 "
3546 «/file src/lib.rs»
3547 "
3548 .unindent()
3549 .trim_end(),
3550 cx,
3551 );
3552
3553 // Edit the argument of the slash command.
3554 buffer.update(cx, |buffer, cx| {
3555 let edit_offset = buffer.text().find("lib.rs").unwrap();
3556 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
3557 });
3558 assert_text_and_output_ranges(
3559 &buffer,
3560 &output_ranges.borrow(),
3561 "
3562 «/file src/main.rs»
3563 "
3564 .unindent()
3565 .trim_end(),
3566 cx,
3567 );
3568
3569 // Edit the name of the slash command, using one that doesn't exist.
3570 buffer.update(cx, |buffer, cx| {
3571 let edit_offset = buffer.text().find("/file").unwrap();
3572 buffer.edit(
3573 [(edit_offset..edit_offset + "/file".len(), "/unknown")],
3574 None,
3575 cx,
3576 );
3577 });
3578 assert_text_and_output_ranges(
3579 &buffer,
3580 &output_ranges.borrow(),
3581 "
3582 /unknown src/main.rs
3583 "
3584 .unindent()
3585 .trim_end(),
3586 cx,
3587 );
3588
3589 #[track_caller]
3590 fn assert_text_and_output_ranges(
3591 buffer: &Model<Buffer>,
3592 ranges: &HashSet<Range<language::Anchor>>,
3593 expected_marked_text: &str,
3594 cx: &mut TestAppContext,
3595 ) {
3596 let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
3597 let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
3598 let mut ranges = ranges
3599 .iter()
3600 .map(|range| range.to_offset(buffer))
3601 .collect::<Vec<_>>();
3602 ranges.sort_by_key(|a| a.start);
3603 (buffer.text(), ranges)
3604 });
3605
3606 assert_eq!(actual_text, expected_text);
3607 assert_eq!(actual_ranges, expected_ranges);
3608 }
3609 }
3610
3611 #[test]
3612 fn test_parse_next_edit_suggestion() {
3613 let text = "
3614 some output:
3615
3616 ```edit src/foo.rs
3617 let a = 1;
3618 let b = 2;
3619 ---
3620 let w = 1;
3621 let x = 2;
3622 let y = 3;
3623 let z = 4;
3624 ```
3625
3626 some more output:
3627
3628 ```edit src/foo.rs
3629 let c = 1;
3630 ---
3631 ```
3632
3633 and the conclusion.
3634 "
3635 .unindent();
3636
3637 let rope = Rope::from(text.as_str());
3638 let mut lines = rope.chunks().lines();
3639 let mut suggestions = vec![];
3640 while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
3641 suggestions.push((
3642 suggestion.path.clone(),
3643 text[suggestion.old_text_range].to_string(),
3644 text[suggestion.new_text_range].to_string(),
3645 ));
3646 }
3647
3648 assert_eq!(
3649 suggestions,
3650 vec![
3651 (
3652 Path::new("src/foo.rs").into(),
3653 [
3654 " let a = 1;", //
3655 " let b = 2;",
3656 "",
3657 ]
3658 .join("\n"),
3659 [
3660 " let w = 1;",
3661 " let x = 2;",
3662 " let y = 3;",
3663 " let z = 4;",
3664 "",
3665 ]
3666 .join("\n"),
3667 ),
3668 (
3669 Path::new("src/foo.rs").into(),
3670 [
3671 " let c = 1;", //
3672 "",
3673 ]
3674 .join("\n"),
3675 String::new(),
3676 )
3677 ]
3678 );
3679 }
3680
3681 #[gpui::test]
3682 async fn test_serialization(cx: &mut TestAppContext) {
3683 let settings_store = cx.update(SettingsStore::test);
3684 cx.set_global(settings_store);
3685 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3686 cx.update(init);
3687 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3688 let conversation =
3689 cx.new_model(|cx| Conversation::new(registry.clone(), Default::default(), None, cx));
3690 let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3691 let message_0 =
3692 conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
3693 let message_1 = conversation.update(cx, |conversation, cx| {
3694 conversation
3695 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3696 .unwrap()
3697 });
3698 let message_2 = conversation.update(cx, |conversation, cx| {
3699 conversation
3700 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3701 .unwrap()
3702 });
3703 buffer.update(cx, |buffer, cx| {
3704 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3705 buffer.finalize_last_transaction();
3706 });
3707 let _message_3 = conversation.update(cx, |conversation, cx| {
3708 conversation
3709 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3710 .unwrap()
3711 });
3712 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3713 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
3714 assert_eq!(
3715 cx.read(|cx| messages(&conversation, cx)),
3716 [
3717 (message_0, Role::User, 0..2),
3718 (message_1.id, Role::Assistant, 2..6),
3719 (message_2.id, Role::System, 6..6),
3720 ]
3721 );
3722
3723 let deserialized_conversation = Conversation::deserialize(
3724 conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
3725 Default::default(),
3726 registry.clone(),
3727 Default::default(),
3728 None,
3729 &mut cx.to_async(),
3730 )
3731 .await
3732 .unwrap();
3733 let deserialized_buffer =
3734 deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3735 assert_eq!(
3736 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
3737 "a\nb\nc\n"
3738 );
3739 assert_eq!(
3740 cx.read(|cx| messages(&deserialized_conversation, cx)),
3741 [
3742 (message_0, Role::User, 0..2),
3743 (message_1.id, Role::Assistant, 2..6),
3744 (message_2.id, Role::System, 6..6),
3745 ]
3746 );
3747 }
3748
3749 fn messages(
3750 conversation: &Model<Conversation>,
3751 cx: &AppContext,
3752 ) -> Vec<(MessageId, Role, Range<usize>)> {
3753 conversation
3754 .read(cx)
3755 .messages(cx)
3756 .map(|message| (message.id, message.role, message.offset_range))
3757 .collect()
3758 }
3759}