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