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