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