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