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