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