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 count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1226 let request = self.to_completion_request(cx);
1227 self.pending_token_count = cx.spawn(|this, mut cx| {
1228 async move {
1229 cx.background_executor()
1230 .timer(Duration::from_millis(200))
1231 .await;
1232
1233 let token_count = cx
1234 .update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
1235 .await?;
1236
1237 this.update(&mut cx, |this, cx| {
1238 this.token_count = Some(token_count);
1239 cx.notify()
1240 })?;
1241 anyhow::Ok(())
1242 }
1243 .log_err()
1244 });
1245 }
1246
1247 fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
1248 let buffer = self.buffer.read(cx);
1249 let mut row_ranges = self
1250 .edits_since_last_slash_command_parse
1251 .consume()
1252 .into_iter()
1253 .map(|edit| {
1254 let start_row = buffer.offset_to_point(edit.new.start).row;
1255 let end_row = buffer.offset_to_point(edit.new.end).row + 1;
1256 start_row..end_row
1257 })
1258 .peekable();
1259
1260 let mut removed = Vec::new();
1261 let mut updated = Vec::new();
1262 while let Some(mut row_range) = row_ranges.next() {
1263 while let Some(next_row_range) = row_ranges.peek() {
1264 if row_range.end >= next_row_range.start {
1265 row_range.end = next_row_range.end;
1266 row_ranges.next();
1267 } else {
1268 break;
1269 }
1270 }
1271
1272 let start = buffer.anchor_before(Point::new(row_range.start, 0));
1273 let end = buffer.anchor_after(Point::new(
1274 row_range.end - 1,
1275 buffer.line_len(row_range.end - 1),
1276 ));
1277
1278 let old_range = self.pending_command_indices_for_range(start..end, cx);
1279
1280 let mut new_commands = Vec::new();
1281 let mut lines = buffer.text_for_range(start..end).lines();
1282 let mut offset = lines.offset();
1283 while let Some(line) = lines.next() {
1284 if let Some(command_line) = SlashCommandLine::parse(line) {
1285 let name = &line[command_line.name.clone()];
1286 let argument = command_line.argument.as_ref().and_then(|argument| {
1287 (!argument.is_empty()).then_some(&line[argument.clone()])
1288 });
1289 if let Some(command) = self.slash_command_registry.command(name) {
1290 if !command.requires_argument() || argument.is_some() {
1291 let start_ix = offset + command_line.name.start - 1;
1292 let end_ix = offset
1293 + command_line
1294 .argument
1295 .map_or(command_line.name.end, |argument| argument.end);
1296 let source_range =
1297 buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
1298 let pending_command = PendingSlashCommand {
1299 name: name.to_string(),
1300 argument: argument.map(ToString::to_string),
1301 source_range,
1302 status: PendingSlashCommandStatus::Idle,
1303 };
1304 updated.push(pending_command.clone());
1305 new_commands.push(pending_command);
1306 }
1307 }
1308 }
1309
1310 offset = lines.offset();
1311 }
1312
1313 let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
1314 removed.extend(removed_commands.map(|command| command.source_range));
1315 }
1316
1317 if !updated.is_empty() || !removed.is_empty() {
1318 cx.emit(ContextEvent::PendingSlashCommandsUpdated { removed, updated });
1319 }
1320 }
1321
1322 fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
1323 self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
1324 cx.background_executor()
1325 .timer(Duration::from_millis(200))
1326 .await;
1327
1328 this.update(&mut cx, |this, cx| {
1329 this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx);
1330 })
1331 .ok();
1332 }));
1333 }
1334
1335 fn reparse_edit_suggestions_in_range(
1336 &mut self,
1337 range: Range<usize>,
1338 cx: &mut ModelContext<Self>,
1339 ) {
1340 self.buffer.update(cx, |buffer, _| {
1341 let range_start = buffer.anchor_before(range.start);
1342 let range_end = buffer.anchor_after(range.end);
1343 let start_ix = self
1344 .edit_suggestions
1345 .binary_search_by(|probe| {
1346 probe
1347 .source_range
1348 .end
1349 .cmp(&range_start, buffer)
1350 .then(Ordering::Greater)
1351 })
1352 .unwrap_err();
1353 let end_ix = self
1354 .edit_suggestions
1355 .binary_search_by(|probe| {
1356 probe
1357 .source_range
1358 .start
1359 .cmp(&range_end, buffer)
1360 .then(Ordering::Less)
1361 })
1362 .unwrap_err();
1363
1364 let mut new_edit_suggestions = Vec::new();
1365 let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
1366 while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) {
1367 let start_anchor = buffer.anchor_after(suggestion.outer_range.start);
1368 let end_anchor = buffer.anchor_before(suggestion.outer_range.end);
1369 new_edit_suggestions.push(EditSuggestion {
1370 source_range: start_anchor..end_anchor,
1371 full_path: suggestion.path,
1372 });
1373 }
1374 self.edit_suggestions
1375 .splice(start_ix..end_ix, new_edit_suggestions);
1376 });
1377 cx.emit(ContextEvent::EditSuggestionsChanged);
1378 cx.notify();
1379 }
1380
1381 fn pending_command_for_position(
1382 &mut self,
1383 position: language::Anchor,
1384 cx: &mut ModelContext<Self>,
1385 ) -> Option<&mut PendingSlashCommand> {
1386 let buffer = self.buffer.read(cx);
1387 match self
1388 .pending_slash_commands
1389 .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
1390 {
1391 Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
1392 Err(ix) => {
1393 let cmd = self.pending_slash_commands.get_mut(ix)?;
1394 if position.cmp(&cmd.source_range.start, buffer).is_ge()
1395 && position.cmp(&cmd.source_range.end, buffer).is_le()
1396 {
1397 Some(cmd)
1398 } else {
1399 None
1400 }
1401 }
1402 }
1403 }
1404
1405 fn pending_commands_for_range(
1406 &self,
1407 range: Range<language::Anchor>,
1408 cx: &AppContext,
1409 ) -> &[PendingSlashCommand] {
1410 let range = self.pending_command_indices_for_range(range, cx);
1411 &self.pending_slash_commands[range]
1412 }
1413
1414 fn pending_command_indices_for_range(
1415 &self,
1416 range: Range<language::Anchor>,
1417 cx: &AppContext,
1418 ) -> Range<usize> {
1419 let buffer = self.buffer.read(cx);
1420 let start_ix = match self
1421 .pending_slash_commands
1422 .binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
1423 {
1424 Ok(ix) | Err(ix) => ix,
1425 };
1426 let end_ix = match self
1427 .pending_slash_commands
1428 .binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
1429 {
1430 Ok(ix) => ix + 1,
1431 Err(ix) => ix,
1432 };
1433 start_ix..end_ix
1434 }
1435
1436 fn insert_command_output(
1437 &mut self,
1438 command_range: Range<language::Anchor>,
1439 output: Task<Result<SlashCommandOutput>>,
1440 insert_trailing_newline: bool,
1441 cx: &mut ModelContext<Self>,
1442 ) {
1443 self.reparse_slash_commands(cx);
1444
1445 let insert_output_task = cx.spawn(|this, mut cx| {
1446 let command_range = command_range.clone();
1447 async move {
1448 let output = output.await;
1449 this.update(&mut cx, |this, cx| match output {
1450 Ok(mut output) => {
1451 if insert_trailing_newline {
1452 output.text.push('\n');
1453 }
1454
1455 let event = this.buffer.update(cx, |buffer, cx| {
1456 let start = command_range.start.to_offset(buffer);
1457 let old_end = command_range.end.to_offset(buffer);
1458 let new_end = start + output.text.len();
1459 buffer.edit([(start..old_end, output.text)], None, cx);
1460
1461 let mut sections = output
1462 .sections
1463 .into_iter()
1464 .map(|section| SlashCommandOutputSection {
1465 range: buffer.anchor_after(start + section.range.start)
1466 ..buffer.anchor_before(start + section.range.end),
1467 render_placeholder: section.render_placeholder,
1468 })
1469 .collect::<Vec<_>>();
1470 sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
1471 ContextEvent::SlashCommandFinished {
1472 output_range: buffer.anchor_after(start)
1473 ..buffer.anchor_before(new_end),
1474 sections,
1475 run_commands_in_output: output.run_commands_in_text,
1476 }
1477 });
1478 cx.emit(event);
1479 }
1480 Err(error) => {
1481 if let Some(pending_command) =
1482 this.pending_command_for_position(command_range.start, cx)
1483 {
1484 pending_command.status =
1485 PendingSlashCommandStatus::Error(error.to_string());
1486 cx.emit(ContextEvent::PendingSlashCommandsUpdated {
1487 removed: vec![pending_command.source_range.clone()],
1488 updated: vec![pending_command.clone()],
1489 });
1490 }
1491 }
1492 })
1493 .ok();
1494 }
1495 });
1496
1497 if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) {
1498 pending_command.status = PendingSlashCommandStatus::Running {
1499 _task: insert_output_task.shared(),
1500 };
1501 cx.emit(ContextEvent::PendingSlashCommandsUpdated {
1502 removed: vec![pending_command.source_range.clone()],
1503 updated: vec![pending_command.clone()],
1504 });
1505 }
1506 }
1507
1508 fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
1509 let model = CompletionProvider::global(cx).model();
1510 Some(model.max_token_count() as isize - self.token_count? as isize)
1511 }
1512
1513 fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
1514 self.count_remaining_tokens(cx);
1515 }
1516
1517 fn assist(
1518 &mut self,
1519 selected_messages: HashSet<MessageId>,
1520 cx: &mut ModelContext<Self>,
1521 ) -> Vec<MessageAnchor> {
1522 let mut user_messages = Vec::new();
1523
1524 let last_message_id = if let Some(last_message_id) =
1525 self.message_anchors.iter().rev().find_map(|message| {
1526 message
1527 .start
1528 .is_valid(self.buffer.read(cx))
1529 .then_some(message.id)
1530 }) {
1531 last_message_id
1532 } else {
1533 return Default::default();
1534 };
1535
1536 let mut should_assist = false;
1537 for selected_message_id in selected_messages {
1538 let selected_message_role =
1539 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1540 metadata.role
1541 } else {
1542 continue;
1543 };
1544
1545 if selected_message_role == Role::Assistant {
1546 if let Some(user_message) = self.insert_message_after(
1547 selected_message_id,
1548 Role::User,
1549 MessageStatus::Done,
1550 cx,
1551 ) {
1552 user_messages.push(user_message);
1553 }
1554 } else {
1555 should_assist = true;
1556 }
1557 }
1558
1559 if should_assist {
1560 if !CompletionProvider::global(cx).is_authenticated() {
1561 log::info!("completion provider has no credentials");
1562 return Default::default();
1563 }
1564
1565 let request = self.to_completion_request(cx);
1566 let stream = CompletionProvider::global(cx).complete(request);
1567 let assistant_message = self
1568 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1569 .unwrap();
1570
1571 // Queue up the user's next reply.
1572 let user_message = self
1573 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1574 .unwrap();
1575 user_messages.push(user_message);
1576
1577 let task = cx.spawn({
1578 |this, mut cx| async move {
1579 let assistant_message_id = assistant_message.id;
1580 let mut response_latency = None;
1581 let stream_completion = async {
1582 let request_start = Instant::now();
1583 let mut messages = stream.await?;
1584
1585 while let Some(message) = messages.next().await {
1586 if response_latency.is_none() {
1587 response_latency = Some(request_start.elapsed());
1588 }
1589 let text = message?;
1590
1591 this.update(&mut cx, |this, cx| {
1592 let message_ix = this
1593 .message_anchors
1594 .iter()
1595 .position(|message| message.id == assistant_message_id)?;
1596 let message_range = this.buffer.update(cx, |buffer, cx| {
1597 let message_start_offset =
1598 this.message_anchors[message_ix].start.to_offset(buffer);
1599 let message_old_end_offset = this.message_anchors
1600 [message_ix + 1..]
1601 .iter()
1602 .find(|message| message.start.is_valid(buffer))
1603 .map_or(buffer.len(), |message| {
1604 message.start.to_offset(buffer).saturating_sub(1)
1605 });
1606 let message_new_end_offset =
1607 message_old_end_offset + text.len();
1608 buffer.edit(
1609 [(message_old_end_offset..message_old_end_offset, text)],
1610 None,
1611 cx,
1612 );
1613 message_start_offset..message_new_end_offset
1614 });
1615 this.reparse_edit_suggestions_in_range(message_range, cx);
1616 cx.emit(ContextEvent::StreamedCompletion);
1617
1618 Some(())
1619 })?;
1620 smol::future::yield_now().await;
1621 }
1622
1623 this.update(&mut cx, |this, cx| {
1624 this.pending_completions
1625 .retain(|completion| completion.id != this.completion_count);
1626 this.summarize(cx);
1627 })?;
1628
1629 anyhow::Ok(())
1630 };
1631
1632 let result = stream_completion.await;
1633
1634 this.update(&mut cx, |this, cx| {
1635 if let Some(metadata) =
1636 this.messages_metadata.get_mut(&assistant_message.id)
1637 {
1638 let error_message = result
1639 .err()
1640 .map(|error| error.to_string().trim().to_string());
1641 if let Some(error_message) = error_message.as_ref() {
1642 metadata.status =
1643 MessageStatus::Error(SharedString::from(error_message.clone()));
1644 } else {
1645 metadata.status = MessageStatus::Done;
1646 }
1647
1648 if let Some(telemetry) = this.telemetry.as_ref() {
1649 let model = CompletionProvider::global(cx).model();
1650 telemetry.report_assistant_event(
1651 this.id.clone(),
1652 AssistantKind::Panel,
1653 model.telemetry_id(),
1654 response_latency,
1655 error_message,
1656 );
1657 }
1658
1659 cx.emit(ContextEvent::MessagesEdited);
1660 }
1661 })
1662 .ok();
1663 }
1664 });
1665
1666 self.pending_completions.push(PendingCompletion {
1667 id: post_inc(&mut self.completion_count),
1668 _task: task,
1669 });
1670 }
1671
1672 user_messages
1673 }
1674
1675 pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
1676 let messages = self
1677 .messages(cx)
1678 .filter(|message| matches!(message.status, MessageStatus::Done))
1679 .map(|message| message.to_request_message(self.buffer.read(cx)));
1680
1681 LanguageModelRequest {
1682 model: CompletionProvider::global(cx).model(),
1683 messages: messages.collect(),
1684 stop: vec![],
1685 temperature: 1.0,
1686 }
1687 }
1688
1689 fn cancel_last_assist(&mut self) -> bool {
1690 self.pending_completions.pop().is_some()
1691 }
1692
1693 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1694 for id in ids {
1695 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1696 metadata.role.cycle();
1697 cx.emit(ContextEvent::MessagesEdited);
1698 cx.notify();
1699 }
1700 }
1701 }
1702
1703 fn insert_message_after(
1704 &mut self,
1705 message_id: MessageId,
1706 role: Role,
1707 status: MessageStatus,
1708 cx: &mut ModelContext<Self>,
1709 ) -> Option<MessageAnchor> {
1710 if let Some(prev_message_ix) = self
1711 .message_anchors
1712 .iter()
1713 .position(|message| message.id == message_id)
1714 {
1715 // Find the next valid message after the one we were given.
1716 let mut next_message_ix = prev_message_ix + 1;
1717 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1718 if next_message.start.is_valid(self.buffer.read(cx)) {
1719 break;
1720 }
1721 next_message_ix += 1;
1722 }
1723
1724 let start = self.buffer.update(cx, |buffer, cx| {
1725 let offset = self
1726 .message_anchors
1727 .get(next_message_ix)
1728 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1729 buffer.edit([(offset..offset, "\n")], None, cx);
1730 buffer.anchor_before(offset + 1)
1731 });
1732 let message = MessageAnchor {
1733 id: MessageId(post_inc(&mut self.next_message_id.0)),
1734 start,
1735 };
1736 self.message_anchors
1737 .insert(next_message_ix, message.clone());
1738 self.messages_metadata
1739 .insert(message.id, MessageMetadata { role, status });
1740 cx.emit(ContextEvent::MessagesEdited);
1741 Some(message)
1742 } else {
1743 None
1744 }
1745 }
1746
1747 fn split_message(
1748 &mut self,
1749 range: Range<usize>,
1750 cx: &mut ModelContext<Self>,
1751 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1752 let start_message = self.message_for_offset(range.start, cx);
1753 let end_message = self.message_for_offset(range.end, cx);
1754 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1755 // Prevent splitting when range spans multiple messages.
1756 if start_message.id != end_message.id {
1757 return (None, None);
1758 }
1759
1760 let message = start_message;
1761 let role = message.role;
1762 let mut edited_buffer = false;
1763
1764 let mut suffix_start = None;
1765 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1766 {
1767 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1768 suffix_start = Some(range.end + 1);
1769 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1770 suffix_start = Some(range.end);
1771 }
1772 }
1773
1774 let suffix = if let Some(suffix_start) = suffix_start {
1775 MessageAnchor {
1776 id: MessageId(post_inc(&mut self.next_message_id.0)),
1777 start: self.buffer.read(cx).anchor_before(suffix_start),
1778 }
1779 } else {
1780 self.buffer.update(cx, |buffer, cx| {
1781 buffer.edit([(range.end..range.end, "\n")], None, cx);
1782 });
1783 edited_buffer = true;
1784 MessageAnchor {
1785 id: MessageId(post_inc(&mut self.next_message_id.0)),
1786 start: self.buffer.read(cx).anchor_before(range.end + 1),
1787 }
1788 };
1789
1790 self.message_anchors
1791 .insert(message.index_range.end + 1, suffix.clone());
1792 self.messages_metadata.insert(
1793 suffix.id,
1794 MessageMetadata {
1795 role,
1796 status: MessageStatus::Done,
1797 },
1798 );
1799
1800 let new_messages =
1801 if range.start == range.end || range.start == message.offset_range.start {
1802 (None, Some(suffix))
1803 } else {
1804 let mut prefix_end = None;
1805 if range.start > message.offset_range.start
1806 && range.end < message.offset_range.end - 1
1807 {
1808 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1809 prefix_end = Some(range.start + 1);
1810 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1811 == Some('\n')
1812 {
1813 prefix_end = Some(range.start);
1814 }
1815 }
1816
1817 let selection = if let Some(prefix_end) = prefix_end {
1818 cx.emit(ContextEvent::MessagesEdited);
1819 MessageAnchor {
1820 id: MessageId(post_inc(&mut self.next_message_id.0)),
1821 start: self.buffer.read(cx).anchor_before(prefix_end),
1822 }
1823 } else {
1824 self.buffer.update(cx, |buffer, cx| {
1825 buffer.edit([(range.start..range.start, "\n")], None, cx)
1826 });
1827 edited_buffer = true;
1828 MessageAnchor {
1829 id: MessageId(post_inc(&mut self.next_message_id.0)),
1830 start: self.buffer.read(cx).anchor_before(range.end + 1),
1831 }
1832 };
1833
1834 self.message_anchors
1835 .insert(message.index_range.end + 1, selection.clone());
1836 self.messages_metadata.insert(
1837 selection.id,
1838 MessageMetadata {
1839 role,
1840 status: MessageStatus::Done,
1841 },
1842 );
1843 (Some(selection), Some(suffix))
1844 };
1845
1846 if !edited_buffer {
1847 cx.emit(ContextEvent::MessagesEdited);
1848 }
1849 new_messages
1850 } else {
1851 (None, None)
1852 }
1853 }
1854
1855 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1856 if self.message_anchors.len() >= 2 && self.summary.is_none() {
1857 if !CompletionProvider::global(cx).is_authenticated() {
1858 return;
1859 }
1860
1861 let messages = self
1862 .messages(cx)
1863 .map(|message| message.to_request_message(self.buffer.read(cx)))
1864 .chain(Some(LanguageModelRequestMessage {
1865 role: Role::User,
1866 content: "Summarize the context into a short title without punctuation.".into(),
1867 }));
1868 let request = LanguageModelRequest {
1869 model: CompletionProvider::global(cx).model(),
1870 messages: messages.collect(),
1871 stop: vec![],
1872 temperature: 1.0,
1873 };
1874
1875 let stream = CompletionProvider::global(cx).complete(request);
1876 self.pending_summary = cx.spawn(|this, mut cx| {
1877 async move {
1878 let mut messages = stream.await?;
1879
1880 while let Some(message) = messages.next().await {
1881 let text = message?;
1882 let mut lines = text.lines();
1883 this.update(&mut cx, |this, cx| {
1884 let summary = this.summary.get_or_insert(Default::default());
1885 summary.text.extend(lines.next());
1886 cx.emit(ContextEvent::SummaryChanged);
1887 })?;
1888
1889 // Stop if the LLM generated multiple lines.
1890 if lines.next().is_some() {
1891 break;
1892 }
1893 }
1894
1895 this.update(&mut cx, |this, cx| {
1896 if let Some(summary) = this.summary.as_mut() {
1897 summary.done = true;
1898 cx.emit(ContextEvent::SummaryChanged);
1899 }
1900 })?;
1901
1902 anyhow::Ok(())
1903 }
1904 .log_err()
1905 });
1906 }
1907 }
1908
1909 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
1910 self.messages_for_offsets([offset], cx).pop()
1911 }
1912
1913 fn messages_for_offsets(
1914 &self,
1915 offsets: impl IntoIterator<Item = usize>,
1916 cx: &AppContext,
1917 ) -> Vec<Message> {
1918 let mut result = Vec::new();
1919
1920 let mut messages = self.messages(cx).peekable();
1921 let mut offsets = offsets.into_iter().peekable();
1922 let mut current_message = messages.next();
1923 while let Some(offset) = offsets.next() {
1924 // Locate the message that contains the offset.
1925 while current_message.as_ref().map_or(false, |message| {
1926 !message.offset_range.contains(&offset) && messages.peek().is_some()
1927 }) {
1928 current_message = messages.next();
1929 }
1930 let Some(message) = current_message.as_ref() else {
1931 break;
1932 };
1933
1934 // Skip offsets that are in the same message.
1935 while offsets.peek().map_or(false, |offset| {
1936 message.offset_range.contains(offset) || messages.peek().is_none()
1937 }) {
1938 offsets.next();
1939 }
1940
1941 result.push(message.clone());
1942 }
1943 result
1944 }
1945
1946 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1947 let buffer = self.buffer.read(cx);
1948 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1949 iter::from_fn(move || {
1950 if let Some((start_ix, message_anchor)) = message_anchors.next() {
1951 let metadata = self.messages_metadata.get(&message_anchor.id)?;
1952 let message_start = message_anchor.start.to_offset(buffer);
1953 let mut message_end = None;
1954 let mut end_ix = start_ix;
1955 while let Some((_, next_message)) = message_anchors.peek() {
1956 if next_message.start.is_valid(buffer) {
1957 message_end = Some(next_message.start);
1958 break;
1959 } else {
1960 end_ix += 1;
1961 message_anchors.next();
1962 }
1963 }
1964 let message_end = message_end
1965 .unwrap_or(language::Anchor::MAX)
1966 .to_offset(buffer);
1967
1968 return Some(Message {
1969 index_range: start_ix..end_ix,
1970 offset_range: message_start..message_end,
1971 id: message_anchor.id,
1972 anchor: message_anchor.start,
1973 role: metadata.role,
1974 status: metadata.status.clone(),
1975 });
1976 }
1977 None
1978 })
1979 }
1980
1981 fn save(
1982 &mut self,
1983 debounce: Option<Duration>,
1984 fs: Arc<dyn Fs>,
1985 cx: &mut ModelContext<Context>,
1986 ) {
1987 self.pending_save = cx.spawn(|this, mut cx| async move {
1988 if let Some(debounce) = debounce {
1989 cx.background_executor().timer(debounce).await;
1990 }
1991
1992 let (old_path, summary) = this.read_with(&cx, |this, _| {
1993 let path = this.path.clone();
1994 let summary = if let Some(summary) = this.summary.as_ref() {
1995 if summary.done {
1996 Some(summary.text.clone())
1997 } else {
1998 None
1999 }
2000 } else {
2001 None
2002 };
2003 (path, summary)
2004 })?;
2005
2006 if let Some(summary) = summary {
2007 let context = this.read_with(&cx, |this, cx| this.serialize(cx))?;
2008 let path = if let Some(old_path) = old_path {
2009 old_path
2010 } else {
2011 let mut discriminant = 1;
2012 let mut new_path;
2013 loop {
2014 new_path = CONTEXTS_DIR.join(&format!(
2015 "{} - {}.zed.json",
2016 summary.trim(),
2017 discriminant
2018 ));
2019 if fs.is_file(&new_path).await {
2020 discriminant += 1;
2021 } else {
2022 break;
2023 }
2024 }
2025 new_path
2026 };
2027
2028 fs.create_dir(CONTEXTS_DIR.as_ref()).await?;
2029 fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap())
2030 .await?;
2031 this.update(&mut cx, |this, _| this.path = Some(path))?;
2032 }
2033
2034 Ok(())
2035 });
2036 }
2037}
2038
2039#[derive(Debug)]
2040enum EditParsingState {
2041 None,
2042 InOldText {
2043 path: PathBuf,
2044 start_offset: usize,
2045 old_text_start_offset: usize,
2046 },
2047 InNewText {
2048 path: PathBuf,
2049 start_offset: usize,
2050 old_text_range: Range<usize>,
2051 new_text_start_offset: usize,
2052 },
2053}
2054
2055#[derive(Clone, Debug, PartialEq)]
2056struct EditSuggestion {
2057 source_range: Range<language::Anchor>,
2058 full_path: PathBuf,
2059}
2060
2061struct ParsedEditSuggestion {
2062 path: PathBuf,
2063 outer_range: Range<usize>,
2064 old_text_range: Range<usize>,
2065 new_text_range: Range<usize>,
2066}
2067
2068fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSuggestion> {
2069 let mut state = EditParsingState::None;
2070 loop {
2071 let offset = lines.offset();
2072 let message_line = lines.next()?;
2073 match state {
2074 EditParsingState::None => {
2075 if let Some(rest) = message_line.strip_prefix("```edit ") {
2076 let path = rest.trim();
2077 if !path.is_empty() {
2078 state = EditParsingState::InOldText {
2079 path: PathBuf::from(path),
2080 start_offset: offset,
2081 old_text_start_offset: lines.offset(),
2082 };
2083 }
2084 }
2085 }
2086 EditParsingState::InOldText {
2087 path,
2088 start_offset,
2089 old_text_start_offset,
2090 } => {
2091 if message_line == "---" {
2092 state = EditParsingState::InNewText {
2093 path,
2094 start_offset,
2095 old_text_range: old_text_start_offset..offset,
2096 new_text_start_offset: lines.offset(),
2097 };
2098 } else {
2099 state = EditParsingState::InOldText {
2100 path,
2101 start_offset,
2102 old_text_start_offset,
2103 };
2104 }
2105 }
2106 EditParsingState::InNewText {
2107 path,
2108 start_offset,
2109 old_text_range,
2110 new_text_start_offset,
2111 } => {
2112 if message_line == "```" {
2113 return Some(ParsedEditSuggestion {
2114 path,
2115 outer_range: start_offset..offset + "```".len(),
2116 old_text_range,
2117 new_text_range: new_text_start_offset..offset,
2118 });
2119 } else {
2120 state = EditParsingState::InNewText {
2121 path,
2122 start_offset,
2123 old_text_range,
2124 new_text_start_offset,
2125 };
2126 }
2127 }
2128 }
2129 }
2130}
2131
2132#[derive(Clone)]
2133struct PendingSlashCommand {
2134 name: String,
2135 argument: Option<String>,
2136 status: PendingSlashCommandStatus,
2137 source_range: Range<language::Anchor>,
2138}
2139
2140#[derive(Clone)]
2141enum PendingSlashCommandStatus {
2142 Idle,
2143 Running { _task: Shared<Task<()>> },
2144 Error(String),
2145}
2146
2147struct PendingCompletion {
2148 id: usize,
2149 _task: Task<()>,
2150}
2151
2152enum ContextEditorEvent {
2153 TabContentChanged,
2154}
2155
2156#[derive(Copy, Clone, Debug, PartialEq)]
2157struct ScrollPosition {
2158 offset_before_cursor: gpui::Point<f32>,
2159 cursor: Anchor,
2160}
2161
2162pub struct ContextEditor {
2163 context: Model<Context>,
2164 fs: Arc<dyn Fs>,
2165 workspace: WeakView<Workspace>,
2166 slash_command_registry: Arc<SlashCommandRegistry>,
2167 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2168 editor: View<Editor>,
2169 blocks: HashSet<BlockId>,
2170 scroll_position: Option<ScrollPosition>,
2171 pending_slash_command_flaps: HashMap<Range<language::Anchor>, FlapId>,
2172 _subscriptions: Vec<Subscription>,
2173}
2174
2175impl ContextEditor {
2176 fn new(
2177 language_registry: Arc<LanguageRegistry>,
2178 slash_command_registry: Arc<SlashCommandRegistry>,
2179 fs: Arc<dyn Fs>,
2180 workspace: View<Workspace>,
2181 cx: &mut ViewContext<Self>,
2182 ) -> Self {
2183 let telemetry = workspace.read(cx).client().telemetry().clone();
2184 let project = workspace.read(cx).project().clone();
2185 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
2186
2187 let context = cx.new_model(|cx| {
2188 Context::new(
2189 language_registry,
2190 slash_command_registry,
2191 Some(telemetry),
2192 cx,
2193 )
2194 });
2195
2196 let mut this = Self::for_context(context, fs, workspace, lsp_adapter_delegate, cx);
2197 this.insert_default_prompt(cx);
2198 this
2199 }
2200
2201 fn for_context(
2202 context: Model<Context>,
2203 fs: Arc<dyn Fs>,
2204 workspace: View<Workspace>,
2205 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2206 cx: &mut ViewContext<Self>,
2207 ) -> Self {
2208 let slash_command_registry = context.read(cx).slash_command_registry.clone();
2209
2210 let completion_provider = SlashCommandCompletionProvider::new(
2211 slash_command_registry.clone(),
2212 Some(cx.view().downgrade()),
2213 Some(workspace.downgrade()),
2214 );
2215
2216 let editor = cx.new_view(|cx| {
2217 let mut editor = Editor::for_buffer(context.read(cx).buffer.clone(), None, cx);
2218 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2219 editor.set_show_line_numbers(false, cx);
2220 editor.set_show_git_diff_gutter(false, cx);
2221 editor.set_show_code_actions(false, cx);
2222 editor.set_show_wrap_guides(false, cx);
2223 editor.set_show_indent_guides(false, cx);
2224 editor.set_completion_provider(Box::new(completion_provider));
2225 editor
2226 });
2227
2228 let _subscriptions = vec![
2229 cx.observe(&context, |_, _, cx| cx.notify()),
2230 cx.subscribe(&context, Self::handle_context_event),
2231 cx.subscribe(&editor, Self::handle_editor_event),
2232 ];
2233
2234 let mut this = Self {
2235 context,
2236 editor,
2237 slash_command_registry,
2238 lsp_adapter_delegate,
2239 blocks: Default::default(),
2240 scroll_position: None,
2241 fs,
2242 workspace: workspace.downgrade(),
2243 pending_slash_command_flaps: HashMap::default(),
2244 _subscriptions,
2245 };
2246 this.update_message_headers(cx);
2247 this
2248 }
2249
2250 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
2251 let command_name = DefaultSlashCommand.name();
2252 self.editor.update(cx, |editor, cx| {
2253 editor.insert(&format!("/{command_name}"), cx)
2254 });
2255 self.split(&Split, cx);
2256 let command = self.context.update(cx, |context, cx| {
2257 context
2258 .messages_metadata
2259 .get_mut(&MessageId::default())
2260 .unwrap()
2261 .role = Role::System;
2262 context.reparse_slash_commands(cx);
2263 context.pending_slash_commands[0].clone()
2264 });
2265
2266 self.run_command(
2267 command.source_range,
2268 &command.name,
2269 command.argument.as_deref(),
2270 false,
2271 self.workspace.clone(),
2272 cx,
2273 );
2274 }
2275
2276 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2277 let cursors = self.cursors(cx);
2278
2279 let user_messages = self.context.update(cx, |context, cx| {
2280 let selected_messages = context
2281 .messages_for_offsets(cursors, cx)
2282 .into_iter()
2283 .map(|message| message.id)
2284 .collect();
2285 context.assist(selected_messages, cx)
2286 });
2287 let new_selections = user_messages
2288 .iter()
2289 .map(|message| {
2290 let cursor = message
2291 .start
2292 .to_offset(self.context.read(cx).buffer.read(cx));
2293 cursor..cursor
2294 })
2295 .collect::<Vec<_>>();
2296 if !new_selections.is_empty() {
2297 self.editor.update(cx, |editor, cx| {
2298 editor.change_selections(
2299 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2300 cx,
2301 |selections| selections.select_ranges(new_selections),
2302 );
2303 });
2304 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2305 cx.defer(|this, _| this.scroll_position = None);
2306 }
2307 }
2308
2309 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2310 if !self
2311 .context
2312 .update(cx, |context, _| context.cancel_last_assist())
2313 {
2314 cx.propagate();
2315 }
2316 }
2317
2318 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2319 let cursors = self.cursors(cx);
2320 self.context.update(cx, |context, cx| {
2321 let messages = context
2322 .messages_for_offsets(cursors, cx)
2323 .into_iter()
2324 .map(|message| message.id)
2325 .collect();
2326 context.cycle_message_roles(messages, cx)
2327 });
2328 }
2329
2330 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2331 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2332 selections
2333 .into_iter()
2334 .map(|selection| selection.head())
2335 .collect()
2336 }
2337
2338 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2339 if let Some(command) = self.slash_command_registry.command(name) {
2340 self.editor.update(cx, |editor, cx| {
2341 editor.transact(cx, |editor, cx| {
2342 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2343 let snapshot = editor.buffer().read(cx).snapshot(cx);
2344 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2345 if newest_cursor.column > 0
2346 || snapshot
2347 .chars_at(newest_cursor)
2348 .next()
2349 .map_or(false, |ch| ch != '\n')
2350 {
2351 editor.move_to_end_of_line(
2352 &MoveToEndOfLine {
2353 stop_at_soft_wraps: false,
2354 },
2355 cx,
2356 );
2357 editor.newline(&Newline, cx);
2358 }
2359
2360 editor.insert(&format!("/{name}"), cx);
2361 if command.requires_argument() {
2362 editor.insert(" ", cx);
2363 editor.show_completions(&ShowCompletions, cx);
2364 }
2365 });
2366 });
2367 if !command.requires_argument() {
2368 self.confirm_command(&ConfirmCommand, cx);
2369 }
2370 }
2371 }
2372
2373 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2374 let selections = self.editor.read(cx).selections.disjoint_anchors();
2375 let mut commands_by_range = HashMap::default();
2376 let workspace = self.workspace.clone();
2377 self.context.update(cx, |context, cx| {
2378 context.reparse_slash_commands(cx);
2379 for selection in selections.iter() {
2380 if let Some(command) =
2381 context.pending_command_for_position(selection.head().text_anchor, cx)
2382 {
2383 commands_by_range
2384 .entry(command.source_range.clone())
2385 .or_insert_with(|| command.clone());
2386 }
2387 }
2388 });
2389
2390 if commands_by_range.is_empty() {
2391 cx.propagate();
2392 } else {
2393 for command in commands_by_range.into_values() {
2394 self.run_command(
2395 command.source_range,
2396 &command.name,
2397 command.argument.as_deref(),
2398 true,
2399 workspace.clone(),
2400 cx,
2401 );
2402 }
2403 cx.stop_propagation();
2404 }
2405 }
2406
2407 pub fn run_command(
2408 &mut self,
2409 command_range: Range<language::Anchor>,
2410 name: &str,
2411 argument: Option<&str>,
2412 insert_trailing_newline: bool,
2413 workspace: WeakView<Workspace>,
2414 cx: &mut ViewContext<Self>,
2415 ) {
2416 if let Some(command) = self.slash_command_registry.command(name) {
2417 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
2418 let argument = argument.map(ToString::to_string);
2419 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
2420 self.context.update(cx, |context, cx| {
2421 context.insert_command_output(
2422 command_range,
2423 output,
2424 insert_trailing_newline,
2425 cx,
2426 )
2427 });
2428 }
2429 }
2430 }
2431
2432 fn handle_context_event(
2433 &mut self,
2434 _: Model<Context>,
2435 event: &ContextEvent,
2436 cx: &mut ViewContext<Self>,
2437 ) {
2438 let context_editor = cx.view().downgrade();
2439
2440 match event {
2441 ContextEvent::MessagesEdited => {
2442 self.update_message_headers(cx);
2443 self.context.update(cx, |context, cx| {
2444 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2445 });
2446 }
2447 ContextEvent::EditSuggestionsChanged => {
2448 self.editor.update(cx, |editor, cx| {
2449 let buffer = editor.buffer().read(cx).snapshot(cx);
2450 let excerpt_id = *buffer.as_singleton().unwrap().0;
2451 let context = self.context.read(cx);
2452 let highlighted_rows = context
2453 .edit_suggestions
2454 .iter()
2455 .map(|suggestion| {
2456 let start = buffer
2457 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2458 .unwrap();
2459 let end = buffer
2460 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2461 .unwrap();
2462 start..=end
2463 })
2464 .collect::<Vec<_>>();
2465
2466 editor.clear_row_highlights::<EditSuggestion>();
2467 for range in highlighted_rows {
2468 editor.highlight_rows::<EditSuggestion>(
2469 range,
2470 Some(
2471 cx.theme()
2472 .colors()
2473 .editor_document_highlight_read_background,
2474 ),
2475 false,
2476 cx,
2477 );
2478 }
2479 });
2480 }
2481 ContextEvent::SummaryChanged => {
2482 cx.emit(ContextEditorEvent::TabContentChanged);
2483 self.context.update(cx, |context, cx| {
2484 context.save(None, self.fs.clone(), cx);
2485 });
2486 }
2487 ContextEvent::StreamedCompletion => {
2488 self.editor.update(cx, |editor, cx| {
2489 if let Some(scroll_position) = self.scroll_position {
2490 let snapshot = editor.snapshot(cx);
2491 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2492 let scroll_top =
2493 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2494 editor.set_scroll_position(
2495 point(scroll_position.offset_before_cursor.x, scroll_top),
2496 cx,
2497 );
2498 }
2499 });
2500 }
2501 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2502 self.editor.update(cx, |editor, cx| {
2503 let buffer = editor.buffer().read(cx).snapshot(cx);
2504 let excerpt_id = *buffer.as_singleton().unwrap().0;
2505
2506 editor.remove_flaps(
2507 removed
2508 .iter()
2509 .filter_map(|range| self.pending_slash_command_flaps.remove(range)),
2510 cx,
2511 );
2512
2513 let flap_ids = editor.insert_flaps(
2514 updated.iter().map(|command| {
2515 let workspace = self.workspace.clone();
2516 let confirm_command = Arc::new({
2517 let context_editor = context_editor.clone();
2518 let command = command.clone();
2519 move |cx: &mut WindowContext| {
2520 context_editor
2521 .update(cx, |context_editor, cx| {
2522 context_editor.run_command(
2523 command.source_range.clone(),
2524 &command.name,
2525 command.argument.as_deref(),
2526 false,
2527 workspace.clone(),
2528 cx,
2529 );
2530 })
2531 .ok();
2532 }
2533 });
2534 let placeholder = FoldPlaceholder {
2535 render: Arc::new(move |_, _, _| Empty.into_any()),
2536 constrain_width: false,
2537 merge_adjacent: false,
2538 };
2539 let render_toggle = {
2540 let confirm_command = confirm_command.clone();
2541 let command = command.clone();
2542 move |row, _, _, _cx: &mut WindowContext| {
2543 render_pending_slash_command_gutter_decoration(
2544 row,
2545 command.status.clone(),
2546 confirm_command.clone(),
2547 )
2548 }
2549 };
2550 let render_trailer =
2551 |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2552
2553 let start = buffer
2554 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2555 .unwrap();
2556 let end = buffer
2557 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2558 .unwrap();
2559 Flap::new(start..end, placeholder, render_toggle, render_trailer)
2560 }),
2561 cx,
2562 );
2563
2564 self.pending_slash_command_flaps.extend(
2565 updated
2566 .iter()
2567 .map(|command| command.source_range.clone())
2568 .zip(flap_ids),
2569 );
2570 })
2571 }
2572 ContextEvent::SlashCommandFinished {
2573 output_range,
2574 sections,
2575 run_commands_in_output,
2576 } => {
2577 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2578
2579 if *run_commands_in_output {
2580 let commands = self.context.update(cx, |context, cx| {
2581 context.reparse_slash_commands(cx);
2582 context
2583 .pending_commands_for_range(output_range.clone(), cx)
2584 .to_vec()
2585 });
2586
2587 for command in commands {
2588 self.run_command(
2589 command.source_range,
2590 &command.name,
2591 command.argument.as_deref(),
2592 false,
2593 self.workspace.clone(),
2594 cx,
2595 );
2596 }
2597 }
2598 }
2599 }
2600 }
2601
2602 fn insert_slash_command_output_sections(
2603 &mut self,
2604 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2605 cx: &mut ViewContext<Self>,
2606 ) {
2607 self.editor.update(cx, |editor, cx| {
2608 let buffer = editor.buffer().read(cx).snapshot(cx);
2609 let excerpt_id = *buffer.as_singleton().unwrap().0;
2610 let mut buffer_rows_to_fold = BTreeSet::new();
2611 let mut flaps = Vec::new();
2612 for section in sections {
2613 let start = buffer
2614 .anchor_in_excerpt(excerpt_id, section.range.start)
2615 .unwrap();
2616 let end = buffer
2617 .anchor_in_excerpt(excerpt_id, section.range.end)
2618 .unwrap();
2619 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2620 buffer_rows_to_fold.insert(buffer_row);
2621 flaps.push(Flap::new(
2622 start..end,
2623 FoldPlaceholder {
2624 render: Arc::new({
2625 let editor = cx.view().downgrade();
2626 let render_placeholder = section.render_placeholder.clone();
2627 move |fold_id, fold_range, cx| {
2628 let editor = editor.clone();
2629 let unfold = Arc::new(move |cx: &mut WindowContext| {
2630 editor
2631 .update(cx, |editor, cx| {
2632 let buffer_start = fold_range
2633 .start
2634 .to_point(&editor.buffer().read(cx).read(cx));
2635 let buffer_row = MultiBufferRow(buffer_start.row);
2636 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2637 })
2638 .ok();
2639 });
2640 render_placeholder(fold_id.into(), unfold, cx)
2641 }
2642 }),
2643 constrain_width: false,
2644 merge_adjacent: false,
2645 },
2646 render_slash_command_output_toggle,
2647 |_, _, _| Empty.into_any_element(),
2648 ));
2649 }
2650
2651 editor.insert_flaps(flaps, cx);
2652
2653 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2654 editor.fold_at(&FoldAt { buffer_row }, cx);
2655 }
2656 });
2657 }
2658
2659 fn handle_editor_event(
2660 &mut self,
2661 _: View<Editor>,
2662 event: &EditorEvent,
2663 cx: &mut ViewContext<Self>,
2664 ) {
2665 match event {
2666 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2667 let cursor_scroll_position = self.cursor_scroll_position(cx);
2668 if *autoscroll {
2669 self.scroll_position = cursor_scroll_position;
2670 } else if self.scroll_position != cursor_scroll_position {
2671 self.scroll_position = None;
2672 }
2673 }
2674 EditorEvent::SelectionsChanged { .. } => {
2675 self.scroll_position = self.cursor_scroll_position(cx);
2676 }
2677 _ => {}
2678 }
2679 }
2680
2681 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2682 self.editor.update(cx, |editor, cx| {
2683 let snapshot = editor.snapshot(cx);
2684 let cursor = editor.selections.newest_anchor().head();
2685 let cursor_row = cursor
2686 .to_display_point(&snapshot.display_snapshot)
2687 .row()
2688 .as_f32();
2689 let scroll_position = editor
2690 .scroll_manager
2691 .anchor()
2692 .scroll_position(&snapshot.display_snapshot);
2693
2694 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2695 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2696 Some(ScrollPosition {
2697 cursor,
2698 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2699 })
2700 } else {
2701 None
2702 }
2703 })
2704 }
2705
2706 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2707 self.editor.update(cx, |editor, cx| {
2708 let buffer = editor.buffer().read(cx).snapshot(cx);
2709 let excerpt_id = *buffer.as_singleton().unwrap().0;
2710 let old_blocks = std::mem::take(&mut self.blocks);
2711 let new_blocks = self
2712 .context
2713 .read(cx)
2714 .messages(cx)
2715 .map(|message| BlockProperties {
2716 position: buffer
2717 .anchor_in_excerpt(excerpt_id, message.anchor)
2718 .unwrap(),
2719 height: 2,
2720 style: BlockStyle::Sticky,
2721 render: Box::new({
2722 let context = self.context.clone();
2723 move |cx| {
2724 let message_id = message.id;
2725 let sender = ButtonLike::new("role")
2726 .style(ButtonStyle::Filled)
2727 .child(match message.role {
2728 Role::User => Label::new("You").color(Color::Default),
2729 Role::Assistant => Label::new("Assistant").color(Color::Info),
2730 Role::System => Label::new("System").color(Color::Warning),
2731 })
2732 .tooltip(|cx| {
2733 Tooltip::with_meta(
2734 "Toggle message role",
2735 None,
2736 "Available roles: You (User), Assistant, System",
2737 cx,
2738 )
2739 })
2740 .on_click({
2741 let context = context.clone();
2742 move |_, cx| {
2743 context.update(cx, |context, cx| {
2744 context.cycle_message_roles(
2745 HashSet::from_iter(Some(message_id)),
2746 cx,
2747 )
2748 })
2749 }
2750 });
2751
2752 h_flex()
2753 .id(("message_header", message_id.0))
2754 .pl(cx.gutter_dimensions.full_width())
2755 .h_11()
2756 .w_full()
2757 .relative()
2758 .gap_1()
2759 .child(sender)
2760 .children(
2761 if let MessageStatus::Error(error) = message.status.clone() {
2762 Some(
2763 div()
2764 .id("error")
2765 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2766 .child(Icon::new(IconName::XCircle)),
2767 )
2768 } else {
2769 None
2770 },
2771 )
2772 .into_any_element()
2773 }
2774 }),
2775 disposition: BlockDisposition::Above,
2776 })
2777 .collect::<Vec<_>>();
2778
2779 editor.remove_blocks(old_blocks, None, cx);
2780 let ids = editor.insert_blocks(new_blocks, None, cx);
2781 self.blocks = HashSet::from_iter(ids);
2782 });
2783 }
2784
2785 fn quote_selection(
2786 workspace: &mut Workspace,
2787 _: &QuoteSelection,
2788 cx: &mut ViewContext<Workspace>,
2789 ) {
2790 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2791 return;
2792 };
2793 let Some(editor) = workspace
2794 .active_item(cx)
2795 .and_then(|item| item.act_as::<Editor>(cx))
2796 else {
2797 return;
2798 };
2799
2800 let editor = editor.read(cx);
2801 let range = editor.selections.newest::<usize>(cx).range();
2802 let buffer = editor.buffer().read(cx).snapshot(cx);
2803 let start_language = buffer.language_at(range.start);
2804 let end_language = buffer.language_at(range.end);
2805 let language_name = if start_language == end_language {
2806 start_language.map(|language| language.code_fence_block_name())
2807 } else {
2808 None
2809 };
2810 let language_name = language_name.as_deref().unwrap_or("");
2811
2812 let selected_text = buffer.text_for_range(range).collect::<String>();
2813 let text = if selected_text.is_empty() {
2814 None
2815 } else {
2816 Some(if language_name == "markdown" {
2817 selected_text
2818 .lines()
2819 .map(|line| format!("> {}", line))
2820 .collect::<Vec<_>>()
2821 .join("\n")
2822 } else {
2823 format!("```{language_name}\n{selected_text}\n```")
2824 })
2825 };
2826
2827 // Activate the panel
2828 if !panel.focus_handle(cx).contains_focused(cx) {
2829 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2830 }
2831
2832 if let Some(text) = text {
2833 panel.update(cx, |_, cx| {
2834 // Wait to create a new context until the workspace is no longer
2835 // being updated.
2836 cx.defer(move |panel, cx| {
2837 if let Some(context) = panel
2838 .active_context_editor()
2839 .cloned()
2840 .or_else(|| panel.new_context(cx))
2841 {
2842 context.update(cx, |context, cx| {
2843 context
2844 .editor
2845 .update(cx, |editor, cx| editor.insert(&text, cx))
2846 });
2847 };
2848 });
2849 });
2850 }
2851 }
2852
2853 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2854 let editor = self.editor.read(cx);
2855 let context = self.context.read(cx);
2856 if editor.selections.count() == 1 {
2857 let selection = editor.selections.newest::<usize>(cx);
2858 let mut copied_text = String::new();
2859 let mut spanned_messages = 0;
2860 for message in context.messages(cx) {
2861 if message.offset_range.start >= selection.range().end {
2862 break;
2863 } else if message.offset_range.end >= selection.range().start {
2864 let range = cmp::max(message.offset_range.start, selection.range().start)
2865 ..cmp::min(message.offset_range.end, selection.range().end);
2866 if !range.is_empty() {
2867 spanned_messages += 1;
2868 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2869 for chunk in context.buffer.read(cx).text_for_range(range) {
2870 copied_text.push_str(chunk);
2871 }
2872 copied_text.push('\n');
2873 }
2874 }
2875 }
2876
2877 if spanned_messages > 1 {
2878 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2879 return;
2880 }
2881 }
2882
2883 cx.propagate();
2884 }
2885
2886 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2887 self.context.update(cx, |context, cx| {
2888 let selections = self.editor.read(cx).selections.disjoint_anchors();
2889 for selection in selections.as_ref() {
2890 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2891 let range = selection
2892 .map(|endpoint| endpoint.to_offset(&buffer))
2893 .range();
2894 context.split_message(range, cx);
2895 }
2896 });
2897 }
2898
2899 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
2900 let Some(workspace) = self.workspace.upgrade() else {
2901 return;
2902 };
2903 let project = workspace.read(cx).project().clone();
2904
2905 struct Edit {
2906 old_text: String,
2907 new_text: String,
2908 }
2909
2910 let context = self.context.read(cx);
2911 let context_buffer = context.buffer.read(cx);
2912 let context_buffer_snapshot = context_buffer.snapshot();
2913
2914 let selections = self.editor.read(cx).selections.disjoint_anchors();
2915 let mut selections = selections.iter().peekable();
2916 let selected_suggestions = context
2917 .edit_suggestions
2918 .iter()
2919 .filter(|suggestion| {
2920 while let Some(selection) = selections.peek() {
2921 if selection
2922 .end
2923 .text_anchor
2924 .cmp(&suggestion.source_range.start, context_buffer)
2925 .is_lt()
2926 {
2927 selections.next();
2928 continue;
2929 }
2930 if selection
2931 .start
2932 .text_anchor
2933 .cmp(&suggestion.source_range.end, context_buffer)
2934 .is_gt()
2935 {
2936 break;
2937 }
2938 return true;
2939 }
2940 false
2941 })
2942 .cloned()
2943 .collect::<Vec<_>>();
2944
2945 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
2946 project.update(cx, |project, cx| {
2947 for suggestion in &selected_suggestions {
2948 opened_buffers
2949 .entry(suggestion.full_path.clone())
2950 .or_insert_with(|| {
2951 project.open_buffer_for_full_path(&suggestion.full_path, cx)
2952 });
2953 }
2954 });
2955
2956 cx.spawn(|this, mut cx| async move {
2957 let mut buffers_by_full_path = HashMap::default();
2958 for (full_path, buffer) in opened_buffers {
2959 if let Some(buffer) = buffer.await.log_err() {
2960 buffers_by_full_path.insert(full_path, buffer);
2961 }
2962 }
2963
2964 let mut suggestions_by_buffer = HashMap::default();
2965 cx.update(|cx| {
2966 for suggestion in selected_suggestions {
2967 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
2968 let (_, edits) = suggestions_by_buffer
2969 .entry(buffer.clone())
2970 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
2971
2972 let mut lines = context_buffer_snapshot
2973 .as_rope()
2974 .chunks_in_range(
2975 suggestion.source_range.to_offset(&context_buffer_snapshot),
2976 )
2977 .lines();
2978 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
2979 let old_text = context_buffer_snapshot
2980 .text_for_range(suggestion.old_text_range)
2981 .collect();
2982 let new_text = context_buffer_snapshot
2983 .text_for_range(suggestion.new_text_range)
2984 .collect();
2985 edits.push(Edit { old_text, new_text });
2986 }
2987 }
2988 }
2989 })?;
2990
2991 let edits_by_buffer = cx
2992 .background_executor()
2993 .spawn(async move {
2994 let mut result = HashMap::default();
2995 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
2996 let edits =
2997 result
2998 .entry(buffer)
2999 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
3000 for suggestion in suggestions {
3001 if let Some(range) =
3002 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
3003 {
3004 let edit_start = snapshot.anchor_after(range.start);
3005 let edit_end = snapshot.anchor_before(range.end);
3006 if let Err(ix) = edits.binary_search_by(|(range, _)| {
3007 range.start.cmp(&edit_start, &snapshot)
3008 }) {
3009 edits.insert(
3010 ix,
3011 (edit_start..edit_end, suggestion.new_text.clone()),
3012 );
3013 }
3014 } else {
3015 log::info!(
3016 "assistant edit did not match any text in buffer {:?}",
3017 &suggestion.old_text
3018 );
3019 }
3020 }
3021 }
3022 result
3023 })
3024 .await;
3025
3026 let mut project_transaction = ProjectTransaction::default();
3027 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
3028 for (buffer_handle, edits) in edits_by_buffer {
3029 buffer_handle.update(cx, |buffer, cx| {
3030 buffer.start_transaction();
3031 buffer.edit(
3032 edits,
3033 Some(AutoindentMode::Block {
3034 original_indent_columns: Vec::new(),
3035 }),
3036 cx,
3037 );
3038 buffer.end_transaction(cx);
3039 if let Some(transaction) = buffer.finalize_last_transaction() {
3040 project_transaction
3041 .0
3042 .insert(buffer_handle.clone(), transaction.clone());
3043 }
3044 });
3045 }
3046
3047 (
3048 this.editor.downgrade(),
3049 this.workspace.clone(),
3050 this.title(cx),
3051 )
3052 })?;
3053
3054 Editor::open_project_transaction(
3055 &editor,
3056 workspace,
3057 project_transaction,
3058 format!("Edits from {}", title),
3059 cx,
3060 )
3061 .await
3062 })
3063 .detach_and_log_err(cx);
3064 }
3065
3066 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3067 self.context
3068 .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
3069 }
3070
3071 fn title(&self, cx: &AppContext) -> String {
3072 self.context
3073 .read(cx)
3074 .summary
3075 .as_ref()
3076 .map(|summary| summary.text.clone())
3077 .unwrap_or_else(|| "New Context".into())
3078 }
3079}
3080
3081impl EventEmitter<ContextEditorEvent> for ContextEditor {}
3082
3083impl Render for ContextEditor {
3084 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3085 div()
3086 .key_context("ContextEditor")
3087 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
3088 .capture_action(cx.listener(ContextEditor::save))
3089 .capture_action(cx.listener(ContextEditor::copy))
3090 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3091 .capture_action(cx.listener(ContextEditor::confirm_command))
3092 .on_action(cx.listener(ContextEditor::assist))
3093 .on_action(cx.listener(ContextEditor::split))
3094 .on_action(cx.listener(ContextEditor::apply_edit))
3095 .size_full()
3096 .v_flex()
3097 .child(
3098 div()
3099 .flex_grow()
3100 .bg(cx.theme().colors().editor_background)
3101 .child(self.editor.clone()),
3102 )
3103 }
3104}
3105
3106impl FocusableView for ContextEditor {
3107 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3108 self.editor.focus_handle(cx)
3109 }
3110}
3111
3112#[derive(Clone, Debug)]
3113struct MessageAnchor {
3114 id: MessageId,
3115 start: language::Anchor,
3116}
3117
3118#[derive(Clone, Debug)]
3119pub struct Message {
3120 offset_range: Range<usize>,
3121 index_range: Range<usize>,
3122 id: MessageId,
3123 anchor: language::Anchor,
3124 role: Role,
3125 status: MessageStatus,
3126}
3127
3128impl Message {
3129 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
3130 LanguageModelRequestMessage {
3131 role: self.role,
3132 content: buffer.text_for_range(self.offset_range.clone()).collect(),
3133 }
3134 }
3135}
3136
3137type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3138
3139fn render_slash_command_output_toggle(
3140 row: MultiBufferRow,
3141 is_folded: bool,
3142 fold: ToggleFold,
3143 _cx: &mut WindowContext,
3144) -> AnyElement {
3145 IconButton::new(
3146 ("slash-command-output-fold-indicator", row.0),
3147 ui::IconName::ChevronDown,
3148 )
3149 .on_click(move |_e, cx| fold(!is_folded, cx))
3150 .icon_color(ui::Color::Muted)
3151 .icon_size(ui::IconSize::Small)
3152 .selected(is_folded)
3153 .selected_icon(ui::IconName::ChevronRight)
3154 .size(ui::ButtonSize::None)
3155 .into_any_element()
3156}
3157
3158fn render_pending_slash_command_gutter_decoration(
3159 row: MultiBufferRow,
3160 status: PendingSlashCommandStatus,
3161 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3162) -> AnyElement {
3163 let mut icon = IconButton::new(
3164 ("slash-command-gutter-decoration", row.0),
3165 ui::IconName::TriangleRight,
3166 )
3167 .on_click(move |_e, cx| confirm_command(cx))
3168 .icon_size(ui::IconSize::Small)
3169 .size(ui::ButtonSize::None);
3170
3171 match status {
3172 PendingSlashCommandStatus::Idle => {
3173 icon = icon.icon_color(Color::Muted);
3174 }
3175 PendingSlashCommandStatus::Running { .. } => {
3176 icon = icon.selected(true);
3177 }
3178 PendingSlashCommandStatus::Error(error) => {
3179 icon = icon
3180 .icon_color(Color::Error)
3181 .tooltip(move |cx| Tooltip::text(format!("error: {error}"), cx));
3182 }
3183 }
3184
3185 icon.into_any_element()
3186}
3187
3188fn make_lsp_adapter_delegate(
3189 project: &Model<Project>,
3190 cx: &mut AppContext,
3191) -> Result<Arc<dyn LspAdapterDelegate>> {
3192 project.update(cx, |project, cx| {
3193 // TODO: Find the right worktree.
3194 let worktree = project
3195 .worktrees()
3196 .next()
3197 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3198 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3199 })
3200}
3201
3202#[cfg(test)]
3203mod tests {
3204 use super::*;
3205 use crate::{
3206 slash_command::{active_command, file_command},
3207 FakeCompletionProvider, MessageId,
3208 };
3209 use fs::FakeFs;
3210 use gpui::{AppContext, TestAppContext};
3211 use rope::Rope;
3212 use serde_json::json;
3213 use settings::SettingsStore;
3214 use std::{cell::RefCell, path::Path, rc::Rc};
3215 use unindent::Unindent;
3216 use util::test::marked_text_ranges;
3217
3218 #[gpui::test]
3219 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3220 let settings_store = SettingsStore::test(cx);
3221 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3222 cx.set_global(settings_store);
3223 init(cx);
3224 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3225
3226 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3227 let buffer = context.read(cx).buffer.clone();
3228
3229 let message_1 = context.read(cx).message_anchors[0].clone();
3230 assert_eq!(
3231 messages(&context, cx),
3232 vec![(message_1.id, Role::User, 0..0)]
3233 );
3234
3235 let message_2 = context.update(cx, |context, cx| {
3236 context
3237 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3238 .unwrap()
3239 });
3240 assert_eq!(
3241 messages(&context, cx),
3242 vec![
3243 (message_1.id, Role::User, 0..1),
3244 (message_2.id, Role::Assistant, 1..1)
3245 ]
3246 );
3247
3248 buffer.update(cx, |buffer, cx| {
3249 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3250 });
3251 assert_eq!(
3252 messages(&context, cx),
3253 vec![
3254 (message_1.id, Role::User, 0..2),
3255 (message_2.id, Role::Assistant, 2..3)
3256 ]
3257 );
3258
3259 let message_3 = context.update(cx, |context, cx| {
3260 context
3261 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3262 .unwrap()
3263 });
3264 assert_eq!(
3265 messages(&context, cx),
3266 vec![
3267 (message_1.id, Role::User, 0..2),
3268 (message_2.id, Role::Assistant, 2..4),
3269 (message_3.id, Role::User, 4..4)
3270 ]
3271 );
3272
3273 let message_4 = context.update(cx, |context, cx| {
3274 context
3275 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3276 .unwrap()
3277 });
3278 assert_eq!(
3279 messages(&context, cx),
3280 vec![
3281 (message_1.id, Role::User, 0..2),
3282 (message_2.id, Role::Assistant, 2..4),
3283 (message_4.id, Role::User, 4..5),
3284 (message_3.id, Role::User, 5..5),
3285 ]
3286 );
3287
3288 buffer.update(cx, |buffer, cx| {
3289 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3290 });
3291 assert_eq!(
3292 messages(&context, cx),
3293 vec![
3294 (message_1.id, Role::User, 0..2),
3295 (message_2.id, Role::Assistant, 2..4),
3296 (message_4.id, Role::User, 4..6),
3297 (message_3.id, Role::User, 6..7),
3298 ]
3299 );
3300
3301 // Deleting across message boundaries merges the messages.
3302 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3303 assert_eq!(
3304 messages(&context, cx),
3305 vec![
3306 (message_1.id, Role::User, 0..3),
3307 (message_3.id, Role::User, 3..4),
3308 ]
3309 );
3310
3311 // Undoing the deletion should also undo the merge.
3312 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3313 assert_eq!(
3314 messages(&context, cx),
3315 vec![
3316 (message_1.id, Role::User, 0..2),
3317 (message_2.id, Role::Assistant, 2..4),
3318 (message_4.id, Role::User, 4..6),
3319 (message_3.id, Role::User, 6..7),
3320 ]
3321 );
3322
3323 // Redoing the deletion should also redo the merge.
3324 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3325 assert_eq!(
3326 messages(&context, cx),
3327 vec![
3328 (message_1.id, Role::User, 0..3),
3329 (message_3.id, Role::User, 3..4),
3330 ]
3331 );
3332
3333 // Ensure we can still insert after a merged message.
3334 let message_5 = context.update(cx, |context, cx| {
3335 context
3336 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3337 .unwrap()
3338 });
3339 assert_eq!(
3340 messages(&context, cx),
3341 vec![
3342 (message_1.id, Role::User, 0..3),
3343 (message_5.id, Role::System, 3..4),
3344 (message_3.id, Role::User, 4..5)
3345 ]
3346 );
3347 }
3348
3349 #[gpui::test]
3350 fn test_message_splitting(cx: &mut AppContext) {
3351 let settings_store = SettingsStore::test(cx);
3352 cx.set_global(settings_store);
3353 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3354 init(cx);
3355 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3356
3357 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3358 let buffer = context.read(cx).buffer.clone();
3359
3360 let message_1 = context.read(cx).message_anchors[0].clone();
3361 assert_eq!(
3362 messages(&context, cx),
3363 vec![(message_1.id, Role::User, 0..0)]
3364 );
3365
3366 buffer.update(cx, |buffer, cx| {
3367 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3368 });
3369
3370 let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3371 let message_2 = message_2.unwrap();
3372
3373 // We recycle newlines in the middle of a split message
3374 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3375 assert_eq!(
3376 messages(&context, cx),
3377 vec![
3378 (message_1.id, Role::User, 0..4),
3379 (message_2.id, Role::User, 4..16),
3380 ]
3381 );
3382
3383 let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3384 let message_3 = message_3.unwrap();
3385
3386 // We don't recycle newlines at the end of a split message
3387 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3388 assert_eq!(
3389 messages(&context, cx),
3390 vec![
3391 (message_1.id, Role::User, 0..4),
3392 (message_3.id, Role::User, 4..5),
3393 (message_2.id, Role::User, 5..17),
3394 ]
3395 );
3396
3397 let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3398 let message_4 = message_4.unwrap();
3399 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3400 assert_eq!(
3401 messages(&context, cx),
3402 vec![
3403 (message_1.id, Role::User, 0..4),
3404 (message_3.id, Role::User, 4..5),
3405 (message_2.id, Role::User, 5..9),
3406 (message_4.id, Role::User, 9..17),
3407 ]
3408 );
3409
3410 let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3411 let message_5 = message_5.unwrap();
3412 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3413 assert_eq!(
3414 messages(&context, cx),
3415 vec![
3416 (message_1.id, Role::User, 0..4),
3417 (message_3.id, Role::User, 4..5),
3418 (message_2.id, Role::User, 5..9),
3419 (message_4.id, Role::User, 9..10),
3420 (message_5.id, Role::User, 10..18),
3421 ]
3422 );
3423
3424 let (message_6, message_7) =
3425 context.update(cx, |context, cx| context.split_message(14..16, cx));
3426 let message_6 = message_6.unwrap();
3427 let message_7 = message_7.unwrap();
3428 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3429 assert_eq!(
3430 messages(&context, cx),
3431 vec![
3432 (message_1.id, Role::User, 0..4),
3433 (message_3.id, Role::User, 4..5),
3434 (message_2.id, Role::User, 5..9),
3435 (message_4.id, Role::User, 9..10),
3436 (message_5.id, Role::User, 10..14),
3437 (message_6.id, Role::User, 14..17),
3438 (message_7.id, Role::User, 17..19),
3439 ]
3440 );
3441 }
3442
3443 #[gpui::test]
3444 fn test_messages_for_offsets(cx: &mut AppContext) {
3445 let settings_store = SettingsStore::test(cx);
3446 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3447 cx.set_global(settings_store);
3448 init(cx);
3449 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3450 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3451 let buffer = context.read(cx).buffer.clone();
3452
3453 let message_1 = context.read(cx).message_anchors[0].clone();
3454 assert_eq!(
3455 messages(&context, cx),
3456 vec![(message_1.id, Role::User, 0..0)]
3457 );
3458
3459 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3460 let message_2 = context
3461 .update(cx, |context, cx| {
3462 context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3463 })
3464 .unwrap();
3465 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3466
3467 let message_3 = context
3468 .update(cx, |context, cx| {
3469 context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3470 })
3471 .unwrap();
3472 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3473
3474 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3475 assert_eq!(
3476 messages(&context, cx),
3477 vec![
3478 (message_1.id, Role::User, 0..4),
3479 (message_2.id, Role::User, 4..8),
3480 (message_3.id, Role::User, 8..11)
3481 ]
3482 );
3483
3484 assert_eq!(
3485 message_ids_for_offsets(&context, &[0, 4, 9], cx),
3486 [message_1.id, message_2.id, message_3.id]
3487 );
3488 assert_eq!(
3489 message_ids_for_offsets(&context, &[0, 1, 11], cx),
3490 [message_1.id, message_3.id]
3491 );
3492
3493 let message_4 = context
3494 .update(cx, |context, cx| {
3495 context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3496 })
3497 .unwrap();
3498 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3499 assert_eq!(
3500 messages(&context, cx),
3501 vec![
3502 (message_1.id, Role::User, 0..4),
3503 (message_2.id, Role::User, 4..8),
3504 (message_3.id, Role::User, 8..12),
3505 (message_4.id, Role::User, 12..12)
3506 ]
3507 );
3508 assert_eq!(
3509 message_ids_for_offsets(&context, &[0, 4, 8, 12], cx),
3510 [message_1.id, message_2.id, message_3.id, message_4.id]
3511 );
3512
3513 fn message_ids_for_offsets(
3514 context: &Model<Context>,
3515 offsets: &[usize],
3516 cx: &AppContext,
3517 ) -> Vec<MessageId> {
3518 context
3519 .read(cx)
3520 .messages_for_offsets(offsets.iter().copied(), cx)
3521 .into_iter()
3522 .map(|message| message.id)
3523 .collect()
3524 }
3525 }
3526
3527 #[gpui::test]
3528 async fn test_slash_commands(cx: &mut TestAppContext) {
3529 let settings_store = cx.update(SettingsStore::test);
3530 cx.set_global(settings_store);
3531 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3532 cx.update(Project::init_settings);
3533 cx.update(init);
3534 let fs = FakeFs::new(cx.background_executor.clone());
3535
3536 fs.insert_tree(
3537 "/test",
3538 json!({
3539 "src": {
3540 "lib.rs": "fn one() -> usize { 1 }",
3541 "main.rs": "
3542 use crate::one;
3543 fn main() { one(); }
3544 ".unindent(),
3545 }
3546 }),
3547 )
3548 .await;
3549
3550 let slash_command_registry = SlashCommandRegistry::new();
3551 slash_command_registry.register_command(file_command::FileSlashCommand, false);
3552 slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
3553
3554 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3555 let context =
3556 cx.new_model(|cx| Context::new(registry.clone(), slash_command_registry, None, cx));
3557
3558 let output_ranges = Rc::new(RefCell::new(HashSet::default()));
3559 context.update(cx, |_, cx| {
3560 cx.subscribe(&context, {
3561 let ranges = output_ranges.clone();
3562 move |_, _, event, _| match event {
3563 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
3564 for range in removed {
3565 ranges.borrow_mut().remove(range);
3566 }
3567 for command in updated {
3568 ranges.borrow_mut().insert(command.source_range.clone());
3569 }
3570 }
3571 _ => {}
3572 }
3573 })
3574 .detach();
3575 });
3576
3577 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
3578
3579 // Insert a slash command
3580 buffer.update(cx, |buffer, cx| {
3581 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
3582 });
3583 assert_text_and_output_ranges(
3584 &buffer,
3585 &output_ranges.borrow(),
3586 "
3587 «/file src/lib.rs»
3588 "
3589 .unindent()
3590 .trim_end(),
3591 cx,
3592 );
3593
3594 // Edit the argument of the slash command.
3595 buffer.update(cx, |buffer, cx| {
3596 let edit_offset = buffer.text().find("lib.rs").unwrap();
3597 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
3598 });
3599 assert_text_and_output_ranges(
3600 &buffer,
3601 &output_ranges.borrow(),
3602 "
3603 «/file src/main.rs»
3604 "
3605 .unindent()
3606 .trim_end(),
3607 cx,
3608 );
3609
3610 // Edit the name of the slash command, using one that doesn't exist.
3611 buffer.update(cx, |buffer, cx| {
3612 let edit_offset = buffer.text().find("/file").unwrap();
3613 buffer.edit(
3614 [(edit_offset..edit_offset + "/file".len(), "/unknown")],
3615 None,
3616 cx,
3617 );
3618 });
3619 assert_text_and_output_ranges(
3620 &buffer,
3621 &output_ranges.borrow(),
3622 "
3623 /unknown src/main.rs
3624 "
3625 .unindent()
3626 .trim_end(),
3627 cx,
3628 );
3629
3630 #[track_caller]
3631 fn assert_text_and_output_ranges(
3632 buffer: &Model<Buffer>,
3633 ranges: &HashSet<Range<language::Anchor>>,
3634 expected_marked_text: &str,
3635 cx: &mut TestAppContext,
3636 ) {
3637 let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
3638 let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
3639 let mut ranges = ranges
3640 .iter()
3641 .map(|range| range.to_offset(buffer))
3642 .collect::<Vec<_>>();
3643 ranges.sort_by_key(|a| a.start);
3644 (buffer.text(), ranges)
3645 });
3646
3647 assert_eq!(actual_text, expected_text);
3648 assert_eq!(actual_ranges, expected_ranges);
3649 }
3650 }
3651
3652 #[test]
3653 fn test_parse_next_edit_suggestion() {
3654 let text = "
3655 some output:
3656
3657 ```edit src/foo.rs
3658 let a = 1;
3659 let b = 2;
3660 ---
3661 let w = 1;
3662 let x = 2;
3663 let y = 3;
3664 let z = 4;
3665 ```
3666
3667 some more output:
3668
3669 ```edit src/foo.rs
3670 let c = 1;
3671 ---
3672 ```
3673
3674 and the conclusion.
3675 "
3676 .unindent();
3677
3678 let rope = Rope::from(text.as_str());
3679 let mut lines = rope.chunks().lines();
3680 let mut suggestions = vec![];
3681 while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
3682 suggestions.push((
3683 suggestion.path.clone(),
3684 text[suggestion.old_text_range].to_string(),
3685 text[suggestion.new_text_range].to_string(),
3686 ));
3687 }
3688
3689 assert_eq!(
3690 suggestions,
3691 vec![
3692 (
3693 Path::new("src/foo.rs").into(),
3694 [
3695 " let a = 1;", //
3696 " let b = 2;",
3697 "",
3698 ]
3699 .join("\n"),
3700 [
3701 " let w = 1;",
3702 " let x = 2;",
3703 " let y = 3;",
3704 " let z = 4;",
3705 "",
3706 ]
3707 .join("\n"),
3708 ),
3709 (
3710 Path::new("src/foo.rs").into(),
3711 [
3712 " let c = 1;", //
3713 "",
3714 ]
3715 .join("\n"),
3716 String::new(),
3717 )
3718 ]
3719 );
3720 }
3721
3722 #[gpui::test]
3723 async fn test_serialization(cx: &mut TestAppContext) {
3724 let settings_store = cx.update(SettingsStore::test);
3725 cx.set_global(settings_store);
3726 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3727 cx.update(init);
3728 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3729 let context =
3730 cx.new_model(|cx| Context::new(registry.clone(), Default::default(), None, cx));
3731 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
3732 let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
3733 let message_1 = context.update(cx, |context, cx| {
3734 context
3735 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3736 .unwrap()
3737 });
3738 let message_2 = context.update(cx, |context, cx| {
3739 context
3740 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3741 .unwrap()
3742 });
3743 buffer.update(cx, |buffer, cx| {
3744 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3745 buffer.finalize_last_transaction();
3746 });
3747 let _message_3 = context.update(cx, |context, cx| {
3748 context
3749 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3750 .unwrap()
3751 });
3752 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3753 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
3754 assert_eq!(
3755 cx.read(|cx| messages(&context, cx)),
3756 [
3757 (message_0, Role::User, 0..2),
3758 (message_1.id, Role::Assistant, 2..6),
3759 (message_2.id, Role::System, 6..6),
3760 ]
3761 );
3762
3763 let deserialized_context = Context::deserialize(
3764 context.read_with(cx, |context, cx| context.serialize(cx)),
3765 Default::default(),
3766 registry.clone(),
3767 Default::default(),
3768 None,
3769 &mut cx.to_async(),
3770 )
3771 .await
3772 .unwrap();
3773 let deserialized_buffer =
3774 deserialized_context.read_with(cx, |context, _| context.buffer.clone());
3775 assert_eq!(
3776 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
3777 "a\nb\nc\n"
3778 );
3779 assert_eq!(
3780 cx.read(|cx| messages(&deserialized_context, cx)),
3781 [
3782 (message_0, Role::User, 0..2),
3783 (message_1.id, Role::Assistant, 2..6),
3784 (message_2.id, Role::System, 6..6),
3785 ]
3786 );
3787 }
3788
3789 fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
3790 context
3791 .read(cx)
3792 .messages(cx)
3793 .map(|message| (message.id, message.role, message.offset_range))
3794 .collect()
3795 }
3796}