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