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