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