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