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