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