inline_assistant.rs

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