1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
3 stream_completion,
4 streaming_diff::{Hunk, StreamingDiff},
5 MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role,
6 SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
7};
8use anyhow::{anyhow, Result};
9use chrono::{DateTime, Local};
10use collections::{hash_map, HashMap, HashSet, VecDeque};
11use editor::{
12 display_map::{
13 BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
14 },
15 scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
16 Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
17};
18use fs::Fs;
19use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
20use gpui::{
21 actions,
22 elements::{
23 ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable,
24 Stack, Svg, Text, UniformList, UniformListState,
25 },
26 fonts::HighlightStyle,
27 geometry::vector::{vec2f, Vector2F},
28 platform::{CursorStyle, MouseButton},
29 Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
30 ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
31 WindowContext,
32};
33use language::{
34 language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _,
35 TransactionId,
36};
37use search::BufferSearchBar;
38use settings::SettingsStore;
39use std::{
40 cell::{Cell, RefCell},
41 cmp, env,
42 fmt::Write,
43 future, iter,
44 ops::Range,
45 path::{Path, PathBuf},
46 rc::Rc,
47 sync::Arc,
48 time::Duration,
49};
50use theme::{
51 components::{action_button::Button, ComponentExt},
52 AssistantStyle,
53};
54use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
55use workspace::{
56 dock::{DockPosition, Panel},
57 searchable::Direction,
58 Save, Toast, ToggleZoom, Toolbar, Workspace,
59};
60
61actions!(
62 assistant,
63 [
64 NewConversation,
65 Assist,
66 Split,
67 CycleMessageRole,
68 QuoteSelection,
69 ToggleFocus,
70 ResetKey,
71 InlineAssist,
72 ToggleIncludeConversation,
73 ]
74);
75
76pub fn init(cx: &mut AppContext) {
77 settings::register::<AssistantSettings>(cx);
78 cx.add_action(
79 |this: &mut AssistantPanel,
80 _: &workspace::NewFile,
81 cx: &mut ViewContext<AssistantPanel>| {
82 this.new_conversation(cx);
83 },
84 );
85 cx.add_action(ConversationEditor::assist);
86 cx.capture_action(ConversationEditor::cancel_last_assist);
87 cx.capture_action(ConversationEditor::save);
88 cx.add_action(ConversationEditor::quote_selection);
89 cx.capture_action(ConversationEditor::copy);
90 cx.add_action(ConversationEditor::split);
91 cx.capture_action(ConversationEditor::cycle_message_role);
92 cx.add_action(AssistantPanel::save_api_key);
93 cx.add_action(AssistantPanel::reset_api_key);
94 cx.add_action(AssistantPanel::toggle_zoom);
95 cx.add_action(AssistantPanel::deploy);
96 cx.add_action(AssistantPanel::select_next_match);
97 cx.add_action(AssistantPanel::select_prev_match);
98 cx.add_action(AssistantPanel::handle_editor_cancel);
99 cx.add_action(
100 |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
101 workspace.toggle_panel_focus::<AssistantPanel>(cx);
102 },
103 );
104 cx.add_action(AssistantPanel::inline_assist);
105 cx.add_action(AssistantPanel::cancel_last_inline_assist);
106 cx.add_action(InlineAssistant::confirm);
107 cx.add_action(InlineAssistant::cancel);
108 cx.add_action(InlineAssistant::toggle_include_conversation);
109 cx.add_action(InlineAssistant::move_up);
110 cx.add_action(InlineAssistant::move_down);
111}
112
113#[derive(Debug)]
114pub enum AssistantPanelEvent {
115 ZoomIn,
116 ZoomOut,
117 Focus,
118 Close,
119 DockPositionChanged,
120}
121
122pub struct AssistantPanel {
123 workspace: WeakViewHandle<Workspace>,
124 width: Option<f32>,
125 height: Option<f32>,
126 active_editor_index: Option<usize>,
127 prev_active_editor_index: Option<usize>,
128 editors: Vec<ViewHandle<ConversationEditor>>,
129 saved_conversations: Vec<SavedConversationMetadata>,
130 saved_conversations_list_state: UniformListState,
131 zoomed: bool,
132 has_focus: bool,
133 toolbar: ViewHandle<Toolbar>,
134 api_key: Rc<RefCell<Option<String>>>,
135 api_key_editor: Option<ViewHandle<Editor>>,
136 has_read_credentials: bool,
137 languages: Arc<LanguageRegistry>,
138 fs: Arc<dyn Fs>,
139 subscriptions: Vec<Subscription>,
140 next_inline_assist_id: usize,
141 pending_inline_assists: HashMap<usize, PendingInlineAssist>,
142 pending_inline_assist_ids_by_editor: HashMap<WeakViewHandle<Editor>, Vec<usize>>,
143 include_conversation_in_next_inline_assist: bool,
144 inline_prompt_history: VecDeque<String>,
145 _watch_saved_conversations: Task<Result<()>>,
146}
147
148impl AssistantPanel {
149 const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
150
151 pub fn load(
152 workspace: WeakViewHandle<Workspace>,
153 cx: AsyncAppContext,
154 ) -> Task<Result<ViewHandle<Self>>> {
155 cx.spawn(|mut cx| async move {
156 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
157 let saved_conversations = SavedConversationMetadata::list(fs.clone())
158 .await
159 .log_err()
160 .unwrap_or_default();
161
162 // TODO: deserialize state.
163 let workspace_handle = workspace.clone();
164 workspace.update(&mut cx, |workspace, cx| {
165 cx.add_view::<Self, _>(|cx| {
166 const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
167 let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
168 let mut events = fs
169 .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
170 .await;
171 while events.next().await.is_some() {
172 let saved_conversations = SavedConversationMetadata::list(fs.clone())
173 .await
174 .log_err()
175 .unwrap_or_default();
176 this.update(&mut cx, |this, cx| {
177 this.saved_conversations = saved_conversations;
178 cx.notify();
179 })
180 .ok();
181 }
182
183 anyhow::Ok(())
184 });
185
186 let toolbar = cx.add_view(|cx| {
187 let mut toolbar = Toolbar::new();
188 toolbar.set_can_navigate(false, cx);
189 toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
190 toolbar
191 });
192 let mut this = Self {
193 workspace: workspace_handle,
194 active_editor_index: Default::default(),
195 prev_active_editor_index: Default::default(),
196 editors: Default::default(),
197 saved_conversations,
198 saved_conversations_list_state: Default::default(),
199 zoomed: false,
200 has_focus: false,
201 toolbar,
202 api_key: Rc::new(RefCell::new(None)),
203 api_key_editor: None,
204 has_read_credentials: false,
205 languages: workspace.app_state().languages.clone(),
206 fs: workspace.app_state().fs.clone(),
207 width: None,
208 height: None,
209 subscriptions: Default::default(),
210 next_inline_assist_id: 0,
211 pending_inline_assists: Default::default(),
212 pending_inline_assist_ids_by_editor: Default::default(),
213 include_conversation_in_next_inline_assist: false,
214 inline_prompt_history: Default::default(),
215 _watch_saved_conversations,
216 };
217
218 let mut old_dock_position = this.position(cx);
219 this.subscriptions =
220 vec![cx.observe_global::<SettingsStore, _>(move |this, cx| {
221 let new_dock_position = this.position(cx);
222 if new_dock_position != old_dock_position {
223 old_dock_position = new_dock_position;
224 cx.emit(AssistantPanelEvent::DockPositionChanged);
225 }
226 cx.notify();
227 })];
228
229 this
230 })
231 })
232 })
233 }
234
235 pub fn inline_assist(
236 workspace: &mut Workspace,
237 _: &InlineAssist,
238 cx: &mut ViewContext<Workspace>,
239 ) {
240 let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
241 if this
242 .update(cx, |assistant, cx| assistant.load_api_key(cx))
243 .is_some()
244 {
245 this
246 } else {
247 workspace.focus_panel::<AssistantPanel>(cx);
248 return;
249 }
250 } else {
251 return;
252 };
253
254 let active_editor = if let Some(active_editor) = workspace
255 .active_item(cx)
256 .and_then(|item| item.act_as::<Editor>(cx))
257 {
258 active_editor
259 } else {
260 return;
261 };
262
263 this.update(cx, |assistant, cx| {
264 assistant.new_inline_assist(&active_editor, cx)
265 });
266 }
267
268 fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
269 let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
270 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
271 let selection = editor.read(cx).selections.newest_anchor().clone();
272 let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot);
273 let assist_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
274 InlineAssistKind::Generate
275 } else {
276 InlineAssistKind::Transform
277 };
278 let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
279 let inline_assistant = cx.add_view(|cx| {
280 let assistant = InlineAssistant::new(
281 inline_assist_id,
282 assist_kind,
283 measurements.clone(),
284 self.include_conversation_in_next_inline_assist,
285 self.inline_prompt_history.clone(),
286 cx,
287 );
288 cx.focus_self();
289 assistant
290 });
291 let block_id = editor.update(cx, |editor, cx| {
292 editor.change_selections(None, cx, |selections| {
293 selections.select_anchor_ranges([selection.head()..selection.head()])
294 });
295 editor.insert_blocks(
296 [BlockProperties {
297 style: BlockStyle::Flex,
298 position: selection.head().bias_left(&snapshot),
299 height: 2,
300 render: Arc::new({
301 let inline_assistant = inline_assistant.clone();
302 move |cx: &mut BlockContext| {
303 measurements.set(BlockMeasurements {
304 anchor_x: cx.anchor_x,
305 gutter_width: cx.gutter_width,
306 });
307 ChildView::new(&inline_assistant, cx).into_any()
308 }
309 }),
310 disposition: if selection.reversed {
311 BlockDisposition::Above
312 } else {
313 BlockDisposition::Below
314 },
315 }],
316 Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
317 cx,
318 )[0]
319 });
320
321 self.pending_inline_assists.insert(
322 inline_assist_id,
323 PendingInlineAssist {
324 kind: assist_kind,
325 editor: editor.downgrade(),
326 range,
327 highlighted_ranges: Default::default(),
328 inline_assistant: Some((block_id, inline_assistant.clone())),
329 code_generation: Task::ready(None),
330 transaction_id: None,
331 _subscriptions: vec![
332 cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
333 cx.subscribe(editor, {
334 let inline_assistant = inline_assistant.downgrade();
335 move |this, editor, event, cx| {
336 if let Some(inline_assistant) = inline_assistant.upgrade(cx) {
337 match event {
338 editor::Event::SelectionsChanged { local } => {
339 if *local && inline_assistant.read(cx).has_focus {
340 cx.focus(&editor);
341 }
342 }
343 editor::Event::TransactionUndone {
344 transaction_id: tx_id,
345 } => {
346 if let Some(pending_assist) =
347 this.pending_inline_assists.get(&inline_assist_id)
348 {
349 if pending_assist.transaction_id == Some(*tx_id) {
350 // Notice we are supplying `undo: false` here. This
351 // is because there's no need to undo the transaction
352 // because the user just did so.
353 this.close_inline_assist(
354 inline_assist_id,
355 false,
356 cx,
357 );
358 }
359 }
360 }
361 _ => {}
362 }
363 }
364 }
365 }),
366 ],
367 },
368 );
369 self.pending_inline_assist_ids_by_editor
370 .entry(editor.downgrade())
371 .or_default()
372 .push(inline_assist_id);
373 self.update_highlights_for_editor(&editor, cx);
374 }
375
376 fn handle_inline_assistant_event(
377 &mut self,
378 inline_assistant: ViewHandle<InlineAssistant>,
379 event: &InlineAssistantEvent,
380 cx: &mut ViewContext<Self>,
381 ) {
382 let assist_id = inline_assistant.read(cx).id;
383 match event {
384 InlineAssistantEvent::Confirmed {
385 prompt,
386 include_conversation,
387 } => {
388 self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
389 }
390 InlineAssistantEvent::Canceled => {
391 self.close_inline_assist(assist_id, true, cx);
392 }
393 InlineAssistantEvent::Dismissed => {
394 self.hide_inline_assist(assist_id, cx);
395 }
396 InlineAssistantEvent::IncludeConversationToggled {
397 include_conversation,
398 } => {
399 self.include_conversation_in_next_inline_assist = *include_conversation;
400 }
401 }
402 }
403
404 fn cancel_last_inline_assist(
405 workspace: &mut Workspace,
406 _: &editor::Cancel,
407 cx: &mut ViewContext<Workspace>,
408 ) {
409 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
410 if let Some(editor) = workspace
411 .active_item(cx)
412 .and_then(|item| item.downcast::<Editor>())
413 {
414 let handled = panel.update(cx, |panel, cx| {
415 if let Some(assist_id) = panel
416 .pending_inline_assist_ids_by_editor
417 .get(&editor.downgrade())
418 .and_then(|assist_ids| assist_ids.last().copied())
419 {
420 panel.close_inline_assist(assist_id, true, cx);
421 true
422 } else {
423 false
424 }
425 });
426 if handled {
427 return;
428 }
429 }
430 }
431
432 cx.propagate_action();
433 }
434
435 fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
436 self.hide_inline_assist(assist_id, cx);
437
438 if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
439 if let hash_map::Entry::Occupied(mut entry) = self
440 .pending_inline_assist_ids_by_editor
441 .entry(pending_assist.editor)
442 {
443 entry.get_mut().retain(|id| *id != assist_id);
444 if entry.get().is_empty() {
445 entry.remove();
446 }
447 }
448
449 if let Some(editor) = pending_assist.editor.upgrade(cx) {
450 self.update_highlights_for_editor(&editor, cx);
451
452 if undo {
453 if let Some(transaction_id) = pending_assist.transaction_id {
454 editor.update(cx, |editor, cx| {
455 editor.buffer().update(cx, |buffer, cx| {
456 buffer.undo_transaction(transaction_id, cx)
457 });
458 });
459 }
460 }
461 }
462 }
463 }
464
465 fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
466 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
467 if let Some(editor) = pending_assist.editor.upgrade(cx) {
468 if let Some((block_id, _)) = pending_assist.inline_assistant.take() {
469 editor.update(cx, |editor, cx| {
470 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
471 });
472 }
473 }
474 }
475 }
476
477 fn confirm_inline_assist(
478 &mut self,
479 inline_assist_id: usize,
480 user_prompt: &str,
481 include_conversation: bool,
482 cx: &mut ViewContext<Self>,
483 ) {
484 let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
485 api_key
486 } else {
487 return;
488 };
489
490 let conversation = if include_conversation {
491 self.active_editor()
492 .map(|editor| editor.read(cx).conversation.clone())
493 } else {
494 None
495 };
496
497 let pending_assist =
498 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
499 pending_assist
500 } else {
501 return;
502 };
503
504 let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) {
505 editor
506 } else {
507 return;
508 };
509
510 self.inline_prompt_history
511 .retain(|prompt| prompt != user_prompt);
512 self.inline_prompt_history.push_back(user_prompt.into());
513 if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
514 self.inline_prompt_history.pop_front();
515 }
516
517 let range = pending_assist.range.clone();
518 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
519 let selected_text = snapshot
520 .text_for_range(range.start..range.end)
521 .collect::<Rope>();
522
523 let selection_start = range.start.to_point(&snapshot);
524 let selection_end = range.end.to_point(&snapshot);
525
526 let mut base_indent: Option<language::IndentSize> = None;
527 let mut start_row = selection_start.row;
528 if snapshot.is_line_blank(start_row) {
529 if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) {
530 start_row = prev_non_blank_row;
531 }
532 }
533 for row in start_row..=selection_end.row {
534 if snapshot.is_line_blank(row) {
535 continue;
536 }
537
538 let line_indent = snapshot.indent_size_for_line(row);
539 if let Some(base_indent) = base_indent.as_mut() {
540 if line_indent.len < base_indent.len {
541 *base_indent = line_indent;
542 }
543 } else {
544 base_indent = Some(line_indent);
545 }
546 }
547
548 let mut normalized_selected_text = selected_text.clone();
549 if let Some(base_indent) = base_indent {
550 for row in selection_start.row..=selection_end.row {
551 let selection_row = row - selection_start.row;
552 let line_start =
553 normalized_selected_text.point_to_offset(Point::new(selection_row, 0));
554 let indent_len = if row == selection_start.row {
555 base_indent.len.saturating_sub(selection_start.column)
556 } else {
557 let line_len = normalized_selected_text.line_len(selection_row);
558 cmp::min(line_len, base_indent.len)
559 };
560 let indent_end = cmp::min(
561 line_start + indent_len as usize,
562 normalized_selected_text.len(),
563 );
564 normalized_selected_text.replace(line_start..indent_end, "");
565 }
566 }
567
568 let language = snapshot.language_at(range.start);
569 let language_name = if let Some(language) = language.as_ref() {
570 if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
571 None
572 } else {
573 Some(language.name())
574 }
575 } else {
576 None
577 };
578 let language_name = language_name.as_deref();
579
580 let mut prompt = String::new();
581 if let Some(language_name) = language_name {
582 writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
583 }
584 match pending_assist.kind {
585 InlineAssistKind::Transform => {
586 writeln!(
587 prompt,
588 "You're currently working inside an editor on this file:"
589 )
590 .unwrap();
591 if let Some(language_name) = language_name {
592 writeln!(prompt, "```{language_name}").unwrap();
593 } else {
594 writeln!(prompt, "```").unwrap();
595 }
596 for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) {
597 write!(prompt, "{chunk}").unwrap();
598 }
599 writeln!(prompt, "```").unwrap();
600
601 writeln!(
602 prompt,
603 "In particular, the user has selected the following text:"
604 )
605 .unwrap();
606 if let Some(language_name) = language_name {
607 writeln!(prompt, "```{language_name}").unwrap();
608 } else {
609 writeln!(prompt, "```").unwrap();
610 }
611 writeln!(prompt, "{normalized_selected_text}").unwrap();
612 writeln!(prompt, "```").unwrap();
613 writeln!(prompt).unwrap();
614 writeln!(
615 prompt,
616 "Modify the selected text given the user prompt: {user_prompt}"
617 )
618 .unwrap();
619 writeln!(
620 prompt,
621 "You MUST reply only with the edited selected text, not the entire file."
622 )
623 .unwrap();
624 }
625 InlineAssistKind::Generate => {
626 writeln!(
627 prompt,
628 "You're currently working inside an editor on this file:"
629 )
630 .unwrap();
631 if let Some(language_name) = language_name {
632 writeln!(prompt, "```{language_name}").unwrap();
633 } else {
634 writeln!(prompt, "```").unwrap();
635 }
636 for chunk in snapshot.text_for_range(Anchor::min()..range.start) {
637 write!(prompt, "{chunk}").unwrap();
638 }
639 write!(prompt, "<|>").unwrap();
640 for chunk in snapshot.text_for_range(range.start..Anchor::max()) {
641 write!(prompt, "{chunk}").unwrap();
642 }
643 writeln!(prompt).unwrap();
644 writeln!(prompt, "```").unwrap();
645 writeln!(
646 prompt,
647 "Assume the cursor is located where the `<|>` marker is."
648 )
649 .unwrap();
650 writeln!(
651 prompt,
652 "Text can't be replaced, so assume your answer will be inserted at the cursor."
653 )
654 .unwrap();
655 writeln!(
656 prompt,
657 "Complete the text given the user prompt: {user_prompt}"
658 )
659 .unwrap();
660 }
661 }
662 if let Some(language_name) = language_name {
663 writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap();
664 }
665 writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap();
666 writeln!(prompt, "Never make remarks about the output.").unwrap();
667
668 let mut messages = Vec::new();
669 let mut model = settings::get::<AssistantSettings>(cx)
670 .default_open_ai_model
671 .clone();
672 if let Some(conversation) = conversation {
673 let conversation = conversation.read(cx);
674 let buffer = conversation.buffer.read(cx);
675 messages.extend(
676 conversation
677 .messages(cx)
678 .map(|message| message.to_open_ai_message(buffer)),
679 );
680 model = conversation.model.clone();
681 }
682
683 messages.push(RequestMessage {
684 role: Role::User,
685 content: prompt,
686 });
687 let request = OpenAIRequest {
688 model: model.full_name().into(),
689 messages,
690 stream: true,
691 };
692 let response = stream_completion(api_key, cx.background().clone(), request);
693 let editor = editor.downgrade();
694
695 pending_assist.code_generation = cx.spawn(|this, mut cx| {
696 async move {
697 let mut edit_start = range.start.to_offset(&snapshot);
698
699 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
700 let diff = cx.background().spawn(async move {
701 let chunks = strip_markdown_codeblock(response.await?.filter_map(
702 |message| async move {
703 match message {
704 Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)),
705 Err(error) => Some(Err(error)),
706 }
707 },
708 ));
709 futures::pin_mut!(chunks);
710 let mut diff = StreamingDiff::new(selected_text.to_string());
711
712 let mut indent_len;
713 let indent_text;
714 if let Some(base_indent) = base_indent {
715 indent_len = base_indent.len;
716 indent_text = match base_indent.kind {
717 language::IndentKind::Space => " ",
718 language::IndentKind::Tab => "\t",
719 };
720 } else {
721 indent_len = 0;
722 indent_text = "";
723 };
724
725 let mut first_line_len = 0;
726 let mut first_line_non_whitespace_char_ix = None;
727 let mut first_line = true;
728 let mut new_text = String::new();
729
730 while let Some(chunk) = chunks.next().await {
731 let chunk = chunk?;
732
733 let mut lines = chunk.split('\n');
734 if let Some(mut line) = lines.next() {
735 if first_line {
736 if first_line_non_whitespace_char_ix.is_none() {
737 if let Some(mut char_ix) =
738 line.find(|ch: char| !ch.is_whitespace())
739 {
740 line = &line[char_ix..];
741 char_ix += first_line_len;
742 first_line_non_whitespace_char_ix = Some(char_ix);
743 let first_line_indent = char_ix
744 .saturating_sub(selection_start.column as usize)
745 as usize;
746 new_text.push_str(&indent_text.repeat(first_line_indent));
747 indent_len = indent_len.saturating_sub(char_ix as u32);
748 }
749 }
750 first_line_len += line.len();
751 }
752
753 if first_line_non_whitespace_char_ix.is_some() {
754 new_text.push_str(line);
755 }
756 }
757
758 for line in lines {
759 first_line = false;
760 new_text.push('\n');
761 if !line.is_empty() {
762 new_text.push_str(&indent_text.repeat(indent_len as usize));
763 }
764 new_text.push_str(line);
765 }
766
767 let hunks = diff.push_new(&new_text);
768 hunks_tx.send(hunks).await?;
769 new_text.clear();
770 }
771 hunks_tx.send(diff.finish()).await?;
772
773 anyhow::Ok(())
774 });
775
776 while let Some(hunks) = hunks_rx.next().await {
777 let editor = if let Some(editor) = editor.upgrade(&cx) {
778 editor
779 } else {
780 break;
781 };
782
783 let this = if let Some(this) = this.upgrade(&cx) {
784 this
785 } else {
786 break;
787 };
788
789 this.update(&mut cx, |this, cx| {
790 let pending_assist = if let Some(pending_assist) =
791 this.pending_inline_assists.get_mut(&inline_assist_id)
792 {
793 pending_assist
794 } else {
795 return;
796 };
797
798 pending_assist.highlighted_ranges.clear();
799 editor.update(cx, |editor, cx| {
800 let transaction = editor.buffer().update(cx, |buffer, cx| {
801 // Avoid grouping assistant edits with user edits.
802 buffer.finalize_last_transaction(cx);
803
804 buffer.start_transaction(cx);
805 buffer.edit(
806 hunks.into_iter().filter_map(|hunk| match hunk {
807 Hunk::Insert { text } => {
808 let edit_start = snapshot.anchor_after(edit_start);
809 Some((edit_start..edit_start, text))
810 }
811 Hunk::Remove { len } => {
812 let edit_end = edit_start + len;
813 let edit_range = snapshot.anchor_after(edit_start)
814 ..snapshot.anchor_before(edit_end);
815 edit_start = edit_end;
816 Some((edit_range, String::new()))
817 }
818 Hunk::Keep { len } => {
819 let edit_end = edit_start + len;
820 let edit_range = snapshot.anchor_after(edit_start)
821 ..snapshot.anchor_before(edit_end);
822 edit_start += len;
823 pending_assist.highlighted_ranges.push(edit_range);
824 None
825 }
826 }),
827 None,
828 cx,
829 );
830
831 buffer.end_transaction(cx)
832 });
833
834 if let Some(transaction) = transaction {
835 if let Some(first_transaction) = pending_assist.transaction_id {
836 // Group all assistant edits into the first transaction.
837 editor.buffer().update(cx, |buffer, cx| {
838 buffer.merge_transactions(
839 transaction,
840 first_transaction,
841 cx,
842 )
843 });
844 } else {
845 pending_assist.transaction_id = Some(transaction);
846 editor.buffer().update(cx, |buffer, cx| {
847 buffer.finalize_last_transaction(cx)
848 });
849 }
850 }
851 });
852
853 this.update_highlights_for_editor(&editor, cx);
854 });
855 }
856
857 if let Err(error) = diff.await {
858 this.update(&mut cx, |this, cx| {
859 let pending_assist = if let Some(pending_assist) =
860 this.pending_inline_assists.get_mut(&inline_assist_id)
861 {
862 pending_assist
863 } else {
864 return;
865 };
866
867 if let Some((_, inline_assistant)) =
868 pending_assist.inline_assistant.as_ref()
869 {
870 inline_assistant.update(cx, |inline_assistant, cx| {
871 inline_assistant.set_error(error, cx);
872 });
873 } else if let Some(workspace) = this.workspace.upgrade(cx) {
874 workspace.update(cx, |workspace, cx| {
875 workspace.show_toast(
876 Toast::new(
877 inline_assist_id,
878 format!("Inline assistant error: {}", error),
879 ),
880 cx,
881 );
882 })
883 }
884 })?;
885 } else {
886 let _ = this.update(&mut cx, |this, cx| {
887 this.close_inline_assist(inline_assist_id, false, cx)
888 });
889 }
890
891 anyhow::Ok(())
892 }
893 .log_err()
894 });
895 }
896
897 fn update_highlights_for_editor(
898 &self,
899 editor: &ViewHandle<Editor>,
900 cx: &mut ViewContext<Self>,
901 ) {
902 let mut background_ranges = Vec::new();
903 let mut foreground_ranges = Vec::new();
904 let empty_inline_assist_ids = Vec::new();
905 let inline_assist_ids = self
906 .pending_inline_assist_ids_by_editor
907 .get(&editor.downgrade())
908 .unwrap_or(&empty_inline_assist_ids);
909
910 for inline_assist_id in inline_assist_ids {
911 if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
912 background_ranges.push(pending_assist.range.clone());
913 foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned());
914 }
915 }
916
917 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
918 merge_ranges(&mut background_ranges, &snapshot);
919 merge_ranges(&mut foreground_ranges, &snapshot);
920 editor.update(cx, |editor, cx| {
921 if background_ranges.is_empty() {
922 editor.clear_background_highlights::<PendingInlineAssist>(cx);
923 } else {
924 editor.highlight_background::<PendingInlineAssist>(
925 background_ranges,
926 |theme| theme.assistant.inline.pending_edit_background,
927 cx,
928 );
929 }
930
931 if foreground_ranges.is_empty() {
932 editor.clear_text_highlights::<PendingInlineAssist>(cx);
933 } else {
934 editor.highlight_text::<PendingInlineAssist>(
935 foreground_ranges,
936 HighlightStyle {
937 fade_out: Some(0.6),
938 ..Default::default()
939 },
940 cx,
941 );
942 }
943 });
944 }
945
946 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<ConversationEditor> {
947 let editor = cx.add_view(|cx| {
948 ConversationEditor::new(
949 self.api_key.clone(),
950 self.languages.clone(),
951 self.fs.clone(),
952 cx,
953 )
954 });
955 self.add_conversation(editor.clone(), cx);
956 editor
957 }
958
959 fn add_conversation(
960 &mut self,
961 editor: ViewHandle<ConversationEditor>,
962 cx: &mut ViewContext<Self>,
963 ) {
964 self.subscriptions
965 .push(cx.subscribe(&editor, Self::handle_conversation_editor_event));
966
967 let conversation = editor.read(cx).conversation.clone();
968 self.subscriptions
969 .push(cx.observe(&conversation, |_, _, cx| cx.notify()));
970
971 let index = self.editors.len();
972 self.editors.push(editor);
973 self.set_active_editor_index(Some(index), cx);
974 }
975
976 fn set_active_editor_index(&mut self, index: Option<usize>, cx: &mut ViewContext<Self>) {
977 self.prev_active_editor_index = self.active_editor_index;
978 self.active_editor_index = index;
979 if let Some(editor) = self.active_editor() {
980 let editor = editor.read(cx).editor.clone();
981 self.toolbar.update(cx, |toolbar, cx| {
982 toolbar.set_active_item(Some(&editor), cx);
983 });
984 if self.has_focus(cx) {
985 cx.focus(&editor);
986 }
987 } else {
988 self.toolbar.update(cx, |toolbar, cx| {
989 toolbar.set_active_item(None, cx);
990 });
991 }
992
993 cx.notify();
994 }
995
996 fn handle_conversation_editor_event(
997 &mut self,
998 _: ViewHandle<ConversationEditor>,
999 event: &ConversationEditorEvent,
1000 cx: &mut ViewContext<Self>,
1001 ) {
1002 match event {
1003 ConversationEditorEvent::TabContentChanged => cx.notify(),
1004 }
1005 }
1006
1007 fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
1008 if let Some(api_key) = self
1009 .api_key_editor
1010 .as_ref()
1011 .map(|editor| editor.read(cx).text(cx))
1012 {
1013 if !api_key.is_empty() {
1014 cx.platform()
1015 .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
1016 .log_err();
1017 *self.api_key.borrow_mut() = Some(api_key);
1018 self.api_key_editor.take();
1019 cx.focus_self();
1020 cx.notify();
1021 }
1022 } else {
1023 cx.propagate_action();
1024 }
1025 }
1026
1027 fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
1028 cx.platform().delete_credentials(OPENAI_API_URL).log_err();
1029 self.api_key.take();
1030 self.api_key_editor = Some(build_api_key_editor(cx));
1031 cx.focus_self();
1032 cx.notify();
1033 }
1034
1035 fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
1036 if self.zoomed {
1037 cx.emit(AssistantPanelEvent::ZoomOut)
1038 } else {
1039 cx.emit(AssistantPanelEvent::ZoomIn)
1040 }
1041 }
1042
1043 fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
1044 let mut propagate_action = true;
1045 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
1046 search_bar.update(cx, |search_bar, cx| {
1047 if search_bar.show(cx) {
1048 search_bar.search_suggested(cx);
1049 if action.focus {
1050 search_bar.select_query(cx);
1051 cx.focus_self();
1052 }
1053 propagate_action = false
1054 }
1055 });
1056 }
1057 if propagate_action {
1058 cx.propagate_action();
1059 }
1060 }
1061
1062 fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
1063 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
1064 if !search_bar.read(cx).is_dismissed() {
1065 search_bar.update(cx, |search_bar, cx| {
1066 search_bar.dismiss(&Default::default(), cx)
1067 });
1068 return;
1069 }
1070 }
1071 cx.propagate_action();
1072 }
1073
1074 fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
1075 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
1076 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
1077 }
1078 }
1079
1080 fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
1081 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
1082 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
1083 }
1084 }
1085
1086 fn active_editor(&self) -> Option<&ViewHandle<ConversationEditor>> {
1087 self.editors.get(self.active_editor_index?)
1088 }
1089
1090 fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
1091 enum History {}
1092 let theme = theme::current(cx);
1093 let tooltip_style = theme::current(cx).tooltip.clone();
1094 MouseEventHandler::new::<History, _>(0, cx, |state, _| {
1095 let style = theme.assistant.hamburger_button.style_for(state);
1096 Svg::for_style(style.icon.clone())
1097 .contained()
1098 .with_style(style.container)
1099 })
1100 .with_cursor_style(CursorStyle::PointingHand)
1101 .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
1102 if this.active_editor().is_some() {
1103 this.set_active_editor_index(None, cx);
1104 } else {
1105 this.set_active_editor_index(this.prev_active_editor_index, cx);
1106 }
1107 })
1108 .with_tooltip::<History>(1, "History", None, tooltip_style, cx)
1109 }
1110
1111 fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement<Self>> {
1112 if self.active_editor().is_some() {
1113 vec![
1114 Self::render_split_button(cx).into_any(),
1115 Self::render_quote_button(cx).into_any(),
1116 Self::render_assist_button(cx).into_any(),
1117 ]
1118 } else {
1119 Default::default()
1120 }
1121 }
1122
1123 fn render_split_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
1124 let theme = theme::current(cx);
1125 let tooltip_style = theme::current(cx).tooltip.clone();
1126 MouseEventHandler::new::<Split, _>(0, cx, |state, _| {
1127 let style = theme.assistant.split_button.style_for(state);
1128 Svg::for_style(style.icon.clone())
1129 .contained()
1130 .with_style(style.container)
1131 })
1132 .with_cursor_style(CursorStyle::PointingHand)
1133 .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
1134 if let Some(active_editor) = this.active_editor() {
1135 active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
1136 }
1137 })
1138 .with_tooltip::<Split>(
1139 1,
1140 "Split Message",
1141 Some(Box::new(Split)),
1142 tooltip_style,
1143 cx,
1144 )
1145 }
1146
1147 fn render_assist_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
1148 let theme = theme::current(cx);
1149 let tooltip_style = theme::current(cx).tooltip.clone();
1150 MouseEventHandler::new::<Assist, _>(0, cx, |state, _| {
1151 let style = theme.assistant.assist_button.style_for(state);
1152 Svg::for_style(style.icon.clone())
1153 .contained()
1154 .with_style(style.container)
1155 })
1156 .with_cursor_style(CursorStyle::PointingHand)
1157 .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
1158 if let Some(active_editor) = this.active_editor() {
1159 active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
1160 }
1161 })
1162 .with_tooltip::<Assist>(1, "Assist", Some(Box::new(Assist)), tooltip_style, cx)
1163 }
1164
1165 fn render_quote_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
1166 let theme = theme::current(cx);
1167 let tooltip_style = theme::current(cx).tooltip.clone();
1168 MouseEventHandler::new::<QuoteSelection, _>(0, cx, |state, _| {
1169 let style = theme.assistant.quote_button.style_for(state);
1170 Svg::for_style(style.icon.clone())
1171 .contained()
1172 .with_style(style.container)
1173 })
1174 .with_cursor_style(CursorStyle::PointingHand)
1175 .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
1176 if let Some(workspace) = this.workspace.upgrade(cx) {
1177 cx.window_context().defer(move |cx| {
1178 workspace.update(cx, |workspace, cx| {
1179 ConversationEditor::quote_selection(workspace, &Default::default(), cx)
1180 });
1181 });
1182 }
1183 })
1184 .with_tooltip::<QuoteSelection>(
1185 1,
1186 "Quote Selection",
1187 Some(Box::new(QuoteSelection)),
1188 tooltip_style,
1189 cx,
1190 )
1191 }
1192
1193 fn render_plus_button(cx: &mut ViewContext<Self>) -> impl Element<Self> {
1194 let theme = theme::current(cx);
1195 let tooltip_style = theme::current(cx).tooltip.clone();
1196 MouseEventHandler::new::<NewConversation, _>(0, cx, |state, _| {
1197 let style = theme.assistant.plus_button.style_for(state);
1198 Svg::for_style(style.icon.clone())
1199 .contained()
1200 .with_style(style.container)
1201 })
1202 .with_cursor_style(CursorStyle::PointingHand)
1203 .on_click(MouseButton::Left, |_, this: &mut Self, cx| {
1204 this.new_conversation(cx);
1205 })
1206 .with_tooltip::<NewConversation>(
1207 1,
1208 "New Conversation",
1209 Some(Box::new(NewConversation)),
1210 tooltip_style,
1211 cx,
1212 )
1213 }
1214
1215 fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
1216 enum ToggleZoomButton {}
1217
1218 let theme = theme::current(cx);
1219 let tooltip_style = theme::current(cx).tooltip.clone();
1220 let style = if self.zoomed {
1221 &theme.assistant.zoom_out_button
1222 } else {
1223 &theme.assistant.zoom_in_button
1224 };
1225
1226 MouseEventHandler::new::<ToggleZoomButton, _>(0, cx, |state, _| {
1227 let style = style.style_for(state);
1228 Svg::for_style(style.icon.clone())
1229 .contained()
1230 .with_style(style.container)
1231 })
1232 .with_cursor_style(CursorStyle::PointingHand)
1233 .on_click(MouseButton::Left, |_, this, cx| {
1234 this.toggle_zoom(&ToggleZoom, cx);
1235 })
1236 .with_tooltip::<ToggleZoom>(
1237 0,
1238 if self.zoomed { "Zoom Out" } else { "Zoom In" },
1239 Some(Box::new(ToggleZoom)),
1240 tooltip_style,
1241 cx,
1242 )
1243 }
1244
1245 fn render_saved_conversation(
1246 &mut self,
1247 index: usize,
1248 cx: &mut ViewContext<Self>,
1249 ) -> impl Element<Self> {
1250 let conversation = &self.saved_conversations[index];
1251 let path = conversation.path.clone();
1252 MouseEventHandler::new::<SavedConversationMetadata, _>(index, cx, move |state, cx| {
1253 let style = &theme::current(cx).assistant.saved_conversation;
1254 Flex::row()
1255 .with_child(
1256 Label::new(
1257 conversation.mtime.format("%F %I:%M%p").to_string(),
1258 style.saved_at.text.clone(),
1259 )
1260 .aligned()
1261 .contained()
1262 .with_style(style.saved_at.container),
1263 )
1264 .with_child(
1265 Label::new(conversation.title.clone(), style.title.text.clone())
1266 .aligned()
1267 .contained()
1268 .with_style(style.title.container),
1269 )
1270 .contained()
1271 .with_style(*style.container.style_for(state))
1272 })
1273 .with_cursor_style(CursorStyle::PointingHand)
1274 .on_click(MouseButton::Left, move |_, this, cx| {
1275 this.open_conversation(path.clone(), cx)
1276 .detach_and_log_err(cx)
1277 })
1278 }
1279
1280 fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1281 if let Some(ix) = self.editor_index_for_path(&path, cx) {
1282 self.set_active_editor_index(Some(ix), cx);
1283 return Task::ready(Ok(()));
1284 }
1285
1286 let fs = self.fs.clone();
1287 let api_key = self.api_key.clone();
1288 let languages = self.languages.clone();
1289 cx.spawn(|this, mut cx| async move {
1290 let saved_conversation = fs.load(&path).await?;
1291 let saved_conversation = serde_json::from_str(&saved_conversation)?;
1292 let conversation = cx.add_model(|cx| {
1293 Conversation::deserialize(saved_conversation, path.clone(), api_key, languages, cx)
1294 });
1295 this.update(&mut cx, |this, cx| {
1296 // If, by the time we've loaded the conversation, the user has already opened
1297 // the same conversation, we don't want to open it again.
1298 if let Some(ix) = this.editor_index_for_path(&path, cx) {
1299 this.set_active_editor_index(Some(ix), cx);
1300 } else {
1301 let editor = cx
1302 .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx));
1303 this.add_conversation(editor, cx);
1304 }
1305 })?;
1306 Ok(())
1307 })
1308 }
1309
1310 fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option<usize> {
1311 self.editors
1312 .iter()
1313 .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
1314 }
1315
1316 fn load_api_key(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
1317 if self.api_key.borrow().is_none() && !self.has_read_credentials {
1318 self.has_read_credentials = true;
1319 let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
1320 Some(api_key)
1321 } else if let Some((_, api_key)) = cx
1322 .platform()
1323 .read_credentials(OPENAI_API_URL)
1324 .log_err()
1325 .flatten()
1326 {
1327 String::from_utf8(api_key).log_err()
1328 } else {
1329 None
1330 };
1331 if let Some(api_key) = api_key {
1332 *self.api_key.borrow_mut() = Some(api_key);
1333 } else if self.api_key_editor.is_none() {
1334 self.api_key_editor = Some(build_api_key_editor(cx));
1335 cx.notify();
1336 }
1337 }
1338
1339 self.api_key.borrow().clone()
1340 }
1341}
1342
1343fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
1344 cx.add_view(|cx| {
1345 let mut editor = Editor::single_line(
1346 Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())),
1347 cx,
1348 );
1349 editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
1350 editor
1351 })
1352}
1353
1354impl Entity for AssistantPanel {
1355 type Event = AssistantPanelEvent;
1356}
1357
1358impl View for AssistantPanel {
1359 fn ui_name() -> &'static str {
1360 "AssistantPanel"
1361 }
1362
1363 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1364 let theme = &theme::current(cx);
1365 let style = &theme.assistant;
1366 if let Some(api_key_editor) = self.api_key_editor.as_ref() {
1367 Flex::column()
1368 .with_child(
1369 Text::new(
1370 "Paste your OpenAI API key and press Enter to use the assistant",
1371 style.api_key_prompt.text.clone(),
1372 )
1373 .aligned(),
1374 )
1375 .with_child(
1376 ChildView::new(api_key_editor, cx)
1377 .contained()
1378 .with_style(style.api_key_editor.container)
1379 .aligned(),
1380 )
1381 .contained()
1382 .with_style(style.api_key_prompt.container)
1383 .aligned()
1384 .into_any()
1385 } else {
1386 let title = self.active_editor().map(|editor| {
1387 Label::new(editor.read(cx).title(cx), style.title.text.clone())
1388 .contained()
1389 .with_style(style.title.container)
1390 .aligned()
1391 .left()
1392 .flex(1., false)
1393 });
1394 let mut header = Flex::row()
1395 .with_child(Self::render_hamburger_button(cx).aligned())
1396 .with_children(title);
1397 if self.has_focus {
1398 header.add_children(
1399 self.render_editor_tools(cx)
1400 .into_iter()
1401 .map(|tool| tool.aligned().flex_float()),
1402 );
1403 header.add_child(Self::render_plus_button(cx).aligned().flex_float());
1404 header.add_child(self.render_zoom_button(cx).aligned());
1405 }
1406
1407 Flex::column()
1408 .with_child(
1409 header
1410 .contained()
1411 .with_style(theme.workspace.tab_bar.container)
1412 .expanded()
1413 .constrained()
1414 .with_height(theme.workspace.tab_bar.height),
1415 )
1416 .with_children(if self.toolbar.read(cx).hidden() {
1417 None
1418 } else {
1419 Some(ChildView::new(&self.toolbar, cx).expanded())
1420 })
1421 .with_child(if let Some(editor) = self.active_editor() {
1422 ChildView::new(editor, cx).flex(1., true).into_any()
1423 } else {
1424 UniformList::new(
1425 self.saved_conversations_list_state.clone(),
1426 self.saved_conversations.len(),
1427 cx,
1428 |this, range, items, cx| {
1429 for ix in range {
1430 items.push(this.render_saved_conversation(ix, cx).into_any());
1431 }
1432 },
1433 )
1434 .flex(1., true)
1435 .into_any()
1436 })
1437 .into_any()
1438 }
1439 }
1440
1441 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1442 self.has_focus = true;
1443 self.toolbar
1444 .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
1445 cx.notify();
1446 if cx.is_self_focused() {
1447 if let Some(editor) = self.active_editor() {
1448 cx.focus(editor);
1449 } else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
1450 cx.focus(api_key_editor);
1451 }
1452 }
1453 }
1454
1455 fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1456 self.has_focus = false;
1457 self.toolbar
1458 .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
1459 cx.notify();
1460 }
1461}
1462
1463impl Panel for AssistantPanel {
1464 fn position(&self, cx: &WindowContext) -> DockPosition {
1465 match settings::get::<AssistantSettings>(cx).dock {
1466 AssistantDockPosition::Left => DockPosition::Left,
1467 AssistantDockPosition::Bottom => DockPosition::Bottom,
1468 AssistantDockPosition::Right => DockPosition::Right,
1469 }
1470 }
1471
1472 fn position_is_valid(&self, _: DockPosition) -> bool {
1473 true
1474 }
1475
1476 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1477 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
1478 let dock = match position {
1479 DockPosition::Left => AssistantDockPosition::Left,
1480 DockPosition::Bottom => AssistantDockPosition::Bottom,
1481 DockPosition::Right => AssistantDockPosition::Right,
1482 };
1483 settings.dock = Some(dock);
1484 });
1485 }
1486
1487 fn size(&self, cx: &WindowContext) -> f32 {
1488 let settings = settings::get::<AssistantSettings>(cx);
1489 match self.position(cx) {
1490 DockPosition::Left | DockPosition::Right => {
1491 self.width.unwrap_or_else(|| settings.default_width)
1492 }
1493 DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
1494 }
1495 }
1496
1497 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
1498 match self.position(cx) {
1499 DockPosition::Left | DockPosition::Right => self.width = size,
1500 DockPosition::Bottom => self.height = size,
1501 }
1502 cx.notify();
1503 }
1504
1505 fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool {
1506 matches!(event, AssistantPanelEvent::ZoomIn)
1507 }
1508
1509 fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool {
1510 matches!(event, AssistantPanelEvent::ZoomOut)
1511 }
1512
1513 fn is_zoomed(&self, _: &WindowContext) -> bool {
1514 self.zoomed
1515 }
1516
1517 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1518 self.zoomed = zoomed;
1519 cx.notify();
1520 }
1521
1522 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1523 if active {
1524 self.load_api_key(cx);
1525
1526 if self.editors.is_empty() {
1527 self.new_conversation(cx);
1528 }
1529 }
1530 }
1531
1532 fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
1533 settings::get::<AssistantSettings>(cx)
1534 .button
1535 .then(|| "icons/ai.svg")
1536 }
1537
1538 fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
1539 ("Assistant Panel".into(), Some(Box::new(ToggleFocus)))
1540 }
1541
1542 fn should_change_position_on_event(event: &Self::Event) -> bool {
1543 matches!(event, AssistantPanelEvent::DockPositionChanged)
1544 }
1545
1546 fn should_activate_on_event(_: &Self::Event) -> bool {
1547 false
1548 }
1549
1550 fn should_close_on_event(event: &AssistantPanelEvent) -> bool {
1551 matches!(event, AssistantPanelEvent::Close)
1552 }
1553
1554 fn has_focus(&self, _: &WindowContext) -> bool {
1555 self.has_focus
1556 }
1557
1558 fn is_focus_event(event: &Self::Event) -> bool {
1559 matches!(event, AssistantPanelEvent::Focus)
1560 }
1561}
1562
1563enum ConversationEvent {
1564 MessagesEdited,
1565 SummaryChanged,
1566 StreamedCompletion,
1567}
1568
1569#[derive(Default)]
1570struct Summary {
1571 text: String,
1572 done: bool,
1573}
1574
1575struct Conversation {
1576 buffer: ModelHandle<Buffer>,
1577 message_anchors: Vec<MessageAnchor>,
1578 messages_metadata: HashMap<MessageId, MessageMetadata>,
1579 next_message_id: MessageId,
1580 summary: Option<Summary>,
1581 pending_summary: Task<Option<()>>,
1582 completion_count: usize,
1583 pending_completions: Vec<PendingCompletion>,
1584 model: OpenAIModel,
1585 token_count: Option<usize>,
1586 max_token_count: usize,
1587 pending_token_count: Task<Option<()>>,
1588 api_key: Rc<RefCell<Option<String>>>,
1589 pending_save: Task<Result<()>>,
1590 path: Option<PathBuf>,
1591 _subscriptions: Vec<Subscription>,
1592}
1593
1594impl Entity for Conversation {
1595 type Event = ConversationEvent;
1596}
1597
1598impl Conversation {
1599 fn new(
1600 api_key: Rc<RefCell<Option<String>>>,
1601 language_registry: Arc<LanguageRegistry>,
1602 cx: &mut ModelContext<Self>,
1603 ) -> Self {
1604 let markdown = language_registry.language_for_name("Markdown");
1605 let buffer = cx.add_model(|cx| {
1606 let mut buffer = Buffer::new(0, cx.model_id() as u64, "");
1607 buffer.set_language_registry(language_registry);
1608 cx.spawn_weak(|buffer, mut cx| async move {
1609 let markdown = markdown.await?;
1610 let buffer = buffer
1611 .upgrade(&cx)
1612 .ok_or_else(|| anyhow!("buffer was dropped"))?;
1613 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1614 buffer.set_language(Some(markdown), cx)
1615 });
1616 anyhow::Ok(())
1617 })
1618 .detach_and_log_err(cx);
1619 buffer
1620 });
1621
1622 let settings = settings::get::<AssistantSettings>(cx);
1623 let model = settings.default_open_ai_model.clone();
1624
1625 let mut this = Self {
1626 message_anchors: Default::default(),
1627 messages_metadata: Default::default(),
1628 next_message_id: Default::default(),
1629 summary: None,
1630 pending_summary: Task::ready(None),
1631 completion_count: Default::default(),
1632 pending_completions: Default::default(),
1633 token_count: None,
1634 max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1635 pending_token_count: Task::ready(None),
1636 model: model.clone(),
1637 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1638 pending_save: Task::ready(Ok(())),
1639 path: None,
1640 api_key,
1641 buffer,
1642 };
1643 let message = MessageAnchor {
1644 id: MessageId(post_inc(&mut this.next_message_id.0)),
1645 start: language::Anchor::MIN,
1646 };
1647 this.message_anchors.push(message.clone());
1648 this.messages_metadata.insert(
1649 message.id,
1650 MessageMetadata {
1651 role: Role::User,
1652 sent_at: Local::now(),
1653 status: MessageStatus::Done,
1654 },
1655 );
1656
1657 this.count_remaining_tokens(cx);
1658 this
1659 }
1660
1661 fn serialize(&self, cx: &AppContext) -> SavedConversation {
1662 SavedConversation {
1663 zed: "conversation".into(),
1664 version: SavedConversation::VERSION.into(),
1665 text: self.buffer.read(cx).text(),
1666 message_metadata: self.messages_metadata.clone(),
1667 messages: self
1668 .messages(cx)
1669 .map(|message| SavedMessage {
1670 id: message.id,
1671 start: message.offset_range.start,
1672 })
1673 .collect(),
1674 summary: self
1675 .summary
1676 .as_ref()
1677 .map(|summary| summary.text.clone())
1678 .unwrap_or_default(),
1679 model: self.model.clone(),
1680 }
1681 }
1682
1683 fn deserialize(
1684 saved_conversation: SavedConversation,
1685 path: PathBuf,
1686 api_key: Rc<RefCell<Option<String>>>,
1687 language_registry: Arc<LanguageRegistry>,
1688 cx: &mut ModelContext<Self>,
1689 ) -> Self {
1690 let model = saved_conversation.model;
1691 let markdown = language_registry.language_for_name("Markdown");
1692 let mut message_anchors = Vec::new();
1693 let mut next_message_id = MessageId(0);
1694 let buffer = cx.add_model(|cx| {
1695 let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text);
1696 for message in saved_conversation.messages {
1697 message_anchors.push(MessageAnchor {
1698 id: message.id,
1699 start: buffer.anchor_before(message.start),
1700 });
1701 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1702 }
1703 buffer.set_language_registry(language_registry);
1704 cx.spawn_weak(|buffer, mut cx| async move {
1705 let markdown = markdown.await?;
1706 let buffer = buffer
1707 .upgrade(&cx)
1708 .ok_or_else(|| anyhow!("buffer was dropped"))?;
1709 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1710 buffer.set_language(Some(markdown), cx)
1711 });
1712 anyhow::Ok(())
1713 })
1714 .detach_and_log_err(cx);
1715 buffer
1716 });
1717
1718 let mut this = Self {
1719 message_anchors,
1720 messages_metadata: saved_conversation.message_metadata,
1721 next_message_id,
1722 summary: Some(Summary {
1723 text: saved_conversation.summary,
1724 done: true,
1725 }),
1726 pending_summary: Task::ready(None),
1727 completion_count: Default::default(),
1728 pending_completions: Default::default(),
1729 token_count: None,
1730 max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1731 pending_token_count: Task::ready(None),
1732 model,
1733 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1734 pending_save: Task::ready(Ok(())),
1735 path: Some(path),
1736 api_key,
1737 buffer,
1738 };
1739 this.count_remaining_tokens(cx);
1740 this
1741 }
1742
1743 fn handle_buffer_event(
1744 &mut self,
1745 _: ModelHandle<Buffer>,
1746 event: &language::Event,
1747 cx: &mut ModelContext<Self>,
1748 ) {
1749 match event {
1750 language::Event::Edited => {
1751 self.count_remaining_tokens(cx);
1752 cx.emit(ConversationEvent::MessagesEdited);
1753 }
1754 _ => {}
1755 }
1756 }
1757
1758 fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1759 let messages = self
1760 .messages(cx)
1761 .into_iter()
1762 .filter_map(|message| {
1763 Some(tiktoken_rs::ChatCompletionRequestMessage {
1764 role: match message.role {
1765 Role::User => "user".into(),
1766 Role::Assistant => "assistant".into(),
1767 Role::System => "system".into(),
1768 },
1769 content: self
1770 .buffer
1771 .read(cx)
1772 .text_for_range(message.offset_range)
1773 .collect(),
1774 name: None,
1775 })
1776 })
1777 .collect::<Vec<_>>();
1778 let model = self.model.clone();
1779 self.pending_token_count = cx.spawn_weak(|this, mut cx| {
1780 async move {
1781 cx.background().timer(Duration::from_millis(200)).await;
1782 let token_count = cx
1783 .background()
1784 .spawn(async move {
1785 tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
1786 })
1787 .await?;
1788
1789 this.upgrade(&cx)
1790 .ok_or_else(|| anyhow!("conversation was dropped"))?
1791 .update(&mut cx, |this, cx| {
1792 this.max_token_count =
1793 tiktoken_rs::model::get_context_size(&this.model.full_name());
1794 this.token_count = Some(token_count);
1795 cx.notify()
1796 });
1797 anyhow::Ok(())
1798 }
1799 .log_err()
1800 });
1801 }
1802
1803 fn remaining_tokens(&self) -> Option<isize> {
1804 Some(self.max_token_count as isize - self.token_count? as isize)
1805 }
1806
1807 fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext<Self>) {
1808 self.model = model;
1809 self.count_remaining_tokens(cx);
1810 cx.notify();
1811 }
1812
1813 fn assist(
1814 &mut self,
1815 selected_messages: HashSet<MessageId>,
1816 cx: &mut ModelContext<Self>,
1817 ) -> Vec<MessageAnchor> {
1818 let mut user_messages = Vec::new();
1819
1820 let last_message_id = if let Some(last_message_id) =
1821 self.message_anchors.iter().rev().find_map(|message| {
1822 message
1823 .start
1824 .is_valid(self.buffer.read(cx))
1825 .then_some(message.id)
1826 }) {
1827 last_message_id
1828 } else {
1829 return Default::default();
1830 };
1831
1832 let mut should_assist = false;
1833 for selected_message_id in selected_messages {
1834 let selected_message_role =
1835 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1836 metadata.role
1837 } else {
1838 continue;
1839 };
1840
1841 if selected_message_role == Role::Assistant {
1842 if let Some(user_message) = self.insert_message_after(
1843 selected_message_id,
1844 Role::User,
1845 MessageStatus::Done,
1846 cx,
1847 ) {
1848 user_messages.push(user_message);
1849 }
1850 } else {
1851 should_assist = true;
1852 }
1853 }
1854
1855 if should_assist {
1856 let Some(api_key) = self.api_key.borrow().clone() else {
1857 return Default::default();
1858 };
1859
1860 let request = OpenAIRequest {
1861 model: self.model.full_name().to_string(),
1862 messages: self
1863 .messages(cx)
1864 .filter(|message| matches!(message.status, MessageStatus::Done))
1865 .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
1866 .collect(),
1867 stream: true,
1868 };
1869
1870 let stream = stream_completion(api_key, cx.background().clone(), request);
1871 let assistant_message = self
1872 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1873 .unwrap();
1874
1875 // Queue up the user's next reply.
1876 let user_message = self
1877 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1878 .unwrap();
1879 user_messages.push(user_message);
1880
1881 let task = cx.spawn_weak({
1882 |this, mut cx| async move {
1883 let assistant_message_id = assistant_message.id;
1884 let stream_completion = async {
1885 let mut messages = stream.await?;
1886
1887 while let Some(message) = messages.next().await {
1888 let mut message = message?;
1889 if let Some(choice) = message.choices.pop() {
1890 this.upgrade(&cx)
1891 .ok_or_else(|| anyhow!("conversation was dropped"))?
1892 .update(&mut cx, |this, cx| {
1893 let text: Arc<str> = choice.delta.content?.into();
1894 let message_ix =
1895 this.message_anchors.iter().position(|message| {
1896 message.id == assistant_message_id
1897 })?;
1898 this.buffer.update(cx, |buffer, cx| {
1899 let offset = this.message_anchors[message_ix + 1..]
1900 .iter()
1901 .find(|message| message.start.is_valid(buffer))
1902 .map_or(buffer.len(), |message| {
1903 message
1904 .start
1905 .to_offset(buffer)
1906 .saturating_sub(1)
1907 });
1908 buffer.edit([(offset..offset, text)], None, cx);
1909 });
1910 cx.emit(ConversationEvent::StreamedCompletion);
1911
1912 Some(())
1913 });
1914 }
1915 smol::future::yield_now().await;
1916 }
1917
1918 this.upgrade(&cx)
1919 .ok_or_else(|| anyhow!("conversation was dropped"))?
1920 .update(&mut cx, |this, cx| {
1921 this.pending_completions
1922 .retain(|completion| completion.id != this.completion_count);
1923 this.summarize(cx);
1924 });
1925
1926 anyhow::Ok(())
1927 };
1928
1929 let result = stream_completion.await;
1930 if let Some(this) = this.upgrade(&cx) {
1931 this.update(&mut cx, |this, cx| {
1932 if let Some(metadata) =
1933 this.messages_metadata.get_mut(&assistant_message.id)
1934 {
1935 match result {
1936 Ok(_) => {
1937 metadata.status = MessageStatus::Done;
1938 }
1939 Err(error) => {
1940 metadata.status =
1941 MessageStatus::Error(error.to_string().trim().into());
1942 }
1943 }
1944 cx.notify();
1945 }
1946 });
1947 }
1948 }
1949 });
1950
1951 self.pending_completions.push(PendingCompletion {
1952 id: post_inc(&mut self.completion_count),
1953 _task: task,
1954 });
1955 }
1956
1957 user_messages
1958 }
1959
1960 fn cancel_last_assist(&mut self) -> bool {
1961 self.pending_completions.pop().is_some()
1962 }
1963
1964 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1965 for id in ids {
1966 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1967 metadata.role.cycle();
1968 cx.emit(ConversationEvent::MessagesEdited);
1969 cx.notify();
1970 }
1971 }
1972 }
1973
1974 fn insert_message_after(
1975 &mut self,
1976 message_id: MessageId,
1977 role: Role,
1978 status: MessageStatus,
1979 cx: &mut ModelContext<Self>,
1980 ) -> Option<MessageAnchor> {
1981 if let Some(prev_message_ix) = self
1982 .message_anchors
1983 .iter()
1984 .position(|message| message.id == message_id)
1985 {
1986 // Find the next valid message after the one we were given.
1987 let mut next_message_ix = prev_message_ix + 1;
1988 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1989 if next_message.start.is_valid(self.buffer.read(cx)) {
1990 break;
1991 }
1992 next_message_ix += 1;
1993 }
1994
1995 let start = self.buffer.update(cx, |buffer, cx| {
1996 let offset = self
1997 .message_anchors
1998 .get(next_message_ix)
1999 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
2000 buffer.edit([(offset..offset, "\n")], None, cx);
2001 buffer.anchor_before(offset + 1)
2002 });
2003 let message = MessageAnchor {
2004 id: MessageId(post_inc(&mut self.next_message_id.0)),
2005 start,
2006 };
2007 self.message_anchors
2008 .insert(next_message_ix, message.clone());
2009 self.messages_metadata.insert(
2010 message.id,
2011 MessageMetadata {
2012 role,
2013 sent_at: Local::now(),
2014 status,
2015 },
2016 );
2017 cx.emit(ConversationEvent::MessagesEdited);
2018 Some(message)
2019 } else {
2020 None
2021 }
2022 }
2023
2024 fn split_message(
2025 &mut self,
2026 range: Range<usize>,
2027 cx: &mut ModelContext<Self>,
2028 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
2029 let start_message = self.message_for_offset(range.start, cx);
2030 let end_message = self.message_for_offset(range.end, cx);
2031 if let Some((start_message, end_message)) = start_message.zip(end_message) {
2032 // Prevent splitting when range spans multiple messages.
2033 if start_message.id != end_message.id {
2034 return (None, None);
2035 }
2036
2037 let message = start_message;
2038 let role = message.role;
2039 let mut edited_buffer = false;
2040
2041 let mut suffix_start = None;
2042 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
2043 {
2044 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
2045 suffix_start = Some(range.end + 1);
2046 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
2047 suffix_start = Some(range.end);
2048 }
2049 }
2050
2051 let suffix = if let Some(suffix_start) = suffix_start {
2052 MessageAnchor {
2053 id: MessageId(post_inc(&mut self.next_message_id.0)),
2054 start: self.buffer.read(cx).anchor_before(suffix_start),
2055 }
2056 } else {
2057 self.buffer.update(cx, |buffer, cx| {
2058 buffer.edit([(range.end..range.end, "\n")], None, cx);
2059 });
2060 edited_buffer = true;
2061 MessageAnchor {
2062 id: MessageId(post_inc(&mut self.next_message_id.0)),
2063 start: self.buffer.read(cx).anchor_before(range.end + 1),
2064 }
2065 };
2066
2067 self.message_anchors
2068 .insert(message.index_range.end + 1, suffix.clone());
2069 self.messages_metadata.insert(
2070 suffix.id,
2071 MessageMetadata {
2072 role,
2073 sent_at: Local::now(),
2074 status: MessageStatus::Done,
2075 },
2076 );
2077
2078 let new_messages =
2079 if range.start == range.end || range.start == message.offset_range.start {
2080 (None, Some(suffix))
2081 } else {
2082 let mut prefix_end = None;
2083 if range.start > message.offset_range.start
2084 && range.end < message.offset_range.end - 1
2085 {
2086 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
2087 prefix_end = Some(range.start + 1);
2088 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
2089 == Some('\n')
2090 {
2091 prefix_end = Some(range.start);
2092 }
2093 }
2094
2095 let selection = if let Some(prefix_end) = prefix_end {
2096 cx.emit(ConversationEvent::MessagesEdited);
2097 MessageAnchor {
2098 id: MessageId(post_inc(&mut self.next_message_id.0)),
2099 start: self.buffer.read(cx).anchor_before(prefix_end),
2100 }
2101 } else {
2102 self.buffer.update(cx, |buffer, cx| {
2103 buffer.edit([(range.start..range.start, "\n")], None, cx)
2104 });
2105 edited_buffer = true;
2106 MessageAnchor {
2107 id: MessageId(post_inc(&mut self.next_message_id.0)),
2108 start: self.buffer.read(cx).anchor_before(range.end + 1),
2109 }
2110 };
2111
2112 self.message_anchors
2113 .insert(message.index_range.end + 1, selection.clone());
2114 self.messages_metadata.insert(
2115 selection.id,
2116 MessageMetadata {
2117 role,
2118 sent_at: Local::now(),
2119 status: MessageStatus::Done,
2120 },
2121 );
2122 (Some(selection), Some(suffix))
2123 };
2124
2125 if !edited_buffer {
2126 cx.emit(ConversationEvent::MessagesEdited);
2127 }
2128 new_messages
2129 } else {
2130 (None, None)
2131 }
2132 }
2133
2134 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
2135 if self.message_anchors.len() >= 2 && self.summary.is_none() {
2136 let api_key = self.api_key.borrow().clone();
2137 if let Some(api_key) = api_key {
2138 let messages = self
2139 .messages(cx)
2140 .take(2)
2141 .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
2142 .chain(Some(RequestMessage {
2143 role: Role::User,
2144 content:
2145 "Summarize the conversation into a short title without punctuation"
2146 .into(),
2147 }));
2148 let request = OpenAIRequest {
2149 model: self.model.full_name().to_string(),
2150 messages: messages.collect(),
2151 stream: true,
2152 };
2153
2154 let stream = stream_completion(api_key, cx.background().clone(), request);
2155 self.pending_summary = cx.spawn(|this, mut cx| {
2156 async move {
2157 let mut messages = stream.await?;
2158
2159 while let Some(message) = messages.next().await {
2160 let mut message = message?;
2161 if let Some(choice) = message.choices.pop() {
2162 let text = choice.delta.content.unwrap_or_default();
2163 this.update(&mut cx, |this, cx| {
2164 this.summary
2165 .get_or_insert(Default::default())
2166 .text
2167 .push_str(&text);
2168 cx.emit(ConversationEvent::SummaryChanged);
2169 });
2170 }
2171 }
2172
2173 this.update(&mut cx, |this, cx| {
2174 if let Some(summary) = this.summary.as_mut() {
2175 summary.done = true;
2176 cx.emit(ConversationEvent::SummaryChanged);
2177 }
2178 });
2179
2180 anyhow::Ok(())
2181 }
2182 .log_err()
2183 });
2184 }
2185 }
2186 }
2187
2188 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
2189 self.messages_for_offsets([offset], cx).pop()
2190 }
2191
2192 fn messages_for_offsets(
2193 &self,
2194 offsets: impl IntoIterator<Item = usize>,
2195 cx: &AppContext,
2196 ) -> Vec<Message> {
2197 let mut result = Vec::new();
2198
2199 let mut messages = self.messages(cx).peekable();
2200 let mut offsets = offsets.into_iter().peekable();
2201 let mut current_message = messages.next();
2202 while let Some(offset) = offsets.next() {
2203 // Locate the message that contains the offset.
2204 while current_message.as_ref().map_or(false, |message| {
2205 !message.offset_range.contains(&offset) && messages.peek().is_some()
2206 }) {
2207 current_message = messages.next();
2208 }
2209 let Some(message) = current_message.as_ref() else {
2210 break;
2211 };
2212
2213 // Skip offsets that are in the same message.
2214 while offsets.peek().map_or(false, |offset| {
2215 message.offset_range.contains(offset) || messages.peek().is_none()
2216 }) {
2217 offsets.next();
2218 }
2219
2220 result.push(message.clone());
2221 }
2222 result
2223 }
2224
2225 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
2226 let buffer = self.buffer.read(cx);
2227 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
2228 iter::from_fn(move || {
2229 while let Some((start_ix, message_anchor)) = message_anchors.next() {
2230 let metadata = self.messages_metadata.get(&message_anchor.id)?;
2231 let message_start = message_anchor.start.to_offset(buffer);
2232 let mut message_end = None;
2233 let mut end_ix = start_ix;
2234 while let Some((_, next_message)) = message_anchors.peek() {
2235 if next_message.start.is_valid(buffer) {
2236 message_end = Some(next_message.start);
2237 break;
2238 } else {
2239 end_ix += 1;
2240 message_anchors.next();
2241 }
2242 }
2243 let message_end = message_end
2244 .unwrap_or(language::Anchor::MAX)
2245 .to_offset(buffer);
2246 return Some(Message {
2247 index_range: start_ix..end_ix,
2248 offset_range: message_start..message_end,
2249 id: message_anchor.id,
2250 anchor: message_anchor.start,
2251 role: metadata.role,
2252 sent_at: metadata.sent_at,
2253 status: metadata.status.clone(),
2254 });
2255 }
2256 None
2257 })
2258 }
2259
2260 fn save(
2261 &mut self,
2262 debounce: Option<Duration>,
2263 fs: Arc<dyn Fs>,
2264 cx: &mut ModelContext<Conversation>,
2265 ) {
2266 self.pending_save = cx.spawn(|this, mut cx| async move {
2267 if let Some(debounce) = debounce {
2268 cx.background().timer(debounce).await;
2269 }
2270
2271 let (old_path, summary) = this.read_with(&cx, |this, _| {
2272 let path = this.path.clone();
2273 let summary = if let Some(summary) = this.summary.as_ref() {
2274 if summary.done {
2275 Some(summary.text.clone())
2276 } else {
2277 None
2278 }
2279 } else {
2280 None
2281 };
2282 (path, summary)
2283 });
2284
2285 if let Some(summary) = summary {
2286 let conversation = this.read_with(&cx, |this, cx| this.serialize(cx));
2287 let path = if let Some(old_path) = old_path {
2288 old_path
2289 } else {
2290 let mut discriminant = 1;
2291 let mut new_path;
2292 loop {
2293 new_path = CONVERSATIONS_DIR.join(&format!(
2294 "{} - {}.zed.json",
2295 summary.trim(),
2296 discriminant
2297 ));
2298 if fs.is_file(&new_path).await {
2299 discriminant += 1;
2300 } else {
2301 break;
2302 }
2303 }
2304 new_path
2305 };
2306
2307 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
2308 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
2309 .await?;
2310 this.update(&mut cx, |this, _| this.path = Some(path));
2311 }
2312
2313 Ok(())
2314 });
2315 }
2316}
2317
2318struct PendingCompletion {
2319 id: usize,
2320 _task: Task<()>,
2321}
2322
2323enum ConversationEditorEvent {
2324 TabContentChanged,
2325}
2326
2327#[derive(Copy, Clone, Debug, PartialEq)]
2328struct ScrollPosition {
2329 offset_before_cursor: Vector2F,
2330 cursor: Anchor,
2331}
2332
2333struct ConversationEditor {
2334 conversation: ModelHandle<Conversation>,
2335 fs: Arc<dyn Fs>,
2336 editor: ViewHandle<Editor>,
2337 blocks: HashSet<BlockId>,
2338 scroll_position: Option<ScrollPosition>,
2339 _subscriptions: Vec<Subscription>,
2340}
2341
2342impl ConversationEditor {
2343 fn new(
2344 api_key: Rc<RefCell<Option<String>>>,
2345 language_registry: Arc<LanguageRegistry>,
2346 fs: Arc<dyn Fs>,
2347 cx: &mut ViewContext<Self>,
2348 ) -> Self {
2349 let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
2350 Self::for_conversation(conversation, fs, cx)
2351 }
2352
2353 fn for_conversation(
2354 conversation: ModelHandle<Conversation>,
2355 fs: Arc<dyn Fs>,
2356 cx: &mut ViewContext<Self>,
2357 ) -> Self {
2358 let editor = cx.add_view(|cx| {
2359 let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2360 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2361 editor.set_show_gutter(false, cx);
2362 editor.set_show_wrap_guides(false, cx);
2363 editor
2364 });
2365
2366 let _subscriptions = vec![
2367 cx.observe(&conversation, |_, _, cx| cx.notify()),
2368 cx.subscribe(&conversation, Self::handle_conversation_event),
2369 cx.subscribe(&editor, Self::handle_editor_event),
2370 ];
2371
2372 let mut this = Self {
2373 conversation,
2374 editor,
2375 blocks: Default::default(),
2376 scroll_position: None,
2377 fs,
2378 _subscriptions,
2379 };
2380 this.update_message_headers(cx);
2381 this
2382 }
2383
2384 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2385 let cursors = self.cursors(cx);
2386
2387 let user_messages = self.conversation.update(cx, |conversation, cx| {
2388 let selected_messages = conversation
2389 .messages_for_offsets(cursors, cx)
2390 .into_iter()
2391 .map(|message| message.id)
2392 .collect();
2393 conversation.assist(selected_messages, cx)
2394 });
2395 let new_selections = user_messages
2396 .iter()
2397 .map(|message| {
2398 let cursor = message
2399 .start
2400 .to_offset(self.conversation.read(cx).buffer.read(cx));
2401 cursor..cursor
2402 })
2403 .collect::<Vec<_>>();
2404 if !new_selections.is_empty() {
2405 self.editor.update(cx, |editor, cx| {
2406 editor.change_selections(
2407 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2408 cx,
2409 |selections| selections.select_ranges(new_selections),
2410 );
2411 });
2412 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2413 cx.defer(|this, _| this.scroll_position = None);
2414 }
2415 }
2416
2417 fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
2418 if !self
2419 .conversation
2420 .update(cx, |conversation, _| conversation.cancel_last_assist())
2421 {
2422 cx.propagate_action();
2423 }
2424 }
2425
2426 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2427 let cursors = self.cursors(cx);
2428 self.conversation.update(cx, |conversation, cx| {
2429 let messages = conversation
2430 .messages_for_offsets(cursors, cx)
2431 .into_iter()
2432 .map(|message| message.id)
2433 .collect();
2434 conversation.cycle_message_roles(messages, cx)
2435 });
2436 }
2437
2438 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2439 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2440 selections
2441 .into_iter()
2442 .map(|selection| selection.head())
2443 .collect()
2444 }
2445
2446 fn handle_conversation_event(
2447 &mut self,
2448 _: ModelHandle<Conversation>,
2449 event: &ConversationEvent,
2450 cx: &mut ViewContext<Self>,
2451 ) {
2452 match event {
2453 ConversationEvent::MessagesEdited => {
2454 self.update_message_headers(cx);
2455 self.conversation.update(cx, |conversation, cx| {
2456 conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2457 });
2458 }
2459 ConversationEvent::SummaryChanged => {
2460 cx.emit(ConversationEditorEvent::TabContentChanged);
2461 self.conversation.update(cx, |conversation, cx| {
2462 conversation.save(None, self.fs.clone(), cx);
2463 });
2464 }
2465 ConversationEvent::StreamedCompletion => {
2466 self.editor.update(cx, |editor, cx| {
2467 if let Some(scroll_position) = self.scroll_position {
2468 let snapshot = editor.snapshot(cx);
2469 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2470 let scroll_top =
2471 cursor_point.row() as f32 - scroll_position.offset_before_cursor.y();
2472 editor.set_scroll_position(
2473 vec2f(scroll_position.offset_before_cursor.x(), scroll_top),
2474 cx,
2475 );
2476 }
2477 });
2478 }
2479 }
2480 }
2481
2482 fn handle_editor_event(
2483 &mut self,
2484 _: ViewHandle<Editor>,
2485 event: &editor::Event,
2486 cx: &mut ViewContext<Self>,
2487 ) {
2488 match event {
2489 editor::Event::ScrollPositionChanged { autoscroll, .. } => {
2490 let cursor_scroll_position = self.cursor_scroll_position(cx);
2491 if *autoscroll {
2492 self.scroll_position = cursor_scroll_position;
2493 } else if self.scroll_position != cursor_scroll_position {
2494 self.scroll_position = None;
2495 }
2496 }
2497 editor::Event::SelectionsChanged { .. } => {
2498 self.scroll_position = self.cursor_scroll_position(cx);
2499 }
2500 _ => {}
2501 }
2502 }
2503
2504 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2505 self.editor.update(cx, |editor, cx| {
2506 let snapshot = editor.snapshot(cx);
2507 let cursor = editor.selections.newest_anchor().head();
2508 let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
2509 let scroll_position = editor
2510 .scroll_manager
2511 .anchor()
2512 .scroll_position(&snapshot.display_snapshot);
2513
2514 let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
2515 if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
2516 Some(ScrollPosition {
2517 cursor,
2518 offset_before_cursor: vec2f(
2519 scroll_position.x(),
2520 cursor_row - scroll_position.y(),
2521 ),
2522 })
2523 } else {
2524 None
2525 }
2526 })
2527 }
2528
2529 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2530 self.editor.update(cx, |editor, cx| {
2531 let buffer = editor.buffer().read(cx).snapshot(cx);
2532 let excerpt_id = *buffer.as_singleton().unwrap().0;
2533 let old_blocks = std::mem::take(&mut self.blocks);
2534 let new_blocks = self
2535 .conversation
2536 .read(cx)
2537 .messages(cx)
2538 .map(|message| BlockProperties {
2539 position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
2540 height: 2,
2541 style: BlockStyle::Sticky,
2542 render: Arc::new({
2543 let conversation = self.conversation.clone();
2544 // let metadata = message.metadata.clone();
2545 // let message = message.clone();
2546 move |cx| {
2547 enum Sender {}
2548 enum ErrorTooltip {}
2549
2550 let theme = theme::current(cx);
2551 let style = &theme.assistant;
2552 let message_id = message.id;
2553 let sender = MouseEventHandler::new::<Sender, _>(
2554 message_id.0,
2555 cx,
2556 |state, _| match message.role {
2557 Role::User => {
2558 let style = style.user_sender.style_for(state);
2559 Label::new("You", style.text.clone())
2560 .contained()
2561 .with_style(style.container)
2562 }
2563 Role::Assistant => {
2564 let style = style.assistant_sender.style_for(state);
2565 Label::new("Assistant", style.text.clone())
2566 .contained()
2567 .with_style(style.container)
2568 }
2569 Role::System => {
2570 let style = style.system_sender.style_for(state);
2571 Label::new("System", style.text.clone())
2572 .contained()
2573 .with_style(style.container)
2574 }
2575 },
2576 )
2577 .with_cursor_style(CursorStyle::PointingHand)
2578 .on_down(MouseButton::Left, {
2579 let conversation = conversation.clone();
2580 move |_, _, cx| {
2581 conversation.update(cx, |conversation, cx| {
2582 conversation.cycle_message_roles(
2583 HashSet::from_iter(Some(message_id)),
2584 cx,
2585 )
2586 })
2587 }
2588 });
2589
2590 Flex::row()
2591 .with_child(sender.aligned())
2592 .with_child(
2593 Label::new(
2594 message.sent_at.format("%I:%M%P").to_string(),
2595 style.sent_at.text.clone(),
2596 )
2597 .contained()
2598 .with_style(style.sent_at.container)
2599 .aligned(),
2600 )
2601 .with_children(
2602 if let MessageStatus::Error(error) = &message.status {
2603 Some(
2604 Svg::new("icons/circle_x_mark_12.svg")
2605 .with_color(style.error_icon.color)
2606 .constrained()
2607 .with_width(style.error_icon.width)
2608 .contained()
2609 .with_style(style.error_icon.container)
2610 .with_tooltip::<ErrorTooltip>(
2611 message_id.0,
2612 error.to_string(),
2613 None,
2614 theme.tooltip.clone(),
2615 cx,
2616 )
2617 .aligned(),
2618 )
2619 } else {
2620 None
2621 },
2622 )
2623 .aligned()
2624 .left()
2625 .contained()
2626 .with_style(style.message_header)
2627 .into_any()
2628 }
2629 }),
2630 disposition: BlockDisposition::Above,
2631 })
2632 .collect::<Vec<_>>();
2633
2634 editor.remove_blocks(old_blocks, None, cx);
2635 let ids = editor.insert_blocks(new_blocks, None, cx);
2636 self.blocks = HashSet::from_iter(ids);
2637 });
2638 }
2639
2640 fn quote_selection(
2641 workspace: &mut Workspace,
2642 _: &QuoteSelection,
2643 cx: &mut ViewContext<Workspace>,
2644 ) {
2645 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2646 return;
2647 };
2648 let Some(editor) = workspace
2649 .active_item(cx)
2650 .and_then(|item| item.act_as::<Editor>(cx))
2651 else {
2652 return;
2653 };
2654
2655 let text = editor.read_with(cx, |editor, cx| {
2656 let range = editor.selections.newest::<usize>(cx).range();
2657 let buffer = editor.buffer().read(cx).snapshot(cx);
2658 let start_language = buffer.language_at(range.start);
2659 let end_language = buffer.language_at(range.end);
2660 let language_name = if start_language == end_language {
2661 start_language.map(|language| language.name())
2662 } else {
2663 None
2664 };
2665 let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
2666
2667 let selected_text = buffer.text_for_range(range).collect::<String>();
2668 if selected_text.is_empty() {
2669 None
2670 } else {
2671 Some(if language_name == "markdown" {
2672 selected_text
2673 .lines()
2674 .map(|line| format!("> {}", line))
2675 .collect::<Vec<_>>()
2676 .join("\n")
2677 } else {
2678 format!("```{language_name}\n{selected_text}\n```")
2679 })
2680 }
2681 });
2682
2683 // Activate the panel
2684 if !panel.read(cx).has_focus(cx) {
2685 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2686 }
2687
2688 if let Some(text) = text {
2689 panel.update(cx, |panel, cx| {
2690 let conversation = panel
2691 .active_editor()
2692 .cloned()
2693 .unwrap_or_else(|| panel.new_conversation(cx));
2694 conversation.update(cx, |conversation, cx| {
2695 conversation
2696 .editor
2697 .update(cx, |editor, cx| editor.insert(&text, cx))
2698 });
2699 });
2700 }
2701 }
2702
2703 fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext<Self>) {
2704 let editor = self.editor.read(cx);
2705 let conversation = self.conversation.read(cx);
2706 if editor.selections.count() == 1 {
2707 let selection = editor.selections.newest::<usize>(cx);
2708 let mut copied_text = String::new();
2709 let mut spanned_messages = 0;
2710 for message in conversation.messages(cx) {
2711 if message.offset_range.start >= selection.range().end {
2712 break;
2713 } else if message.offset_range.end >= selection.range().start {
2714 let range = cmp::max(message.offset_range.start, selection.range().start)
2715 ..cmp::min(message.offset_range.end, selection.range().end);
2716 if !range.is_empty() {
2717 spanned_messages += 1;
2718 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2719 for chunk in conversation.buffer.read(cx).text_for_range(range) {
2720 copied_text.push_str(&chunk);
2721 }
2722 copied_text.push('\n');
2723 }
2724 }
2725 }
2726
2727 if spanned_messages > 1 {
2728 cx.platform()
2729 .write_to_clipboard(ClipboardItem::new(copied_text));
2730 return;
2731 }
2732 }
2733
2734 cx.propagate_action();
2735 }
2736
2737 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2738 self.conversation.update(cx, |conversation, cx| {
2739 let selections = self.editor.read(cx).selections.disjoint_anchors();
2740 for selection in selections.into_iter() {
2741 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2742 let range = selection
2743 .map(|endpoint| endpoint.to_offset(&buffer))
2744 .range();
2745 conversation.split_message(range, cx);
2746 }
2747 });
2748 }
2749
2750 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2751 self.conversation.update(cx, |conversation, cx| {
2752 conversation.save(None, self.fs.clone(), cx)
2753 });
2754 }
2755
2756 fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
2757 self.conversation.update(cx, |conversation, cx| {
2758 let new_model = conversation.model.cycle();
2759 conversation.set_model(new_model, cx);
2760 });
2761 }
2762
2763 fn title(&self, cx: &AppContext) -> String {
2764 self.conversation
2765 .read(cx)
2766 .summary
2767 .as_ref()
2768 .map(|summary| summary.text.clone())
2769 .unwrap_or_else(|| "New Conversation".into())
2770 }
2771
2772 fn render_current_model(
2773 &self,
2774 style: &AssistantStyle,
2775 cx: &mut ViewContext<Self>,
2776 ) -> impl Element<Self> {
2777 enum Model {}
2778
2779 MouseEventHandler::new::<Model, _>(0, cx, |state, cx| {
2780 let style = style.model.style_for(state);
2781 let model_display_name = self.conversation.read(cx).model.short_name();
2782 Label::new(model_display_name, style.text.clone())
2783 .contained()
2784 .with_style(style.container)
2785 })
2786 .with_cursor_style(CursorStyle::PointingHand)
2787 .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx))
2788 }
2789
2790 fn render_remaining_tokens(
2791 &self,
2792 style: &AssistantStyle,
2793 cx: &mut ViewContext<Self>,
2794 ) -> Option<impl Element<Self>> {
2795 let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
2796 let remaining_tokens_style = if remaining_tokens <= 0 {
2797 &style.no_remaining_tokens
2798 } else if remaining_tokens <= 500 {
2799 &style.low_remaining_tokens
2800 } else {
2801 &style.remaining_tokens
2802 };
2803 Some(
2804 Label::new(
2805 remaining_tokens.to_string(),
2806 remaining_tokens_style.text.clone(),
2807 )
2808 .contained()
2809 .with_style(remaining_tokens_style.container),
2810 )
2811 }
2812}
2813
2814impl Entity for ConversationEditor {
2815 type Event = ConversationEditorEvent;
2816}
2817
2818impl View for ConversationEditor {
2819 fn ui_name() -> &'static str {
2820 "ConversationEditor"
2821 }
2822
2823 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2824 let theme = &theme::current(cx).assistant;
2825 Stack::new()
2826 .with_child(
2827 ChildView::new(&self.editor, cx)
2828 .contained()
2829 .with_style(theme.container),
2830 )
2831 .with_child(
2832 Flex::row()
2833 .with_child(self.render_current_model(theme, cx))
2834 .with_children(self.render_remaining_tokens(theme, cx))
2835 .aligned()
2836 .top()
2837 .right(),
2838 )
2839 .into_any()
2840 }
2841
2842 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2843 if cx.is_self_focused() {
2844 cx.focus(&self.editor);
2845 }
2846 }
2847}
2848
2849#[derive(Clone, Debug)]
2850struct MessageAnchor {
2851 id: MessageId,
2852 start: language::Anchor,
2853}
2854
2855#[derive(Clone, Debug)]
2856pub struct Message {
2857 offset_range: Range<usize>,
2858 index_range: Range<usize>,
2859 id: MessageId,
2860 anchor: language::Anchor,
2861 role: Role,
2862 sent_at: DateTime<Local>,
2863 status: MessageStatus,
2864}
2865
2866impl Message {
2867 fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage {
2868 let content = buffer
2869 .text_for_range(self.offset_range.clone())
2870 .collect::<String>();
2871 RequestMessage {
2872 role: self.role,
2873 content: content.trim_end().into(),
2874 }
2875 }
2876}
2877
2878enum InlineAssistantEvent {
2879 Confirmed {
2880 prompt: String,
2881 include_conversation: bool,
2882 },
2883 Canceled,
2884 Dismissed,
2885 IncludeConversationToggled {
2886 include_conversation: bool,
2887 },
2888}
2889
2890#[derive(Copy, Clone)]
2891enum InlineAssistKind {
2892 Transform,
2893 Generate,
2894}
2895
2896struct InlineAssistant {
2897 id: usize,
2898 prompt_editor: ViewHandle<Editor>,
2899 confirmed: bool,
2900 has_focus: bool,
2901 include_conversation: bool,
2902 measurements: Rc<Cell<BlockMeasurements>>,
2903 error: Option<anyhow::Error>,
2904 prompt_history: VecDeque<String>,
2905 prompt_history_ix: Option<usize>,
2906 pending_prompt: String,
2907 _subscription: Subscription,
2908}
2909
2910impl Entity for InlineAssistant {
2911 type Event = InlineAssistantEvent;
2912}
2913
2914impl View for InlineAssistant {
2915 fn ui_name() -> &'static str {
2916 "InlineAssistant"
2917 }
2918
2919 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
2920 enum ErrorIcon {}
2921 let theme = theme::current(cx);
2922
2923 Flex::row()
2924 .with_child(
2925 Flex::row()
2926 .with_child(
2927 Button::action(ToggleIncludeConversation)
2928 .with_tooltip("Include Conversation", theme.tooltip.clone())
2929 .with_id(self.id)
2930 .with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
2931 .toggleable(self.include_conversation)
2932 .with_style(theme.assistant.inline.include_conversation.clone())
2933 .element()
2934 .aligned(),
2935 )
2936 .with_children(if let Some(error) = self.error.as_ref() {
2937 Some(
2938 Svg::new("icons/circle_x_mark_12.svg")
2939 .with_color(theme.assistant.error_icon.color)
2940 .constrained()
2941 .with_width(theme.assistant.error_icon.width)
2942 .contained()
2943 .with_style(theme.assistant.error_icon.container)
2944 .with_tooltip::<ErrorIcon>(
2945 self.id,
2946 error.to_string(),
2947 None,
2948 theme.tooltip.clone(),
2949 cx,
2950 )
2951 .aligned(),
2952 )
2953 } else {
2954 None
2955 })
2956 .aligned()
2957 .constrained()
2958 .dynamically({
2959 let measurements = self.measurements.clone();
2960 move |constraint, _, _| {
2961 let measurements = measurements.get();
2962 SizeConstraint {
2963 min: vec2f(measurements.gutter_width, constraint.min.y()),
2964 max: vec2f(measurements.gutter_width, constraint.max.y()),
2965 }
2966 }
2967 }),
2968 )
2969 .with_child(Empty::new().constrained().dynamically({
2970 let measurements = self.measurements.clone();
2971 move |constraint, _, _| {
2972 let measurements = measurements.get();
2973 SizeConstraint {
2974 min: vec2f(
2975 measurements.anchor_x - measurements.gutter_width,
2976 constraint.min.y(),
2977 ),
2978 max: vec2f(
2979 measurements.anchor_x - measurements.gutter_width,
2980 constraint.max.y(),
2981 ),
2982 }
2983 }
2984 }))
2985 .with_child(
2986 ChildView::new(&self.prompt_editor, cx)
2987 .aligned()
2988 .left()
2989 .flex(1., true),
2990 )
2991 .contained()
2992 .with_style(theme.assistant.inline.container)
2993 .into_any()
2994 .into_any()
2995 }
2996
2997 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2998 cx.focus(&self.prompt_editor);
2999 self.has_focus = true;
3000 }
3001
3002 fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
3003 self.has_focus = false;
3004 }
3005}
3006
3007impl InlineAssistant {
3008 fn new(
3009 id: usize,
3010 kind: InlineAssistKind,
3011 measurements: Rc<Cell<BlockMeasurements>>,
3012 include_conversation: bool,
3013 prompt_history: VecDeque<String>,
3014 cx: &mut ViewContext<Self>,
3015 ) -> Self {
3016 let prompt_editor = cx.add_view(|cx| {
3017 let mut editor = Editor::single_line(
3018 Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
3019 cx,
3020 );
3021 let placeholder = match kind {
3022 InlineAssistKind::Transform => "Enter transformation prompt…",
3023 InlineAssistKind::Generate => "Enter generation prompt…",
3024 };
3025 editor.set_placeholder_text(placeholder, cx);
3026 editor
3027 });
3028 let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events);
3029 Self {
3030 id,
3031 prompt_editor,
3032 confirmed: false,
3033 has_focus: false,
3034 include_conversation,
3035 measurements,
3036 error: None,
3037 prompt_history,
3038 prompt_history_ix: None,
3039 pending_prompt: String::new(),
3040 _subscription: subscription,
3041 }
3042 }
3043
3044 fn handle_prompt_editor_events(
3045 &mut self,
3046 _: ViewHandle<Editor>,
3047 event: &editor::Event,
3048 cx: &mut ViewContext<Self>,
3049 ) {
3050 if let editor::Event::Edited = event {
3051 self.pending_prompt = self.prompt_editor.read(cx).text(cx);
3052 cx.notify();
3053 }
3054 }
3055
3056 fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
3057 cx.emit(InlineAssistantEvent::Canceled);
3058 }
3059
3060 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
3061 if self.confirmed {
3062 cx.emit(InlineAssistantEvent::Dismissed);
3063 } else {
3064 let prompt = self.prompt_editor.read(cx).text(cx);
3065 self.prompt_editor.update(cx, |editor, cx| {
3066 editor.set_read_only(true);
3067 editor.set_field_editor_style(
3068 Some(Arc::new(|theme| {
3069 theme.assistant.inline.disabled_editor.clone()
3070 })),
3071 cx,
3072 );
3073 });
3074 cx.emit(InlineAssistantEvent::Confirmed {
3075 prompt,
3076 include_conversation: self.include_conversation,
3077 });
3078 self.confirmed = true;
3079 self.error = None;
3080 cx.notify();
3081 }
3082 }
3083
3084 fn toggle_include_conversation(
3085 &mut self,
3086 _: &ToggleIncludeConversation,
3087 cx: &mut ViewContext<Self>,
3088 ) {
3089 self.include_conversation = !self.include_conversation;
3090 cx.emit(InlineAssistantEvent::IncludeConversationToggled {
3091 include_conversation: self.include_conversation,
3092 });
3093 cx.notify();
3094 }
3095
3096 fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext<Self>) {
3097 self.error = Some(error);
3098 self.confirmed = false;
3099 self.prompt_editor.update(cx, |editor, cx| {
3100 editor.set_read_only(false);
3101 editor.set_field_editor_style(
3102 Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
3103 cx,
3104 );
3105 });
3106 cx.notify();
3107 }
3108
3109 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
3110 if let Some(ix) = self.prompt_history_ix {
3111 if ix > 0 {
3112 self.prompt_history_ix = Some(ix - 1);
3113 let prompt = self.prompt_history[ix - 1].clone();
3114 self.set_prompt(&prompt, cx);
3115 }
3116 } else if !self.prompt_history.is_empty() {
3117 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
3118 let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
3119 self.set_prompt(&prompt, cx);
3120 }
3121 }
3122
3123 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
3124 if let Some(ix) = self.prompt_history_ix {
3125 if ix < self.prompt_history.len() - 1 {
3126 self.prompt_history_ix = Some(ix + 1);
3127 let prompt = self.prompt_history[ix + 1].clone();
3128 self.set_prompt(&prompt, cx);
3129 } else {
3130 self.prompt_history_ix = None;
3131 let pending_prompt = self.pending_prompt.clone();
3132 self.set_prompt(&pending_prompt, cx);
3133 }
3134 }
3135 }
3136
3137 fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
3138 self.prompt_editor.update(cx, |editor, cx| {
3139 editor.buffer().update(cx, |buffer, cx| {
3140 let len = buffer.len(cx);
3141 buffer.edit([(0..len, prompt)], None, cx);
3142 });
3143 });
3144 }
3145}
3146
3147// This wouldn't need to exist if we could pass parameters when rendering child views.
3148#[derive(Copy, Clone, Default)]
3149struct BlockMeasurements {
3150 anchor_x: f32,
3151 gutter_width: f32,
3152}
3153
3154struct PendingInlineAssist {
3155 kind: InlineAssistKind,
3156 editor: WeakViewHandle<Editor>,
3157 range: Range<Anchor>,
3158 highlighted_ranges: Vec<Range<Anchor>>,
3159 inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
3160 code_generation: Task<Option<()>>,
3161 transaction_id: Option<TransactionId>,
3162 _subscriptions: Vec<Subscription>,
3163}
3164
3165fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
3166 ranges.sort_unstable_by(|a, b| {
3167 a.start
3168 .cmp(&b.start, buffer)
3169 .then_with(|| b.end.cmp(&a.end, buffer))
3170 });
3171
3172 let mut ix = 0;
3173 while ix + 1 < ranges.len() {
3174 let b = ranges[ix + 1].clone();
3175 let a = &mut ranges[ix];
3176 if a.end.cmp(&b.start, buffer).is_gt() {
3177 if a.end.cmp(&b.end, buffer).is_lt() {
3178 a.end = b.end;
3179 }
3180 ranges.remove(ix + 1);
3181 } else {
3182 ix += 1;
3183 }
3184 }
3185}
3186
3187fn strip_markdown_codeblock(
3188 stream: impl Stream<Item = Result<String>>,
3189) -> impl Stream<Item = Result<String>> {
3190 let mut first_line = true;
3191 let mut buffer = String::new();
3192 let mut starts_with_fenced_code_block = false;
3193 stream.filter_map(move |chunk| {
3194 let chunk = match chunk {
3195 Ok(chunk) => chunk,
3196 Err(err) => return future::ready(Some(Err(err))),
3197 };
3198 buffer.push_str(&chunk);
3199
3200 if first_line {
3201 if buffer == "" || buffer == "`" || buffer == "``" {
3202 return future::ready(None);
3203 } else if buffer.starts_with("```") {
3204 starts_with_fenced_code_block = true;
3205 if let Some(newline_ix) = buffer.find('\n') {
3206 buffer.replace_range(..newline_ix + 1, "");
3207 first_line = false;
3208 } else {
3209 return future::ready(None);
3210 }
3211 }
3212 }
3213
3214 let text = if starts_with_fenced_code_block {
3215 buffer
3216 .strip_suffix("\n```\n")
3217 .or_else(|| buffer.strip_suffix("\n```"))
3218 .or_else(|| buffer.strip_suffix("\n``"))
3219 .or_else(|| buffer.strip_suffix("\n`"))
3220 .or_else(|| buffer.strip_suffix('\n'))
3221 .unwrap_or(&buffer)
3222 } else {
3223 &buffer
3224 };
3225
3226 if text.contains('\n') {
3227 first_line = false;
3228 }
3229
3230 let remainder = buffer.split_off(text.len());
3231 let result = if buffer.is_empty() {
3232 None
3233 } else {
3234 Some(Ok(buffer.clone()))
3235 };
3236 buffer = remainder;
3237 future::ready(result)
3238 })
3239}
3240
3241#[cfg(test)]
3242mod tests {
3243 use super::*;
3244 use crate::MessageId;
3245 use futures::stream;
3246 use gpui::AppContext;
3247
3248 #[gpui::test]
3249 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3250 cx.set_global(SettingsStore::test(cx));
3251 init(cx);
3252 let registry = Arc::new(LanguageRegistry::test());
3253 let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
3254 let buffer = conversation.read(cx).buffer.clone();
3255
3256 let message_1 = conversation.read(cx).message_anchors[0].clone();
3257 assert_eq!(
3258 messages(&conversation, cx),
3259 vec![(message_1.id, Role::User, 0..0)]
3260 );
3261
3262 let message_2 = conversation.update(cx, |conversation, cx| {
3263 conversation
3264 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3265 .unwrap()
3266 });
3267 assert_eq!(
3268 messages(&conversation, cx),
3269 vec![
3270 (message_1.id, Role::User, 0..1),
3271 (message_2.id, Role::Assistant, 1..1)
3272 ]
3273 );
3274
3275 buffer.update(cx, |buffer, cx| {
3276 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3277 });
3278 assert_eq!(
3279 messages(&conversation, cx),
3280 vec![
3281 (message_1.id, Role::User, 0..2),
3282 (message_2.id, Role::Assistant, 2..3)
3283 ]
3284 );
3285
3286 let message_3 = conversation.update(cx, |conversation, cx| {
3287 conversation
3288 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3289 .unwrap()
3290 });
3291 assert_eq!(
3292 messages(&conversation, cx),
3293 vec![
3294 (message_1.id, Role::User, 0..2),
3295 (message_2.id, Role::Assistant, 2..4),
3296 (message_3.id, Role::User, 4..4)
3297 ]
3298 );
3299
3300 let message_4 = conversation.update(cx, |conversation, cx| {
3301 conversation
3302 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3303 .unwrap()
3304 });
3305 assert_eq!(
3306 messages(&conversation, cx),
3307 vec![
3308 (message_1.id, Role::User, 0..2),
3309 (message_2.id, Role::Assistant, 2..4),
3310 (message_4.id, Role::User, 4..5),
3311 (message_3.id, Role::User, 5..5),
3312 ]
3313 );
3314
3315 buffer.update(cx, |buffer, cx| {
3316 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3317 });
3318 assert_eq!(
3319 messages(&conversation, cx),
3320 vec![
3321 (message_1.id, Role::User, 0..2),
3322 (message_2.id, Role::Assistant, 2..4),
3323 (message_4.id, Role::User, 4..6),
3324 (message_3.id, Role::User, 6..7),
3325 ]
3326 );
3327
3328 // Deleting across message boundaries merges the messages.
3329 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3330 assert_eq!(
3331 messages(&conversation, cx),
3332 vec![
3333 (message_1.id, Role::User, 0..3),
3334 (message_3.id, Role::User, 3..4),
3335 ]
3336 );
3337
3338 // Undoing the deletion should also undo the merge.
3339 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3340 assert_eq!(
3341 messages(&conversation, cx),
3342 vec![
3343 (message_1.id, Role::User, 0..2),
3344 (message_2.id, Role::Assistant, 2..4),
3345 (message_4.id, Role::User, 4..6),
3346 (message_3.id, Role::User, 6..7),
3347 ]
3348 );
3349
3350 // Redoing the deletion should also redo the merge.
3351 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3352 assert_eq!(
3353 messages(&conversation, cx),
3354 vec![
3355 (message_1.id, Role::User, 0..3),
3356 (message_3.id, Role::User, 3..4),
3357 ]
3358 );
3359
3360 // Ensure we can still insert after a merged message.
3361 let message_5 = conversation.update(cx, |conversation, cx| {
3362 conversation
3363 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3364 .unwrap()
3365 });
3366 assert_eq!(
3367 messages(&conversation, cx),
3368 vec![
3369 (message_1.id, Role::User, 0..3),
3370 (message_5.id, Role::System, 3..4),
3371 (message_3.id, Role::User, 4..5)
3372 ]
3373 );
3374 }
3375
3376 #[gpui::test]
3377 fn test_message_splitting(cx: &mut AppContext) {
3378 cx.set_global(SettingsStore::test(cx));
3379 init(cx);
3380 let registry = Arc::new(LanguageRegistry::test());
3381 let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
3382 let buffer = conversation.read(cx).buffer.clone();
3383
3384 let message_1 = conversation.read(cx).message_anchors[0].clone();
3385 assert_eq!(
3386 messages(&conversation, cx),
3387 vec![(message_1.id, Role::User, 0..0)]
3388 );
3389
3390 buffer.update(cx, |buffer, cx| {
3391 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3392 });
3393
3394 let (_, message_2) =
3395 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3396 let message_2 = message_2.unwrap();
3397
3398 // We recycle newlines in the middle of a split message
3399 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3400 assert_eq!(
3401 messages(&conversation, cx),
3402 vec![
3403 (message_1.id, Role::User, 0..4),
3404 (message_2.id, Role::User, 4..16),
3405 ]
3406 );
3407
3408 let (_, message_3) =
3409 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3410 let message_3 = message_3.unwrap();
3411
3412 // We don't recycle newlines at the end of a split message
3413 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3414 assert_eq!(
3415 messages(&conversation, cx),
3416 vec![
3417 (message_1.id, Role::User, 0..4),
3418 (message_3.id, Role::User, 4..5),
3419 (message_2.id, Role::User, 5..17),
3420 ]
3421 );
3422
3423 let (_, message_4) =
3424 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3425 let message_4 = message_4.unwrap();
3426 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3427 assert_eq!(
3428 messages(&conversation, cx),
3429 vec![
3430 (message_1.id, Role::User, 0..4),
3431 (message_3.id, Role::User, 4..5),
3432 (message_2.id, Role::User, 5..9),
3433 (message_4.id, Role::User, 9..17),
3434 ]
3435 );
3436
3437 let (_, message_5) =
3438 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3439 let message_5 = message_5.unwrap();
3440 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3441 assert_eq!(
3442 messages(&conversation, cx),
3443 vec![
3444 (message_1.id, Role::User, 0..4),
3445 (message_3.id, Role::User, 4..5),
3446 (message_2.id, Role::User, 5..9),
3447 (message_4.id, Role::User, 9..10),
3448 (message_5.id, Role::User, 10..18),
3449 ]
3450 );
3451
3452 let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
3453 conversation.split_message(14..16, cx)
3454 });
3455 let message_6 = message_6.unwrap();
3456 let message_7 = message_7.unwrap();
3457 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3458 assert_eq!(
3459 messages(&conversation, cx),
3460 vec![
3461 (message_1.id, Role::User, 0..4),
3462 (message_3.id, Role::User, 4..5),
3463 (message_2.id, Role::User, 5..9),
3464 (message_4.id, Role::User, 9..10),
3465 (message_5.id, Role::User, 10..14),
3466 (message_6.id, Role::User, 14..17),
3467 (message_7.id, Role::User, 17..19),
3468 ]
3469 );
3470 }
3471
3472 #[gpui::test]
3473 fn test_messages_for_offsets(cx: &mut AppContext) {
3474 cx.set_global(SettingsStore::test(cx));
3475 init(cx);
3476 let registry = Arc::new(LanguageRegistry::test());
3477 let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx));
3478 let buffer = conversation.read(cx).buffer.clone();
3479
3480 let message_1 = conversation.read(cx).message_anchors[0].clone();
3481 assert_eq!(
3482 messages(&conversation, cx),
3483 vec![(message_1.id, Role::User, 0..0)]
3484 );
3485
3486 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3487 let message_2 = conversation
3488 .update(cx, |conversation, cx| {
3489 conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3490 })
3491 .unwrap();
3492 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3493
3494 let message_3 = conversation
3495 .update(cx, |conversation, cx| {
3496 conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3497 })
3498 .unwrap();
3499 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3500
3501 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3502 assert_eq!(
3503 messages(&conversation, cx),
3504 vec![
3505 (message_1.id, Role::User, 0..4),
3506 (message_2.id, Role::User, 4..8),
3507 (message_3.id, Role::User, 8..11)
3508 ]
3509 );
3510
3511 assert_eq!(
3512 message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
3513 [message_1.id, message_2.id, message_3.id]
3514 );
3515 assert_eq!(
3516 message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
3517 [message_1.id, message_3.id]
3518 );
3519
3520 let message_4 = conversation
3521 .update(cx, |conversation, cx| {
3522 conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3523 })
3524 .unwrap();
3525 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3526 assert_eq!(
3527 messages(&conversation, cx),
3528 vec![
3529 (message_1.id, Role::User, 0..4),
3530 (message_2.id, Role::User, 4..8),
3531 (message_3.id, Role::User, 8..12),
3532 (message_4.id, Role::User, 12..12)
3533 ]
3534 );
3535 assert_eq!(
3536 message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
3537 [message_1.id, message_2.id, message_3.id, message_4.id]
3538 );
3539
3540 fn message_ids_for_offsets(
3541 conversation: &ModelHandle<Conversation>,
3542 offsets: &[usize],
3543 cx: &AppContext,
3544 ) -> Vec<MessageId> {
3545 conversation
3546 .read(cx)
3547 .messages_for_offsets(offsets.iter().copied(), cx)
3548 .into_iter()
3549 .map(|message| message.id)
3550 .collect()
3551 }
3552 }
3553
3554 #[gpui::test]
3555 fn test_serialization(cx: &mut AppContext) {
3556 cx.set_global(SettingsStore::test(cx));
3557 init(cx);
3558 let registry = Arc::new(LanguageRegistry::test());
3559 let conversation =
3560 cx.add_model(|cx| Conversation::new(Default::default(), registry.clone(), cx));
3561 let buffer = conversation.read(cx).buffer.clone();
3562 let message_0 = conversation.read(cx).message_anchors[0].id;
3563 let message_1 = conversation.update(cx, |conversation, cx| {
3564 conversation
3565 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3566 .unwrap()
3567 });
3568 let message_2 = conversation.update(cx, |conversation, cx| {
3569 conversation
3570 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3571 .unwrap()
3572 });
3573 buffer.update(cx, |buffer, cx| {
3574 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3575 buffer.finalize_last_transaction();
3576 });
3577 let _message_3 = conversation.update(cx, |conversation, cx| {
3578 conversation
3579 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3580 .unwrap()
3581 });
3582 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3583 assert_eq!(buffer.read(cx).text(), "a\nb\nc\n");
3584 assert_eq!(
3585 messages(&conversation, cx),
3586 [
3587 (message_0, Role::User, 0..2),
3588 (message_1.id, Role::Assistant, 2..6),
3589 (message_2.id, Role::System, 6..6),
3590 ]
3591 );
3592
3593 let deserialized_conversation = cx.add_model(|cx| {
3594 Conversation::deserialize(
3595 conversation.read(cx).serialize(cx),
3596 Default::default(),
3597 Default::default(),
3598 registry.clone(),
3599 cx,
3600 )
3601 });
3602 let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone();
3603 assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n");
3604 assert_eq!(
3605 messages(&deserialized_conversation, cx),
3606 [
3607 (message_0, Role::User, 0..2),
3608 (message_1.id, Role::Assistant, 2..6),
3609 (message_2.id, Role::System, 6..6),
3610 ]
3611 );
3612 }
3613
3614 #[gpui::test]
3615 async fn test_strip_markdown_codeblock() {
3616 assert_eq!(
3617 strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2))
3618 .map(|chunk| chunk.unwrap())
3619 .collect::<String>()
3620 .await,
3621 "Lorem ipsum dolor"
3622 );
3623 assert_eq!(
3624 strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2))
3625 .map(|chunk| chunk.unwrap())
3626 .collect::<String>()
3627 .await,
3628 "Lorem ipsum dolor"
3629 );
3630 assert_eq!(
3631 strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
3632 .map(|chunk| chunk.unwrap())
3633 .collect::<String>()
3634 .await,
3635 "Lorem ipsum dolor"
3636 );
3637 assert_eq!(
3638 strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
3639 .map(|chunk| chunk.unwrap())
3640 .collect::<String>()
3641 .await,
3642 "Lorem ipsum dolor"
3643 );
3644 assert_eq!(
3645 strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
3646 .map(|chunk| chunk.unwrap())
3647 .collect::<String>()
3648 .await,
3649 "```js\nLorem ipsum dolor\n```"
3650 );
3651 assert_eq!(
3652 strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
3653 .map(|chunk| chunk.unwrap())
3654 .collect::<String>()
3655 .await,
3656 "``\nLorem ipsum dolor\n```"
3657 );
3658
3659 fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
3660 stream::iter(
3661 text.chars()
3662 .collect::<Vec<_>>()
3663 .chunks(size)
3664 .map(|chunk| Ok(chunk.iter().collect::<String>()))
3665 .collect::<Vec<_>>(),
3666 )
3667 }
3668 }
3669
3670 fn messages(
3671 conversation: &ModelHandle<Conversation>,
3672 cx: &AppContext,
3673 ) -> Vec<(MessageId, Role, Range<usize>)> {
3674 conversation
3675 .read(cx)
3676 .messages(cx)
3677 .map(|message| (message.id, message.role, message.offset_range))
3678 .collect()
3679 }
3680}