1use language_model::AnthropicEventData;
2use language_model::report_anthropic_event;
3use std::cmp;
4use std::mem;
5use std::ops::Range;
6use std::rc::Rc;
7use std::sync::Arc;
8use uuid::Uuid;
9
10use crate::context::load_context;
11use crate::mention_set::MentionSet;
12use crate::{
13 AgentPanel,
14 buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
15 inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
16 terminal_inline_assistant::TerminalInlineAssistant,
17};
18use agent::HistoryStore;
19use agent_settings::AgentSettings;
20use anyhow::{Context as _, Result};
21use collections::{HashMap, HashSet, VecDeque, hash_map};
22use editor::EditorSnapshot;
23use editor::MultiBufferOffset;
24use editor::RowExt;
25use editor::SelectionEffects;
26use editor::scroll::ScrollOffset;
27use editor::{
28 Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
29 MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
30 actions::SelectAll,
31 display_map::{
32 BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, EditorMargins,
33 RenderBlock, ToDisplayPoint,
34 },
35};
36use fs::Fs;
37use futures::{FutureExt, channel::mpsc};
38use gpui::{
39 App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
40 WeakEntity, Window, point,
41};
42use language::{Buffer, Point, Selection, TransactionId};
43use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
44use multi_buffer::MultiBufferRow;
45use parking_lot::Mutex;
46use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
47use prompt_store::{PromptBuilder, PromptStore};
48use settings::{Settings, SettingsStore};
49
50use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
51use text::{OffsetRangeExt, ToPoint as _};
52use ui::prelude::*;
53use util::{RangeExt, ResultExt, maybe};
54use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
55use zed_actions::agent::OpenSettings;
56
57pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
58 cx.set_global(InlineAssistant::new(fs, prompt_builder));
59
60 cx.observe_global::<SettingsStore>(|cx| {
61 if DisableAiSettings::get_global(cx).disable_ai {
62 // Hide any active inline assist UI when AI is disabled
63 InlineAssistant::update_global(cx, |assistant, cx| {
64 assistant.cancel_all_active_completions(cx);
65 });
66 }
67 })
68 .detach();
69
70 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
71 let Some(window) = window else {
72 return;
73 };
74 let workspace = cx.entity();
75 InlineAssistant::update_global(cx, |inline_assistant, cx| {
76 inline_assistant.register_workspace(&workspace, window, cx)
77 });
78 })
79 .detach();
80}
81
82const PROMPT_HISTORY_MAX_LEN: usize = 20;
83
84enum InlineAssistTarget {
85 Editor(Entity<Editor>),
86 Terminal(Entity<TerminalView>),
87}
88
89pub struct InlineAssistant {
90 next_assist_id: InlineAssistId,
91 next_assist_group_id: InlineAssistGroupId,
92 assists: HashMap<InlineAssistId, InlineAssist>,
93 assists_by_editor: HashMap<WeakEntity<Editor>, EditorInlineAssists>,
94 assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
95 confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
96 prompt_history: VecDeque<String>,
97 prompt_builder: Arc<PromptBuilder>,
98 fs: Arc<dyn Fs>,
99 _inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
100}
101
102impl Global for InlineAssistant {}
103
104impl InlineAssistant {
105 pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> Self {
106 Self {
107 next_assist_id: InlineAssistId::default(),
108 next_assist_group_id: InlineAssistGroupId::default(),
109 assists: HashMap::default(),
110 assists_by_editor: HashMap::default(),
111 assist_groups: HashMap::default(),
112 confirmed_assists: HashMap::default(),
113 prompt_history: VecDeque::default(),
114 prompt_builder,
115 fs,
116 _inline_assistant_completions: None,
117 }
118 }
119
120 #[cfg(any(test, feature = "test-support"))]
121 pub fn set_completion_receiver(
122 &mut self,
123 sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
124 ) {
125 self._inline_assistant_completions = Some(sender);
126 }
127
128 pub fn register_workspace(
129 &mut self,
130 workspace: &Entity<Workspace>,
131 window: &mut Window,
132 cx: &mut App,
133 ) {
134 window
135 .subscribe(workspace, cx, |workspace, event, window, cx| {
136 Self::update_global(cx, |this, cx| {
137 this.handle_workspace_event(workspace, event, window, cx)
138 });
139 })
140 .detach();
141
142 let workspace = workspace.downgrade();
143 cx.observe_global::<SettingsStore>(move |cx| {
144 let Some(workspace) = workspace.upgrade() else {
145 return;
146 };
147 let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
148 return;
149 };
150 let enabled = AgentSettings::get_global(cx).enabled(cx);
151 terminal_panel.update(cx, |terminal_panel, cx| {
152 terminal_panel.set_assistant_enabled(enabled, cx)
153 });
154 })
155 .detach();
156 }
157
158 /// Hides all active inline assists when AI is disabled
159 pub fn cancel_all_active_completions(&mut self, cx: &mut App) {
160 // Cancel all active completions in editors
161 for (editor_handle, _) in self.assists_by_editor.iter() {
162 if let Some(editor) = editor_handle.upgrade() {
163 let windows = cx.windows();
164 if !windows.is_empty() {
165 let window = windows[0];
166 let _ = window.update(cx, |_, window, cx| {
167 editor.update(cx, |editor, cx| {
168 if editor.has_active_edit_prediction() {
169 editor.cancel(&Default::default(), window, cx);
170 }
171 });
172 });
173 }
174 }
175 }
176 }
177
178 fn handle_workspace_event(
179 &mut self,
180 workspace: Entity<Workspace>,
181 event: &workspace::Event,
182 window: &mut Window,
183 cx: &mut App,
184 ) {
185 match event {
186 workspace::Event::UserSavedItem { item, .. } => {
187 // When the user manually saves an editor, automatically accepts all finished transformations.
188 if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx))
189 && let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade())
190 {
191 for assist_id in editor_assists.assist_ids.clone() {
192 let assist = &self.assists[&assist_id];
193 if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
194 self.finish_assist(assist_id, false, window, cx)
195 }
196 }
197 }
198 }
199 workspace::Event::ItemAdded { item } => {
200 self.register_workspace_item(&workspace, item.as_ref(), window, cx);
201 }
202 _ => (),
203 }
204 }
205
206 fn register_workspace_item(
207 &mut self,
208 workspace: &Entity<Workspace>,
209 item: &dyn ItemHandle,
210 window: &mut Window,
211 cx: &mut App,
212 ) {
213 let is_ai_enabled = !DisableAiSettings::get_global(cx).disable_ai;
214
215 if let Some(editor) = item.act_as::<Editor>(cx) {
216 editor.update(cx, |editor, cx| {
217 if is_ai_enabled {
218 editor.add_code_action_provider(
219 Rc::new(AssistantCodeActionProvider {
220 editor: cx.entity().downgrade(),
221 workspace: workspace.downgrade(),
222 }),
223 window,
224 cx,
225 );
226
227 if DisableAiSettings::get_global(cx).disable_ai {
228 // Cancel any active edit predictions
229 if editor.has_active_edit_prediction() {
230 editor.cancel(&Default::default(), window, cx);
231 }
232 }
233 } else {
234 editor.remove_code_action_provider(
235 ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
236 window,
237 cx,
238 );
239 }
240 });
241 }
242 }
243
244 pub fn inline_assist(
245 workspace: &mut Workspace,
246 action: &zed_actions::assistant::InlineAssist,
247 window: &mut Window,
248 cx: &mut Context<Workspace>,
249 ) {
250 if !AgentSettings::get_global(cx).enabled(cx) {
251 return;
252 }
253
254 let Some(inline_assist_target) = Self::resolve_inline_assist_target(
255 workspace,
256 workspace.panel::<AgentPanel>(cx),
257 window,
258 cx,
259 ) else {
260 return;
261 };
262
263 let configuration_error = || {
264 let model_registry = LanguageModelRegistry::read_global(cx);
265 model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
266 };
267
268 let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
269 return;
270 };
271 let agent_panel = agent_panel.read(cx);
272
273 let prompt_store = agent_panel.prompt_store().as_ref().cloned();
274 let thread_store = agent_panel.thread_store().clone();
275
276 let handle_assist =
277 |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
278 InlineAssistTarget::Editor(active_editor) => {
279 InlineAssistant::update_global(cx, |assistant, cx| {
280 assistant.assist(
281 &active_editor,
282 cx.entity().downgrade(),
283 workspace.project().downgrade(),
284 thread_store,
285 prompt_store,
286 action.prompt.clone(),
287 window,
288 cx,
289 );
290 })
291 }
292 InlineAssistTarget::Terminal(active_terminal) => {
293 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
294 assistant.assist(
295 &active_terminal,
296 cx.entity().downgrade(),
297 workspace.project().downgrade(),
298 thread_store,
299 prompt_store,
300 action.prompt.clone(),
301 window,
302 cx,
303 );
304 });
305 }
306 };
307
308 if let Some(error) = configuration_error() {
309 if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
310 cx.spawn(async move |_, cx| {
311 cx.update(|cx| provider.authenticate(cx))?.await?;
312 anyhow::Ok(())
313 })
314 .detach_and_log_err(cx);
315
316 if configuration_error().is_none() {
317 handle_assist(window, cx);
318 }
319 } else {
320 cx.spawn_in(window, async move |_, cx| {
321 let answer = cx
322 .prompt(
323 gpui::PromptLevel::Warning,
324 &error.to_string(),
325 None,
326 &["Configure", "Cancel"],
327 )
328 .await
329 .ok();
330 if let Some(answer) = answer
331 && answer == 0
332 {
333 cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx))
334 .ok();
335 }
336 anyhow::Ok(())
337 })
338 .detach_and_log_err(cx);
339 }
340 } else {
341 handle_assist(window, cx);
342 }
343 }
344
345 fn codegen_ranges(
346 &mut self,
347 editor: &Entity<Editor>,
348 snapshot: &EditorSnapshot,
349 window: &mut Window,
350 cx: &mut App,
351 ) -> Option<(Vec<Range<Anchor>>, Selection<Point>)> {
352 let (initial_selections, newest_selection) = editor.update(cx, |editor, _| {
353 (
354 editor.selections.all::<Point>(&snapshot.display_snapshot),
355 editor
356 .selections
357 .newest::<Point>(&snapshot.display_snapshot),
358 )
359 });
360
361 // Check if there is already an inline assistant that contains the
362 // newest selection, if there is, focus it
363 if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
364 for assist_id in &editor_assists.assist_ids {
365 let assist = &self.assists[assist_id];
366 let range = assist.range.to_point(&snapshot.buffer_snapshot());
367 if range.start.row <= newest_selection.start.row
368 && newest_selection.end.row <= range.end.row
369 {
370 self.focus_assist(*assist_id, window, cx);
371 return None;
372 }
373 }
374 }
375
376 let mut selections = Vec::<Selection<Point>>::new();
377 let mut newest_selection = None;
378 for mut selection in initial_selections {
379 if selection.end == selection.start
380 && let Some(fold) =
381 snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
382 {
383 selection.start = fold.range().start;
384 selection.end = fold.range().end;
385 if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot().max_row() {
386 let chars = snapshot
387 .buffer_snapshot()
388 .chars_at(Point::new(selection.end.row + 1, 0));
389
390 for c in chars {
391 if c == '\n' {
392 break;
393 }
394 if c.is_whitespace() {
395 continue;
396 }
397 if snapshot
398 .language_at(selection.end)
399 .is_some_and(|language| language.config().brackets.is_closing_brace(c))
400 {
401 selection.end.row += 1;
402 selection.end.column = snapshot
403 .buffer_snapshot()
404 .line_len(MultiBufferRow(selection.end.row));
405 }
406 }
407 }
408 } else {
409 selection.start.column = 0;
410 // If the selection ends at the start of the line, we don't want to include it.
411 if selection.end.column == 0 && selection.start.row != selection.end.row {
412 selection.end.row -= 1;
413 }
414 selection.end.column = snapshot
415 .buffer_snapshot()
416 .line_len(MultiBufferRow(selection.end.row));
417 }
418
419 if let Some(prev_selection) = selections.last_mut()
420 && selection.start <= prev_selection.end
421 {
422 prev_selection.end = selection.end;
423 continue;
424 }
425
426 let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
427 if selection.id > latest_selection.id {
428 *latest_selection = selection.clone();
429 }
430 selections.push(selection);
431 }
432 let snapshot = &snapshot.buffer_snapshot();
433 let newest_selection = newest_selection.unwrap();
434
435 let mut codegen_ranges = Vec::new();
436 for (buffer, buffer_range, excerpt_id) in
437 snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| {
438 snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
439 }))
440 {
441 let anchor_range = Anchor::range_in_buffer(
442 excerpt_id,
443 buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end),
444 );
445
446 codegen_ranges.push(anchor_range);
447
448 if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
449 telemetry::event!(
450 "Assistant Invoked",
451 kind = "inline",
452 phase = "invoked",
453 model = model.model.telemetry_id(),
454 model_provider = model.provider.id().to_string(),
455 language_name = buffer.language().map(|language| language.name().to_proto())
456 );
457
458 report_anthropic_event(
459 &model.model,
460 AnthropicEventData {
461 completion_type: language_model::AnthropicCompletionType::Editor,
462 event: language_model::AnthropicEventType::Invoked,
463 language_name: buffer.language().map(|language| language.name().to_proto()),
464 message_id: None,
465 },
466 cx,
467 );
468 }
469 }
470
471 Some((codegen_ranges, newest_selection))
472 }
473
474 fn batch_assist(
475 &mut self,
476 editor: &Entity<Editor>,
477 workspace: WeakEntity<Workspace>,
478 project: WeakEntity<Project>,
479 thread_store: Entity<HistoryStore>,
480 prompt_store: Option<Entity<PromptStore>>,
481 initial_prompt: Option<String>,
482 window: &mut Window,
483 codegen_ranges: &[Range<Anchor>],
484 newest_selection: Option<Selection<Point>>,
485 initial_transaction_id: Option<TransactionId>,
486 cx: &mut App,
487 ) -> Option<InlineAssistId> {
488 let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
489
490 let assist_group_id = self.next_assist_group_id.post_inc();
491 let session_id = Uuid::new_v4();
492 let prompt_buffer = cx.new(|cx| {
493 MultiBuffer::singleton(
494 cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
495 cx,
496 )
497 });
498
499 let mut assists = Vec::new();
500 let mut assist_to_focus = None;
501
502 for range in codegen_ranges {
503 let assist_id = self.next_assist_id.post_inc();
504 let codegen = cx.new(|cx| {
505 BufferCodegen::new(
506 editor.read(cx).buffer().clone(),
507 range.clone(),
508 initial_transaction_id,
509 session_id,
510 self.prompt_builder.clone(),
511 cx,
512 )
513 });
514
515 let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
516 let prompt_editor = cx.new(|cx| {
517 PromptEditor::new_buffer(
518 assist_id,
519 editor_margins,
520 self.prompt_history.clone(),
521 prompt_buffer.clone(),
522 codegen.clone(),
523 session_id,
524 self.fs.clone(),
525 thread_store.clone(),
526 prompt_store.clone(),
527 project.clone(),
528 workspace.clone(),
529 window,
530 cx,
531 )
532 });
533
534 if let Some(newest_selection) = newest_selection.as_ref()
535 && assist_to_focus.is_none()
536 {
537 let focus_assist = if newest_selection.reversed {
538 range.start.to_point(&snapshot) == newest_selection.start
539 } else {
540 range.end.to_point(&snapshot) == newest_selection.end
541 };
542 if focus_assist {
543 assist_to_focus = Some(assist_id);
544 }
545 }
546
547 let [prompt_block_id, tool_description_block_id, end_block_id] =
548 self.insert_assist_blocks(&editor, &range, &prompt_editor, cx);
549
550 assists.push((
551 assist_id,
552 range.clone(),
553 prompt_editor,
554 prompt_block_id,
555 tool_description_block_id,
556 end_block_id,
557 ));
558 }
559
560 let editor_assists = self
561 .assists_by_editor
562 .entry(editor.downgrade())
563 .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
564
565 let assist_to_focus = if let Some(focus_id) = assist_to_focus {
566 Some(focus_id)
567 } else if assists.len() >= 1 {
568 Some(assists[0].0)
569 } else {
570 None
571 };
572
573 let mut assist_group = InlineAssistGroup::new();
574 for (
575 assist_id,
576 range,
577 prompt_editor,
578 prompt_block_id,
579 tool_description_block_id,
580 end_block_id,
581 ) in assists
582 {
583 let codegen = prompt_editor.read(cx).codegen().clone();
584
585 self.assists.insert(
586 assist_id,
587 InlineAssist::new(
588 assist_id,
589 assist_group_id,
590 editor,
591 &prompt_editor,
592 prompt_block_id,
593 tool_description_block_id,
594 end_block_id,
595 range,
596 codegen,
597 workspace.clone(),
598 window,
599 cx,
600 ),
601 );
602 assist_group.assist_ids.push(assist_id);
603 editor_assists.assist_ids.push(assist_id);
604 }
605
606 self.assist_groups.insert(assist_group_id, assist_group);
607
608 assist_to_focus
609 }
610
611 pub fn assist(
612 &mut self,
613 editor: &Entity<Editor>,
614 workspace: WeakEntity<Workspace>,
615 project: WeakEntity<Project>,
616 thread_store: Entity<HistoryStore>,
617 prompt_store: Option<Entity<PromptStore>>,
618 initial_prompt: Option<String>,
619 window: &mut Window,
620 cx: &mut App,
621 ) -> Option<InlineAssistId> {
622 let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
623
624 let Some((codegen_ranges, newest_selection)) =
625 self.codegen_ranges(editor, &snapshot, window, cx)
626 else {
627 return None;
628 };
629
630 let assist_to_focus = self.batch_assist(
631 editor,
632 workspace,
633 project,
634 thread_store,
635 prompt_store,
636 initial_prompt,
637 window,
638 &codegen_ranges,
639 Some(newest_selection),
640 None,
641 cx,
642 );
643
644 if let Some(assist_id) = assist_to_focus {
645 self.focus_assist(assist_id, window, cx);
646 }
647
648 assist_to_focus
649 }
650
651 pub fn suggest_assist(
652 &mut self,
653 editor: &Entity<Editor>,
654 mut range: Range<Anchor>,
655 initial_prompt: String,
656 initial_transaction_id: Option<TransactionId>,
657 focus: bool,
658 workspace: Entity<Workspace>,
659 thread_store: Entity<HistoryStore>,
660 prompt_store: Option<Entity<PromptStore>>,
661 window: &mut Window,
662 cx: &mut App,
663 ) -> InlineAssistId {
664 let buffer = editor.read(cx).buffer().clone();
665 {
666 let snapshot = buffer.read(cx).read(cx);
667 range.start = range.start.bias_left(&snapshot);
668 range.end = range.end.bias_right(&snapshot);
669 }
670
671 let project = workspace.read(cx).project().downgrade();
672
673 let assist_id = self
674 .batch_assist(
675 editor,
676 workspace.downgrade(),
677 project,
678 thread_store,
679 prompt_store,
680 Some(initial_prompt),
681 window,
682 &[range],
683 None,
684 initial_transaction_id,
685 cx,
686 )
687 .expect("batch_assist returns an id if there's only one range");
688
689 if focus {
690 self.focus_assist(assist_id, window, cx);
691 }
692
693 assist_id
694 }
695
696 fn insert_assist_blocks(
697 &self,
698 editor: &Entity<Editor>,
699 range: &Range<Anchor>,
700 prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
701 cx: &mut App,
702 ) -> [CustomBlockId; 3] {
703 let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
704 prompt_editor
705 .editor
706 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1 + 2)
707 });
708 let assist_blocks = vec![
709 BlockProperties {
710 style: BlockStyle::Sticky,
711 placement: BlockPlacement::Above(range.start),
712 height: Some(prompt_editor_height),
713 render: build_assist_editor_renderer(prompt_editor),
714 priority: 0,
715 },
716 // Placeholder for tool description - will be updated dynamically
717 BlockProperties {
718 style: BlockStyle::Flex,
719 placement: BlockPlacement::Below(range.end),
720 height: Some(0),
721 render: Arc::new(|_cx| div().into_any_element()),
722 priority: 0,
723 },
724 BlockProperties {
725 style: BlockStyle::Sticky,
726 placement: BlockPlacement::Below(range.end),
727 height: None,
728 render: Arc::new(|cx| {
729 v_flex()
730 .h_full()
731 .w_full()
732 .border_t_1()
733 .border_color(cx.theme().status().info_border)
734 .into_any_element()
735 }),
736 priority: 0,
737 },
738 ];
739
740 editor.update(cx, |editor, cx| {
741 let block_ids = editor.insert_blocks(assist_blocks, None, cx);
742 [block_ids[0], block_ids[1], block_ids[2]]
743 })
744 }
745
746 fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut App) {
747 let assist = &self.assists[&assist_id];
748 let Some(decorations) = assist.decorations.as_ref() else {
749 return;
750 };
751 let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
752 let editor_assists = self.assists_by_editor.get_mut(&assist.editor).unwrap();
753
754 assist_group.active_assist_id = Some(assist_id);
755 if assist_group.linked {
756 for assist_id in &assist_group.assist_ids {
757 if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
758 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
759 prompt_editor.set_show_cursor_when_unfocused(true, cx)
760 });
761 }
762 }
763 }
764
765 assist
766 .editor
767 .update(cx, |editor, cx| {
768 let scroll_top = editor.scroll_position(cx).y;
769 let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
770 editor_assists.scroll_lock = editor
771 .row_for_block(decorations.prompt_block_id, cx)
772 .map(|row| row.as_f64())
773 .filter(|prompt_row| (scroll_top..scroll_bottom).contains(&prompt_row))
774 .map(|prompt_row| InlineAssistScrollLock {
775 assist_id,
776 distance_from_top: prompt_row - scroll_top,
777 });
778 })
779 .ok();
780 }
781
782 fn handle_prompt_editor_focus_out(&mut self, assist_id: InlineAssistId, cx: &mut App) {
783 let assist = &self.assists[&assist_id];
784 let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
785 if assist_group.active_assist_id == Some(assist_id) {
786 assist_group.active_assist_id = None;
787 if assist_group.linked {
788 for assist_id in &assist_group.assist_ids {
789 if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
790 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
791 prompt_editor.set_show_cursor_when_unfocused(false, cx)
792 });
793 }
794 }
795 }
796 }
797 }
798
799 fn handle_prompt_editor_event(
800 &mut self,
801 prompt_editor: Entity<PromptEditor<BufferCodegen>>,
802 event: &PromptEditorEvent,
803 window: &mut Window,
804 cx: &mut App,
805 ) {
806 let assist_id = prompt_editor.read(cx).id();
807 match event {
808 PromptEditorEvent::StartRequested => {
809 self.start_assist(assist_id, window, cx);
810 }
811 PromptEditorEvent::StopRequested => {
812 self.stop_assist(assist_id, cx);
813 }
814 PromptEditorEvent::ConfirmRequested { execute: _ } => {
815 self.finish_assist(assist_id, false, window, cx);
816 }
817 PromptEditorEvent::CancelRequested => {
818 self.finish_assist(assist_id, true, window, cx);
819 }
820 PromptEditorEvent::Resized { .. } => {
821 // This only matters for the terminal inline assistant
822 }
823 }
824 }
825
826 fn handle_editor_newline(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
827 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
828 return;
829 };
830
831 if editor.read(cx).selections.count() == 1 {
832 let (selection, buffer) = editor.update(cx, |editor, cx| {
833 (
834 editor
835 .selections
836 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
837 editor.buffer().read(cx).snapshot(cx),
838 )
839 });
840 for assist_id in &editor_assists.assist_ids {
841 let assist = &self.assists[assist_id];
842 let assist_range = assist.range.to_offset(&buffer);
843 if assist_range.contains(&selection.start) && assist_range.contains(&selection.end)
844 {
845 if matches!(assist.codegen.read(cx).status(cx), CodegenStatus::Pending) {
846 self.dismiss_assist(*assist_id, window, cx);
847 } else {
848 self.finish_assist(*assist_id, false, window, cx);
849 }
850
851 return;
852 }
853 }
854 }
855
856 cx.propagate();
857 }
858
859 fn handle_editor_cancel(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
860 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
861 return;
862 };
863
864 if editor.read(cx).selections.count() == 1 {
865 let (selection, buffer) = editor.update(cx, |editor, cx| {
866 (
867 editor
868 .selections
869 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
870 editor.buffer().read(cx).snapshot(cx),
871 )
872 });
873 let mut closest_assist_fallback = None;
874 for assist_id in &editor_assists.assist_ids {
875 let assist = &self.assists[assist_id];
876 let assist_range = assist.range.to_offset(&buffer);
877 if assist.decorations.is_some() {
878 if assist_range.contains(&selection.start)
879 && assist_range.contains(&selection.end)
880 {
881 self.focus_assist(*assist_id, window, cx);
882 return;
883 } else {
884 let distance_from_selection = assist_range
885 .start
886 .0
887 .abs_diff(selection.start.0)
888 .min(assist_range.start.0.abs_diff(selection.end.0))
889 + assist_range
890 .end
891 .0
892 .abs_diff(selection.start.0)
893 .min(assist_range.end.0.abs_diff(selection.end.0));
894 match closest_assist_fallback {
895 Some((_, old_distance)) => {
896 if distance_from_selection < old_distance {
897 closest_assist_fallback =
898 Some((assist_id, distance_from_selection));
899 }
900 }
901 None => {
902 closest_assist_fallback = Some((assist_id, distance_from_selection))
903 }
904 }
905 }
906 }
907 }
908
909 if let Some((&assist_id, _)) = closest_assist_fallback {
910 self.focus_assist(assist_id, window, cx);
911 }
912 }
913
914 cx.propagate();
915 }
916
917 fn handle_editor_release(
918 &mut self,
919 editor: WeakEntity<Editor>,
920 window: &mut Window,
921 cx: &mut App,
922 ) {
923 if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) {
924 for assist_id in editor_assists.assist_ids.clone() {
925 self.finish_assist(assist_id, true, window, cx);
926 }
927 }
928 }
929
930 fn handle_editor_change(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
931 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
932 return;
933 };
934 let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() else {
935 return;
936 };
937 let assist = &self.assists[&scroll_lock.assist_id];
938 let Some(decorations) = assist.decorations.as_ref() else {
939 return;
940 };
941
942 editor.update(cx, |editor, cx| {
943 let scroll_position = editor.scroll_position(cx);
944 let target_scroll_top = editor
945 .row_for_block(decorations.prompt_block_id, cx)?
946 .as_f64()
947 - scroll_lock.distance_from_top;
948 if target_scroll_top != scroll_position.y {
949 editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx);
950 }
951 Some(())
952 });
953 }
954
955 fn handle_editor_event(
956 &mut self,
957 editor: Entity<Editor>,
958 event: &EditorEvent,
959 window: &mut Window,
960 cx: &mut App,
961 ) {
962 let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else {
963 return;
964 };
965
966 match event {
967 EditorEvent::Edited { transaction_id } => {
968 let buffer = editor.read(cx).buffer().read(cx);
969 let edited_ranges =
970 buffer.edited_ranges_for_transaction::<MultiBufferOffset>(*transaction_id, cx);
971 let snapshot = buffer.snapshot(cx);
972
973 for assist_id in editor_assists.assist_ids.clone() {
974 let assist = &self.assists[&assist_id];
975 if matches!(
976 assist.codegen.read(cx).status(cx),
977 CodegenStatus::Error(_) | CodegenStatus::Done
978 ) {
979 let assist_range = assist.range.to_offset(&snapshot);
980 if edited_ranges
981 .iter()
982 .any(|range| range.overlaps(&assist_range))
983 {
984 self.finish_assist(assist_id, false, window, cx);
985 }
986 }
987 }
988 }
989 EditorEvent::ScrollPositionChanged { .. } => {
990 if let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() {
991 let assist = &self.assists[&scroll_lock.assist_id];
992 if let Some(decorations) = assist.decorations.as_ref() {
993 let distance_from_top = editor.update(cx, |editor, cx| {
994 let scroll_top = editor.scroll_position(cx).y;
995 let prompt_row = editor
996 .row_for_block(decorations.prompt_block_id, cx)?
997 .0 as ScrollOffset;
998 Some(prompt_row - scroll_top)
999 });
1000
1001 if distance_from_top.is_none_or(|distance_from_top| {
1002 distance_from_top != scroll_lock.distance_from_top
1003 }) {
1004 editor_assists.scroll_lock = None;
1005 }
1006 }
1007 }
1008 }
1009 EditorEvent::SelectionsChanged { .. } => {
1010 for assist_id in editor_assists.assist_ids.clone() {
1011 let assist = &self.assists[&assist_id];
1012 if let Some(decorations) = assist.decorations.as_ref()
1013 && decorations
1014 .prompt_editor
1015 .focus_handle(cx)
1016 .is_focused(window)
1017 {
1018 return;
1019 }
1020 }
1021
1022 editor_assists.scroll_lock = None;
1023 }
1024 _ => {}
1025 }
1026 }
1027
1028 pub fn finish_assist(
1029 &mut self,
1030 assist_id: InlineAssistId,
1031 undo: bool,
1032 window: &mut Window,
1033 cx: &mut App,
1034 ) {
1035 if let Some(assist) = self.assists.get(&assist_id) {
1036 let assist_group_id = assist.group_id;
1037 if self.assist_groups[&assist_group_id].linked {
1038 for assist_id in self.unlink_assist_group(assist_group_id, window, cx) {
1039 self.finish_assist(assist_id, undo, window, cx);
1040 }
1041 return;
1042 }
1043 }
1044
1045 self.dismiss_assist(assist_id, window, cx);
1046
1047 if let Some(assist) = self.assists.remove(&assist_id) {
1048 if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id)
1049 {
1050 entry.get_mut().assist_ids.retain(|id| *id != assist_id);
1051 if entry.get().assist_ids.is_empty() {
1052 entry.remove();
1053 }
1054 }
1055
1056 if let hash_map::Entry::Occupied(mut entry) =
1057 self.assists_by_editor.entry(assist.editor.clone())
1058 {
1059 entry.get_mut().assist_ids.retain(|id| *id != assist_id);
1060 if entry.get().assist_ids.is_empty() {
1061 entry.remove();
1062 if let Some(editor) = assist.editor.upgrade() {
1063 self.update_editor_highlights(&editor, cx);
1064 }
1065 } else {
1066 entry.get_mut().highlight_updates.send(()).ok();
1067 }
1068 }
1069
1070 let active_alternative = assist.codegen.read(cx).active_alternative().clone();
1071 if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
1072 let language_name = assist.editor.upgrade().and_then(|editor| {
1073 let multibuffer = editor.read(cx).buffer().read(cx);
1074 let snapshot = multibuffer.snapshot(cx);
1075 let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
1076 ranges
1077 .first()
1078 .and_then(|(buffer, _, _)| buffer.language())
1079 .map(|language| language.name().0.to_string())
1080 });
1081
1082 let codegen = assist.codegen.read(cx);
1083 let session_id = codegen.session_id();
1084 let message_id = active_alternative.read(cx).message_id.clone();
1085 let model_telemetry_id = model.model.telemetry_id();
1086 let model_provider_id = model.model.provider_id().to_string();
1087
1088 let (phase, event_type, anthropic_event_type) = if undo {
1089 (
1090 "rejected",
1091 "Assistant Response Rejected",
1092 language_model::AnthropicEventType::Reject,
1093 )
1094 } else {
1095 (
1096 "accepted",
1097 "Assistant Response Accepted",
1098 language_model::AnthropicEventType::Accept,
1099 )
1100 };
1101
1102 telemetry::event!(
1103 event_type,
1104 phase,
1105 session_id = session_id.to_string(),
1106 kind = "inline",
1107 model = model_telemetry_id,
1108 model_provider = model_provider_id,
1109 language_name = language_name,
1110 message_id = message_id.as_deref(),
1111 );
1112
1113 report_anthropic_event(
1114 &model.model,
1115 language_model::AnthropicEventData {
1116 completion_type: language_model::AnthropicCompletionType::Editor,
1117 event: anthropic_event_type,
1118 language_name,
1119 message_id,
1120 },
1121 cx,
1122 );
1123 }
1124
1125 if undo {
1126 assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
1127 } else {
1128 self.confirmed_assists.insert(assist_id, active_alternative);
1129 }
1130 }
1131 }
1132
1133 fn dismiss_assist(
1134 &mut self,
1135 assist_id: InlineAssistId,
1136 window: &mut Window,
1137 cx: &mut App,
1138 ) -> bool {
1139 let Some(assist) = self.assists.get_mut(&assist_id) else {
1140 return false;
1141 };
1142 let Some(editor) = assist.editor.upgrade() else {
1143 return false;
1144 };
1145 let Some(decorations) = assist.decorations.take() else {
1146 return false;
1147 };
1148
1149 editor.update(cx, |editor, cx| {
1150 let mut to_remove = decorations.removed_line_block_ids;
1151 to_remove.insert(decorations.prompt_block_id);
1152 to_remove.insert(decorations.end_block_id);
1153 if let Some(tool_description_block_id) = decorations.model_explanation {
1154 to_remove.insert(tool_description_block_id);
1155 }
1156 editor.remove_blocks(to_remove, None, cx);
1157 });
1158
1159 if decorations
1160 .prompt_editor
1161 .focus_handle(cx)
1162 .contains_focused(window, cx)
1163 {
1164 self.focus_next_assist(assist_id, window, cx);
1165 }
1166
1167 if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
1168 if editor_assists
1169 .scroll_lock
1170 .as_ref()
1171 .is_some_and(|lock| lock.assist_id == assist_id)
1172 {
1173 editor_assists.scroll_lock = None;
1174 }
1175 editor_assists.highlight_updates.send(()).ok();
1176 }
1177
1178 true
1179 }
1180
1181 fn focus_next_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1182 let Some(assist) = self.assists.get(&assist_id) else {
1183 return;
1184 };
1185
1186 let assist_group = &self.assist_groups[&assist.group_id];
1187 let assist_ix = assist_group
1188 .assist_ids
1189 .iter()
1190 .position(|id| *id == assist_id)
1191 .unwrap();
1192 let assist_ids = assist_group
1193 .assist_ids
1194 .iter()
1195 .skip(assist_ix + 1)
1196 .chain(assist_group.assist_ids.iter().take(assist_ix));
1197
1198 for assist_id in assist_ids {
1199 let assist = &self.assists[assist_id];
1200 if assist.decorations.is_some() {
1201 self.focus_assist(*assist_id, window, cx);
1202 return;
1203 }
1204 }
1205
1206 assist
1207 .editor
1208 .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
1209 .ok();
1210 }
1211
1212 fn focus_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1213 let Some(assist) = self.assists.get(&assist_id) else {
1214 return;
1215 };
1216
1217 if let Some(decorations) = assist.decorations.as_ref() {
1218 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
1219 prompt_editor.editor.update(cx, |editor, cx| {
1220 window.focus(&editor.focus_handle(cx));
1221 editor.select_all(&SelectAll, window, cx);
1222 })
1223 });
1224 }
1225
1226 self.scroll_to_assist(assist_id, window, cx);
1227 }
1228
1229 pub fn scroll_to_assist(
1230 &mut self,
1231 assist_id: InlineAssistId,
1232 window: &mut Window,
1233 cx: &mut App,
1234 ) {
1235 let Some(assist) = self.assists.get(&assist_id) else {
1236 return;
1237 };
1238 let Some(editor) = assist.editor.upgrade() else {
1239 return;
1240 };
1241
1242 let position = assist.range.start;
1243 editor.update(cx, |editor, cx| {
1244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
1245 selections.select_anchor_ranges([position..position])
1246 });
1247
1248 let mut scroll_target_range = None;
1249 if let Some(decorations) = assist.decorations.as_ref() {
1250 scroll_target_range = maybe!({
1251 let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f64;
1252 let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f64;
1253 Some((top, bottom))
1254 });
1255 if scroll_target_range.is_none() {
1256 log::error!("bug: failed to find blocks for scrolling to inline assist");
1257 }
1258 }
1259 let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
1260 let snapshot = editor.snapshot(window, cx);
1261 let start_row = assist
1262 .range
1263 .start
1264 .to_display_point(&snapshot.display_snapshot)
1265 .row();
1266 let top = start_row.0 as ScrollOffset;
1267 let bottom = top + 1.0;
1268 (top, bottom)
1269 });
1270 let mut scroll_target_top = scroll_target_range.0;
1271 let mut scroll_target_bottom = scroll_target_range.1;
1272
1273 scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
1274 scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
1275
1276 let height_in_lines = editor.visible_line_count().unwrap_or(0.);
1277 let scroll_top = editor.scroll_position(cx).y;
1278 let scroll_bottom = scroll_top + height_in_lines;
1279
1280 if scroll_target_top < scroll_top {
1281 editor.set_scroll_position(point(0., scroll_target_top), window, cx);
1282 } else if scroll_target_bottom > scroll_bottom {
1283 if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
1284 editor.set_scroll_position(
1285 point(0., scroll_target_bottom - height_in_lines),
1286 window,
1287 cx,
1288 );
1289 } else {
1290 editor.set_scroll_position(point(0., scroll_target_top), window, cx);
1291 }
1292 }
1293 });
1294 }
1295
1296 fn unlink_assist_group(
1297 &mut self,
1298 assist_group_id: InlineAssistGroupId,
1299 window: &mut Window,
1300 cx: &mut App,
1301 ) -> Vec<InlineAssistId> {
1302 let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
1303 assist_group.linked = false;
1304
1305 for assist_id in &assist_group.assist_ids {
1306 let assist = self.assists.get_mut(assist_id).unwrap();
1307 if let Some(editor_decorations) = assist.decorations.as_ref() {
1308 editor_decorations
1309 .prompt_editor
1310 .update(cx, |prompt_editor, cx| prompt_editor.unlink(window, cx));
1311 }
1312 }
1313 assist_group.assist_ids.clone()
1314 }
1315
1316 pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1317 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1318 assist
1319 } else {
1320 return;
1321 };
1322
1323 let assist_group_id = assist.group_id;
1324 if self.assist_groups[&assist_group_id].linked {
1325 for assist_id in self.unlink_assist_group(assist_group_id, window, cx) {
1326 self.start_assist(assist_id, window, cx);
1327 }
1328 return;
1329 }
1330
1331 let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
1332 else {
1333 return;
1334 };
1335
1336 self.prompt_history.retain(|prompt| *prompt != user_prompt);
1337 self.prompt_history.push_back(user_prompt.clone());
1338 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
1339 self.prompt_history.pop_front();
1340 }
1341
1342 let Some(ConfiguredModel { model, .. }) =
1343 LanguageModelRegistry::read_global(cx).inline_assistant_model()
1344 else {
1345 return;
1346 };
1347
1348 let context_task = load_context(&mention_set, cx).shared();
1349 assist
1350 .codegen
1351 .update(cx, |codegen, cx| {
1352 codegen.start(model, user_prompt, context_task, cx)
1353 })
1354 .log_err();
1355 }
1356
1357 pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) {
1358 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1359 assist
1360 } else {
1361 return;
1362 };
1363
1364 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
1365 }
1366
1367 fn update_editor_highlights(&self, editor: &Entity<Editor>, cx: &mut App) {
1368 let mut gutter_pending_ranges = Vec::new();
1369 let mut gutter_transformed_ranges = Vec::new();
1370 let mut foreground_ranges = Vec::new();
1371 let mut inserted_row_ranges = Vec::new();
1372 let empty_assist_ids = Vec::new();
1373 let assist_ids = self
1374 .assists_by_editor
1375 .get(&editor.downgrade())
1376 .map_or(&empty_assist_ids, |editor_assists| {
1377 &editor_assists.assist_ids
1378 });
1379
1380 for assist_id in assist_ids {
1381 if let Some(assist) = self.assists.get(assist_id) {
1382 let codegen = assist.codegen.read(cx);
1383 let buffer = codegen.buffer(cx).read(cx).read(cx);
1384 foreground_ranges.extend(codegen.last_equal_ranges(cx).iter().cloned());
1385
1386 let pending_range =
1387 codegen.edit_position(cx).unwrap_or(assist.range.start)..assist.range.end;
1388 if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
1389 gutter_pending_ranges.push(pending_range);
1390 }
1391
1392 if let Some(edit_position) = codegen.edit_position(cx) {
1393 let edited_range = assist.range.start..edit_position;
1394 if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
1395 gutter_transformed_ranges.push(edited_range);
1396 }
1397 }
1398
1399 if assist.decorations.is_some() {
1400 inserted_row_ranges
1401 .extend(codegen.diff(cx).inserted_row_ranges.iter().cloned());
1402 }
1403 }
1404 }
1405
1406 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
1407 merge_ranges(&mut foreground_ranges, &snapshot);
1408 merge_ranges(&mut gutter_pending_ranges, &snapshot);
1409 merge_ranges(&mut gutter_transformed_ranges, &snapshot);
1410 editor.update(cx, |editor, cx| {
1411 enum GutterPendingRange {}
1412 if gutter_pending_ranges.is_empty() {
1413 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1414 } else {
1415 editor.highlight_gutter::<GutterPendingRange>(
1416 gutter_pending_ranges,
1417 |cx| cx.theme().status().info_background,
1418 cx,
1419 )
1420 }
1421
1422 enum GutterTransformedRange {}
1423 if gutter_transformed_ranges.is_empty() {
1424 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1425 } else {
1426 editor.highlight_gutter::<GutterTransformedRange>(
1427 gutter_transformed_ranges,
1428 |cx| cx.theme().status().info,
1429 cx,
1430 )
1431 }
1432
1433 if foreground_ranges.is_empty() {
1434 editor.clear_highlights::<InlineAssist>(cx);
1435 } else {
1436 editor.highlight_text::<InlineAssist>(
1437 foreground_ranges,
1438 HighlightStyle {
1439 fade_out: Some(0.6),
1440 ..Default::default()
1441 },
1442 cx,
1443 );
1444 }
1445
1446 editor.clear_row_highlights::<InlineAssist>();
1447 for row_range in inserted_row_ranges {
1448 editor.highlight_rows::<InlineAssist>(
1449 row_range,
1450 cx.theme().status().info_background,
1451 Default::default(),
1452 cx,
1453 );
1454 }
1455 });
1456 }
1457
1458 fn update_editor_blocks(
1459 &mut self,
1460 editor: &Entity<Editor>,
1461 assist_id: InlineAssistId,
1462 window: &mut Window,
1463 cx: &mut App,
1464 ) {
1465 let Some(assist) = self.assists.get_mut(&assist_id) else {
1466 return;
1467 };
1468 let Some(decorations) = assist.decorations.as_mut() else {
1469 return;
1470 };
1471
1472 let codegen = assist.codegen.read(cx);
1473 let old_snapshot = codegen.snapshot(cx);
1474 let old_buffer = codegen.old_buffer(cx);
1475 let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
1476
1477 editor.update(cx, |editor, cx| {
1478 let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
1479 editor.remove_blocks(old_blocks, None, cx);
1480
1481 let mut new_blocks = Vec::new();
1482 for (new_row, old_row_range) in deleted_row_ranges {
1483 let (_, buffer_start) = old_snapshot
1484 .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
1485 .unwrap();
1486 let (_, buffer_end) = old_snapshot
1487 .point_to_buffer_offset(Point::new(
1488 *old_row_range.end(),
1489 old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
1490 ))
1491 .unwrap();
1492
1493 let deleted_lines_editor = cx.new(|cx| {
1494 let multi_buffer =
1495 cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
1496 multi_buffer.update(cx, |multi_buffer, cx| {
1497 multi_buffer.push_excerpts(
1498 old_buffer.clone(),
1499 // todo(lw): buffer_start and buffer_end might come from different snapshots!
1500 Some(ExcerptRange::new(buffer_start..buffer_end)),
1501 cx,
1502 );
1503 });
1504
1505 enum DeletedLines {}
1506 let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
1507 editor.disable_scrollbars_and_minimap(window, cx);
1508 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1509 editor.set_show_wrap_guides(false, cx);
1510 editor.set_show_gutter(false, cx);
1511 editor.set_offset_content(false, cx);
1512 editor.scroll_manager.set_forbid_vertical_scroll(true);
1513 editor.set_read_only(true);
1514 editor.set_show_edit_predictions(Some(false), window, cx);
1515 editor.highlight_rows::<DeletedLines>(
1516 Anchor::min()..Anchor::max(),
1517 cx.theme().status().deleted_background,
1518 Default::default(),
1519 cx,
1520 );
1521 editor
1522 });
1523
1524 let height =
1525 deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
1526 new_blocks.push(BlockProperties {
1527 placement: BlockPlacement::Above(new_row),
1528 height: Some(height),
1529 style: BlockStyle::Flex,
1530 render: Arc::new(move |cx| {
1531 div()
1532 .block_mouse_except_scroll()
1533 .bg(cx.theme().status().deleted_background)
1534 .size_full()
1535 .h(height as f32 * cx.window.line_height())
1536 .pl(cx.margins.gutter.full_width())
1537 .child(deleted_lines_editor.clone())
1538 .into_any_element()
1539 }),
1540 priority: 0,
1541 });
1542 }
1543
1544 decorations.removed_line_block_ids = editor
1545 .insert_blocks(new_blocks, None, cx)
1546 .into_iter()
1547 .collect();
1548 })
1549 }
1550
1551 fn resolve_inline_assist_target(
1552 workspace: &mut Workspace,
1553 agent_panel: Option<Entity<AgentPanel>>,
1554 window: &mut Window,
1555 cx: &mut App,
1556 ) -> Option<InlineAssistTarget> {
1557 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx)
1558 && terminal_panel
1559 .read(cx)
1560 .focus_handle(cx)
1561 .contains_focused(window, cx)
1562 && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
1563 pane.read(cx)
1564 .active_item()
1565 .and_then(|t| t.downcast::<TerminalView>())
1566 })
1567 {
1568 return Some(InlineAssistTarget::Terminal(terminal_view));
1569 }
1570
1571 let text_thread_editor = agent_panel
1572 .and_then(|panel| panel.read(cx).active_text_thread_editor())
1573 .and_then(|editor| {
1574 let editor = &editor.read(cx).editor().clone();
1575 if editor.read(cx).is_focused(window) {
1576 Some(editor.clone())
1577 } else {
1578 None
1579 }
1580 });
1581
1582 if let Some(text_thread_editor) = text_thread_editor {
1583 Some(InlineAssistTarget::Editor(text_thread_editor))
1584 } else if let Some(workspace_editor) = workspace
1585 .active_item(cx)
1586 .and_then(|item| item.act_as::<Editor>(cx))
1587 {
1588 Some(InlineAssistTarget::Editor(workspace_editor))
1589 } else {
1590 workspace
1591 .active_item(cx)
1592 .and_then(|item| item.act_as::<TerminalView>(cx))
1593 .map(InlineAssistTarget::Terminal)
1594 }
1595 }
1596}
1597
1598struct EditorInlineAssists {
1599 assist_ids: Vec<InlineAssistId>,
1600 scroll_lock: Option<InlineAssistScrollLock>,
1601 highlight_updates: watch::Sender<()>,
1602 _update_highlights: Task<Result<()>>,
1603 _subscriptions: Vec<gpui::Subscription>,
1604}
1605
1606struct InlineAssistScrollLock {
1607 assist_id: InlineAssistId,
1608 distance_from_top: ScrollOffset,
1609}
1610
1611impl EditorInlineAssists {
1612 fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
1613 let (highlight_updates_tx, mut highlight_updates_rx) = watch::channel(());
1614 Self {
1615 assist_ids: Vec::new(),
1616 scroll_lock: None,
1617 highlight_updates: highlight_updates_tx,
1618 _update_highlights: cx.spawn({
1619 let editor = editor.downgrade();
1620 async move |cx| {
1621 while let Ok(()) = highlight_updates_rx.changed().await {
1622 let editor = editor.upgrade().context("editor was dropped")?;
1623 cx.update_global(|assistant: &mut InlineAssistant, cx| {
1624 assistant.update_editor_highlights(&editor, cx);
1625 })?;
1626 }
1627 Ok(())
1628 }
1629 }),
1630 _subscriptions: vec![
1631 cx.observe_release_in(editor, window, {
1632 let editor = editor.downgrade();
1633 |_, window, cx| {
1634 InlineAssistant::update_global(cx, |this, cx| {
1635 this.handle_editor_release(editor, window, cx);
1636 })
1637 }
1638 }),
1639 window.observe(editor, cx, move |editor, window, cx| {
1640 InlineAssistant::update_global(cx, |this, cx| {
1641 this.handle_editor_change(editor, window, cx)
1642 })
1643 }),
1644 window.subscribe(editor, cx, move |editor, event, window, cx| {
1645 InlineAssistant::update_global(cx, |this, cx| {
1646 this.handle_editor_event(editor, event, window, cx)
1647 })
1648 }),
1649 editor.update(cx, |editor, cx| {
1650 let editor_handle = cx.entity().downgrade();
1651 editor.register_action(move |_: &editor::actions::Newline, window, cx| {
1652 InlineAssistant::update_global(cx, |this, cx| {
1653 if let Some(editor) = editor_handle.upgrade() {
1654 this.handle_editor_newline(editor, window, cx)
1655 }
1656 })
1657 })
1658 }),
1659 editor.update(cx, |editor, cx| {
1660 let editor_handle = cx.entity().downgrade();
1661 editor.register_action(move |_: &editor::actions::Cancel, window, cx| {
1662 InlineAssistant::update_global(cx, |this, cx| {
1663 if let Some(editor) = editor_handle.upgrade() {
1664 this.handle_editor_cancel(editor, window, cx)
1665 }
1666 })
1667 })
1668 }),
1669 ],
1670 }
1671 }
1672}
1673
1674struct InlineAssistGroup {
1675 assist_ids: Vec<InlineAssistId>,
1676 linked: bool,
1677 active_assist_id: Option<InlineAssistId>,
1678}
1679
1680impl InlineAssistGroup {
1681 fn new() -> Self {
1682 Self {
1683 assist_ids: Vec::new(),
1684 linked: true,
1685 active_assist_id: None,
1686 }
1687 }
1688}
1689
1690fn build_assist_editor_renderer(editor: &Entity<PromptEditor<BufferCodegen>>) -> RenderBlock {
1691 let editor = editor.clone();
1692
1693 Arc::new(move |cx: &mut BlockContext| {
1694 let editor_margins = editor.read(cx).editor_margins();
1695
1696 *editor_margins.lock() = *cx.margins;
1697 editor.clone().into_any_element()
1698 })
1699}
1700
1701#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1702struct InlineAssistGroupId(usize);
1703
1704impl InlineAssistGroupId {
1705 fn post_inc(&mut self) -> InlineAssistGroupId {
1706 let id = *self;
1707 self.0 += 1;
1708 id
1709 }
1710}
1711
1712pub struct InlineAssist {
1713 group_id: InlineAssistGroupId,
1714 range: Range<Anchor>,
1715 editor: WeakEntity<Editor>,
1716 decorations: Option<InlineAssistDecorations>,
1717 codegen: Entity<BufferCodegen>,
1718 _subscriptions: Vec<Subscription>,
1719 workspace: WeakEntity<Workspace>,
1720}
1721
1722impl InlineAssist {
1723 fn new(
1724 assist_id: InlineAssistId,
1725 group_id: InlineAssistGroupId,
1726 editor: &Entity<Editor>,
1727 prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
1728 prompt_block_id: CustomBlockId,
1729 tool_description_block_id: CustomBlockId,
1730 end_block_id: CustomBlockId,
1731 range: Range<Anchor>,
1732 codegen: Entity<BufferCodegen>,
1733 workspace: WeakEntity<Workspace>,
1734 window: &mut Window,
1735 cx: &mut App,
1736 ) -> Self {
1737 let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
1738 InlineAssist {
1739 group_id,
1740 editor: editor.downgrade(),
1741 decorations: Some(InlineAssistDecorations {
1742 prompt_block_id,
1743 prompt_editor: prompt_editor.clone(),
1744 removed_line_block_ids: Default::default(),
1745 model_explanation: Some(tool_description_block_id),
1746 end_block_id,
1747 }),
1748 range,
1749 codegen: codegen.clone(),
1750 workspace,
1751 _subscriptions: vec![
1752 window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| {
1753 InlineAssistant::update_global(cx, |this, cx| {
1754 this.handle_prompt_editor_focus_in(assist_id, cx)
1755 })
1756 }),
1757 window.on_focus_out(&prompt_editor_focus_handle, cx, move |_, _, cx| {
1758 InlineAssistant::update_global(cx, |this, cx| {
1759 this.handle_prompt_editor_focus_out(assist_id, cx)
1760 })
1761 }),
1762 window.subscribe(prompt_editor, cx, |prompt_editor, event, window, cx| {
1763 InlineAssistant::update_global(cx, |this, cx| {
1764 this.handle_prompt_editor_event(prompt_editor, event, window, cx)
1765 })
1766 }),
1767 window.observe(&codegen, cx, {
1768 let editor = editor.downgrade();
1769 move |_, window, cx| {
1770 if let Some(editor) = editor.upgrade() {
1771 InlineAssistant::update_global(cx, |this, cx| {
1772 if let Some(editor_assists) =
1773 this.assists_by_editor.get_mut(&editor.downgrade())
1774 {
1775 editor_assists.highlight_updates.send(()).ok();
1776 }
1777
1778 this.update_editor_blocks(&editor, assist_id, window, cx);
1779 })
1780 }
1781 }
1782 }),
1783 window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
1784 InlineAssistant::update_global(cx, |this, cx| match event {
1785 CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx),
1786 CodegenEvent::Finished => {
1787 let assist = if let Some(assist) = this.assists.get(&assist_id) {
1788 assist
1789 } else {
1790 return;
1791 };
1792
1793 if let CodegenStatus::Error(error) = codegen.read(cx).status(cx)
1794 && assist.decorations.is_none()
1795 && let Some(workspace) = assist.workspace.upgrade()
1796 {
1797 #[cfg(any(test, feature = "test-support"))]
1798 if let Some(sender) = &mut this._inline_assistant_completions {
1799 sender
1800 .unbounded_send(Err(anyhow::anyhow!(
1801 "Inline assistant error: {}",
1802 error
1803 )))
1804 .ok();
1805 }
1806
1807 let error = format!("Inline assistant error: {}", error);
1808 workspace.update(cx, |workspace, cx| {
1809 struct InlineAssistantError;
1810
1811 let id = NotificationId::composite::<InlineAssistantError>(
1812 assist_id.0,
1813 );
1814
1815 workspace.show_toast(Toast::new(id, error), cx);
1816 })
1817 } else {
1818 #[cfg(any(test, feature = "test-support"))]
1819 if let Some(sender) = &mut this._inline_assistant_completions {
1820 sender.unbounded_send(Ok(assist_id)).ok();
1821 }
1822 }
1823
1824 if assist.decorations.is_none() {
1825 this.finish_assist(assist_id, false, window, cx);
1826 }
1827 }
1828 })
1829 }),
1830 ],
1831 }
1832 }
1833
1834 fn user_prompt(&self, cx: &App) -> Option<String> {
1835 let decorations = self.decorations.as_ref()?;
1836 Some(decorations.prompt_editor.read(cx).prompt(cx))
1837 }
1838
1839 fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
1840 let decorations = self.decorations.as_ref()?;
1841 Some(decorations.prompt_editor.read(cx).mention_set().clone())
1842 }
1843}
1844
1845struct InlineAssistDecorations {
1846 prompt_block_id: CustomBlockId,
1847 prompt_editor: Entity<PromptEditor<BufferCodegen>>,
1848 removed_line_block_ids: HashSet<CustomBlockId>,
1849 model_explanation: Option<CustomBlockId>,
1850 end_block_id: CustomBlockId,
1851}
1852
1853struct AssistantCodeActionProvider {
1854 editor: WeakEntity<Editor>,
1855 workspace: WeakEntity<Workspace>,
1856}
1857
1858const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
1859
1860impl CodeActionProvider for AssistantCodeActionProvider {
1861 fn id(&self) -> Arc<str> {
1862 ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
1863 }
1864
1865 fn code_actions(
1866 &self,
1867 buffer: &Entity<Buffer>,
1868 range: Range<text::Anchor>,
1869 _: &mut Window,
1870 cx: &mut App,
1871 ) -> Task<Result<Vec<CodeAction>>> {
1872 if !AgentSettings::get_global(cx).enabled(cx) {
1873 return Task::ready(Ok(Vec::new()));
1874 }
1875
1876 let snapshot = buffer.read(cx).snapshot();
1877 let mut range = range.to_point(&snapshot);
1878
1879 // Expand the range to line boundaries.
1880 range.start.column = 0;
1881 range.end.column = snapshot.line_len(range.end.row);
1882
1883 let mut has_diagnostics = false;
1884 for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
1885 range.start = cmp::min(range.start, diagnostic.range.start);
1886 range.end = cmp::max(range.end, diagnostic.range.end);
1887 has_diagnostics = true;
1888 }
1889 if has_diagnostics {
1890 let symbols_containing_start = snapshot.symbols_containing(range.start, None);
1891 if let Some(symbol) = symbols_containing_start.last() {
1892 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1893 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1894 }
1895 let symbols_containing_end = snapshot.symbols_containing(range.end, None);
1896 if let Some(symbol) = symbols_containing_end.last() {
1897 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1898 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1899 }
1900
1901 Task::ready(Ok(vec![CodeAction {
1902 server_id: language::LanguageServerId(0),
1903 range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
1904 lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
1905 title: "Fix with Assistant".into(),
1906 ..Default::default()
1907 })),
1908 resolved: true,
1909 }]))
1910 } else {
1911 Task::ready(Ok(Vec::new()))
1912 }
1913 }
1914
1915 fn apply_code_action(
1916 &self,
1917 buffer: Entity<Buffer>,
1918 action: CodeAction,
1919 excerpt_id: ExcerptId,
1920 _push_to_history: bool,
1921 window: &mut Window,
1922 cx: &mut App,
1923 ) -> Task<Result<ProjectTransaction>> {
1924 let editor = self.editor.clone();
1925 let workspace = self.workspace.clone();
1926 let prompt_store = PromptStore::global(cx);
1927 window.spawn(cx, async move |cx| {
1928 let workspace = workspace.upgrade().context("workspace was released")?;
1929 let thread_store = cx.update(|_window, cx| {
1930 anyhow::Ok(
1931 workspace
1932 .read(cx)
1933 .panel::<AgentPanel>(cx)
1934 .context("missing agent panel")?
1935 .read(cx)
1936 .thread_store()
1937 .clone(),
1938 )
1939 })??;
1940 let editor = editor.upgrade().context("editor was released")?;
1941 let range = editor
1942 .update(cx, |editor, cx| {
1943 editor.buffer().update(cx, |multibuffer, cx| {
1944 let buffer = buffer.read(cx);
1945 let multibuffer_snapshot = multibuffer.read(cx);
1946
1947 let old_context_range =
1948 multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
1949 let mut new_context_range = old_context_range.clone();
1950 if action
1951 .range
1952 .start
1953 .cmp(&old_context_range.start, buffer)
1954 .is_lt()
1955 {
1956 new_context_range.start = action.range.start;
1957 }
1958 if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
1959 new_context_range.end = action.range.end;
1960 }
1961 drop(multibuffer_snapshot);
1962
1963 if new_context_range != old_context_range {
1964 multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
1965 }
1966
1967 let multibuffer_snapshot = multibuffer.read(cx);
1968 multibuffer_snapshot.anchor_range_in_excerpt(excerpt_id, action.range)
1969 })
1970 })?
1971 .context("invalid range")?;
1972
1973 let prompt_store = prompt_store.await.ok();
1974 cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
1975 let assist_id = assistant.suggest_assist(
1976 &editor,
1977 range,
1978 "Fix Diagnostics".into(),
1979 None,
1980 true,
1981 workspace,
1982 thread_store,
1983 prompt_store,
1984 window,
1985 cx,
1986 );
1987 assistant.start_assist(assist_id, window, cx);
1988 })?;
1989
1990 Ok(ProjectTransaction::default())
1991 })
1992 }
1993}
1994
1995fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1996 ranges.sort_unstable_by(|a, b| {
1997 a.start
1998 .cmp(&b.start, buffer)
1999 .then_with(|| b.end.cmp(&a.end, buffer))
2000 });
2001
2002 let mut ix = 0;
2003 while ix + 1 < ranges.len() {
2004 let b = ranges[ix + 1].clone();
2005 let a = &mut ranges[ix];
2006 if a.end.cmp(&b.start, buffer).is_gt() {
2007 if a.end.cmp(&b.end, buffer).is_lt() {
2008 a.end = b.end;
2009 }
2010 ranges.remove(ix + 1);
2011 } else {
2012 ix += 1;
2013 }
2014 }
2015}
2016
2017#[cfg(any(test, feature = "test-support"))]
2018pub mod test {
2019 use std::sync::Arc;
2020
2021 use agent::HistoryStore;
2022 use assistant_text_thread::TextThreadStore;
2023 use client::{Client, UserStore};
2024 use editor::{Editor, MultiBuffer, MultiBufferOffset};
2025 use fs::FakeFs;
2026 use futures::channel::mpsc;
2027 use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
2028 use language::Buffer;
2029 use language_model::LanguageModelRegistry;
2030 use project::Project;
2031 use prompt_store::PromptBuilder;
2032 use smol::stream::StreamExt as _;
2033 use util::test::marked_text_ranges;
2034 use workspace::Workspace;
2035
2036 use crate::InlineAssistant;
2037
2038 pub fn run_inline_assistant_test<SetupF, TestF>(
2039 base_buffer: String,
2040 prompt: String,
2041 setup: SetupF,
2042 test: TestF,
2043 cx: &mut TestAppContext,
2044 ) -> String
2045 where
2046 SetupF: FnOnce(&mut gpui::VisualTestContext),
2047 TestF: FnOnce(&mut gpui::VisualTestContext),
2048 {
2049 let fs = FakeFs::new(cx.executor());
2050 let app_state = cx.update(|cx| workspace::AppState::test(cx));
2051 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
2052 let http = Arc::new(reqwest_client::ReqwestClient::user_agent("agent tests").unwrap());
2053 let client = cx.update(|cx| {
2054 cx.set_http_client(http);
2055 Client::production(cx)
2056 });
2057 let mut inline_assistant = InlineAssistant::new(fs.clone(), prompt_builder);
2058
2059 let (tx, mut completion_rx) = mpsc::unbounded();
2060 inline_assistant.set_completion_receiver(tx);
2061
2062 // Initialize settings and client
2063 cx.update(|cx| {
2064 gpui_tokio::init(cx);
2065 settings::init(cx);
2066 client::init(&client, cx);
2067 workspace::init(app_state.clone(), cx);
2068 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2069 language_model::init(client.clone(), cx);
2070 language_models::init(user_store, client.clone(), cx);
2071
2072 cx.set_global(inline_assistant);
2073 });
2074
2075 let project = cx
2076 .executor()
2077 .block_test(async { Project::test(fs.clone(), [], cx).await });
2078
2079 // Create workspace with window
2080 let (workspace, cx) = cx.add_window_view(|window, cx| {
2081 window.activate_window();
2082 Workspace::new(None, project.clone(), app_state.clone(), window, cx)
2083 });
2084
2085 setup(cx);
2086
2087 let (_editor, buffer) = cx.update(|window, cx| {
2088 let buffer = cx.new(|cx| Buffer::local("", cx));
2089 let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
2090 let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx));
2091 editor.update(cx, |editor, cx| {
2092 let (unmarked_text, selection_ranges) = marked_text_ranges(&base_buffer, true);
2093 editor.set_text(unmarked_text, window, cx);
2094 editor.change_selections(Default::default(), window, cx, |s| {
2095 s.select_ranges(
2096 selection_ranges.into_iter().map(|range| {
2097 MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
2098 }),
2099 )
2100 })
2101 });
2102
2103 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
2104 let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
2105
2106 // Add editor to workspace
2107 workspace.update(cx, |workspace, cx| {
2108 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
2109 });
2110
2111 // Call assist method
2112 InlineAssistant::update_global(cx, |inline_assistant, cx| {
2113 let assist_id = inline_assistant
2114 .assist(
2115 &editor,
2116 workspace.downgrade(),
2117 project.downgrade(),
2118 history_store, // thread_store
2119 None, // prompt_store
2120 Some(prompt),
2121 window,
2122 cx,
2123 )
2124 .unwrap();
2125
2126 inline_assistant.start_assist(assist_id, window, cx);
2127 });
2128
2129 (editor, buffer)
2130 });
2131
2132 cx.run_until_parked();
2133
2134 test(cx);
2135
2136 cx.executor()
2137 .block_test(async { completion_rx.next().await });
2138
2139 buffer.read_with(cx, |buffer, _| buffer.text())
2140 }
2141
2142 #[allow(unused)]
2143 pub fn test_inline_assistant(
2144 base_buffer: &'static str,
2145 llm_output: &'static str,
2146 cx: &mut TestAppContext,
2147 ) -> String {
2148 run_inline_assistant_test(
2149 base_buffer.to_string(),
2150 "Prompt doesn't matter because we're using a fake model".to_string(),
2151 |cx| {
2152 cx.update(|_, cx| LanguageModelRegistry::test(cx));
2153 },
2154 |cx| {
2155 let fake_model = cx.update(|_, cx| {
2156 LanguageModelRegistry::global(cx)
2157 .update(cx, |registry, _| registry.fake_model())
2158 });
2159 let fake = fake_model.as_fake();
2160
2161 // let fake = fake_model;
2162 fake.send_last_completion_stream_text_chunk(llm_output.to_string());
2163 fake.end_last_completion_stream();
2164
2165 // Run again to process the model's response
2166 cx.run_until_parked();
2167 },
2168 cx,
2169 )
2170 }
2171}