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