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)
1136 })
1137 .log_err();
1138
1139 cx.spawn(|this, mut cx| async move {
1140 let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
1141 let model = this.update(&mut cx, |this, _| this.model.clone())?;
1142 let conversation = Conversation::deserialize(
1143 saved_conversation,
1144 model,
1145 path.clone(),
1146 languages,
1147 slash_commands,
1148 Some(telemetry),
1149 lsp_adapter_delegate,
1150 &mut cx,
1151 )
1152 .await?;
1153
1154 this.update(&mut cx, |this, cx| {
1155 let workspace = workspace
1156 .upgrade()
1157 .ok_or_else(|| anyhow!("workspace dropped"))?;
1158 let editor = cx.new_view(|cx| {
1159 ConversationEditor::for_conversation(conversation, fs, workspace, cx)
1160 });
1161 this.show_conversation(editor, cx);
1162 anyhow::Ok(())
1163 })??;
1164 Ok(())
1165 })
1166 }
1167
1168 fn show_prompt_manager(&mut self, cx: &mut ViewContext<Self>) {
1169 if let Some(workspace) = self.workspace.upgrade() {
1170 workspace.update(cx, |workspace, cx| {
1171 workspace.toggle_modal(cx, |cx| {
1172 PromptManager::new(
1173 self.prompt_library.clone(),
1174 self.languages.clone(),
1175 self.fs.clone(),
1176 cx,
1177 )
1178 })
1179 })
1180 }
1181 }
1182
1183 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1184 CompletionProvider::global(cx).is_authenticated()
1185 }
1186
1187 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1188 cx.update_global::<CompletionProvider, _>(|provider, cx| provider.authenticate(cx))
1189 }
1190
1191 fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1192 let header =
1193 TabBar::new("assistant_header")
1194 .start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
1195 .children(self.active_conversation_editor().map(|editor| {
1196 h_flex()
1197 .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
1198 .flex_1()
1199 .px_2()
1200 .child(Label::new(editor.read(cx).title(cx)).into_element())
1201 }))
1202 .end_child(
1203 h_flex()
1204 .gap_2()
1205 .when_some(self.active_conversation_editor(), |this, editor| {
1206 let conversation = editor.read(cx).conversation.clone();
1207 this.child(
1208 h_flex()
1209 .gap_1()
1210 .child(self.render_model(&conversation, cx))
1211 .children(self.render_remaining_tokens(&conversation, cx)),
1212 )
1213 .child(
1214 ui::Divider::vertical()
1215 .inset()
1216 .color(ui::DividerColor::Border),
1217 )
1218 })
1219 .child(
1220 h_flex()
1221 .gap_1()
1222 .child(self.render_inject_context_menu(cx))
1223 .child(
1224 IconButton::new("show_prompt_manager", IconName::Library)
1225 .icon_size(IconSize::Small)
1226 .on_click(cx.listener(|this, _event, cx| {
1227 this.show_prompt_manager(cx)
1228 }))
1229 .tooltip(|cx| Tooltip::text("Prompt Library…", cx)),
1230 ),
1231 ),
1232 );
1233
1234 let contents = if self.active_conversation_editor().is_some() {
1235 let mut registrar = DivRegistrar::new(
1236 |panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
1237 cx,
1238 );
1239 BufferSearchBar::register(&mut registrar);
1240 registrar.into_div()
1241 } else {
1242 div()
1243 };
1244
1245 v_flex()
1246 .key_context("AssistantPanel")
1247 .size_full()
1248 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1249 this.new_conversation(cx);
1250 }))
1251 .on_action(cx.listener(AssistantPanel::toggle_zoom))
1252 .on_action(cx.listener(AssistantPanel::toggle_history))
1253 .on_action(cx.listener(AssistantPanel::deploy))
1254 .on_action(cx.listener(AssistantPanel::select_next_match))
1255 .on_action(cx.listener(AssistantPanel::select_prev_match))
1256 .on_action(cx.listener(AssistantPanel::handle_editor_cancel))
1257 .on_action(cx.listener(AssistantPanel::reset_credentials))
1258 .track_focus(&self.focus_handle)
1259 .child(header)
1260 .children(if self.toolbar.read(cx).hidden() {
1261 None
1262 } else {
1263 Some(self.toolbar.clone())
1264 })
1265 .child(contents.flex_1().child(
1266 if self.show_saved_conversations || self.active_conversation_editor().is_none() {
1267 let view = cx.view().clone();
1268 let scroll_handle = self.saved_conversations_scroll_handle.clone();
1269 let conversation_count = self.saved_conversations.len();
1270 canvas(
1271 move |bounds, cx| {
1272 let mut saved_conversations = uniform_list(
1273 view,
1274 "saved_conversations",
1275 conversation_count,
1276 |this, range, cx| {
1277 range
1278 .map(|ix| this.render_saved_conversation(ix, cx))
1279 .collect()
1280 },
1281 )
1282 .track_scroll(scroll_handle)
1283 .into_any_element();
1284 saved_conversations.prepaint_as_root(
1285 bounds.origin,
1286 bounds.size.map(AvailableSpace::Definite),
1287 cx,
1288 );
1289 saved_conversations
1290 },
1291 |_bounds, mut saved_conversations, cx| saved_conversations.paint(cx),
1292 )
1293 .size_full()
1294 .into_any_element()
1295 } else if let Some(editor) = self.active_conversation_editor() {
1296 let editor = editor.clone();
1297 div()
1298 .size_full()
1299 .child(editor.clone())
1300 .child(
1301 h_flex()
1302 .w_full()
1303 .absolute()
1304 .bottom_0()
1305 .p_4()
1306 .justify_end()
1307 .children(self.render_send_button(cx)),
1308 )
1309 .into_any_element()
1310 } else {
1311 div().into_any_element()
1312 },
1313 ))
1314 }
1315
1316 fn render_model(
1317 &self,
1318 conversation: &Model<Conversation>,
1319 cx: &mut ViewContext<Self>,
1320 ) -> impl IntoElement {
1321 Button::new("current_model", conversation.read(cx).model.display_name())
1322 .style(ButtonStyle::Filled)
1323 .tooltip(move |cx| Tooltip::text("Change Model", cx))
1324 .on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
1325 }
1326
1327 fn render_remaining_tokens(
1328 &self,
1329 conversation: &Model<Conversation>,
1330 cx: &mut ViewContext<Self>,
1331 ) -> Option<impl IntoElement> {
1332 let remaining_tokens = conversation.read(cx).remaining_tokens()?;
1333 let remaining_tokens_color = if remaining_tokens <= 0 {
1334 Color::Error
1335 } else if remaining_tokens <= 500 {
1336 Color::Warning
1337 } else {
1338 Color::Muted
1339 };
1340 Some(
1341 Label::new(remaining_tokens.to_string())
1342 .size(LabelSize::Small)
1343 .color(remaining_tokens_color),
1344 )
1345 }
1346}
1347
1348impl Render for AssistantPanel {
1349 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1350 if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
1351 authentication_prompt.clone().into_any()
1352 } else {
1353 self.render_signed_in(cx).into_any_element()
1354 }
1355 }
1356}
1357
1358impl Panel for AssistantPanel {
1359 fn persistent_name() -> &'static str {
1360 "AssistantPanel"
1361 }
1362
1363 fn position(&self, cx: &WindowContext) -> DockPosition {
1364 match AssistantSettings::get_global(cx).dock {
1365 AssistantDockPosition::Left => DockPosition::Left,
1366 AssistantDockPosition::Bottom => DockPosition::Bottom,
1367 AssistantDockPosition::Right => DockPosition::Right,
1368 }
1369 }
1370
1371 fn position_is_valid(&self, _: DockPosition) -> bool {
1372 true
1373 }
1374
1375 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1376 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
1377 let dock = match position {
1378 DockPosition::Left => AssistantDockPosition::Left,
1379 DockPosition::Bottom => AssistantDockPosition::Bottom,
1380 DockPosition::Right => AssistantDockPosition::Right,
1381 };
1382 settings.set_dock(dock);
1383 });
1384 }
1385
1386 fn size(&self, cx: &WindowContext) -> Pixels {
1387 let settings = AssistantSettings::get_global(cx);
1388 match self.position(cx) {
1389 DockPosition::Left | DockPosition::Right => {
1390 self.width.unwrap_or(settings.default_width)
1391 }
1392 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1393 }
1394 }
1395
1396 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1397 match self.position(cx) {
1398 DockPosition::Left | DockPosition::Right => self.width = size,
1399 DockPosition::Bottom => self.height = size,
1400 }
1401 cx.notify();
1402 }
1403
1404 fn is_zoomed(&self, _: &WindowContext) -> bool {
1405 self.zoomed
1406 }
1407
1408 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1409 self.zoomed = zoomed;
1410 cx.notify();
1411 }
1412
1413 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1414 if active {
1415 let load_credentials = self.authenticate(cx);
1416 cx.spawn(|this, mut cx| async move {
1417 load_credentials.await?;
1418 this.update(&mut cx, |this, cx| {
1419 if this.is_authenticated(cx) && this.active_conversation_editor().is_none() {
1420 this.new_conversation(cx);
1421 }
1422 })
1423 })
1424 .detach_and_log_err(cx);
1425 }
1426 }
1427
1428 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1429 let settings = AssistantSettings::get_global(cx);
1430 if !settings.enabled || !settings.button {
1431 return None;
1432 }
1433
1434 Some(IconName::Ai)
1435 }
1436
1437 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1438 Some("Assistant Panel")
1439 }
1440
1441 fn toggle_action(&self) -> Box<dyn Action> {
1442 Box::new(ToggleFocus)
1443 }
1444}
1445
1446impl EventEmitter<PanelEvent> for AssistantPanel {}
1447
1448impl FocusableView for AssistantPanel {
1449 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1450 self.focus_handle.clone()
1451 }
1452}
1453
1454#[derive(Clone)]
1455enum ConversationEvent {
1456 MessagesEdited,
1457 SummaryChanged,
1458 EditSuggestionsChanged,
1459 StreamedCompletion,
1460 SlashCommandsChanged,
1461 SlashCommandOutputAdded(Range<language::Anchor>),
1462 SlashCommandOutputRemoved(Range<language::Anchor>),
1463}
1464
1465#[derive(Default)]
1466struct Summary {
1467 text: String,
1468 done: bool,
1469}
1470
1471pub struct Conversation {
1472 id: Option<String>,
1473 buffer: Model<Buffer>,
1474 pub(crate) ambient_context: AmbientContext,
1475 edit_suggestions: Vec<EditSuggestion>,
1476 slash_command_calls: Vec<SlashCommandCall>,
1477 message_anchors: Vec<MessageAnchor>,
1478 messages_metadata: HashMap<MessageId, MessageMetadata>,
1479 next_message_id: MessageId,
1480 summary: Option<Summary>,
1481 pending_summary: Task<Option<()>>,
1482 completion_count: usize,
1483 pending_completions: Vec<PendingCompletion>,
1484 model: LanguageModel,
1485 token_count: Option<usize>,
1486 pending_token_count: Task<Option<()>>,
1487 pending_edit_suggestion_parse: Option<Task<()>>,
1488 pending_command_invocation_parse: Option<Task<()>>,
1489 pending_save: Task<Result<()>>,
1490 path: Option<PathBuf>,
1491 _subscriptions: Vec<Subscription>,
1492 telemetry: Option<Arc<Telemetry>>,
1493 slash_command_registry: Arc<SlashCommandRegistry>,
1494 language_registry: Arc<LanguageRegistry>,
1495 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1496}
1497
1498impl EventEmitter<ConversationEvent> for Conversation {}
1499
1500impl Conversation {
1501 fn new(
1502 model: LanguageModel,
1503 language_registry: Arc<LanguageRegistry>,
1504 slash_command_registry: Arc<SlashCommandRegistry>,
1505 telemetry: Option<Arc<Telemetry>>,
1506 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1507 cx: &mut ModelContext<Self>,
1508 ) -> Self {
1509 let buffer = cx.new_model(|cx| {
1510 let mut buffer = Buffer::local("", cx);
1511 buffer.set_language_registry(language_registry.clone());
1512 buffer
1513 });
1514
1515 let mut this = Self {
1516 id: Some(Uuid::new_v4().to_string()),
1517 message_anchors: Default::default(),
1518 messages_metadata: Default::default(),
1519 next_message_id: Default::default(),
1520 ambient_context: AmbientContext::default(),
1521 edit_suggestions: Vec::new(),
1522 slash_command_calls: Vec::new(),
1523 summary: None,
1524 pending_summary: Task::ready(None),
1525 completion_count: Default::default(),
1526 pending_completions: Default::default(),
1527 token_count: None,
1528 pending_token_count: Task::ready(None),
1529 pending_edit_suggestion_parse: None,
1530 pending_command_invocation_parse: None,
1531 model,
1532 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1533 pending_save: Task::ready(Ok(())),
1534 path: None,
1535 buffer,
1536 telemetry,
1537 slash_command_registry,
1538 language_registry,
1539 lsp_adapter_delegate,
1540 };
1541
1542 let message = MessageAnchor {
1543 id: MessageId(post_inc(&mut this.next_message_id.0)),
1544 start: language::Anchor::MIN,
1545 };
1546 this.message_anchors.push(message.clone());
1547 this.messages_metadata.insert(
1548 message.id,
1549 MessageMetadata {
1550 role: Role::User,
1551 status: MessageStatus::Done,
1552 ambient_context: AmbientContextSnapshot::default(),
1553 },
1554 );
1555
1556 this.set_language(cx);
1557 this.count_remaining_tokens(cx);
1558 this
1559 }
1560
1561 fn serialize(&self, cx: &AppContext) -> SavedConversation {
1562 SavedConversation {
1563 id: self.id.clone(),
1564 zed: "conversation".into(),
1565 version: SavedConversation::VERSION.into(),
1566 text: self.buffer.read(cx).text(),
1567 message_metadata: self.messages_metadata.clone(),
1568 messages: self
1569 .messages(cx)
1570 .map(|message| SavedMessage {
1571 id: message.id,
1572 start: message.offset_range.start,
1573 })
1574 .collect(),
1575 summary: self
1576 .summary
1577 .as_ref()
1578 .map(|summary| summary.text.clone())
1579 .unwrap_or_default(),
1580 }
1581 }
1582
1583 #[allow(clippy::too_many_arguments)]
1584 async fn deserialize(
1585 saved_conversation: SavedConversation,
1586 model: LanguageModel,
1587 path: PathBuf,
1588 language_registry: Arc<LanguageRegistry>,
1589 slash_command_registry: Arc<SlashCommandRegistry>,
1590 telemetry: Option<Arc<Telemetry>>,
1591 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1592 cx: &mut AsyncAppContext,
1593 ) -> Result<Model<Self>> {
1594 let id = match saved_conversation.id {
1595 Some(id) => Some(id),
1596 None => Some(Uuid::new_v4().to_string()),
1597 };
1598
1599 let markdown = language_registry.language_for_name("Markdown");
1600 let mut message_anchors = Vec::new();
1601 let mut next_message_id = MessageId(0);
1602 let buffer = cx.new_model(|cx| {
1603 let mut buffer = Buffer::local(saved_conversation.text, cx);
1604 for message in saved_conversation.messages {
1605 message_anchors.push(MessageAnchor {
1606 id: message.id,
1607 start: buffer.anchor_before(message.start),
1608 });
1609 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1610 }
1611 buffer.set_language_registry(language_registry.clone());
1612 cx.spawn(|buffer, mut cx| async move {
1613 let markdown = markdown.await?;
1614 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1615 buffer.set_language(Some(markdown), cx)
1616 })?;
1617 anyhow::Ok(())
1618 })
1619 .detach_and_log_err(cx);
1620 buffer
1621 })?;
1622
1623 cx.new_model(move |cx| {
1624 let mut this = Self {
1625 id,
1626 message_anchors,
1627 messages_metadata: saved_conversation.message_metadata,
1628 next_message_id,
1629 ambient_context: AmbientContext::default(),
1630 edit_suggestions: Vec::new(),
1631 slash_command_calls: Vec::new(),
1632 summary: Some(Summary {
1633 text: saved_conversation.summary,
1634 done: true,
1635 }),
1636 pending_summary: Task::ready(None),
1637 completion_count: Default::default(),
1638 pending_completions: Default::default(),
1639 token_count: None,
1640 pending_edit_suggestion_parse: None,
1641 pending_command_invocation_parse: None,
1642 pending_token_count: Task::ready(None),
1643 model,
1644 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1645 pending_save: Task::ready(Ok(())),
1646 path: Some(path),
1647 buffer,
1648 telemetry,
1649 language_registry,
1650 slash_command_registry,
1651 lsp_adapter_delegate,
1652 };
1653 this.set_language(cx);
1654 this.reparse_edit_suggestions(cx);
1655 this.count_remaining_tokens(cx);
1656 this
1657 })
1658 }
1659
1660 fn set_language(&mut self, cx: &mut ModelContext<Self>) {
1661 let markdown = self.language_registry.language_for_name("Markdown");
1662 cx.spawn(|this, mut cx| async move {
1663 let markdown = markdown.await?;
1664 this.update(&mut cx, |this, cx| {
1665 this.buffer
1666 .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx));
1667 })
1668 })
1669 .detach_and_log_err(cx);
1670 }
1671
1672 fn toggle_recent_buffers(&mut self, cx: &mut ModelContext<Self>) {
1673 self.ambient_context.recent_buffers.enabled = !self.ambient_context.recent_buffers.enabled;
1674 match self.ambient_context.recent_buffers.update(cx) {
1675 ContextUpdated::Updating => {}
1676 ContextUpdated::Disabled => {
1677 self.count_remaining_tokens(cx);
1678 }
1679 }
1680 }
1681
1682 fn toggle_current_project_context(
1683 &mut self,
1684 fs: Arc<dyn Fs>,
1685 project: WeakModel<Project>,
1686 cx: &mut ModelContext<Self>,
1687 ) {
1688 self.ambient_context.current_project.enabled =
1689 !self.ambient_context.current_project.enabled;
1690 match self.ambient_context.current_project.update(fs, project, cx) {
1691 ContextUpdated::Updating => {}
1692 ContextUpdated::Disabled => {
1693 self.count_remaining_tokens(cx);
1694 }
1695 }
1696 }
1697
1698 fn set_recent_buffers(
1699 &mut self,
1700 buffers: impl IntoIterator<Item = Model<Buffer>>,
1701 cx: &mut ModelContext<Self>,
1702 ) {
1703 self.ambient_context.recent_buffers.buffers.clear();
1704 self.ambient_context
1705 .recent_buffers
1706 .buffers
1707 .extend(buffers.into_iter().map(|buffer| RecentBuffer {
1708 buffer: buffer.downgrade(),
1709 _subscription: cx.observe(&buffer, |this, _, cx| {
1710 match this.ambient_context.recent_buffers.update(cx) {
1711 ContextUpdated::Updating => {}
1712 ContextUpdated::Disabled => {
1713 this.count_remaining_tokens(cx);
1714 }
1715 }
1716 }),
1717 }));
1718 match self.ambient_context.recent_buffers.update(cx) {
1719 ContextUpdated::Updating => {}
1720 ContextUpdated::Disabled => {
1721 self.count_remaining_tokens(cx);
1722 }
1723 }
1724 }
1725
1726 fn handle_buffer_event(
1727 &mut self,
1728 _: Model<Buffer>,
1729 event: &language::Event,
1730 cx: &mut ModelContext<Self>,
1731 ) {
1732 if *event == language::Event::Edited {
1733 self.count_remaining_tokens(cx);
1734 self.reparse_edit_suggestions(cx);
1735 self.reparse_slash_command_calls(cx);
1736 cx.emit(ConversationEvent::MessagesEdited);
1737 }
1738 }
1739
1740 pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1741 let request = self.to_completion_request(cx);
1742 self.pending_token_count = cx.spawn(|this, mut cx| {
1743 async move {
1744 cx.background_executor()
1745 .timer(Duration::from_millis(200))
1746 .await;
1747
1748 let token_count = cx
1749 .update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
1750 .await?;
1751
1752 this.update(&mut cx, |this, cx| {
1753 this.token_count = Some(token_count);
1754 cx.notify()
1755 })?;
1756 anyhow::Ok(())
1757 }
1758 .log_err()
1759 });
1760 }
1761
1762 fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
1763 self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
1764 cx.background_executor()
1765 .timer(Duration::from_millis(200))
1766 .await;
1767
1768 this.update(&mut cx, |this, cx| {
1769 this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx);
1770 })
1771 .ok();
1772 }));
1773 }
1774
1775 fn reparse_edit_suggestions_in_range(
1776 &mut self,
1777 range: Range<usize>,
1778 cx: &mut ModelContext<Self>,
1779 ) {
1780 self.buffer.update(cx, |buffer, _| {
1781 let range_start = buffer.anchor_before(range.start);
1782 let range_end = buffer.anchor_after(range.end);
1783 let start_ix = self
1784 .edit_suggestions
1785 .binary_search_by(|probe| {
1786 probe
1787 .source_range
1788 .end
1789 .cmp(&range_start, buffer)
1790 .then(Ordering::Greater)
1791 })
1792 .unwrap_err();
1793 let end_ix = self
1794 .edit_suggestions
1795 .binary_search_by(|probe| {
1796 probe
1797 .source_range
1798 .start
1799 .cmp(&range_end, buffer)
1800 .then(Ordering::Less)
1801 })
1802 .unwrap_err();
1803
1804 let mut new_edit_suggestions = Vec::new();
1805 let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
1806 while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) {
1807 let start_anchor = buffer.anchor_after(suggestion.outer_range.start);
1808 let end_anchor = buffer.anchor_before(suggestion.outer_range.end);
1809 new_edit_suggestions.push(EditSuggestion {
1810 source_range: start_anchor..end_anchor,
1811 full_path: suggestion.path,
1812 });
1813 }
1814 self.edit_suggestions
1815 .splice(start_ix..end_ix, new_edit_suggestions);
1816 });
1817 cx.emit(ConversationEvent::EditSuggestionsChanged);
1818 cx.notify();
1819 }
1820
1821 fn reparse_slash_command_calls(&mut self, cx: &mut ModelContext<Self>) {
1822 self.pending_command_invocation_parse = Some(cx.spawn(|this, mut cx| async move {
1823 cx.background_executor().timer(SLASH_COMMAND_DEBOUNCE).await;
1824
1825 this.update(&mut cx, |this, cx| {
1826 let buffer = this.buffer.read(cx).snapshot();
1827
1828 let mut changed = false;
1829 let mut new_calls = Vec::new();
1830 let mut old_calls = mem::take(&mut this.slash_command_calls)
1831 .into_iter()
1832 .peekable();
1833 let mut lines = buffer.as_rope().chunks().lines();
1834 let mut offset = 0;
1835 while let Some(line) = lines.next() {
1836 let line_end_offset = offset + line.len();
1837 if let Some(call) = SlashCommandLine::parse(line) {
1838 let mut unchanged_call = None;
1839 while let Some(old_call) = old_calls.peek() {
1840 match old_call.source_range.start.to_offset(&buffer).cmp(&offset) {
1841 Ordering::Greater => break,
1842 Ordering::Equal
1843 if this.slash_command_is_unchanged(
1844 old_call, &call, line, &buffer,
1845 ) =>
1846 {
1847 unchanged_call = old_calls.next();
1848 }
1849 _ => {
1850 changed = true;
1851 let old_call = old_calls.next().unwrap();
1852 this.slash_command_call_removed(old_call, cx);
1853 }
1854 }
1855 }
1856
1857 let name = &line[call.name];
1858 if let Some(call) = unchanged_call {
1859 new_calls.push(call);
1860 } else if let Some(command) = this.slash_command_registry.command(name) {
1861 changed = true;
1862 let name = name.to_string();
1863 let source_range =
1864 buffer.anchor_after(offset)..buffer.anchor_before(line_end_offset);
1865
1866 let argument = call.argument.map(|range| &line[range]);
1867 let invocation = command.run(
1868 argument,
1869 this.lsp_adapter_delegate
1870 .clone()
1871 .expect("no LspAdapterDelegate present when invoking command"),
1872 cx,
1873 );
1874
1875 new_calls.push(SlashCommandCall {
1876 name,
1877 argument: argument.map(|s| s.to_string()),
1878 source_range: source_range.clone(),
1879 output_range: None,
1880 should_rerun: false,
1881 _invalidate: cx.spawn(|this, mut cx| {
1882 let source_range = source_range.clone();
1883 let invalidated = invocation.invalidated;
1884 async move {
1885 if invalidated.await.is_ok() {
1886 _ = this.update(&mut cx, |this, cx| {
1887 let buffer = this.buffer.read(cx);
1888 let call_ix = this
1889 .slash_command_calls
1890 .binary_search_by(|probe| {
1891 probe
1892 .source_range
1893 .start
1894 .cmp(&source_range.start, buffer)
1895 });
1896 if let Ok(call_ix) = call_ix {
1897 this.slash_command_calls[call_ix]
1898 .should_rerun = true;
1899 this.reparse_slash_command_calls(cx);
1900 }
1901 });
1902 }
1903 }
1904 }),
1905 _command_cleanup: invocation.cleanup,
1906 });
1907
1908 cx.spawn(|this, mut cx| async move {
1909 let output = invocation.output.await;
1910 this.update(&mut cx, |this, cx| {
1911 let output_range = this.buffer.update(cx, |buffer, cx| {
1912 let call_ix = this
1913 .slash_command_calls
1914 .binary_search_by(|probe| {
1915 probe
1916 .source_range
1917 .start
1918 .cmp(&source_range.start, buffer)
1919 })
1920 .ok()?;
1921
1922 let mut output = output.log_err()?;
1923 output.truncate(output.trim_end().len());
1924
1925 let source_end = source_range.end.to_offset(buffer);
1926 let output_start = source_end + '\n'.len_utf8();
1927 let output_end = output_start + output.len();
1928
1929 if buffer
1930 .chars_at(source_end)
1931 .next()
1932 .map_or(false, |c| c != '\n')
1933 {
1934 output.push('\n');
1935 }
1936
1937 buffer.edit(
1938 [
1939 (source_end..source_end, "\n".to_string()),
1940 (source_end..source_end, output),
1941 ],
1942 None,
1943 cx,
1944 );
1945
1946 let output_start = buffer.anchor_after(output_start);
1947 let output_end = buffer.anchor_before(output_end);
1948 this.slash_command_calls[call_ix].output_range =
1949 Some(output_start..output_end);
1950 Some(source_range.end..output_end)
1951 });
1952 if let Some(output_range) = output_range {
1953 cx.emit(ConversationEvent::SlashCommandOutputAdded(
1954 output_range,
1955 ));
1956 cx.emit(ConversationEvent::SlashCommandsChanged);
1957 }
1958 })
1959 .ok();
1960 })
1961 .detach();
1962 }
1963 }
1964 offset = lines.offset();
1965 }
1966
1967 for old_call in old_calls {
1968 changed = true;
1969 this.slash_command_call_removed(old_call, cx);
1970 }
1971
1972 if changed {
1973 cx.emit(ConversationEvent::SlashCommandsChanged);
1974 }
1975
1976 this.slash_command_calls = new_calls;
1977 })
1978 .ok();
1979 }));
1980 }
1981
1982 fn slash_command_is_unchanged(
1983 &self,
1984 old_call: &SlashCommandCall,
1985 new_call: &SlashCommandLine,
1986 new_text: &str,
1987 buffer: &BufferSnapshot,
1988 ) -> bool {
1989 if old_call.name != new_text[new_call.name.clone()] {
1990 return false;
1991 }
1992
1993 if old_call.argument.as_deref() != new_call.argument.clone().map(|range| &new_text[range]) {
1994 return false;
1995 }
1996
1997 if old_call.should_rerun {
1998 return false;
1999 }
2000
2001 if let Some(output_range) = &old_call.output_range {
2002 let source_range = old_call.source_range.to_point(buffer);
2003 let output_start = output_range.start.to_point(buffer);
2004 if source_range.start.column != 0 {
2005 return false;
2006 }
2007 if source_range.end.column != new_text.len() as u32 {
2008 return false;
2009 }
2010 if output_start != Point::new(source_range.end.row + 1, 0) {
2011 return false;
2012 }
2013 if let Some(next_char) = buffer.chars_at(output_range.end).next() {
2014 if next_char != '\n' {
2015 return false;
2016 }
2017 }
2018 }
2019 true
2020 }
2021
2022 fn slash_command_call_removed(
2023 &self,
2024 old_call: SlashCommandCall,
2025 cx: &mut ModelContext<Conversation>,
2026 ) {
2027 if let Some(output_range) = old_call.output_range {
2028 self.buffer.update(cx, |buffer, cx| {
2029 buffer.edit(
2030 [(old_call.source_range.end..output_range.end, "")],
2031 None,
2032 cx,
2033 );
2034 });
2035 cx.emit(ConversationEvent::SlashCommandOutputRemoved(
2036 old_call.source_range.end..output_range.end,
2037 ))
2038 }
2039 }
2040
2041 fn remaining_tokens(&self) -> Option<isize> {
2042 Some(self.model.max_token_count() as isize - self.token_count? as isize)
2043 }
2044
2045 fn set_model(&mut self, model: LanguageModel, cx: &mut ModelContext<Self>) {
2046 self.model = model;
2047 self.count_remaining_tokens(cx);
2048 }
2049
2050 fn assist(
2051 &mut self,
2052 selected_messages: HashSet<MessageId>,
2053 cx: &mut ModelContext<Self>,
2054 ) -> Vec<MessageAnchor> {
2055 let mut user_messages = Vec::new();
2056
2057 let last_message_id = if let Some(last_message_id) =
2058 self.message_anchors.iter().rev().find_map(|message| {
2059 message
2060 .start
2061 .is_valid(self.buffer.read(cx))
2062 .then_some(message.id)
2063 }) {
2064 last_message_id
2065 } else {
2066 return Default::default();
2067 };
2068
2069 let mut should_assist = false;
2070 for selected_message_id in selected_messages {
2071 let selected_message_role =
2072 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
2073 metadata.role
2074 } else {
2075 continue;
2076 };
2077
2078 if selected_message_role == Role::Assistant {
2079 if let Some(user_message) = self.insert_message_after(
2080 selected_message_id,
2081 Role::User,
2082 MessageStatus::Done,
2083 cx,
2084 ) {
2085 user_messages.push(user_message);
2086 }
2087 } else {
2088 should_assist = true;
2089 }
2090 }
2091
2092 if should_assist {
2093 if !CompletionProvider::global(cx).is_authenticated() {
2094 log::info!("completion provider has no credentials");
2095 return Default::default();
2096 }
2097
2098 let request = self.to_completion_request(cx);
2099 let stream = CompletionProvider::global(cx).complete(request);
2100 let assistant_message = self
2101 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
2102 .unwrap();
2103
2104 // Queue up the user's next reply.
2105 let user_message = self
2106 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
2107 .unwrap();
2108 user_messages.push(user_message);
2109
2110 let task = cx.spawn({
2111 |this, mut cx| async move {
2112 let assistant_message_id = assistant_message.id;
2113 let mut response_latency = None;
2114 let stream_completion = async {
2115 let request_start = Instant::now();
2116 let mut messages = stream.await?;
2117
2118 while let Some(message) = messages.next().await {
2119 if response_latency.is_none() {
2120 response_latency = Some(request_start.elapsed());
2121 }
2122 let text = message?;
2123
2124 this.update(&mut cx, |this, cx| {
2125 let message_ix = this
2126 .message_anchors
2127 .iter()
2128 .position(|message| message.id == assistant_message_id)?;
2129 let message_range = this.buffer.update(cx, |buffer, cx| {
2130 let message_start_offset =
2131 this.message_anchors[message_ix].start.to_offset(buffer);
2132 let message_old_end_offset = this.message_anchors
2133 [message_ix + 1..]
2134 .iter()
2135 .find(|message| message.start.is_valid(buffer))
2136 .map_or(buffer.len(), |message| {
2137 message.start.to_offset(buffer).saturating_sub(1)
2138 });
2139 let message_new_end_offset =
2140 message_old_end_offset + text.len();
2141 buffer.edit(
2142 [(message_old_end_offset..message_old_end_offset, text)],
2143 None,
2144 cx,
2145 );
2146 message_start_offset..message_new_end_offset
2147 });
2148 this.reparse_edit_suggestions_in_range(message_range, cx);
2149 cx.emit(ConversationEvent::StreamedCompletion);
2150
2151 Some(())
2152 })?;
2153 smol::future::yield_now().await;
2154 }
2155
2156 this.update(&mut cx, |this, cx| {
2157 this.pending_completions
2158 .retain(|completion| completion.id != this.completion_count);
2159 this.summarize(cx);
2160 })?;
2161
2162 anyhow::Ok(())
2163 };
2164
2165 let result = stream_completion.await;
2166
2167 this.update(&mut cx, |this, cx| {
2168 if let Some(metadata) =
2169 this.messages_metadata.get_mut(&assistant_message.id)
2170 {
2171 let error_message = result
2172 .err()
2173 .map(|error| error.to_string().trim().to_string());
2174 if let Some(error_message) = error_message.as_ref() {
2175 metadata.status =
2176 MessageStatus::Error(SharedString::from(error_message.clone()));
2177 } else {
2178 metadata.status = MessageStatus::Done;
2179 }
2180
2181 if let Some(telemetry) = this.telemetry.as_ref() {
2182 telemetry.report_assistant_event(
2183 this.id.clone(),
2184 AssistantKind::Panel,
2185 this.model.telemetry_id(),
2186 response_latency,
2187 error_message,
2188 );
2189 }
2190
2191 cx.emit(ConversationEvent::MessagesEdited);
2192 }
2193 })
2194 .ok();
2195 }
2196 });
2197
2198 self.pending_completions.push(PendingCompletion {
2199 id: post_inc(&mut self.completion_count),
2200 _task: task,
2201 });
2202 }
2203
2204 user_messages
2205 }
2206
2207 fn to_completion_request(&self, cx: &mut ModelContext<Conversation>) -> LanguageModelRequest {
2208 let edits_system_prompt = LanguageModelRequestMessage {
2209 role: Role::System,
2210 content: include_str!("./system_prompts/edits.md").to_string(),
2211 };
2212
2213 let recent_buffers_context = self.ambient_context.recent_buffers.to_message();
2214 let current_project_context = self.ambient_context.current_project.to_message();
2215
2216 let messages = Some(edits_system_prompt)
2217 .into_iter()
2218 .chain(recent_buffers_context)
2219 .chain(current_project_context)
2220 .chain(
2221 self.messages(cx)
2222 .filter(|message| matches!(message.status, MessageStatus::Done))
2223 .map(|message| message.to_request_message(self.buffer.read(cx))),
2224 );
2225
2226 LanguageModelRequest {
2227 model: self.model.clone(),
2228 messages: messages.collect(),
2229 stop: vec![],
2230 temperature: 1.0,
2231 }
2232 }
2233
2234 fn cancel_last_assist(&mut self) -> bool {
2235 self.pending_completions.pop().is_some()
2236 }
2237
2238 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
2239 for id in ids {
2240 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
2241 metadata.role.cycle();
2242 cx.emit(ConversationEvent::MessagesEdited);
2243 cx.notify();
2244 }
2245 }
2246 }
2247
2248 fn insert_message_after(
2249 &mut self,
2250 message_id: MessageId,
2251 role: Role,
2252 status: MessageStatus,
2253 cx: &mut ModelContext<Self>,
2254 ) -> Option<MessageAnchor> {
2255 if let Some(prev_message_ix) = self
2256 .message_anchors
2257 .iter()
2258 .position(|message| message.id == message_id)
2259 {
2260 // Find the next valid message after the one we were given.
2261 let mut next_message_ix = prev_message_ix + 1;
2262 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
2263 if next_message.start.is_valid(self.buffer.read(cx)) {
2264 break;
2265 }
2266 next_message_ix += 1;
2267 }
2268
2269 let start = self.buffer.update(cx, |buffer, cx| {
2270 let offset = self
2271 .message_anchors
2272 .get(next_message_ix)
2273 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
2274 buffer.edit([(offset..offset, "\n")], None, cx);
2275 buffer.anchor_before(offset + 1)
2276 });
2277 let message = MessageAnchor {
2278 id: MessageId(post_inc(&mut self.next_message_id.0)),
2279 start,
2280 };
2281 self.message_anchors
2282 .insert(next_message_ix, message.clone());
2283 self.messages_metadata.insert(
2284 message.id,
2285 MessageMetadata {
2286 role,
2287 status,
2288 ambient_context: self.ambient_context.snapshot(),
2289 },
2290 );
2291 cx.emit(ConversationEvent::MessagesEdited);
2292 Some(message)
2293 } else {
2294 None
2295 }
2296 }
2297
2298 fn split_message(
2299 &mut self,
2300 range: Range<usize>,
2301 cx: &mut ModelContext<Self>,
2302 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
2303 let start_message = self.message_for_offset(range.start, cx);
2304 let end_message = self.message_for_offset(range.end, cx);
2305 if let Some((start_message, end_message)) = start_message.zip(end_message) {
2306 // Prevent splitting when range spans multiple messages.
2307 if start_message.id != end_message.id {
2308 return (None, None);
2309 }
2310
2311 let message = start_message;
2312 let role = message.role;
2313 let mut edited_buffer = false;
2314
2315 let mut suffix_start = None;
2316 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
2317 {
2318 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
2319 suffix_start = Some(range.end + 1);
2320 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
2321 suffix_start = Some(range.end);
2322 }
2323 }
2324
2325 let suffix = if let Some(suffix_start) = suffix_start {
2326 MessageAnchor {
2327 id: MessageId(post_inc(&mut self.next_message_id.0)),
2328 start: self.buffer.read(cx).anchor_before(suffix_start),
2329 }
2330 } else {
2331 self.buffer.update(cx, |buffer, cx| {
2332 buffer.edit([(range.end..range.end, "\n")], None, cx);
2333 });
2334 edited_buffer = true;
2335 MessageAnchor {
2336 id: MessageId(post_inc(&mut self.next_message_id.0)),
2337 start: self.buffer.read(cx).anchor_before(range.end + 1),
2338 }
2339 };
2340
2341 self.message_anchors
2342 .insert(message.index_range.end + 1, suffix.clone());
2343 self.messages_metadata.insert(
2344 suffix.id,
2345 MessageMetadata {
2346 role,
2347 status: MessageStatus::Done,
2348 ambient_context: message.ambient_context.clone(),
2349 },
2350 );
2351
2352 let new_messages =
2353 if range.start == range.end || range.start == message.offset_range.start {
2354 (None, Some(suffix))
2355 } else {
2356 let mut prefix_end = None;
2357 if range.start > message.offset_range.start
2358 && range.end < message.offset_range.end - 1
2359 {
2360 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
2361 prefix_end = Some(range.start + 1);
2362 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
2363 == Some('\n')
2364 {
2365 prefix_end = Some(range.start);
2366 }
2367 }
2368
2369 let selection = if let Some(prefix_end) = prefix_end {
2370 cx.emit(ConversationEvent::MessagesEdited);
2371 MessageAnchor {
2372 id: MessageId(post_inc(&mut self.next_message_id.0)),
2373 start: self.buffer.read(cx).anchor_before(prefix_end),
2374 }
2375 } else {
2376 self.buffer.update(cx, |buffer, cx| {
2377 buffer.edit([(range.start..range.start, "\n")], None, cx)
2378 });
2379 edited_buffer = true;
2380 MessageAnchor {
2381 id: MessageId(post_inc(&mut self.next_message_id.0)),
2382 start: self.buffer.read(cx).anchor_before(range.end + 1),
2383 }
2384 };
2385
2386 self.message_anchors
2387 .insert(message.index_range.end + 1, selection.clone());
2388 self.messages_metadata.insert(
2389 selection.id,
2390 MessageMetadata {
2391 role,
2392 status: MessageStatus::Done,
2393 ambient_context: message.ambient_context,
2394 },
2395 );
2396 (Some(selection), Some(suffix))
2397 };
2398
2399 if !edited_buffer {
2400 cx.emit(ConversationEvent::MessagesEdited);
2401 }
2402 new_messages
2403 } else {
2404 (None, None)
2405 }
2406 }
2407
2408 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
2409 if self.message_anchors.len() >= 2 && self.summary.is_none() {
2410 if !CompletionProvider::global(cx).is_authenticated() {
2411 return;
2412 }
2413
2414 let messages = self
2415 .messages(cx)
2416 .take(2)
2417 .map(|message| message.to_request_message(self.buffer.read(cx)))
2418 .chain(Some(LanguageModelRequestMessage {
2419 role: Role::User,
2420 content: "Summarize the conversation into a short title without punctuation"
2421 .into(),
2422 }));
2423 let request = LanguageModelRequest {
2424 model: self.model.clone(),
2425 messages: messages.collect(),
2426 stop: vec![],
2427 temperature: 1.0,
2428 };
2429
2430 let stream = CompletionProvider::global(cx).complete(request);
2431 self.pending_summary = cx.spawn(|this, mut cx| {
2432 async move {
2433 let mut messages = stream.await?;
2434
2435 while let Some(message) = messages.next().await {
2436 let text = message?;
2437 this.update(&mut cx, |this, cx| {
2438 this.summary
2439 .get_or_insert(Default::default())
2440 .text
2441 .push_str(&text);
2442 cx.emit(ConversationEvent::SummaryChanged);
2443 })?;
2444 }
2445
2446 this.update(&mut cx, |this, cx| {
2447 if let Some(summary) = this.summary.as_mut() {
2448 summary.done = true;
2449 cx.emit(ConversationEvent::SummaryChanged);
2450 }
2451 })?;
2452
2453 anyhow::Ok(())
2454 }
2455 .log_err()
2456 });
2457 }
2458 }
2459
2460 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
2461 self.messages_for_offsets([offset], cx).pop()
2462 }
2463
2464 fn messages_for_offsets(
2465 &self,
2466 offsets: impl IntoIterator<Item = usize>,
2467 cx: &AppContext,
2468 ) -> Vec<Message> {
2469 let mut result = Vec::new();
2470
2471 let mut messages = self.messages(cx).peekable();
2472 let mut offsets = offsets.into_iter().peekable();
2473 let mut current_message = messages.next();
2474 while let Some(offset) = offsets.next() {
2475 // Locate the message that contains the offset.
2476 while current_message.as_ref().map_or(false, |message| {
2477 !message.offset_range.contains(&offset) && messages.peek().is_some()
2478 }) {
2479 current_message = messages.next();
2480 }
2481 let Some(message) = current_message.as_ref() else {
2482 break;
2483 };
2484
2485 // Skip offsets that are in the same message.
2486 while offsets.peek().map_or(false, |offset| {
2487 message.offset_range.contains(offset) || messages.peek().is_none()
2488 }) {
2489 offsets.next();
2490 }
2491
2492 result.push(message.clone());
2493 }
2494 result
2495 }
2496
2497 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
2498 let buffer = self.buffer.read(cx);
2499 let mut slash_command_calls = self
2500 .slash_command_calls
2501 .iter()
2502 .map(|call| {
2503 if let Some(output) = &call.output_range {
2504 call.source_range.start.to_offset(buffer)..output.start.to_offset(buffer)
2505 } else {
2506 call.source_range.to_offset(buffer)
2507 }
2508 })
2509 .peekable();
2510 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
2511 iter::from_fn(move || {
2512 if let Some((start_ix, message_anchor)) = message_anchors.next() {
2513 let metadata = self.messages_metadata.get(&message_anchor.id)?;
2514 let message_start = message_anchor.start.to_offset(buffer);
2515 let mut message_end = None;
2516 let mut end_ix = start_ix;
2517 while let Some((_, next_message)) = message_anchors.peek() {
2518 if next_message.start.is_valid(buffer) {
2519 message_end = Some(next_message.start);
2520 break;
2521 } else {
2522 end_ix += 1;
2523 message_anchors.next();
2524 }
2525 }
2526 let message_end = message_end
2527 .unwrap_or(language::Anchor::MAX)
2528 .to_offset(buffer);
2529
2530 let mut slash_command_ranges = Vec::new();
2531 while let Some(call_range) = slash_command_calls.peek() {
2532 if call_range.end <= message_end {
2533 slash_command_ranges.push(slash_command_calls.next().unwrap());
2534 } else {
2535 break;
2536 }
2537 }
2538
2539 return Some(Message {
2540 index_range: start_ix..end_ix,
2541 offset_range: message_start..message_end,
2542 id: message_anchor.id,
2543 anchor: message_anchor.start,
2544 role: metadata.role,
2545 status: metadata.status.clone(),
2546 slash_command_ranges,
2547 ambient_context: metadata.ambient_context.clone(),
2548 });
2549 }
2550 None
2551 })
2552 }
2553
2554 fn save(
2555 &mut self,
2556 debounce: Option<Duration>,
2557 fs: Arc<dyn Fs>,
2558 cx: &mut ModelContext<Conversation>,
2559 ) {
2560 self.pending_save = cx.spawn(|this, mut cx| async move {
2561 if let Some(debounce) = debounce {
2562 cx.background_executor().timer(debounce).await;
2563 }
2564
2565 let (old_path, summary) = this.read_with(&cx, |this, _| {
2566 let path = this.path.clone();
2567 let summary = if let Some(summary) = this.summary.as_ref() {
2568 if summary.done {
2569 Some(summary.text.clone())
2570 } else {
2571 None
2572 }
2573 } else {
2574 None
2575 };
2576 (path, summary)
2577 })?;
2578
2579 if let Some(summary) = summary {
2580 let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
2581 let path = if let Some(old_path) = old_path {
2582 old_path
2583 } else {
2584 let mut discriminant = 1;
2585 let mut new_path;
2586 loop {
2587 new_path = CONVERSATIONS_DIR.join(&format!(
2588 "{} - {}.zed.json",
2589 summary.trim(),
2590 discriminant
2591 ));
2592 if fs.is_file(&new_path).await {
2593 discriminant += 1;
2594 } else {
2595 break;
2596 }
2597 }
2598 new_path
2599 };
2600
2601 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
2602 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
2603 .await?;
2604 this.update(&mut cx, |this, _| this.path = Some(path))?;
2605 }
2606
2607 Ok(())
2608 });
2609 }
2610}
2611
2612#[derive(Debug)]
2613enum EditParsingState {
2614 None,
2615 InOldText {
2616 path: PathBuf,
2617 start_offset: usize,
2618 old_text_start_offset: usize,
2619 },
2620 InNewText {
2621 path: PathBuf,
2622 start_offset: usize,
2623 old_text_range: Range<usize>,
2624 new_text_start_offset: usize,
2625 },
2626}
2627
2628#[derive(Clone, Debug, PartialEq)]
2629struct EditSuggestion {
2630 source_range: Range<language::Anchor>,
2631 full_path: PathBuf,
2632}
2633
2634struct ParsedEditSuggestion {
2635 path: PathBuf,
2636 outer_range: Range<usize>,
2637 old_text_range: Range<usize>,
2638 new_text_range: Range<usize>,
2639}
2640
2641fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSuggestion> {
2642 let mut state = EditParsingState::None;
2643 loop {
2644 let offset = lines.offset();
2645 let message_line = lines.next()?;
2646 match state {
2647 EditParsingState::None => {
2648 if let Some(rest) = message_line.strip_prefix("```edit ") {
2649 let path = rest.trim();
2650 if !path.is_empty() {
2651 state = EditParsingState::InOldText {
2652 path: PathBuf::from(path),
2653 start_offset: offset,
2654 old_text_start_offset: lines.offset(),
2655 };
2656 }
2657 }
2658 }
2659 EditParsingState::InOldText {
2660 path,
2661 start_offset,
2662 old_text_start_offset,
2663 } => {
2664 if message_line == "---" {
2665 state = EditParsingState::InNewText {
2666 path,
2667 start_offset,
2668 old_text_range: old_text_start_offset..offset,
2669 new_text_start_offset: lines.offset(),
2670 };
2671 } else {
2672 state = EditParsingState::InOldText {
2673 path,
2674 start_offset,
2675 old_text_start_offset,
2676 };
2677 }
2678 }
2679 EditParsingState::InNewText {
2680 path,
2681 start_offset,
2682 old_text_range,
2683 new_text_start_offset,
2684 } => {
2685 if message_line == "```" {
2686 return Some(ParsedEditSuggestion {
2687 path,
2688 outer_range: start_offset..offset + "```".len(),
2689 old_text_range,
2690 new_text_range: new_text_start_offset..offset,
2691 });
2692 } else {
2693 state = EditParsingState::InNewText {
2694 path,
2695 start_offset,
2696 old_text_range,
2697 new_text_start_offset,
2698 };
2699 }
2700 }
2701 }
2702 }
2703}
2704
2705struct SlashCommandCall {
2706 source_range: Range<language::Anchor>,
2707 output_range: Option<Range<language::Anchor>>,
2708 name: String,
2709 argument: Option<String>,
2710 should_rerun: bool,
2711 _invalidate: Task<()>,
2712 _command_cleanup: SlashCommandCleanup,
2713}
2714
2715struct PendingCompletion {
2716 id: usize,
2717 _task: Task<()>,
2718}
2719
2720enum ConversationEditorEvent {
2721 TabContentChanged,
2722}
2723
2724#[derive(Copy, Clone, Debug, PartialEq)]
2725struct ScrollPosition {
2726 offset_before_cursor: gpui::Point<f32>,
2727 cursor: Anchor,
2728}
2729
2730struct ConversationEditor {
2731 conversation: Model<Conversation>,
2732 fs: Arc<dyn Fs>,
2733 workspace: WeakView<Workspace>,
2734 editor: View<Editor>,
2735 flap_ids: HashMap<Range<language::Anchor>, FlapId>,
2736 blocks: HashSet<BlockId>,
2737 scroll_position: Option<ScrollPosition>,
2738 _subscriptions: Vec<Subscription>,
2739}
2740
2741impl ConversationEditor {
2742 fn new(
2743 model: LanguageModel,
2744 language_registry: Arc<LanguageRegistry>,
2745 slash_command_registry: Arc<SlashCommandRegistry>,
2746 fs: Arc<dyn Fs>,
2747 workspace: View<Workspace>,
2748 cx: &mut ViewContext<Self>,
2749 ) -> Self {
2750 let telemetry = workspace.read(cx).client().telemetry().clone();
2751 let project = workspace.read(cx).project().clone();
2752 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx);
2753
2754 let conversation = cx.new_model(|cx| {
2755 Conversation::new(
2756 model,
2757 language_registry,
2758 slash_command_registry,
2759 Some(telemetry),
2760 Some(lsp_adapter_delegate),
2761 cx,
2762 )
2763 });
2764 Self::for_conversation(conversation, fs, workspace, cx)
2765 }
2766
2767 fn for_conversation(
2768 conversation: Model<Conversation>,
2769 fs: Arc<dyn Fs>,
2770 workspace: View<Workspace>,
2771 cx: &mut ViewContext<Self>,
2772 ) -> Self {
2773 let command_registry = conversation.read(cx).slash_command_registry.clone();
2774 let completion_provider = SlashCommandCompletionProvider::new(command_registry);
2775
2776 let editor = cx.new_view(|cx| {
2777 let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2778 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2779 editor.set_show_line_numbers(false, cx);
2780 editor.set_show_git_diff_gutter(false, cx);
2781 editor.set_show_code_actions(false, cx);
2782 editor.set_show_wrap_guides(false, cx);
2783 editor.set_show_indent_guides(false, cx);
2784 editor.set_completion_provider(Box::new(completion_provider));
2785 editor
2786 });
2787
2788 let _subscriptions = vec![
2789 cx.observe(&conversation, |_, _, cx| cx.notify()),
2790 cx.subscribe(&conversation, Self::handle_conversation_event),
2791 cx.subscribe(&editor, Self::handle_editor_event),
2792 cx.subscribe(&workspace, Self::handle_workspace_event),
2793 ];
2794
2795 let mut this = Self {
2796 conversation,
2797 editor,
2798 blocks: Default::default(),
2799 scroll_position: None,
2800 flap_ids: Default::default(),
2801 fs,
2802 workspace: workspace.downgrade(),
2803 _subscriptions,
2804 };
2805 this.update_recent_editors(cx);
2806 this.update_message_headers(cx);
2807 this
2808 }
2809
2810 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2811 let cursors = self.cursors(cx);
2812
2813 let user_messages = self.conversation.update(cx, |conversation, cx| {
2814 let selected_messages = conversation
2815 .messages_for_offsets(cursors, cx)
2816 .into_iter()
2817 .map(|message| message.id)
2818 .collect();
2819 conversation.assist(selected_messages, cx)
2820 });
2821 let new_selections = user_messages
2822 .iter()
2823 .map(|message| {
2824 let cursor = message
2825 .start
2826 .to_offset(self.conversation.read(cx).buffer.read(cx));
2827 cursor..cursor
2828 })
2829 .collect::<Vec<_>>();
2830 if !new_selections.is_empty() {
2831 self.editor.update(cx, |editor, cx| {
2832 editor.change_selections(
2833 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2834 cx,
2835 |selections| selections.select_ranges(new_selections),
2836 );
2837 });
2838 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2839 cx.defer(|this, _| this.scroll_position = None);
2840 }
2841 }
2842
2843 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2844 if !self
2845 .conversation
2846 .update(cx, |conversation, _| conversation.cancel_last_assist())
2847 {
2848 cx.propagate();
2849 }
2850 }
2851
2852 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2853 let cursors = self.cursors(cx);
2854 self.conversation.update(cx, |conversation, cx| {
2855 let messages = conversation
2856 .messages_for_offsets(cursors, cx)
2857 .into_iter()
2858 .map(|message| message.id)
2859 .collect();
2860 conversation.cycle_message_roles(messages, cx)
2861 });
2862 }
2863
2864 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2865 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2866 selections
2867 .into_iter()
2868 .map(|selection| selection.head())
2869 .collect()
2870 }
2871
2872 fn handle_conversation_event(
2873 &mut self,
2874 _: Model<Conversation>,
2875 event: &ConversationEvent,
2876 cx: &mut ViewContext<Self>,
2877 ) {
2878 match event {
2879 ConversationEvent::MessagesEdited => {
2880 self.update_message_headers(cx);
2881 self.conversation.update(cx, |conversation, cx| {
2882 conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2883 });
2884 }
2885 ConversationEvent::EditSuggestionsChanged => {
2886 self.editor.update(cx, |editor, cx| {
2887 let buffer = editor.buffer().read(cx).snapshot(cx);
2888 let excerpt_id = *buffer.as_singleton().unwrap().0;
2889 let conversation = self.conversation.read(cx);
2890 let highlighted_rows = conversation
2891 .edit_suggestions
2892 .iter()
2893 .map(|suggestion| {
2894 let start = buffer
2895 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2896 .unwrap();
2897 let end = buffer
2898 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2899 .unwrap();
2900 start..=end
2901 })
2902 .collect::<Vec<_>>();
2903
2904 editor.clear_row_highlights::<EditSuggestion>();
2905 for range in highlighted_rows {
2906 editor.highlight_rows::<EditSuggestion>(
2907 range,
2908 Some(
2909 cx.theme()
2910 .colors()
2911 .editor_document_highlight_read_background,
2912 ),
2913 false,
2914 cx,
2915 );
2916 }
2917 });
2918 }
2919 ConversationEvent::SummaryChanged => {
2920 cx.emit(ConversationEditorEvent::TabContentChanged);
2921 self.conversation.update(cx, |conversation, cx| {
2922 conversation.save(None, self.fs.clone(), cx);
2923 });
2924 }
2925 ConversationEvent::StreamedCompletion => {
2926 self.editor.update(cx, |editor, cx| {
2927 if let Some(scroll_position) = self.scroll_position {
2928 let snapshot = editor.snapshot(cx);
2929 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2930 let scroll_top =
2931 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2932 editor.set_scroll_position(
2933 point(scroll_position.offset_before_cursor.x, scroll_top),
2934 cx,
2935 );
2936 }
2937 });
2938 }
2939 ConversationEvent::SlashCommandsChanged => {
2940 self.editor.update(cx, |editor, cx| {
2941 let buffer = editor.buffer().read(cx).snapshot(cx);
2942 let excerpt_id = *buffer.as_singleton().unwrap().0;
2943 let conversation = self.conversation.read(cx);
2944 let colors = cx.theme().colors();
2945 let highlighted_rows = conversation
2946 .slash_command_calls
2947 .iter()
2948 .map(|call| {
2949 let start = call.source_range.start;
2950 let end = if let Some(output) = &call.output_range {
2951 output.end
2952 } else {
2953 call.source_range.end
2954 };
2955 let start = buffer.anchor_in_excerpt(excerpt_id, start).unwrap();
2956 let end = buffer.anchor_in_excerpt(excerpt_id, end).unwrap();
2957 (
2958 start..=end,
2959 Some(colors.editor_document_highlight_read_background),
2960 )
2961 })
2962 .collect::<Vec<_>>();
2963
2964 editor.clear_row_highlights::<SlashCommandCall>();
2965 for (range, color) in highlighted_rows {
2966 editor.highlight_rows::<SlashCommandCall>(range, color, false, cx);
2967 }
2968 });
2969 }
2970 ConversationEvent::SlashCommandOutputAdded(range) => {
2971 self.editor.update(cx, |editor, cx| {
2972 let buffer = editor.buffer().read(cx).snapshot(cx);
2973 let excerpt_id = *buffer.as_singleton().unwrap().0;
2974 let start = buffer.anchor_in_excerpt(excerpt_id, range.start).unwrap();
2975 let end = buffer.anchor_in_excerpt(excerpt_id, range.end).unwrap();
2976 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2977
2978 let flap_id = editor
2979 .insert_flaps(
2980 [Flap::new(
2981 start..end,
2982 FoldPlaceholder {
2983 render: Arc::new(|_, _, _| Empty.into_any()),
2984 constrain_width: false,
2985 },
2986 render_slash_command_output_toggle,
2987 render_slash_command_output_trailer,
2988 )],
2989 cx,
2990 )
2991 .into_iter()
2992 .next()
2993 .unwrap();
2994 self.flap_ids.insert(range.clone(), flap_id);
2995 editor.fold_at(&FoldAt { buffer_row }, cx);
2996 });
2997 }
2998 ConversationEvent::SlashCommandOutputRemoved(range) => {
2999 if let Some(flap_id) = self.flap_ids.remove(range) {
3000 self.editor.update(cx, |editor, cx| {
3001 editor.remove_flaps([flap_id], cx);
3002 });
3003 }
3004 }
3005 }
3006 }
3007
3008 fn handle_editor_event(
3009 &mut self,
3010 _: View<Editor>,
3011 event: &EditorEvent,
3012 cx: &mut ViewContext<Self>,
3013 ) {
3014 match event {
3015 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
3016 let cursor_scroll_position = self.cursor_scroll_position(cx);
3017 if *autoscroll {
3018 self.scroll_position = cursor_scroll_position;
3019 } else if self.scroll_position != cursor_scroll_position {
3020 self.scroll_position = None;
3021 }
3022 }
3023 EditorEvent::SelectionsChanged { .. } => {
3024 self.scroll_position = self.cursor_scroll_position(cx);
3025 }
3026 _ => {}
3027 }
3028 }
3029
3030 fn handle_workspace_event(
3031 &mut self,
3032 _: View<Workspace>,
3033 event: &WorkspaceEvent,
3034 cx: &mut ViewContext<Self>,
3035 ) {
3036 match event {
3037 WorkspaceEvent::ActiveItemChanged
3038 | WorkspaceEvent::ItemAdded
3039 | WorkspaceEvent::ItemRemoved
3040 | WorkspaceEvent::PaneAdded(_)
3041 | WorkspaceEvent::PaneRemoved => self.update_recent_editors(cx),
3042 _ => {}
3043 }
3044 }
3045
3046 fn update_recent_editors(&mut self, cx: &mut ViewContext<ConversationEditor>) {
3047 let Some(workspace) = self.workspace.upgrade() else {
3048 return;
3049 };
3050
3051 let mut timestamps_by_entity_id = HashMap::default();
3052 for pane in workspace.read(cx).panes() {
3053 let pane = pane.read(cx);
3054 for entry in pane.activation_history() {
3055 timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
3056 }
3057 }
3058
3059 let mut timestamps_by_buffer = HashMap::default();
3060 for editor in workspace.read(cx).items_of_type::<Editor>(cx) {
3061 let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
3062 continue;
3063 };
3064
3065 let new_timestamp = timestamps_by_entity_id
3066 .get(&editor.entity_id())
3067 .copied()
3068 .unwrap_or_default();
3069 let timestamp = timestamps_by_buffer.entry(buffer).or_insert(new_timestamp);
3070 *timestamp = cmp::max(*timestamp, new_timestamp);
3071 }
3072
3073 let mut recent_buffers = timestamps_by_buffer.into_iter().collect::<Vec<_>>();
3074 recent_buffers.sort_unstable_by_key(|(_, timestamp)| *timestamp);
3075 if recent_buffers.len() > MAX_RECENT_BUFFERS {
3076 let excess = recent_buffers.len() - MAX_RECENT_BUFFERS;
3077 recent_buffers.drain(..excess);
3078 }
3079
3080 self.conversation.update(cx, |conversation, cx| {
3081 conversation
3082 .set_recent_buffers(recent_buffers.into_iter().map(|(buffer, _)| buffer), cx);
3083 });
3084 }
3085
3086 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
3087 self.editor.update(cx, |editor, cx| {
3088 let snapshot = editor.snapshot(cx);
3089 let cursor = editor.selections.newest_anchor().head();
3090 let cursor_row = cursor
3091 .to_display_point(&snapshot.display_snapshot)
3092 .row()
3093 .as_f32();
3094 let scroll_position = editor
3095 .scroll_manager
3096 .anchor()
3097 .scroll_position(&snapshot.display_snapshot);
3098
3099 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
3100 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
3101 Some(ScrollPosition {
3102 cursor,
3103 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
3104 })
3105 } else {
3106 None
3107 }
3108 })
3109 }
3110
3111 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
3112 let project = self
3113 .workspace
3114 .update(cx, |workspace, _cx| workspace.project().downgrade())
3115 .unwrap();
3116
3117 self.editor.update(cx, |editor, cx| {
3118 let buffer = editor.buffer().read(cx).snapshot(cx);
3119 let excerpt_id = *buffer.as_singleton().unwrap().0;
3120 let old_blocks = std::mem::take(&mut self.blocks);
3121 let new_blocks = self
3122 .conversation
3123 .read(cx)
3124 .messages(cx)
3125 .enumerate()
3126 .map(|(ix, message)| BlockProperties {
3127 position: buffer
3128 .anchor_in_excerpt(excerpt_id, message.anchor)
3129 .unwrap(),
3130 height: 2,
3131 style: BlockStyle::Sticky,
3132 render: Box::new({
3133 let fs = self.fs.clone();
3134 let project = project.clone();
3135 let conversation = self.conversation.clone();
3136 move |cx| {
3137 let message_id = message.id;
3138 let sender = ButtonLike::new("role")
3139 .style(ButtonStyle::Filled)
3140 .child(match message.role {
3141 Role::User => Label::new("You").color(Color::Default),
3142 Role::Assistant => Label::new("Assistant").color(Color::Info),
3143 Role::System => Label::new("System").color(Color::Warning),
3144 })
3145 .tooltip(|cx| {
3146 Tooltip::with_meta(
3147 "Toggle message role",
3148 None,
3149 "Available roles: You (User), Assistant, System",
3150 cx,
3151 )
3152 })
3153 .on_click({
3154 let conversation = conversation.clone();
3155 move |_, cx| {
3156 conversation.update(cx, |conversation, cx| {
3157 conversation.cycle_message_roles(
3158 HashSet::from_iter(Some(message_id)),
3159 cx,
3160 )
3161 })
3162 }
3163 });
3164
3165 h_flex()
3166 .id(("message_header", message_id.0))
3167 .pl(cx.gutter_dimensions.width)
3168 .h_11()
3169 .w_full()
3170 .relative()
3171 .gap_1()
3172 .child(sender)
3173 .children(
3174 if let MessageStatus::Error(error) = message.status.clone() {
3175 Some(
3176 div()
3177 .id("error")
3178 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
3179 .child(Icon::new(IconName::XCircle)),
3180 )
3181 } else {
3182 None
3183 },
3184 )
3185 .children((ix == 0).then(|| {
3186 div()
3187 .h_flex()
3188 .flex_1()
3189 .justify_end()
3190 .pr_4()
3191 .gap_1()
3192 .child(
3193 IconButton::new("include_file", IconName::File)
3194 .icon_size(IconSize::Small)
3195 .selected(
3196 conversation
3197 .read(cx)
3198 .ambient_context
3199 .recent_buffers
3200 .enabled,
3201 )
3202 .on_click({
3203 let conversation = conversation.downgrade();
3204 move |_, cx| {
3205 conversation
3206 .update(cx, |conversation, cx| {
3207 conversation
3208 .toggle_recent_buffers(cx);
3209 })
3210 .ok();
3211 }
3212 })
3213 .tooltip(|cx| {
3214 Tooltip::text("Include Open Files", cx)
3215 }),
3216 )
3217 .child(
3218 IconButton::new(
3219 "include_current_project",
3220 IconName::FileTree,
3221 )
3222 .icon_size(IconSize::Small)
3223 .selected(
3224 conversation
3225 .read(cx)
3226 .ambient_context
3227 .current_project
3228 .enabled,
3229 )
3230 .on_click({
3231 let fs = fs.clone();
3232 let project = project.clone();
3233 let conversation = conversation.downgrade();
3234 move |_, cx| {
3235 let fs = fs.clone();
3236 let project = project.clone();
3237 conversation
3238 .update(cx, |conversation, cx| {
3239 conversation
3240 .toggle_current_project_context(
3241 fs, project, cx,
3242 );
3243 })
3244 .ok();
3245 }
3246 })
3247 .tooltip(
3248 |cx| Tooltip::text("Include Current Project", cx),
3249 ),
3250 )
3251 .into_any()
3252 }))
3253 .into_any_element()
3254 }
3255 }),
3256 disposition: BlockDisposition::Above,
3257 })
3258 .collect::<Vec<_>>();
3259
3260 editor.remove_blocks(old_blocks, None, cx);
3261 let ids = editor.insert_blocks(new_blocks, None, cx);
3262 self.blocks = HashSet::from_iter(ids);
3263 });
3264 }
3265
3266 fn quote_selection(
3267 workspace: &mut Workspace,
3268 _: &QuoteSelection,
3269 cx: &mut ViewContext<Workspace>,
3270 ) {
3271 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3272 return;
3273 };
3274 let Some(editor) = workspace
3275 .active_item(cx)
3276 .and_then(|item| item.act_as::<Editor>(cx))
3277 else {
3278 return;
3279 };
3280
3281 let editor = editor.read(cx);
3282 let range = editor.selections.newest::<usize>(cx).range();
3283 let buffer = editor.buffer().read(cx).snapshot(cx);
3284 let start_language = buffer.language_at(range.start);
3285 let end_language = buffer.language_at(range.end);
3286 let language_name = if start_language == end_language {
3287 start_language.map(|language| language.code_fence_block_name())
3288 } else {
3289 None
3290 };
3291 let language_name = language_name.as_deref().unwrap_or("");
3292
3293 let selected_text = buffer.text_for_range(range).collect::<String>();
3294 let text = if selected_text.is_empty() {
3295 None
3296 } else {
3297 Some(if language_name == "markdown" {
3298 selected_text
3299 .lines()
3300 .map(|line| format!("> {}", line))
3301 .collect::<Vec<_>>()
3302 .join("\n")
3303 } else {
3304 format!("```{language_name}\n{selected_text}\n```")
3305 })
3306 };
3307
3308 // Activate the panel
3309 if !panel.focus_handle(cx).contains_focused(cx) {
3310 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3311 }
3312
3313 if let Some(text) = text {
3314 panel.update(cx, |_, cx| {
3315 // Wait to create a new conversation until the workspace is no longer
3316 // being updated.
3317 cx.defer(move |panel, cx| {
3318 if let Some(conversation) = panel
3319 .active_conversation_editor()
3320 .cloned()
3321 .or_else(|| panel.new_conversation(cx))
3322 {
3323 conversation.update(cx, |conversation, cx| {
3324 conversation
3325 .editor
3326 .update(cx, |editor, cx| editor.insert(&text, cx))
3327 });
3328 };
3329 });
3330 });
3331 }
3332 }
3333
3334 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3335 let editor = self.editor.read(cx);
3336 let conversation = self.conversation.read(cx);
3337 if editor.selections.count() == 1 {
3338 let selection = editor.selections.newest::<usize>(cx);
3339 let mut copied_text = String::new();
3340 let mut spanned_messages = 0;
3341 for message in conversation.messages(cx) {
3342 if message.offset_range.start >= selection.range().end {
3343 break;
3344 } else if message.offset_range.end >= selection.range().start {
3345 let range = cmp::max(message.offset_range.start, selection.range().start)
3346 ..cmp::min(message.offset_range.end, selection.range().end);
3347 if !range.is_empty() {
3348 spanned_messages += 1;
3349 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
3350 for chunk in conversation.buffer.read(cx).text_for_range(range) {
3351 copied_text.push_str(chunk);
3352 }
3353 copied_text.push('\n');
3354 }
3355 }
3356 }
3357
3358 if spanned_messages > 1 {
3359 cx.write_to_clipboard(ClipboardItem::new(copied_text));
3360 return;
3361 }
3362 }
3363
3364 cx.propagate();
3365 }
3366
3367 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3368 self.conversation.update(cx, |conversation, cx| {
3369 let selections = self.editor.read(cx).selections.disjoint_anchors();
3370 for selection in selections.as_ref() {
3371 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3372 let range = selection
3373 .map(|endpoint| endpoint.to_offset(&buffer))
3374 .range();
3375 conversation.split_message(range, cx);
3376 }
3377 });
3378 }
3379
3380 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
3381 struct Edit {
3382 old_text: String,
3383 new_text: String,
3384 }
3385
3386 let conversation = self.conversation.read(cx);
3387 let conversation_buffer = conversation.buffer.read(cx);
3388 let conversation_buffer_snapshot = conversation_buffer.snapshot();
3389
3390 let selections = self.editor.read(cx).selections.disjoint_anchors();
3391 let mut selections = selections.iter().peekable();
3392 let selected_suggestions = conversation.edit_suggestions.iter().filter(|suggestion| {
3393 while let Some(selection) = selections.peek() {
3394 if selection
3395 .end
3396 .text_anchor
3397 .cmp(&suggestion.source_range.start, conversation_buffer)
3398 .is_lt()
3399 {
3400 selections.next();
3401 continue;
3402 }
3403 if selection
3404 .start
3405 .text_anchor
3406 .cmp(&suggestion.source_range.end, conversation_buffer)
3407 .is_gt()
3408 {
3409 break;
3410 }
3411 return true;
3412 }
3413 false
3414 });
3415
3416 let mut suggestions_by_buffer =
3417 HashMap::<Model<Buffer>, (BufferSnapshot, Vec<Edit>)>::default();
3418 for suggestion in selected_suggestions {
3419 let offset = suggestion.source_range.start.to_offset(conversation_buffer);
3420 if let Some(message) = conversation.message_for_offset(offset, cx) {
3421 if let Some(buffer) = message
3422 .ambient_context
3423 .recent_buffers
3424 .source_buffers
3425 .iter()
3426 .find(|source_buffer| {
3427 source_buffer.full_path.as_ref() == Some(&suggestion.full_path)
3428 })
3429 {
3430 if let Some(buffer) = buffer.model.upgrade() {
3431 let (_, edits) = suggestions_by_buffer
3432 .entry(buffer.clone())
3433 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
3434
3435 let mut lines = conversation_buffer_snapshot
3436 .as_rope()
3437 .chunks_in_range(
3438 suggestion
3439 .source_range
3440 .to_offset(&conversation_buffer_snapshot),
3441 )
3442 .lines();
3443 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
3444 let old_text = conversation_buffer_snapshot
3445 .text_for_range(suggestion.old_text_range)
3446 .collect();
3447 let new_text = conversation_buffer_snapshot
3448 .text_for_range(suggestion.new_text_range)
3449 .collect();
3450 edits.push(Edit { old_text, new_text });
3451 }
3452 }
3453 }
3454 }
3455 }
3456
3457 cx.spawn(|this, mut cx| async move {
3458 let edits_by_buffer = cx
3459 .background_executor()
3460 .spawn(async move {
3461 let mut result = HashMap::default();
3462 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
3463 let edits =
3464 result
3465 .entry(buffer)
3466 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
3467 for suggestion in suggestions {
3468 if let Some(range) =
3469 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
3470 {
3471 let edit_start = snapshot.anchor_after(range.start);
3472 let edit_end = snapshot.anchor_before(range.end);
3473 if let Err(ix) = edits.binary_search_by(|(range, _)| {
3474 range.start.cmp(&edit_start, &snapshot)
3475 }) {
3476 edits.insert(
3477 ix,
3478 (edit_start..edit_end, suggestion.new_text.clone()),
3479 );
3480 }
3481 } else {
3482 log::info!(
3483 "assistant edit did not match any text in buffer {:?}",
3484 &suggestion.old_text
3485 );
3486 }
3487 }
3488 }
3489 result
3490 })
3491 .await;
3492
3493 let mut project_transaction = ProjectTransaction::default();
3494 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
3495 for (buffer_handle, edits) in edits_by_buffer {
3496 buffer_handle.update(cx, |buffer, cx| {
3497 buffer.start_transaction();
3498 buffer.edit(
3499 edits,
3500 Some(AutoindentMode::Block {
3501 original_indent_columns: Vec::new(),
3502 }),
3503 cx,
3504 );
3505 buffer.end_transaction(cx);
3506 if let Some(transaction) = buffer.finalize_last_transaction() {
3507 project_transaction
3508 .0
3509 .insert(buffer_handle.clone(), transaction.clone());
3510 }
3511 });
3512 }
3513
3514 (
3515 this.editor.downgrade(),
3516 this.workspace.clone(),
3517 this.title(cx),
3518 )
3519 })?;
3520
3521 Editor::open_project_transaction(
3522 &editor,
3523 workspace,
3524 project_transaction,
3525 format!("Edits from {}", title),
3526 cx,
3527 )
3528 .await
3529 })
3530 .detach_and_log_err(cx);
3531 }
3532
3533 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3534 self.conversation.update(cx, |conversation, cx| {
3535 conversation.save(None, self.fs.clone(), cx)
3536 });
3537 }
3538
3539 fn title(&self, cx: &AppContext) -> String {
3540 self.conversation
3541 .read(cx)
3542 .summary
3543 .as_ref()
3544 .map(|summary| summary.text.clone())
3545 .unwrap_or_else(|| "New Context".into())
3546 }
3547}
3548
3549impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
3550
3551impl Render for ConversationEditor {
3552 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3553 div()
3554 .key_context("ConversationEditor")
3555 .capture_action(cx.listener(ConversationEditor::cancel_last_assist))
3556 .capture_action(cx.listener(ConversationEditor::save))
3557 .capture_action(cx.listener(ConversationEditor::copy))
3558 .capture_action(cx.listener(ConversationEditor::cycle_message_role))
3559 .on_action(cx.listener(ConversationEditor::assist))
3560 .on_action(cx.listener(ConversationEditor::split))
3561 .on_action(cx.listener(ConversationEditor::apply_edit))
3562 .size_full()
3563 .v_flex()
3564 .child(
3565 div()
3566 .flex_grow()
3567 .bg(cx.theme().colors().editor_background)
3568 .child(self.editor.clone()),
3569 )
3570 }
3571}
3572
3573impl FocusableView for ConversationEditor {
3574 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3575 self.editor.focus_handle(cx)
3576 }
3577}
3578
3579#[derive(Clone, Debug)]
3580struct MessageAnchor {
3581 id: MessageId,
3582 start: language::Anchor,
3583}
3584
3585#[derive(Clone, Debug)]
3586pub struct Message {
3587 offset_range: Range<usize>,
3588 index_range: Range<usize>,
3589 id: MessageId,
3590 anchor: language::Anchor,
3591 role: Role,
3592 status: MessageStatus,
3593 slash_command_ranges: Vec<Range<usize>>,
3594 ambient_context: AmbientContextSnapshot,
3595}
3596
3597impl Message {
3598 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
3599 let mut content = text_in_range_omitting_ranges(
3600 buffer.as_rope(),
3601 self.offset_range.clone(),
3602 &self.slash_command_ranges,
3603 );
3604 content.truncate(content.trim_end().len());
3605 LanguageModelRequestMessage {
3606 role: self.role,
3607 content,
3608 }
3609 }
3610}
3611
3612enum InlineAssistantEvent {
3613 Confirmed {
3614 prompt: String,
3615 include_conversation: bool,
3616 },
3617 Canceled,
3618 Dismissed,
3619 IncludeConversationToggled {
3620 include_conversation: bool,
3621 },
3622}
3623
3624struct InlineAssistant {
3625 id: usize,
3626 prompt_editor: View<Editor>,
3627 confirmed: bool,
3628 show_include_conversation: bool,
3629 include_conversation: bool,
3630 measurements: Arc<Mutex<BlockMeasurements>>,
3631 prompt_history: VecDeque<String>,
3632 prompt_history_ix: Option<usize>,
3633 pending_prompt: String,
3634 codegen: Model<Codegen>,
3635 _subscriptions: Vec<Subscription>,
3636}
3637
3638impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
3639
3640impl Render for InlineAssistant {
3641 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3642 let measurements = *self.measurements.lock();
3643 h_flex()
3644 .w_full()
3645 .py_2()
3646 .border_y_1()
3647 .border_color(cx.theme().colors().border)
3648 .on_action(cx.listener(Self::confirm))
3649 .on_action(cx.listener(Self::cancel))
3650 .on_action(cx.listener(Self::toggle_include_conversation))
3651 .on_action(cx.listener(Self::move_up))
3652 .on_action(cx.listener(Self::move_down))
3653 .child(
3654 h_flex()
3655 .justify_center()
3656 .w(measurements.gutter_width)
3657 .children(self.show_include_conversation.then(|| {
3658 IconButton::new("include_conversation", IconName::Ai)
3659 .on_click(cx.listener(|this, _, cx| {
3660 this.toggle_include_conversation(&ToggleIncludeConversation, cx)
3661 }))
3662 .selected(self.include_conversation)
3663 .tooltip(|cx| {
3664 Tooltip::for_action(
3665 "Include Conversation",
3666 &ToggleIncludeConversation,
3667 cx,
3668 )
3669 })
3670 }))
3671 .children(if let Some(error) = self.codegen.read(cx).error() {
3672 let error_message = SharedString::from(error.to_string());
3673 Some(
3674 div()
3675 .id("error")
3676 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
3677 .child(Icon::new(IconName::XCircle).color(Color::Error)),
3678 )
3679 } else {
3680 None
3681 }),
3682 )
3683 .child(
3684 h_flex()
3685 .w_full()
3686 .ml(measurements.anchor_x - measurements.gutter_width)
3687 .child(self.render_prompt_editor(cx)),
3688 )
3689 }
3690}
3691
3692impl FocusableView for InlineAssistant {
3693 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3694 self.prompt_editor.focus_handle(cx)
3695 }
3696}
3697
3698impl InlineAssistant {
3699 #[allow(clippy::too_many_arguments)]
3700 fn new(
3701 id: usize,
3702 measurements: Arc<Mutex<BlockMeasurements>>,
3703 show_include_conversation: bool,
3704 include_conversation: bool,
3705 prompt_history: VecDeque<String>,
3706 codegen: Model<Codegen>,
3707 cx: &mut ViewContext<Self>,
3708 ) -> Self {
3709 let prompt_editor = cx.new_view(|cx| {
3710 let mut editor = Editor::single_line(cx);
3711 let placeholder = match codegen.read(cx).kind() {
3712 CodegenKind::Transform { .. } => "Enter transformation prompt…",
3713 CodegenKind::Generate { .. } => "Enter generation prompt…",
3714 };
3715 editor.set_placeholder_text(placeholder, cx);
3716 editor
3717 });
3718 cx.focus_view(&prompt_editor);
3719
3720 let subscriptions = vec![
3721 cx.observe(&codegen, Self::handle_codegen_changed),
3722 cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
3723 ];
3724
3725 Self {
3726 id,
3727 prompt_editor,
3728 confirmed: false,
3729 show_include_conversation,
3730 include_conversation,
3731 measurements,
3732 prompt_history,
3733 prompt_history_ix: None,
3734 pending_prompt: String::new(),
3735 codegen,
3736 _subscriptions: subscriptions,
3737 }
3738 }
3739
3740 fn handle_prompt_editor_events(
3741 &mut self,
3742 _: View<Editor>,
3743 event: &EditorEvent,
3744 cx: &mut ViewContext<Self>,
3745 ) {
3746 if let EditorEvent::Edited = event {
3747 self.pending_prompt = self.prompt_editor.read(cx).text(cx);
3748 cx.notify();
3749 }
3750 }
3751
3752 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
3753 let is_read_only = !self.codegen.read(cx).idle();
3754 self.prompt_editor.update(cx, |editor, cx| {
3755 let was_read_only = editor.read_only(cx);
3756 if was_read_only != is_read_only {
3757 if is_read_only {
3758 editor.set_read_only(true);
3759 } else {
3760 self.confirmed = false;
3761 editor.set_read_only(false);
3762 }
3763 }
3764 });
3765 cx.notify();
3766 }
3767
3768 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
3769 cx.emit(InlineAssistantEvent::Canceled);
3770 }
3771
3772 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
3773 if self.confirmed {
3774 cx.emit(InlineAssistantEvent::Dismissed);
3775 } else {
3776 let prompt = self.prompt_editor.read(cx).text(cx);
3777 self.prompt_editor
3778 .update(cx, |editor, _cx| editor.set_read_only(true));
3779 cx.emit(InlineAssistantEvent::Confirmed {
3780 prompt,
3781 include_conversation: self.include_conversation,
3782 });
3783 self.confirmed = true;
3784 cx.notify();
3785 }
3786 }
3787
3788 fn toggle_include_conversation(
3789 &mut self,
3790 _: &ToggleIncludeConversation,
3791 cx: &mut ViewContext<Self>,
3792 ) {
3793 self.include_conversation = !self.include_conversation;
3794 cx.emit(InlineAssistantEvent::IncludeConversationToggled {
3795 include_conversation: self.include_conversation,
3796 });
3797 cx.notify();
3798 }
3799
3800 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
3801 if let Some(ix) = self.prompt_history_ix {
3802 if ix > 0 {
3803 self.prompt_history_ix = Some(ix - 1);
3804 let prompt = self.prompt_history[ix - 1].clone();
3805 self.set_prompt(&prompt, cx);
3806 }
3807 } else if !self.prompt_history.is_empty() {
3808 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
3809 let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
3810 self.set_prompt(&prompt, cx);
3811 }
3812 }
3813
3814 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
3815 if let Some(ix) = self.prompt_history_ix {
3816 if ix < self.prompt_history.len() - 1 {
3817 self.prompt_history_ix = Some(ix + 1);
3818 let prompt = self.prompt_history[ix + 1].clone();
3819 self.set_prompt(&prompt, cx);
3820 } else {
3821 self.prompt_history_ix = None;
3822 let pending_prompt = self.pending_prompt.clone();
3823 self.set_prompt(&pending_prompt, cx);
3824 }
3825 }
3826 }
3827
3828 fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
3829 self.prompt_editor.update(cx, |editor, cx| {
3830 editor.buffer().update(cx, |buffer, cx| {
3831 let len = buffer.len(cx);
3832 buffer.edit([(0..len, prompt)], None, cx);
3833 });
3834 });
3835 }
3836
3837 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3838 let settings = ThemeSettings::get_global(cx);
3839 let text_style = TextStyle {
3840 color: if self.prompt_editor.read(cx).read_only(cx) {
3841 cx.theme().colors().text_disabled
3842 } else {
3843 cx.theme().colors().text
3844 },
3845 font_family: settings.ui_font.family.clone(),
3846 font_features: settings.ui_font.features.clone(),
3847 font_size: rems(0.875).into(),
3848 font_weight: FontWeight::NORMAL,
3849 font_style: FontStyle::Normal,
3850 line_height: relative(1.3),
3851 background_color: None,
3852 underline: None,
3853 strikethrough: None,
3854 white_space: WhiteSpace::Normal,
3855 };
3856 EditorElement::new(
3857 &self.prompt_editor,
3858 EditorStyle {
3859 background: cx.theme().colors().editor_background,
3860 local_player: cx.theme().players().local(),
3861 text: text_style,
3862 ..Default::default()
3863 },
3864 )
3865 }
3866}
3867
3868// This wouldn't need to exist if we could pass parameters when rendering child views.
3869#[derive(Copy, Clone, Default)]
3870struct BlockMeasurements {
3871 anchor_x: Pixels,
3872 gutter_width: Pixels,
3873}
3874
3875struct PendingInlineAssist {
3876 editor: WeakView<Editor>,
3877 inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
3878 codegen: Model<Codegen>,
3879 _subscriptions: Vec<Subscription>,
3880 project: WeakModel<Project>,
3881}
3882
3883type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3884
3885fn render_slash_command_output_toggle(
3886 row: MultiBufferRow,
3887 is_folded: bool,
3888 fold: ToggleFold,
3889 _cx: &mut WindowContext,
3890) -> AnyElement {
3891 IconButton::new(
3892 ("slash-command-output-fold-indicator", row.0),
3893 ui::IconName::ChevronDown,
3894 )
3895 .on_click(move |_e, cx| fold(!is_folded, cx))
3896 .icon_color(ui::Color::Muted)
3897 .icon_size(ui::IconSize::Small)
3898 .selected(is_folded)
3899 .selected_icon(ui::IconName::ChevronRight)
3900 .size(ui::ButtonSize::None)
3901 .into_any_element()
3902}
3903
3904fn render_slash_command_output_trailer(
3905 _row: MultiBufferRow,
3906 _is_folded: bool,
3907 _cx: &mut WindowContext,
3908) -> AnyElement {
3909 div().into_any_element()
3910}
3911
3912fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
3913 ranges.sort_unstable_by(|a, b| {
3914 a.start
3915 .cmp(&b.start, buffer)
3916 .then_with(|| b.end.cmp(&a.end, buffer))
3917 });
3918
3919 let mut ix = 0;
3920 while ix + 1 < ranges.len() {
3921 let b = ranges[ix + 1].clone();
3922 let a = &mut ranges[ix];
3923 if a.end.cmp(&b.start, buffer).is_gt() {
3924 if a.end.cmp(&b.end, buffer).is_lt() {
3925 a.end = b.end;
3926 }
3927 ranges.remove(ix + 1);
3928 } else {
3929 ix += 1;
3930 }
3931 }
3932}
3933
3934fn make_lsp_adapter_delegate(
3935 project: &Model<Project>,
3936 cx: &mut AppContext,
3937) -> Arc<dyn LspAdapterDelegate> {
3938 project.update(cx, |project, cx| {
3939 // TODO: Find the right worktree.
3940 let worktree = project
3941 .worktrees()
3942 .next()
3943 .expect("expected at least one worktree");
3944 ProjectLspAdapterDelegate::new(project, &worktree, cx)
3945 })
3946}
3947
3948#[cfg(test)]
3949mod tests {
3950 use std::{cell::RefCell, path::Path, rc::Rc};
3951
3952 use super::*;
3953 use crate::{FakeCompletionProvider, MessageId};
3954 use fs::FakeFs;
3955 use gpui::{AppContext, TestAppContext};
3956 use rope::Rope;
3957 use serde_json::json;
3958 use settings::SettingsStore;
3959 use unindent::Unindent;
3960 use util::test::marked_text_ranges;
3961
3962 #[gpui::test]
3963 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3964 let settings_store = SettingsStore::test(cx);
3965 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3966 cx.set_global(settings_store);
3967 init(cx);
3968 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3969
3970 let conversation = cx.new_model(|cx| {
3971 Conversation::new(
3972 LanguageModel::default(),
3973 registry,
3974 Default::default(),
3975 None,
3976 None,
3977 cx,
3978 )
3979 });
3980 let buffer = conversation.read(cx).buffer.clone();
3981
3982 let message_1 = conversation.read(cx).message_anchors[0].clone();
3983 assert_eq!(
3984 messages(&conversation, cx),
3985 vec![(message_1.id, Role::User, 0..0)]
3986 );
3987
3988 let message_2 = conversation.update(cx, |conversation, cx| {
3989 conversation
3990 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3991 .unwrap()
3992 });
3993 assert_eq!(
3994 messages(&conversation, cx),
3995 vec![
3996 (message_1.id, Role::User, 0..1),
3997 (message_2.id, Role::Assistant, 1..1)
3998 ]
3999 );
4000
4001 buffer.update(cx, |buffer, cx| {
4002 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
4003 });
4004 assert_eq!(
4005 messages(&conversation, cx),
4006 vec![
4007 (message_1.id, Role::User, 0..2),
4008 (message_2.id, Role::Assistant, 2..3)
4009 ]
4010 );
4011
4012 let message_3 = conversation.update(cx, |conversation, cx| {
4013 conversation
4014 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
4015 .unwrap()
4016 });
4017 assert_eq!(
4018 messages(&conversation, cx),
4019 vec![
4020 (message_1.id, Role::User, 0..2),
4021 (message_2.id, Role::Assistant, 2..4),
4022 (message_3.id, Role::User, 4..4)
4023 ]
4024 );
4025
4026 let message_4 = conversation.update(cx, |conversation, cx| {
4027 conversation
4028 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
4029 .unwrap()
4030 });
4031 assert_eq!(
4032 messages(&conversation, cx),
4033 vec![
4034 (message_1.id, Role::User, 0..2),
4035 (message_2.id, Role::Assistant, 2..4),
4036 (message_4.id, Role::User, 4..5),
4037 (message_3.id, Role::User, 5..5),
4038 ]
4039 );
4040
4041 buffer.update(cx, |buffer, cx| {
4042 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
4043 });
4044 assert_eq!(
4045 messages(&conversation, cx),
4046 vec![
4047 (message_1.id, Role::User, 0..2),
4048 (message_2.id, Role::Assistant, 2..4),
4049 (message_4.id, Role::User, 4..6),
4050 (message_3.id, Role::User, 6..7),
4051 ]
4052 );
4053
4054 // Deleting across message boundaries merges the messages.
4055 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
4056 assert_eq!(
4057 messages(&conversation, cx),
4058 vec![
4059 (message_1.id, Role::User, 0..3),
4060 (message_3.id, Role::User, 3..4),
4061 ]
4062 );
4063
4064 // Undoing the deletion should also undo the merge.
4065 buffer.update(cx, |buffer, cx| buffer.undo(cx));
4066 assert_eq!(
4067 messages(&conversation, cx),
4068 vec![
4069 (message_1.id, Role::User, 0..2),
4070 (message_2.id, Role::Assistant, 2..4),
4071 (message_4.id, Role::User, 4..6),
4072 (message_3.id, Role::User, 6..7),
4073 ]
4074 );
4075
4076 // Redoing the deletion should also redo the merge.
4077 buffer.update(cx, |buffer, cx| buffer.redo(cx));
4078 assert_eq!(
4079 messages(&conversation, cx),
4080 vec![
4081 (message_1.id, Role::User, 0..3),
4082 (message_3.id, Role::User, 3..4),
4083 ]
4084 );
4085
4086 // Ensure we can still insert after a merged message.
4087 let message_5 = conversation.update(cx, |conversation, cx| {
4088 conversation
4089 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
4090 .unwrap()
4091 });
4092 assert_eq!(
4093 messages(&conversation, cx),
4094 vec![
4095 (message_1.id, Role::User, 0..3),
4096 (message_5.id, Role::System, 3..4),
4097 (message_3.id, Role::User, 4..5)
4098 ]
4099 );
4100 }
4101
4102 #[gpui::test]
4103 fn test_message_splitting(cx: &mut AppContext) {
4104 let settings_store = SettingsStore::test(cx);
4105 cx.set_global(settings_store);
4106 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
4107 init(cx);
4108 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
4109
4110 let conversation = cx.new_model(|cx| {
4111 Conversation::new(
4112 LanguageModel::default(),
4113 registry,
4114 Default::default(),
4115 None,
4116 None,
4117 cx,
4118 )
4119 });
4120 let buffer = conversation.read(cx).buffer.clone();
4121
4122 let message_1 = conversation.read(cx).message_anchors[0].clone();
4123 assert_eq!(
4124 messages(&conversation, cx),
4125 vec![(message_1.id, Role::User, 0..0)]
4126 );
4127
4128 buffer.update(cx, |buffer, cx| {
4129 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
4130 });
4131
4132 let (_, message_2) =
4133 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
4134 let message_2 = message_2.unwrap();
4135
4136 // We recycle newlines in the middle of a split message
4137 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
4138 assert_eq!(
4139 messages(&conversation, cx),
4140 vec![
4141 (message_1.id, Role::User, 0..4),
4142 (message_2.id, Role::User, 4..16),
4143 ]
4144 );
4145
4146 let (_, message_3) =
4147 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
4148 let message_3 = message_3.unwrap();
4149
4150 // We don't recycle newlines at the end of a split message
4151 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
4152 assert_eq!(
4153 messages(&conversation, cx),
4154 vec![
4155 (message_1.id, Role::User, 0..4),
4156 (message_3.id, Role::User, 4..5),
4157 (message_2.id, Role::User, 5..17),
4158 ]
4159 );
4160
4161 let (_, message_4) =
4162 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
4163 let message_4 = message_4.unwrap();
4164 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
4165 assert_eq!(
4166 messages(&conversation, cx),
4167 vec![
4168 (message_1.id, Role::User, 0..4),
4169 (message_3.id, Role::User, 4..5),
4170 (message_2.id, Role::User, 5..9),
4171 (message_4.id, Role::User, 9..17),
4172 ]
4173 );
4174
4175 let (_, message_5) =
4176 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
4177 let message_5 = message_5.unwrap();
4178 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
4179 assert_eq!(
4180 messages(&conversation, cx),
4181 vec![
4182 (message_1.id, Role::User, 0..4),
4183 (message_3.id, Role::User, 4..5),
4184 (message_2.id, Role::User, 5..9),
4185 (message_4.id, Role::User, 9..10),
4186 (message_5.id, Role::User, 10..18),
4187 ]
4188 );
4189
4190 let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
4191 conversation.split_message(14..16, cx)
4192 });
4193 let message_6 = message_6.unwrap();
4194 let message_7 = message_7.unwrap();
4195 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
4196 assert_eq!(
4197 messages(&conversation, cx),
4198 vec![
4199 (message_1.id, Role::User, 0..4),
4200 (message_3.id, Role::User, 4..5),
4201 (message_2.id, Role::User, 5..9),
4202 (message_4.id, Role::User, 9..10),
4203 (message_5.id, Role::User, 10..14),
4204 (message_6.id, Role::User, 14..17),
4205 (message_7.id, Role::User, 17..19),
4206 ]
4207 );
4208 }
4209
4210 #[gpui::test]
4211 fn test_messages_for_offsets(cx: &mut AppContext) {
4212 let settings_store = SettingsStore::test(cx);
4213 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
4214 cx.set_global(settings_store);
4215 init(cx);
4216 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
4217 let conversation = cx.new_model(|cx| {
4218 Conversation::new(
4219 LanguageModel::default(),
4220 registry,
4221 Default::default(),
4222 None,
4223 None,
4224 cx,
4225 )
4226 });
4227 let buffer = conversation.read(cx).buffer.clone();
4228
4229 let message_1 = conversation.read(cx).message_anchors[0].clone();
4230 assert_eq!(
4231 messages(&conversation, cx),
4232 vec![(message_1.id, Role::User, 0..0)]
4233 );
4234
4235 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
4236 let message_2 = conversation
4237 .update(cx, |conversation, cx| {
4238 conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
4239 })
4240 .unwrap();
4241 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
4242
4243 let message_3 = conversation
4244 .update(cx, |conversation, cx| {
4245 conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
4246 })
4247 .unwrap();
4248 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
4249
4250 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
4251 assert_eq!(
4252 messages(&conversation, cx),
4253 vec![
4254 (message_1.id, Role::User, 0..4),
4255 (message_2.id, Role::User, 4..8),
4256 (message_3.id, Role::User, 8..11)
4257 ]
4258 );
4259
4260 assert_eq!(
4261 message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
4262 [message_1.id, message_2.id, message_3.id]
4263 );
4264 assert_eq!(
4265 message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
4266 [message_1.id, message_3.id]
4267 );
4268
4269 let message_4 = conversation
4270 .update(cx, |conversation, cx| {
4271 conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
4272 })
4273 .unwrap();
4274 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
4275 assert_eq!(
4276 messages(&conversation, cx),
4277 vec![
4278 (message_1.id, Role::User, 0..4),
4279 (message_2.id, Role::User, 4..8),
4280 (message_3.id, Role::User, 8..12),
4281 (message_4.id, Role::User, 12..12)
4282 ]
4283 );
4284 assert_eq!(
4285 message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
4286 [message_1.id, message_2.id, message_3.id, message_4.id]
4287 );
4288
4289 fn message_ids_for_offsets(
4290 conversation: &Model<Conversation>,
4291 offsets: &[usize],
4292 cx: &AppContext,
4293 ) -> Vec<MessageId> {
4294 conversation
4295 .read(cx)
4296 .messages_for_offsets(offsets.iter().copied(), cx)
4297 .into_iter()
4298 .map(|message| message.id)
4299 .collect()
4300 }
4301 }
4302
4303 #[gpui::test]
4304 async fn test_slash_commands(cx: &mut TestAppContext) {
4305 let settings_store = cx.update(SettingsStore::test);
4306 cx.set_global(settings_store);
4307 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
4308 cx.update(Project::init_settings);
4309 cx.update(init);
4310 let fs = FakeFs::new(cx.background_executor.clone());
4311
4312 fs.insert_tree(
4313 "/test",
4314 json!({
4315 "src": {
4316 "lib.rs": "fn one() -> usize { 1 }",
4317 "main.rs": "
4318 use crate::one;
4319 fn main() { one(); }
4320 ".unindent(),
4321 }
4322 }),
4323 )
4324 .await;
4325
4326 let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
4327 let prompt_library = Arc::new(PromptLibrary::default());
4328 let slash_command_registry = SlashCommandRegistry::new();
4329
4330 slash_command_registry
4331 .register_command(file_command::FileSlashCommand::new(project.clone()));
4332 slash_command_registry.register_command(prompt_command::PromptSlashCommand::new(
4333 prompt_library.clone(),
4334 ));
4335
4336 let lsp_adapter_delegate = project.update(cx, |project, cx| {
4337 // TODO: Find the right worktree.
4338 let worktree = project
4339 .worktrees()
4340 .next()
4341 .expect("expected at least one worktree");
4342 ProjectLspAdapterDelegate::new(project, &worktree, cx)
4343 });
4344
4345 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
4346 let conversation = cx.new_model(|cx| {
4347 Conversation::new(
4348 LanguageModel::default(),
4349 registry.clone(),
4350 slash_command_registry,
4351 None,
4352 Some(lsp_adapter_delegate),
4353 cx,
4354 )
4355 });
4356
4357 let output_ranges = Rc::new(RefCell::new(HashSet::default()));
4358 conversation.update(cx, |_, cx| {
4359 cx.subscribe(&conversation, {
4360 let ranges = output_ranges.clone();
4361 move |_, _, event, _| match event {
4362 ConversationEvent::SlashCommandOutputAdded(range) => {
4363 ranges.borrow_mut().insert(range.clone());
4364 }
4365 ConversationEvent::SlashCommandOutputRemoved(range) => {
4366 ranges.borrow_mut().remove(range);
4367 }
4368 _ => {}
4369 }
4370 })
4371 .detach();
4372 });
4373
4374 let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
4375
4376 // Insert a slash command
4377 buffer.update(cx, |buffer, cx| {
4378 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
4379 });
4380 assert_text_and_output_ranges(
4381 &buffer,
4382 &output_ranges.borrow(),
4383 "
4384 /file src/lib.rs
4385 "
4386 .unindent()
4387 .trim_end(),
4388 cx,
4389 );
4390
4391 // The slash command runs
4392 cx.executor().advance_clock(SLASH_COMMAND_DEBOUNCE);
4393 assert_text_and_output_ranges(
4394 &buffer,
4395 &output_ranges.borrow(),
4396 &"
4397 /file src/lib.rs«
4398 ```src/lib.rs
4399 fn one() -> usize { 1 }
4400 ```»"
4401 .unindent(),
4402 cx,
4403 );
4404
4405 // Edit the slash command
4406 buffer.update(cx, |buffer, cx| {
4407 let edit_offset = buffer.text().find("lib.rs").unwrap();
4408 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
4409 });
4410 assert_text_and_output_ranges(
4411 &buffer,
4412 &output_ranges.borrow(),
4413 &"
4414 /file src/main.rs«
4415 ```src/lib.rs
4416 fn one() -> usize { 1 }
4417 ```»"
4418 .unindent(),
4419 cx,
4420 );
4421
4422 cx.executor().advance_clock(SLASH_COMMAND_DEBOUNCE);
4423 assert_text_and_output_ranges(
4424 &buffer,
4425 &output_ranges.borrow(),
4426 &"
4427 /file src/main.rs«
4428 ```src/main.rs
4429 use crate::one;
4430 fn main() { one(); }
4431 ```»"
4432 .unindent(),
4433 cx,
4434 );
4435
4436 // Insert newlines between the slash command and its output
4437 buffer.update(cx, |buffer, cx| {
4438 let edit_offset = buffer.text().find("\n```src/main.rs").unwrap();
4439 buffer.edit([(edit_offset..edit_offset, "\n")], None, cx);
4440 });
4441 assert_text_and_output_ranges(
4442 &buffer,
4443 &output_ranges.borrow(),
4444 &"
4445 /file src/main.rs«
4446
4447 ```src/main.rs
4448 use crate::one;
4449 fn main() { one(); }
4450 ```»"
4451 .unindent(),
4452 cx,
4453 );
4454
4455 cx.executor().advance_clock(SLASH_COMMAND_DEBOUNCE);
4456 assert_text_and_output_ranges(
4457 &buffer,
4458 &output_ranges.borrow(),
4459 &"
4460 /file src/main.rs«
4461 ```src/main.rs
4462 use crate::one;
4463 fn main() { one(); }
4464 ```»"
4465 .unindent(),
4466 cx,
4467 );
4468
4469 // Insert text at the beginning of the output
4470 buffer.update(cx, |buffer, cx| {
4471 let edit_offset = buffer.text().find("```src/main.rs").unwrap();
4472 buffer.edit([(edit_offset..edit_offset, "!")], None, cx);
4473 });
4474 assert_text_and_output_ranges(
4475 &buffer,
4476 &output_ranges.borrow(),
4477 &"
4478 /file src/main.rs«
4479 !```src/main.rs
4480 use crate::one;
4481 fn main() { one(); }
4482 ```»"
4483 .unindent(),
4484 cx,
4485 );
4486
4487 cx.executor().advance_clock(SLASH_COMMAND_DEBOUNCE);
4488 assert_text_and_output_ranges(
4489 &buffer,
4490 &output_ranges.borrow(),
4491 &"
4492 /file src/main.rs«
4493 ```src/main.rs
4494 use crate::one;
4495 fn main() { one(); }
4496 ```»"
4497 .unindent(),
4498 cx,
4499 );
4500
4501 // Slash commands are omitted from completion requests. Only their
4502 // output is included.
4503 let request = conversation.update(cx, |conversation, cx| {
4504 conversation.to_completion_request(cx)
4505 });
4506 assert_eq!(
4507 &request.messages[1..],
4508 &[LanguageModelRequestMessage {
4509 role: Role::User,
4510 content: "
4511 ```src/main.rs
4512 use crate::one;
4513 fn main() { one(); }
4514 ```"
4515 .unindent()
4516 }]
4517 );
4518
4519 buffer.update(cx, |buffer, cx| {
4520 buffer.edit([(0..0, "hello\n")], None, cx);
4521 });
4522 buffer.update(cx, |buffer, cx| {
4523 buffer.edit(
4524 [(buffer.len()..buffer.len(), "\ngoodbye\nfarewell\n")],
4525 None,
4526 cx,
4527 );
4528 });
4529 let request = conversation.update(cx, |conversation, cx| {
4530 conversation.to_completion_request(cx)
4531 });
4532 assert_eq!(
4533 &request.messages[1..],
4534 &[LanguageModelRequestMessage {
4535 role: Role::User,
4536 content: "
4537 hello
4538 ```src/main.rs
4539 use crate::one;
4540 fn main() { one(); }
4541 ```
4542 goodbye
4543 farewell"
4544 .unindent()
4545 }]
4546 );
4547
4548 #[track_caller]
4549 fn assert_text_and_output_ranges(
4550 buffer: &Model<Buffer>,
4551 ranges: &HashSet<Range<language::Anchor>>,
4552 expected_marked_text: &str,
4553 cx: &mut TestAppContext,
4554 ) {
4555 let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
4556 let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
4557 let mut ranges = ranges
4558 .iter()
4559 .map(|range| range.to_offset(buffer))
4560 .collect::<Vec<_>>();
4561 ranges.sort_by_key(|a| a.start);
4562 (buffer.text(), ranges)
4563 });
4564
4565 assert_eq!(actual_text, expected_text);
4566 assert_eq!(actual_ranges, expected_ranges);
4567 }
4568 }
4569
4570 #[test]
4571 fn test_parse_next_edit_suggestion() {
4572 let text = "
4573 some output:
4574
4575 ```edit src/foo.rs
4576 let a = 1;
4577 let b = 2;
4578 ---
4579 let w = 1;
4580 let x = 2;
4581 let y = 3;
4582 let z = 4;
4583 ```
4584
4585 some more output:
4586
4587 ```edit src/foo.rs
4588 let c = 1;
4589 ---
4590 ```
4591
4592 and the conclusion.
4593 "
4594 .unindent();
4595
4596 let rope = Rope::from(text.as_str());
4597 let mut lines = rope.chunks().lines();
4598 let mut suggestions = vec![];
4599 while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
4600 suggestions.push((
4601 suggestion.path.clone(),
4602 text[suggestion.old_text_range].to_string(),
4603 text[suggestion.new_text_range].to_string(),
4604 ));
4605 }
4606
4607 assert_eq!(
4608 suggestions,
4609 vec![
4610 (
4611 Path::new("src/foo.rs").into(),
4612 [
4613 " let a = 1;", //
4614 " let b = 2;",
4615 "",
4616 ]
4617 .join("\n"),
4618 [
4619 " let w = 1;",
4620 " let x = 2;",
4621 " let y = 3;",
4622 " let z = 4;",
4623 "",
4624 ]
4625 .join("\n"),
4626 ),
4627 (
4628 Path::new("src/foo.rs").into(),
4629 [
4630 " let c = 1;", //
4631 "",
4632 ]
4633 .join("\n"),
4634 String::new(),
4635 )
4636 ]
4637 );
4638 }
4639
4640 #[gpui::test]
4641 async fn test_serialization(cx: &mut TestAppContext) {
4642 let settings_store = cx.update(SettingsStore::test);
4643 cx.set_global(settings_store);
4644 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
4645 cx.update(init);
4646 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
4647 let conversation = cx.new_model(|cx| {
4648 Conversation::new(
4649 LanguageModel::default(),
4650 registry.clone(),
4651 Default::default(),
4652 None,
4653 None,
4654 cx,
4655 )
4656 });
4657 let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
4658 let message_0 =
4659 conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
4660 let message_1 = conversation.update(cx, |conversation, cx| {
4661 conversation
4662 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
4663 .unwrap()
4664 });
4665 let message_2 = conversation.update(cx, |conversation, cx| {
4666 conversation
4667 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
4668 .unwrap()
4669 });
4670 buffer.update(cx, |buffer, cx| {
4671 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
4672 buffer.finalize_last_transaction();
4673 });
4674 let _message_3 = conversation.update(cx, |conversation, cx| {
4675 conversation
4676 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
4677 .unwrap()
4678 });
4679 buffer.update(cx, |buffer, cx| buffer.undo(cx));
4680 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
4681 assert_eq!(
4682 cx.read(|cx| messages(&conversation, cx)),
4683 [
4684 (message_0, Role::User, 0..2),
4685 (message_1.id, Role::Assistant, 2..6),
4686 (message_2.id, Role::System, 6..6),
4687 ]
4688 );
4689
4690 let deserialized_conversation = Conversation::deserialize(
4691 conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
4692 LanguageModel::default(),
4693 Default::default(),
4694 registry.clone(),
4695 Default::default(),
4696 None,
4697 None,
4698 &mut cx.to_async(),
4699 )
4700 .await
4701 .unwrap();
4702 let deserialized_buffer =
4703 deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
4704 assert_eq!(
4705 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
4706 "a\nb\nc\n"
4707 );
4708 assert_eq!(
4709 cx.read(|cx| messages(&deserialized_conversation, cx)),
4710 [
4711 (message_0, Role::User, 0..2),
4712 (message_1.id, Role::Assistant, 2..6),
4713 (message_2.id, Role::System, 6..6),
4714 ]
4715 );
4716 }
4717
4718 fn messages(
4719 conversation: &Model<Conversation>,
4720 cx: &AppContext,
4721 ) -> Vec<(MessageId, Role, Range<usize>)> {
4722 conversation
4723 .read(cx)
4724 .messages(cx)
4725 .map(|message| (message.id, message.role, message.offset_range))
4726 .collect()
4727 }
4728}