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 return cx.spawn(|this, mut cx| async move {
620 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
621 });
622 }
623
624 let saved_context = self.context_store.read(cx).load(path.clone(), cx);
625 let fs = self.fs.clone();
626 let workspace = self.workspace.clone();
627 let slash_commands = self.slash_commands.clone();
628 let languages = self.languages.clone();
629 let telemetry = self.telemetry.clone();
630
631 let lsp_adapter_delegate = workspace
632 .update(cx, |workspace, cx| {
633 make_lsp_adapter_delegate(workspace.project(), cx).log_err()
634 })
635 .log_err()
636 .flatten();
637
638 cx.spawn(|this, mut cx| async move {
639 let saved_context = saved_context.await?;
640 let context = Context::deserialize(
641 saved_context,
642 path,
643 languages,
644 slash_commands,
645 Some(telemetry),
646 &mut cx,
647 )
648 .await?;
649
650 this.update(&mut cx, |this, cx| {
651 let workspace = workspace
652 .upgrade()
653 .ok_or_else(|| anyhow!("workspace dropped"))?;
654 let editor = cx.new_view(|cx| {
655 ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
656 });
657 this.show_context(editor, cx);
658 anyhow::Ok(())
659 })??;
660 Ok(())
661 })
662 }
663
664 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
665 CompletionProvider::global(cx).is_authenticated()
666 }
667
668 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
669 cx.update_global::<CompletionProvider, _>(|provider, cx| provider.authenticate(cx))
670 }
671
672 fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
673 let mut registrar = DivRegistrar::new(
674 |panel, cx| {
675 panel
676 .pane
677 .read(cx)
678 .toolbar()
679 .read(cx)
680 .item_of_type::<BufferSearchBar>()
681 },
682 cx,
683 );
684 BufferSearchBar::register(&mut registrar);
685 let registrar = registrar.into_div();
686
687 v_flex()
688 .key_context("AssistantPanel")
689 .size_full()
690 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
691 this.new_context(cx);
692 }))
693 .on_action(cx.listener(AssistantPanel::deploy_history))
694 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
695 .on_action(cx.listener(AssistantPanel::reset_credentials))
696 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
697 .child(registrar.size_full().child(self.pane.clone()))
698 }
699}
700
701impl Render for AssistantPanel {
702 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
703 if let Some(authentication_prompt) = self.authentication_prompt.as_ref() {
704 authentication_prompt.clone().into_any()
705 } else {
706 self.render_signed_in(cx).into_any_element()
707 }
708 }
709}
710
711impl Panel for AssistantPanel {
712 fn persistent_name() -> &'static str {
713 "AssistantPanel"
714 }
715
716 fn position(&self, cx: &WindowContext) -> DockPosition {
717 match AssistantSettings::get_global(cx).dock {
718 AssistantDockPosition::Left => DockPosition::Left,
719 AssistantDockPosition::Bottom => DockPosition::Bottom,
720 AssistantDockPosition::Right => DockPosition::Right,
721 }
722 }
723
724 fn position_is_valid(&self, _: DockPosition) -> bool {
725 true
726 }
727
728 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
729 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
730 let dock = match position {
731 DockPosition::Left => AssistantDockPosition::Left,
732 DockPosition::Bottom => AssistantDockPosition::Bottom,
733 DockPosition::Right => AssistantDockPosition::Right,
734 };
735 settings.set_dock(dock);
736 });
737 }
738
739 fn size(&self, cx: &WindowContext) -> Pixels {
740 let settings = AssistantSettings::get_global(cx);
741 match self.position(cx) {
742 DockPosition::Left | DockPosition::Right => {
743 self.width.unwrap_or(settings.default_width)
744 }
745 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
746 }
747 }
748
749 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
750 match self.position(cx) {
751 DockPosition::Left | DockPosition::Right => self.width = size,
752 DockPosition::Bottom => self.height = size,
753 }
754 cx.notify();
755 }
756
757 fn is_zoomed(&self, cx: &WindowContext) -> bool {
758 self.pane.read(cx).is_zoomed()
759 }
760
761 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
762 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
763 }
764
765 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
766 if active {
767 let load_credentials = self.authenticate(cx);
768 cx.spawn(|this, mut cx| async move {
769 load_credentials.await?;
770 this.update(&mut cx, |this, cx| {
771 if this.is_authenticated(cx) && this.active_context_editor(cx).is_none() {
772 this.new_context(cx);
773 }
774 })
775 })
776 .detach_and_log_err(cx);
777 }
778 }
779
780 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
781 let settings = AssistantSettings::get_global(cx);
782 if !settings.enabled || !settings.button {
783 return None;
784 }
785
786 Some(IconName::ZedAssistant)
787 }
788
789 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
790 Some("Assistant Panel")
791 }
792
793 fn toggle_action(&self) -> Box<dyn Action> {
794 Box::new(ToggleFocus)
795 }
796}
797
798impl EventEmitter<PanelEvent> for AssistantPanel {}
799impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
800
801impl FocusableView for AssistantPanel {
802 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
803 self.pane.focus_handle(cx)
804 }
805}
806
807#[derive(Clone)]
808enum ContextEvent {
809 MessagesEdited,
810 SummaryChanged,
811 EditSuggestionsChanged,
812 StreamedCompletion,
813 PendingSlashCommandsUpdated {
814 removed: Vec<Range<language::Anchor>>,
815 updated: Vec<PendingSlashCommand>,
816 },
817 SlashCommandFinished {
818 output_range: Range<language::Anchor>,
819 sections: Vec<SlashCommandOutputSection<language::Anchor>>,
820 run_commands_in_output: bool,
821 },
822}
823
824#[derive(Default)]
825struct Summary {
826 text: String,
827 done: bool,
828}
829
830pub struct Context {
831 id: Option<String>,
832 buffer: Model<Buffer>,
833 edit_suggestions: Vec<EditSuggestion>,
834 pending_slash_commands: Vec<PendingSlashCommand>,
835 edits_since_last_slash_command_parse: language::Subscription,
836 slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
837 message_anchors: Vec<MessageAnchor>,
838 messages_metadata: HashMap<MessageId, MessageMetadata>,
839 next_message_id: MessageId,
840 summary: Option<Summary>,
841 pending_summary: Task<Option<()>>,
842 completion_count: usize,
843 pending_completions: Vec<PendingCompletion>,
844 token_count: Option<usize>,
845 pending_token_count: Task<Option<()>>,
846 pending_edit_suggestion_parse: Option<Task<()>>,
847 pending_save: Task<Result<()>>,
848 path: Option<PathBuf>,
849 _subscriptions: Vec<Subscription>,
850 telemetry: Option<Arc<Telemetry>>,
851 slash_command_registry: Arc<SlashCommandRegistry>,
852 language_registry: Arc<LanguageRegistry>,
853}
854
855impl EventEmitter<ContextEvent> for Context {}
856
857impl Context {
858 fn new(
859 language_registry: Arc<LanguageRegistry>,
860 slash_command_registry: Arc<SlashCommandRegistry>,
861 telemetry: Option<Arc<Telemetry>>,
862 cx: &mut ModelContext<Self>,
863 ) -> Self {
864 let buffer = cx.new_model(|cx| {
865 let mut buffer = Buffer::local("", cx);
866 buffer.set_language_registry(language_registry.clone());
867 buffer
868 });
869 let edits_since_last_slash_command_parse =
870 buffer.update(cx, |buffer, _| buffer.subscribe());
871 let mut this = Self {
872 id: Some(Uuid::new_v4().to_string()),
873 message_anchors: Default::default(),
874 messages_metadata: Default::default(),
875 next_message_id: Default::default(),
876 edit_suggestions: Vec::new(),
877 pending_slash_commands: Vec::new(),
878 slash_command_output_sections: Vec::new(),
879 edits_since_last_slash_command_parse,
880 summary: None,
881 pending_summary: Task::ready(None),
882 completion_count: Default::default(),
883 pending_completions: Default::default(),
884 token_count: None,
885 pending_token_count: Task::ready(None),
886 pending_edit_suggestion_parse: None,
887 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
888 pending_save: Task::ready(Ok(())),
889 path: None,
890 buffer,
891 telemetry,
892 language_registry,
893 slash_command_registry,
894 };
895
896 let message = MessageAnchor {
897 id: MessageId(post_inc(&mut this.next_message_id.0)),
898 start: language::Anchor::MIN,
899 };
900 this.message_anchors.push(message.clone());
901 this.messages_metadata.insert(
902 message.id,
903 MessageMetadata {
904 role: Role::User,
905 status: MessageStatus::Done,
906 },
907 );
908
909 this.set_language(cx);
910 this.count_remaining_tokens(cx);
911 this
912 }
913
914 fn serialize(&self, cx: &AppContext) -> SavedContext {
915 let buffer = self.buffer.read(cx);
916 SavedContext {
917 id: self.id.clone(),
918 zed: "context".into(),
919 version: SavedContext::VERSION.into(),
920 text: buffer.text(),
921 message_metadata: self.messages_metadata.clone(),
922 messages: self
923 .messages(cx)
924 .map(|message| SavedMessage {
925 id: message.id,
926 start: message.offset_range.start,
927 })
928 .collect(),
929 summary: self
930 .summary
931 .as_ref()
932 .map(|summary| summary.text.clone())
933 .unwrap_or_default(),
934 slash_command_output_sections: self
935 .slash_command_output_sections
936 .iter()
937 .filter_map(|section| {
938 let range = section.range.to_offset(buffer);
939 if section.range.start.is_valid(buffer) && !range.is_empty() {
940 Some(SlashCommandOutputSection {
941 range,
942 icon: section.icon,
943 label: section.label.clone(),
944 })
945 } else {
946 None
947 }
948 })
949 .collect(),
950 }
951 }
952
953 #[allow(clippy::too_many_arguments)]
954 async fn deserialize(
955 saved_context: SavedContext,
956 path: PathBuf,
957 language_registry: Arc<LanguageRegistry>,
958 slash_command_registry: Arc<SlashCommandRegistry>,
959 telemetry: Option<Arc<Telemetry>>,
960 cx: &mut AsyncAppContext,
961 ) -> Result<Model<Self>> {
962 let id = match saved_context.id {
963 Some(id) => Some(id),
964 None => Some(Uuid::new_v4().to_string()),
965 };
966
967 let markdown = language_registry.language_for_name("Markdown");
968 let mut message_anchors = Vec::new();
969 let mut next_message_id = MessageId(0);
970 let buffer = cx.new_model(|cx| {
971 let mut buffer = Buffer::local(saved_context.text, cx);
972 for message in saved_context.messages {
973 message_anchors.push(MessageAnchor {
974 id: message.id,
975 start: buffer.anchor_before(message.start),
976 });
977 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
978 }
979 buffer.set_language_registry(language_registry.clone());
980 cx.spawn(|buffer, mut cx| async move {
981 let markdown = markdown.await?;
982 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
983 buffer.set_language(Some(markdown), cx)
984 })?;
985 anyhow::Ok(())
986 })
987 .detach_and_log_err(cx);
988 buffer
989 })?;
990
991 cx.new_model(move |cx| {
992 let edits_since_last_slash_command_parse =
993 buffer.update(cx, |buffer, _| buffer.subscribe());
994 let mut this = Self {
995 id,
996 message_anchors,
997 messages_metadata: saved_context.message_metadata,
998 next_message_id,
999 edit_suggestions: Vec::new(),
1000 pending_slash_commands: Vec::new(),
1001 slash_command_output_sections: saved_context
1002 .slash_command_output_sections
1003 .into_iter()
1004 .map(|section| {
1005 let buffer = buffer.read(cx);
1006 SlashCommandOutputSection {
1007 range: buffer.anchor_after(section.range.start)
1008 ..buffer.anchor_before(section.range.end),
1009 icon: section.icon,
1010 label: section.label,
1011 }
1012 })
1013 .collect(),
1014 edits_since_last_slash_command_parse,
1015 summary: Some(Summary {
1016 text: saved_context.summary,
1017 done: true,
1018 }),
1019 pending_summary: Task::ready(None),
1020 completion_count: Default::default(),
1021 pending_completions: Default::default(),
1022 token_count: None,
1023 pending_edit_suggestion_parse: None,
1024 pending_token_count: Task::ready(None),
1025 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1026 pending_save: Task::ready(Ok(())),
1027 path: Some(path),
1028 buffer,
1029 telemetry,
1030 language_registry,
1031 slash_command_registry,
1032 };
1033 this.set_language(cx);
1034 this.reparse_edit_suggestions(cx);
1035 this.count_remaining_tokens(cx);
1036 this
1037 })
1038 }
1039
1040 fn set_language(&mut self, cx: &mut ModelContext<Self>) {
1041 let markdown = self.language_registry.language_for_name("Markdown");
1042 cx.spawn(|this, mut cx| async move {
1043 let markdown = markdown.await?;
1044 this.update(&mut cx, |this, cx| {
1045 this.buffer
1046 .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx));
1047 })
1048 })
1049 .detach_and_log_err(cx);
1050 }
1051
1052 fn handle_buffer_event(
1053 &mut self,
1054 _: Model<Buffer>,
1055 event: &language::Event,
1056 cx: &mut ModelContext<Self>,
1057 ) {
1058 if *event == language::Event::Edited {
1059 self.count_remaining_tokens(cx);
1060 self.reparse_edit_suggestions(cx);
1061 self.reparse_slash_commands(cx);
1062 cx.emit(ContextEvent::MessagesEdited);
1063 }
1064 }
1065
1066 pub(crate) fn token_count(&self) -> Option<usize> {
1067 self.token_count
1068 }
1069
1070 pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1071 let request = self.to_completion_request(cx);
1072 self.pending_token_count = cx.spawn(|this, mut cx| {
1073 async move {
1074 cx.background_executor()
1075 .timer(Duration::from_millis(200))
1076 .await;
1077
1078 let token_count = cx
1079 .update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
1080 .await?;
1081
1082 this.update(&mut cx, |this, cx| {
1083 this.token_count = Some(token_count);
1084 cx.notify()
1085 })?;
1086 anyhow::Ok(())
1087 }
1088 .log_err()
1089 });
1090 }
1091
1092 fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
1093 let buffer = self.buffer.read(cx);
1094 let mut row_ranges = self
1095 .edits_since_last_slash_command_parse
1096 .consume()
1097 .into_iter()
1098 .map(|edit| {
1099 let start_row = buffer.offset_to_point(edit.new.start).row;
1100 let end_row = buffer.offset_to_point(edit.new.end).row + 1;
1101 start_row..end_row
1102 })
1103 .peekable();
1104
1105 let mut removed = Vec::new();
1106 let mut updated = Vec::new();
1107 while let Some(mut row_range) = row_ranges.next() {
1108 while let Some(next_row_range) = row_ranges.peek() {
1109 if row_range.end >= next_row_range.start {
1110 row_range.end = next_row_range.end;
1111 row_ranges.next();
1112 } else {
1113 break;
1114 }
1115 }
1116
1117 let start = buffer.anchor_before(Point::new(row_range.start, 0));
1118 let end = buffer.anchor_after(Point::new(
1119 row_range.end - 1,
1120 buffer.line_len(row_range.end - 1),
1121 ));
1122
1123 let old_range = self.pending_command_indices_for_range(start..end, cx);
1124
1125 let mut new_commands = Vec::new();
1126 let mut lines = buffer.text_for_range(start..end).lines();
1127 let mut offset = lines.offset();
1128 while let Some(line) = lines.next() {
1129 if let Some(command_line) = SlashCommandLine::parse(line) {
1130 let name = &line[command_line.name.clone()];
1131 let argument = command_line.argument.as_ref().and_then(|argument| {
1132 (!argument.is_empty()).then_some(&line[argument.clone()])
1133 });
1134 if let Some(command) = self.slash_command_registry.command(name) {
1135 if !command.requires_argument() || argument.is_some() {
1136 let start_ix = offset + command_line.name.start - 1;
1137 let end_ix = offset
1138 + command_line
1139 .argument
1140 .map_or(command_line.name.end, |argument| argument.end);
1141 let source_range =
1142 buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
1143 let pending_command = PendingSlashCommand {
1144 name: name.to_string(),
1145 argument: argument.map(ToString::to_string),
1146 source_range,
1147 status: PendingSlashCommandStatus::Idle,
1148 };
1149 updated.push(pending_command.clone());
1150 new_commands.push(pending_command);
1151 }
1152 }
1153 }
1154
1155 offset = lines.offset();
1156 }
1157
1158 let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
1159 removed.extend(removed_commands.map(|command| command.source_range));
1160 }
1161
1162 if !updated.is_empty() || !removed.is_empty() {
1163 cx.emit(ContextEvent::PendingSlashCommandsUpdated { removed, updated });
1164 }
1165 }
1166
1167 fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
1168 self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
1169 cx.background_executor()
1170 .timer(Duration::from_millis(200))
1171 .await;
1172
1173 this.update(&mut cx, |this, cx| {
1174 this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx);
1175 })
1176 .ok();
1177 }));
1178 }
1179
1180 fn reparse_edit_suggestions_in_range(
1181 &mut self,
1182 range: Range<usize>,
1183 cx: &mut ModelContext<Self>,
1184 ) {
1185 self.buffer.update(cx, |buffer, _| {
1186 let range_start = buffer.anchor_before(range.start);
1187 let range_end = buffer.anchor_after(range.end);
1188 let start_ix = self
1189 .edit_suggestions
1190 .binary_search_by(|probe| {
1191 probe
1192 .source_range
1193 .end
1194 .cmp(&range_start, buffer)
1195 .then(Ordering::Greater)
1196 })
1197 .unwrap_err();
1198 let end_ix = self
1199 .edit_suggestions
1200 .binary_search_by(|probe| {
1201 probe
1202 .source_range
1203 .start
1204 .cmp(&range_end, buffer)
1205 .then(Ordering::Less)
1206 })
1207 .unwrap_err();
1208
1209 let mut new_edit_suggestions = Vec::new();
1210 let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
1211 while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) {
1212 let start_anchor = buffer.anchor_after(suggestion.outer_range.start);
1213 let end_anchor = buffer.anchor_before(suggestion.outer_range.end);
1214 new_edit_suggestions.push(EditSuggestion {
1215 source_range: start_anchor..end_anchor,
1216 full_path: suggestion.path,
1217 });
1218 }
1219 self.edit_suggestions
1220 .splice(start_ix..end_ix, new_edit_suggestions);
1221 });
1222 cx.emit(ContextEvent::EditSuggestionsChanged);
1223 cx.notify();
1224 }
1225
1226 fn pending_command_for_position(
1227 &mut self,
1228 position: language::Anchor,
1229 cx: &mut ModelContext<Self>,
1230 ) -> Option<&mut PendingSlashCommand> {
1231 let buffer = self.buffer.read(cx);
1232 match self
1233 .pending_slash_commands
1234 .binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
1235 {
1236 Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
1237 Err(ix) => {
1238 let cmd = self.pending_slash_commands.get_mut(ix)?;
1239 if position.cmp(&cmd.source_range.start, buffer).is_ge()
1240 && position.cmp(&cmd.source_range.end, buffer).is_le()
1241 {
1242 Some(cmd)
1243 } else {
1244 None
1245 }
1246 }
1247 }
1248 }
1249
1250 fn pending_commands_for_range(
1251 &self,
1252 range: Range<language::Anchor>,
1253 cx: &AppContext,
1254 ) -> &[PendingSlashCommand] {
1255 let range = self.pending_command_indices_for_range(range, cx);
1256 &self.pending_slash_commands[range]
1257 }
1258
1259 fn pending_command_indices_for_range(
1260 &self,
1261 range: Range<language::Anchor>,
1262 cx: &AppContext,
1263 ) -> Range<usize> {
1264 let buffer = self.buffer.read(cx);
1265 let start_ix = match self
1266 .pending_slash_commands
1267 .binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
1268 {
1269 Ok(ix) | Err(ix) => ix,
1270 };
1271 let end_ix = match self
1272 .pending_slash_commands
1273 .binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
1274 {
1275 Ok(ix) => ix + 1,
1276 Err(ix) => ix,
1277 };
1278 start_ix..end_ix
1279 }
1280
1281 fn insert_command_output(
1282 &mut self,
1283 command_range: Range<language::Anchor>,
1284 output: Task<Result<SlashCommandOutput>>,
1285 insert_trailing_newline: bool,
1286 cx: &mut ModelContext<Self>,
1287 ) {
1288 self.reparse_slash_commands(cx);
1289
1290 let insert_output_task = cx.spawn(|this, mut cx| {
1291 let command_range = command_range.clone();
1292 async move {
1293 let output = output.await;
1294 this.update(&mut cx, |this, cx| match output {
1295 Ok(mut output) => {
1296 if insert_trailing_newline {
1297 output.text.push('\n');
1298 }
1299
1300 let event = this.buffer.update(cx, |buffer, cx| {
1301 let start = command_range.start.to_offset(buffer);
1302 let old_end = command_range.end.to_offset(buffer);
1303 let new_end = start + output.text.len();
1304 buffer.edit([(start..old_end, output.text)], None, cx);
1305
1306 let mut sections = output
1307 .sections
1308 .into_iter()
1309 .map(|section| SlashCommandOutputSection {
1310 range: buffer.anchor_after(start + section.range.start)
1311 ..buffer.anchor_before(start + section.range.end),
1312 icon: section.icon,
1313 label: section.label,
1314 })
1315 .collect::<Vec<_>>();
1316 sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
1317
1318 this.slash_command_output_sections
1319 .extend(sections.iter().cloned());
1320 this.slash_command_output_sections
1321 .sort_by(|a, b| a.range.cmp(&b.range, buffer));
1322
1323 ContextEvent::SlashCommandFinished {
1324 output_range: buffer.anchor_after(start)
1325 ..buffer.anchor_before(new_end),
1326 sections,
1327 run_commands_in_output: output.run_commands_in_text,
1328 }
1329 });
1330 cx.emit(event);
1331 }
1332 Err(error) => {
1333 if let Some(pending_command) =
1334 this.pending_command_for_position(command_range.start, cx)
1335 {
1336 pending_command.status =
1337 PendingSlashCommandStatus::Error(error.to_string());
1338 cx.emit(ContextEvent::PendingSlashCommandsUpdated {
1339 removed: vec![pending_command.source_range.clone()],
1340 updated: vec![pending_command.clone()],
1341 });
1342 }
1343 }
1344 })
1345 .ok();
1346 }
1347 });
1348
1349 if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) {
1350 pending_command.status = PendingSlashCommandStatus::Running {
1351 _task: insert_output_task.shared(),
1352 };
1353 cx.emit(ContextEvent::PendingSlashCommandsUpdated {
1354 removed: vec![pending_command.source_range.clone()],
1355 updated: vec![pending_command.clone()],
1356 });
1357 }
1358 }
1359
1360 fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
1361 self.count_remaining_tokens(cx);
1362 }
1363
1364 fn assist(
1365 &mut self,
1366 selected_messages: HashSet<MessageId>,
1367 cx: &mut ModelContext<Self>,
1368 ) -> Vec<MessageAnchor> {
1369 let mut user_messages = Vec::new();
1370
1371 let last_message_id = if let Some(last_message_id) =
1372 self.message_anchors.iter().rev().find_map(|message| {
1373 message
1374 .start
1375 .is_valid(self.buffer.read(cx))
1376 .then_some(message.id)
1377 }) {
1378 last_message_id
1379 } else {
1380 return Default::default();
1381 };
1382
1383 let mut should_assist = false;
1384 for selected_message_id in selected_messages {
1385 let selected_message_role =
1386 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1387 metadata.role
1388 } else {
1389 continue;
1390 };
1391
1392 if selected_message_role == Role::Assistant {
1393 if let Some(user_message) = self.insert_message_after(
1394 selected_message_id,
1395 Role::User,
1396 MessageStatus::Done,
1397 cx,
1398 ) {
1399 user_messages.push(user_message);
1400 }
1401 } else {
1402 should_assist = true;
1403 }
1404 }
1405
1406 if should_assist {
1407 if !CompletionProvider::global(cx).is_authenticated() {
1408 log::info!("completion provider has no credentials");
1409 return Default::default();
1410 }
1411
1412 let request = self.to_completion_request(cx);
1413 let response = CompletionProvider::global(cx).complete(request, cx);
1414 let assistant_message = self
1415 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1416 .unwrap();
1417
1418 // Queue up the user's next reply.
1419 let user_message = self
1420 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1421 .unwrap();
1422 user_messages.push(user_message);
1423
1424 let task = cx.spawn({
1425 |this, mut cx| async move {
1426 let response = response.await;
1427 let assistant_message_id = assistant_message.id;
1428 let mut response_latency = None;
1429 let stream_completion = async {
1430 let request_start = Instant::now();
1431 let mut messages = response.inner.await?;
1432
1433 while let Some(message) = messages.next().await {
1434 if response_latency.is_none() {
1435 response_latency = Some(request_start.elapsed());
1436 }
1437 let text = message?;
1438
1439 this.update(&mut cx, |this, cx| {
1440 let message_ix = this
1441 .message_anchors
1442 .iter()
1443 .position(|message| message.id == assistant_message_id)?;
1444 let message_range = this.buffer.update(cx, |buffer, cx| {
1445 let message_start_offset =
1446 this.message_anchors[message_ix].start.to_offset(buffer);
1447 let message_old_end_offset = this.message_anchors
1448 [message_ix + 1..]
1449 .iter()
1450 .find(|message| message.start.is_valid(buffer))
1451 .map_or(buffer.len(), |message| {
1452 message.start.to_offset(buffer).saturating_sub(1)
1453 });
1454 let message_new_end_offset =
1455 message_old_end_offset + text.len();
1456 buffer.edit(
1457 [(message_old_end_offset..message_old_end_offset, text)],
1458 None,
1459 cx,
1460 );
1461 message_start_offset..message_new_end_offset
1462 });
1463 this.reparse_edit_suggestions_in_range(message_range, cx);
1464 cx.emit(ContextEvent::StreamedCompletion);
1465
1466 Some(())
1467 })?;
1468 smol::future::yield_now().await;
1469 }
1470
1471 this.update(&mut cx, |this, cx| {
1472 this.pending_completions
1473 .retain(|completion| completion.id != this.completion_count);
1474 this.summarize(cx);
1475 })?;
1476
1477 anyhow::Ok(())
1478 };
1479
1480 let result = stream_completion.await;
1481
1482 this.update(&mut cx, |this, cx| {
1483 if let Some(metadata) =
1484 this.messages_metadata.get_mut(&assistant_message.id)
1485 {
1486 let error_message = result
1487 .err()
1488 .map(|error| error.to_string().trim().to_string());
1489 if let Some(error_message) = error_message.as_ref() {
1490 metadata.status =
1491 MessageStatus::Error(SharedString::from(error_message.clone()));
1492 } else {
1493 metadata.status = MessageStatus::Done;
1494 }
1495
1496 if let Some(telemetry) = this.telemetry.as_ref() {
1497 let model = CompletionProvider::global(cx).model();
1498 telemetry.report_assistant_event(
1499 this.id.clone(),
1500 AssistantKind::Panel,
1501 model.telemetry_id(),
1502 response_latency,
1503 error_message,
1504 );
1505 }
1506
1507 cx.emit(ContextEvent::MessagesEdited);
1508 }
1509 })
1510 .ok();
1511 }
1512 });
1513
1514 self.pending_completions.push(PendingCompletion {
1515 id: post_inc(&mut self.completion_count),
1516 _task: task,
1517 });
1518 }
1519
1520 user_messages
1521 }
1522
1523 pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
1524 let messages = self
1525 .messages(cx)
1526 .filter(|message| matches!(message.status, MessageStatus::Done))
1527 .map(|message| message.to_request_message(self.buffer.read(cx)));
1528
1529 LanguageModelRequest {
1530 model: CompletionProvider::global(cx).model(),
1531 messages: messages.collect(),
1532 stop: vec![],
1533 temperature: 1.0,
1534 }
1535 }
1536
1537 fn cancel_last_assist(&mut self) -> bool {
1538 self.pending_completions.pop().is_some()
1539 }
1540
1541 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1542 for id in ids {
1543 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1544 metadata.role.cycle();
1545 cx.emit(ContextEvent::MessagesEdited);
1546 cx.notify();
1547 }
1548 }
1549 }
1550
1551 fn insert_message_after(
1552 &mut self,
1553 message_id: MessageId,
1554 role: Role,
1555 status: MessageStatus,
1556 cx: &mut ModelContext<Self>,
1557 ) -> Option<MessageAnchor> {
1558 if let Some(prev_message_ix) = self
1559 .message_anchors
1560 .iter()
1561 .position(|message| message.id == message_id)
1562 {
1563 // Find the next valid message after the one we were given.
1564 let mut next_message_ix = prev_message_ix + 1;
1565 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1566 if next_message.start.is_valid(self.buffer.read(cx)) {
1567 break;
1568 }
1569 next_message_ix += 1;
1570 }
1571
1572 let start = self.buffer.update(cx, |buffer, cx| {
1573 let offset = self
1574 .message_anchors
1575 .get(next_message_ix)
1576 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1577 buffer.edit([(offset..offset, "\n")], None, cx);
1578 buffer.anchor_before(offset + 1)
1579 });
1580 let message = MessageAnchor {
1581 id: MessageId(post_inc(&mut self.next_message_id.0)),
1582 start,
1583 };
1584 self.message_anchors
1585 .insert(next_message_ix, message.clone());
1586 self.messages_metadata
1587 .insert(message.id, MessageMetadata { role, status });
1588 cx.emit(ContextEvent::MessagesEdited);
1589 Some(message)
1590 } else {
1591 None
1592 }
1593 }
1594
1595 fn split_message(
1596 &mut self,
1597 range: Range<usize>,
1598 cx: &mut ModelContext<Self>,
1599 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1600 let start_message = self.message_for_offset(range.start, cx);
1601 let end_message = self.message_for_offset(range.end, cx);
1602 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1603 // Prevent splitting when range spans multiple messages.
1604 if start_message.id != end_message.id {
1605 return (None, None);
1606 }
1607
1608 let message = start_message;
1609 let role = message.role;
1610 let mut edited_buffer = false;
1611
1612 let mut suffix_start = None;
1613 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1614 {
1615 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1616 suffix_start = Some(range.end + 1);
1617 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1618 suffix_start = Some(range.end);
1619 }
1620 }
1621
1622 let suffix = if let Some(suffix_start) = suffix_start {
1623 MessageAnchor {
1624 id: MessageId(post_inc(&mut self.next_message_id.0)),
1625 start: self.buffer.read(cx).anchor_before(suffix_start),
1626 }
1627 } else {
1628 self.buffer.update(cx, |buffer, cx| {
1629 buffer.edit([(range.end..range.end, "\n")], None, cx);
1630 });
1631 edited_buffer = true;
1632 MessageAnchor {
1633 id: MessageId(post_inc(&mut self.next_message_id.0)),
1634 start: self.buffer.read(cx).anchor_before(range.end + 1),
1635 }
1636 };
1637
1638 self.message_anchors
1639 .insert(message.index_range.end + 1, suffix.clone());
1640 self.messages_metadata.insert(
1641 suffix.id,
1642 MessageMetadata {
1643 role,
1644 status: MessageStatus::Done,
1645 },
1646 );
1647
1648 let new_messages =
1649 if range.start == range.end || range.start == message.offset_range.start {
1650 (None, Some(suffix))
1651 } else {
1652 let mut prefix_end = None;
1653 if range.start > message.offset_range.start
1654 && range.end < message.offset_range.end - 1
1655 {
1656 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1657 prefix_end = Some(range.start + 1);
1658 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1659 == Some('\n')
1660 {
1661 prefix_end = Some(range.start);
1662 }
1663 }
1664
1665 let selection = if let Some(prefix_end) = prefix_end {
1666 cx.emit(ContextEvent::MessagesEdited);
1667 MessageAnchor {
1668 id: MessageId(post_inc(&mut self.next_message_id.0)),
1669 start: self.buffer.read(cx).anchor_before(prefix_end),
1670 }
1671 } else {
1672 self.buffer.update(cx, |buffer, cx| {
1673 buffer.edit([(range.start..range.start, "\n")], None, cx)
1674 });
1675 edited_buffer = true;
1676 MessageAnchor {
1677 id: MessageId(post_inc(&mut self.next_message_id.0)),
1678 start: self.buffer.read(cx).anchor_before(range.end + 1),
1679 }
1680 };
1681
1682 self.message_anchors
1683 .insert(message.index_range.end + 1, selection.clone());
1684 self.messages_metadata.insert(
1685 selection.id,
1686 MessageMetadata {
1687 role,
1688 status: MessageStatus::Done,
1689 },
1690 );
1691 (Some(selection), Some(suffix))
1692 };
1693
1694 if !edited_buffer {
1695 cx.emit(ContextEvent::MessagesEdited);
1696 }
1697 new_messages
1698 } else {
1699 (None, None)
1700 }
1701 }
1702
1703 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1704 if self.message_anchors.len() >= 2 && self.summary.is_none() {
1705 if !CompletionProvider::global(cx).is_authenticated() {
1706 return;
1707 }
1708
1709 let messages = self
1710 .messages(cx)
1711 .map(|message| message.to_request_message(self.buffer.read(cx)))
1712 .chain(Some(LanguageModelRequestMessage {
1713 role: Role::User,
1714 content: "Summarize the context into a short title without punctuation.".into(),
1715 }));
1716 let request = LanguageModelRequest {
1717 model: CompletionProvider::global(cx).model(),
1718 messages: messages.collect(),
1719 stop: vec![],
1720 temperature: 1.0,
1721 };
1722
1723 let response = CompletionProvider::global(cx).complete(request, cx);
1724 self.pending_summary = cx.spawn(|this, mut cx| {
1725 async move {
1726 let response = response.await;
1727 let mut messages = response.inner.await?;
1728
1729 while let Some(message) = messages.next().await {
1730 let text = message?;
1731 let mut lines = text.lines();
1732 this.update(&mut cx, |this, cx| {
1733 let summary = this.summary.get_or_insert(Default::default());
1734 summary.text.extend(lines.next());
1735 cx.emit(ContextEvent::SummaryChanged);
1736 })?;
1737
1738 // Stop if the LLM generated multiple lines.
1739 if lines.next().is_some() {
1740 break;
1741 }
1742 }
1743
1744 this.update(&mut cx, |this, cx| {
1745 if let Some(summary) = this.summary.as_mut() {
1746 summary.done = true;
1747 cx.emit(ContextEvent::SummaryChanged);
1748 }
1749 })?;
1750
1751 anyhow::Ok(())
1752 }
1753 .log_err()
1754 });
1755 }
1756 }
1757
1758 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
1759 self.messages_for_offsets([offset], cx).pop()
1760 }
1761
1762 fn messages_for_offsets(
1763 &self,
1764 offsets: impl IntoIterator<Item = usize>,
1765 cx: &AppContext,
1766 ) -> Vec<Message> {
1767 let mut result = Vec::new();
1768
1769 let mut messages = self.messages(cx).peekable();
1770 let mut offsets = offsets.into_iter().peekable();
1771 let mut current_message = messages.next();
1772 while let Some(offset) = offsets.next() {
1773 // Locate the message that contains the offset.
1774 while current_message.as_ref().map_or(false, |message| {
1775 !message.offset_range.contains(&offset) && messages.peek().is_some()
1776 }) {
1777 current_message = messages.next();
1778 }
1779 let Some(message) = current_message.as_ref() else {
1780 break;
1781 };
1782
1783 // Skip offsets that are in the same message.
1784 while offsets.peek().map_or(false, |offset| {
1785 message.offset_range.contains(offset) || messages.peek().is_none()
1786 }) {
1787 offsets.next();
1788 }
1789
1790 result.push(message.clone());
1791 }
1792 result
1793 }
1794
1795 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1796 let buffer = self.buffer.read(cx);
1797 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1798 iter::from_fn(move || {
1799 if let Some((start_ix, message_anchor)) = message_anchors.next() {
1800 let metadata = self.messages_metadata.get(&message_anchor.id)?;
1801 let message_start = message_anchor.start.to_offset(buffer);
1802 let mut message_end = None;
1803 let mut end_ix = start_ix;
1804 while let Some((_, next_message)) = message_anchors.peek() {
1805 if next_message.start.is_valid(buffer) {
1806 message_end = Some(next_message.start);
1807 break;
1808 } else {
1809 end_ix += 1;
1810 message_anchors.next();
1811 }
1812 }
1813 let message_end = message_end
1814 .unwrap_or(language::Anchor::MAX)
1815 .to_offset(buffer);
1816
1817 return Some(Message {
1818 index_range: start_ix..end_ix,
1819 offset_range: message_start..message_end,
1820 id: message_anchor.id,
1821 anchor: message_anchor.start,
1822 role: metadata.role,
1823 status: metadata.status.clone(),
1824 });
1825 }
1826 None
1827 })
1828 }
1829
1830 fn save(
1831 &mut self,
1832 debounce: Option<Duration>,
1833 fs: Arc<dyn Fs>,
1834 cx: &mut ModelContext<Context>,
1835 ) {
1836 self.pending_save = cx.spawn(|this, mut cx| async move {
1837 if let Some(debounce) = debounce {
1838 cx.background_executor().timer(debounce).await;
1839 }
1840
1841 let (old_path, summary) = this.read_with(&cx, |this, _| {
1842 let path = this.path.clone();
1843 let summary = if let Some(summary) = this.summary.as_ref() {
1844 if summary.done {
1845 Some(summary.text.clone())
1846 } else {
1847 None
1848 }
1849 } else {
1850 None
1851 };
1852 (path, summary)
1853 })?;
1854
1855 if let Some(summary) = summary {
1856 let context = this.read_with(&cx, |this, cx| this.serialize(cx))?;
1857 let path = if let Some(old_path) = old_path {
1858 old_path
1859 } else {
1860 let mut discriminant = 1;
1861 let mut new_path;
1862 loop {
1863 new_path = contexts_dir().join(&format!(
1864 "{} - {}.zed.json",
1865 summary.trim(),
1866 discriminant
1867 ));
1868 if fs.is_file(&new_path).await {
1869 discriminant += 1;
1870 } else {
1871 break;
1872 }
1873 }
1874 new_path
1875 };
1876
1877 fs.create_dir(contexts_dir().as_ref()).await?;
1878 fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap())
1879 .await?;
1880 this.update(&mut cx, |this, _| this.path = Some(path))?;
1881 }
1882
1883 Ok(())
1884 });
1885 }
1886}
1887
1888#[derive(Debug)]
1889enum EditParsingState {
1890 None,
1891 InOldText {
1892 path: PathBuf,
1893 start_offset: usize,
1894 old_text_start_offset: usize,
1895 },
1896 InNewText {
1897 path: PathBuf,
1898 start_offset: usize,
1899 old_text_range: Range<usize>,
1900 new_text_start_offset: usize,
1901 },
1902}
1903
1904#[derive(Clone, Debug, PartialEq)]
1905struct EditSuggestion {
1906 source_range: Range<language::Anchor>,
1907 full_path: PathBuf,
1908}
1909
1910struct ParsedEditSuggestion {
1911 path: PathBuf,
1912 outer_range: Range<usize>,
1913 old_text_range: Range<usize>,
1914 new_text_range: Range<usize>,
1915}
1916
1917fn parse_next_edit_suggestion(lines: &mut rope::Lines) -> Option<ParsedEditSuggestion> {
1918 let mut state = EditParsingState::None;
1919 loop {
1920 let offset = lines.offset();
1921 let message_line = lines.next()?;
1922 match state {
1923 EditParsingState::None => {
1924 if let Some(rest) = message_line.strip_prefix("```edit ") {
1925 let path = rest.trim();
1926 if !path.is_empty() {
1927 state = EditParsingState::InOldText {
1928 path: PathBuf::from(path),
1929 start_offset: offset,
1930 old_text_start_offset: lines.offset(),
1931 };
1932 }
1933 }
1934 }
1935 EditParsingState::InOldText {
1936 path,
1937 start_offset,
1938 old_text_start_offset,
1939 } => {
1940 if message_line == "---" {
1941 state = EditParsingState::InNewText {
1942 path,
1943 start_offset,
1944 old_text_range: old_text_start_offset..offset,
1945 new_text_start_offset: lines.offset(),
1946 };
1947 } else {
1948 state = EditParsingState::InOldText {
1949 path,
1950 start_offset,
1951 old_text_start_offset,
1952 };
1953 }
1954 }
1955 EditParsingState::InNewText {
1956 path,
1957 start_offset,
1958 old_text_range,
1959 new_text_start_offset,
1960 } => {
1961 if message_line == "```" {
1962 return Some(ParsedEditSuggestion {
1963 path,
1964 outer_range: start_offset..offset + "```".len(),
1965 old_text_range,
1966 new_text_range: new_text_start_offset..offset,
1967 });
1968 } else {
1969 state = EditParsingState::InNewText {
1970 path,
1971 start_offset,
1972 old_text_range,
1973 new_text_start_offset,
1974 };
1975 }
1976 }
1977 }
1978 }
1979}
1980
1981#[derive(Clone)]
1982struct PendingSlashCommand {
1983 name: String,
1984 argument: Option<String>,
1985 status: PendingSlashCommandStatus,
1986 source_range: Range<language::Anchor>,
1987}
1988
1989#[derive(Clone)]
1990enum PendingSlashCommandStatus {
1991 Idle,
1992 Running { _task: Shared<Task<()>> },
1993 Error(String),
1994}
1995
1996struct PendingCompletion {
1997 id: usize,
1998 _task: Task<()>,
1999}
2000
2001pub enum ContextEditorEvent {
2002 Edited,
2003 TabContentChanged,
2004}
2005
2006#[derive(Copy, Clone, Debug, PartialEq)]
2007struct ScrollPosition {
2008 offset_before_cursor: gpui::Point<f32>,
2009 cursor: Anchor,
2010}
2011
2012pub struct ContextEditor {
2013 context: Model<Context>,
2014 fs: Arc<dyn Fs>,
2015 workspace: WeakView<Workspace>,
2016 slash_command_registry: Arc<SlashCommandRegistry>,
2017 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2018 editor: View<Editor>,
2019 blocks: HashSet<BlockId>,
2020 scroll_position: Option<ScrollPosition>,
2021 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
2022 pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
2023 _subscriptions: Vec<Subscription>,
2024}
2025
2026impl ContextEditor {
2027 const MAX_TAB_TITLE_LEN: usize = 16;
2028
2029 fn new(
2030 language_registry: Arc<LanguageRegistry>,
2031 slash_command_registry: Arc<SlashCommandRegistry>,
2032 fs: Arc<dyn Fs>,
2033 workspace: View<Workspace>,
2034 cx: &mut ViewContext<Self>,
2035 ) -> Self {
2036 let telemetry = workspace.read(cx).client().telemetry().clone();
2037 let project = workspace.read(cx).project().clone();
2038 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
2039
2040 let context = cx.new_model(|cx| {
2041 Context::new(
2042 language_registry,
2043 slash_command_registry,
2044 Some(telemetry),
2045 cx,
2046 )
2047 });
2048
2049 let mut this = Self::for_context(context, fs, workspace, lsp_adapter_delegate, cx);
2050 this.insert_default_prompt(cx);
2051 this
2052 }
2053
2054 fn for_context(
2055 context: Model<Context>,
2056 fs: Arc<dyn Fs>,
2057 workspace: View<Workspace>,
2058 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
2059 cx: &mut ViewContext<Self>,
2060 ) -> Self {
2061 let slash_command_registry = context.read(cx).slash_command_registry.clone();
2062
2063 let completion_provider = SlashCommandCompletionProvider::new(
2064 slash_command_registry.clone(),
2065 Some(cx.view().downgrade()),
2066 Some(workspace.downgrade()),
2067 );
2068
2069 let editor = cx.new_view(|cx| {
2070 let mut editor = Editor::for_buffer(context.read(cx).buffer.clone(), None, cx);
2071 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2072 editor.set_show_line_numbers(false, cx);
2073 editor.set_show_git_diff_gutter(false, cx);
2074 editor.set_show_code_actions(false, cx);
2075 editor.set_show_runnables(false, cx);
2076 editor.set_show_wrap_guides(false, cx);
2077 editor.set_show_indent_guides(false, cx);
2078 editor.set_completion_provider(Box::new(completion_provider));
2079 editor
2080 });
2081
2082 let _subscriptions = vec![
2083 cx.observe(&context, |_, _, cx| cx.notify()),
2084 cx.subscribe(&context, Self::handle_context_event),
2085 cx.subscribe(&editor, Self::handle_editor_event),
2086 cx.subscribe(&editor, Self::handle_editor_search_event),
2087 ];
2088
2089 let sections = context.read(cx).slash_command_output_sections.clone();
2090 let mut this = Self {
2091 context,
2092 editor,
2093 slash_command_registry,
2094 lsp_adapter_delegate,
2095 blocks: Default::default(),
2096 scroll_position: None,
2097 fs,
2098 workspace: workspace.downgrade(),
2099 pending_slash_command_creases: HashMap::default(),
2100 pending_slash_command_blocks: HashMap::default(),
2101 _subscriptions,
2102 };
2103 this.update_message_headers(cx);
2104 this.insert_slash_command_output_sections(sections, cx);
2105 this
2106 }
2107
2108 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
2109 let command_name = DefaultSlashCommand.name();
2110 self.editor.update(cx, |editor, cx| {
2111 editor.insert(&format!("/{command_name}"), cx)
2112 });
2113 self.split(&Split, cx);
2114 let command = self.context.update(cx, |context, cx| {
2115 context
2116 .messages_metadata
2117 .get_mut(&MessageId::default())
2118 .unwrap()
2119 .role = Role::System;
2120 context.reparse_slash_commands(cx);
2121 context.pending_slash_commands[0].clone()
2122 });
2123
2124 self.run_command(
2125 command.source_range,
2126 &command.name,
2127 command.argument.as_deref(),
2128 false,
2129 self.workspace.clone(),
2130 cx,
2131 );
2132 }
2133
2134 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2135 let cursors = self.cursors(cx);
2136
2137 let user_messages = self.context.update(cx, |context, cx| {
2138 let selected_messages = context
2139 .messages_for_offsets(cursors, cx)
2140 .into_iter()
2141 .map(|message| message.id)
2142 .collect();
2143 context.assist(selected_messages, cx)
2144 });
2145 let new_selections = user_messages
2146 .iter()
2147 .map(|message| {
2148 let cursor = message
2149 .start
2150 .to_offset(self.context.read(cx).buffer.read(cx));
2151 cursor..cursor
2152 })
2153 .collect::<Vec<_>>();
2154 if !new_selections.is_empty() {
2155 self.editor.update(cx, |editor, cx| {
2156 editor.change_selections(
2157 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2158 cx,
2159 |selections| selections.select_ranges(new_selections),
2160 );
2161 });
2162 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2163 cx.defer(|this, _| this.scroll_position = None);
2164 }
2165 }
2166
2167 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2168 if !self
2169 .context
2170 .update(cx, |context, _| context.cancel_last_assist())
2171 {
2172 cx.propagate();
2173 }
2174 }
2175
2176 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2177 let cursors = self.cursors(cx);
2178 self.context.update(cx, |context, cx| {
2179 let messages = context
2180 .messages_for_offsets(cursors, cx)
2181 .into_iter()
2182 .map(|message| message.id)
2183 .collect();
2184 context.cycle_message_roles(messages, cx)
2185 });
2186 }
2187
2188 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2189 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2190 selections
2191 .into_iter()
2192 .map(|selection| selection.head())
2193 .collect()
2194 }
2195
2196 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2197 if let Some(command) = self.slash_command_registry.command(name) {
2198 self.editor.update(cx, |editor, cx| {
2199 editor.transact(cx, |editor, cx| {
2200 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2201 let snapshot = editor.buffer().read(cx).snapshot(cx);
2202 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2203 if newest_cursor.column > 0
2204 || snapshot
2205 .chars_at(newest_cursor)
2206 .next()
2207 .map_or(false, |ch| ch != '\n')
2208 {
2209 editor.move_to_end_of_line(
2210 &MoveToEndOfLine {
2211 stop_at_soft_wraps: false,
2212 },
2213 cx,
2214 );
2215 editor.newline(&Newline, cx);
2216 }
2217
2218 editor.insert(&format!("/{name}"), cx);
2219 if command.requires_argument() {
2220 editor.insert(" ", cx);
2221 editor.show_completions(&ShowCompletions::default(), cx);
2222 }
2223 });
2224 });
2225 if !command.requires_argument() {
2226 self.confirm_command(&ConfirmCommand, cx);
2227 }
2228 }
2229 }
2230
2231 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2232 let selections = self.editor.read(cx).selections.disjoint_anchors();
2233 let mut commands_by_range = HashMap::default();
2234 let workspace = self.workspace.clone();
2235 self.context.update(cx, |context, cx| {
2236 context.reparse_slash_commands(cx);
2237 for selection in selections.iter() {
2238 if let Some(command) =
2239 context.pending_command_for_position(selection.head().text_anchor, cx)
2240 {
2241 commands_by_range
2242 .entry(command.source_range.clone())
2243 .or_insert_with(|| command.clone());
2244 }
2245 }
2246 });
2247
2248 if commands_by_range.is_empty() {
2249 cx.propagate();
2250 } else {
2251 for command in commands_by_range.into_values() {
2252 self.run_command(
2253 command.source_range,
2254 &command.name,
2255 command.argument.as_deref(),
2256 true,
2257 workspace.clone(),
2258 cx,
2259 );
2260 }
2261 cx.stop_propagation();
2262 }
2263 }
2264
2265 pub fn run_command(
2266 &mut self,
2267 command_range: Range<language::Anchor>,
2268 name: &str,
2269 argument: Option<&str>,
2270 insert_trailing_newline: bool,
2271 workspace: WeakView<Workspace>,
2272 cx: &mut ViewContext<Self>,
2273 ) {
2274 if let Some(command) = self.slash_command_registry.command(name) {
2275 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
2276 let argument = argument.map(ToString::to_string);
2277 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
2278 self.context.update(cx, |context, cx| {
2279 context.insert_command_output(
2280 command_range,
2281 output,
2282 insert_trailing_newline,
2283 cx,
2284 )
2285 });
2286 }
2287 }
2288 }
2289
2290 fn handle_context_event(
2291 &mut self,
2292 _: Model<Context>,
2293 event: &ContextEvent,
2294 cx: &mut ViewContext<Self>,
2295 ) {
2296 let context_editor = cx.view().downgrade();
2297
2298 match event {
2299 ContextEvent::MessagesEdited => {
2300 self.update_message_headers(cx);
2301 self.context.update(cx, |context, cx| {
2302 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2303 });
2304 }
2305 ContextEvent::EditSuggestionsChanged => {
2306 self.editor.update(cx, |editor, cx| {
2307 let buffer = editor.buffer().read(cx).snapshot(cx);
2308 let excerpt_id = *buffer.as_singleton().unwrap().0;
2309 let context = self.context.read(cx);
2310 let highlighted_rows = context
2311 .edit_suggestions
2312 .iter()
2313 .map(|suggestion| {
2314 let start = buffer
2315 .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
2316 .unwrap();
2317 let end = buffer
2318 .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
2319 .unwrap();
2320 start..=end
2321 })
2322 .collect::<Vec<_>>();
2323
2324 editor.clear_row_highlights::<EditSuggestion>();
2325 for range in highlighted_rows {
2326 editor.highlight_rows::<EditSuggestion>(
2327 range,
2328 Some(
2329 cx.theme()
2330 .colors()
2331 .editor_document_highlight_read_background,
2332 ),
2333 false,
2334 cx,
2335 );
2336 }
2337 });
2338 }
2339 ContextEvent::SummaryChanged => {
2340 cx.emit(ContextEditorEvent::TabContentChanged);
2341 self.context.update(cx, |context, cx| {
2342 context.save(None, self.fs.clone(), cx);
2343 });
2344 }
2345 ContextEvent::StreamedCompletion => {
2346 self.editor.update(cx, |editor, cx| {
2347 if let Some(scroll_position) = self.scroll_position {
2348 let snapshot = editor.snapshot(cx);
2349 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2350 let scroll_top =
2351 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2352 editor.set_scroll_position(
2353 point(scroll_position.offset_before_cursor.x, scroll_top),
2354 cx,
2355 );
2356 }
2357 });
2358 }
2359 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2360 self.editor.update(cx, |editor, cx| {
2361 let buffer = editor.buffer().read(cx).snapshot(cx);
2362 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2363 let excerpt_id = *excerpt_id;
2364
2365 editor.remove_creases(
2366 removed
2367 .iter()
2368 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2369 cx,
2370 );
2371
2372 editor.remove_blocks(
2373 HashSet::from_iter(
2374 removed.iter().filter_map(|range| {
2375 self.pending_slash_command_blocks.remove(range)
2376 }),
2377 ),
2378 None,
2379 cx,
2380 );
2381
2382 let crease_ids = editor.insert_creases(
2383 updated.iter().map(|command| {
2384 let workspace = self.workspace.clone();
2385 let confirm_command = Arc::new({
2386 let context_editor = context_editor.clone();
2387 let command = command.clone();
2388 move |cx: &mut WindowContext| {
2389 context_editor
2390 .update(cx, |context_editor, cx| {
2391 context_editor.run_command(
2392 command.source_range.clone(),
2393 &command.name,
2394 command.argument.as_deref(),
2395 false,
2396 workspace.clone(),
2397 cx,
2398 );
2399 })
2400 .ok();
2401 }
2402 });
2403 let placeholder = FoldPlaceholder {
2404 render: Arc::new(move |_, _, _| Empty.into_any()),
2405 constrain_width: false,
2406 merge_adjacent: false,
2407 };
2408 let render_toggle = {
2409 let confirm_command = confirm_command.clone();
2410 let command = command.clone();
2411 move |row, _, _, _cx: &mut WindowContext| {
2412 render_pending_slash_command_gutter_decoration(
2413 row,
2414 &command.status,
2415 confirm_command.clone(),
2416 )
2417 }
2418 };
2419 let render_trailer = {
2420 let command = command.clone();
2421 move |row, _unfold, cx: &mut WindowContext| {
2422 // TODO: In the future we should investigate how we can expose
2423 // this as a hook on the `SlashCommand` trait so that we don't
2424 // need to special-case it here.
2425 if command.name == DocsSlashCommand::NAME {
2426 return render_docs_slash_command_trailer(
2427 row,
2428 command.clone(),
2429 cx,
2430 );
2431 }
2432
2433 Empty.into_any()
2434 }
2435 };
2436
2437 let start = buffer
2438 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2439 .unwrap();
2440 let end = buffer
2441 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2442 .unwrap();
2443 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2444 }),
2445 cx,
2446 );
2447
2448 let block_ids = editor.insert_blocks(
2449 updated
2450 .iter()
2451 .filter_map(|command| match &command.status {
2452 PendingSlashCommandStatus::Error(error) => {
2453 Some((command, error.clone()))
2454 }
2455 _ => None,
2456 })
2457 .map(|(command, error_message)| BlockProperties {
2458 style: BlockStyle::Fixed,
2459 position: Anchor {
2460 buffer_id: Some(buffer_id),
2461 excerpt_id,
2462 text_anchor: command.source_range.start,
2463 },
2464 height: 1,
2465 disposition: BlockDisposition::Below,
2466 render: slash_command_error_block_renderer(error_message),
2467 }),
2468 None,
2469 cx,
2470 );
2471
2472 self.pending_slash_command_creases.extend(
2473 updated
2474 .iter()
2475 .map(|command| command.source_range.clone())
2476 .zip(crease_ids),
2477 );
2478
2479 self.pending_slash_command_blocks.extend(
2480 updated
2481 .iter()
2482 .map(|command| command.source_range.clone())
2483 .zip(block_ids),
2484 );
2485 })
2486 }
2487 ContextEvent::SlashCommandFinished {
2488 output_range,
2489 sections,
2490 run_commands_in_output,
2491 } => {
2492 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2493
2494 if *run_commands_in_output {
2495 let commands = self.context.update(cx, |context, cx| {
2496 context.reparse_slash_commands(cx);
2497 context
2498 .pending_commands_for_range(output_range.clone(), cx)
2499 .to_vec()
2500 });
2501
2502 for command in commands {
2503 self.run_command(
2504 command.source_range,
2505 &command.name,
2506 command.argument.as_deref(),
2507 false,
2508 self.workspace.clone(),
2509 cx,
2510 );
2511 }
2512 }
2513 }
2514 }
2515 }
2516
2517 fn insert_slash_command_output_sections(
2518 &mut self,
2519 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2520 cx: &mut ViewContext<Self>,
2521 ) {
2522 self.editor.update(cx, |editor, cx| {
2523 let buffer = editor.buffer().read(cx).snapshot(cx);
2524 let excerpt_id = *buffer.as_singleton().unwrap().0;
2525 let mut buffer_rows_to_fold = BTreeSet::new();
2526 let mut creases = Vec::new();
2527 for section in sections {
2528 let start = buffer
2529 .anchor_in_excerpt(excerpt_id, section.range.start)
2530 .unwrap();
2531 let end = buffer
2532 .anchor_in_excerpt(excerpt_id, section.range.end)
2533 .unwrap();
2534 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2535 buffer_rows_to_fold.insert(buffer_row);
2536 creases.push(Crease::new(
2537 start..end,
2538 FoldPlaceholder {
2539 render: Arc::new({
2540 let editor = cx.view().downgrade();
2541 let icon = section.icon;
2542 let label = section.label.clone();
2543 move |fold_id, fold_range, _cx| {
2544 let editor = editor.clone();
2545 ButtonLike::new(fold_id)
2546 .style(ButtonStyle::Filled)
2547 .layer(ElevationIndex::ElevatedSurface)
2548 .child(Icon::new(icon))
2549 .child(Label::new(label.clone()).single_line())
2550 .on_click(move |_, cx| {
2551 editor
2552 .update(cx, |editor, cx| {
2553 let buffer_start = fold_range
2554 .start
2555 .to_point(&editor.buffer().read(cx).read(cx));
2556 let buffer_row = MultiBufferRow(buffer_start.row);
2557 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2558 })
2559 .ok();
2560 })
2561 .into_any_element()
2562 }
2563 }),
2564 constrain_width: false,
2565 merge_adjacent: false,
2566 },
2567 render_slash_command_output_toggle,
2568 |_, _, _| Empty.into_any_element(),
2569 ));
2570 }
2571
2572 editor.insert_creases(creases, cx);
2573
2574 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2575 editor.fold_at(&FoldAt { buffer_row }, cx);
2576 }
2577 });
2578 }
2579
2580 fn handle_editor_event(
2581 &mut self,
2582 _: View<Editor>,
2583 event: &EditorEvent,
2584 cx: &mut ViewContext<Self>,
2585 ) {
2586 match event {
2587 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2588 let cursor_scroll_position = self.cursor_scroll_position(cx);
2589 if *autoscroll {
2590 self.scroll_position = cursor_scroll_position;
2591 } else if self.scroll_position != cursor_scroll_position {
2592 self.scroll_position = None;
2593 }
2594 }
2595 EditorEvent::SelectionsChanged { .. } => {
2596 self.scroll_position = self.cursor_scroll_position(cx);
2597 }
2598 EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
2599 _ => {}
2600 }
2601 }
2602
2603 fn handle_editor_search_event(
2604 &mut self,
2605 _: View<Editor>,
2606 event: &SearchEvent,
2607 cx: &mut ViewContext<Self>,
2608 ) {
2609 cx.emit(event.clone());
2610 }
2611
2612 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2613 self.editor.update(cx, |editor, cx| {
2614 let snapshot = editor.snapshot(cx);
2615 let cursor = editor.selections.newest_anchor().head();
2616 let cursor_row = cursor
2617 .to_display_point(&snapshot.display_snapshot)
2618 .row()
2619 .as_f32();
2620 let scroll_position = editor
2621 .scroll_manager
2622 .anchor()
2623 .scroll_position(&snapshot.display_snapshot);
2624
2625 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2626 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2627 Some(ScrollPosition {
2628 cursor,
2629 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2630 })
2631 } else {
2632 None
2633 }
2634 })
2635 }
2636
2637 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2638 self.editor.update(cx, |editor, cx| {
2639 let buffer = editor.buffer().read(cx).snapshot(cx);
2640 let excerpt_id = *buffer.as_singleton().unwrap().0;
2641 let old_blocks = std::mem::take(&mut self.blocks);
2642 let new_blocks = self
2643 .context
2644 .read(cx)
2645 .messages(cx)
2646 .map(|message| BlockProperties {
2647 position: buffer
2648 .anchor_in_excerpt(excerpt_id, message.anchor)
2649 .unwrap(),
2650 height: 2,
2651 style: BlockStyle::Sticky,
2652 render: Box::new({
2653 let context = self.context.clone();
2654 move |cx| {
2655 let message_id = message.id;
2656 let sender = ButtonLike::new("role")
2657 .style(ButtonStyle::Filled)
2658 .child(match message.role {
2659 Role::User => Label::new("You").color(Color::Default),
2660 Role::Assistant => Label::new("Assistant").color(Color::Info),
2661 Role::System => Label::new("System").color(Color::Warning),
2662 })
2663 .tooltip(|cx| {
2664 Tooltip::with_meta(
2665 "Toggle message role",
2666 None,
2667 "Available roles: You (User), Assistant, System",
2668 cx,
2669 )
2670 })
2671 .on_click({
2672 let context = context.clone();
2673 move |_, cx| {
2674 context.update(cx, |context, cx| {
2675 context.cycle_message_roles(
2676 HashSet::from_iter(Some(message_id)),
2677 cx,
2678 )
2679 })
2680 }
2681 });
2682
2683 h_flex()
2684 .id(("message_header", message_id.0))
2685 .pl(cx.gutter_dimensions.full_width())
2686 .h_11()
2687 .w_full()
2688 .relative()
2689 .gap_1()
2690 .child(sender)
2691 .children(
2692 if let MessageStatus::Error(error) = message.status.clone() {
2693 Some(
2694 div()
2695 .id("error")
2696 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2697 .child(Icon::new(IconName::XCircle)),
2698 )
2699 } else {
2700 None
2701 },
2702 )
2703 .into_any_element()
2704 }
2705 }),
2706 disposition: BlockDisposition::Above,
2707 })
2708 .collect::<Vec<_>>();
2709
2710 editor.remove_blocks(old_blocks, None, cx);
2711 let ids = editor.insert_blocks(new_blocks, None, cx);
2712 self.blocks = HashSet::from_iter(ids);
2713 });
2714 }
2715
2716 fn insert_selection(
2717 workspace: &mut Workspace,
2718 _: &InsertIntoEditor,
2719 cx: &mut ViewContext<Workspace>,
2720 ) {
2721 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2722 return;
2723 };
2724 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2725 return;
2726 };
2727 let Some(active_editor_view) = workspace
2728 .active_item(cx)
2729 .and_then(|item| item.act_as::<Editor>(cx))
2730 else {
2731 return;
2732 };
2733
2734 let context_editor = context_editor_view.read(cx).editor.read(cx);
2735 let anchor = context_editor.selections.newest_anchor();
2736 let text = context_editor
2737 .buffer()
2738 .read(cx)
2739 .read(cx)
2740 .text_for_range(anchor.range())
2741 .collect::<String>();
2742
2743 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2744 if !text.is_empty() {
2745 active_editor_view.update(cx, |editor, cx| {
2746 editor.insert(&text, cx);
2747 editor.focus(cx);
2748 })
2749 }
2750 }
2751
2752 fn quote_selection(
2753 workspace: &mut Workspace,
2754 _: &QuoteSelection,
2755 cx: &mut ViewContext<Workspace>,
2756 ) {
2757 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2758 return;
2759 };
2760 let Some(editor) = workspace
2761 .active_item(cx)
2762 .and_then(|item| item.act_as::<Editor>(cx))
2763 else {
2764 return;
2765 };
2766
2767 let editor = editor.read(cx);
2768 let range = editor.selections.newest::<usize>(cx).range();
2769 let buffer = editor.buffer().read(cx).snapshot(cx);
2770 let start_language = buffer.language_at(range.start);
2771 let end_language = buffer.language_at(range.end);
2772 let language_name = if start_language == end_language {
2773 start_language.map(|language| language.code_fence_block_name())
2774 } else {
2775 None
2776 };
2777 let language_name = language_name.as_deref().unwrap_or("");
2778
2779 let selected_text = buffer.text_for_range(range).collect::<String>();
2780 let text = if selected_text.is_empty() {
2781 None
2782 } else {
2783 Some(if language_name == "markdown" {
2784 selected_text
2785 .lines()
2786 .map(|line| format!("> {}", line))
2787 .collect::<Vec<_>>()
2788 .join("\n")
2789 } else {
2790 format!("```{language_name}\n{selected_text}\n```")
2791 })
2792 };
2793
2794 // Activate the panel
2795 if !panel.focus_handle(cx).contains_focused(cx) {
2796 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2797 }
2798
2799 if let Some(text) = text {
2800 panel.update(cx, |_, cx| {
2801 // Wait to create a new context until the workspace is no longer
2802 // being updated.
2803 cx.defer(move |panel, cx| {
2804 if let Some(context) = panel
2805 .active_context_editor(cx)
2806 .or_else(|| panel.new_context(cx))
2807 {
2808 context.update(cx, |context, cx| {
2809 context
2810 .editor
2811 .update(cx, |editor, cx| editor.insert(&text, cx))
2812 });
2813 };
2814 });
2815 });
2816 }
2817 }
2818
2819 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2820 let editor = self.editor.read(cx);
2821 let context = self.context.read(cx);
2822 if editor.selections.count() == 1 {
2823 let selection = editor.selections.newest::<usize>(cx);
2824 let mut copied_text = String::new();
2825 let mut spanned_messages = 0;
2826 for message in context.messages(cx) {
2827 if message.offset_range.start >= selection.range().end {
2828 break;
2829 } else if message.offset_range.end >= selection.range().start {
2830 let range = cmp::max(message.offset_range.start, selection.range().start)
2831 ..cmp::min(message.offset_range.end, selection.range().end);
2832 if !range.is_empty() {
2833 spanned_messages += 1;
2834 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2835 for chunk in context.buffer.read(cx).text_for_range(range) {
2836 copied_text.push_str(chunk);
2837 }
2838 copied_text.push('\n');
2839 }
2840 }
2841 }
2842
2843 if spanned_messages > 1 {
2844 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2845 return;
2846 }
2847 }
2848
2849 cx.propagate();
2850 }
2851
2852 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2853 self.context.update(cx, |context, cx| {
2854 let selections = self.editor.read(cx).selections.disjoint_anchors();
2855 for selection in selections.as_ref() {
2856 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2857 let range = selection
2858 .map(|endpoint| endpoint.to_offset(&buffer))
2859 .range();
2860 context.split_message(range, cx);
2861 }
2862 });
2863 }
2864
2865 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
2866 let Some(workspace) = self.workspace.upgrade() else {
2867 return;
2868 };
2869 let project = workspace.read(cx).project().clone();
2870
2871 struct Edit {
2872 old_text: String,
2873 new_text: String,
2874 }
2875
2876 let context = self.context.read(cx);
2877 let context_buffer = context.buffer.read(cx);
2878 let context_buffer_snapshot = context_buffer.snapshot();
2879
2880 let selections = self.editor.read(cx).selections.disjoint_anchors();
2881 let mut selections = selections.iter().peekable();
2882 let selected_suggestions = context
2883 .edit_suggestions
2884 .iter()
2885 .filter(|suggestion| {
2886 while let Some(selection) = selections.peek() {
2887 if selection
2888 .end
2889 .text_anchor
2890 .cmp(&suggestion.source_range.start, context_buffer)
2891 .is_lt()
2892 {
2893 selections.next();
2894 continue;
2895 }
2896 if selection
2897 .start
2898 .text_anchor
2899 .cmp(&suggestion.source_range.end, context_buffer)
2900 .is_gt()
2901 {
2902 break;
2903 }
2904 return true;
2905 }
2906 false
2907 })
2908 .cloned()
2909 .collect::<Vec<_>>();
2910
2911 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
2912 project.update(cx, |project, cx| {
2913 for suggestion in &selected_suggestions {
2914 opened_buffers
2915 .entry(suggestion.full_path.clone())
2916 .or_insert_with(|| {
2917 project.open_buffer_for_full_path(&suggestion.full_path, cx)
2918 });
2919 }
2920 });
2921
2922 cx.spawn(|this, mut cx| async move {
2923 let mut buffers_by_full_path = HashMap::default();
2924 for (full_path, buffer) in opened_buffers {
2925 if let Some(buffer) = buffer.await.log_err() {
2926 buffers_by_full_path.insert(full_path, buffer);
2927 }
2928 }
2929
2930 let mut suggestions_by_buffer = HashMap::default();
2931 cx.update(|cx| {
2932 for suggestion in selected_suggestions {
2933 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
2934 let (_, edits) = suggestions_by_buffer
2935 .entry(buffer.clone())
2936 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
2937
2938 let mut lines = context_buffer_snapshot
2939 .as_rope()
2940 .chunks_in_range(
2941 suggestion.source_range.to_offset(&context_buffer_snapshot),
2942 )
2943 .lines();
2944 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
2945 let old_text = context_buffer_snapshot
2946 .text_for_range(suggestion.old_text_range)
2947 .collect();
2948 let new_text = context_buffer_snapshot
2949 .text_for_range(suggestion.new_text_range)
2950 .collect();
2951 edits.push(Edit { old_text, new_text });
2952 }
2953 }
2954 }
2955 })?;
2956
2957 let edits_by_buffer = cx
2958 .background_executor()
2959 .spawn(async move {
2960 let mut result = HashMap::default();
2961 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
2962 let edits =
2963 result
2964 .entry(buffer)
2965 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
2966 for suggestion in suggestions {
2967 if let Some(range) =
2968 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
2969 {
2970 let edit_start = snapshot.anchor_after(range.start);
2971 let edit_end = snapshot.anchor_before(range.end);
2972 if let Err(ix) = edits.binary_search_by(|(range, _)| {
2973 range.start.cmp(&edit_start, &snapshot)
2974 }) {
2975 edits.insert(
2976 ix,
2977 (edit_start..edit_end, suggestion.new_text.clone()),
2978 );
2979 }
2980 } else {
2981 log::info!(
2982 "assistant edit did not match any text in buffer {:?}",
2983 &suggestion.old_text
2984 );
2985 }
2986 }
2987 }
2988 result
2989 })
2990 .await;
2991
2992 let mut project_transaction = ProjectTransaction::default();
2993 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
2994 for (buffer_handle, edits) in edits_by_buffer {
2995 buffer_handle.update(cx, |buffer, cx| {
2996 buffer.start_transaction();
2997 buffer.edit(
2998 edits,
2999 Some(AutoindentMode::Block {
3000 original_indent_columns: Vec::new(),
3001 }),
3002 cx,
3003 );
3004 buffer.end_transaction(cx);
3005 if let Some(transaction) = buffer.finalize_last_transaction() {
3006 project_transaction
3007 .0
3008 .insert(buffer_handle.clone(), transaction.clone());
3009 }
3010 });
3011 }
3012
3013 (
3014 this.editor.downgrade(),
3015 this.workspace.clone(),
3016 this.title(cx),
3017 )
3018 })?;
3019
3020 Editor::open_project_transaction(
3021 &editor,
3022 workspace,
3023 project_transaction,
3024 format!("Edits from {}", title),
3025 cx,
3026 )
3027 .await
3028 })
3029 .detach_and_log_err(cx);
3030 }
3031
3032 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3033 self.context
3034 .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
3035 }
3036
3037 fn title(&self, cx: &AppContext) -> String {
3038 self.context
3039 .read(cx)
3040 .summary
3041 .as_ref()
3042 .map(|summary| summary.text.clone())
3043 .unwrap_or_else(|| "New Context".into())
3044 }
3045
3046 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3047 let focus_handle = self.focus_handle(cx).clone();
3048 ButtonLike::new("send_button")
3049 .style(ButtonStyle::Filled)
3050 .layer(ElevationIndex::ModalSurface)
3051 .children(
3052 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3053 .map(|binding| binding.into_any_element()),
3054 )
3055 .child(Label::new("Send"))
3056 .on_click(move |_event, cx| {
3057 focus_handle.dispatch_action(&Assist, cx);
3058 })
3059 }
3060}
3061
3062impl EventEmitter<ContextEditorEvent> for ContextEditor {}
3063impl EventEmitter<SearchEvent> for ContextEditor {}
3064
3065impl Render for ContextEditor {
3066 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3067 div()
3068 .key_context("ContextEditor")
3069 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
3070 .capture_action(cx.listener(ContextEditor::save))
3071 .capture_action(cx.listener(ContextEditor::copy))
3072 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3073 .capture_action(cx.listener(ContextEditor::confirm_command))
3074 .on_action(cx.listener(ContextEditor::assist))
3075 .on_action(cx.listener(ContextEditor::split))
3076 .on_action(cx.listener(ContextEditor::apply_edit))
3077 .size_full()
3078 .v_flex()
3079 .child(
3080 div()
3081 .flex_grow()
3082 .bg(cx.theme().colors().editor_background)
3083 .child(self.editor.clone())
3084 .child(
3085 h_flex()
3086 .w_full()
3087 .absolute()
3088 .bottom_0()
3089 .p_4()
3090 .justify_end()
3091 .child(self.render_send_button(cx)),
3092 ),
3093 )
3094 }
3095}
3096
3097impl FocusableView for ContextEditor {
3098 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3099 self.editor.focus_handle(cx)
3100 }
3101}
3102
3103impl Item for ContextEditor {
3104 type Event = ContextEditorEvent;
3105
3106 fn tab_content(
3107 &self,
3108 params: workspace::item::TabContentParams,
3109 cx: &WindowContext,
3110 ) -> AnyElement {
3111 let color = if params.selected {
3112 Color::Default
3113 } else {
3114 Color::Muted
3115 };
3116 Label::new(util::truncate_and_trailoff(
3117 &self.title(cx),
3118 Self::MAX_TAB_TITLE_LEN,
3119 ))
3120 .color(color)
3121 .into_any_element()
3122 }
3123
3124 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
3125 match event {
3126 ContextEditorEvent::Edited => {
3127 f(workspace::item::ItemEvent::Edit);
3128 f(workspace::item::ItemEvent::UpdateBreadcrumbs);
3129 }
3130 ContextEditorEvent::TabContentChanged => {
3131 f(workspace::item::ItemEvent::UpdateTab);
3132 }
3133 }
3134 }
3135
3136 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
3137 Some(self.title(cx).into())
3138 }
3139
3140 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
3141 Some(Box::new(handle.clone()))
3142 }
3143
3144 fn breadcrumbs(
3145 &self,
3146 theme: &theme::Theme,
3147 cx: &AppContext,
3148 ) -> Option<Vec<workspace::item::BreadcrumbText>> {
3149 let editor = self.editor.read(cx);
3150 let cursor = editor.selections.newest_anchor().head();
3151 let multibuffer = &editor.buffer().read(cx);
3152 let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?;
3153
3154 let settings = ThemeSettings::get_global(cx);
3155
3156 let mut breadcrumbs = Vec::new();
3157
3158 let title = self.title(cx);
3159 if title.chars().count() > Self::MAX_TAB_TITLE_LEN {
3160 breadcrumbs.push(BreadcrumbText {
3161 text: title,
3162 highlights: None,
3163 font: Some(settings.buffer_font.clone()),
3164 });
3165 }
3166
3167 breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
3168 text: symbol.text,
3169 highlights: Some(symbol.highlight_ranges),
3170 font: Some(settings.buffer_font.clone()),
3171 }));
3172 Some(breadcrumbs)
3173 }
3174
3175 fn breadcrumb_location(&self) -> ToolbarItemLocation {
3176 ToolbarItemLocation::PrimaryLeft
3177 }
3178
3179 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
3180 self.editor.update(cx, |editor, cx| {
3181 Item::set_nav_history(editor, nav_history, cx)
3182 })
3183 }
3184
3185 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
3186 self.editor
3187 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
3188 }
3189
3190 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3191 self.editor
3192 .update(cx, |editor, cx| Item::deactivated(editor, cx))
3193 }
3194}
3195
3196impl SearchableItem for ContextEditor {
3197 type Match = <Editor as SearchableItem>::Match;
3198
3199 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
3200 self.editor.update(cx, |editor, cx| {
3201 editor.clear_matches(cx);
3202 });
3203 }
3204
3205 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3206 self.editor
3207 .update(cx, |editor, cx| editor.update_matches(matches, cx));
3208 }
3209
3210 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
3211 self.editor
3212 .update(cx, |editor, cx| editor.query_suggestion(cx))
3213 }
3214
3215 fn activate_match(
3216 &mut self,
3217 index: usize,
3218 matches: &[Self::Match],
3219 cx: &mut ViewContext<Self>,
3220 ) {
3221 self.editor.update(cx, |editor, cx| {
3222 editor.activate_match(index, matches, cx);
3223 });
3224 }
3225
3226 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3227 self.editor
3228 .update(cx, |editor, cx| editor.select_matches(matches, cx));
3229 }
3230
3231 fn replace(
3232 &mut self,
3233 identifier: &Self::Match,
3234 query: &project::search::SearchQuery,
3235 cx: &mut ViewContext<Self>,
3236 ) {
3237 self.editor
3238 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
3239 }
3240
3241 fn find_matches(
3242 &mut self,
3243 query: Arc<project::search::SearchQuery>,
3244 cx: &mut ViewContext<Self>,
3245 ) -> Task<Vec<Self::Match>> {
3246 self.editor
3247 .update(cx, |editor, cx| editor.find_matches(query, cx))
3248 }
3249
3250 fn active_match_index(
3251 &mut self,
3252 matches: &[Self::Match],
3253 cx: &mut ViewContext<Self>,
3254 ) -> Option<usize> {
3255 self.editor
3256 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
3257 }
3258}
3259
3260pub struct ContextEditorToolbarItem {
3261 fs: Arc<dyn Fs>,
3262 workspace: WeakView<Workspace>,
3263 active_context_editor: Option<WeakView<ContextEditor>>,
3264 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
3265}
3266
3267impl ContextEditorToolbarItem {
3268 pub fn new(
3269 workspace: &Workspace,
3270 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
3271 ) -> Self {
3272 Self {
3273 fs: workspace.app_state().fs.clone(),
3274 workspace: workspace.weak_handle(),
3275 active_context_editor: None,
3276 model_selector_menu_handle,
3277 }
3278 }
3279
3280 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
3281 let commands = SlashCommandRegistry::global(cx);
3282 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
3283 Some(
3284 workspace
3285 .read(cx)
3286 .active_item_as::<Editor>(cx)?
3287 .focus_handle(cx),
3288 )
3289 });
3290 let active_context_editor = self.active_context_editor.clone();
3291
3292 PopoverMenu::new("inject-context-menu")
3293 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
3294 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
3295 }))
3296 .menu(move |cx| {
3297 let active_context_editor = active_context_editor.clone()?;
3298 ContextMenu::build(cx, |mut menu, _cx| {
3299 for command_name in commands.featured_command_names() {
3300 if let Some(command) = commands.command(&command_name) {
3301 let menu_text = SharedString::from(Arc::from(command.menu_text()));
3302 menu = menu.custom_entry(
3303 {
3304 let command_name = command_name.clone();
3305 move |_cx| {
3306 h_flex()
3307 .gap_4()
3308 .w_full()
3309 .justify_between()
3310 .child(Label::new(menu_text.clone()))
3311 .child(
3312 Label::new(format!("/{command_name}"))
3313 .color(Color::Muted),
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}