inline_assistant.rs

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