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