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