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_runnables(false, cx);
2257 editor.set_show_wrap_guides(false, cx);
2258 editor.set_show_indent_guides(false, cx);
2259 editor.set_completion_provider(Box::new(completion_provider));
2260 editor
2261 });
2262
2263 let _subscriptions = vec![
2264 cx.observe(&context, |_, _, cx| cx.notify()),
2265 cx.subscribe(&context, Self::handle_context_event),
2266 cx.subscribe(&editor, Self::handle_editor_event),
2267 ];
2268
2269 let sections = context.read(cx).slash_command_output_sections.clone();
2270 let mut this = Self {
2271 context,
2272 editor,
2273 slash_command_registry,
2274 lsp_adapter_delegate,
2275 blocks: Default::default(),
2276 scroll_position: None,
2277 fs,
2278 workspace: workspace.downgrade(),
2279 pending_slash_command_creases: HashMap::default(),
2280 pending_slash_command_blocks: HashMap::default(),
2281 _subscriptions,
2282 };
2283 this.update_message_headers(cx);
2284 this.insert_slash_command_output_sections(sections, cx);
2285 this
2286 }
2287
2288 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
2289 let command_name = DefaultSlashCommand.name();
2290 self.editor.update(cx, |editor, cx| {
2291 editor.insert(&format!("/{command_name}"), cx)
2292 });
2293 self.split(&Split, cx);
2294 let command = self.context.update(cx, |context, cx| {
2295 context
2296 .messages_metadata
2297 .get_mut(&MessageId::default())
2298 .unwrap()
2299 .role = Role::System;
2300 context.reparse_slash_commands(cx);
2301 context.pending_slash_commands[0].clone()
2302 });
2303
2304 self.run_command(
2305 command.source_range,
2306 &command.name,
2307 command.argument.as_deref(),
2308 false,
2309 self.workspace.clone(),
2310 cx,
2311 );
2312 }
2313
2314 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2315 let cursors = self.cursors(cx);
2316
2317 let user_messages = self.context.update(cx, |context, cx| {
2318 let selected_messages = context
2319 .messages_for_offsets(cursors, cx)
2320 .into_iter()
2321 .map(|message| message.id)
2322 .collect();
2323 context.assist(selected_messages, cx)
2324 });
2325 let new_selections = user_messages
2326 .iter()
2327 .map(|message| {
2328 let cursor = message
2329 .start
2330 .to_offset(self.context.read(cx).buffer.read(cx));
2331 cursor..cursor
2332 })
2333 .collect::<Vec<_>>();
2334 if !new_selections.is_empty() {
2335 self.editor.update(cx, |editor, cx| {
2336 editor.change_selections(
2337 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2338 cx,
2339 |selections| selections.select_ranges(new_selections),
2340 );
2341 });
2342 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2343 cx.defer(|this, _| this.scroll_position = None);
2344 }
2345 }
2346
2347 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2348 if !self
2349 .context
2350 .update(cx, |context, _| context.cancel_last_assist())
2351 {
2352 cx.propagate();
2353 }
2354 }
2355
2356 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2357 let cursors = self.cursors(cx);
2358 self.context.update(cx, |context, cx| {
2359 let messages = context
2360 .messages_for_offsets(cursors, cx)
2361 .into_iter()
2362 .map(|message| message.id)
2363 .collect();
2364 context.cycle_message_roles(messages, cx)
2365 });
2366 }
2367
2368 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2369 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2370 selections
2371 .into_iter()
2372 .map(|selection| selection.head())
2373 .collect()
2374 }
2375
2376 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2377 if let Some(command) = self.slash_command_registry.command(name) {
2378 self.editor.update(cx, |editor, cx| {
2379 editor.transact(cx, |editor, cx| {
2380 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2381 let snapshot = editor.buffer().read(cx).snapshot(cx);
2382 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2383 if newest_cursor.column > 0
2384 || snapshot
2385 .chars_at(newest_cursor)
2386 .next()
2387 .map_or(false, |ch| ch != '\n')
2388 {
2389 editor.move_to_end_of_line(
2390 &MoveToEndOfLine {
2391 stop_at_soft_wraps: false,
2392 },
2393 cx,
2394 );
2395 editor.newline(&Newline, cx);
2396 }
2397
2398 editor.insert(&format!("/{name}"), cx);
2399 if command.requires_argument() {
2400 editor.insert(" ", cx);
2401 editor.show_completions(&ShowCompletions::default(), cx);
2402 }
2403 });
2404 });
2405 if !command.requires_argument() {
2406 self.confirm_command(&ConfirmCommand, cx);
2407 }
2408 }
2409 }
2410
2411 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2412 let selections = self.editor.read(cx).selections.disjoint_anchors();
2413 let mut commands_by_range = HashMap::default();
2414 let workspace = self.workspace.clone();
2415 self.context.update(cx, |context, cx| {
2416 context.reparse_slash_commands(cx);
2417 for selection in selections.iter() {
2418 if let Some(command) =
2419 context.pending_command_for_position(selection.head().text_anchor, cx)
2420 {
2421 commands_by_range
2422 .entry(command.source_range.clone())
2423 .or_insert_with(|| command.clone());
2424 }
2425 }
2426 });
2427
2428 if commands_by_range.is_empty() {
2429 cx.propagate();
2430 } else {
2431 for command in commands_by_range.into_values() {
2432 self.run_command(
2433 command.source_range,
2434 &command.name,
2435 command.argument.as_deref(),
2436 true,
2437 workspace.clone(),
2438 cx,
2439 );
2440 }
2441 cx.stop_propagation();
2442 }
2443 }
2444
2445 pub fn run_command(
2446 &mut self,
2447 command_range: Range<language::Anchor>,
2448 name: &str,
2449 argument: Option<&str>,
2450 insert_trailing_newline: bool,
2451 workspace: WeakView<Workspace>,
2452 cx: &mut ViewContext<Self>,
2453 ) {
2454 if let Some(command) = self.slash_command_registry.command(name) {
2455 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
2456 let argument = argument.map(ToString::to_string);
2457 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
2458 self.context.update(cx, |context, cx| {
2459 context.insert_command_output(
2460 command_range,
2461 output,
2462 insert_trailing_newline,
2463 cx,
2464 )
2465 });
2466 }
2467 }
2468 }
2469
2470 fn handle_context_event(
2471 &mut self,
2472 _: Model<Context>,
2473 event: &ContextEvent,
2474 cx: &mut ViewContext<Self>,
2475 ) {
2476 let context_editor = cx.view().downgrade();
2477
2478 match event {
2479 ContextEvent::MessagesEdited => {
2480 self.update_message_headers(cx);
2481 self.context.update(cx, |context, cx| {
2482 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2483 });
2484 }
2485 ContextEvent::EditSuggestionsChanged => {
2486 self.editor.update(cx, |editor, cx| {
2487 let buffer = editor.buffer().read(cx).snapshot(cx);
2488 let excerpt_id = *buffer.as_singleton().unwrap().0;
2489 let context = self.context.read(cx);
2490 let highlighted_rows = context
2491 .edit_suggestions
2492 .iter()
2493 .map(|suggestion| {
2494 let start = buffer
2495 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2496 .unwrap();
2497 let end = buffer
2498 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2499 .unwrap();
2500 start..=end
2501 })
2502 .collect::<Vec<_>>();
2503
2504 editor.clear_row_highlights::<EditSuggestion>();
2505 for range in highlighted_rows {
2506 editor.highlight_rows::<EditSuggestion>(
2507 range,
2508 Some(
2509 cx.theme()
2510 .colors()
2511 .editor_document_highlight_read_background,
2512 ),
2513 false,
2514 cx,
2515 );
2516 }
2517 });
2518 }
2519 ContextEvent::SummaryChanged => {
2520 cx.emit(ContextEditorEvent::TabContentChanged);
2521 self.context.update(cx, |context, cx| {
2522 context.save(None, self.fs.clone(), cx);
2523 });
2524 }
2525 ContextEvent::StreamedCompletion => {
2526 self.editor.update(cx, |editor, cx| {
2527 if let Some(scroll_position) = self.scroll_position {
2528 let snapshot = editor.snapshot(cx);
2529 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2530 let scroll_top =
2531 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2532 editor.set_scroll_position(
2533 point(scroll_position.offset_before_cursor.x, scroll_top),
2534 cx,
2535 );
2536 }
2537 });
2538 }
2539 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2540 self.editor.update(cx, |editor, cx| {
2541 let buffer = editor.buffer().read(cx).snapshot(cx);
2542 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2543 let excerpt_id = *excerpt_id;
2544
2545 editor.remove_creases(
2546 removed
2547 .iter()
2548 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2549 cx,
2550 );
2551
2552 editor.remove_blocks(
2553 HashSet::from_iter(
2554 removed.iter().filter_map(|range| {
2555 self.pending_slash_command_blocks.remove(range)
2556 }),
2557 ),
2558 None,
2559 cx,
2560 );
2561
2562 let crease_ids = editor.insert_creases(
2563 updated.iter().map(|command| {
2564 let workspace = self.workspace.clone();
2565 let confirm_command = Arc::new({
2566 let context_editor = context_editor.clone();
2567 let command = command.clone();
2568 move |cx: &mut WindowContext| {
2569 context_editor
2570 .update(cx, |context_editor, cx| {
2571 context_editor.run_command(
2572 command.source_range.clone(),
2573 &command.name,
2574 command.argument.as_deref(),
2575 false,
2576 workspace.clone(),
2577 cx,
2578 );
2579 })
2580 .ok();
2581 }
2582 });
2583 let placeholder = FoldPlaceholder {
2584 render: Arc::new(move |_, _, _| Empty.into_any()),
2585 constrain_width: false,
2586 merge_adjacent: false,
2587 };
2588 let render_toggle = {
2589 let confirm_command = confirm_command.clone();
2590 let command = command.clone();
2591 move |row, _, _, _cx: &mut WindowContext| {
2592 render_pending_slash_command_gutter_decoration(
2593 row,
2594 &command.status,
2595 confirm_command.clone(),
2596 )
2597 }
2598 };
2599 let render_trailer = {
2600 let command = command.clone();
2601 move |row, _unfold, cx: &mut WindowContext| {
2602 // TODO: In the future we should investigate how we can expose
2603 // this as a hook on the `SlashCommand` trait so that we don't
2604 // need to special-case it here.
2605 if command.name == "rustdoc" {
2606 return render_rustdoc_slash_command_trailer(
2607 row,
2608 command.clone(),
2609 cx,
2610 );
2611 }
2612
2613 Empty.into_any()
2614 }
2615 };
2616
2617 let start = buffer
2618 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2619 .unwrap();
2620 let end = buffer
2621 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2622 .unwrap();
2623 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2624 }),
2625 cx,
2626 );
2627
2628 let block_ids = editor.insert_blocks(
2629 updated
2630 .iter()
2631 .filter_map(|command| match &command.status {
2632 PendingSlashCommandStatus::Error(error) => {
2633 Some((command, error.clone()))
2634 }
2635 _ => None,
2636 })
2637 .map(|(command, error_message)| BlockProperties {
2638 style: BlockStyle::Fixed,
2639 position: Anchor {
2640 buffer_id: Some(buffer_id),
2641 excerpt_id,
2642 text_anchor: command.source_range.start,
2643 },
2644 height: 1,
2645 disposition: BlockDisposition::Below,
2646 render: slash_command_error_block_renderer(error_message),
2647 }),
2648 None,
2649 cx,
2650 );
2651
2652 self.pending_slash_command_creases.extend(
2653 updated
2654 .iter()
2655 .map(|command| command.source_range.clone())
2656 .zip(crease_ids),
2657 );
2658
2659 self.pending_slash_command_blocks.extend(
2660 updated
2661 .iter()
2662 .map(|command| command.source_range.clone())
2663 .zip(block_ids),
2664 );
2665 })
2666 }
2667 ContextEvent::SlashCommandFinished {
2668 output_range,
2669 sections,
2670 run_commands_in_output,
2671 } => {
2672 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2673
2674 if *run_commands_in_output {
2675 let commands = self.context.update(cx, |context, cx| {
2676 context.reparse_slash_commands(cx);
2677 context
2678 .pending_commands_for_range(output_range.clone(), cx)
2679 .to_vec()
2680 });
2681
2682 for command in commands {
2683 self.run_command(
2684 command.source_range,
2685 &command.name,
2686 command.argument.as_deref(),
2687 false,
2688 self.workspace.clone(),
2689 cx,
2690 );
2691 }
2692 }
2693 }
2694 }
2695 }
2696
2697 fn insert_slash_command_output_sections(
2698 &mut self,
2699 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2700 cx: &mut ViewContext<Self>,
2701 ) {
2702 self.editor.update(cx, |editor, cx| {
2703 let buffer = editor.buffer().read(cx).snapshot(cx);
2704 let excerpt_id = *buffer.as_singleton().unwrap().0;
2705 let mut buffer_rows_to_fold = BTreeSet::new();
2706 let mut creases = Vec::new();
2707 for section in sections {
2708 let start = buffer
2709 .anchor_in_excerpt(excerpt_id, section.range.start)
2710 .unwrap();
2711 let end = buffer
2712 .anchor_in_excerpt(excerpt_id, section.range.end)
2713 .unwrap();
2714 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2715 buffer_rows_to_fold.insert(buffer_row);
2716 creases.push(Crease::new(
2717 start..end,
2718 FoldPlaceholder {
2719 render: Arc::new({
2720 let editor = cx.view().downgrade();
2721 let icon = section.icon;
2722 let label = section.label.clone();
2723 move |fold_id, fold_range, _cx| {
2724 let editor = editor.clone();
2725 ButtonLike::new(fold_id)
2726 .style(ButtonStyle::Filled)
2727 .layer(ElevationIndex::ElevatedSurface)
2728 .child(Icon::new(icon))
2729 .child(Label::new(label.clone()).single_line())
2730 .on_click(move |_, cx| {
2731 editor
2732 .update(cx, |editor, cx| {
2733 let buffer_start = fold_range
2734 .start
2735 .to_point(&editor.buffer().read(cx).read(cx));
2736 let buffer_row = MultiBufferRow(buffer_start.row);
2737 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2738 })
2739 .ok();
2740 })
2741 .into_any_element()
2742 }
2743 }),
2744 constrain_width: false,
2745 merge_adjacent: false,
2746 },
2747 render_slash_command_output_toggle,
2748 |_, _, _| Empty.into_any_element(),
2749 ));
2750 }
2751
2752 editor.insert_creases(creases, cx);
2753
2754 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2755 editor.fold_at(&FoldAt { buffer_row }, cx);
2756 }
2757 });
2758 }
2759
2760 fn handle_editor_event(
2761 &mut self,
2762 _: View<Editor>,
2763 event: &EditorEvent,
2764 cx: &mut ViewContext<Self>,
2765 ) {
2766 match event {
2767 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2768 let cursor_scroll_position = self.cursor_scroll_position(cx);
2769 if *autoscroll {
2770 self.scroll_position = cursor_scroll_position;
2771 } else if self.scroll_position != cursor_scroll_position {
2772 self.scroll_position = None;
2773 }
2774 }
2775 EditorEvent::SelectionsChanged { .. } => {
2776 self.scroll_position = self.cursor_scroll_position(cx);
2777 }
2778 _ => {}
2779 }
2780 }
2781
2782 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2783 self.editor.update(cx, |editor, cx| {
2784 let snapshot = editor.snapshot(cx);
2785 let cursor = editor.selections.newest_anchor().head();
2786 let cursor_row = cursor
2787 .to_display_point(&snapshot.display_snapshot)
2788 .row()
2789 .as_f32();
2790 let scroll_position = editor
2791 .scroll_manager
2792 .anchor()
2793 .scroll_position(&snapshot.display_snapshot);
2794
2795 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2796 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2797 Some(ScrollPosition {
2798 cursor,
2799 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2800 })
2801 } else {
2802 None
2803 }
2804 })
2805 }
2806
2807 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2808 self.editor.update(cx, |editor, cx| {
2809 let buffer = editor.buffer().read(cx).snapshot(cx);
2810 let excerpt_id = *buffer.as_singleton().unwrap().0;
2811 let old_blocks = std::mem::take(&mut self.blocks);
2812 let new_blocks = self
2813 .context
2814 .read(cx)
2815 .messages(cx)
2816 .map(|message| BlockProperties {
2817 position: buffer
2818 .anchor_in_excerpt(excerpt_id, message.anchor)
2819 .unwrap(),
2820 height: 2,
2821 style: BlockStyle::Sticky,
2822 render: Box::new({
2823 let context = self.context.clone();
2824 move |cx| {
2825 let message_id = message.id;
2826 let sender = ButtonLike::new("role")
2827 .style(ButtonStyle::Filled)
2828 .child(match message.role {
2829 Role::User => Label::new("You").color(Color::Default),
2830 Role::Assistant => Label::new("Assistant").color(Color::Info),
2831 Role::System => Label::new("System").color(Color::Warning),
2832 })
2833 .tooltip(|cx| {
2834 Tooltip::with_meta(
2835 "Toggle message role",
2836 None,
2837 "Available roles: You (User), Assistant, System",
2838 cx,
2839 )
2840 })
2841 .on_click({
2842 let context = context.clone();
2843 move |_, cx| {
2844 context.update(cx, |context, cx| {
2845 context.cycle_message_roles(
2846 HashSet::from_iter(Some(message_id)),
2847 cx,
2848 )
2849 })
2850 }
2851 });
2852
2853 h_flex()
2854 .id(("message_header", message_id.0))
2855 .pl(cx.gutter_dimensions.full_width())
2856 .h_11()
2857 .w_full()
2858 .relative()
2859 .gap_1()
2860 .child(sender)
2861 .children(
2862 if let MessageStatus::Error(error) = message.status.clone() {
2863 Some(
2864 div()
2865 .id("error")
2866 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2867 .child(Icon::new(IconName::XCircle)),
2868 )
2869 } else {
2870 None
2871 },
2872 )
2873 .into_any_element()
2874 }
2875 }),
2876 disposition: BlockDisposition::Above,
2877 })
2878 .collect::<Vec<_>>();
2879
2880 editor.remove_blocks(old_blocks, None, cx);
2881 let ids = editor.insert_blocks(new_blocks, None, cx);
2882 self.blocks = HashSet::from_iter(ids);
2883 });
2884 }
2885
2886 fn quote_selection(
2887 workspace: &mut Workspace,
2888 _: &QuoteSelection,
2889 cx: &mut ViewContext<Workspace>,
2890 ) {
2891 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2892 return;
2893 };
2894 let Some(editor) = workspace
2895 .active_item(cx)
2896 .and_then(|item| item.act_as::<Editor>(cx))
2897 else {
2898 return;
2899 };
2900
2901 let editor = editor.read(cx);
2902 let range = editor.selections.newest::<usize>(cx).range();
2903 let buffer = editor.buffer().read(cx).snapshot(cx);
2904 let start_language = buffer.language_at(range.start);
2905 let end_language = buffer.language_at(range.end);
2906 let language_name = if start_language == end_language {
2907 start_language.map(|language| language.code_fence_block_name())
2908 } else {
2909 None
2910 };
2911 let language_name = language_name.as_deref().unwrap_or("");
2912
2913 let selected_text = buffer.text_for_range(range).collect::<String>();
2914 let text = if selected_text.is_empty() {
2915 None
2916 } else {
2917 Some(if language_name == "markdown" {
2918 selected_text
2919 .lines()
2920 .map(|line| format!("> {}", line))
2921 .collect::<Vec<_>>()
2922 .join("\n")
2923 } else {
2924 format!("```{language_name}\n{selected_text}\n```")
2925 })
2926 };
2927
2928 // Activate the panel
2929 if !panel.focus_handle(cx).contains_focused(cx) {
2930 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2931 }
2932
2933 if let Some(text) = text {
2934 panel.update(cx, |_, cx| {
2935 // Wait to create a new context until the workspace is no longer
2936 // being updated.
2937 cx.defer(move |panel, cx| {
2938 if let Some(context) = panel
2939 .active_context_editor()
2940 .cloned()
2941 .or_else(|| panel.new_context(cx))
2942 {
2943 context.update(cx, |context, cx| {
2944 context
2945 .editor
2946 .update(cx, |editor, cx| editor.insert(&text, cx))
2947 });
2948 };
2949 });
2950 });
2951 }
2952 }
2953
2954 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2955 let editor = self.editor.read(cx);
2956 let context = self.context.read(cx);
2957 if editor.selections.count() == 1 {
2958 let selection = editor.selections.newest::<usize>(cx);
2959 let mut copied_text = String::new();
2960 let mut spanned_messages = 0;
2961 for message in context.messages(cx) {
2962 if message.offset_range.start >= selection.range().end {
2963 break;
2964 } else if message.offset_range.end >= selection.range().start {
2965 let range = cmp::max(message.offset_range.start, selection.range().start)
2966 ..cmp::min(message.offset_range.end, selection.range().end);
2967 if !range.is_empty() {
2968 spanned_messages += 1;
2969 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2970 for chunk in context.buffer.read(cx).text_for_range(range) {
2971 copied_text.push_str(chunk);
2972 }
2973 copied_text.push('\n');
2974 }
2975 }
2976 }
2977
2978 if spanned_messages > 1 {
2979 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2980 return;
2981 }
2982 }
2983
2984 cx.propagate();
2985 }
2986
2987 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2988 self.context.update(cx, |context, cx| {
2989 let selections = self.editor.read(cx).selections.disjoint_anchors();
2990 for selection in selections.as_ref() {
2991 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2992 let range = selection
2993 .map(|endpoint| endpoint.to_offset(&buffer))
2994 .range();
2995 context.split_message(range, cx);
2996 }
2997 });
2998 }
2999
3000 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
3001 let Some(workspace) = self.workspace.upgrade() else {
3002 return;
3003 };
3004 let project = workspace.read(cx).project().clone();
3005
3006 struct Edit {
3007 old_text: String,
3008 new_text: String,
3009 }
3010
3011 let context = self.context.read(cx);
3012 let context_buffer = context.buffer.read(cx);
3013 let context_buffer_snapshot = context_buffer.snapshot();
3014
3015 let selections = self.editor.read(cx).selections.disjoint_anchors();
3016 let mut selections = selections.iter().peekable();
3017 let selected_suggestions = context
3018 .edit_suggestions
3019 .iter()
3020 .filter(|suggestion| {
3021 while let Some(selection) = selections.peek() {
3022 if selection
3023 .end
3024 .text_anchor
3025 .cmp(&suggestion.source_range.start, context_buffer)
3026 .is_lt()
3027 {
3028 selections.next();
3029 continue;
3030 }
3031 if selection
3032 .start
3033 .text_anchor
3034 .cmp(&suggestion.source_range.end, context_buffer)
3035 .is_gt()
3036 {
3037 break;
3038 }
3039 return true;
3040 }
3041 false
3042 })
3043 .cloned()
3044 .collect::<Vec<_>>();
3045
3046 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
3047 project.update(cx, |project, cx| {
3048 for suggestion in &selected_suggestions {
3049 opened_buffers
3050 .entry(suggestion.full_path.clone())
3051 .or_insert_with(|| {
3052 project.open_buffer_for_full_path(&suggestion.full_path, cx)
3053 });
3054 }
3055 });
3056
3057 cx.spawn(|this, mut cx| async move {
3058 let mut buffers_by_full_path = HashMap::default();
3059 for (full_path, buffer) in opened_buffers {
3060 if let Some(buffer) = buffer.await.log_err() {
3061 buffers_by_full_path.insert(full_path, buffer);
3062 }
3063 }
3064
3065 let mut suggestions_by_buffer = HashMap::default();
3066 cx.update(|cx| {
3067 for suggestion in selected_suggestions {
3068 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
3069 let (_, edits) = suggestions_by_buffer
3070 .entry(buffer.clone())
3071 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
3072
3073 let mut lines = context_buffer_snapshot
3074 .as_rope()
3075 .chunks_in_range(
3076 suggestion.source_range.to_offset(&context_buffer_snapshot),
3077 )
3078 .lines();
3079 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
3080 let old_text = context_buffer_snapshot
3081 .text_for_range(suggestion.old_text_range)
3082 .collect();
3083 let new_text = context_buffer_snapshot
3084 .text_for_range(suggestion.new_text_range)
3085 .collect();
3086 edits.push(Edit { old_text, new_text });
3087 }
3088 }
3089 }
3090 })?;
3091
3092 let edits_by_buffer = cx
3093 .background_executor()
3094 .spawn(async move {
3095 let mut result = HashMap::default();
3096 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
3097 let edits =
3098 result
3099 .entry(buffer)
3100 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
3101 for suggestion in suggestions {
3102 if let Some(range) =
3103 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
3104 {
3105 let edit_start = snapshot.anchor_after(range.start);
3106 let edit_end = snapshot.anchor_before(range.end);
3107 if let Err(ix) = edits.binary_search_by(|(range, _)| {
3108 range.start.cmp(&edit_start, &snapshot)
3109 }) {
3110 edits.insert(
3111 ix,
3112 (edit_start..edit_end, suggestion.new_text.clone()),
3113 );
3114 }
3115 } else {
3116 log::info!(
3117 "assistant edit did not match any text in buffer {:?}",
3118 &suggestion.old_text
3119 );
3120 }
3121 }
3122 }
3123 result
3124 })
3125 .await;
3126
3127 let mut project_transaction = ProjectTransaction::default();
3128 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
3129 for (buffer_handle, edits) in edits_by_buffer {
3130 buffer_handle.update(cx, |buffer, cx| {
3131 buffer.start_transaction();
3132 buffer.edit(
3133 edits,
3134 Some(AutoindentMode::Block {
3135 original_indent_columns: Vec::new(),
3136 }),
3137 cx,
3138 );
3139 buffer.end_transaction(cx);
3140 if let Some(transaction) = buffer.finalize_last_transaction() {
3141 project_transaction
3142 .0
3143 .insert(buffer_handle.clone(), transaction.clone());
3144 }
3145 });
3146 }
3147
3148 (
3149 this.editor.downgrade(),
3150 this.workspace.clone(),
3151 this.title(cx),
3152 )
3153 })?;
3154
3155 Editor::open_project_transaction(
3156 &editor,
3157 workspace,
3158 project_transaction,
3159 format!("Edits from {}", title),
3160 cx,
3161 )
3162 .await
3163 })
3164 .detach_and_log_err(cx);
3165 }
3166
3167 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3168 self.context
3169 .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
3170 }
3171
3172 fn title(&self, cx: &AppContext) -> String {
3173 self.context
3174 .read(cx)
3175 .summary
3176 .as_ref()
3177 .map(|summary| summary.text.clone())
3178 .unwrap_or_else(|| "New Context".into())
3179 }
3180}
3181
3182impl EventEmitter<ContextEditorEvent> for ContextEditor {}
3183
3184impl Render for ContextEditor {
3185 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3186 div()
3187 .key_context("ContextEditor")
3188 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
3189 .capture_action(cx.listener(ContextEditor::save))
3190 .capture_action(cx.listener(ContextEditor::copy))
3191 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3192 .capture_action(cx.listener(ContextEditor::confirm_command))
3193 .on_action(cx.listener(ContextEditor::assist))
3194 .on_action(cx.listener(ContextEditor::split))
3195 .on_action(cx.listener(ContextEditor::apply_edit))
3196 .size_full()
3197 .v_flex()
3198 .child(
3199 div()
3200 .flex_grow()
3201 .bg(cx.theme().colors().editor_background)
3202 .child(self.editor.clone()),
3203 )
3204 }
3205}
3206
3207impl FocusableView for ContextEditor {
3208 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3209 self.editor.focus_handle(cx)
3210 }
3211}
3212
3213#[derive(Clone, Debug)]
3214struct MessageAnchor {
3215 id: MessageId,
3216 start: language::Anchor,
3217}
3218
3219#[derive(Clone, Debug)]
3220pub struct Message {
3221 offset_range: Range<usize>,
3222 index_range: Range<usize>,
3223 id: MessageId,
3224 anchor: language::Anchor,
3225 role: Role,
3226 status: MessageStatus,
3227}
3228
3229impl Message {
3230 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
3231 LanguageModelRequestMessage {
3232 role: self.role,
3233 content: buffer.text_for_range(self.offset_range.clone()).collect(),
3234 }
3235 }
3236}
3237
3238type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3239
3240fn render_slash_command_output_toggle(
3241 row: MultiBufferRow,
3242 is_folded: bool,
3243 fold: ToggleFold,
3244 _cx: &mut WindowContext,
3245) -> AnyElement {
3246 Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
3247 .selected(is_folded)
3248 .on_click(move |_e, cx| fold(!is_folded, cx))
3249 .into_any_element()
3250}
3251
3252fn render_pending_slash_command_gutter_decoration(
3253 row: MultiBufferRow,
3254 status: &PendingSlashCommandStatus,
3255 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3256) -> AnyElement {
3257 let mut icon = IconButton::new(
3258 ("slash-command-gutter-decoration", row.0),
3259 ui::IconName::TriangleRight,
3260 )
3261 .on_click(move |_e, cx| confirm_command(cx))
3262 .icon_size(ui::IconSize::Small)
3263 .size(ui::ButtonSize::None);
3264
3265 match status {
3266 PendingSlashCommandStatus::Idle => {
3267 icon = icon.icon_color(Color::Muted);
3268 }
3269 PendingSlashCommandStatus::Running { .. } => {
3270 icon = icon.selected(true);
3271 }
3272 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3273 }
3274
3275 icon.into_any_element()
3276}
3277
3278fn render_rustdoc_slash_command_trailer(
3279 row: MultiBufferRow,
3280 command: PendingSlashCommand,
3281 cx: &mut WindowContext,
3282) -> AnyElement {
3283 let rustdoc_store = RustdocStore::global(cx);
3284
3285 let Some((crate_name, _)) = command
3286 .argument
3287 .as_ref()
3288 .and_then(|arg| arg.split_once(':'))
3289 else {
3290 return Empty.into_any();
3291 };
3292
3293 let crate_name = CrateName::from(crate_name);
3294 if !rustdoc_store.is_indexing(&crate_name) {
3295 return Empty.into_any();
3296 }
3297
3298 div()
3299 .id(("crates-being-indexed", row.0))
3300 .child(Icon::new(IconName::ArrowCircle).with_animation(
3301 "arrow-circle",
3302 Animation::new(Duration::from_secs(4)).repeat(),
3303 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3304 ))
3305 .tooltip(move |cx| Tooltip::text(format!("Indexing {crate_name}…"), cx))
3306 .into_any_element()
3307}
3308
3309fn make_lsp_adapter_delegate(
3310 project: &Model<Project>,
3311 cx: &mut AppContext,
3312) -> Result<Arc<dyn LspAdapterDelegate>> {
3313 project.update(cx, |project, cx| {
3314 // TODO: Find the right worktree.
3315 let worktree = project
3316 .worktrees()
3317 .next()
3318 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3319 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3320 })
3321}
3322
3323fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3324 Box::new(move |_| {
3325 div()
3326 .pl_6()
3327 .child(
3328 Label::new(format!("error: {}", message))
3329 .single_line()
3330 .color(Color::Error),
3331 )
3332 .into_any()
3333 })
3334}
3335
3336#[cfg(test)]
3337mod tests {
3338 use super::*;
3339 use crate::{
3340 slash_command::{active_command, file_command},
3341 FakeCompletionProvider, MessageId,
3342 };
3343 use fs::FakeFs;
3344 use gpui::{AppContext, TestAppContext};
3345 use rope::Rope;
3346 use serde_json::json;
3347 use settings::SettingsStore;
3348 use std::{cell::RefCell, path::Path, rc::Rc};
3349 use unindent::Unindent;
3350 use util::test::marked_text_ranges;
3351
3352 #[gpui::test]
3353 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3354 let settings_store = SettingsStore::test(cx);
3355 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3356 cx.set_global(settings_store);
3357 init(cx);
3358 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3359
3360 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3361 let buffer = context.read(cx).buffer.clone();
3362
3363 let message_1 = context.read(cx).message_anchors[0].clone();
3364 assert_eq!(
3365 messages(&context, cx),
3366 vec![(message_1.id, Role::User, 0..0)]
3367 );
3368
3369 let message_2 = context.update(cx, |context, cx| {
3370 context
3371 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3372 .unwrap()
3373 });
3374 assert_eq!(
3375 messages(&context, cx),
3376 vec![
3377 (message_1.id, Role::User, 0..1),
3378 (message_2.id, Role::Assistant, 1..1)
3379 ]
3380 );
3381
3382 buffer.update(cx, |buffer, cx| {
3383 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3384 });
3385 assert_eq!(
3386 messages(&context, cx),
3387 vec![
3388 (message_1.id, Role::User, 0..2),
3389 (message_2.id, Role::Assistant, 2..3)
3390 ]
3391 );
3392
3393 let message_3 = context.update(cx, |context, cx| {
3394 context
3395 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3396 .unwrap()
3397 });
3398 assert_eq!(
3399 messages(&context, cx),
3400 vec![
3401 (message_1.id, Role::User, 0..2),
3402 (message_2.id, Role::Assistant, 2..4),
3403 (message_3.id, Role::User, 4..4)
3404 ]
3405 );
3406
3407 let message_4 = context.update(cx, |context, cx| {
3408 context
3409 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3410 .unwrap()
3411 });
3412 assert_eq!(
3413 messages(&context, cx),
3414 vec![
3415 (message_1.id, Role::User, 0..2),
3416 (message_2.id, Role::Assistant, 2..4),
3417 (message_4.id, Role::User, 4..5),
3418 (message_3.id, Role::User, 5..5),
3419 ]
3420 );
3421
3422 buffer.update(cx, |buffer, cx| {
3423 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3424 });
3425 assert_eq!(
3426 messages(&context, cx),
3427 vec![
3428 (message_1.id, Role::User, 0..2),
3429 (message_2.id, Role::Assistant, 2..4),
3430 (message_4.id, Role::User, 4..6),
3431 (message_3.id, Role::User, 6..7),
3432 ]
3433 );
3434
3435 // Deleting across message boundaries merges the messages.
3436 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3437 assert_eq!(
3438 messages(&context, cx),
3439 vec![
3440 (message_1.id, Role::User, 0..3),
3441 (message_3.id, Role::User, 3..4),
3442 ]
3443 );
3444
3445 // Undoing the deletion should also undo the merge.
3446 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3447 assert_eq!(
3448 messages(&context, cx),
3449 vec![
3450 (message_1.id, Role::User, 0..2),
3451 (message_2.id, Role::Assistant, 2..4),
3452 (message_4.id, Role::User, 4..6),
3453 (message_3.id, Role::User, 6..7),
3454 ]
3455 );
3456
3457 // Redoing the deletion should also redo the merge.
3458 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3459 assert_eq!(
3460 messages(&context, cx),
3461 vec![
3462 (message_1.id, Role::User, 0..3),
3463 (message_3.id, Role::User, 3..4),
3464 ]
3465 );
3466
3467 // Ensure we can still insert after a merged message.
3468 let message_5 = context.update(cx, |context, cx| {
3469 context
3470 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3471 .unwrap()
3472 });
3473 assert_eq!(
3474 messages(&context, cx),
3475 vec![
3476 (message_1.id, Role::User, 0..3),
3477 (message_5.id, Role::System, 3..4),
3478 (message_3.id, Role::User, 4..5)
3479 ]
3480 );
3481 }
3482
3483 #[gpui::test]
3484 fn test_message_splitting(cx: &mut AppContext) {
3485 let settings_store = SettingsStore::test(cx);
3486 cx.set_global(settings_store);
3487 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3488 init(cx);
3489 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3490
3491 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3492 let buffer = context.read(cx).buffer.clone();
3493
3494 let message_1 = context.read(cx).message_anchors[0].clone();
3495 assert_eq!(
3496 messages(&context, cx),
3497 vec![(message_1.id, Role::User, 0..0)]
3498 );
3499
3500 buffer.update(cx, |buffer, cx| {
3501 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3502 });
3503
3504 let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3505 let message_2 = message_2.unwrap();
3506
3507 // We recycle newlines in the middle of a split message
3508 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3509 assert_eq!(
3510 messages(&context, cx),
3511 vec![
3512 (message_1.id, Role::User, 0..4),
3513 (message_2.id, Role::User, 4..16),
3514 ]
3515 );
3516
3517 let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3518 let message_3 = message_3.unwrap();
3519
3520 // We don't recycle newlines at the end of a split message
3521 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3522 assert_eq!(
3523 messages(&context, cx),
3524 vec![
3525 (message_1.id, Role::User, 0..4),
3526 (message_3.id, Role::User, 4..5),
3527 (message_2.id, Role::User, 5..17),
3528 ]
3529 );
3530
3531 let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3532 let message_4 = message_4.unwrap();
3533 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3534 assert_eq!(
3535 messages(&context, cx),
3536 vec![
3537 (message_1.id, Role::User, 0..4),
3538 (message_3.id, Role::User, 4..5),
3539 (message_2.id, Role::User, 5..9),
3540 (message_4.id, Role::User, 9..17),
3541 ]
3542 );
3543
3544 let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3545 let message_5 = message_5.unwrap();
3546 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3547 assert_eq!(
3548 messages(&context, cx),
3549 vec![
3550 (message_1.id, Role::User, 0..4),
3551 (message_3.id, Role::User, 4..5),
3552 (message_2.id, Role::User, 5..9),
3553 (message_4.id, Role::User, 9..10),
3554 (message_5.id, Role::User, 10..18),
3555 ]
3556 );
3557
3558 let (message_6, message_7) =
3559 context.update(cx, |context, cx| context.split_message(14..16, cx));
3560 let message_6 = message_6.unwrap();
3561 let message_7 = message_7.unwrap();
3562 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3563 assert_eq!(
3564 messages(&context, cx),
3565 vec![
3566 (message_1.id, Role::User, 0..4),
3567 (message_3.id, Role::User, 4..5),
3568 (message_2.id, Role::User, 5..9),
3569 (message_4.id, Role::User, 9..10),
3570 (message_5.id, Role::User, 10..14),
3571 (message_6.id, Role::User, 14..17),
3572 (message_7.id, Role::User, 17..19),
3573 ]
3574 );
3575 }
3576
3577 #[gpui::test]
3578 fn test_messages_for_offsets(cx: &mut AppContext) {
3579 let settings_store = SettingsStore::test(cx);
3580 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3581 cx.set_global(settings_store);
3582 init(cx);
3583 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3584 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3585 let buffer = context.read(cx).buffer.clone();
3586
3587 let message_1 = context.read(cx).message_anchors[0].clone();
3588 assert_eq!(
3589 messages(&context, cx),
3590 vec![(message_1.id, Role::User, 0..0)]
3591 );
3592
3593 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3594 let message_2 = context
3595 .update(cx, |context, cx| {
3596 context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3597 })
3598 .unwrap();
3599 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3600
3601 let message_3 = context
3602 .update(cx, |context, cx| {
3603 context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3604 })
3605 .unwrap();
3606 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3607
3608 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3609 assert_eq!(
3610 messages(&context, cx),
3611 vec![
3612 (message_1.id, Role::User, 0..4),
3613 (message_2.id, Role::User, 4..8),
3614 (message_3.id, Role::User, 8..11)
3615 ]
3616 );
3617
3618 assert_eq!(
3619 message_ids_for_offsets(&context, &[0, 4, 9], cx),
3620 [message_1.id, message_2.id, message_3.id]
3621 );
3622 assert_eq!(
3623 message_ids_for_offsets(&context, &[0, 1, 11], cx),
3624 [message_1.id, message_3.id]
3625 );
3626
3627 let message_4 = context
3628 .update(cx, |context, cx| {
3629 context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3630 })
3631 .unwrap();
3632 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3633 assert_eq!(
3634 messages(&context, cx),
3635 vec![
3636 (message_1.id, Role::User, 0..4),
3637 (message_2.id, Role::User, 4..8),
3638 (message_3.id, Role::User, 8..12),
3639 (message_4.id, Role::User, 12..12)
3640 ]
3641 );
3642 assert_eq!(
3643 message_ids_for_offsets(&context, &[0, 4, 8, 12], cx),
3644 [message_1.id, message_2.id, message_3.id, message_4.id]
3645 );
3646
3647 fn message_ids_for_offsets(
3648 context: &Model<Context>,
3649 offsets: &[usize],
3650 cx: &AppContext,
3651 ) -> Vec<MessageId> {
3652 context
3653 .read(cx)
3654 .messages_for_offsets(offsets.iter().copied(), cx)
3655 .into_iter()
3656 .map(|message| message.id)
3657 .collect()
3658 }
3659 }
3660
3661 #[gpui::test]
3662 async fn test_slash_commands(cx: &mut TestAppContext) {
3663 let settings_store = cx.update(SettingsStore::test);
3664 cx.set_global(settings_store);
3665 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3666 cx.update(Project::init_settings);
3667 cx.update(init);
3668 let fs = FakeFs::new(cx.background_executor.clone());
3669
3670 fs.insert_tree(
3671 "/test",
3672 json!({
3673 "src": {
3674 "lib.rs": "fn one() -> usize { 1 }",
3675 "main.rs": "
3676 use crate::one;
3677 fn main() { one(); }
3678 ".unindent(),
3679 }
3680 }),
3681 )
3682 .await;
3683
3684 let slash_command_registry = SlashCommandRegistry::new();
3685 slash_command_registry.register_command(file_command::FileSlashCommand, false);
3686 slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
3687
3688 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3689 let context =
3690 cx.new_model(|cx| Context::new(registry.clone(), slash_command_registry, None, cx));
3691
3692 let output_ranges = Rc::new(RefCell::new(HashSet::default()));
3693 context.update(cx, |_, cx| {
3694 cx.subscribe(&context, {
3695 let ranges = output_ranges.clone();
3696 move |_, _, event, _| match event {
3697 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
3698 for range in removed {
3699 ranges.borrow_mut().remove(range);
3700 }
3701 for command in updated {
3702 ranges.borrow_mut().insert(command.source_range.clone());
3703 }
3704 }
3705 _ => {}
3706 }
3707 })
3708 .detach();
3709 });
3710
3711 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
3712
3713 // Insert a slash command
3714 buffer.update(cx, |buffer, cx| {
3715 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
3716 });
3717 assert_text_and_output_ranges(
3718 &buffer,
3719 &output_ranges.borrow(),
3720 "
3721 «/file src/lib.rs»
3722 "
3723 .unindent()
3724 .trim_end(),
3725 cx,
3726 );
3727
3728 // Edit the argument of the slash command.
3729 buffer.update(cx, |buffer, cx| {
3730 let edit_offset = buffer.text().find("lib.rs").unwrap();
3731 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
3732 });
3733 assert_text_and_output_ranges(
3734 &buffer,
3735 &output_ranges.borrow(),
3736 "
3737 «/file src/main.rs»
3738 "
3739 .unindent()
3740 .trim_end(),
3741 cx,
3742 );
3743
3744 // Edit the name of the slash command, using one that doesn't exist.
3745 buffer.update(cx, |buffer, cx| {
3746 let edit_offset = buffer.text().find("/file").unwrap();
3747 buffer.edit(
3748 [(edit_offset..edit_offset + "/file".len(), "/unknown")],
3749 None,
3750 cx,
3751 );
3752 });
3753 assert_text_and_output_ranges(
3754 &buffer,
3755 &output_ranges.borrow(),
3756 "
3757 /unknown src/main.rs
3758 "
3759 .unindent()
3760 .trim_end(),
3761 cx,
3762 );
3763
3764 #[track_caller]
3765 fn assert_text_and_output_ranges(
3766 buffer: &Model<Buffer>,
3767 ranges: &HashSet<Range<language::Anchor>>,
3768 expected_marked_text: &str,
3769 cx: &mut TestAppContext,
3770 ) {
3771 let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
3772 let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
3773 let mut ranges = ranges
3774 .iter()
3775 .map(|range| range.to_offset(buffer))
3776 .collect::<Vec<_>>();
3777 ranges.sort_by_key(|a| a.start);
3778 (buffer.text(), ranges)
3779 });
3780
3781 assert_eq!(actual_text, expected_text);
3782 assert_eq!(actual_ranges, expected_ranges);
3783 }
3784 }
3785
3786 #[test]
3787 fn test_parse_next_edit_suggestion() {
3788 let text = "
3789 some output:
3790
3791 ```edit src/foo.rs
3792 let a = 1;
3793 let b = 2;
3794 ---
3795 let w = 1;
3796 let x = 2;
3797 let y = 3;
3798 let z = 4;
3799 ```
3800
3801 some more output:
3802
3803 ```edit src/foo.rs
3804 let c = 1;
3805 ---
3806 ```
3807
3808 and the conclusion.
3809 "
3810 .unindent();
3811
3812 let rope = Rope::from(text.as_str());
3813 let mut lines = rope.chunks().lines();
3814 let mut suggestions = vec![];
3815 while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
3816 suggestions.push((
3817 suggestion.path.clone(),
3818 text[suggestion.old_text_range].to_string(),
3819 text[suggestion.new_text_range].to_string(),
3820 ));
3821 }
3822
3823 assert_eq!(
3824 suggestions,
3825 vec![
3826 (
3827 Path::new("src/foo.rs").into(),
3828 [
3829 " let a = 1;", //
3830 " let b = 2;",
3831 "",
3832 ]
3833 .join("\n"),
3834 [
3835 " let w = 1;",
3836 " let x = 2;",
3837 " let y = 3;",
3838 " let z = 4;",
3839 "",
3840 ]
3841 .join("\n"),
3842 ),
3843 (
3844 Path::new("src/foo.rs").into(),
3845 [
3846 " let c = 1;", //
3847 "",
3848 ]
3849 .join("\n"),
3850 String::new(),
3851 )
3852 ]
3853 );
3854 }
3855
3856 #[gpui::test]
3857 async fn test_serialization(cx: &mut TestAppContext) {
3858 let settings_store = cx.update(SettingsStore::test);
3859 cx.set_global(settings_store);
3860 cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
3861 cx.update(init);
3862 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3863 let context =
3864 cx.new_model(|cx| Context::new(registry.clone(), Default::default(), None, cx));
3865 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
3866 let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
3867 let message_1 = context.update(cx, |context, cx| {
3868 context
3869 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3870 .unwrap()
3871 });
3872 let message_2 = context.update(cx, |context, cx| {
3873 context
3874 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3875 .unwrap()
3876 });
3877 buffer.update(cx, |buffer, cx| {
3878 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3879 buffer.finalize_last_transaction();
3880 });
3881 let _message_3 = context.update(cx, |context, cx| {
3882 context
3883 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3884 .unwrap()
3885 });
3886 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3887 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
3888 assert_eq!(
3889 cx.read(|cx| messages(&context, cx)),
3890 [
3891 (message_0, Role::User, 0..2),
3892 (message_1.id, Role::Assistant, 2..6),
3893 (message_2.id, Role::System, 6..6),
3894 ]
3895 );
3896
3897 let deserialized_context = Context::deserialize(
3898 context.read_with(cx, |context, cx| context.serialize(cx)),
3899 Default::default(),
3900 registry.clone(),
3901 Default::default(),
3902 None,
3903 &mut cx.to_async(),
3904 )
3905 .await
3906 .unwrap();
3907 let deserialized_buffer =
3908 deserialized_context.read_with(cx, |context, _| context.buffer.clone());
3909 assert_eq!(
3910 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
3911 "a\nb\nc\n"
3912 );
3913 assert_eq!(
3914 cx.read(|cx| messages(&deserialized_context, cx)),
3915 [
3916 (message_0, Role::User, 0..2),
3917 (message_1.id, Role::Assistant, 2..6),
3918 (message_2.id, Role::System, 6..6),
3919 ]
3920 );
3921 }
3922
3923 fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
3924 context
3925 .read(cx)
3926 .messages(cx)
3927 .map(|message| (message.id, message.role, message.offset_range))
3928 .collect()
3929 }
3930}