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