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