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