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