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