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 response = CompletionProvider::global(cx).complete(request, cx);
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 response = response.await;
1426 let assistant_message_id = assistant_message.id;
1427 let mut response_latency = None;
1428 let stream_completion = async {
1429 let request_start = Instant::now();
1430 let mut messages = response.inner.await?;
1431
1432 while let Some(message) = messages.next().await {
1433 if response_latency.is_none() {
1434 response_latency = Some(request_start.elapsed());
1435 }
1436 let text = message?;
1437
1438 this.update(&mut cx, |this, cx| {
1439 let message_ix = this
1440 .message_anchors
1441 .iter()
1442 .position(|message| message.id == assistant_message_id)?;
1443 let message_range = this.buffer.update(cx, |buffer, cx| {
1444 let message_start_offset =
1445 this.message_anchors[message_ix].start.to_offset(buffer);
1446 let message_old_end_offset = this.message_anchors
1447 [message_ix + 1..]
1448 .iter()
1449 .find(|message| message.start.is_valid(buffer))
1450 .map_or(buffer.len(), |message| {
1451 message.start.to_offset(buffer).saturating_sub(1)
1452 });
1453 let message_new_end_offset =
1454 message_old_end_offset + text.len();
1455 buffer.edit(
1456 [(message_old_end_offset..message_old_end_offset, text)],
1457 None,
1458 cx,
1459 );
1460 message_start_offset..message_new_end_offset
1461 });
1462 this.reparse_edit_suggestions_in_range(message_range, cx);
1463 cx.emit(ContextEvent::StreamedCompletion);
1464
1465 Some(())
1466 })?;
1467 smol::future::yield_now().await;
1468 }
1469
1470 this.update(&mut cx, |this, cx| {
1471 this.pending_completions
1472 .retain(|completion| completion.id != this.completion_count);
1473 this.summarize(cx);
1474 })?;
1475
1476 anyhow::Ok(())
1477 };
1478
1479 let result = stream_completion.await;
1480
1481 this.update(&mut cx, |this, cx| {
1482 if let Some(metadata) =
1483 this.messages_metadata.get_mut(&assistant_message.id)
1484 {
1485 let error_message = result
1486 .err()
1487 .map(|error| error.to_string().trim().to_string());
1488 if let Some(error_message) = error_message.as_ref() {
1489 metadata.status =
1490 MessageStatus::Error(SharedString::from(error_message.clone()));
1491 } else {
1492 metadata.status = MessageStatus::Done;
1493 }
1494
1495 if let Some(telemetry) = this.telemetry.as_ref() {
1496 let model = CompletionProvider::global(cx).model();
1497 telemetry.report_assistant_event(
1498 this.id.clone(),
1499 AssistantKind::Panel,
1500 model.telemetry_id(),
1501 response_latency,
1502 error_message,
1503 );
1504 }
1505
1506 cx.emit(ContextEvent::MessagesEdited);
1507 }
1508 })
1509 .ok();
1510 }
1511 });
1512
1513 self.pending_completions.push(PendingCompletion {
1514 id: post_inc(&mut self.completion_count),
1515 _task: task,
1516 });
1517 }
1518
1519 user_messages
1520 }
1521
1522 pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
1523 let messages = self
1524 .messages(cx)
1525 .filter(|message| matches!(message.status, MessageStatus::Done))
1526 .map(|message| message.to_request_message(self.buffer.read(cx)));
1527
1528 LanguageModelRequest {
1529 model: CompletionProvider::global(cx).model(),
1530 messages: messages.collect(),
1531 stop: vec![],
1532 temperature: 1.0,
1533 }
1534 }
1535
1536 fn cancel_last_assist(&mut self) -> bool {
1537 self.pending_completions.pop().is_some()
1538 }
1539
1540 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1541 for id in ids {
1542 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1543 metadata.role.cycle();
1544 cx.emit(ContextEvent::MessagesEdited);
1545 cx.notify();
1546 }
1547 }
1548 }
1549
1550 fn insert_message_after(
1551 &mut self,
1552 message_id: MessageId,
1553 role: Role,
1554 status: MessageStatus,
1555 cx: &mut ModelContext<Self>,
1556 ) -> Option<MessageAnchor> {
1557 if let Some(prev_message_ix) = self
1558 .message_anchors
1559 .iter()
1560 .position(|message| message.id == message_id)
1561 {
1562 // Find the next valid message after the one we were given.
1563 let mut next_message_ix = prev_message_ix + 1;
1564 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1565 if next_message.start.is_valid(self.buffer.read(cx)) {
1566 break;
1567 }
1568 next_message_ix += 1;
1569 }
1570
1571 let start = self.buffer.update(cx, |buffer, cx| {
1572 let offset = self
1573 .message_anchors
1574 .get(next_message_ix)
1575 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1576 buffer.edit([(offset..offset, "\n")], None, cx);
1577 buffer.anchor_before(offset + 1)
1578 });
1579 let message = MessageAnchor {
1580 id: MessageId(post_inc(&mut self.next_message_id.0)),
1581 start,
1582 };
1583 self.message_anchors
1584 .insert(next_message_ix, message.clone());
1585 self.messages_metadata
1586 .insert(message.id, MessageMetadata { role, status });
1587 cx.emit(ContextEvent::MessagesEdited);
1588 Some(message)
1589 } else {
1590 None
1591 }
1592 }
1593
1594 fn split_message(
1595 &mut self,
1596 range: Range<usize>,
1597 cx: &mut ModelContext<Self>,
1598 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1599 let start_message = self.message_for_offset(range.start, cx);
1600 let end_message = self.message_for_offset(range.end, cx);
1601 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1602 // Prevent splitting when range spans multiple messages.
1603 if start_message.id != end_message.id {
1604 return (None, None);
1605 }
1606
1607 let message = start_message;
1608 let role = message.role;
1609 let mut edited_buffer = false;
1610
1611 let mut suffix_start = None;
1612 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1613 {
1614 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1615 suffix_start = Some(range.end + 1);
1616 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1617 suffix_start = Some(range.end);
1618 }
1619 }
1620
1621 let suffix = if let Some(suffix_start) = suffix_start {
1622 MessageAnchor {
1623 id: MessageId(post_inc(&mut self.next_message_id.0)),
1624 start: self.buffer.read(cx).anchor_before(suffix_start),
1625 }
1626 } else {
1627 self.buffer.update(cx, |buffer, cx| {
1628 buffer.edit([(range.end..range.end, "\n")], None, cx);
1629 });
1630 edited_buffer = true;
1631 MessageAnchor {
1632 id: MessageId(post_inc(&mut self.next_message_id.0)),
1633 start: self.buffer.read(cx).anchor_before(range.end + 1),
1634 }
1635 };
1636
1637 self.message_anchors
1638 .insert(message.index_range.end + 1, suffix.clone());
1639 self.messages_metadata.insert(
1640 suffix.id,
1641 MessageMetadata {
1642 role,
1643 status: MessageStatus::Done,
1644 },
1645 );
1646
1647 let new_messages =
1648 if range.start == range.end || range.start == message.offset_range.start {
1649 (None, Some(suffix))
1650 } else {
1651 let mut prefix_end = None;
1652 if range.start > message.offset_range.start
1653 && range.end < message.offset_range.end - 1
1654 {
1655 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1656 prefix_end = Some(range.start + 1);
1657 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1658 == Some('\n')
1659 {
1660 prefix_end = Some(range.start);
1661 }
1662 }
1663
1664 let selection = if let Some(prefix_end) = prefix_end {
1665 cx.emit(ContextEvent::MessagesEdited);
1666 MessageAnchor {
1667 id: MessageId(post_inc(&mut self.next_message_id.0)),
1668 start: self.buffer.read(cx).anchor_before(prefix_end),
1669 }
1670 } else {
1671 self.buffer.update(cx, |buffer, cx| {
1672 buffer.edit([(range.start..range.start, "\n")], None, cx)
1673 });
1674 edited_buffer = true;
1675 MessageAnchor {
1676 id: MessageId(post_inc(&mut self.next_message_id.0)),
1677 start: self.buffer.read(cx).anchor_before(range.end + 1),
1678 }
1679 };
1680
1681 self.message_anchors
1682 .insert(message.index_range.end + 1, selection.clone());
1683 self.messages_metadata.insert(
1684 selection.id,
1685 MessageMetadata {
1686 role,
1687 status: MessageStatus::Done,
1688 },
1689 );
1690 (Some(selection), Some(suffix))
1691 };
1692
1693 if !edited_buffer {
1694 cx.emit(ContextEvent::MessagesEdited);
1695 }
1696 new_messages
1697 } else {
1698 (None, None)
1699 }
1700 }
1701
1702 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1703 if self.message_anchors.len() >= 2 && self.summary.is_none() {
1704 if !CompletionProvider::global(cx).is_authenticated() {
1705 return;
1706 }
1707
1708 let messages = self
1709 .messages(cx)
1710 .map(|message| message.to_request_message(self.buffer.read(cx)))
1711 .chain(Some(LanguageModelRequestMessage {
1712 role: Role::User,
1713 content: "Summarize the context into a short title without punctuation.".into(),
1714 }));
1715 let request = LanguageModelRequest {
1716 model: CompletionProvider::global(cx).model(),
1717 messages: messages.collect(),
1718 stop: vec![],
1719 temperature: 1.0,
1720 };
1721
1722 let response = CompletionProvider::global(cx).complete(request, cx);
1723 self.pending_summary = cx.spawn(|this, mut cx| {
1724 async move {
1725 let response = response.await;
1726 let mut messages = response.inner.await?;
1727
1728 while let Some(message) = messages.next().await {
1729 let text = message?;
1730 let mut lines = text.lines();
1731 this.update(&mut cx, |this, cx| {
1732 let summary = this.summary.get_or_insert(Default::default());
1733 summary.text.extend(lines.next());
1734 cx.emit(ContextEvent::SummaryChanged);
1735 })?;
1736
1737 // Stop if the LLM generated multiple lines.
1738 if lines.next().is_some() {
1739 break;
1740 }
1741 }
1742
1743 this.update(&mut cx, |this, cx| {
1744 if let Some(summary) = this.summary.as_mut() {
1745 summary.done = true;
1746 cx.emit(ContextEvent::SummaryChanged);
1747 }
1748 })?;
1749
1750 anyhow::Ok(())
1751 }
1752 .log_err()
1753 });
1754 }
1755 }
1756
1757 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
1758 self.messages_for_offsets([offset], cx).pop()
1759 }
1760
1761 fn messages_for_offsets(
1762 &self,
1763 offsets: impl IntoIterator<Item = usize>,
1764 cx: &AppContext,
1765 ) -> Vec<Message> {
1766 let mut result = Vec::new();
1767
1768 let mut messages = self.messages(cx).peekable();
1769 let mut offsets = offsets.into_iter().peekable();
1770 let mut current_message = messages.next();
1771 while let Some(offset) = offsets.next() {
1772 // Locate the message that contains the offset.
1773 while current_message.as_ref().map_or(false, |message| {
1774 !message.offset_range.contains(&offset) && messages.peek().is_some()
1775 }) {
1776 current_message = messages.next();
1777 }
1778 let Some(message) = current_message.as_ref() else {
1779 break;
1780 };
1781
1782 // Skip offsets that are in the same message.
1783 while offsets.peek().map_or(false, |offset| {
1784 message.offset_range.contains(offset) || messages.peek().is_none()
1785 }) {
1786 offsets.next();
1787 }
1788
1789 result.push(message.clone());
1790 }
1791 result
1792 }
1793
1794 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1795 let buffer = self.buffer.read(cx);
1796 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1797 iter::from_fn(move || {
1798 if let Some((start_ix, message_anchor)) = message_anchors.next() {
1799 let metadata = self.messages_metadata.get(&message_anchor.id)?;
1800 let message_start = message_anchor.start.to_offset(buffer);
1801 let mut message_end = None;
1802 let mut end_ix = start_ix;
1803 while let Some((_, next_message)) = message_anchors.peek() {
1804 if next_message.start.is_valid(buffer) {
1805 message_end = Some(next_message.start);
1806 break;
1807 } else {
1808 end_ix += 1;
1809 message_anchors.next();
1810 }
1811 }
1812 let message_end = message_end
1813 .unwrap_or(language::Anchor::MAX)
1814 .to_offset(buffer);
1815
1816 return Some(Message {
1817 index_range: start_ix..end_ix,
1818 offset_range: message_start..message_end,
1819 id: message_anchor.id,
1820 anchor: message_anchor.start,
1821 role: metadata.role,
1822 status: metadata.status.clone(),
1823 });
1824 }
1825 None
1826 })
1827 }
1828
1829 fn save(
1830 &mut self,
1831 debounce: Option<Duration>,
1832 fs: Arc<dyn Fs>,
1833 cx: &mut ModelContext<Context>,
1834 ) {
1835 self.pending_save = cx.spawn(|this, mut cx| async move {
1836 if let Some(debounce) = debounce {
1837 cx.background_executor().timer(debounce).await;
1838 }
1839
1840 let (old_path, summary) = this.read_with(&cx, |this, _| {
1841 let path = this.path.clone();
1842 let summary = if let Some(summary) = this.summary.as_ref() {
1843 if summary.done {
1844 Some(summary.text.clone())
1845 } else {
1846 None
1847 }
1848 } else {
1849 None
1850 };
1851 (path, summary)
1852 })?;
1853
1854 if let Some(summary) = summary {
1855 let context = this.read_with(&cx, |this, cx| this.serialize(cx))?;
1856 let path = if let Some(old_path) = old_path {
1857 old_path
1858 } else {
1859 let mut discriminant = 1;
1860 let mut new_path;
1861 loop {
1862 new_path = contexts_dir().join(&format!(
1863 "{} - {}.zed.json",
1864 summary.trim(),
1865 discriminant
1866 ));
1867 if fs.is_file(&new_path).await {
1868 discriminant += 1;
1869 } else {
1870 break;
1871 }
1872 }
1873 new_path
1874 };
1875
1876 fs.create_dir(contexts_dir().as_ref()).await?;
1877 fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap())
1878 .await?;
1879 this.update(&mut cx, |this, _| this.path = Some(path))?;
1880 }
1881
1882 Ok(())
1883 });
1884 }
1885}
1886
1887#[derive(Debug)]
1888enum EditParsingState {
1889 None,
1890 InOldText {
1891 path: PathBuf,
1892 start_offset: usize,
1893 old_text_start_offset: usize,
1894 },
1895 InNewText {
1896 path: PathBuf,
1897 start_offset: usize,
1898 old_text_range: Range<usize>,
1899 new_text_start_offset: usize,
1900 },
1901}
1902
1903#[derive(Clone, Debug, PartialEq)]
1904struct EditSuggestion {
1905 source_range: Range<language::Anchor>,
1906 full_path: PathBuf,
1907}
1908
1909struct ParsedEditSuggestion {
1910 path: PathBuf,
1911 outer_range: Range<usize>,
1912 old_text_range: Range<usize>,
1913 new_text_range: Range<usize>,
1914}
1915
1916fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSuggestion> {
1917 let mut state = EditParsingState::None;
1918 loop {
1919 let offset = lines.offset();
1920 let message_line = lines.next()?;
1921 match state {
1922 EditParsingState::None => {
1923 if let Some(rest) = message_line.strip_prefix("```edit ") {
1924 let path = rest.trim();
1925 if !path.is_empty() {
1926 state = EditParsingState::InOldText {
1927 path: PathBuf::from(path),
1928 start_offset: offset,
1929 old_text_start_offset: lines.offset(),
1930 };
1931 }
1932 }
1933 }
1934 EditParsingState::InOldText {
1935 path,
1936 start_offset,
1937 old_text_start_offset,
1938 } => {
1939 if message_line == "---" {
1940 state = EditParsingState::InNewText {
1941 path,
1942 start_offset,
1943 old_text_range: old_text_start_offset..offset,
1944 new_text_start_offset: lines.offset(),
1945 };
1946 } else {
1947 state = EditParsingState::InOldText {
1948 path,
1949 start_offset,
1950 old_text_start_offset,
1951 };
1952 }
1953 }
1954 EditParsingState::InNewText {
1955 path,
1956 start_offset,
1957 old_text_range,
1958 new_text_start_offset,
1959 } => {
1960 if message_line == "```" {
1961 return Some(ParsedEditSuggestion {
1962 path,
1963 outer_range: start_offset..offset + "```".len(),
1964 old_text_range,
1965 new_text_range: new_text_start_offset..offset,
1966 });
1967 } else {
1968 state = EditParsingState::InNewText {
1969 path,
1970 start_offset,
1971 old_text_range,
1972 new_text_start_offset,
1973 };
1974 }
1975 }
1976 }
1977 }
1978}
1979
1980#[derive(Clone)]
1981struct PendingSlashCommand {
1982 name: String,
1983 argument: Option<String>,
1984 status: PendingSlashCommandStatus,
1985 source_range: Range<language::Anchor>,
1986}
1987
1988#[derive(Clone)]
1989enum PendingSlashCommandStatus {
1990 Idle,
1991 Running { _task: Shared<Task<()>> },
1992 Error(String),
1993}
1994
1995struct PendingCompletion {
1996 id: usize,
1997 _task: Task<()>,
1998}
1999
2000pub enum ContextEditorEvent {
2001 Edited,
2002 TabContentChanged,
2003}
2004
2005#[derive(Copy, Clone, Debug, PartialEq)]
2006struct ScrollPosition {
2007 offset_before_cursor: gpui::Point<f32>,
2008 cursor: Anchor,
2009}
2010
2011pub struct ContextEditor {
2012 context: Model<Context>,
2013 fs: Arc<dyn Fs>,
2014 workspace: WeakView<Workspace>,
2015 slash_command_registry: Arc<SlashCommandRegistry>,
2016 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2017 editor: View<Editor>,
2018 blocks: HashSet<BlockId>,
2019 scroll_position: Option<ScrollPosition>,
2020 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
2021 pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
2022 _subscriptions: Vec<Subscription>,
2023}
2024
2025impl ContextEditor {
2026 const MAX_TAB_TITLE_LEN: usize = 16;
2027
2028 fn new(
2029 language_registry: Arc<LanguageRegistry>,
2030 slash_command_registry: Arc<SlashCommandRegistry>,
2031 fs: Arc<dyn Fs>,
2032 workspace: View<Workspace>,
2033 cx: &mut ViewContext<Self>,
2034 ) -> Self {
2035 let telemetry = workspace.read(cx).client().telemetry().clone();
2036 let project = workspace.read(cx).project().clone();
2037 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
2038
2039 let context = cx.new_model(|cx| {
2040 Context::new(
2041 language_registry,
2042 slash_command_registry,
2043 Some(telemetry),
2044 cx,
2045 )
2046 });
2047
2048 let mut this = Self::for_context(context, fs, workspace, lsp_adapter_delegate, cx);
2049 this.insert_default_prompt(cx);
2050 this
2051 }
2052
2053 fn for_context(
2054 context: Model<Context>,
2055 fs: Arc<dyn Fs>,
2056 workspace: View<Workspace>,
2057 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2058 cx: &mut ViewContext<Self>,
2059 ) -> Self {
2060 let slash_command_registry = context.read(cx).slash_command_registry.clone();
2061
2062 let completion_provider = SlashCommandCompletionProvider::new(
2063 slash_command_registry.clone(),
2064 Some(cx.view().downgrade()),
2065 Some(workspace.downgrade()),
2066 );
2067
2068 let editor = cx.new_view(|cx| {
2069 let mut editor = Editor::for_buffer(context.read(cx).buffer.clone(), None, cx);
2070 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2071 editor.set_show_line_numbers(false, cx);
2072 editor.set_show_git_diff_gutter(false, cx);
2073 editor.set_show_code_actions(false, cx);
2074 editor.set_show_runnables(false, cx);
2075 editor.set_show_wrap_guides(false, cx);
2076 editor.set_show_indent_guides(false, cx);
2077 editor.set_completion_provider(Box::new(completion_provider));
2078 editor
2079 });
2080
2081 let _subscriptions = vec![
2082 cx.observe(&context, |_, _, cx| cx.notify()),
2083 cx.subscribe(&context, Self::handle_context_event),
2084 cx.subscribe(&editor, Self::handle_editor_event),
2085 cx.subscribe(&editor, Self::handle_editor_search_event),
2086 ];
2087
2088 let sections = context.read(cx).slash_command_output_sections.clone();
2089 let mut this = Self {
2090 context,
2091 editor,
2092 slash_command_registry,
2093 lsp_adapter_delegate,
2094 blocks: Default::default(),
2095 scroll_position: None,
2096 fs,
2097 workspace: workspace.downgrade(),
2098 pending_slash_command_creases: HashMap::default(),
2099 pending_slash_command_blocks: HashMap::default(),
2100 _subscriptions,
2101 };
2102 this.update_message_headers(cx);
2103 this.insert_slash_command_output_sections(sections, cx);
2104 this
2105 }
2106
2107 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
2108 let command_name = DefaultSlashCommand.name();
2109 self.editor.update(cx, |editor, cx| {
2110 editor.insert(&format!("/{command_name}"), cx)
2111 });
2112 self.split(&Split, cx);
2113 let command = self.context.update(cx, |context, cx| {
2114 context
2115 .messages_metadata
2116 .get_mut(&MessageId::default())
2117 .unwrap()
2118 .role = Role::System;
2119 context.reparse_slash_commands(cx);
2120 context.pending_slash_commands[0].clone()
2121 });
2122
2123 self.run_command(
2124 command.source_range,
2125 &command.name,
2126 command.argument.as_deref(),
2127 false,
2128 self.workspace.clone(),
2129 cx,
2130 );
2131 }
2132
2133 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2134 let cursors = self.cursors(cx);
2135
2136 let user_messages = self.context.update(cx, |context, cx| {
2137 let selected_messages = context
2138 .messages_for_offsets(cursors, cx)
2139 .into_iter()
2140 .map(|message| message.id)
2141 .collect();
2142 context.assist(selected_messages, cx)
2143 });
2144 let new_selections = user_messages
2145 .iter()
2146 .map(|message| {
2147 let cursor = message
2148 .start
2149 .to_offset(self.context.read(cx).buffer.read(cx));
2150 cursor..cursor
2151 })
2152 .collect::<Vec<_>>();
2153 if !new_selections.is_empty() {
2154 self.editor.update(cx, |editor, cx| {
2155 editor.change_selections(
2156 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2157 cx,
2158 |selections| selections.select_ranges(new_selections),
2159 );
2160 });
2161 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2162 cx.defer(|this, _| this.scroll_position = None);
2163 }
2164 }
2165
2166 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2167 if !self
2168 .context
2169 .update(cx, |context, _| context.cancel_last_assist())
2170 {
2171 cx.propagate();
2172 }
2173 }
2174
2175 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2176 let cursors = self.cursors(cx);
2177 self.context.update(cx, |context, cx| {
2178 let messages = context
2179 .messages_for_offsets(cursors, cx)
2180 .into_iter()
2181 .map(|message| message.id)
2182 .collect();
2183 context.cycle_message_roles(messages, cx)
2184 });
2185 }
2186
2187 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2188 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2189 selections
2190 .into_iter()
2191 .map(|selection| selection.head())
2192 .collect()
2193 }
2194
2195 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2196 if let Some(command) = self.slash_command_registry.command(name) {
2197 self.editor.update(cx, |editor, cx| {
2198 editor.transact(cx, |editor, cx| {
2199 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2200 let snapshot = editor.buffer().read(cx).snapshot(cx);
2201 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2202 if newest_cursor.column > 0
2203 || snapshot
2204 .chars_at(newest_cursor)
2205 .next()
2206 .map_or(false, |ch| ch != '\n')
2207 {
2208 editor.move_to_end_of_line(
2209 &MoveToEndOfLine {
2210 stop_at_soft_wraps: false,
2211 },
2212 cx,
2213 );
2214 editor.newline(&Newline, cx);
2215 }
2216
2217 editor.insert(&format!("/{name}"), cx);
2218 if command.requires_argument() {
2219 editor.insert(" ", cx);
2220 editor.show_completions(&ShowCompletions::default(), cx);
2221 }
2222 });
2223 });
2224 if !command.requires_argument() {
2225 self.confirm_command(&ConfirmCommand, cx);
2226 }
2227 }
2228 }
2229
2230 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2231 let selections = self.editor.read(cx).selections.disjoint_anchors();
2232 let mut commands_by_range = HashMap::default();
2233 let workspace = self.workspace.clone();
2234 self.context.update(cx, |context, cx| {
2235 context.reparse_slash_commands(cx);
2236 for selection in selections.iter() {
2237 if let Some(command) =
2238 context.pending_command_for_position(selection.head().text_anchor, cx)
2239 {
2240 commands_by_range
2241 .entry(command.source_range.clone())
2242 .or_insert_with(|| command.clone());
2243 }
2244 }
2245 });
2246
2247 if commands_by_range.is_empty() {
2248 cx.propagate();
2249 } else {
2250 for command in commands_by_range.into_values() {
2251 self.run_command(
2252 command.source_range,
2253 &command.name,
2254 command.argument.as_deref(),
2255 true,
2256 workspace.clone(),
2257 cx,
2258 );
2259 }
2260 cx.stop_propagation();
2261 }
2262 }
2263
2264 pub fn run_command(
2265 &mut self,
2266 command_range: Range<language::Anchor>,
2267 name: &str,
2268 argument: Option<&str>,
2269 insert_trailing_newline: bool,
2270 workspace: WeakView<Workspace>,
2271 cx: &mut ViewContext<Self>,
2272 ) {
2273 if let Some(command) = self.slash_command_registry.command(name) {
2274 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
2275 let argument = argument.map(ToString::to_string);
2276 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
2277 self.context.update(cx, |context, cx| {
2278 context.insert_command_output(
2279 command_range,
2280 output,
2281 insert_trailing_newline,
2282 cx,
2283 )
2284 });
2285 }
2286 }
2287 }
2288
2289 fn handle_context_event(
2290 &mut self,
2291 _: Model<Context>,
2292 event: &ContextEvent,
2293 cx: &mut ViewContext<Self>,
2294 ) {
2295 let context_editor = cx.view().downgrade();
2296
2297 match event {
2298 ContextEvent::MessagesEdited => {
2299 self.update_message_headers(cx);
2300 self.context.update(cx, |context, cx| {
2301 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2302 });
2303 }
2304 ContextEvent::EditSuggestionsChanged => {
2305 self.editor.update(cx, |editor, cx| {
2306 let buffer = editor.buffer().read(cx).snapshot(cx);
2307 let excerpt_id = *buffer.as_singleton().unwrap().0;
2308 let context = self.context.read(cx);
2309 let highlighted_rows = context
2310 .edit_suggestions
2311 .iter()
2312 .map(|suggestion| {
2313 let start = buffer
2314 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2315 .unwrap();
2316 let end = buffer
2317 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2318 .unwrap();
2319 start..=end
2320 })
2321 .collect::<Vec<_>>();
2322
2323 editor.clear_row_highlights::<EditSuggestion>();
2324 for range in highlighted_rows {
2325 editor.highlight_rows::<EditSuggestion>(
2326 range,
2327 Some(
2328 cx.theme()
2329 .colors()
2330 .editor_document_highlight_read_background,
2331 ),
2332 false,
2333 cx,
2334 );
2335 }
2336 });
2337 }
2338 ContextEvent::SummaryChanged => {
2339 cx.emit(ContextEditorEvent::TabContentChanged);
2340 self.context.update(cx, |context, cx| {
2341 context.save(None, self.fs.clone(), cx);
2342 });
2343 }
2344 ContextEvent::StreamedCompletion => {
2345 self.editor.update(cx, |editor, cx| {
2346 if let Some(scroll_position) = self.scroll_position {
2347 let snapshot = editor.snapshot(cx);
2348 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2349 let scroll_top =
2350 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2351 editor.set_scroll_position(
2352 point(scroll_position.offset_before_cursor.x, scroll_top),
2353 cx,
2354 );
2355 }
2356 });
2357 }
2358 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2359 self.editor.update(cx, |editor, cx| {
2360 let buffer = editor.buffer().read(cx).snapshot(cx);
2361 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2362 let excerpt_id = *excerpt_id;
2363
2364 editor.remove_creases(
2365 removed
2366 .iter()
2367 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2368 cx,
2369 );
2370
2371 editor.remove_blocks(
2372 HashSet::from_iter(
2373 removed.iter().filter_map(|range| {
2374 self.pending_slash_command_blocks.remove(range)
2375 }),
2376 ),
2377 None,
2378 cx,
2379 );
2380
2381 let crease_ids = editor.insert_creases(
2382 updated.iter().map(|command| {
2383 let workspace = self.workspace.clone();
2384 let confirm_command = Arc::new({
2385 let context_editor = context_editor.clone();
2386 let command = command.clone();
2387 move |cx: &mut WindowContext| {
2388 context_editor
2389 .update(cx, |context_editor, cx| {
2390 context_editor.run_command(
2391 command.source_range.clone(),
2392 &command.name,
2393 command.argument.as_deref(),
2394 false,
2395 workspace.clone(),
2396 cx,
2397 );
2398 })
2399 .ok();
2400 }
2401 });
2402 let placeholder = FoldPlaceholder {
2403 render: Arc::new(move |_, _, _| Empty.into_any()),
2404 constrain_width: false,
2405 merge_adjacent: false,
2406 };
2407 let render_toggle = {
2408 let confirm_command = confirm_command.clone();
2409 let command = command.clone();
2410 move |row, _, _, _cx: &mut WindowContext| {
2411 render_pending_slash_command_gutter_decoration(
2412 row,
2413 &command.status,
2414 confirm_command.clone(),
2415 )
2416 }
2417 };
2418 let render_trailer = {
2419 let command = command.clone();
2420 move |row, _unfold, cx: &mut WindowContext| {
2421 // TODO: In the future we should investigate how we can expose
2422 // this as a hook on the `SlashCommand` trait so that we don't
2423 // need to special-case it here.
2424 if command.name == DocsSlashCommand::NAME {
2425 return render_docs_slash_command_trailer(
2426 row,
2427 command.clone(),
2428 cx,
2429 );
2430 }
2431
2432 Empty.into_any()
2433 }
2434 };
2435
2436 let start = buffer
2437 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2438 .unwrap();
2439 let end = buffer
2440 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2441 .unwrap();
2442 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2443 }),
2444 cx,
2445 );
2446
2447 let block_ids = editor.insert_blocks(
2448 updated
2449 .iter()
2450 .filter_map(|command| match &command.status {
2451 PendingSlashCommandStatus::Error(error) => {
2452 Some((command, error.clone()))
2453 }
2454 _ => None,
2455 })
2456 .map(|(command, error_message)| BlockProperties {
2457 style: BlockStyle::Fixed,
2458 position: Anchor {
2459 buffer_id: Some(buffer_id),
2460 excerpt_id,
2461 text_anchor: command.source_range.start,
2462 },
2463 height: 1,
2464 disposition: BlockDisposition::Below,
2465 render: slash_command_error_block_renderer(error_message),
2466 }),
2467 None,
2468 cx,
2469 );
2470
2471 self.pending_slash_command_creases.extend(
2472 updated
2473 .iter()
2474 .map(|command| command.source_range.clone())
2475 .zip(crease_ids),
2476 );
2477
2478 self.pending_slash_command_blocks.extend(
2479 updated
2480 .iter()
2481 .map(|command| command.source_range.clone())
2482 .zip(block_ids),
2483 );
2484 })
2485 }
2486 ContextEvent::SlashCommandFinished {
2487 output_range,
2488 sections,
2489 run_commands_in_output,
2490 } => {
2491 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2492
2493 if *run_commands_in_output {
2494 let commands = self.context.update(cx, |context, cx| {
2495 context.reparse_slash_commands(cx);
2496 context
2497 .pending_commands_for_range(output_range.clone(), cx)
2498 .to_vec()
2499 });
2500
2501 for command in commands {
2502 self.run_command(
2503 command.source_range,
2504 &command.name,
2505 command.argument.as_deref(),
2506 false,
2507 self.workspace.clone(),
2508 cx,
2509 );
2510 }
2511 }
2512 }
2513 }
2514 }
2515
2516 fn insert_slash_command_output_sections(
2517 &mut self,
2518 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2519 cx: &mut ViewContext<Self>,
2520 ) {
2521 self.editor.update(cx, |editor, cx| {
2522 let buffer = editor.buffer().read(cx).snapshot(cx);
2523 let excerpt_id = *buffer.as_singleton().unwrap().0;
2524 let mut buffer_rows_to_fold = BTreeSet::new();
2525 let mut creases = Vec::new();
2526 for section in sections {
2527 let start = buffer
2528 .anchor_in_excerpt(excerpt_id, section.range.start)
2529 .unwrap();
2530 let end = buffer
2531 .anchor_in_excerpt(excerpt_id, section.range.end)
2532 .unwrap();
2533 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2534 buffer_rows_to_fold.insert(buffer_row);
2535 creases.push(Crease::new(
2536 start..end,
2537 FoldPlaceholder {
2538 render: Arc::new({
2539 let editor = cx.view().downgrade();
2540 let icon = section.icon;
2541 let label = section.label.clone();
2542 move |fold_id, fold_range, _cx| {
2543 let editor = editor.clone();
2544 ButtonLike::new(fold_id)
2545 .style(ButtonStyle::Filled)
2546 .layer(ElevationIndex::ElevatedSurface)
2547 .child(Icon::new(icon))
2548 .child(Label::new(label.clone()).single_line())
2549 .on_click(move |_, cx| {
2550 editor
2551 .update(cx, |editor, cx| {
2552 let buffer_start = fold_range
2553 .start
2554 .to_point(&editor.buffer().read(cx).read(cx));
2555 let buffer_row = MultiBufferRow(buffer_start.row);
2556 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2557 })
2558 .ok();
2559 })
2560 .into_any_element()
2561 }
2562 }),
2563 constrain_width: false,
2564 merge_adjacent: false,
2565 },
2566 render_slash_command_output_toggle,
2567 |_, _, _| Empty.into_any_element(),
2568 ));
2569 }
2570
2571 editor.insert_creases(creases, cx);
2572
2573 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2574 editor.fold_at(&FoldAt { buffer_row }, cx);
2575 }
2576 });
2577 }
2578
2579 fn handle_editor_event(
2580 &mut self,
2581 _: View<Editor>,
2582 event: &EditorEvent,
2583 cx: &mut ViewContext<Self>,
2584 ) {
2585 match event {
2586 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2587 let cursor_scroll_position = self.cursor_scroll_position(cx);
2588 if *autoscroll {
2589 self.scroll_position = cursor_scroll_position;
2590 } else if self.scroll_position != cursor_scroll_position {
2591 self.scroll_position = None;
2592 }
2593 }
2594 EditorEvent::SelectionsChanged { .. } => {
2595 self.scroll_position = self.cursor_scroll_position(cx);
2596 }
2597 EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
2598 _ => {}
2599 }
2600 }
2601
2602 fn handle_editor_search_event(
2603 &mut self,
2604 _: View<Editor>,
2605 event: &SearchEvent,
2606 cx: &mut ViewContext<Self>,
2607 ) {
2608 cx.emit(event.clone());
2609 }
2610
2611 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2612 self.editor.update(cx, |editor, cx| {
2613 let snapshot = editor.snapshot(cx);
2614 let cursor = editor.selections.newest_anchor().head();
2615 let cursor_row = cursor
2616 .to_display_point(&snapshot.display_snapshot)
2617 .row()
2618 .as_f32();
2619 let scroll_position = editor
2620 .scroll_manager
2621 .anchor()
2622 .scroll_position(&snapshot.display_snapshot);
2623
2624 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2625 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2626 Some(ScrollPosition {
2627 cursor,
2628 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2629 })
2630 } else {
2631 None
2632 }
2633 })
2634 }
2635
2636 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2637 self.editor.update(cx, |editor, cx| {
2638 let buffer = editor.buffer().read(cx).snapshot(cx);
2639 let excerpt_id = *buffer.as_singleton().unwrap().0;
2640 let old_blocks = std::mem::take(&mut self.blocks);
2641 let new_blocks = self
2642 .context
2643 .read(cx)
2644 .messages(cx)
2645 .map(|message| BlockProperties {
2646 position: buffer
2647 .anchor_in_excerpt(excerpt_id, message.anchor)
2648 .unwrap(),
2649 height: 2,
2650 style: BlockStyle::Sticky,
2651 render: Box::new({
2652 let context = self.context.clone();
2653 move |cx| {
2654 let message_id = message.id;
2655 let sender = ButtonLike::new("role")
2656 .style(ButtonStyle::Filled)
2657 .child(match message.role {
2658 Role::User => Label::new("You").color(Color::Default),
2659 Role::Assistant => Label::new("Assistant").color(Color::Info),
2660 Role::System => Label::new("System").color(Color::Warning),
2661 })
2662 .tooltip(|cx| {
2663 Tooltip::with_meta(
2664 "Toggle message role",
2665 None,
2666 "Available roles: You (User), Assistant, System",
2667 cx,
2668 )
2669 })
2670 .on_click({
2671 let context = context.clone();
2672 move |_, cx| {
2673 context.update(cx, |context, cx| {
2674 context.cycle_message_roles(
2675 HashSet::from_iter(Some(message_id)),
2676 cx,
2677 )
2678 })
2679 }
2680 });
2681
2682 h_flex()
2683 .id(("message_header", message_id.0))
2684 .pl(cx.gutter_dimensions.full_width())
2685 .h_11()
2686 .w_full()
2687 .relative()
2688 .gap_1()
2689 .child(sender)
2690 .children(
2691 if let MessageStatus::Error(error) = message.status.clone() {
2692 Some(
2693 div()
2694 .id("error")
2695 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2696 .child(Icon::new(IconName::XCircle)),
2697 )
2698 } else {
2699 None
2700 },
2701 )
2702 .into_any_element()
2703 }
2704 }),
2705 disposition: BlockDisposition::Above,
2706 })
2707 .collect::<Vec<_>>();
2708
2709 editor.remove_blocks(old_blocks, None, cx);
2710 let ids = editor.insert_blocks(new_blocks, None, cx);
2711 self.blocks = HashSet::from_iter(ids);
2712 });
2713 }
2714
2715 fn insert_selection(
2716 workspace: &mut Workspace,
2717 _: &InsertIntoEditor,
2718 cx: &mut ViewContext<Workspace>,
2719 ) {
2720 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2721 return;
2722 };
2723 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2724 return;
2725 };
2726 let Some(active_editor_view) = workspace
2727 .active_item(cx)
2728 .and_then(|item| item.act_as::<Editor>(cx))
2729 else {
2730 return;
2731 };
2732
2733 let context_editor = context_editor_view.read(cx).editor.read(cx);
2734 let anchor = context_editor.selections.newest_anchor();
2735 let text = context_editor
2736 .buffer()
2737 .read(cx)
2738 .read(cx)
2739 .text_for_range(anchor.range())
2740 .collect::<String>();
2741
2742 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2743 if !text.is_empty() {
2744 active_editor_view.update(cx, |editor, cx| {
2745 editor.insert(&text, cx);
2746 editor.focus(cx);
2747 })
2748 }
2749 }
2750
2751 fn quote_selection(
2752 workspace: &mut Workspace,
2753 _: &QuoteSelection,
2754 cx: &mut ViewContext<Workspace>,
2755 ) {
2756 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2757 return;
2758 };
2759 let Some(editor) = workspace
2760 .active_item(cx)
2761 .and_then(|item| item.act_as::<Editor>(cx))
2762 else {
2763 return;
2764 };
2765
2766 let editor = editor.read(cx);
2767 let range = editor.selections.newest::<usize>(cx).range();
2768 let buffer = editor.buffer().read(cx).snapshot(cx);
2769 let start_language = buffer.language_at(range.start);
2770 let end_language = buffer.language_at(range.end);
2771 let language_name = if start_language == end_language {
2772 start_language.map(|language| language.code_fence_block_name())
2773 } else {
2774 None
2775 };
2776 let language_name = language_name.as_deref().unwrap_or("");
2777
2778 let selected_text = buffer.text_for_range(range).collect::<String>();
2779 let text = if selected_text.is_empty() {
2780 None
2781 } else {
2782 Some(if language_name == "markdown" {
2783 selected_text
2784 .lines()
2785 .map(|line| format!("> {}", line))
2786 .collect::<Vec<_>>()
2787 .join("\n")
2788 } else {
2789 format!("```{language_name}\n{selected_text}\n```")
2790 })
2791 };
2792
2793 // Activate the panel
2794 if !panel.focus_handle(cx).contains_focused(cx) {
2795 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2796 }
2797
2798 if let Some(text) = text {
2799 panel.update(cx, |_, cx| {
2800 // Wait to create a new context until the workspace is no longer
2801 // being updated.
2802 cx.defer(move |panel, cx| {
2803 if let Some(context) = panel
2804 .active_context_editor(cx)
2805 .or_else(|| panel.new_context(cx))
2806 {
2807 context.update(cx, |context, cx| {
2808 context
2809 .editor
2810 .update(cx, |editor, cx| editor.insert(&text, cx))
2811 });
2812 };
2813 });
2814 });
2815 }
2816 }
2817
2818 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2819 let editor = self.editor.read(cx);
2820 let context = self.context.read(cx);
2821 if editor.selections.count() == 1 {
2822 let selection = editor.selections.newest::<usize>(cx);
2823 let mut copied_text = String::new();
2824 let mut spanned_messages = 0;
2825 for message in context.messages(cx) {
2826 if message.offset_range.start >= selection.range().end {
2827 break;
2828 } else if message.offset_range.end >= selection.range().start {
2829 let range = cmp::max(message.offset_range.start, selection.range().start)
2830 ..cmp::min(message.offset_range.end, selection.range().end);
2831 if !range.is_empty() {
2832 spanned_messages += 1;
2833 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2834 for chunk in context.buffer.read(cx).text_for_range(range) {
2835 copied_text.push_str(chunk);
2836 }
2837 copied_text.push('\n');
2838 }
2839 }
2840 }
2841
2842 if spanned_messages > 1 {
2843 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2844 return;
2845 }
2846 }
2847
2848 cx.propagate();
2849 }
2850
2851 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2852 self.context.update(cx, |context, cx| {
2853 let selections = self.editor.read(cx).selections.disjoint_anchors();
2854 for selection in selections.as_ref() {
2855 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2856 let range = selection
2857 .map(|endpoint| endpoint.to_offset(&buffer))
2858 .range();
2859 context.split_message(range, cx);
2860 }
2861 });
2862 }
2863
2864 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
2865 let Some(workspace) = self.workspace.upgrade() else {
2866 return;
2867 };
2868 let project = workspace.read(cx).project().clone();
2869
2870 struct Edit {
2871 old_text: String,
2872 new_text: String,
2873 }
2874
2875 let context = self.context.read(cx);
2876 let context_buffer = context.buffer.read(cx);
2877 let context_buffer_snapshot = context_buffer.snapshot();
2878
2879 let selections = self.editor.read(cx).selections.disjoint_anchors();
2880 let mut selections = selections.iter().peekable();
2881 let selected_suggestions = context
2882 .edit_suggestions
2883 .iter()
2884 .filter(|suggestion| {
2885 while let Some(selection) = selections.peek() {
2886 if selection
2887 .end
2888 .text_anchor
2889 .cmp(&suggestion.source_range.start, context_buffer)
2890 .is_lt()
2891 {
2892 selections.next();
2893 continue;
2894 }
2895 if selection
2896 .start
2897 .text_anchor
2898 .cmp(&suggestion.source_range.end, context_buffer)
2899 .is_gt()
2900 {
2901 break;
2902 }
2903 return true;
2904 }
2905 false
2906 })
2907 .cloned()
2908 .collect::<Vec<_>>();
2909
2910 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
2911 project.update(cx, |project, cx| {
2912 for suggestion in &selected_suggestions {
2913 opened_buffers
2914 .entry(suggestion.full_path.clone())
2915 .or_insert_with(|| {
2916 project.open_buffer_for_full_path(&suggestion.full_path, cx)
2917 });
2918 }
2919 });
2920
2921 cx.spawn(|this, mut cx| async move {
2922 let mut buffers_by_full_path = HashMap::default();
2923 for (full_path, buffer) in opened_buffers {
2924 if let Some(buffer) = buffer.await.log_err() {
2925 buffers_by_full_path.insert(full_path, buffer);
2926 }
2927 }
2928
2929 let mut suggestions_by_buffer = HashMap::default();
2930 cx.update(|cx| {
2931 for suggestion in selected_suggestions {
2932 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
2933 let (_, edits) = suggestions_by_buffer
2934 .entry(buffer.clone())
2935 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
2936
2937 let mut lines = context_buffer_snapshot
2938 .as_rope()
2939 .chunks_in_range(
2940 suggestion.source_range.to_offset(&context_buffer_snapshot),
2941 )
2942 .lines();
2943 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
2944 let old_text = context_buffer_snapshot
2945 .text_for_range(suggestion.old_text_range)
2946 .collect();
2947 let new_text = context_buffer_snapshot
2948 .text_for_range(suggestion.new_text_range)
2949 .collect();
2950 edits.push(Edit { old_text, new_text });
2951 }
2952 }
2953 }
2954 })?;
2955
2956 let edits_by_buffer = cx
2957 .background_executor()
2958 .spawn(async move {
2959 let mut result = HashMap::default();
2960 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
2961 let edits =
2962 result
2963 .entry(buffer)
2964 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
2965 for suggestion in suggestions {
2966 if let Some(range) =
2967 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
2968 {
2969 let edit_start = snapshot.anchor_after(range.start);
2970 let edit_end = snapshot.anchor_before(range.end);
2971 if let Err(ix) = edits.binary_search_by(|(range, _)| {
2972 range.start.cmp(&edit_start, &snapshot)
2973 }) {
2974 edits.insert(
2975 ix,
2976 (edit_start..edit_end, suggestion.new_text.clone()),
2977 );
2978 }
2979 } else {
2980 log::info!(
2981 "assistant edit did not match any text in buffer {:?}",
2982 &suggestion.old_text
2983 );
2984 }
2985 }
2986 }
2987 result
2988 })
2989 .await;
2990
2991 let mut project_transaction = ProjectTransaction::default();
2992 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
2993 for (buffer_handle, edits) in edits_by_buffer {
2994 buffer_handle.update(cx, |buffer, cx| {
2995 buffer.start_transaction();
2996 buffer.edit(
2997 edits,
2998 Some(AutoindentMode::Block {
2999 original_indent_columns: Vec::new(),
3000 }),
3001 cx,
3002 );
3003 buffer.end_transaction(cx);
3004 if let Some(transaction) = buffer.finalize_last_transaction() {
3005 project_transaction
3006 .0
3007 .insert(buffer_handle.clone(), transaction.clone());
3008 }
3009 });
3010 }
3011
3012 (
3013 this.editor.downgrade(),
3014 this.workspace.clone(),
3015 this.title(cx),
3016 )
3017 })?;
3018
3019 Editor::open_project_transaction(
3020 &editor,
3021 workspace,
3022 project_transaction,
3023 format!("Edits from {}", title),
3024 cx,
3025 )
3026 .await
3027 })
3028 .detach_and_log_err(cx);
3029 }
3030
3031 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3032 self.context
3033 .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
3034 }
3035
3036 fn title(&self, cx: &AppContext) -> String {
3037 self.context
3038 .read(cx)
3039 .summary
3040 .as_ref()
3041 .map(|summary| summary.text.clone())
3042 .unwrap_or_else(|| "New Context".into())
3043 }
3044
3045 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3046 let focus_handle = self.focus_handle(cx).clone();
3047 ButtonLike::new("send_button")
3048 .style(ButtonStyle::Filled)
3049 .layer(ElevationIndex::ModalSurface)
3050 .children(
3051 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3052 .map(|binding| binding.into_any_element()),
3053 )
3054 .child(Label::new("Send"))
3055 .on_click(move |_event, cx| {
3056 focus_handle.dispatch_action(&Assist, cx);
3057 })
3058 }
3059}
3060
3061impl EventEmitter<ContextEditorEvent> for ContextEditor {}
3062impl EventEmitter<SearchEvent> for ContextEditor {}
3063
3064impl Render for ContextEditor {
3065 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3066 div()
3067 .key_context("ContextEditor")
3068 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
3069 .capture_action(cx.listener(ContextEditor::save))
3070 .capture_action(cx.listener(ContextEditor::copy))
3071 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3072 .capture_action(cx.listener(ContextEditor::confirm_command))
3073 .on_action(cx.listener(ContextEditor::assist))
3074 .on_action(cx.listener(ContextEditor::split))
3075 .on_action(cx.listener(ContextEditor::apply_edit))
3076 .size_full()
3077 .v_flex()
3078 .child(
3079 div()
3080 .flex_grow()
3081 .bg(cx.theme().colors().editor_background)
3082 .child(self.editor.clone())
3083 .child(
3084 h_flex()
3085 .w_full()
3086 .absolute()
3087 .bottom_0()
3088 .p_4()
3089 .justify_end()
3090 .child(self.render_send_button(cx)),
3091 ),
3092 )
3093 }
3094}
3095
3096impl FocusableView for ContextEditor {
3097 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3098 self.editor.focus_handle(cx)
3099 }
3100}
3101
3102impl Item for ContextEditor {
3103 type Event = ContextEditorEvent;
3104
3105 fn tab_content(
3106 &self,
3107 params: workspace::item::TabContentParams,
3108 cx: &WindowContext,
3109 ) -> AnyElement {
3110 let color = if params.selected {
3111 Color::Default
3112 } else {
3113 Color::Muted
3114 };
3115 Label::new(util::truncate_and_trailoff(
3116 &self.title(cx),
3117 Self::MAX_TAB_TITLE_LEN,
3118 ))
3119 .color(color)
3120 .into_any_element()
3121 }
3122
3123 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
3124 match event {
3125 ContextEditorEvent::Edited => {
3126 f(workspace::item::ItemEvent::Edit);
3127 f(workspace::item::ItemEvent::UpdateBreadcrumbs);
3128 }
3129 ContextEditorEvent::TabContentChanged => {
3130 f(workspace::item::ItemEvent::UpdateTab);
3131 }
3132 }
3133 }
3134
3135 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
3136 Some(self.title(cx).into())
3137 }
3138
3139 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
3140 Some(Box::new(handle.clone()))
3141 }
3142
3143 fn breadcrumbs(
3144 &self,
3145 theme: &theme::Theme,
3146 cx: &AppContext,
3147 ) -> Option<Vec<workspace::item::BreadcrumbText>> {
3148 let editor = self.editor.read(cx);
3149 let cursor = editor.selections.newest_anchor().head();
3150 let multibuffer = &editor.buffer().read(cx);
3151 let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?;
3152
3153 let settings = ThemeSettings::get_global(cx);
3154
3155 let mut breadcrumbs = Vec::new();
3156
3157 let title = self.title(cx);
3158 if title.chars().count() > Self::MAX_TAB_TITLE_LEN {
3159 breadcrumbs.push(BreadcrumbText {
3160 text: title,
3161 highlights: None,
3162 font: Some(settings.buffer_font.clone()),
3163 });
3164 }
3165
3166 breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
3167 text: symbol.text,
3168 highlights: Some(symbol.highlight_ranges),
3169 font: Some(settings.buffer_font.clone()),
3170 }));
3171 Some(breadcrumbs)
3172 }
3173
3174 fn breadcrumb_location(&self) -> ToolbarItemLocation {
3175 ToolbarItemLocation::PrimaryLeft
3176 }
3177
3178 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
3179 self.editor.update(cx, |editor, cx| {
3180 Item::set_nav_history(editor, nav_history, cx)
3181 })
3182 }
3183
3184 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
3185 self.editor
3186 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
3187 }
3188
3189 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3190 self.editor
3191 .update(cx, |editor, cx| Item::deactivated(editor, cx))
3192 }
3193}
3194
3195impl SearchableItem for ContextEditor {
3196 type Match = <Editor as SearchableItem>::Match;
3197
3198 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
3199 self.editor.update(cx, |editor, cx| {
3200 editor.clear_matches(cx);
3201 });
3202 }
3203
3204 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3205 self.editor
3206 .update(cx, |editor, cx| editor.update_matches(matches, cx));
3207 }
3208
3209 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
3210 self.editor
3211 .update(cx, |editor, cx| editor.query_suggestion(cx))
3212 }
3213
3214 fn activate_match(
3215 &mut self,
3216 index: usize,
3217 matches: &[Self::Match],
3218 cx: &mut ViewContext<Self>,
3219 ) {
3220 self.editor.update(cx, |editor, cx| {
3221 editor.activate_match(index, matches, cx);
3222 });
3223 }
3224
3225 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3226 self.editor
3227 .update(cx, |editor, cx| editor.select_matches(matches, cx));
3228 }
3229
3230 fn replace(
3231 &mut self,
3232 identifier: &Self::Match,
3233 query: &project::search::SearchQuery,
3234 cx: &mut ViewContext<Self>,
3235 ) {
3236 self.editor
3237 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
3238 }
3239
3240 fn find_matches(
3241 &mut self,
3242 query: Arc<project::search::SearchQuery>,
3243 cx: &mut ViewContext<Self>,
3244 ) -> Task<Vec<Self::Match>> {
3245 self.editor
3246 .update(cx, |editor, cx| editor.find_matches(query, cx))
3247 }
3248
3249 fn active_match_index(
3250 &mut self,
3251 matches: &[Self::Match],
3252 cx: &mut ViewContext<Self>,
3253 ) -> Option<usize> {
3254 self.editor
3255 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
3256 }
3257}
3258
3259pub struct ContextEditorToolbarItem {
3260 fs: Arc<dyn Fs>,
3261 workspace: WeakView<Workspace>,
3262 active_context_editor: Option<WeakView<ContextEditor>>,
3263 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
3264}
3265
3266impl ContextEditorToolbarItem {
3267 pub fn new(
3268 workspace: &Workspace,
3269 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
3270 ) -> Self {
3271 Self {
3272 fs: workspace.app_state().fs.clone(),
3273 workspace: workspace.weak_handle(),
3274 active_context_editor: None,
3275 model_selector_menu_handle,
3276 }
3277 }
3278
3279 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
3280 let commands = SlashCommandRegistry::global(cx);
3281 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
3282 Some(
3283 workspace
3284 .read(cx)
3285 .active_item_as::<Editor>(cx)?
3286 .focus_handle(cx),
3287 )
3288 });
3289 let active_context_editor = self.active_context_editor.clone();
3290
3291 PopoverMenu::new("inject-context-menu")
3292 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
3293 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
3294 }))
3295 .menu(move |cx| {
3296 let active_context_editor = active_context_editor.clone()?;
3297 ContextMenu::build(cx, |mut menu, _cx| {
3298 for command_name in commands.featured_command_names() {
3299 if let Some(command) = commands.command(&command_name) {
3300 let menu_text = SharedString::from(Arc::from(command.menu_text()));
3301 menu = menu.custom_entry(
3302 {
3303 let command_name = command_name.clone();
3304 move |_cx| {
3305 h_flex()
3306 .w_full()
3307 .justify_between()
3308 .child(Label::new(menu_text.clone()))
3309 .child(
3310 div().ml_4().child(
3311 Label::new(format!("/{command_name}"))
3312 .color(Color::Muted),
3313 ),
3314 )
3315 .into_any()
3316 }
3317 },
3318 {
3319 let active_context_editor = active_context_editor.clone();
3320 move |cx| {
3321 active_context_editor
3322 .update(cx, |context_editor, cx| {
3323 context_editor.insert_command(&command_name, cx)
3324 })
3325 .ok();
3326 }
3327 },
3328 )
3329 }
3330 }
3331
3332 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
3333 menu = menu
3334 .context(active_editor_focus_handle)
3335 .action("Quote Selection", Box::new(QuoteSelection));
3336 }
3337
3338 menu
3339 })
3340 .into()
3341 })
3342 }
3343
3344 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
3345 let model = CompletionProvider::global(cx).model();
3346 let context = &self
3347 .active_context_editor
3348 .as_ref()?
3349 .upgrade()?
3350 .read(cx)
3351 .context;
3352 let token_count = context.read(cx).token_count()?;
3353 let max_token_count = model.max_token_count();
3354
3355 let remaining_tokens = max_token_count as isize - token_count as isize;
3356 let token_count_color = if remaining_tokens <= 0 {
3357 Color::Error
3358 } else if token_count as f32 / max_token_count as f32 >= 0.8 {
3359 Color::Warning
3360 } else {
3361 Color::Muted
3362 };
3363
3364 Some(
3365 h_flex()
3366 .gap_0p5()
3367 .child(
3368 Label::new(humanize_token_count(token_count))
3369 .size(LabelSize::Small)
3370 .color(token_count_color),
3371 )
3372 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
3373 .child(
3374 Label::new(humanize_token_count(max_token_count))
3375 .size(LabelSize::Small)
3376 .color(Color::Muted),
3377 ),
3378 )
3379 }
3380}
3381
3382impl Render for ContextEditorToolbarItem {
3383 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3384 h_flex()
3385 .gap_2()
3386 .child(ModelSelector::new(
3387 self.model_selector_menu_handle.clone(),
3388 self.fs.clone(),
3389 ))
3390 .children(self.render_remaining_tokens(cx))
3391 .child(self.render_inject_context_menu(cx))
3392 }
3393}
3394
3395impl ToolbarItemView for ContextEditorToolbarItem {
3396 fn set_active_pane_item(
3397 &mut self,
3398 active_pane_item: Option<&dyn ItemHandle>,
3399 cx: &mut ViewContext<Self>,
3400 ) -> ToolbarItemLocation {
3401 self.active_context_editor = active_pane_item
3402 .and_then(|item| item.act_as::<ContextEditor>(cx))
3403 .map(|editor| editor.downgrade());
3404 cx.notify();
3405 if self.active_context_editor.is_none() {
3406 ToolbarItemLocation::Hidden
3407 } else {
3408 ToolbarItemLocation::PrimaryRight
3409 }
3410 }
3411
3412 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
3413 cx.notify();
3414 }
3415}
3416
3417impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
3418
3419pub struct ContextHistory {
3420 picker: View<Picker<SavedContextPickerDelegate>>,
3421 _subscriptions: Vec<Subscription>,
3422 assistant_panel: WeakView<AssistantPanel>,
3423}
3424
3425impl ContextHistory {
3426 fn new(
3427 context_store: Model<ContextStore>,
3428 assistant_panel: WeakView<AssistantPanel>,
3429 cx: &mut ViewContext<Self>,
3430 ) -> Self {
3431 let picker = cx.new_view(|cx| {
3432 Picker::uniform_list(SavedContextPickerDelegate::new(context_store.clone()), cx)
3433 .modal(false)
3434 .max_height(None)
3435 });
3436
3437 let _subscriptions = vec![
3438 cx.observe(&context_store, |this, _, cx| {
3439 this.picker.update(cx, |picker, cx| picker.refresh(cx));
3440 }),
3441 cx.subscribe(&picker, Self::handle_picker_event),
3442 ];
3443
3444 Self {
3445 picker,
3446 _subscriptions,
3447 assistant_panel,
3448 }
3449 }
3450
3451 fn handle_picker_event(
3452 &mut self,
3453 _: View<Picker<SavedContextPickerDelegate>>,
3454 event: &SavedContextPickerEvent,
3455 cx: &mut ViewContext<Self>,
3456 ) {
3457 let SavedContextPickerEvent::Confirmed { path } = event;
3458 self.assistant_panel
3459 .update(cx, |assistant_panel, cx| {
3460 assistant_panel
3461 .open_context(path.clone(), cx)
3462 .detach_and_log_err(cx);
3463 })
3464 .ok();
3465 }
3466}
3467
3468impl Render for ContextHistory {
3469 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3470 div().size_full().child(self.picker.clone())
3471 }
3472}
3473
3474impl FocusableView for ContextHistory {
3475 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3476 self.picker.focus_handle(cx)
3477 }
3478}
3479
3480impl EventEmitter<()> for ContextHistory {}
3481
3482impl Item for ContextHistory {
3483 type Event = ();
3484
3485 fn tab_content(
3486 &self,
3487 params: workspace::item::TabContentParams,
3488 _: &WindowContext,
3489 ) -> AnyElement {
3490 let color = if params.selected {
3491 Color::Default
3492 } else {
3493 Color::Muted
3494 };
3495 Label::new("History").color(color).into_any_element()
3496 }
3497}
3498
3499#[derive(Clone, Debug)]
3500struct MessageAnchor {
3501 id: MessageId,
3502 start: language::Anchor,
3503}
3504
3505#[derive(Clone, Debug)]
3506pub struct Message {
3507 offset_range: Range<usize>,
3508 index_range: Range<usize>,
3509 id: MessageId,
3510 anchor: language::Anchor,
3511 role: Role,
3512 status: MessageStatus,
3513}
3514
3515impl Message {
3516 fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
3517 LanguageModelRequestMessage {
3518 role: self.role,
3519 content: buffer.text_for_range(self.offset_range.clone()).collect(),
3520 }
3521 }
3522}
3523
3524type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3525
3526fn render_slash_command_output_toggle(
3527 row: MultiBufferRow,
3528 is_folded: bool,
3529 fold: ToggleFold,
3530 _cx: &mut WindowContext,
3531) -> AnyElement {
3532 Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
3533 .selected(is_folded)
3534 .on_click(move |_e, cx| fold(!is_folded, cx))
3535 .into_any_element()
3536}
3537
3538fn render_pending_slash_command_gutter_decoration(
3539 row: MultiBufferRow,
3540 status: &PendingSlashCommandStatus,
3541 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3542) -> AnyElement {
3543 let mut icon = IconButton::new(
3544 ("slash-command-gutter-decoration", row.0),
3545 ui::IconName::TriangleRight,
3546 )
3547 .on_click(move |_e, cx| confirm_command(cx))
3548 .icon_size(ui::IconSize::Small)
3549 .size(ui::ButtonSize::None);
3550
3551 match status {
3552 PendingSlashCommandStatus::Idle => {
3553 icon = icon.icon_color(Color::Muted);
3554 }
3555 PendingSlashCommandStatus::Running { .. } => {
3556 icon = icon.selected(true);
3557 }
3558 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3559 }
3560
3561 icon.into_any_element()
3562}
3563
3564fn render_docs_slash_command_trailer(
3565 row: MultiBufferRow,
3566 command: PendingSlashCommand,
3567 cx: &mut WindowContext,
3568) -> AnyElement {
3569 let Some(argument) = command.argument else {
3570 return Empty.into_any();
3571 };
3572
3573 let args = DocsSlashCommandArgs::parse(&argument);
3574
3575 let Some(store) = args
3576 .provider()
3577 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3578 else {
3579 return Empty.into_any();
3580 };
3581
3582 let Some(package) = args.package() else {
3583 return Empty.into_any();
3584 };
3585
3586 if !store.is_indexing(&package) {
3587 return Empty.into_any();
3588 }
3589
3590 div()
3591 .id(("crates-being-indexed", row.0))
3592 .child(Icon::new(IconName::ArrowCircle).with_animation(
3593 "arrow-circle",
3594 Animation::new(Duration::from_secs(4)).repeat(),
3595 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3596 ))
3597 .tooltip(move |cx| Tooltip::text(format!("Indexing {package}…"), cx))
3598 .into_any_element()
3599}
3600
3601fn make_lsp_adapter_delegate(
3602 project: &Model<Project>,
3603 cx: &mut AppContext,
3604) -> Result<Arc<dyn LspAdapterDelegate>> {
3605 project.update(cx, |project, cx| {
3606 // TODO: Find the right worktree.
3607 let worktree = project
3608 .worktrees()
3609 .next()
3610 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3611 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3612 })
3613}
3614
3615fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3616 Box::new(move |_| {
3617 div()
3618 .pl_6()
3619 .child(
3620 Label::new(format!("error: {}", message))
3621 .single_line()
3622 .color(Color::Error),
3623 )
3624 .into_any()
3625 })
3626}
3627
3628#[cfg(test)]
3629mod tests {
3630 use super::*;
3631 use crate::{
3632 slash_command::{active_command, file_command},
3633 FakeCompletionProvider, MessageId,
3634 };
3635 use fs::FakeFs;
3636 use gpui::{AppContext, TestAppContext};
3637 use rope::Rope;
3638 use serde_json::json;
3639 use settings::SettingsStore;
3640 use std::{cell::RefCell, path::Path, rc::Rc};
3641 use unindent::Unindent;
3642 use util::test::marked_text_ranges;
3643
3644 #[gpui::test]
3645 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3646 let settings_store = SettingsStore::test(cx);
3647 FakeCompletionProvider::setup_test(cx);
3648 cx.set_global(settings_store);
3649 init(cx);
3650 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3651
3652 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3653 let buffer = context.read(cx).buffer.clone();
3654
3655 let message_1 = context.read(cx).message_anchors[0].clone();
3656 assert_eq!(
3657 messages(&context, cx),
3658 vec![(message_1.id, Role::User, 0..0)]
3659 );
3660
3661 let message_2 = context.update(cx, |context, cx| {
3662 context
3663 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3664 .unwrap()
3665 });
3666 assert_eq!(
3667 messages(&context, cx),
3668 vec![
3669 (message_1.id, Role::User, 0..1),
3670 (message_2.id, Role::Assistant, 1..1)
3671 ]
3672 );
3673
3674 buffer.update(cx, |buffer, cx| {
3675 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3676 });
3677 assert_eq!(
3678 messages(&context, cx),
3679 vec![
3680 (message_1.id, Role::User, 0..2),
3681 (message_2.id, Role::Assistant, 2..3)
3682 ]
3683 );
3684
3685 let message_3 = context.update(cx, |context, cx| {
3686 context
3687 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3688 .unwrap()
3689 });
3690 assert_eq!(
3691 messages(&context, cx),
3692 vec![
3693 (message_1.id, Role::User, 0..2),
3694 (message_2.id, Role::Assistant, 2..4),
3695 (message_3.id, Role::User, 4..4)
3696 ]
3697 );
3698
3699 let message_4 = context.update(cx, |context, cx| {
3700 context
3701 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3702 .unwrap()
3703 });
3704 assert_eq!(
3705 messages(&context, cx),
3706 vec![
3707 (message_1.id, Role::User, 0..2),
3708 (message_2.id, Role::Assistant, 2..4),
3709 (message_4.id, Role::User, 4..5),
3710 (message_3.id, Role::User, 5..5),
3711 ]
3712 );
3713
3714 buffer.update(cx, |buffer, cx| {
3715 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3716 });
3717 assert_eq!(
3718 messages(&context, cx),
3719 vec![
3720 (message_1.id, Role::User, 0..2),
3721 (message_2.id, Role::Assistant, 2..4),
3722 (message_4.id, Role::User, 4..6),
3723 (message_3.id, Role::User, 6..7),
3724 ]
3725 );
3726
3727 // Deleting across message boundaries merges the messages.
3728 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3729 assert_eq!(
3730 messages(&context, cx),
3731 vec![
3732 (message_1.id, Role::User, 0..3),
3733 (message_3.id, Role::User, 3..4),
3734 ]
3735 );
3736
3737 // Undoing the deletion should also undo the merge.
3738 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3739 assert_eq!(
3740 messages(&context, cx),
3741 vec![
3742 (message_1.id, Role::User, 0..2),
3743 (message_2.id, Role::Assistant, 2..4),
3744 (message_4.id, Role::User, 4..6),
3745 (message_3.id, Role::User, 6..7),
3746 ]
3747 );
3748
3749 // Redoing the deletion should also redo the merge.
3750 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3751 assert_eq!(
3752 messages(&context, cx),
3753 vec![
3754 (message_1.id, Role::User, 0..3),
3755 (message_3.id, Role::User, 3..4),
3756 ]
3757 );
3758
3759 // Ensure we can still insert after a merged message.
3760 let message_5 = context.update(cx, |context, cx| {
3761 context
3762 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3763 .unwrap()
3764 });
3765 assert_eq!(
3766 messages(&context, cx),
3767 vec![
3768 (message_1.id, Role::User, 0..3),
3769 (message_5.id, Role::System, 3..4),
3770 (message_3.id, Role::User, 4..5)
3771 ]
3772 );
3773 }
3774
3775 #[gpui::test]
3776 fn test_message_splitting(cx: &mut AppContext) {
3777 let settings_store = SettingsStore::test(cx);
3778 cx.set_global(settings_store);
3779 FakeCompletionProvider::setup_test(cx);
3780 init(cx);
3781 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3782
3783 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3784 let buffer = context.read(cx).buffer.clone();
3785
3786 let message_1 = context.read(cx).message_anchors[0].clone();
3787 assert_eq!(
3788 messages(&context, cx),
3789 vec![(message_1.id, Role::User, 0..0)]
3790 );
3791
3792 buffer.update(cx, |buffer, cx| {
3793 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3794 });
3795
3796 let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3797 let message_2 = message_2.unwrap();
3798
3799 // We recycle newlines in the middle of a split message
3800 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3801 assert_eq!(
3802 messages(&context, cx),
3803 vec![
3804 (message_1.id, Role::User, 0..4),
3805 (message_2.id, Role::User, 4..16),
3806 ]
3807 );
3808
3809 let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
3810 let message_3 = message_3.unwrap();
3811
3812 // We don't recycle newlines at the end of a split message
3813 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3814 assert_eq!(
3815 messages(&context, cx),
3816 vec![
3817 (message_1.id, Role::User, 0..4),
3818 (message_3.id, Role::User, 4..5),
3819 (message_2.id, Role::User, 5..17),
3820 ]
3821 );
3822
3823 let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3824 let message_4 = message_4.unwrap();
3825 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3826 assert_eq!(
3827 messages(&context, cx),
3828 vec![
3829 (message_1.id, Role::User, 0..4),
3830 (message_3.id, Role::User, 4..5),
3831 (message_2.id, Role::User, 5..9),
3832 (message_4.id, Role::User, 9..17),
3833 ]
3834 );
3835
3836 let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
3837 let message_5 = message_5.unwrap();
3838 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3839 assert_eq!(
3840 messages(&context, cx),
3841 vec![
3842 (message_1.id, Role::User, 0..4),
3843 (message_3.id, Role::User, 4..5),
3844 (message_2.id, Role::User, 5..9),
3845 (message_4.id, Role::User, 9..10),
3846 (message_5.id, Role::User, 10..18),
3847 ]
3848 );
3849
3850 let (message_6, message_7) =
3851 context.update(cx, |context, cx| context.split_message(14..16, cx));
3852 let message_6 = message_6.unwrap();
3853 let message_7 = message_7.unwrap();
3854 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3855 assert_eq!(
3856 messages(&context, cx),
3857 vec![
3858 (message_1.id, Role::User, 0..4),
3859 (message_3.id, Role::User, 4..5),
3860 (message_2.id, Role::User, 5..9),
3861 (message_4.id, Role::User, 9..10),
3862 (message_5.id, Role::User, 10..14),
3863 (message_6.id, Role::User, 14..17),
3864 (message_7.id, Role::User, 17..19),
3865 ]
3866 );
3867 }
3868
3869 #[gpui::test]
3870 fn test_messages_for_offsets(cx: &mut AppContext) {
3871 let settings_store = SettingsStore::test(cx);
3872 FakeCompletionProvider::setup_test(cx);
3873 cx.set_global(settings_store);
3874 init(cx);
3875 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
3876 let context = cx.new_model(|cx| Context::new(registry, Default::default(), None, cx));
3877 let buffer = context.read(cx).buffer.clone();
3878
3879 let message_1 = context.read(cx).message_anchors[0].clone();
3880 assert_eq!(
3881 messages(&context, cx),
3882 vec![(message_1.id, Role::User, 0..0)]
3883 );
3884
3885 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3886 let message_2 = context
3887 .update(cx, |context, cx| {
3888 context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3889 })
3890 .unwrap();
3891 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3892
3893 let message_3 = context
3894 .update(cx, |context, cx| {
3895 context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3896 })
3897 .unwrap();
3898 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3899
3900 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3901 assert_eq!(
3902 messages(&context, cx),
3903 vec![
3904 (message_1.id, Role::User, 0..4),
3905 (message_2.id, Role::User, 4..8),
3906 (message_3.id, Role::User, 8..11)
3907 ]
3908 );
3909
3910 assert_eq!(
3911 message_ids_for_offsets(&context, &[0, 4, 9], cx),
3912 [message_1.id, message_2.id, message_3.id]
3913 );
3914 assert_eq!(
3915 message_ids_for_offsets(&context, &[0, 1, 11], cx),
3916 [message_1.id, message_3.id]
3917 );
3918
3919 let message_4 = context
3920 .update(cx, |context, cx| {
3921 context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3922 })
3923 .unwrap();
3924 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3925 assert_eq!(
3926 messages(&context, cx),
3927 vec![
3928 (message_1.id, Role::User, 0..4),
3929 (message_2.id, Role::User, 4..8),
3930 (message_3.id, Role::User, 8..12),
3931 (message_4.id, Role::User, 12..12)
3932 ]
3933 );
3934 assert_eq!(
3935 message_ids_for_offsets(&context, &[0, 4, 8, 12], cx),
3936 [message_1.id, message_2.id, message_3.id, message_4.id]
3937 );
3938
3939 fn message_ids_for_offsets(
3940 context: &Model<Context>,
3941 offsets: &[usize],
3942 cx: &AppContext,
3943 ) -> Vec<MessageId> {
3944 context
3945 .read(cx)
3946 .messages_for_offsets(offsets.iter().copied(), cx)
3947 .into_iter()
3948 .map(|message| message.id)
3949 .collect()
3950 }
3951 }
3952
3953 #[gpui::test]
3954 async fn test_slash_commands(cx: &mut TestAppContext) {
3955 let settings_store = cx.update(SettingsStore::test);
3956 cx.set_global(settings_store);
3957 cx.update(|cx| FakeCompletionProvider::setup_test(cx));
3958
3959 cx.update(Project::init_settings);
3960 cx.update(init);
3961 let fs = FakeFs::new(cx.background_executor.clone());
3962
3963 fs.insert_tree(
3964 "/test",
3965 json!({
3966 "src": {
3967 "lib.rs": "fn one() -> usize { 1 }",
3968 "main.rs": "
3969 use crate::one;
3970 fn main() { one(); }
3971 ".unindent(),
3972 }
3973 }),
3974 )
3975 .await;
3976
3977 let slash_command_registry = SlashCommandRegistry::new();
3978 slash_command_registry.register_command(file_command::FileSlashCommand, false);
3979 slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
3980
3981 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3982 let context =
3983 cx.new_model(|cx| Context::new(registry.clone(), slash_command_registry, None, cx));
3984
3985 let output_ranges = Rc::new(RefCell::new(HashSet::default()));
3986 context.update(cx, |_, cx| {
3987 cx.subscribe(&context, {
3988 let ranges = output_ranges.clone();
3989 move |_, _, event, _| match event {
3990 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
3991 for range in removed {
3992 ranges.borrow_mut().remove(range);
3993 }
3994 for command in updated {
3995 ranges.borrow_mut().insert(command.source_range.clone());
3996 }
3997 }
3998 _ => {}
3999 }
4000 })
4001 .detach();
4002 });
4003
4004 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
4005
4006 // Insert a slash command
4007 buffer.update(cx, |buffer, cx| {
4008 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
4009 });
4010 assert_text_and_output_ranges(
4011 &buffer,
4012 &output_ranges.borrow(),
4013 "
4014 «/file src/lib.rs»
4015 "
4016 .unindent()
4017 .trim_end(),
4018 cx,
4019 );
4020
4021 // Edit the argument of the slash command.
4022 buffer.update(cx, |buffer, cx| {
4023 let edit_offset = buffer.text().find("lib.rs").unwrap();
4024 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
4025 });
4026 assert_text_and_output_ranges(
4027 &buffer,
4028 &output_ranges.borrow(),
4029 "
4030 «/file src/main.rs»
4031 "
4032 .unindent()
4033 .trim_end(),
4034 cx,
4035 );
4036
4037 // Edit the name of the slash command, using one that doesn't exist.
4038 buffer.update(cx, |buffer, cx| {
4039 let edit_offset = buffer.text().find("/file").unwrap();
4040 buffer.edit(
4041 [(edit_offset..edit_offset + "/file".len(), "/unknown")],
4042 None,
4043 cx,
4044 );
4045 });
4046 assert_text_and_output_ranges(
4047 &buffer,
4048 &output_ranges.borrow(),
4049 "
4050 /unknown src/main.rs
4051 "
4052 .unindent()
4053 .trim_end(),
4054 cx,
4055 );
4056
4057 #[track_caller]
4058 fn assert_text_and_output_ranges(
4059 buffer: &Model<Buffer>,
4060 ranges: &HashSet<Range<language::Anchor>>,
4061 expected_marked_text: &str,
4062 cx: &mut TestAppContext,
4063 ) {
4064 let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
4065 let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
4066 let mut ranges = ranges
4067 .iter()
4068 .map(|range| range.to_offset(buffer))
4069 .collect::<Vec<_>>();
4070 ranges.sort_by_key(|a| a.start);
4071 (buffer.text(), ranges)
4072 });
4073
4074 assert_eq!(actual_text, expected_text);
4075 assert_eq!(actual_ranges, expected_ranges);
4076 }
4077 }
4078
4079 #[test]
4080 fn test_parse_next_edit_suggestion() {
4081 let text = "
4082 some output:
4083
4084 ```edit src/foo.rs
4085 let a = 1;
4086 let b = 2;
4087 ---
4088 let w = 1;
4089 let x = 2;
4090 let y = 3;
4091 let z = 4;
4092 ```
4093
4094 some more output:
4095
4096 ```edit src/foo.rs
4097 let c = 1;
4098 ---
4099 ```
4100
4101 and the conclusion.
4102 "
4103 .unindent();
4104
4105 let rope = Rope::from(text.as_str());
4106 let mut lines = rope.chunks().lines();
4107 let mut suggestions = vec![];
4108 while let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
4109 suggestions.push((
4110 suggestion.path.clone(),
4111 text[suggestion.old_text_range].to_string(),
4112 text[suggestion.new_text_range].to_string(),
4113 ));
4114 }
4115
4116 assert_eq!(
4117 suggestions,
4118 vec![
4119 (
4120 Path::new("src/foo.rs").into(),
4121 [
4122 " let a = 1;", //
4123 " let b = 2;",
4124 "",
4125 ]
4126 .join("\n"),
4127 [
4128 " let w = 1;",
4129 " let x = 2;",
4130 " let y = 3;",
4131 " let z = 4;",
4132 "",
4133 ]
4134 .join("\n"),
4135 ),
4136 (
4137 Path::new("src/foo.rs").into(),
4138 [
4139 " let c = 1;", //
4140 "",
4141 ]
4142 .join("\n"),
4143 String::new(),
4144 )
4145 ]
4146 );
4147 }
4148
4149 #[gpui::test]
4150 async fn test_serialization(cx: &mut TestAppContext) {
4151 let settings_store = cx.update(SettingsStore::test);
4152 cx.set_global(settings_store);
4153 cx.update(FakeCompletionProvider::setup_test);
4154 cx.update(init);
4155 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
4156 let context =
4157 cx.new_model(|cx| Context::new(registry.clone(), Default::default(), None, cx));
4158 let buffer = context.read_with(cx, |context, _| context.buffer.clone());
4159 let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
4160 let message_1 = context.update(cx, |context, cx| {
4161 context
4162 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
4163 .unwrap()
4164 });
4165 let message_2 = context.update(cx, |context, cx| {
4166 context
4167 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
4168 .unwrap()
4169 });
4170 buffer.update(cx, |buffer, cx| {
4171 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
4172 buffer.finalize_last_transaction();
4173 });
4174 let _message_3 = context.update(cx, |context, cx| {
4175 context
4176 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
4177 .unwrap()
4178 });
4179 buffer.update(cx, |buffer, cx| buffer.undo(cx));
4180 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
4181 assert_eq!(
4182 cx.read(|cx| messages(&context, cx)),
4183 [
4184 (message_0, Role::User, 0..2),
4185 (message_1.id, Role::Assistant, 2..6),
4186 (message_2.id, Role::System, 6..6),
4187 ]
4188 );
4189
4190 let deserialized_context = Context::deserialize(
4191 context.read_with(cx, |context, cx| context.serialize(cx)),
4192 Default::default(),
4193 registry.clone(),
4194 Default::default(),
4195 None,
4196 &mut cx.to_async(),
4197 )
4198 .await
4199 .unwrap();
4200 let deserialized_buffer =
4201 deserialized_context.read_with(cx, |context, _| context.buffer.clone());
4202 assert_eq!(
4203 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
4204 "a\nb\nc\n"
4205 );
4206 assert_eq!(
4207 cx.read(|cx| messages(&deserialized_context, cx)),
4208 [
4209 (message_0, Role::User, 0..2),
4210 (message_1.id, Role::Assistant, 2..6),
4211 (message_2.id, Role::System, 6..6),
4212 ]
4213 );
4214 }
4215
4216 fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
4217 context
4218 .read(cx)
4219 .messages(cx)
4220 .map(|message| (message.id, message.role, message.offset_range))
4221 .collect()
4222 }
4223}