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