inline_assistant.rs

   1use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
   2use crate::context_store::ContextStore;
   3use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
   4use crate::thread_store::ThreadStore;
   5use crate::AssistantPanel;
   6use crate::{
   7    assistant_settings::AssistantSettings, prompts::PromptBuilder,
   8    terminal_inline_assistant::TerminalInlineAssistant,
   9};
  10use anyhow::{Context as _, Result};
  11use client::telemetry::Telemetry;
  12use collections::{hash_map, HashMap, HashSet, VecDeque};
  13use editor::{
  14    actions::SelectAll,
  15    display_map::{
  16        BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
  17        ToDisplayPoint,
  18    },
  19    Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
  20    GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
  21};
  22use fs::Fs;
  23use util::ResultExt;
  24
  25use gpui::{
  26    point, AppContext, FocusableView, Global, HighlightStyle, Model, Subscription, Task,
  27    UpdateGlobal, View, ViewContext, WeakModel, WeakView, WindowContext,
  28};
  29use language::{Buffer, Point, Selection, TransactionId};
  30use language_model::LanguageModelRegistry;
  31use language_models::report_assistant_event;
  32use multi_buffer::MultiBufferRow;
  33use parking_lot::Mutex;
  34use project::{CodeAction, ProjectTransaction};
  35use settings::{Settings, SettingsStore};
  36use std::{cmp, mem, ops::Range, rc::Rc, sync::Arc};
  37use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
  38use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
  39use text::{OffsetRangeExt, ToPoint as _};
  40use ui::prelude::*;
  41use util::RangeExt;
  42use workspace::{dock::Panel, ShowConfiguration};
  43use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
  44
  45pub fn init(
  46    fs: Arc<dyn Fs>,
  47    prompt_builder: Arc<PromptBuilder>,
  48    telemetry: Arc<Telemetry>,
  49    cx: &mut AppContext,
  50) {
  51    cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
  52    cx.observe_new_views(|_workspace: &mut Workspace, cx| {
  53        let workspace = cx.view().clone();
  54        InlineAssistant::update_global(cx, |inline_assistant, cx| {
  55            inline_assistant.register_workspace(&workspace, cx)
  56        })
  57    })
  58    .detach();
  59}
  60
  61const PROMPT_HISTORY_MAX_LEN: usize = 20;
  62
  63enum InlineAssistTarget {
  64    Editor(View<Editor>),
  65    Terminal(View<TerminalView>),
  66}
  67
  68pub struct InlineAssistant {
  69    next_assist_id: InlineAssistId,
  70    next_assist_group_id: InlineAssistGroupId,
  71    assists: HashMap<InlineAssistId, InlineAssist>,
  72    assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
  73    assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
  74    confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
  75    prompt_history: VecDeque<String>,
  76    prompt_builder: Arc<PromptBuilder>,
  77    telemetry: Arc<Telemetry>,
  78    fs: Arc<dyn Fs>,
  79}
  80
  81impl Global for InlineAssistant {}
  82
  83impl InlineAssistant {
  84    pub fn new(
  85        fs: Arc<dyn Fs>,
  86        prompt_builder: Arc<PromptBuilder>,
  87        telemetry: Arc<Telemetry>,
  88    ) -> Self {
  89        Self {
  90            next_assist_id: InlineAssistId::default(),
  91            next_assist_group_id: InlineAssistGroupId::default(),
  92            assists: HashMap::default(),
  93            assists_by_editor: HashMap::default(),
  94            assist_groups: HashMap::default(),
  95            confirmed_assists: HashMap::default(),
  96            prompt_history: VecDeque::default(),
  97            prompt_builder,
  98            telemetry,
  99            fs,
 100        }
 101    }
 102
 103    pub fn register_workspace(&mut self, workspace: &View<Workspace>, cx: &mut WindowContext) {
 104        cx.subscribe(workspace, |workspace, event, cx| {
 105            Self::update_global(cx, |this, cx| {
 106                this.handle_workspace_event(workspace, event, cx)
 107            });
 108        })
 109        .detach();
 110
 111        let workspace = workspace.downgrade();
 112        cx.observe_global::<SettingsStore>(move |cx| {
 113            let Some(workspace) = workspace.upgrade() else {
 114                return;
 115            };
 116            let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
 117                return;
 118            };
 119            let enabled = AssistantSettings::get_global(cx).enabled;
 120            terminal_panel.update(cx, |terminal_panel, cx| {
 121                terminal_panel.asssistant_enabled(enabled, cx)
 122            });
 123        })
 124        .detach();
 125    }
 126
 127    fn handle_workspace_event(
 128        &mut self,
 129        workspace: View<Workspace>,
 130        event: &workspace::Event,
 131        cx: &mut WindowContext,
 132    ) {
 133        match event {
 134            workspace::Event::UserSavedItem { item, .. } => {
 135                // When the user manually saves an editor, automatically accepts all finished transformations.
 136                if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
 137                    if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
 138                        for assist_id in editor_assists.assist_ids.clone() {
 139                            let assist = &self.assists[&assist_id];
 140                            if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
 141                                self.finish_assist(assist_id, false, cx)
 142                            }
 143                        }
 144                    }
 145                }
 146            }
 147            workspace::Event::ItemAdded { item } => {
 148                self.register_workspace_item(&workspace, item.as_ref(), cx);
 149            }
 150            _ => (),
 151        }
 152    }
 153
 154    fn register_workspace_item(
 155        &mut self,
 156        workspace: &View<Workspace>,
 157        item: &dyn ItemHandle,
 158        cx: &mut WindowContext,
 159    ) {
 160        if let Some(editor) = item.act_as::<Editor>(cx) {
 161            editor.update(cx, |editor, cx| {
 162                let thread_store = workspace
 163                    .read(cx)
 164                    .panel::<AssistantPanel>(cx)
 165                    .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
 166
 167                editor.push_code_action_provider(
 168                    Rc::new(AssistantCodeActionProvider {
 169                        editor: cx.view().downgrade(),
 170                        workspace: workspace.downgrade(),
 171                        thread_store,
 172                    }),
 173                    cx,
 174                );
 175            });
 176        }
 177    }
 178
 179    pub fn inline_assist(
 180        workspace: &mut Workspace,
 181        _action: &zed_actions::InlineAssist,
 182        cx: &mut ViewContext<Workspace>,
 183    ) {
 184        let settings = AssistantSettings::get_global(cx);
 185        if !settings.enabled {
 186            return;
 187        }
 188
 189        let Some(inline_assist_target) = Self::resolve_inline_assist_target(workspace, cx) else {
 190            return;
 191        };
 192
 193        let is_authenticated = || {
 194            LanguageModelRegistry::read_global(cx)
 195                .active_provider()
 196                .map_or(false, |provider| provider.is_authenticated(cx))
 197        };
 198
 199        let thread_store = workspace
 200            .panel::<AssistantPanel>(cx)
 201            .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
 202
 203        let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
 204            InlineAssistTarget::Editor(active_editor) => {
 205                InlineAssistant::update_global(cx, |assistant, cx| {
 206                    assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx)
 207                })
 208            }
 209            InlineAssistTarget::Terminal(active_terminal) => {
 210                TerminalInlineAssistant::update_global(cx, |assistant, cx| {
 211                    assistant.assist(&active_terminal, cx.view().downgrade(), thread_store, cx)
 212                })
 213            }
 214        };
 215
 216        if is_authenticated() {
 217            handle_assist(cx);
 218        } else {
 219            cx.spawn(|_workspace, mut cx| async move {
 220                let Some(task) = cx.update(|cx| {
 221                    LanguageModelRegistry::read_global(cx)
 222                        .active_provider()
 223                        .map_or(None, |provider| Some(provider.authenticate(cx)))
 224                })?
 225                else {
 226                    let answer = cx
 227                        .prompt(
 228                            gpui::PromptLevel::Warning,
 229                            "No language model provider configured",
 230                            None,
 231                            &["Configure", "Cancel"],
 232                        )
 233                        .await
 234                        .ok();
 235                    if let Some(answer) = answer {
 236                        if answer == 0 {
 237                            cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration)))
 238                                .ok();
 239                        }
 240                    }
 241                    return Ok(());
 242                };
 243                task.await?;
 244
 245                anyhow::Ok(())
 246            })
 247            .detach_and_log_err(cx);
 248
 249            if is_authenticated() {
 250                handle_assist(cx);
 251            }
 252        }
 253    }
 254
 255    pub fn assist(
 256        &mut self,
 257        editor: &View<Editor>,
 258        workspace: WeakView<Workspace>,
 259        thread_store: Option<WeakModel<ThreadStore>>,
 260        cx: &mut WindowContext,
 261    ) {
 262        let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
 263            (
 264                editor.buffer().read(cx).snapshot(cx),
 265                editor.selections.all::<Point>(cx),
 266            )
 267        });
 268
 269        let mut selections = Vec::<Selection<Point>>::new();
 270        let mut newest_selection = None;
 271        for mut selection in initial_selections {
 272            if selection.end > selection.start {
 273                selection.start.column = 0;
 274                // If the selection ends at the start of the line, we don't want to include it.
 275                if selection.end.column == 0 {
 276                    selection.end.row -= 1;
 277                }
 278                selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
 279            }
 280
 281            if let Some(prev_selection) = selections.last_mut() {
 282                if selection.start <= prev_selection.end {
 283                    prev_selection.end = selection.end;
 284                    continue;
 285                }
 286            }
 287
 288            let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
 289            if selection.id > latest_selection.id {
 290                *latest_selection = selection.clone();
 291            }
 292            selections.push(selection);
 293        }
 294        let newest_selection = newest_selection.unwrap();
 295
 296        let mut codegen_ranges = Vec::new();
 297        for (excerpt_id, buffer, buffer_range) in
 298            snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
 299                snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
 300            }))
 301        {
 302            let start = Anchor {
 303                buffer_id: Some(buffer.remote_id()),
 304                excerpt_id,
 305                text_anchor: buffer.anchor_before(buffer_range.start),
 306            };
 307            let end = Anchor {
 308                buffer_id: Some(buffer.remote_id()),
 309                excerpt_id,
 310                text_anchor: buffer.anchor_after(buffer_range.end),
 311            };
 312            codegen_ranges.push(start..end);
 313
 314            if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
 315                self.telemetry.report_assistant_event(AssistantEvent {
 316                    conversation_id: None,
 317                    kind: AssistantKind::Inline,
 318                    phase: AssistantPhase::Invoked,
 319                    message_id: None,
 320                    model: model.telemetry_id(),
 321                    model_provider: model.provider_id().to_string(),
 322                    response_latency: None,
 323                    error_message: None,
 324                    language_name: buffer.language().map(|language| language.name().to_proto()),
 325                });
 326            }
 327        }
 328
 329        let assist_group_id = self.next_assist_group_id.post_inc();
 330        let prompt_buffer = cx.new_model(|cx| {
 331            MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
 332        });
 333
 334        let mut assists = Vec::new();
 335        let mut assist_to_focus = None;
 336        for range in codegen_ranges {
 337            let assist_id = self.next_assist_id.post_inc();
 338            let context_store = cx.new_model(|_cx| ContextStore::new());
 339            let codegen = cx.new_model(|cx| {
 340                BufferCodegen::new(
 341                    editor.read(cx).buffer().clone(),
 342                    range.clone(),
 343                    None,
 344                    context_store.clone(),
 345                    self.telemetry.clone(),
 346                    self.prompt_builder.clone(),
 347                    cx,
 348                )
 349            });
 350
 351            let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
 352            let prompt_editor = cx.new_view(|cx| {
 353                PromptEditor::new_buffer(
 354                    assist_id,
 355                    gutter_dimensions.clone(),
 356                    self.prompt_history.clone(),
 357                    prompt_buffer.clone(),
 358                    codegen.clone(),
 359                    self.fs.clone(),
 360                    context_store,
 361                    workspace.clone(),
 362                    thread_store.clone(),
 363                    cx,
 364                )
 365            });
 366
 367            if assist_to_focus.is_none() {
 368                let focus_assist = if newest_selection.reversed {
 369                    range.start.to_point(&snapshot) == newest_selection.start
 370                } else {
 371                    range.end.to_point(&snapshot) == newest_selection.end
 372                };
 373                if focus_assist {
 374                    assist_to_focus = Some(assist_id);
 375                }
 376            }
 377
 378            let [prompt_block_id, end_block_id] =
 379                self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
 380
 381            assists.push((
 382                assist_id,
 383                range,
 384                prompt_editor,
 385                prompt_block_id,
 386                end_block_id,
 387            ));
 388        }
 389
 390        let editor_assists = self
 391            .assists_by_editor
 392            .entry(editor.downgrade())
 393            .or_insert_with(|| EditorInlineAssists::new(&editor, cx));
 394        let mut assist_group = InlineAssistGroup::new();
 395        for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
 396            let codegen = prompt_editor.read(cx).codegen().clone();
 397
 398            self.assists.insert(
 399                assist_id,
 400                InlineAssist::new(
 401                    assist_id,
 402                    assist_group_id,
 403                    editor,
 404                    &prompt_editor,
 405                    prompt_block_id,
 406                    end_block_id,
 407                    range,
 408                    codegen,
 409                    workspace.clone(),
 410                    cx,
 411                ),
 412            );
 413            assist_group.assist_ids.push(assist_id);
 414            editor_assists.assist_ids.push(assist_id);
 415        }
 416        self.assist_groups.insert(assist_group_id, assist_group);
 417
 418        if let Some(assist_id) = assist_to_focus {
 419            self.focus_assist(assist_id, cx);
 420        }
 421    }
 422
 423    #[allow(clippy::too_many_arguments)]
 424    pub fn suggest_assist(
 425        &mut self,
 426        editor: &View<Editor>,
 427        mut range: Range<Anchor>,
 428        initial_prompt: String,
 429        initial_transaction_id: Option<TransactionId>,
 430        focus: bool,
 431        workspace: WeakView<Workspace>,
 432        thread_store: Option<WeakModel<ThreadStore>>,
 433        cx: &mut WindowContext,
 434    ) -> InlineAssistId {
 435        let assist_group_id = self.next_assist_group_id.post_inc();
 436        let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx));
 437        let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
 438
 439        let assist_id = self.next_assist_id.post_inc();
 440
 441        let buffer = editor.read(cx).buffer().clone();
 442        {
 443            let snapshot = buffer.read(cx).read(cx);
 444            range.start = range.start.bias_left(&snapshot);
 445            range.end = range.end.bias_right(&snapshot);
 446        }
 447
 448        let context_store = cx.new_model(|_cx| ContextStore::new());
 449
 450        let codegen = cx.new_model(|cx| {
 451            BufferCodegen::new(
 452                editor.read(cx).buffer().clone(),
 453                range.clone(),
 454                initial_transaction_id,
 455                context_store.clone(),
 456                self.telemetry.clone(),
 457                self.prompt_builder.clone(),
 458                cx,
 459            )
 460        });
 461
 462        let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
 463        let prompt_editor = cx.new_view(|cx| {
 464            PromptEditor::new_buffer(
 465                assist_id,
 466                gutter_dimensions.clone(),
 467                self.prompt_history.clone(),
 468                prompt_buffer.clone(),
 469                codegen.clone(),
 470                self.fs.clone(),
 471                context_store,
 472                workspace.clone(),
 473                thread_store,
 474                cx,
 475            )
 476        });
 477
 478        let [prompt_block_id, end_block_id] =
 479            self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
 480
 481        let editor_assists = self
 482            .assists_by_editor
 483            .entry(editor.downgrade())
 484            .or_insert_with(|| EditorInlineAssists::new(&editor, cx));
 485
 486        let mut assist_group = InlineAssistGroup::new();
 487        self.assists.insert(
 488            assist_id,
 489            InlineAssist::new(
 490                assist_id,
 491                assist_group_id,
 492                editor,
 493                &prompt_editor,
 494                prompt_block_id,
 495                end_block_id,
 496                range,
 497                codegen.clone(),
 498                workspace.clone(),
 499                cx,
 500            ),
 501        );
 502        assist_group.assist_ids.push(assist_id);
 503        editor_assists.assist_ids.push(assist_id);
 504        self.assist_groups.insert(assist_group_id, assist_group);
 505
 506        if focus {
 507            self.focus_assist(assist_id, cx);
 508        }
 509
 510        assist_id
 511    }
 512
 513    fn insert_assist_blocks(
 514        &self,
 515        editor: &View<Editor>,
 516        range: &Range<Anchor>,
 517        prompt_editor: &View<PromptEditor<BufferCodegen>>,
 518        cx: &mut WindowContext,
 519    ) -> [CustomBlockId; 2] {
 520        let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
 521            prompt_editor
 522                .editor
 523                .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1 + 2)
 524        });
 525        let assist_blocks = vec![
 526            BlockProperties {
 527                style: BlockStyle::Sticky,
 528                placement: BlockPlacement::Above(range.start),
 529                height: prompt_editor_height,
 530                render: build_assist_editor_renderer(prompt_editor),
 531                priority: 0,
 532            },
 533            BlockProperties {
 534                style: BlockStyle::Sticky,
 535                placement: BlockPlacement::Below(range.end),
 536                height: 0,
 537                render: Arc::new(|cx| {
 538                    v_flex()
 539                        .h_full()
 540                        .w_full()
 541                        .border_t_1()
 542                        .border_color(cx.theme().status().info_border)
 543                        .into_any_element()
 544                }),
 545                priority: 0,
 546            },
 547        ];
 548
 549        editor.update(cx, |editor, cx| {
 550            let block_ids = editor.insert_blocks(assist_blocks, None, cx);
 551            [block_ids[0], block_ids[1]]
 552        })
 553    }
 554
 555    fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 556        let assist = &self.assists[&assist_id];
 557        let Some(decorations) = assist.decorations.as_ref() else {
 558            return;
 559        };
 560        let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
 561        let editor_assists = self.assists_by_editor.get_mut(&assist.editor).unwrap();
 562
 563        assist_group.active_assist_id = Some(assist_id);
 564        if assist_group.linked {
 565            for assist_id in &assist_group.assist_ids {
 566                if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
 567                    decorations.prompt_editor.update(cx, |prompt_editor, cx| {
 568                        prompt_editor.set_show_cursor_when_unfocused(true, cx)
 569                    });
 570                }
 571            }
 572        }
 573
 574        assist
 575            .editor
 576            .update(cx, |editor, cx| {
 577                let scroll_top = editor.scroll_position(cx).y;
 578                let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
 579                let prompt_row = editor
 580                    .row_for_block(decorations.prompt_block_id, cx)
 581                    .unwrap()
 582                    .0 as f32;
 583
 584                if (scroll_top..scroll_bottom).contains(&prompt_row) {
 585                    editor_assists.scroll_lock = Some(InlineAssistScrollLock {
 586                        assist_id,
 587                        distance_from_top: prompt_row - scroll_top,
 588                    });
 589                } else {
 590                    editor_assists.scroll_lock = None;
 591                }
 592            })
 593            .ok();
 594    }
 595
 596    fn handle_prompt_editor_focus_out(
 597        &mut self,
 598        assist_id: InlineAssistId,
 599        cx: &mut WindowContext,
 600    ) {
 601        let assist = &self.assists[&assist_id];
 602        let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
 603        if assist_group.active_assist_id == Some(assist_id) {
 604            assist_group.active_assist_id = None;
 605            if assist_group.linked {
 606                for assist_id in &assist_group.assist_ids {
 607                    if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
 608                        decorations.prompt_editor.update(cx, |prompt_editor, cx| {
 609                            prompt_editor.set_show_cursor_when_unfocused(false, cx)
 610                        });
 611                    }
 612                }
 613            }
 614        }
 615    }
 616
 617    fn handle_prompt_editor_event(
 618        &mut self,
 619        prompt_editor: View<PromptEditor<BufferCodegen>>,
 620        event: &PromptEditorEvent,
 621        cx: &mut WindowContext,
 622    ) {
 623        let assist_id = prompt_editor.read(cx).id();
 624        match event {
 625            PromptEditorEvent::StartRequested => {
 626                self.start_assist(assist_id, cx);
 627            }
 628            PromptEditorEvent::StopRequested => {
 629                self.stop_assist(assist_id, cx);
 630            }
 631            PromptEditorEvent::ConfirmRequested { execute: _ } => {
 632                self.finish_assist(assist_id, false, cx);
 633            }
 634            PromptEditorEvent::CancelRequested => {
 635                self.finish_assist(assist_id, true, cx);
 636            }
 637            PromptEditorEvent::DismissRequested => {
 638                self.dismiss_assist(assist_id, cx);
 639            }
 640            PromptEditorEvent::Resized { .. } => {
 641                // This only matters for the terminal inline assistant
 642            }
 643        }
 644    }
 645
 646    fn handle_editor_newline(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
 647        let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
 648            return;
 649        };
 650
 651        if editor.read(cx).selections.count() == 1 {
 652            let (selection, buffer) = editor.update(cx, |editor, cx| {
 653                (
 654                    editor.selections.newest::<usize>(cx),
 655                    editor.buffer().read(cx).snapshot(cx),
 656                )
 657            });
 658            for assist_id in &editor_assists.assist_ids {
 659                let assist = &self.assists[assist_id];
 660                let assist_range = assist.range.to_offset(&buffer);
 661                if assist_range.contains(&selection.start) && assist_range.contains(&selection.end)
 662                {
 663                    if matches!(assist.codegen.read(cx).status(cx), CodegenStatus::Pending) {
 664                        self.dismiss_assist(*assist_id, cx);
 665                    } else {
 666                        self.finish_assist(*assist_id, false, cx);
 667                    }
 668
 669                    return;
 670                }
 671            }
 672        }
 673
 674        cx.propagate();
 675    }
 676
 677    fn handle_editor_cancel(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
 678        let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
 679            return;
 680        };
 681
 682        if editor.read(cx).selections.count() == 1 {
 683            let (selection, buffer) = editor.update(cx, |editor, cx| {
 684                (
 685                    editor.selections.newest::<usize>(cx),
 686                    editor.buffer().read(cx).snapshot(cx),
 687                )
 688            });
 689            let mut closest_assist_fallback = None;
 690            for assist_id in &editor_assists.assist_ids {
 691                let assist = &self.assists[assist_id];
 692                let assist_range = assist.range.to_offset(&buffer);
 693                if assist.decorations.is_some() {
 694                    if assist_range.contains(&selection.start)
 695                        && assist_range.contains(&selection.end)
 696                    {
 697                        self.focus_assist(*assist_id, cx);
 698                        return;
 699                    } else {
 700                        let distance_from_selection = assist_range
 701                            .start
 702                            .abs_diff(selection.start)
 703                            .min(assist_range.start.abs_diff(selection.end))
 704                            + assist_range
 705                                .end
 706                                .abs_diff(selection.start)
 707                                .min(assist_range.end.abs_diff(selection.end));
 708                        match closest_assist_fallback {
 709                            Some((_, old_distance)) => {
 710                                if distance_from_selection < old_distance {
 711                                    closest_assist_fallback =
 712                                        Some((assist_id, distance_from_selection));
 713                                }
 714                            }
 715                            None => {
 716                                closest_assist_fallback = Some((assist_id, distance_from_selection))
 717                            }
 718                        }
 719                    }
 720                }
 721            }
 722
 723            if let Some((&assist_id, _)) = closest_assist_fallback {
 724                self.focus_assist(assist_id, cx);
 725            }
 726        }
 727
 728        cx.propagate();
 729    }
 730
 731    fn handle_editor_release(&mut self, editor: WeakView<Editor>, cx: &mut WindowContext) {
 732        if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) {
 733            for assist_id in editor_assists.assist_ids.clone() {
 734                self.finish_assist(assist_id, true, cx);
 735            }
 736        }
 737    }
 738
 739    fn handle_editor_change(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
 740        let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
 741            return;
 742        };
 743        let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() else {
 744            return;
 745        };
 746        let assist = &self.assists[&scroll_lock.assist_id];
 747        let Some(decorations) = assist.decorations.as_ref() else {
 748            return;
 749        };
 750
 751        editor.update(cx, |editor, cx| {
 752            let scroll_position = editor.scroll_position(cx);
 753            let target_scroll_top = editor
 754                .row_for_block(decorations.prompt_block_id, cx)
 755                .unwrap()
 756                .0 as f32
 757                - scroll_lock.distance_from_top;
 758            if target_scroll_top != scroll_position.y {
 759                editor.set_scroll_position(point(scroll_position.x, target_scroll_top), cx);
 760            }
 761        });
 762    }
 763
 764    fn handle_editor_event(
 765        &mut self,
 766        editor: View<Editor>,
 767        event: &EditorEvent,
 768        cx: &mut WindowContext,
 769    ) {
 770        let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else {
 771            return;
 772        };
 773
 774        match event {
 775            EditorEvent::Edited { transaction_id } => {
 776                let buffer = editor.read(cx).buffer().read(cx);
 777                let edited_ranges =
 778                    buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
 779                let snapshot = buffer.snapshot(cx);
 780
 781                for assist_id in editor_assists.assist_ids.clone() {
 782                    let assist = &self.assists[&assist_id];
 783                    if matches!(
 784                        assist.codegen.read(cx).status(cx),
 785                        CodegenStatus::Error(_) | CodegenStatus::Done
 786                    ) {
 787                        let assist_range = assist.range.to_offset(&snapshot);
 788                        if edited_ranges
 789                            .iter()
 790                            .any(|range| range.overlaps(&assist_range))
 791                        {
 792                            self.finish_assist(assist_id, false, cx);
 793                        }
 794                    }
 795                }
 796            }
 797            EditorEvent::ScrollPositionChanged { .. } => {
 798                if let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() {
 799                    let assist = &self.assists[&scroll_lock.assist_id];
 800                    if let Some(decorations) = assist.decorations.as_ref() {
 801                        let distance_from_top = editor.update(cx, |editor, cx| {
 802                            let scroll_top = editor.scroll_position(cx).y;
 803                            let prompt_row = editor
 804                                .row_for_block(decorations.prompt_block_id, cx)
 805                                .unwrap()
 806                                .0 as f32;
 807                            prompt_row - scroll_top
 808                        });
 809
 810                        if distance_from_top != scroll_lock.distance_from_top {
 811                            editor_assists.scroll_lock = None;
 812                        }
 813                    }
 814                }
 815            }
 816            EditorEvent::SelectionsChanged { .. } => {
 817                for assist_id in editor_assists.assist_ids.clone() {
 818                    let assist = &self.assists[&assist_id];
 819                    if let Some(decorations) = assist.decorations.as_ref() {
 820                        if decorations.prompt_editor.focus_handle(cx).is_focused(cx) {
 821                            return;
 822                        }
 823                    }
 824                }
 825
 826                editor_assists.scroll_lock = None;
 827            }
 828            _ => {}
 829        }
 830    }
 831
 832    pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
 833        if let Some(assist) = self.assists.get(&assist_id) {
 834            let assist_group_id = assist.group_id;
 835            if self.assist_groups[&assist_group_id].linked {
 836                for assist_id in self.unlink_assist_group(assist_group_id, cx) {
 837                    self.finish_assist(assist_id, undo, cx);
 838                }
 839                return;
 840            }
 841        }
 842
 843        self.dismiss_assist(assist_id, cx);
 844
 845        if let Some(assist) = self.assists.remove(&assist_id) {
 846            if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id)
 847            {
 848                entry.get_mut().assist_ids.retain(|id| *id != assist_id);
 849                if entry.get().assist_ids.is_empty() {
 850                    entry.remove();
 851                }
 852            }
 853
 854            if let hash_map::Entry::Occupied(mut entry) =
 855                self.assists_by_editor.entry(assist.editor.clone())
 856            {
 857                entry.get_mut().assist_ids.retain(|id| *id != assist_id);
 858                if entry.get().assist_ids.is_empty() {
 859                    entry.remove();
 860                    if let Some(editor) = assist.editor.upgrade() {
 861                        self.update_editor_highlights(&editor, cx);
 862                    }
 863                } else {
 864                    entry.get().highlight_updates.send(()).ok();
 865                }
 866            }
 867
 868            let active_alternative = assist.codegen.read(cx).active_alternative().clone();
 869            let message_id = active_alternative.read(cx).message_id.clone();
 870
 871            if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
 872                let language_name = assist.editor.upgrade().and_then(|editor| {
 873                    let multibuffer = editor.read(cx).buffer().read(cx);
 874                    let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
 875                    ranges
 876                        .first()
 877                        .and_then(|(buffer, _, _)| buffer.read(cx).language())
 878                        .map(|language| language.name())
 879                });
 880                report_assistant_event(
 881                    AssistantEvent {
 882                        conversation_id: None,
 883                        kind: AssistantKind::Inline,
 884                        message_id,
 885                        phase: if undo {
 886                            AssistantPhase::Rejected
 887                        } else {
 888                            AssistantPhase::Accepted
 889                        },
 890                        model: model.telemetry_id(),
 891                        model_provider: model.provider_id().to_string(),
 892                        response_latency: None,
 893                        error_message: None,
 894                        language_name: language_name.map(|name| name.to_proto()),
 895                    },
 896                    Some(self.telemetry.clone()),
 897                    cx.http_client(),
 898                    model.api_key(cx),
 899                    cx.background_executor(),
 900                );
 901            }
 902
 903            if undo {
 904                assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
 905            } else {
 906                self.confirmed_assists.insert(assist_id, active_alternative);
 907            }
 908        }
 909    }
 910
 911    fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
 912        let Some(assist) = self.assists.get_mut(&assist_id) else {
 913            return false;
 914        };
 915        let Some(editor) = assist.editor.upgrade() else {
 916            return false;
 917        };
 918        let Some(decorations) = assist.decorations.take() else {
 919            return false;
 920        };
 921
 922        editor.update(cx, |editor, cx| {
 923            let mut to_remove = decorations.removed_line_block_ids;
 924            to_remove.insert(decorations.prompt_block_id);
 925            to_remove.insert(decorations.end_block_id);
 926            editor.remove_blocks(to_remove, None, cx);
 927        });
 928
 929        if decorations
 930            .prompt_editor
 931            .focus_handle(cx)
 932            .contains_focused(cx)
 933        {
 934            self.focus_next_assist(assist_id, cx);
 935        }
 936
 937        if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
 938            if editor_assists
 939                .scroll_lock
 940                .as_ref()
 941                .map_or(false, |lock| lock.assist_id == assist_id)
 942            {
 943                editor_assists.scroll_lock = None;
 944            }
 945            editor_assists.highlight_updates.send(()).ok();
 946        }
 947
 948        true
 949    }
 950
 951    fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 952        let Some(assist) = self.assists.get(&assist_id) else {
 953            return;
 954        };
 955
 956        let assist_group = &self.assist_groups[&assist.group_id];
 957        let assist_ix = assist_group
 958            .assist_ids
 959            .iter()
 960            .position(|id| *id == assist_id)
 961            .unwrap();
 962        let assist_ids = assist_group
 963            .assist_ids
 964            .iter()
 965            .skip(assist_ix + 1)
 966            .chain(assist_group.assist_ids.iter().take(assist_ix));
 967
 968        for assist_id in assist_ids {
 969            let assist = &self.assists[assist_id];
 970            if assist.decorations.is_some() {
 971                self.focus_assist(*assist_id, cx);
 972                return;
 973            }
 974        }
 975
 976        assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok();
 977    }
 978
 979    fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 980        let Some(assist) = self.assists.get(&assist_id) else {
 981            return;
 982        };
 983
 984        if let Some(decorations) = assist.decorations.as_ref() {
 985            decorations.prompt_editor.update(cx, |prompt_editor, cx| {
 986                prompt_editor.editor.update(cx, |editor, cx| {
 987                    editor.focus(cx);
 988                    editor.select_all(&SelectAll, cx);
 989                })
 990            });
 991        }
 992
 993        self.scroll_to_assist(assist_id, cx);
 994    }
 995
 996    pub fn scroll_to_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 997        let Some(assist) = self.assists.get(&assist_id) else {
 998            return;
 999        };
