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