1use anyhow::Result;
2use assistant_settings::AssistantSettings;
3use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
4use assistant_slash_commands::{
5 selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
6 FileSlashCommand,
7};
8use client::{proto, zed_urls};
9use collections::{hash_map, BTreeSet, HashMap, HashSet};
10use editor::{
11 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
12 display_map::{
13 BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
14 CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
15 },
16 scroll::{Autoscroll, AutoscrollStrategy},
17 Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
18 ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
19};
20use editor::{display_map::CreaseId, FoldPlaceholder};
21use fs::Fs;
22use futures::FutureExt;
23use gpui::{
24 actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
25 size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
26 ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
27 Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
28 SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
29 WeakEntity,
30};
31use indexed_docs::IndexedDocsStore;
32use language::{language_settings::SoftWrap, BufferSnapshot, LspAdapterDelegate, ToOffset};
33use language_model::{
34 LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
35 Role,
36};
37use language_model_selector::{AssistantLanguageModelSelector, LanguageModelSelector};
38use multi_buffer::MultiBufferRow;
39use picker::Picker;
40use project::lsp_store::LocalLspAdapterDelegate;
41use project::{Project, Worktree};
42use rope::Point;
43use serde::{Deserialize, Serialize};
44use settings::{update_settings_file, Settings};
45use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
46use text::SelectionGoal;
47use ui::{
48 prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor,
49 Tooltip,
50};
51use util::{maybe, ResultExt};
52use workspace::searchable::SearchableItemHandle;
53use workspace::{
54 item::{self, FollowableItem, Item, ItemHandle},
55 notifications::NotificationId,
56 pane::{self, SaveIntent},
57 searchable::{SearchEvent, SearchableItem},
58 Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
59 Workspace,
60};
61
62use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
63use crate::{
64 AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
65 ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
66 MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
67};
68
69actions!(
70 assistant,
71 [
72 Assist,
73 ConfirmCommand,
74 CopyCode,
75 CycleMessageRole,
76 Edit,
77 InsertIntoEditor,
78 QuoteSelection,
79 Split,
80 ]
81);
82
83#[derive(PartialEq, Clone)]
84pub enum InsertDraggedFiles {
85 ProjectPaths(Vec<PathBuf>),
86 ExternalFiles(Vec<PathBuf>),
87}
88
89impl_internal_actions!(assistant, [InsertDraggedFiles]);
90
91#[derive(Copy, Clone, Debug, PartialEq)]
92struct ScrollPosition {
93 offset_before_cursor: gpui::Point<f32>,
94 cursor: Anchor,
95}
96
97struct PatchViewState {
98 crease_id: CreaseId,
99 editor: Option<PatchEditorState>,
100 update_task: Option<Task<()>>,
101}
102
103struct PatchEditorState {
104 editor: WeakEntity<ProposedChangesEditor>,
105 opened_patch: AssistantPatch,
106}
107
108type MessageHeader = MessageMetadata;
109
110#[derive(Clone)]
111enum AssistError {
112 FileRequired,
113 PaymentRequired,
114 MaxMonthlySpendReached,
115 Message(SharedString),
116}
117
118pub trait AssistantPanelDelegate {
119 fn active_context_editor(
120 &self,
121 workspace: &mut Workspace,
122 window: &mut Window,
123 cx: &mut Context<Workspace>,
124 ) -> Option<Entity<ContextEditor>>;
125
126 fn open_saved_context(
127 &self,
128 workspace: &mut Workspace,
129 path: PathBuf,
130 window: &mut Window,
131 cx: &mut Context<Workspace>,
132 ) -> Task<Result<()>>;
133
134 fn open_remote_context(
135 &self,
136 workspace: &mut Workspace,
137 context_id: ContextId,
138 window: &mut Window,
139 cx: &mut Context<Workspace>,
140 ) -> Task<Result<Entity<ContextEditor>>>;
141
142 fn quote_selection(
143 &self,
144 workspace: &mut Workspace,
145 creases: Vec<(String, String)>,
146 window: &mut Window,
147 cx: &mut Context<Workspace>,
148 );
149}
150
151impl dyn AssistantPanelDelegate {
152 /// Returns the global [`AssistantPanelDelegate`], if it exists.
153 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
154 cx.try_global::<GlobalAssistantPanelDelegate>()
155 .map(|global| global.0.clone())
156 }
157
158 /// Sets the global [`AssistantPanelDelegate`].
159 pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
160 cx.set_global(GlobalAssistantPanelDelegate(delegate));
161 }
162}
163
164struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
165
166impl Global for GlobalAssistantPanelDelegate {}
167
168pub struct ContextEditor {
169 context: Entity<AssistantContext>,
170 fs: Arc<dyn Fs>,
171 slash_commands: Arc<SlashCommandWorkingSet>,
172 workspace: WeakEntity<Workspace>,
173 project: Entity<Project>,
174 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
175 editor: Entity<Editor>,
176 blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
177 image_blocks: HashSet<CustomBlockId>,
178 scroll_position: Option<ScrollPosition>,
179 remote_id: Option<workspace::ViewId>,
180 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
181 invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
182 _subscriptions: Vec<Subscription>,
183 patches: HashMap<Range<language::Anchor>, PatchViewState>,
184 active_patch: Option<Range<language::Anchor>>,
185 last_error: Option<AssistError>,
186 show_accept_terms: bool,
187 pub(crate) slash_menu_handle:
188 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
189 // dragged_file_worktrees is used to keep references to worktrees that were added
190 // when the user drag/dropped an external file onto the context editor. Since
191 // the worktree is not part of the project panel, it would be dropped as soon as
192 // the file is opened. In order to keep the worktree alive for the duration of the
193 // context editor, we keep a reference here.
194 dragged_file_worktrees: Vec<Entity<Worktree>>,
195 language_model_selector: Entity<LanguageModelSelector>,
196}
197
198pub const DEFAULT_TAB_TITLE: &str = "New Chat";
199const MAX_TAB_TITLE_LEN: usize = 16;
200
201impl ContextEditor {
202 pub fn for_context(
203 context: Entity<AssistantContext>,
204 fs: Arc<dyn Fs>,
205 workspace: WeakEntity<Workspace>,
206 project: Entity<Project>,
207 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
208 window: &mut Window,
209 cx: &mut Context<Self>,
210 ) -> Self {
211 let completion_provider = SlashCommandCompletionProvider::new(
212 context.read(cx).slash_commands().clone(),
213 Some(cx.entity().downgrade()),
214 Some(workspace.clone()),
215 );
216
217 let editor = cx.new(|cx| {
218 let mut editor =
219 Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx);
220 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
221 editor.set_show_line_numbers(false, cx);
222 editor.set_show_scrollbars(false, cx);
223 editor.set_show_git_diff_gutter(false, cx);
224 editor.set_show_code_actions(false, cx);
225 editor.set_show_runnables(false, cx);
226 editor.set_show_wrap_guides(false, cx);
227 editor.set_show_indent_guides(false, cx);
228 editor.set_completion_provider(Some(Box::new(completion_provider)));
229 editor.set_menu_inline_completions_policy(MenuInlineCompletionsPolicy::Never);
230 editor.set_collaboration_hub(Box::new(project.clone()));
231 editor
232 });
233
234 let _subscriptions = vec![
235 cx.observe(&context, |_, _, cx| cx.notify()),
236 cx.subscribe_in(&context, window, Self::handle_context_event),
237 cx.subscribe_in(&editor, window, Self::handle_editor_event),
238 cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
239 ];
240
241 let fs_clone = fs.clone();
242 let language_model_selector = cx.new(|cx| {
243 LanguageModelSelector::new(
244 move |model, cx| {
245 update_settings_file::<AssistantSettings>(
246 fs_clone.clone(),
247 cx,
248 move |settings, _| settings.set_model(model.clone()),
249 );
250 },
251 window,
252 cx,
253 )
254 });
255
256 let sections = context.read(cx).slash_command_output_sections().to_vec();
257 let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
258 let slash_commands = context.read(cx).slash_commands().clone();
259 let mut this = Self {
260 context,
261 slash_commands,
262 editor,
263 lsp_adapter_delegate,
264 blocks: Default::default(),
265 image_blocks: Default::default(),
266 scroll_position: None,
267 remote_id: None,
268 fs,
269 workspace,
270 project,
271 pending_slash_command_creases: HashMap::default(),
272 invoked_slash_command_creases: HashMap::default(),
273 _subscriptions,
274 patches: HashMap::default(),
275 active_patch: None,
276 last_error: None,
277 show_accept_terms: false,
278 slash_menu_handle: Default::default(),
279 dragged_file_worktrees: Vec::new(),
280 language_model_selector,
281 };
282 this.update_message_headers(cx);
283 this.update_image_blocks(cx);
284 this.insert_slash_command_output_sections(sections, false, window, cx);
285 this.patches_updated(&Vec::new(), &patch_ranges, window, cx);
286 this
287 }
288
289 pub fn context(&self) -> &Entity<AssistantContext> {
290 &self.context
291 }
292
293 pub fn editor(&self) -> &Entity<Editor> {
294 &self.editor
295 }
296
297 pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
298 let command_name = DefaultSlashCommand.name();
299 self.editor.update(cx, |editor, cx| {
300 editor.insert(&format!("/{command_name}\n\n"), window, cx)
301 });
302 let command = self.context.update(cx, |context, cx| {
303 context.reparse(cx);
304 context.parsed_slash_commands()[0].clone()
305 });
306 self.run_command(
307 command.source_range,
308 &command.name,
309 &command.arguments,
310 false,
311 self.workspace.clone(),
312 window,
313 cx,
314 );
315 }
316
317 fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
318 self.send_to_model(RequestType::Chat, window, cx);
319 }
320
321 fn edit(&mut self, _: &Edit, window: &mut Window, cx: &mut Context<Self>) {
322 self.send_to_model(RequestType::SuggestEdits, window, cx);
323 }
324
325 fn focus_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
326 if let Some((_range, patch)) = self.active_patch() {
327 if let Some(editor) = patch
328 .editor
329 .as_ref()
330 .and_then(|state| state.editor.upgrade())
331 {
332 editor.focus_handle(cx).focus(window);
333 return true;
334 }
335 }
336
337 false
338 }
339
340 fn send_to_model(
341 &mut self,
342 request_type: RequestType,
343 window: &mut Window,
344 cx: &mut Context<Self>,
345 ) {
346 let provider = LanguageModelRegistry::read_global(cx).active_provider();
347 if provider
348 .as_ref()
349 .map_or(false, |provider| provider.must_accept_terms(cx))
350 {
351 self.show_accept_terms = true;
352 cx.notify();
353 return;
354 }
355
356 if self.focus_active_patch(window, cx) {
357 return;
358 }
359
360 self.last_error = None;
361
362 if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
363 self.last_error = Some(AssistError::FileRequired);
364 cx.notify();
365 } else if let Some(user_message) = self
366 .context
367 .update(cx, |context, cx| context.assist(request_type, cx))
368 {
369 let new_selection = {
370 let cursor = user_message
371 .start
372 .to_offset(self.context.read(cx).buffer().read(cx));
373 cursor..cursor
374 };
375 self.editor.update(cx, |editor, cx| {
376 editor.change_selections(
377 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
378 window,
379 cx,
380 |selections| selections.select_ranges([new_selection]),
381 );
382 });
383 // Avoid scrolling to the new cursor position so the assistant's output is stable.
384 cx.defer_in(window, |this, _, _| this.scroll_position = None);
385 }
386
387 cx.notify();
388 }
389
390 fn cancel(
391 &mut self,
392 _: &editor::actions::Cancel,
393 _window: &mut Window,
394 cx: &mut Context<Self>,
395 ) {
396 self.last_error = None;
397
398 if self
399 .context
400 .update(cx, |context, cx| context.cancel_last_assist(cx))
401 {
402 return;
403 }
404
405 cx.propagate();
406 }
407
408 fn cycle_message_role(
409 &mut self,
410 _: &CycleMessageRole,
411 _window: &mut Window,
412 cx: &mut Context<Self>,
413 ) {
414 let cursors = self.cursors(cx);
415 self.context.update(cx, |context, cx| {
416 let messages = context
417 .messages_for_offsets(cursors, cx)
418 .into_iter()
419 .map(|message| message.id)
420 .collect();
421 context.cycle_message_roles(messages, cx)
422 });
423 }
424
425 fn cursors(&self, cx: &mut App) -> Vec<usize> {
426 let selections = self
427 .editor
428 .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
429 selections
430 .into_iter()
431 .map(|selection| selection.head())
432 .collect()
433 }
434
435 pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
436 if let Some(command) = self.slash_commands.command(name, cx) {
437 self.editor.update(cx, |editor, cx| {
438 editor.transact(window, cx, |editor, window, cx| {
439 editor
440 .change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
441 let snapshot = editor.buffer().read(cx).snapshot(cx);
442 let newest_cursor = editor.selections.newest::<Point>(cx).head();
443 if newest_cursor.column > 0
444 || snapshot
445 .chars_at(newest_cursor)
446 .next()
447 .map_or(false, |ch| ch != '\n')
448 {
449 editor.move_to_end_of_line(
450 &MoveToEndOfLine {
451 stop_at_soft_wraps: false,
452 },
453 window,
454 cx,
455 );
456 editor.newline(&Newline, window, cx);
457 }
458
459 editor.insert(&format!("/{name}"), window, cx);
460 if command.accepts_arguments() {
461 editor.insert(" ", window, cx);
462 editor.show_completions(&ShowCompletions::default(), window, cx);
463 }
464 });
465 });
466 if !command.requires_argument() {
467 self.confirm_command(&ConfirmCommand, window, cx);
468 }
469 }
470 }
471
472 pub fn confirm_command(
473 &mut self,
474 _: &ConfirmCommand,
475 window: &mut Window,
476 cx: &mut Context<Self>,
477 ) {
478 if self.editor.read(cx).has_visible_completions_menu() {
479 return;
480 }
481
482 let selections = self.editor.read(cx).selections.disjoint_anchors();
483 let mut commands_by_range = HashMap::default();
484 let workspace = self.workspace.clone();
485 self.context.update(cx, |context, cx| {
486 context.reparse(cx);
487 for selection in selections.iter() {
488 if let Some(command) =
489 context.pending_command_for_position(selection.head().text_anchor, cx)
490 {
491 commands_by_range
492 .entry(command.source_range.clone())
493 .or_insert_with(|| command.clone());
494 }
495 }
496 });
497
498 if commands_by_range.is_empty() {
499 cx.propagate();
500 } else {
501 for command in commands_by_range.into_values() {
502 self.run_command(
503 command.source_range,
504 &command.name,
505 &command.arguments,
506 true,
507 workspace.clone(),
508 window,
509 cx,
510 );
511 }
512 cx.stop_propagation();
513 }
514 }
515
516 #[allow(clippy::too_many_arguments)]
517 pub fn run_command(
518 &mut self,
519 command_range: Range<language::Anchor>,
520 name: &str,
521 arguments: &[String],
522 ensure_trailing_newline: bool,
523 workspace: WeakEntity<Workspace>,
524 window: &mut Window,
525 cx: &mut Context<Self>,
526 ) {
527 if let Some(command) = self.slash_commands.command(name, cx) {
528 let context = self.context.read(cx);
529 let sections = context
530 .slash_command_output_sections()
531 .into_iter()
532 .filter(|section| section.is_valid(context.buffer().read(cx)))
533 .cloned()
534 .collect::<Vec<_>>();
535 let snapshot = context.buffer().read(cx).snapshot();
536 let output = command.run(
537 arguments,
538 §ions,
539 snapshot,
540 workspace,
541 self.lsp_adapter_delegate.clone(),
542 window,
543 cx,
544 );
545 self.context.update(cx, |context, cx| {
546 context.insert_command_output(
547 command_range,
548 name,
549 output,
550 ensure_trailing_newline,
551 cx,
552 )
553 });
554 }
555 }
556
557 fn handle_context_event(
558 &mut self,
559 _: &Entity<AssistantContext>,
560 event: &ContextEvent,
561 window: &mut Window,
562 cx: &mut Context<Self>,
563 ) {
564 let context_editor = cx.entity().downgrade();
565
566 match event {
567 ContextEvent::MessagesEdited => {
568 self.update_message_headers(cx);
569 self.update_image_blocks(cx);
570 self.context.update(cx, |context, cx| {
571 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
572 });
573 }
574 ContextEvent::SummaryChanged => {
575 cx.emit(EditorEvent::TitleChanged);
576 self.context.update(cx, |context, cx| {
577 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
578 });
579 }
580 ContextEvent::StreamedCompletion => {
581 self.editor.update(cx, |editor, cx| {
582 if let Some(scroll_position) = self.scroll_position {
583 let snapshot = editor.snapshot(window, cx);
584 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
585 let scroll_top =
586 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
587 editor.set_scroll_position(
588 point(scroll_position.offset_before_cursor.x, scroll_top),
589 window,
590 cx,
591 );
592 }
593 });
594 }
595 ContextEvent::PatchesUpdated { removed, updated } => {
596 self.patches_updated(removed, updated, window, cx);
597 }
598 ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
599 self.editor.update(cx, |editor, cx| {
600 let buffer = editor.buffer().read(cx).snapshot(cx);
601 let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
602
603 editor.remove_creases(
604 removed
605 .iter()
606 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
607 cx,
608 );
609
610 let crease_ids = editor.insert_creases(
611 updated.iter().map(|command| {
612 let workspace = self.workspace.clone();
613 let confirm_command = Arc::new({
614 let context_editor = context_editor.clone();
615 let command = command.clone();
616 move |window: &mut Window, cx: &mut App| {
617 context_editor
618 .update(cx, |context_editor, cx| {
619 context_editor.run_command(
620 command.source_range.clone(),
621 &command.name,
622 &command.arguments,
623 false,
624 workspace.clone(),
625 window,
626 cx,
627 );
628 })
629 .ok();
630 }
631 });
632 let placeholder = FoldPlaceholder {
633 render: Arc::new(move |_, _, _| Empty.into_any()),
634 ..Default::default()
635 };
636 let render_toggle = {
637 let confirm_command = confirm_command.clone();
638 let command = command.clone();
639 move |row, _, _, _window: &mut Window, _cx: &mut App| {
640 render_pending_slash_command_gutter_decoration(
641 row,
642 &command.status,
643 confirm_command.clone(),
644 )
645 }
646 };
647 let render_trailer = {
648 let command = command.clone();
649 move |row, _unfold, _window: &mut Window, cx: &mut App| {
650 // TODO: In the future we should investigate how we can expose
651 // this as a hook on the `SlashCommand` trait so that we don't
652 // need to special-case it here.
653 if command.name == DocsSlashCommand::NAME {
654 return render_docs_slash_command_trailer(
655 row,
656 command.clone(),
657 cx,
658 );
659 }
660
661 Empty.into_any()
662 }
663 };
664
665 let start = buffer
666 .anchor_in_excerpt(excerpt_id, command.source_range.start)
667 .unwrap();
668 let end = buffer
669 .anchor_in_excerpt(excerpt_id, command.source_range.end)
670 .unwrap();
671 Crease::inline(start..end, placeholder, render_toggle, render_trailer)
672 }),
673 cx,
674 );
675
676 self.pending_slash_command_creases.extend(
677 updated
678 .iter()
679 .map(|command| command.source_range.clone())
680 .zip(crease_ids),
681 );
682 })
683 }
684 ContextEvent::InvokedSlashCommandChanged { command_id } => {
685 self.update_invoked_slash_command(*command_id, window, cx);
686 }
687 ContextEvent::SlashCommandOutputSectionAdded { section } => {
688 self.insert_slash_command_output_sections([section.clone()], false, window, cx);
689 }
690 ContextEvent::Operation(_) => {}
691 ContextEvent::ShowAssistError(error_message) => {
692 self.last_error = Some(AssistError::Message(error_message.clone()));
693 }
694 ContextEvent::ShowPaymentRequiredError => {
695 self.last_error = Some(AssistError::PaymentRequired);
696 }
697 ContextEvent::ShowMaxMonthlySpendReachedError => {
698 self.last_error = Some(AssistError::MaxMonthlySpendReached);
699 }
700 }
701 }
702
703 fn update_invoked_slash_command(
704 &mut self,
705 command_id: InvokedSlashCommandId,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) {
709 if let Some(invoked_slash_command) =
710 self.context.read(cx).invoked_slash_command(&command_id)
711 {
712 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
713 let run_commands_in_ranges = invoked_slash_command
714 .run_commands_in_ranges
715 .iter()
716 .cloned()
717 .collect::<Vec<_>>();
718 for range in run_commands_in_ranges {
719 let commands = self.context.update(cx, |context, cx| {
720 context.reparse(cx);
721 context
722 .pending_commands_for_range(range.clone(), cx)
723 .to_vec()
724 });
725
726 for command in commands {
727 self.run_command(
728 command.source_range,
729 &command.name,
730 &command.arguments,
731 false,
732 self.workspace.clone(),
733 window,
734 cx,
735 );
736 }
737 }
738 }
739 }
740
741 self.editor.update(cx, |editor, cx| {
742 if let Some(invoked_slash_command) =
743 self.context.read(cx).invoked_slash_command(&command_id)
744 {
745 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
746 let buffer = editor.buffer().read(cx).snapshot(cx);
747 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
748 buffer.as_singleton().unwrap();
749
750 let start = buffer
751 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
752 .unwrap();
753 let end = buffer
754 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
755 .unwrap();
756 editor.remove_folds_with_type(
757 &[start..end],
758 TypeId::of::<PendingSlashCommand>(),
759 false,
760 cx,
761 );
762
763 editor.remove_creases(
764 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
765 cx,
766 );
767 } else if let hash_map::Entry::Vacant(entry) =
768 self.invoked_slash_command_creases.entry(command_id)
769 {
770 let buffer = editor.buffer().read(cx).snapshot(cx);
771 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
772 buffer.as_singleton().unwrap();
773 let context = self.context.downgrade();
774 let crease_start = buffer
775 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
776 .unwrap();
777 let crease_end = buffer
778 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
779 .unwrap();
780 let crease = Crease::inline(
781 crease_start..crease_end,
782 invoked_slash_command_fold_placeholder(command_id, context),
783 fold_toggle("invoked-slash-command"),
784 |_row, _folded, _window, _cx| Empty.into_any(),
785 );
786 let crease_ids = editor.insert_creases([crease.clone()], cx);
787 editor.fold_creases(vec![crease], false, window, cx);
788 entry.insert(crease_ids[0]);
789 } else {
790 cx.notify()
791 }
792 } else {
793 editor.remove_creases(
794 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
795 cx,
796 );
797 cx.notify();
798 };
799 });
800 }
801
802 fn patches_updated(
803 &mut self,
804 removed: &Vec<Range<text::Anchor>>,
805 updated: &Vec<Range<text::Anchor>>,
806 window: &mut Window,
807 cx: &mut Context<ContextEditor>,
808 ) {
809 let this = cx.entity().downgrade();
810 let mut editors_to_close = Vec::new();
811
812 self.editor.update(cx, |editor, cx| {
813 let snapshot = editor.snapshot(window, cx);
814 let multibuffer = &snapshot.buffer_snapshot;
815 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
816
817 let mut removed_crease_ids = Vec::new();
818 let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
819 for range in removed {
820 if let Some(state) = self.patches.remove(range) {
821 let patch_start = multibuffer
822 .anchor_in_excerpt(excerpt_id, range.start)
823 .unwrap();
824 let patch_end = multibuffer
825 .anchor_in_excerpt(excerpt_id, range.end)
826 .unwrap();
827
828 editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
829 ranges_to_unfold.push(patch_start..patch_end);
830 removed_crease_ids.push(state.crease_id);
831 }
832 }
833 editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
834 editor.remove_creases(removed_crease_ids, cx);
835
836 for range in updated {
837 let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
838 continue;
839 };
840
841 let path_count = patch.path_count();
842 let patch_start = multibuffer
843 .anchor_in_excerpt(excerpt_id, patch.range.start)
844 .unwrap();
845 let patch_end = multibuffer
846 .anchor_in_excerpt(excerpt_id, patch.range.end)
847 .unwrap();
848 let render_block: RenderBlock = Arc::new({
849 let this = this.clone();
850 let patch_range = range.clone();
851 move |cx: &mut BlockContext| {
852 let max_width = cx.max_width;
853 let gutter_width = cx.gutter_dimensions.full_width();
854 let block_id = cx.block_id;
855 let selected = cx.selected;
856 let window = &mut cx.window;
857 this.update(cx.app, |this, cx| {
858 this.render_patch_block(
859 patch_range.clone(),
860 max_width,
861 gutter_width,
862 block_id,
863 selected,
864 window,
865 cx,
866 )
867 })
868 .ok()
869 .flatten()
870 .unwrap_or_else(|| Empty.into_any())
871 }
872 });
873
874 let height = path_count as u32 + 1;
875 let crease = Crease::block(
876 patch_start..patch_end,
877 height,
878 BlockStyle::Flex,
879 render_block.clone(),
880 );
881
882 let should_refold;
883 if let Some(state) = self.patches.get_mut(&range) {
884 if let Some(editor_state) = &state.editor {
885 if editor_state.opened_patch != patch {
886 state.update_task = Some({
887 let this = this.clone();
888 cx.spawn_in(window, |_, cx| async move {
889 Self::update_patch_editor(this.clone(), patch, cx)
890 .await
891 .log_err();
892 })
893 });
894 }
895 }
896
897 should_refold =
898 snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
899 } else {
900 let crease_id = editor.insert_creases([crease.clone()], cx)[0];
901 self.patches.insert(
902 range.clone(),
903 PatchViewState {
904 crease_id,
905 editor: None,
906 update_task: None,
907 },
908 );
909
910 should_refold = true;
911 }
912
913 if should_refold {
914 editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
915 editor.fold_creases(vec![crease], false, window, cx);
916 }
917 }
918 });
919
920 for editor in editors_to_close {
921 self.close_patch_editor(editor, window, cx);
922 }
923
924 self.update_active_patch(window, cx);
925 }
926
927 fn insert_slash_command_output_sections(
928 &mut self,
929 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
930 expand_result: bool,
931 window: &mut Window,
932 cx: &mut Context<Self>,
933 ) {
934 self.editor.update(cx, |editor, cx| {
935 let buffer = editor.buffer().read(cx).snapshot(cx);
936 let excerpt_id = *buffer.as_singleton().unwrap().0;
937 let mut buffer_rows_to_fold = BTreeSet::new();
938 let mut creases = Vec::new();
939 for section in sections {
940 let start = buffer
941 .anchor_in_excerpt(excerpt_id, section.range.start)
942 .unwrap();
943 let end = buffer
944 .anchor_in_excerpt(excerpt_id, section.range.end)
945 .unwrap();
946 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
947 buffer_rows_to_fold.insert(buffer_row);
948 creases.push(
949 Crease::inline(
950 start..end,
951 FoldPlaceholder {
952 render: render_fold_icon_button(
953 cx.entity().downgrade(),
954 section.icon,
955 section.label.clone(),
956 ),
957 merge_adjacent: false,
958 ..Default::default()
959 },
960 render_slash_command_output_toggle,
961 |_, _, _, _| Empty.into_any_element(),
962 )
963 .with_metadata(CreaseMetadata {
964 icon: section.icon,
965 label: section.label,
966 }),
967 );
968 }
969
970 editor.insert_creases(creases, cx);
971
972 if expand_result {
973 buffer_rows_to_fold.clear();
974 }
975 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
976 editor.fold_at(&FoldAt { buffer_row }, window, cx);
977 }
978 });
979 }
980
981 fn handle_editor_event(
982 &mut self,
983 _: &Entity<Editor>,
984 event: &EditorEvent,
985 window: &mut Window,
986 cx: &mut Context<Self>,
987 ) {
988 match event {
989 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
990 let cursor_scroll_position = self.cursor_scroll_position(window, cx);
991 if *autoscroll {
992 self.scroll_position = cursor_scroll_position;
993 } else if self.scroll_position != cursor_scroll_position {
994 self.scroll_position = None;
995 }
996 }
997 EditorEvent::SelectionsChanged { .. } => {
998 self.scroll_position = self.cursor_scroll_position(window, cx);
999 self.update_active_patch(window, cx);
1000 }
1001 _ => {}
1002 }
1003 cx.emit(event.clone());
1004 }
1005
1006 fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
1007 let patch = self.active_patch.as_ref()?;
1008 Some((patch.clone(), self.patches.get(&patch)?))
1009 }
1010
1011 fn update_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1012 let newest_cursor = self.editor.update(cx, |editor, cx| {
1013 editor.selections.newest::<Point>(cx).head()
1014 });
1015 let context = self.context.read(cx);
1016
1017 let new_patch = context.patch_containing(newest_cursor, cx).cloned();
1018
1019 if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
1020 return;
1021 }
1022
1023 if let Some(old_patch_range) = self.active_patch.take() {
1024 if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
1025 if let Some(state) = patch_state.editor.take() {
1026 if let Some(editor) = state.editor.upgrade() {
1027 self.close_patch_editor(editor, window, cx);
1028 }
1029 }
1030 }
1031 }
1032
1033 if let Some(new_patch) = new_patch {
1034 self.active_patch = Some(new_patch.range.clone());
1035
1036 if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
1037 let mut editor = None;
1038 if let Some(state) = &patch_state.editor {
1039 if let Some(opened_editor) = state.editor.upgrade() {
1040 editor = Some(opened_editor);
1041 }
1042 }
1043
1044 if let Some(editor) = editor {
1045 self.workspace
1046 .update(cx, |workspace, cx| {
1047 workspace.activate_item(&editor, true, false, window, cx);
1048 })
1049 .ok();
1050 } else {
1051 patch_state.update_task =
1052 Some(cx.spawn_in(window, move |this, cx| async move {
1053 Self::open_patch_editor(this, new_patch, cx).await.log_err();
1054 }));
1055 }
1056 }
1057 }
1058 }
1059
1060 fn close_patch_editor(
1061 &mut self,
1062 editor: Entity<ProposedChangesEditor>,
1063 window: &mut Window,
1064 cx: &mut Context<ContextEditor>,
1065 ) {
1066 self.workspace
1067 .update(cx, |workspace, cx| {
1068 if let Some(pane) = workspace.pane_for(&editor) {
1069 pane.update(cx, |pane, cx| {
1070 let item_id = editor.entity_id();
1071 if !editor.read(cx).focus_handle(cx).is_focused(window) {
1072 pane.close_item_by_id(item_id, SaveIntent::Skip, window, cx)
1073 .detach_and_log_err(cx);
1074 }
1075 });
1076 }
1077 })
1078 .ok();
1079 }
1080
1081 async fn open_patch_editor(
1082 this: WeakEntity<Self>,
1083 patch: AssistantPatch,
1084 mut cx: AsyncWindowContext,
1085 ) -> Result<()> {
1086 let project = this.read_with(&cx, |this, _| this.project.clone())?;
1087 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
1088
1089 let editor = cx.new_window_entity(|window, cx| {
1090 let editor = ProposedChangesEditor::new(
1091 patch.title.clone(),
1092 resolved_patch
1093 .edit_groups
1094 .iter()
1095 .map(|(buffer, groups)| ProposedChangeLocation {
1096 buffer: buffer.clone(),
1097 ranges: groups
1098 .iter()
1099 .map(|group| group.context_range.clone())
1100 .collect(),
1101 })
1102 .collect(),
1103 Some(project.clone()),
1104 window,
1105 cx,
1106 );
1107 resolved_patch.apply(&editor, cx);
1108 editor
1109 })?;
1110
1111 this.update(&mut cx, |this, _| {
1112 if let Some(patch_state) = this.patches.get_mut(&patch.range) {
1113 patch_state.editor = Some(PatchEditorState {
1114 editor: editor.downgrade(),
1115 opened_patch: patch,
1116 });
1117 patch_state.update_task.take();
1118 }
1119 })?;
1120 this.read_with(&cx, |this, _| this.workspace.clone())?
1121 .update_in(&mut cx, |workspace, window, cx| {
1122 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, window, cx)
1123 })
1124 .log_err();
1125
1126 Ok(())
1127 }
1128
1129 async fn update_patch_editor(
1130 this: WeakEntity<Self>,
1131 patch: AssistantPatch,
1132 mut cx: AsyncWindowContext,
1133 ) -> Result<()> {
1134 let project = this.update(&mut cx, |this, _| this.project.clone())?;
1135 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
1136 this.update_in(&mut cx, |this, window, cx| {
1137 let patch_state = this.patches.get_mut(&patch.range)?;
1138
1139 let locations = resolved_patch
1140 .edit_groups
1141 .iter()
1142 .map(|(buffer, groups)| ProposedChangeLocation {
1143 buffer: buffer.clone(),
1144 ranges: groups
1145 .iter()
1146 .map(|group| group.context_range.clone())
1147 .collect(),
1148 })
1149 .collect();
1150
1151 if let Some(state) = &mut patch_state.editor {
1152 if let Some(editor) = state.editor.upgrade() {
1153 editor.update(cx, |editor, cx| {
1154 editor.set_title(patch.title.clone(), cx);
1155 editor.reset_locations(locations, window, cx);
1156 resolved_patch.apply(editor, cx);
1157 });
1158
1159 state.opened_patch = patch;
1160 } else {
1161 patch_state.editor.take();
1162 }
1163 }
1164 patch_state.update_task.take();
1165
1166 Some(())
1167 })?;
1168 Ok(())
1169 }
1170
1171 fn handle_editor_search_event(
1172 &mut self,
1173 _: &Entity<Editor>,
1174 event: &SearchEvent,
1175 _window: &mut Window,
1176 cx: &mut Context<Self>,
1177 ) {
1178 cx.emit(event.clone());
1179 }
1180
1181 fn cursor_scroll_position(
1182 &self,
1183 window: &mut Window,
1184 cx: &mut Context<Self>,
1185 ) -> Option<ScrollPosition> {
1186 self.editor.update(cx, |editor, cx| {
1187 let snapshot = editor.snapshot(window, cx);
1188 let cursor = editor.selections.newest_anchor().head();
1189 let cursor_row = cursor
1190 .to_display_point(&snapshot.display_snapshot)
1191 .row()
1192 .as_f32();
1193 let scroll_position = editor
1194 .scroll_manager
1195 .anchor()
1196 .scroll_position(&snapshot.display_snapshot);
1197
1198 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
1199 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
1200 Some(ScrollPosition {
1201 cursor,
1202 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
1203 })
1204 } else {
1205 None
1206 }
1207 })
1208 }
1209
1210 fn esc_kbd(cx: &App) -> Div {
1211 let colors = cx.theme().colors().clone();
1212
1213 h_flex()
1214 .items_center()
1215 .gap_1()
1216 .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
1217 .text_size(TextSize::XSmall.rems(cx))
1218 .text_color(colors.text_muted)
1219 .child("Press")
1220 .child(
1221 h_flex()
1222 .rounded_md()
1223 .px_1()
1224 .mr_0p5()
1225 .border_1()
1226 .border_color(colors.border_variant.alpha(0.6))
1227 .bg(colors.element_background.alpha(0.6))
1228 .child("esc"),
1229 )
1230 .child("to cancel")
1231 }
1232
1233 fn update_message_headers(&mut self, cx: &mut Context<Self>) {
1234 self.editor.update(cx, |editor, cx| {
1235 let buffer = editor.buffer().read(cx).snapshot(cx);
1236
1237 let excerpt_id = *buffer.as_singleton().unwrap().0;
1238 let mut old_blocks = std::mem::take(&mut self.blocks);
1239 let mut blocks_to_remove: HashMap<_, _> = old_blocks
1240 .iter()
1241 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
1242 .collect();
1243 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
1244
1245 let render_block = |message: MessageMetadata| -> RenderBlock {
1246 Arc::new({
1247 let context = self.context.clone();
1248
1249 move |cx| {
1250 let message_id = MessageId(message.timestamp);
1251 let llm_loading = message.role == Role::Assistant
1252 && message.status == MessageStatus::Pending;
1253
1254 let (label, spinner, note) = match message.role {
1255 Role::User => (
1256 Label::new("You").color(Color::Default).into_any_element(),
1257 None,
1258 None,
1259 ),
1260 Role::Assistant => {
1261 let base_label = Label::new("Assistant").color(Color::Info);
1262 let mut spinner = None;
1263 let mut note = None;
1264 let animated_label = if llm_loading {
1265 base_label
1266 .with_animation(
1267 "pulsating-label",
1268 Animation::new(Duration::from_secs(2))
1269 .repeat()
1270 .with_easing(pulsating_between(0.4, 0.8)),
1271 |label, delta| label.alpha(delta),
1272 )
1273 .into_any_element()
1274 } else {
1275 base_label.into_any_element()
1276 };
1277 if llm_loading {
1278 spinner = Some(
1279 Icon::new(IconName::ArrowCircle)
1280 .size(IconSize::XSmall)
1281 .color(Color::Info)
1282 .with_animation(
1283 "arrow-circle",
1284 Animation::new(Duration::from_secs(2)).repeat(),
1285 |icon, delta| {
1286 icon.transform(Transformation::rotate(
1287 percentage(delta),
1288 ))
1289 },
1290 )
1291 .into_any_element(),
1292 );
1293 note = Some(Self::esc_kbd(cx).into_any_element());
1294 }
1295 (animated_label, spinner, note)
1296 }
1297 Role::System => (
1298 Label::new("System")
1299 .color(Color::Warning)
1300 .into_any_element(),
1301 None,
1302 None,
1303 ),
1304 };
1305
1306 let sender = h_flex()
1307 .items_center()
1308 .gap_2p5()
1309 .child(
1310 ButtonLike::new("role")
1311 .style(ButtonStyle::Filled)
1312 .child(
1313 h_flex()
1314 .items_center()
1315 .gap_1p5()
1316 .child(label)
1317 .children(spinner),
1318 )
1319 .tooltip(|window, cx| {
1320 Tooltip::with_meta(
1321 "Toggle message role",
1322 None,
1323 "Available roles: You (User), Assistant, System",
1324 window,
1325 cx,
1326 )
1327 })
1328 .on_click({
1329 let context = context.clone();
1330 move |_, _window, cx| {
1331 context.update(cx, |context, cx| {
1332 context.cycle_message_roles(
1333 HashSet::from_iter(Some(message_id)),
1334 cx,
1335 )
1336 })
1337 }
1338 }),
1339 )
1340 .children(note);
1341
1342 h_flex()
1343 .id(("message_header", message_id.as_u64()))
1344 .pl(cx.gutter_dimensions.full_width())
1345 .h_11()
1346 .w_full()
1347 .relative()
1348 .gap_1p5()
1349 .child(sender)
1350 .children(match &message.cache {
1351 Some(cache) if cache.is_final_anchor => match cache.status {
1352 CacheStatus::Cached => Some(
1353 div()
1354 .id("cached")
1355 .child(
1356 Icon::new(IconName::DatabaseZap)
1357 .size(IconSize::XSmall)
1358 .color(Color::Hint),
1359 )
1360 .tooltip(|window, cx| {
1361 Tooltip::with_meta(
1362 "Context Cached",
1363 None,
1364 "Large messages cached to optimize performance",
1365 window,
1366 cx,
1367 )
1368 })
1369 .into_any_element(),
1370 ),
1371 CacheStatus::Pending => Some(
1372 div()
1373 .child(
1374 Icon::new(IconName::Ellipsis)
1375 .size(IconSize::XSmall)
1376 .color(Color::Hint),
1377 )
1378 .into_any_element(),
1379 ),
1380 },
1381 _ => None,
1382 })
1383 .children(match &message.status {
1384 MessageStatus::Error(error) => Some(
1385 Button::new("show-error", "Error")
1386 .color(Color::Error)
1387 .selected_label_color(Color::Error)
1388 .selected_icon_color(Color::Error)
1389 .icon(IconName::XCircle)
1390 .icon_color(Color::Error)
1391 .icon_size(IconSize::XSmall)
1392 .icon_position(IconPosition::Start)
1393 .tooltip(Tooltip::text("View Details"))
1394 .on_click({
1395 let context = context.clone();
1396 let error = error.clone();
1397 move |_, _window, cx| {
1398 context.update(cx, |_, cx| {
1399 cx.emit(ContextEvent::ShowAssistError(
1400 error.clone(),
1401 ));
1402 });
1403 }
1404 })
1405 .into_any_element(),
1406 ),
1407 MessageStatus::Canceled => Some(
1408 h_flex()
1409 .gap_1()
1410 .items_center()
1411 .child(
1412 Icon::new(IconName::XCircle)
1413 .color(Color::Disabled)
1414 .size(IconSize::XSmall),
1415 )
1416 .child(
1417 Label::new("Canceled")
1418 .size(LabelSize::Small)
1419 .color(Color::Disabled),
1420 )
1421 .into_any_element(),
1422 ),
1423 _ => None,
1424 })
1425 .into_any_element()
1426 }
1427 })
1428 };
1429 let create_block_properties = |message: &Message| BlockProperties {
1430 height: 2,
1431 style: BlockStyle::Sticky,
1432 placement: BlockPlacement::Above(
1433 buffer
1434 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
1435 .unwrap(),
1436 ),
1437 priority: usize::MAX,
1438 render: render_block(MessageMetadata::from(message)),
1439 };
1440 let mut new_blocks = vec![];
1441 let mut block_index_to_message = vec![];
1442 for message in self.context.read(cx).messages(cx) {
1443 if let Some(_) = blocks_to_remove.remove(&message.id) {
1444 // This is an old message that we might modify.
1445 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
1446 debug_assert!(
1447 false,
1448 "old_blocks should contain a message_id we've just removed."
1449 );
1450 continue;
1451 };
1452 // Should we modify it?
1453 let message_meta = MessageMetadata::from(&message);
1454 if meta != &message_meta {
1455 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
1456 *meta = message_meta;
1457 }
1458 } else {
1459 // This is a new message.
1460 new_blocks.push(create_block_properties(&message));
1461 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
1462 }
1463 }
1464 editor.replace_blocks(blocks_to_replace, None, cx);
1465 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
1466
1467 let ids = editor.insert_blocks(new_blocks, None, cx);
1468 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
1469 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
1470 ));
1471 self.blocks = old_blocks;
1472 });
1473 }
1474
1475 /// Returns either the selected text, or the content of the Markdown code
1476 /// block surrounding the cursor.
1477 fn get_selection_or_code_block(
1478 context_editor_view: &Entity<ContextEditor>,
1479 cx: &mut Context<Workspace>,
1480 ) -> Option<(String, bool)> {
1481 const CODE_FENCE_DELIMITER: &'static str = "```";
1482
1483 let context_editor = context_editor_view.read(cx).editor.clone();
1484 context_editor.update(cx, |context_editor, cx| {
1485 if context_editor.selections.newest::<Point>(cx).is_empty() {
1486 let snapshot = context_editor.buffer().read(cx).snapshot(cx);
1487 let (_, _, snapshot) = snapshot.as_singleton()?;
1488
1489 let head = context_editor.selections.newest::<Point>(cx).head();
1490 let offset = snapshot.point_to_offset(head);
1491
1492 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
1493 let mut text = snapshot
1494 .text_for_range(surrounding_code_block_range)
1495 .collect::<String>();
1496
1497 // If there is no newline trailing the closing three-backticks, then
1498 // tree-sitter-md extends the range of the content node to include
1499 // the backticks.
1500 if text.ends_with(CODE_FENCE_DELIMITER) {
1501 text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
1502 }
1503
1504 (!text.is_empty()).then_some((text, true))
1505 } else {
1506 let selection = context_editor.selections.newest_adjusted(cx);
1507 let buffer = context_editor.buffer().read(cx).snapshot(cx);
1508 let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
1509
1510 (!selected_text.is_empty()).then_some((selected_text, false))
1511 }
1512 })
1513 }
1514
1515 pub fn insert_selection(
1516 workspace: &mut Workspace,
1517 _: &InsertIntoEditor,
1518 window: &mut Window,
1519 cx: &mut Context<Workspace>,
1520 ) {
1521 let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
1522 return;
1523 };
1524 let Some(context_editor_view) =
1525 assistant_panel_delegate.active_context_editor(workspace, window, cx)
1526 else {
1527 return;
1528 };
1529 let Some(active_editor_view) = workspace
1530 .active_item(cx)
1531 .and_then(|item| item.act_as::<Editor>(cx))
1532 else {
1533 return;
1534 };
1535
1536 if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
1537 active_editor_view.update(cx, |editor, cx| {
1538 editor.insert(&text, window, cx);
1539 editor.focus_handle(cx).focus(window);
1540 })
1541 }
1542 }
1543
1544 pub fn copy_code(
1545 workspace: &mut Workspace,
1546 _: &CopyCode,
1547 window: &mut Window,
1548 cx: &mut Context<Workspace>,
1549 ) {
1550 let result = maybe!({
1551 let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
1552 let context_editor_view =
1553 assistant_panel_delegate.active_context_editor(workspace, window, cx)?;
1554 Self::get_selection_or_code_block(&context_editor_view, cx)
1555 });
1556 let Some((text, is_code_block)) = result else {
1557 return;
1558 };
1559
1560 cx.write_to_clipboard(ClipboardItem::new_string(text));
1561
1562 struct CopyToClipboardToast;
1563 workspace.show_toast(
1564 Toast::new(
1565 NotificationId::unique::<CopyToClipboardToast>(),
1566 format!(
1567 "{} copied to clipboard.",
1568 if is_code_block {
1569 "Code block"
1570 } else {
1571 "Selection"
1572 }
1573 ),
1574 )
1575 .autohide(),
1576 cx,
1577 );
1578 }
1579
1580 pub fn insert_dragged_files(
1581 workspace: &mut Workspace,
1582 action: &InsertDraggedFiles,
1583 window: &mut Window,
1584 cx: &mut Context<Workspace>,
1585 ) {
1586 let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
1587 return;
1588 };
1589 let Some(context_editor_view) =
1590 assistant_panel_delegate.active_context_editor(workspace, window, cx)
1591 else {
1592 return;
1593 };
1594
1595 let project = workspace.project().clone();
1596
1597 let paths = match action {
1598 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
1599 InsertDraggedFiles::ExternalFiles(paths) => {
1600 let tasks = paths
1601 .clone()
1602 .into_iter()
1603 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
1604 .collect::<Vec<_>>();
1605
1606 cx.spawn(move |_, cx| async move {
1607 let mut paths = vec![];
1608 let mut worktrees = vec![];
1609
1610 let opened_paths = futures::future::join_all(tasks).await;
1611 for (worktree, project_path) in opened_paths.into_iter().flatten() {
1612 let Ok(worktree_root_name) =
1613 worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
1614 else {
1615 continue;
1616 };
1617
1618 let mut full_path = PathBuf::from(worktree_root_name.clone());
1619 full_path.push(&project_path.path);
1620 paths.push(full_path);
1621 worktrees.push(worktree);
1622 }
1623
1624 (paths, worktrees)
1625 })
1626 }
1627 };
1628
1629 window
1630 .spawn(cx, |mut cx| async move {
1631 let (paths, dragged_file_worktrees) = paths.await;
1632 let cmd_name = FileSlashCommand.name();
1633
1634 context_editor_view
1635 .update_in(&mut cx, |context_editor, window, cx| {
1636 let file_argument = paths
1637 .into_iter()
1638 .map(|path| path.to_string_lossy().to_string())
1639 .collect::<Vec<_>>()
1640 .join(" ");
1641
1642 context_editor.editor.update(cx, |editor, cx| {
1643 editor.insert("\n", window, cx);
1644 editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
1645 });
1646
1647 context_editor.confirm_command(&ConfirmCommand, window, cx);
1648
1649 context_editor
1650 .dragged_file_worktrees
1651 .extend(dragged_file_worktrees);
1652 })
1653 .log_err();
1654 })
1655 .detach();
1656 }
1657
1658 pub fn quote_selection(
1659 workspace: &mut Workspace,
1660 _: &QuoteSelection,
1661 window: &mut Window,
1662 cx: &mut Context<Workspace>,
1663 ) {
1664 let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
1665 return;
1666 };
1667
1668 let Some(creases) = selections_creases(workspace, cx) else {
1669 return;
1670 };
1671
1672 if creases.is_empty() {
1673 return;
1674 }
1675
1676 assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
1677 }
1678
1679 pub fn quote_creases(
1680 &mut self,
1681 creases: Vec<(String, String)>,
1682 window: &mut Window,
1683 cx: &mut Context<Self>,
1684 ) {
1685 self.editor.update(cx, |editor, cx| {
1686 editor.insert("\n", window, cx);
1687 for (text, crease_title) in creases {
1688 let point = editor.selections.newest::<Point>(cx).head();
1689 let start_row = MultiBufferRow(point.row);
1690
1691 editor.insert(&text, window, cx);
1692
1693 let snapshot = editor.buffer().read(cx).snapshot(cx);
1694 let anchor_before = snapshot.anchor_after(point);
1695 let anchor_after = editor
1696 .selections
1697 .newest_anchor()
1698 .head()
1699 .bias_left(&snapshot);
1700
1701 editor.insert("\n", window, cx);
1702
1703 let fold_placeholder =
1704 quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1705 let crease = Crease::inline(
1706 anchor_before..anchor_after,
1707 fold_placeholder,
1708 render_quote_selection_output_toggle,
1709 |_, _, _, _| Empty.into_any(),
1710 );
1711 editor.insert_creases(vec![crease], cx);
1712 editor.fold_at(
1713 &FoldAt {
1714 buffer_row: start_row,
1715 },
1716 window,
1717 cx,
1718 );
1719 }
1720 })
1721 }
1722
1723 fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
1724 if self.editor.read(cx).selections.count() == 1 {
1725 let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
1726 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1727 copied_text,
1728 metadata,
1729 ));
1730 cx.stop_propagation();
1731 return;
1732 }
1733
1734 cx.propagate();
1735 }
1736
1737 fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
1738 if self.editor.read(cx).selections.count() == 1 {
1739 let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
1740
1741 self.editor.update(cx, |editor, cx| {
1742 editor.transact(window, cx, |this, window, cx| {
1743 this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1744 s.select(selections);
1745 });
1746 this.insert("", window, cx);
1747 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1748 copied_text,
1749 metadata,
1750 ));
1751 });
1752 });
1753
1754 cx.stop_propagation();
1755 return;
1756 }
1757
1758 cx.propagate();
1759 }
1760
1761 fn get_clipboard_contents(
1762 &mut self,
1763 cx: &mut Context<Self>,
1764 ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
1765 let (selection, creases) = self.editor.update(cx, |editor, cx| {
1766 let mut selection = editor.selections.newest_adjusted(cx);
1767 let snapshot = editor.buffer().read(cx).snapshot(cx);
1768
1769 selection.goal = SelectionGoal::None;
1770
1771 let selection_start = snapshot.point_to_offset(selection.start);
1772
1773 (
1774 selection.map(|point| snapshot.point_to_offset(point)),
1775 editor.display_map.update(cx, |display_map, cx| {
1776 display_map
1777 .snapshot(cx)
1778 .crease_snapshot
1779 .creases_in_range(
1780 MultiBufferRow(selection.start.row)
1781 ..MultiBufferRow(selection.end.row + 1),
1782 &snapshot,
1783 )
1784 .filter_map(|crease| {
1785 if let Crease::Inline {
1786 range, metadata, ..
1787 } = &crease
1788 {
1789 let metadata = metadata.as_ref()?;
1790 let start = range
1791 .start
1792 .to_offset(&snapshot)
1793 .saturating_sub(selection_start);
1794 let end = range
1795 .end
1796 .to_offset(&snapshot)
1797 .saturating_sub(selection_start);
1798
1799 let range_relative_to_selection = start..end;
1800 if !range_relative_to_selection.is_empty() {
1801 return Some(SelectedCreaseMetadata {
1802 range_relative_to_selection,
1803 crease: metadata.clone(),
1804 });
1805 }
1806 }
1807 None
1808 })
1809 .collect::<Vec<_>>()
1810 }),
1811 )
1812 });
1813
1814 let context = self.context.read(cx);
1815
1816 let mut text = String::new();
1817 for message in context.messages(cx) {
1818 if message.offset_range.start >= selection.range().end {
1819 break;
1820 } else if message.offset_range.end >= selection.range().start {
1821 let range = cmp::max(message.offset_range.start, selection.range().start)
1822 ..cmp::min(message.offset_range.end, selection.range().end);
1823 if !range.is_empty() {
1824 for chunk in context.buffer().read(cx).text_for_range(range) {
1825 text.push_str(chunk);
1826 }
1827 if message.offset_range.end < selection.range().end {
1828 text.push('\n');
1829 }
1830 }
1831 }
1832 }
1833
1834 (text, CopyMetadata { creases }, vec![selection])
1835 }
1836
1837 fn paste(
1838 &mut self,
1839 action: &editor::actions::Paste,
1840 window: &mut Window,
1841 cx: &mut Context<Self>,
1842 ) {
1843 cx.stop_propagation();
1844
1845 let images = if let Some(item) = cx.read_from_clipboard() {
1846 item.into_entries()
1847 .filter_map(|entry| {
1848 if let ClipboardEntry::Image(image) = entry {
1849 Some(image)
1850 } else {
1851 None
1852 }
1853 })
1854 .collect()
1855 } else {
1856 Vec::new()
1857 };
1858
1859 let metadata = if let Some(item) = cx.read_from_clipboard() {
1860 item.entries().first().and_then(|entry| {
1861 if let ClipboardEntry::String(text) = entry {
1862 text.metadata_json::<CopyMetadata>()
1863 } else {
1864 None
1865 }
1866 })
1867 } else {
1868 None
1869 };
1870
1871 if images.is_empty() {
1872 self.editor.update(cx, |editor, cx| {
1873 let paste_position = editor.selections.newest::<usize>(cx).head();
1874 editor.paste(action, window, cx);
1875
1876 if let Some(metadata) = metadata {
1877 let buffer = editor.buffer().read(cx).snapshot(cx);
1878
1879 let mut buffer_rows_to_fold = BTreeSet::new();
1880 let weak_editor = cx.entity().downgrade();
1881 editor.insert_creases(
1882 metadata.creases.into_iter().map(|metadata| {
1883 let start = buffer.anchor_after(
1884 paste_position + metadata.range_relative_to_selection.start,
1885 );
1886 let end = buffer.anchor_before(
1887 paste_position + metadata.range_relative_to_selection.end,
1888 );
1889
1890 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1891 buffer_rows_to_fold.insert(buffer_row);
1892 Crease::inline(
1893 start..end,
1894 FoldPlaceholder {
1895 render: render_fold_icon_button(
1896 weak_editor.clone(),
1897 metadata.crease.icon,
1898 metadata.crease.label.clone(),
1899 ),
1900 ..Default::default()
1901 },
1902 render_slash_command_output_toggle,
1903 |_, _, _, _| Empty.into_any(),
1904 )
1905 .with_metadata(metadata.crease.clone())
1906 }),
1907 cx,
1908 );
1909 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1910 editor.fold_at(&FoldAt { buffer_row }, window, cx);
1911 }
1912 }
1913 });
1914 } else {
1915 let mut image_positions = Vec::new();
1916 self.editor.update(cx, |editor, cx| {
1917 editor.transact(window, cx, |editor, _window, cx| {
1918 let edits = editor
1919 .selections
1920 .all::<usize>(cx)
1921 .into_iter()
1922 .map(|selection| (selection.start..selection.end, "\n"));
1923 editor.edit(edits, cx);
1924
1925 let snapshot = editor.buffer().read(cx).snapshot(cx);
1926 for selection in editor.selections.all::<usize>(cx) {
1927 image_positions.push(snapshot.anchor_before(selection.end));
1928 }
1929 });
1930 });
1931
1932 self.context.update(cx, |context, cx| {
1933 for image in images {
1934 let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
1935 else {
1936 continue;
1937 };
1938 let image_id = image.id();
1939 let image_task = LanguageModelImage::from_image(image, cx).shared();
1940
1941 for image_position in image_positions.iter() {
1942 context.insert_content(
1943 Content::Image {
1944 anchor: image_position.text_anchor,
1945 image_id,
1946 image: image_task.clone(),
1947 render_image: render_image.clone(),
1948 },
1949 cx,
1950 );
1951 }
1952 }
1953 });
1954 }
1955 }
1956
1957 fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
1958 self.editor.update(cx, |editor, cx| {
1959 let buffer = editor.buffer().read(cx).snapshot(cx);
1960 let excerpt_id = *buffer.as_singleton().unwrap().0;
1961 let old_blocks = std::mem::take(&mut self.image_blocks);
1962 let new_blocks = self
1963 .context
1964 .read(cx)
1965 .contents(cx)
1966 .map(
1967 |Content::Image {
1968 anchor,
1969 render_image,
1970 ..
1971 }| (anchor, render_image),
1972 )
1973 .filter_map(|(anchor, render_image)| {
1974 const MAX_HEIGHT_IN_LINES: u32 = 8;
1975 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
1976 let image = render_image.clone();
1977 anchor.is_valid(&buffer).then(|| BlockProperties {
1978 placement: BlockPlacement::Above(anchor),
1979 height: MAX_HEIGHT_IN_LINES,
1980 style: BlockStyle::Sticky,
1981 render: Arc::new(move |cx| {
1982 let image_size = size_for_image(
1983 &image,
1984 size(
1985 cx.max_width - cx.gutter_dimensions.full_width(),
1986 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
1987 ),
1988 );
1989 h_flex()
1990 .pl(cx.gutter_dimensions.full_width())
1991 .child(
1992 img(image.clone())
1993 .object_fit(gpui::ObjectFit::ScaleDown)
1994 .w(image_size.width)
1995 .h(image_size.height),
1996 )
1997 .into_any_element()
1998 }),
1999 priority: 0,
2000 })
2001 })
2002 .collect::<Vec<_>>();
2003
2004 editor.remove_blocks(old_blocks, None, cx);
2005 let ids = editor.insert_blocks(new_blocks, None, cx);
2006 self.image_blocks = HashSet::from_iter(ids);
2007 });
2008 }
2009
2010 fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
2011 self.context.update(cx, |context, cx| {
2012 let selections = self.editor.read(cx).selections.disjoint_anchors();
2013 for selection in selections.as_ref() {
2014 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2015 let range = selection
2016 .map(|endpoint| endpoint.to_offset(&buffer))
2017 .range();
2018 context.split_message(range, cx);
2019 }
2020 });
2021 }
2022
2023 fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
2024 self.context.update(cx, |context, cx| {
2025 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2026 });
2027 }
2028
2029 pub fn title(&self, cx: &App) -> Cow<str> {
2030 self.context
2031 .read(cx)
2032 .summary()
2033 .map(|summary| summary.text.clone())
2034 .map(Cow::Owned)
2035 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2036 }
2037
2038 #[allow(clippy::too_many_arguments)]
2039 fn render_patch_block(
2040 &mut self,
2041 range: Range<text::Anchor>,
2042 max_width: Pixels,
2043 gutter_width: Pixels,
2044 id: BlockId,
2045 selected: bool,
2046 window: &mut Window,
2047 cx: &mut Context<Self>,
2048 ) -> Option<AnyElement> {
2049 let snapshot = self
2050 .editor
2051 .update(cx, |editor, cx| editor.snapshot(window, cx));
2052 let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
2053 let excerpt_id = *excerpt_id;
2054 let anchor = snapshot
2055 .buffer_snapshot
2056 .anchor_in_excerpt(excerpt_id, range.start)
2057 .unwrap();
2058
2059 let theme = cx.theme().clone();
2060 let patch = self.context.read(cx).patch_for_range(&range, cx)?;
2061 let paths = patch
2062 .paths()
2063 .map(|p| SharedString::from(p.to_string()))
2064 .collect::<BTreeSet<_>>();
2065
2066 Some(
2067 v_flex()
2068 .id(id)
2069 .bg(theme.colors().editor_background)
2070 .ml(gutter_width)
2071 .pb_1()
2072 .w(max_width - gutter_width)
2073 .rounded_md()
2074 .border_1()
2075 .border_color(theme.colors().border_variant)
2076 .overflow_hidden()
2077 .hover(|style| style.border_color(theme.colors().text_accent))
2078 .when(selected, |this| {
2079 this.border_color(theme.colors().text_accent)
2080 })
2081 .cursor(CursorStyle::PointingHand)
2082 .on_click(cx.listener(move |this, _, window, cx| {
2083 this.editor.update(cx, |editor, cx| {
2084 editor.change_selections(None, window, cx, |selections| {
2085 selections.select_ranges(vec![anchor..anchor]);
2086 });
2087 });
2088 this.focus_active_patch(window, cx);
2089 }))
2090 .child(
2091 div()
2092 .px_2()
2093 .py_1()
2094 .overflow_hidden()
2095 .text_ellipsis()
2096 .border_b_1()
2097 .border_color(theme.colors().border_variant)
2098 .bg(theme.colors().element_background)
2099 .child(
2100 Label::new(patch.title.clone())
2101 .size(LabelSize::Small)
2102 .color(Color::Muted),
2103 ),
2104 )
2105 .children(paths.into_iter().map(|path| {
2106 h_flex()
2107 .px_2()
2108 .pt_1()
2109 .gap_1p5()
2110 .child(Icon::new(IconName::File).size(IconSize::Small))
2111 .child(Label::new(path).size(LabelSize::Small))
2112 }))
2113 .when(patch.status == AssistantPatchStatus::Pending, |div| {
2114 div.child(
2115 h_flex()
2116 .pt_1()
2117 .px_2()
2118 .gap_1()
2119 .child(
2120 Icon::new(IconName::ArrowCircle)
2121 .size(IconSize::XSmall)
2122 .color(Color::Muted)
2123 .with_animation(
2124 "arrow-circle",
2125 Animation::new(Duration::from_secs(2)).repeat(),
2126 |icon, delta| {
2127 icon.transform(Transformation::rotate(percentage(
2128 delta,
2129 )))
2130 },
2131 ),
2132 )
2133 .child(
2134 Label::new("Generating…")
2135 .color(Color::Muted)
2136 .size(LabelSize::Small)
2137 .with_animation(
2138 "pulsating-label",
2139 Animation::new(Duration::from_secs(2))
2140 .repeat()
2141 .with_easing(pulsating_between(0.4, 0.8)),
2142 |label, delta| label.alpha(delta),
2143 ),
2144 ),
2145 )
2146 })
2147 .into_any(),
2148 )
2149 }
2150
2151 fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2152 // This was previously gated behind the `zed-pro` feature flag. Since we
2153 // aren't planning to ship that right now, we're just hard-coding this
2154 // value to not show the nudge.
2155 let nudge = Some(false);
2156
2157 if nudge.map_or(false, |value| value) {
2158 Some(
2159 h_flex()
2160 .p_3()
2161 .border_b_1()
2162 .border_color(cx.theme().colors().border_variant)
2163 .bg(cx.theme().colors().editor_background)
2164 .justify_between()
2165 .child(
2166 h_flex()
2167 .gap_3()
2168 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
2169 .child(Label::new("Zed AI is here! Get started by signing in →")),
2170 )
2171 .child(
2172 Button::new("sign-in", "Sign in")
2173 .size(ButtonSize::Compact)
2174 .style(ButtonStyle::Filled)
2175 .on_click(cx.listener(|this, _event, _window, cx| {
2176 let client = this
2177 .workspace
2178 .update(cx, |workspace, _| workspace.client().clone())
2179 .log_err();
2180
2181 if let Some(client) = client {
2182 cx.spawn(|this, mut cx| async move {
2183 client.authenticate_and_connect(true, &mut cx).await?;
2184 this.update(&mut cx, |_, cx| cx.notify())
2185 })
2186 .detach_and_log_err(cx)
2187 }
2188 })),
2189 )
2190 .into_any_element(),
2191 )
2192 } else if let Some(configuration_error) = configuration_error(cx) {
2193 let label = match configuration_error {
2194 ConfigurationError::NoProvider => "No LLM provider selected.",
2195 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
2196 ConfigurationError::ProviderPendingTermsAcceptance(_) => {
2197 "LLM provider requires accepting the Terms of Service."
2198 }
2199 };
2200 Some(
2201 h_flex()
2202 .px_3()
2203 .py_2()
2204 .border_b_1()
2205 .border_color(cx.theme().colors().border_variant)
2206 .bg(cx.theme().colors().editor_background)
2207 .justify_between()
2208 .child(
2209 h_flex()
2210 .gap_3()
2211 .child(
2212 Icon::new(IconName::Warning)
2213 .size(IconSize::Small)
2214 .color(Color::Warning),
2215 )
2216 .child(Label::new(label)),
2217 )
2218 .child(
2219 Button::new("open-configuration", "Configure Providers")
2220 .size(ButtonSize::Compact)
2221 .icon(Some(IconName::SlidersVertical))
2222 .icon_size(IconSize::Small)
2223 .icon_position(IconPosition::Start)
2224 .style(ButtonStyle::Filled)
2225 .on_click({
2226 let focus_handle = self.focus_handle(cx).clone();
2227 move |_event, window, cx| {
2228 focus_handle.dispatch_action(&ShowConfiguration, window, cx);
2229 }
2230 }),
2231 )
2232 .into_any_element(),
2233 )
2234 } else {
2235 None
2236 }
2237 }
2238
2239 fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2240 let focus_handle = self.focus_handle(cx).clone();
2241
2242 let (style, tooltip) = match token_state(&self.context, cx) {
2243 Some(TokenState::NoTokensLeft { .. }) => (
2244 ButtonStyle::Tinted(TintColor::Error),
2245 Some(Tooltip::text("Token limit reached")(window, cx)),
2246 ),
2247 Some(TokenState::HasMoreTokens {
2248 over_warn_threshold,
2249 ..
2250 }) => {
2251 let (style, tooltip) = if over_warn_threshold {
2252 (
2253 ButtonStyle::Tinted(TintColor::Warning),
2254 Some(Tooltip::text("Token limit is close to exhaustion")(
2255 window, cx,
2256 )),
2257 )
2258 } else {
2259 (ButtonStyle::Filled, None)
2260 };
2261 (style, tooltip)
2262 }
2263 None => (ButtonStyle::Filled, None),
2264 };
2265
2266 let provider = LanguageModelRegistry::read_global(cx).active_provider();
2267
2268 let has_configuration_error = configuration_error(cx).is_some();
2269 let needs_to_accept_terms = self.show_accept_terms
2270 && provider
2271 .as_ref()
2272 .map_or(false, |provider| provider.must_accept_terms(cx));
2273 let disabled = has_configuration_error || needs_to_accept_terms;
2274
2275 ButtonLike::new("send_button")
2276 .disabled(disabled)
2277 .style(style)
2278 .when_some(tooltip, |button, tooltip| {
2279 button.tooltip(move |_, _| tooltip.clone())
2280 })
2281 .layer(ElevationIndex::ModalSurface)
2282 .child(Label::new(
2283 if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
2284 "Chat"
2285 } else {
2286 "Send"
2287 },
2288 ))
2289 .children(
2290 KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
2291 .map(|binding| binding.into_any_element()),
2292 )
2293 .on_click(move |_event, window, cx| {
2294 focus_handle.dispatch_action(&Assist, window, cx);
2295 })
2296 }
2297
2298 fn render_edit_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2299 let focus_handle = self.focus_handle(cx).clone();
2300
2301 let (style, tooltip) = match token_state(&self.context, cx) {
2302 Some(TokenState::NoTokensLeft { .. }) => (
2303 ButtonStyle::Tinted(TintColor::Error),
2304 Some(Tooltip::text("Token limit reached")(window, cx)),
2305 ),
2306 Some(TokenState::HasMoreTokens {
2307 over_warn_threshold,
2308 ..
2309 }) => {
2310 let (style, tooltip) = if over_warn_threshold {
2311 (
2312 ButtonStyle::Tinted(TintColor::Warning),
2313 Some(Tooltip::text("Token limit is close to exhaustion")(
2314 window, cx,
2315 )),
2316 )
2317 } else {
2318 (ButtonStyle::Filled, None)
2319 };
2320 (style, tooltip)
2321 }
2322 None => (ButtonStyle::Filled, None),
2323 };
2324
2325 let provider = LanguageModelRegistry::read_global(cx).active_provider();
2326
2327 let has_configuration_error = configuration_error(cx).is_some();
2328 let needs_to_accept_terms = self.show_accept_terms
2329 && provider
2330 .as_ref()
2331 .map_or(false, |provider| provider.must_accept_terms(cx));
2332 let disabled = has_configuration_error || needs_to_accept_terms;
2333
2334 ButtonLike::new("edit_button")
2335 .disabled(disabled)
2336 .style(style)
2337 .when_some(tooltip, |button, tooltip| {
2338 button.tooltip(move |_, _| tooltip.clone())
2339 })
2340 .layer(ElevationIndex::ModalSurface)
2341 .child(Label::new("Suggest Edits"))
2342 .children(
2343 KeyBinding::for_action_in(&Edit, &focus_handle, window, cx)
2344 .map(|binding| binding.into_any_element()),
2345 )
2346 .on_click(move |_event, window, cx| {
2347 focus_handle.dispatch_action(&Edit, window, cx);
2348 })
2349 }
2350
2351 fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2352 slash_command_picker::SlashCommandSelector::new(
2353 self.slash_commands.clone(),
2354 cx.entity().downgrade(),
2355 IconButton::new("trigger", IconName::Plus)
2356 .icon_size(IconSize::Small)
2357 .icon_color(Color::Muted),
2358 move |window, cx| {
2359 Tooltip::with_meta(
2360 "Add Context",
2361 None,
2362 "Type / to insert via keyboard",
2363 window,
2364 cx,
2365 )
2366 },
2367 )
2368 }
2369
2370 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2371 let last_error = self.last_error.as_ref()?;
2372
2373 Some(
2374 div()
2375 .absolute()
2376 .right_3()
2377 .bottom_12()
2378 .max_w_96()
2379 .py_2()
2380 .px_3()
2381 .elevation_2(cx)
2382 .occlude()
2383 .child(match last_error {
2384 AssistError::FileRequired => self.render_file_required_error(cx),
2385 AssistError::PaymentRequired => self.render_payment_required_error(cx),
2386 AssistError::MaxMonthlySpendReached => {
2387 self.render_max_monthly_spend_reached_error(cx)
2388 }
2389 AssistError::Message(error_message) => {
2390 self.render_assist_error(error_message, cx)
2391 }
2392 })
2393 .into_any(),
2394 )
2395 }
2396
2397 fn render_file_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2398 v_flex()
2399 .gap_0p5()
2400 .child(
2401 h_flex()
2402 .gap_1p5()
2403 .items_center()
2404 .child(Icon::new(IconName::Warning).color(Color::Warning))
2405 .child(
2406 Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
2407 ),
2408 )
2409 .child(
2410 div()
2411 .id("error-message")
2412 .max_h_24()
2413 .overflow_y_scroll()
2414 .child(Label::new(
2415 "To include files, type /file or /tab in your prompt.",
2416 )),
2417 )
2418 .child(
2419 h_flex()
2420 .justify_end()
2421 .mt_1()
2422 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2423 |this, _, _window, cx| {
2424 this.last_error = None;
2425 cx.notify();
2426 },
2427 ))),
2428 )
2429 .into_any()
2430 }
2431
2432 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2433 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2434
2435 v_flex()
2436 .gap_0p5()
2437 .child(
2438 h_flex()
2439 .gap_1p5()
2440 .items_center()
2441 .child(Icon::new(IconName::XCircle).color(Color::Error))
2442 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2443 )
2444 .child(
2445 div()
2446 .id("error-message")
2447 .max_h_24()
2448 .overflow_y_scroll()
2449 .child(Label::new(ERROR_MESSAGE)),
2450 )
2451 .child(
2452 h_flex()
2453 .justify_end()
2454 .mt_1()
2455 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2456 |this, _, _window, cx| {
2457 this.last_error = None;
2458 cx.open_url(&zed_urls::account_url(cx));
2459 cx.notify();
2460 },
2461 )))
2462 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2463 |this, _, _window, cx| {
2464 this.last_error = None;
2465 cx.notify();
2466 },
2467 ))),
2468 )
2469 .into_any()
2470 }
2471
2472 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2473 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2474
2475 v_flex()
2476 .gap_0p5()
2477 .child(
2478 h_flex()
2479 .gap_1p5()
2480 .items_center()
2481 .child(Icon::new(IconName::XCircle).color(Color::Error))
2482 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2483 )
2484 .child(
2485 div()
2486 .id("error-message")
2487 .max_h_24()
2488 .overflow_y_scroll()
2489 .child(Label::new(ERROR_MESSAGE)),
2490 )
2491 .child(
2492 h_flex()
2493 .justify_end()
2494 .mt_1()
2495 .child(
2496 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2497 cx.listener(|this, _, _window, cx| {
2498 this.last_error = None;
2499 cx.open_url(&zed_urls::account_url(cx));
2500 cx.notify();
2501 }),
2502 ),
2503 )
2504 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2505 |this, _, _window, cx| {
2506 this.last_error = None;
2507 cx.notify();
2508 },
2509 ))),
2510 )
2511 .into_any()
2512 }
2513
2514 fn render_assist_error(
2515 &self,
2516 error_message: &SharedString,
2517 cx: &mut Context<Self>,
2518 ) -> AnyElement {
2519 v_flex()
2520 .gap_0p5()
2521 .child(
2522 h_flex()
2523 .gap_1p5()
2524 .items_center()
2525 .child(Icon::new(IconName::XCircle).color(Color::Error))
2526 .child(
2527 Label::new("Error interacting with language model")
2528 .weight(FontWeight::MEDIUM),
2529 ),
2530 )
2531 .child(
2532 div()
2533 .id("error-message")
2534 .max_h_32()
2535 .overflow_y_scroll()
2536 .child(Label::new(error_message.clone())),
2537 )
2538 .child(
2539 h_flex()
2540 .justify_end()
2541 .mt_1()
2542 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2543 |this, _, _window, cx| {
2544 this.last_error = None;
2545 cx.notify();
2546 },
2547 ))),
2548 )
2549 .into_any()
2550 }
2551}
2552
2553/// Returns the contents of the *outermost* fenced code block that contains the given offset.
2554fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
2555 const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
2556 const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
2557
2558 let layer = snapshot.syntax_layers().next()?;
2559
2560 let root_node = layer.node();
2561 let mut cursor = root_node.walk();
2562
2563 // Go to the first child for the given offset
2564 while cursor.goto_first_child_for_byte(offset).is_some() {
2565 // If we're at the end of the node, go to the next one.
2566 // Example: if you have a fenced-code-block, and you're on the start of the line
2567 // right after the closing ```, you want to skip the fenced-code-block and
2568 // go to the next sibling.
2569 if cursor.node().end_byte() == offset {
2570 cursor.goto_next_sibling();
2571 }
2572
2573 if cursor.node().start_byte() > offset {
2574 break;
2575 }
2576
2577 // We found the fenced code block.
2578 if cursor.node().kind() == CODE_BLOCK_NODE {
2579 // Now we need to find the child node that contains the code.
2580 cursor.goto_first_child();
2581 loop {
2582 if cursor.node().kind() == CODE_BLOCK_CONTENT {
2583 return Some(cursor.node().byte_range());
2584 }
2585 if !cursor.goto_next_sibling() {
2586 break;
2587 }
2588 }
2589 }
2590 }
2591
2592 None
2593}
2594
2595fn render_fold_icon_button(
2596 editor: WeakEntity<Editor>,
2597 icon: IconName,
2598 label: SharedString,
2599) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2600 Arc::new(move |fold_id, fold_range, _cx| {
2601 let editor = editor.clone();
2602 ButtonLike::new(fold_id)
2603 .style(ButtonStyle::Filled)
2604 .layer(ElevationIndex::ElevatedSurface)
2605 .child(Icon::new(icon))
2606 .child(Label::new(label.clone()).single_line())
2607 .on_click(move |_, window, cx| {
2608 editor
2609 .update(cx, |editor, cx| {
2610 let buffer_start = fold_range
2611 .start
2612 .to_point(&editor.buffer().read(cx).read(cx));
2613 let buffer_row = MultiBufferRow(buffer_start.row);
2614 editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
2615 })
2616 .ok();
2617 })
2618 .into_any_element()
2619 })
2620}
2621
2622type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
2623
2624fn render_slash_command_output_toggle(
2625 row: MultiBufferRow,
2626 is_folded: bool,
2627 fold: ToggleFold,
2628 _window: &mut Window,
2629 _cx: &mut App,
2630) -> AnyElement {
2631 Disclosure::new(
2632 ("slash-command-output-fold-indicator", row.0 as u64),
2633 !is_folded,
2634 )
2635 .toggle_state(is_folded)
2636 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2637 .into_any_element()
2638}
2639
2640pub fn fold_toggle(
2641 name: &'static str,
2642) -> impl Fn(
2643 MultiBufferRow,
2644 bool,
2645 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
2646 &mut Window,
2647 &mut App,
2648) -> AnyElement {
2649 move |row, is_folded, fold, _window, _cx| {
2650 Disclosure::new((name, row.0 as u64), !is_folded)
2651 .toggle_state(is_folded)
2652 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2653 .into_any_element()
2654 }
2655}
2656
2657fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
2658 FoldPlaceholder {
2659 render: Arc::new({
2660 move |fold_id, fold_range, _cx| {
2661 let editor = editor.clone();
2662 ButtonLike::new(fold_id)
2663 .style(ButtonStyle::Filled)
2664 .layer(ElevationIndex::ElevatedSurface)
2665 .child(Icon::new(IconName::TextSnippet))
2666 .child(Label::new(title.clone()).single_line())
2667 .on_click(move |_, window, cx| {
2668 editor
2669 .update(cx, |editor, cx| {
2670 let buffer_start = fold_range
2671 .start
2672 .to_point(&editor.buffer().read(cx).read(cx));
2673 let buffer_row = MultiBufferRow(buffer_start.row);
2674 editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
2675 })
2676 .ok();
2677 })
2678 .into_any_element()
2679 }
2680 }),
2681 merge_adjacent: false,
2682 ..Default::default()
2683 }
2684}
2685
2686fn render_quote_selection_output_toggle(
2687 row: MultiBufferRow,
2688 is_folded: bool,
2689 fold: ToggleFold,
2690 _window: &mut Window,
2691 _cx: &mut App,
2692) -> AnyElement {
2693 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
2694 .toggle_state(is_folded)
2695 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2696 .into_any_element()
2697}
2698
2699fn render_pending_slash_command_gutter_decoration(
2700 row: MultiBufferRow,
2701 status: &PendingSlashCommandStatus,
2702 confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
2703) -> AnyElement {
2704 let mut icon = IconButton::new(
2705 ("slash-command-gutter-decoration", row.0),
2706 ui::IconName::TriangleRight,
2707 )
2708 .on_click(move |_e, window, cx| confirm_command(window, cx))
2709 .icon_size(ui::IconSize::Small)
2710 .size(ui::ButtonSize::None);
2711
2712 match status {
2713 PendingSlashCommandStatus::Idle => {
2714 icon = icon.icon_color(Color::Muted);
2715 }
2716 PendingSlashCommandStatus::Running { .. } => {
2717 icon = icon.toggle_state(true);
2718 }
2719 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2720 }
2721
2722 icon.into_any_element()
2723}
2724
2725fn render_docs_slash_command_trailer(
2726 row: MultiBufferRow,
2727 command: ParsedSlashCommand,
2728 cx: &mut App,
2729) -> AnyElement {
2730 if command.arguments.is_empty() {
2731 return Empty.into_any();
2732 }
2733 let args = DocsSlashCommandArgs::parse(&command.arguments);
2734
2735 let Some(store) = args
2736 .provider()
2737 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
2738 else {
2739 return Empty.into_any();
2740 };
2741
2742 let Some(package) = args.package() else {
2743 return Empty.into_any();
2744 };
2745
2746 let mut children = Vec::new();
2747
2748 if store.is_indexing(&package) {
2749 children.push(
2750 div()
2751 .id(("crates-being-indexed", row.0))
2752 .child(Icon::new(IconName::ArrowCircle).with_animation(
2753 "arrow-circle",
2754 Animation::new(Duration::from_secs(4)).repeat(),
2755 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2756 ))
2757 .tooltip({
2758 let package = package.clone();
2759 Tooltip::text(format!("Indexing {package}…"))
2760 })
2761 .into_any_element(),
2762 );
2763 }
2764
2765 if let Some(latest_error) = store.latest_error_for_package(&package) {
2766 children.push(
2767 div()
2768 .id(("latest-error", row.0))
2769 .child(
2770 Icon::new(IconName::Warning)
2771 .size(IconSize::Small)
2772 .color(Color::Warning),
2773 )
2774 .tooltip(Tooltip::text(format!("Failed to index: {latest_error}")))
2775 .into_any_element(),
2776 )
2777 }
2778
2779 let is_indexing = store.is_indexing(&package);
2780 let latest_error = store.latest_error_for_package(&package);
2781
2782 if !is_indexing && latest_error.is_none() {
2783 return Empty.into_any();
2784 }
2785
2786 h_flex().gap_2().children(children).into_any_element()
2787}
2788
2789#[derive(Debug, Clone, Serialize, Deserialize)]
2790struct CopyMetadata {
2791 creases: Vec<SelectedCreaseMetadata>,
2792}
2793
2794#[derive(Debug, Clone, Serialize, Deserialize)]
2795struct SelectedCreaseMetadata {
2796 range_relative_to_selection: Range<usize>,
2797 crease: CreaseMetadata,
2798}
2799
2800impl EventEmitter<EditorEvent> for ContextEditor {}
2801impl EventEmitter<SearchEvent> for ContextEditor {}
2802
2803impl Render for ContextEditor {
2804 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2805 let provider = LanguageModelRegistry::read_global(cx).active_provider();
2806 let accept_terms = if self.show_accept_terms {
2807 provider.as_ref().and_then(|provider| {
2808 provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
2809 })
2810 } else {
2811 None
2812 };
2813
2814 let language_model_selector = self.language_model_selector.clone();
2815 v_flex()
2816 .key_context("ContextEditor")
2817 .capture_action(cx.listener(ContextEditor::cancel))
2818 .capture_action(cx.listener(ContextEditor::save))
2819 .capture_action(cx.listener(ContextEditor::copy))
2820 .capture_action(cx.listener(ContextEditor::cut))
2821 .capture_action(cx.listener(ContextEditor::paste))
2822 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2823 .capture_action(cx.listener(ContextEditor::confirm_command))
2824 .on_action(cx.listener(ContextEditor::edit))
2825 .on_action(cx.listener(ContextEditor::assist))
2826 .on_action(cx.listener(ContextEditor::split))
2827 .on_action(move |action, window, cx| {
2828 language_model_selector.update(cx, |this, cx| {
2829 this.toggle_model_selector(action, window, cx);
2830 })
2831 })
2832 .size_full()
2833 .children(self.render_notice(cx))
2834 .child(
2835 div()
2836 .flex_grow()
2837 .bg(cx.theme().colors().editor_background)
2838 .child(self.editor.clone()),
2839 )
2840 .when_some(accept_terms, |this, element| {
2841 this.child(
2842 div()
2843 .absolute()
2844 .right_3()
2845 .bottom_12()
2846 .max_w_96()
2847 .py_2()
2848 .px_3()
2849 .elevation_2(cx)
2850 .bg(cx.theme().colors().surface_background)
2851 .occlude()
2852 .child(element),
2853 )
2854 })
2855 .children(self.render_last_error(cx))
2856 .child(
2857 h_flex().w_full().relative().child(
2858 h_flex()
2859 .p_2()
2860 .w_full()
2861 .border_t_1()
2862 .border_color(cx.theme().colors().border_variant)
2863 .bg(cx.theme().colors().editor_background)
2864 .child(
2865 h_flex()
2866 .gap_1()
2867 .child(self.render_inject_context_menu(cx))
2868 .child(ui::Divider::vertical())
2869 .child(div().pl_0p5().child({
2870 let focus_handle = self.editor().focus_handle(cx).clone();
2871 AssistantLanguageModelSelector::new(
2872 focus_handle,
2873 self.language_model_selector.clone(),
2874 )
2875 .render(window, cx)
2876 })),
2877 )
2878 .child(
2879 h_flex()
2880 .w_full()
2881 .justify_end()
2882 .when(
2883 AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
2884 |buttons| {
2885 buttons
2886 .items_center()
2887 .gap_1p5()
2888 .child(self.render_edit_button(window, cx))
2889 .child(
2890 Label::new("or")
2891 .size(LabelSize::Small)
2892 .color(Color::Muted),
2893 )
2894 },
2895 )
2896 .child(self.render_send_button(window, cx)),
2897 ),
2898 ),
2899 )
2900 }
2901}
2902
2903impl Focusable for ContextEditor {
2904 fn focus_handle(&self, cx: &App) -> FocusHandle {
2905 self.editor.focus_handle(cx)
2906 }
2907}
2908
2909impl Item for ContextEditor {
2910 type Event = editor::EditorEvent;
2911
2912 fn tab_content_text(&self, _window: &Window, cx: &App) -> Option<SharedString> {
2913 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2914 }
2915
2916 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2917 match event {
2918 EditorEvent::Edited { .. } => {
2919 f(item::ItemEvent::Edit);
2920 }
2921 EditorEvent::TitleChanged => {
2922 f(item::ItemEvent::UpdateTab);
2923 }
2924 _ => {}
2925 }
2926 }
2927
2928 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
2929 Some(self.title(cx).to_string().into())
2930 }
2931
2932 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2933 Some(Box::new(handle.clone()))
2934 }
2935
2936 fn set_nav_history(
2937 &mut self,
2938 nav_history: pane::ItemNavHistory,
2939 window: &mut Window,
2940 cx: &mut Context<Self>,
2941 ) {
2942 self.editor.update(cx, |editor, cx| {
2943 Item::set_nav_history(editor, nav_history, window, cx)
2944 })
2945 }
2946
2947 fn navigate(
2948 &mut self,
2949 data: Box<dyn std::any::Any>,
2950 window: &mut Window,
2951 cx: &mut Context<Self>,
2952 ) -> bool {
2953 self.editor
2954 .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
2955 }
2956
2957 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2958 self.editor
2959 .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
2960 }
2961
2962 fn act_as_type<'a>(
2963 &'a self,
2964 type_id: TypeId,
2965 self_handle: &'a Entity<Self>,
2966 _: &'a App,
2967 ) -> Option<AnyView> {
2968 if type_id == TypeId::of::<Self>() {
2969 Some(self_handle.to_any())
2970 } else if type_id == TypeId::of::<Editor>() {
2971 Some(self.editor.to_any())
2972 } else {
2973 None
2974 }
2975 }
2976
2977 fn include_in_nav_history() -> bool {
2978 false
2979 }
2980}
2981
2982impl SearchableItem for ContextEditor {
2983 type Match = <Editor as SearchableItem>::Match;
2984
2985 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2986 self.editor.update(cx, |editor, cx| {
2987 editor.clear_matches(window, cx);
2988 });
2989 }
2990
2991 fn update_matches(
2992 &mut self,
2993 matches: &[Self::Match],
2994 window: &mut Window,
2995 cx: &mut Context<Self>,
2996 ) {
2997 self.editor
2998 .update(cx, |editor, cx| editor.update_matches(matches, window, cx));
2999 }
3000
3001 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
3002 self.editor
3003 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
3004 }
3005
3006 fn activate_match(
3007 &mut self,
3008 index: usize,
3009 matches: &[Self::Match],
3010 window: &mut Window,
3011 cx: &mut Context<Self>,
3012 ) {
3013 self.editor.update(cx, |editor, cx| {
3014 editor.activate_match(index, matches, window, cx);
3015 });
3016 }
3017
3018 fn select_matches(
3019 &mut self,
3020 matches: &[Self::Match],
3021 window: &mut Window,
3022 cx: &mut Context<Self>,
3023 ) {
3024 self.editor
3025 .update(cx, |editor, cx| editor.select_matches(matches, window, cx));
3026 }
3027
3028 fn replace(
3029 &mut self,
3030 identifier: &Self::Match,
3031 query: &project::search::SearchQuery,
3032 window: &mut Window,
3033 cx: &mut Context<Self>,
3034 ) {
3035 self.editor.update(cx, |editor, cx| {
3036 editor.replace(identifier, query, window, cx)
3037 });
3038 }
3039
3040 fn find_matches(
3041 &mut self,
3042 query: Arc<project::search::SearchQuery>,
3043 window: &mut Window,
3044 cx: &mut Context<Self>,
3045 ) -> Task<Vec<Self::Match>> {
3046 self.editor
3047 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
3048 }
3049
3050 fn active_match_index(
3051 &mut self,
3052 matches: &[Self::Match],
3053 window: &mut Window,
3054 cx: &mut Context<Self>,
3055 ) -> Option<usize> {
3056 self.editor.update(cx, |editor, cx| {
3057 editor.active_match_index(matches, window, cx)
3058 })
3059 }
3060}
3061
3062impl FollowableItem for ContextEditor {
3063 fn remote_id(&self) -> Option<workspace::ViewId> {
3064 self.remote_id
3065 }
3066
3067 fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
3068 let context = self.context.read(cx);
3069 Some(proto::view::Variant::ContextEditor(
3070 proto::view::ContextEditor {
3071 context_id: context.id().to_proto(),
3072 editor: if let Some(proto::view::Variant::Editor(proto)) =
3073 self.editor.read(cx).to_state_proto(window, cx)
3074 {
3075 Some(proto)
3076 } else {
3077 None
3078 },
3079 },
3080 ))
3081 }
3082
3083 fn from_state_proto(
3084 workspace: Entity<Workspace>,
3085 id: workspace::ViewId,
3086 state: &mut Option<proto::view::Variant>,
3087 window: &mut Window,
3088 cx: &mut App,
3089 ) -> Option<Task<Result<Entity<Self>>>> {
3090 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
3091 return None;
3092 };
3093 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
3094 unreachable!()
3095 };
3096
3097 let context_id = ContextId::from_proto(state.context_id);
3098 let editor_state = state.editor?;
3099
3100 let project = workspace.read(cx).project().clone();
3101 let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
3102
3103 let context_editor_task = workspace.update(cx, |workspace, cx| {
3104 assistant_panel_delegate.open_remote_context(workspace, context_id, window, cx)
3105 });
3106
3107 Some(window.spawn(cx, |mut cx| async move {
3108 let context_editor = context_editor_task.await?;
3109 context_editor
3110 .update_in(&mut cx, |context_editor, window, cx| {
3111 context_editor.remote_id = Some(id);
3112 context_editor.editor.update(cx, |editor, cx| {
3113 editor.apply_update_proto(
3114 &project,
3115 proto::update_view::Variant::Editor(proto::update_view::Editor {
3116 selections: editor_state.selections,
3117 pending_selection: editor_state.pending_selection,
3118 scroll_top_anchor: editor_state.scroll_top_anchor,
3119 scroll_x: editor_state.scroll_y,
3120 scroll_y: editor_state.scroll_y,
3121 ..Default::default()
3122 }),
3123 window,
3124 cx,
3125 )
3126 })
3127 })?
3128 .await?;
3129 Ok(context_editor)
3130 }))
3131 }
3132
3133 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
3134 Editor::to_follow_event(event)
3135 }
3136
3137 fn add_event_to_update_proto(
3138 &self,
3139 event: &Self::Event,
3140 update: &mut Option<proto::update_view::Variant>,
3141 window: &Window,
3142 cx: &App,
3143 ) -> bool {
3144 self.editor
3145 .read(cx)
3146 .add_event_to_update_proto(event, update, window, cx)
3147 }
3148
3149 fn apply_update_proto(
3150 &mut self,
3151 project: &Entity<Project>,
3152 message: proto::update_view::Variant,
3153 window: &mut Window,
3154 cx: &mut Context<Self>,
3155 ) -> Task<Result<()>> {
3156 self.editor.update(cx, |editor, cx| {
3157 editor.apply_update_proto(project, message, window, cx)
3158 })
3159 }
3160
3161 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
3162 true
3163 }
3164
3165 fn set_leader_peer_id(
3166 &mut self,
3167 leader_peer_id: Option<proto::PeerId>,
3168 window: &mut Window,
3169 cx: &mut Context<Self>,
3170 ) {
3171 self.editor.update(cx, |editor, cx| {
3172 editor.set_leader_peer_id(leader_peer_id, window, cx)
3173 })
3174 }
3175
3176 fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
3177 if existing.context.read(cx).id() == self.context.read(cx).id() {
3178 Some(item::Dedup::KeepExisting)
3179 } else {
3180 None
3181 }
3182 }
3183}
3184
3185pub struct ContextEditorToolbarItem {
3186 active_context_editor: Option<WeakEntity<ContextEditor>>,
3187 model_summary_editor: Entity<Editor>,
3188}
3189
3190impl ContextEditorToolbarItem {
3191 pub fn new(model_summary_editor: Entity<Editor>) -> Self {
3192 Self {
3193 active_context_editor: None,
3194 model_summary_editor,
3195 }
3196 }
3197}
3198
3199pub fn render_remaining_tokens(
3200 context_editor: &Entity<ContextEditor>,
3201 cx: &App,
3202) -> Option<impl IntoElement> {
3203 let context = &context_editor.read(cx).context;
3204
3205 let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
3206 {
3207 TokenState::NoTokensLeft {
3208 max_token_count,
3209 token_count,
3210 } => (
3211 Color::Error,
3212 token_count,
3213 max_token_count,
3214 Some("Token Limit Reached"),
3215 ),
3216 TokenState::HasMoreTokens {
3217 max_token_count,
3218 token_count,
3219 over_warn_threshold,
3220 } => {
3221 let (color, tooltip) = if over_warn_threshold {
3222 (Color::Warning, Some("Token Limit is Close to Exhaustion"))
3223 } else {
3224 (Color::Muted, None)
3225 };
3226 (color, token_count, max_token_count, tooltip)
3227 }
3228 };
3229
3230 Some(
3231 h_flex()
3232 .id("token-count")
3233 .gap_0p5()
3234 .child(
3235 Label::new(humanize_token_count(token_count))
3236 .size(LabelSize::Small)
3237 .color(token_count_color),
3238 )
3239 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
3240 .child(
3241 Label::new(humanize_token_count(max_token_count))
3242 .size(LabelSize::Small)
3243 .color(Color::Muted),
3244 )
3245 .when_some(tooltip, |element, tooltip| {
3246 element.tooltip(Tooltip::text(tooltip))
3247 }),
3248 )
3249}
3250
3251impl Render for ContextEditorToolbarItem {
3252 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3253 let left_side = h_flex()
3254 .group("chat-title-group")
3255 .gap_1()
3256 .items_center()
3257 .flex_grow()
3258 .child(
3259 div()
3260 .w_full()
3261 .when(self.active_context_editor.is_some(), |left_side| {
3262 left_side.child(self.model_summary_editor.clone())
3263 }),
3264 )
3265 .child(
3266 div().visible_on_hover("chat-title-group").child(
3267 IconButton::new("regenerate-context", IconName::RefreshTitle)
3268 .shape(ui::IconButtonShape::Square)
3269 .tooltip(Tooltip::text("Regenerate Title"))
3270 .on_click(cx.listener(move |_, _, _window, cx| {
3271 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
3272 })),
3273 ),
3274 );
3275
3276 let right_side = h_flex()
3277 .gap_2()
3278 // TODO display this in a nicer way, once we have a design for it.
3279 // .children({
3280 // let project = self
3281 // .workspace
3282 // .upgrade()
3283 // .map(|workspace| workspace.read(cx).project().downgrade());
3284 //
3285 // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
3286 // project.and_then(|project| db.remaining_summaries(&project, cx))
3287 // });
3288 // scan_items_remaining
3289 // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
3290 // })
3291 .children(
3292 self.active_context_editor
3293 .as_ref()
3294 .and_then(|editor| editor.upgrade())
3295 .and_then(|editor| render_remaining_tokens(&editor, cx)),
3296 );
3297
3298 h_flex()
3299 .px_0p5()
3300 .size_full()
3301 .gap_2()
3302 .justify_between()
3303 .child(left_side)
3304 .child(right_side)
3305 }
3306}
3307
3308impl ToolbarItemView for ContextEditorToolbarItem {
3309 fn set_active_pane_item(
3310 &mut self,
3311 active_pane_item: Option<&dyn ItemHandle>,
3312 _window: &mut Window,
3313 cx: &mut Context<Self>,
3314 ) -> ToolbarItemLocation {
3315 self.active_context_editor = active_pane_item
3316 .and_then(|item| item.act_as::<ContextEditor>(cx))
3317 .map(|editor| editor.downgrade());
3318 cx.notify();
3319 if self.active_context_editor.is_none() {
3320 ToolbarItemLocation::Hidden
3321 } else {
3322 ToolbarItemLocation::PrimaryRight
3323 }
3324 }
3325
3326 fn pane_focus_update(
3327 &mut self,
3328 _pane_focused: bool,
3329 _window: &mut Window,
3330 cx: &mut Context<Self>,
3331 ) {
3332 cx.notify();
3333 }
3334}
3335
3336impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
3337
3338pub enum ContextEditorToolbarItemEvent {
3339 RegenerateSummary,
3340}
3341impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
3342
3343enum PendingSlashCommand {}
3344
3345fn invoked_slash_command_fold_placeholder(
3346 command_id: InvokedSlashCommandId,
3347 context: WeakEntity<AssistantContext>,
3348) -> FoldPlaceholder {
3349 FoldPlaceholder {
3350 constrain_width: false,
3351 merge_adjacent: false,
3352 render: Arc::new(move |fold_id, _, cx| {
3353 let Some(context) = context.upgrade() else {
3354 return Empty.into_any();
3355 };
3356
3357 let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
3358 return Empty.into_any();
3359 };
3360
3361 h_flex()
3362 .id(fold_id)
3363 .px_1()
3364 .ml_6()
3365 .gap_2()
3366 .bg(cx.theme().colors().surface_background)
3367 .rounded_md()
3368 .child(Label::new(format!("/{}", command.name.clone())))
3369 .map(|parent| match &command.status {
3370 InvokedSlashCommandStatus::Running(_) => {
3371 parent.child(Icon::new(IconName::ArrowCircle).with_animation(
3372 "arrow-circle",
3373 Animation::new(Duration::from_secs(4)).repeat(),
3374 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3375 ))
3376 }
3377 InvokedSlashCommandStatus::Error(message) => parent.child(
3378 Label::new(format!("error: {message}"))
3379 .single_line()
3380 .color(Color::Error),
3381 ),
3382 InvokedSlashCommandStatus::Finished => parent,
3383 })
3384 .into_any_element()
3385 }),
3386 type_tag: Some(TypeId::of::<PendingSlashCommand>()),
3387 }
3388}
3389
3390enum TokenState {
3391 NoTokensLeft {
3392 max_token_count: usize,
3393 token_count: usize,
3394 },
3395 HasMoreTokens {
3396 max_token_count: usize,
3397 token_count: usize,
3398 over_warn_threshold: bool,
3399 },
3400}
3401
3402fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenState> {
3403 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3404
3405 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3406 let token_count = context.read(cx).token_count()?;
3407 let max_token_count = model.max_token_count();
3408
3409 let remaining_tokens = max_token_count as isize - token_count as isize;
3410 let token_state = if remaining_tokens <= 0 {
3411 TokenState::NoTokensLeft {
3412 max_token_count,
3413 token_count,
3414 }
3415 } else {
3416 let over_warn_threshold =
3417 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3418 TokenState::HasMoreTokens {
3419 max_token_count,
3420 token_count,
3421 over_warn_threshold,
3422 }
3423 };
3424 Some(token_state)
3425}
3426
3427fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
3428 let image_size = data
3429 .size(0)
3430 .map(|dimension| Pixels::from(u32::from(dimension)));
3431 let image_ratio = image_size.width / image_size.height;
3432 let bounds_ratio = max_size.width / max_size.height;
3433
3434 if image_size.width > max_size.width || image_size.height > max_size.height {
3435 if bounds_ratio > image_ratio {
3436 size(
3437 image_size.width * (max_size.height / image_size.height),
3438 max_size.height,
3439 )
3440 } else {
3441 size(
3442 max_size.width,
3443 image_size.height * (max_size.width / image_size.width),
3444 )
3445 }
3446 } else {
3447 size(image_size.width, image_size.height)
3448 }
3449}
3450
3451pub enum ConfigurationError {
3452 NoProvider,
3453 ProviderNotAuthenticated,
3454 ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
3455}
3456
3457fn configuration_error(cx: &App) -> Option<ConfigurationError> {
3458 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3459 let is_authenticated = provider
3460 .as_ref()
3461 .map_or(false, |provider| provider.is_authenticated(cx));
3462
3463 if provider.is_some() && is_authenticated {
3464 return None;
3465 }
3466
3467 if provider.is_none() {
3468 return Some(ConfigurationError::NoProvider);
3469 }
3470
3471 if !is_authenticated {
3472 return Some(ConfigurationError::ProviderNotAuthenticated);
3473 }
3474
3475 None
3476}
3477
3478pub fn humanize_token_count(count: usize) -> String {
3479 match count {
3480 0..=999 => count.to_string(),
3481 1000..=9999 => {
3482 let thousands = count / 1000;
3483 let hundreds = (count % 1000 + 50) / 100;
3484 if hundreds == 0 {
3485 format!("{}k", thousands)
3486 } else if hundreds == 10 {
3487 format!("{}k", thousands + 1)
3488 } else {
3489 format!("{}.{}k", thousands, hundreds)
3490 }
3491 }
3492 _ => format!("{}k", (count + 500) / 1000),
3493 }
3494}
3495
3496pub fn make_lsp_adapter_delegate(
3497 project: &Entity<Project>,
3498 cx: &mut App,
3499) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
3500 project.update(cx, |project, cx| {
3501 // TODO: Find the right worktree.
3502 let Some(worktree) = project.worktrees(cx).next() else {
3503 return Ok(None::<Arc<dyn LspAdapterDelegate>>);
3504 };
3505 let http_client = project.client().http_client().clone();
3506 project.lsp_store().update(cx, |_, cx| {
3507 Ok(Some(LocalLspAdapterDelegate::new(
3508 project.languages().clone(),
3509 project.environment(),
3510 cx.weak_entity(),
3511 &worktree,
3512 http_client,
3513 project.fs().clone(),
3514 cx,
3515 ) as Arc<dyn LspAdapterDelegate>))
3516 })
3517 })
3518}
3519
3520#[cfg(test)]
3521mod tests {
3522 use super::*;
3523 use gpui::App;
3524 use language::Buffer;
3525 use unindent::Unindent;
3526
3527 #[gpui::test]
3528 fn test_find_code_blocks(cx: &mut App) {
3529 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
3530
3531 let buffer = cx.new(|cx| {
3532 let text = r#"
3533 line 0
3534 line 1
3535 ```rust
3536 fn main() {}
3537 ```
3538 line 5
3539 line 6
3540 line 7
3541 ```go
3542 func main() {}
3543 ```
3544 line 11
3545 ```
3546 this is plain text code block
3547 ```
3548
3549 ```go
3550 func another() {}
3551 ```
3552 line 19
3553 "#
3554 .unindent();
3555 let mut buffer = Buffer::local(text, cx);
3556 buffer.set_language(Some(markdown.clone()), cx);
3557 buffer
3558 });
3559 let snapshot = buffer.read(cx).snapshot();
3560
3561 let code_blocks = vec![
3562 Point::new(3, 0)..Point::new(4, 0),
3563 Point::new(9, 0)..Point::new(10, 0),
3564 Point::new(13, 0)..Point::new(14, 0),
3565 Point::new(17, 0)..Point::new(18, 0),
3566 ]
3567 .into_iter()
3568 .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
3569 .collect::<Vec<_>>();
3570
3571 let expected_results = vec![
3572 (0, None),
3573 (1, None),
3574 (2, Some(code_blocks[0].clone())),
3575 (3, Some(code_blocks[0].clone())),
3576 (4, Some(code_blocks[0].clone())),
3577 (5, None),
3578 (6, None),
3579 (7, None),
3580 (8, Some(code_blocks[1].clone())),
3581 (9, Some(code_blocks[1].clone())),
3582 (10, Some(code_blocks[1].clone())),
3583 (11, None),
3584 (12, Some(code_blocks[2].clone())),
3585 (13, Some(code_blocks[2].clone())),
3586 (14, Some(code_blocks[2].clone())),
3587 (15, None),
3588 (16, Some(code_blocks[3].clone())),
3589 (17, Some(code_blocks[3].clone())),
3590 (18, Some(code_blocks[3].clone())),
3591 (19, None),
3592 ];
3593
3594 for (row, expected) in expected_results {
3595 let offset = snapshot.point_to_offset(Point::new(row, 0));
3596 let range = find_surrounding_code_block(&snapshot, offset);
3597 assert_eq!(range, expected, "unexpected result on row {:?}", row);
3598 }
3599 }
3600}