1000        let Some(editor) = assist.editor.upgrade() else {
1001            return;
1002        };
1003
1004        let position = assist.range.start;
1005        editor.update(cx, |editor, cx| {
1006            editor.change_selections(None, cx, |selections| {
1007                selections.select_anchor_ranges([position..position])
1008            });
1009
1010            let mut scroll_target_top;
1011            let mut scroll_target_bottom;
1012            if let Some(decorations) = assist.decorations.as_ref() {
1013                scroll_target_top = editor
1014                    .row_for_block(decorations.prompt_block_id, cx)
1015                    .unwrap()
1016                    .0 as f32;
1017                scroll_target_bottom = editor
1018                    .row_for_block(decorations.end_block_id, cx)
1019                    .unwrap()
1020                    .0 as f32;
1021            } else {
1022                let snapshot = editor.snapshot(cx);
1023                let start_row = assist
1024                    .range
1025                    .start
1026                    .to_display_point(&snapshot.display_snapshot)
1027                    .row();
1028                scroll_target_top = start_row.0 as f32;
1029                scroll_target_bottom = scroll_target_top + 1.;
1030            }
1031            scroll_target_top -= editor.vertical_scroll_margin() as f32;
1032            scroll_target_bottom += editor.vertical_scroll_margin() as f32;
1033
1034            let height_in_lines = editor.visible_line_count().unwrap_or(0.);
1035            let scroll_top = editor.scroll_position(cx).y;
1036            let scroll_bottom = scroll_top + height_in_lines;
1037
1038            if scroll_target_top < scroll_top {
1039                editor.set_scroll_position(point(0., scroll_target_top), cx);
1040            } else if scroll_target_bottom > scroll_bottom {
1041                if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
1042                    editor
1043                        .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx);
1044                } else {
1045                    editor.set_scroll_position(point(0., scroll_target_top), cx);
1046                }
1047            }
1048        });
1049    }
1050
1051    fn unlink_assist_group(
1052        &mut self,
1053        assist_group_id: InlineAssistGroupId,
1054        cx: &mut WindowContext,
1055    ) -> Vec<InlineAssistId> {
1056        let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
1057        assist_group.linked = false;
1058        for assist_id in &assist_group.assist_ids {
1059            let assist = self.assists.get_mut(assist_id).unwrap();
1060            if let Some(editor_decorations) = assist.decorations.as_ref() {
1061                editor_decorations
1062                    .prompt_editor
1063                    .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx));
1064            }
1065        }
1066        assist_group.assist_ids.clone()
1067    }
1068
1069    pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1070        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1071            assist
1072        } else {
1073            return;
1074        };
1075
1076        let assist_group_id = assist.group_id;
1077        if self.assist_groups[&assist_group_id].linked {
1078            for assist_id in self.unlink_assist_group(assist_group_id, cx) {
1079                self.start_assist(assist_id, cx);
1080            }
1081            return;
1082        }
1083
1084        let Some(user_prompt) = assist.user_prompt(cx) else {
1085            return;
1086        };
1087
1088        self.prompt_history.retain(|prompt| *prompt != user_prompt);
1089        self.prompt_history.push_back(user_prompt.clone());
1090        if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
1091            self.prompt_history.pop_front();
1092        }
1093
1094        assist
1095            .codegen
1096            .update(cx, |codegen, cx| codegen.start(user_prompt, cx))
1097            .log_err();
1098    }
1099
1100    pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1101        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1102            assist
1103        } else {
1104            return;
1105        };
1106
1107        assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
1108    }
1109
1110    fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
1111        let mut gutter_pending_ranges = Vec::new();
1112        let mut gutter_transformed_ranges = Vec::new();
1113        let mut foreground_ranges = Vec::new();
1114        let mut inserted_row_ranges = Vec::new();
1115        let empty_assist_ids = Vec::new();
1116        let assist_ids = self
1117            .assists_by_editor
1118            .get(&editor.downgrade())
1119            .map_or(&empty_assist_ids, |editor_assists| {
1120                &editor_assists.assist_ids
1121            });
1122
1123        for assist_id in assist_ids {
1124            if let Some(assist) = self.assists.get(assist_id) {
1125                let codegen = assist.codegen.read(cx);
1126                let buffer = codegen.buffer(cx).read(cx).read(cx);
1127                foreground_ranges.extend(codegen.last_equal_ranges(cx).iter().cloned());
1128
1129                let pending_range =
1130                    codegen.edit_position(cx).unwrap_or(assist.range.start)..assist.range.end;
1131                if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
1132                    gutter_pending_ranges.push(pending_range);
1133                }
1134
1135                if let Some(edit_position) = codegen.edit_position(cx) {
1136                    let edited_range = assist.range.start..edit_position;
1137                    if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
1138                        gutter_transformed_ranges.push(edited_range);
1139                    }
1140                }
1141
1142                if assist.decorations.is_some() {
1143                    inserted_row_ranges
1144                        .extend(codegen.diff(cx).inserted_row_ranges.iter().cloned());
1145                }
1146            }
1147        }
1148
1149        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
1150        merge_ranges(&mut foreground_ranges, &snapshot);
1151        merge_ranges(&mut gutter_pending_ranges, &snapshot);
1152        merge_ranges(&mut gutter_transformed_ranges, &snapshot);
1153        editor.update(cx, |editor, cx| {
1154            enum GutterPendingRange {}
1155            if gutter_pending_ranges.is_empty() {
1156                editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1157            } else {
1158                editor.highlight_gutter::<GutterPendingRange>(
1159                    &gutter_pending_ranges,
1160                    |cx| cx.theme().status().info_background,
1161                    cx,
1162                )
1163            }
1164
1165            enum GutterTransformedRange {}
1166            if gutter_transformed_ranges.is_empty() {
1167                editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1168            } else {
1169                editor.highlight_gutter::<GutterTransformedRange>(
1170                    &gutter_transformed_ranges,
1171                    |cx| cx.theme().status().info,
1172                    cx,
1173                )
1174            }
1175
1176            if foreground_ranges.is_empty() {
1177                editor.clear_highlights::<InlineAssist>(cx);
1178            } else {
1179                editor.highlight_text::<InlineAssist>(
1180                    foreground_ranges,
1181                    HighlightStyle {
1182                        fade_out: Some(0.6),
1183                        ..Default::default()
1184                    },
1185                    cx,
1186                );
1187            }
1188
1189            editor.clear_row_highlights::<InlineAssist>();
1190            for row_range in inserted_row_ranges {
1191                editor.highlight_rows::<InlineAssist>(
1192                    row_range,
1193                    cx.theme().status().info_background,
1194                    false,
1195                    cx,
1196                );
1197            }
1198        });
1199    }
1200
1201    fn update_editor_blocks(
1202        &mut self,
1203        editor: &View<Editor>,
1204        assist_id: InlineAssistId,
1205        cx: &mut WindowContext,
1206    ) {
1207        let Some(assist) = self.assists.get_mut(&assist_id) else {
1208            return;
1209        };
1210        let Some(decorations) = assist.decorations.as_mut() else {
1211            return;
1212        };
1213
1214        let codegen = assist.codegen.read(cx);
1215        let old_snapshot = codegen.snapshot(cx);
1216        let old_buffer = codegen.old_buffer(cx);
1217        let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
1218
1219        editor.update(cx, |editor, cx| {
1220            let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
1221            editor.remove_blocks(old_blocks, None, cx);
1222
1223            let mut new_blocks = Vec::new();
1224            for (new_row, old_row_range) in deleted_row_ranges {
1225                let (_, buffer_start) = old_snapshot
1226                    .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
1227                    .unwrap();
1228                let (_, buffer_end) = old_snapshot
1229                    .point_to_buffer_offset(Point::new(
1230                        *old_row_range.end(),
1231                        old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
1232                    ))
1233                    .unwrap();
1234
1235                let deleted_lines_editor = cx.new_view(|cx| {
1236                    let multi_buffer = cx.new_model(|_| {
1237                        MultiBuffer::without_headers(language::Capability::ReadOnly)
1238                    });
1239                    multi_buffer.update(cx, |multi_buffer, cx| {
1240                        multi_buffer.push_excerpts(
1241                            old_buffer.clone(),
1242                            Some(ExcerptRange {
1243                                context: buffer_start..buffer_end,
1244                                primary: None,
1245                            }),
1246                            cx,
1247                        );
1248                    });
1249
1250                    enum DeletedLines {}
1251                    let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
1252                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1253                    editor.set_show_wrap_guides(false, cx);
1254                    editor.set_show_gutter(false, cx);
1255                    editor.scroll_manager.set_forbid_vertical_scroll(true);
1256                    editor.set_read_only(true);
1257                    editor.set_show_inline_completions(Some(false), cx);
1258                    editor.highlight_rows::<DeletedLines>(
1259                        Anchor::min()..Anchor::max(),
1260                        cx.theme().status().deleted_background,
1261                        false,
1262                        cx,
1263                    );
1264                    editor
1265                });
1266
1267                let height =
1268                    deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
1269                new_blocks.push(BlockProperties {
1270                    placement: BlockPlacement::Above(new_row),
1271                    height,
1272                    style: BlockStyle::Flex,
1273                    render: Arc::new(move |cx| {
1274                        div()
1275                            .block_mouse_down()
1276                            .bg(cx.theme().status().deleted_background)
1277                            .size_full()
1278                            .h(height as f32 * cx.line_height())
1279                            .pl(cx.gutter_dimensions.full_width())
1280                            .child(deleted_lines_editor.clone())
1281                            .into_any_element()
1282                    }),
1283                    priority: 0,
1284                });
1285            }
1286
1287            decorations.removed_line_block_ids = editor
1288                .insert_blocks(new_blocks, None, cx)
1289                .into_iter()
1290                .collect();
1291        })
1292    }
1293
1294    fn resolve_inline_assist_target(
1295        workspace: &mut Workspace,
1296        cx: &mut WindowContext,
1297    ) -> Option<InlineAssistTarget> {
1298        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
1299            if terminal_panel
1300                .read(cx)
1301                .focus_handle(cx)
1302                .contains_focused(cx)
1303            {
1304                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
1305                    pane.read(cx)
1306                        .active_item()
1307                        .and_then(|t| t.downcast::<TerminalView>())
1308                }) {
1309                    return Some(InlineAssistTarget::Terminal(terminal_view));
1310                }
1311            }
1312        }
1313
1314        if let Some(workspace_editor) = workspace
1315            .active_item(cx)
1316            .and_then(|item| item.act_as::<Editor>(cx))
1317        {
1318            Some(InlineAssistTarget::Editor(workspace_editor))
1319        } else if let Some(terminal_view) = workspace
1320            .active_item(cx)
1321            .and_then(|item| item.act_as::<TerminalView>(cx))
1322        {
1323            Some(InlineAssistTarget::Terminal(terminal_view))
1324        } else {
1325            None
1326        }
1327    }
1328}
1329
1330struct EditorInlineAssists {
1331    assist_ids: Vec<InlineAssistId>,
1332    scroll_lock: Option<InlineAssistScrollLock>,
1333    highlight_updates: async_watch::Sender<()>,
1334    _update_highlights: Task<Result<()>>,
1335    _subscriptions: Vec<gpui::Subscription>,
1336}
1337
1338struct InlineAssistScrollLock {
1339    assist_id: InlineAssistId,
1340    distance_from_top: f32,
1341}
1342
1343impl EditorInlineAssists {
1344    #[allow(clippy::too_many_arguments)]
1345    fn new(editor: &View<Editor>, cx: &mut WindowContext) -> Self {
1346        let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
1347        Self {
1348            assist_ids: Vec::new(),
1349            scroll_lock: None,
1350            highlight_updates: highlight_updates_tx,
1351            _update_highlights: cx.spawn(|mut cx| {
1352                let editor = editor.downgrade();
1353                async move {
1354                    while let Ok(()) = highlight_updates_rx.changed().await {
1355                        let editor = editor.upgrade().context("editor was dropped")?;
1356                        cx.update_global(|assistant: &mut InlineAssistant, cx| {
1357                            assistant.update_editor_highlights(&editor, cx);
1358                        })?;
1359                    }
1360                    Ok(())
1361                }
1362            }),
1363            _subscriptions: vec![
1364                cx.observe_release(editor, {
1365                    let editor = editor.downgrade();
1366                    |_, cx| {
1367                        InlineAssistant::update_global(cx, |this, cx| {
1368                            this.handle_editor_release(editor, cx);
1369                        })
1370                    }
1371                }),
1372                cx.observe(editor, move |editor, cx| {
1373                    InlineAssistant::update_global(cx, |this, cx| {
1374                        this.handle_editor_change(editor, cx)
1375                    })
1376                }),
1377                cx.subscribe(editor, move |editor, event, cx| {
1378                    InlineAssistant::update_global(cx, |this, cx| {
1379                        this.handle_editor_event(editor, event, cx)
1380                    })
1381                }),
1382                editor.update(cx, |editor, cx| {
1383                    let editor_handle = cx.view().downgrade();
1384                    editor.register_action(
1385                        move |_: &editor::actions::Newline, cx: &mut WindowContext| {
1386                            InlineAssistant::update_global(cx, |this, cx| {
1387                                if let Some(editor) = editor_handle.upgrade() {
1388                                    this.handle_editor_newline(editor, cx)
1389                                }
1390                            })
1391                        },
1392                    )
1393                }),
1394                editor.update(cx, |editor, cx| {
1395                    let editor_handle = cx.view().downgrade();
1396                    editor.register_action(
1397                        move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
1398                            InlineAssistant::update_global(cx, |this, cx| {
1399                                if let Some(editor) = editor_handle.upgrade() {
1400                                    this.handle_editor_cancel(editor, cx)
1401                                }
1402                            })
1403                        },
1404                    )
1405                }),
1406            ],
1407        }
1408    }
1409}
1410
1411struct InlineAssistGroup {
1412    assist_ids: Vec<InlineAssistId>,
1413    linked: bool,
1414    active_assist_id: Option<InlineAssistId>,
1415}
1416
1417impl InlineAssistGroup {
1418    fn new() -> Self {
1419        Self {
1420            assist_ids: Vec::new(),
1421            linked: true,
1422            active_assist_id: None,
1423        }
1424    }
1425}
1426
1427fn build_assist_editor_renderer(editor: &View<PromptEditor<BufferCodegen>>) -> RenderBlock {
1428    let editor = editor.clone();
1429
1430    Arc::new(move |cx: &mut BlockContext| {
1431        let gutter_dimensions = editor.read(cx).gutter_dimensions();
1432
1433        *gutter_dimensions.lock() = *cx.gutter_dimensions;
1434        editor.clone().into_any_element()
1435    })
1436}
1437
1438#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1439struct InlineAssistGroupId(usize);
1440
1441impl InlineAssistGroupId {
1442    fn post_inc(&mut self) -> InlineAssistGroupId {
1443        let id = *self;
1444        self.0 += 1;
1445        id
1446    }
1447}
1448
1449pub struct InlineAssist {
1450    group_id: InlineAssistGroupId,
1451    range: Range<Anchor>,
1452    editor: WeakView<Editor>,
1453    decorations: Option<InlineAssistDecorations>,
1454    codegen: Model<BufferCodegen>,
1455    _subscriptions: Vec<Subscription>,
1456    workspace: WeakView<Workspace>,
1457}
1458
1459impl InlineAssist {
1460    #[allow(clippy::too_many_arguments)]
1461    fn new(
1462        assist_id: InlineAssistId,
1463        group_id: InlineAssistGroupId,
1464        editor: &View<Editor>,
1465        prompt_editor: &View<PromptEditor<BufferCodegen>>,
1466        prompt_block_id: CustomBlockId,
1467        end_block_id: CustomBlockId,
1468        range: Range<Anchor>,
1469        codegen: Model<BufferCodegen>,
1470        workspace: WeakView<Workspace>,
1471        cx: &mut WindowContext,
1472    ) -> Self {
1473        let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
1474        InlineAssist {
1475            group_id,
1476            editor: editor.downgrade(),
1477            decorations: Some(InlineAssistDecorations {
1478                prompt_block_id,
1479                prompt_editor: prompt_editor.clone(),
1480                removed_line_block_ids: HashSet::default(),
1481                end_block_id,
1482            }),
1483            range,
1484            codegen: codegen.clone(),
1485            workspace: workspace.clone(),
1486            _subscriptions: vec![
1487                cx.on_focus_in(&prompt_editor_focus_handle, move |cx| {
1488                    InlineAssistant::update_global(cx, |this, cx| {
1489                        this.handle_prompt_editor_focus_in(assist_id, cx)
1490                    })
1491                }),
1492                cx.on_focus_out(&prompt_editor_focus_handle, move |_, cx| {
1493                    InlineAssistant::update_global(cx, |this, cx| {
1494                        this.handle_prompt_editor_focus_out(assist_id, cx)
1495                    })
1496                }),
1497                cx.subscribe(prompt_editor, |prompt_editor, event, cx| {
1498                    InlineAssistant::update_global(cx, |this, cx| {
1499                        this.handle_prompt_editor_event(prompt_editor, event, cx)
1500                    })
1501                }),
1502                cx.observe(&codegen, {
1503                    let editor = editor.downgrade();
1504                    move |_, cx| {
1505                        if let Some(editor) = editor.upgrade() {
1506                            InlineAssistant::update_global(cx, |this, cx| {
1507                                if let Some(editor_assists) =
1508                                    this.assists_by_editor.get(&editor.downgrade())
1509                                {
1510                                    editor_assists.highlight_updates.send(()).ok();
1511                                }
1512
1513                                this.update_editor_blocks(&editor, assist_id, cx);
1514                            })
1515                        }
1516                    }
1517                }),
1518                cx.subscribe(&codegen, move |codegen, event, cx| {
1519                    InlineAssistant::update_global(cx, |this, cx| match event {
1520                        CodegenEvent::Undone => this.finish_assist(assist_id, false, cx),
1521                        CodegenEvent::Finished => {
1522                            let assist = if let Some(assist) = this.assists.get(&assist_id) {
1523                                assist
1524                            } else {
1525                                return;
1526                            };
1527
1528                            if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
1529                                if assist.decorations.is_none() {
1530                                    if let Some(workspace) = assist.workspace.upgrade() {
1531                                        let error = format!("Inline assistant error: {}", error);
1532                                        workspace.update(cx, |workspace, cx| {
1533                                            struct InlineAssistantError;
1534
1535                                            let id =
1536                                                NotificationId::composite::<InlineAssistantError>(
1537                                                    assist_id.0,
1538                                                );
1539
1540                                            workspace.show_toast(Toast::new(id, error), cx);
1541                                        })
1542                                    }
1543                                }
1544                            }
1545
1546                            if assist.decorations.is_none() {
1547                                this.finish_assist(assist_id, false, cx);
1548                            }
1549                        }
1550                    })
1551                }),
1552            ],
1553        }
1554    }
1555
1556    fn user_prompt(&self, cx: &AppContext) -> Option<String> {
1557        let decorations = self.decorations.as_ref()?;
1558        Some(decorations.prompt_editor.read(cx).prompt(cx))
1559    }
1560}
1561
1562struct InlineAssistDecorations {
1563    prompt_block_id: CustomBlockId,
1564    prompt_editor: View<PromptEditor<BufferCodegen>>,
1565    removed_line_block_ids: HashSet<CustomBlockId>,
1566    end_block_id: CustomBlockId,
1567}
1568
1569struct AssistantCodeActionProvider {
1570    editor: WeakView<Editor>,
1571    workspace: WeakView<Workspace>,
1572    thread_store: Option<WeakModel<ThreadStore>>,
1573}
1574
1575impl CodeActionProvider for AssistantCodeActionProvider {
1576    fn code_actions(
1577        &self,
1578        buffer: &Model<Buffer>,
1579        range: Range<text::Anchor>,
1580        cx: &mut WindowContext,
1581    ) -> Task<Result<Vec<CodeAction>>> {
1582        if !AssistantSettings::get_global(cx).enabled {
1583            return Task::ready(Ok(Vec::new()));
1584        }
1585
1586        let snapshot = buffer.read(cx).snapshot();
1587        let mut range = range.to_point(&snapshot);
1588
1589        // Expand the range to line boundaries.
1590        range.start.column = 0;
1591        range.end.column = snapshot.line_len(range.end.row);
1592
1593        let mut has_diagnostics = false;
1594        for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
1595            range.start = cmp::min(range.start, diagnostic.range.start);
1596            range.end = cmp::max(range.end, diagnostic.range.end);
1597            has_diagnostics = true;
1598        }
1599        if has_diagnostics {
1600            if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
1601                if let Some(symbol) = symbols_containing_start.last() {
1602                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1603                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1604                }
1605            }
1606
1607            if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
1608                if let Some(symbol) = symbols_containing_end.last() {
1609                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1610                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1611                }
1612            }
1613
1614            Task::ready(Ok(vec![CodeAction {
1615                server_id: language::LanguageServerId(0),
1616                range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
1617                lsp_action: lsp::CodeAction {
1618                    title: "Fix with Assistant".into(),
1619                    ..Default::default()
1620                },
1621            }]))
1622        } else {
1623            Task::ready(Ok(Vec::new()))
1624        }
1625    }
1626
1627    fn apply_code_action(
1628        &self,
1629        buffer: Model<Buffer>,
1630        action: CodeAction,
1631        excerpt_id: ExcerptId,
1632        _push_to_history: bool,
1633        cx: &mut WindowContext,
1634    ) -> Task<Result<ProjectTransaction>> {
1635        let editor = self.editor.clone();
1636        let workspace = self.workspace.clone();
1637        let thread_store = self.thread_store.clone();
1638        cx.spawn(|mut cx| async move {
1639            let editor = editor.upgrade().context("editor was released")?;
1640            let range = editor
1641                .update(&mut cx, |editor, cx| {
1642                    editor.buffer().update(cx, |multibuffer, cx| {
1643                        let buffer = buffer.read(cx);
1644                        let multibuffer_snapshot = multibuffer.read(cx);
1645
1646                        let old_context_range =
1647                            multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
1648                        let mut new_context_range = old_context_range.clone();
1649                        if action
1650                            .range
1651                            .start
1652                            .cmp(&old_context_range.start, buffer)
1653                            .is_lt()
1654                        {
1655                            new_context_range.start = action.range.start;
1656                        }
1657                        if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
1658                            new_context_range.end = action.range.end;
1659                        }
1660                        drop(multibuffer_snapshot);
1661
1662                        if new_context_range != old_context_range {
1663                            multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
1664                        }
1665
1666                        let multibuffer_snapshot = multibuffer.read(cx);
1667                        Some(
1668                            multibuffer_snapshot
1669                                .anchor_in_excerpt(excerpt_id, action.range.start)?
1670                                ..multibuffer_snapshot
1671                                    .anchor_in_excerpt(excerpt_id, action.range.end)?,
1672                        )
1673                    })
1674                })?
1675                .context("invalid range")?;
1676
1677            cx.update_global(|assistant: &mut InlineAssistant, cx| {
1678                let assist_id = assistant.suggest_assist(
1679                    &editor,
1680                    range,
1681                    "Fix Diagnostics".into(),
1682                    None,
1683                    true,
1684                    workspace,
1685                    thread_store,
1686                    cx,
1687                );
1688                assistant.start_assist(assist_id, cx);
1689            })?;
1690
1691            Ok(ProjectTransaction::default())
1692        })
1693    }
1694}
1695
1696fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1697    ranges.sort_unstable_by(|a, b| {
1698        a.start
1699            .cmp(&b.start, buffer)
1700            .then_with(|| b.end.cmp(&a.end, buffer))
1701    });
1702
1703    let mut ix = 0;
1704    while ix + 1 < ranges.len() {
1705        let b = ranges[ix + 1].clone();
1706        let a = &mut ranges[ix];
1707        if a.end.cmp(&b.start, buffer).is_gt() {
1708            if a.end.cmp(&b.end, buffer).is_lt() {
1709                a.end = b.end;
1710            }
1711            ranges.remove(ix + 1);
1712        } else {
1713            ix += 1;
1714        }
1715    }
1716}