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