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