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