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.set_assistant_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(workspace.clone()));
 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(workspace.clone()));
 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 snapshot = multibuffer.snapshot(cx);
 875                    let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
 876                    ranges
 877                        .first()
 878                        .and_then(|(excerpt, _)| excerpt.buffer().language())
 879                        .map(|language| language.name())
 880                });
 881                report_assistant_event(
 882                    AssistantEvent {
 883                        conversation_id: None,
 884                        kind: AssistantKind::Inline,
 885                        message_id,
 886                        phase: if undo {
 887                            AssistantPhase::Rejected
 888                        } else {
 889                            AssistantPhase::Accepted
 890                        },
 891                        model: model.telemetry_id(),
 892                        model_provider: model.provider_id().to_string(),
 893                        response_latency: None,
 894                        error_message: None,
 895                        language_name: language_name.map(|name| name.to_proto()),
 896                    },
 897                    Some(self.telemetry.clone()),
 898                    cx.http_client(),
 899                    model.api_key(cx),
 900                    cx.background_executor(),
 901                );
 902            }
 903
 904            if undo {
 905                assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
 906            } else {
 907                self.confirmed_assists.insert(assist_id, active_alternative);
 908            }
 909        }
 910    }
 911
 912    fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
 913        let Some(assist) = self.assists.get_mut(&assist_id) else {
 914            return false;
 915        };
 916        let Some(editor) = assist.editor.upgrade() else {
 917            return false;
 918        };
 919        let Some(decorations) = assist.decorations.take() else {
 920            return false;
 921        };
 922
 923        editor.update(cx, |editor, cx| {
 924            let mut to_remove = decorations.removed_line_block_ids;
 925            to_remove.insert(decorations.prompt_block_id);
 926            to_remove.insert(decorations.end_block_id);
 927            editor.remove_blocks(to_remove, None, cx);
 928        });
 929
 930        if decorations
 931            .prompt_editor
 932            .focus_handle(cx)
 933            .contains_focused(cx)
 934        {
 935            self.focus_next_assist(assist_id, cx);
 936        }
 937
 938        if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
 939            if editor_assists
 940                .scroll_lock
 941                .as_ref()
 942                .map_or(false, |lock| lock.assist_id == assist_id)
 943            {
 944                editor_assists.scroll_lock = None;
 945            }
 946            editor_assists.highlight_updates.send(()).ok();
 947        }
 948
 949        true
 950    }
 951
 952    fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 953        let Some(assist) = self.assists.get(&assist_id) else {
 954            return;
 955        };
 956
 957        let assist_group = &self.assist_groups[&assist.group_id];
 958        let assist_ix = assist_group
 959            .assist_ids
 960            .iter()
 961            .position(|id| *id == assist_id)
 962            .unwrap();
 963        let assist_ids = assist_group
 964            .assist_ids
 965            .iter()
 966            .skip(assist_ix + 1)
 967            .chain(assist_group.assist_ids.iter().take(assist_ix));
 968
 969        for assist_id in assist_ids {
 970            let assist = &self.assists[assist_id];
 971            if assist.decorations.is_some() {
 972                self.focus_assist(*assist_id, cx);
 973                return;
 974            }
 975        }
 976
 977        assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok();
 978    }
 979
 980    fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 981        let Some(assist) = self.assists.get(&assist_id) else {
 982            return;
 983        };
 984
 985        if let Some(decorations) = assist.decorations.as_ref() {
 986            decorations.prompt_editor.update(cx, |prompt_editor, cx| {
 987                prompt_editor.editor.update(cx, |editor, cx| {
 988                    editor.focus(cx);
 989                    editor.select_all(&SelectAll, cx);
 990                })
 991            });
 992        }
 993
 994        self.scroll_to_assist(assist_id, cx);
 995    }
 996
 997    pub fn scroll_to_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
 998        let Some(assist) = self.assists.get(&assist_id) else {
 999            return;
1000        };
1001        let Some(editor) = assist.editor.upgrade() else {
1002            return;
1003        };
1004
1005        let position = assist.range.start;
1006        editor.update(cx, |editor, cx| {
1007            editor.change_selections(None, cx, |selections| {
1008                selections.select_anchor_ranges([position..position])
1009            });
1010
1011            let mut scroll_target_top;
1012            let mut scroll_target_bottom;
1013            if let Some(decorations) = assist.decorations.as_ref() {
1014                scroll_target_top = editor
1015                    .row_for_block(decorations.prompt_block_id, cx)
1016                    .unwrap()
1017                    .0 as f32;
1018                scroll_target_bottom = editor
1019                    .row_for_block(decorations.end_block_id, cx)
1020                    .unwrap()
1021                    .0 as f32;
1022            } else {
1023                let snapshot = editor.snapshot(cx);
1024                let start_row = assist
1025                    .range
1026                    .start
1027                    .to_display_point(&snapshot.display_snapshot)
1028                    .row();
1029                scroll_target_top = start_row.0 as f32;
1030                scroll_target_bottom = scroll_target_top + 1.;
1031            }
1032            scroll_target_top -= editor.vertical_scroll_margin() as f32;
1033            scroll_target_bottom += editor.vertical_scroll_margin() as f32;
1034
1035            let height_in_lines = editor.visible_line_count().unwrap_or(0.);
1036            let scroll_top = editor.scroll_position(cx).y;
1037            let scroll_bottom = scroll_top + height_in_lines;
1038
1039            if scroll_target_top < scroll_top {
1040                editor.set_scroll_position(point(0., scroll_target_top), cx);
1041            } else if scroll_target_bottom > scroll_bottom {
1042                if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
1043                    editor
1044                        .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx);
1045                } else {
1046                    editor.set_scroll_position(point(0., scroll_target_top), cx);
1047                }
1048            }
1049        });
1050    }
1051
1052    fn unlink_assist_group(
1053        &mut self,
1054        assist_group_id: InlineAssistGroupId,
1055        cx: &mut WindowContext,
1056    ) -> Vec<InlineAssistId> {
1057        let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
1058        assist_group.linked = false;
1059        for assist_id in &assist_group.assist_ids {
1060            let assist = self.assists.get_mut(assist_id).unwrap();
1061            if let Some(editor_decorations) = assist.decorations.as_ref() {
1062                editor_decorations
1063                    .prompt_editor
1064                    .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx));
1065            }
1066        }
1067        assist_group.assist_ids.clone()
1068    }
1069
1070    pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1071        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1072            assist
1073        } else {
1074            return;
1075        };
1076
1077        let assist_group_id = assist.group_id;
1078        if self.assist_groups[&assist_group_id].linked {
1079            for assist_id in self.unlink_assist_group(assist_group_id, cx) {
1080                self.start_assist(assist_id, cx);
1081            }
1082            return;
1083        }
1084
1085        let Some(user_prompt) = assist.user_prompt(cx) else {
1086            return;
1087        };
1088
1089        self.prompt_history.retain(|prompt| *prompt != user_prompt);
1090        self.prompt_history.push_back(user_prompt.clone());
1091        if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
1092            self.prompt_history.pop_front();
1093        }
1094
1095        assist
1096            .codegen
1097            .update(cx, |codegen, cx| codegen.start(user_prompt, cx))
1098            .log_err();
1099    }
1100
1101    pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1102        let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1103            assist
1104        } else {
1105            return;
1106        };
1107
1108        assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
1109    }
1110
1111    fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
1112        let mut gutter_pending_ranges = Vec::new();
1113        let mut gutter_transformed_ranges = Vec::new();
1114        let mut foreground_ranges = Vec::new();
1115        let mut inserted_row_ranges = Vec::new();
1116        let empty_assist_ids = Vec::new();
1117        let assist_ids = self
1118            .assists_by_editor
1119            .get(&editor.downgrade())
1120            .map_or(&empty_assist_ids, |editor_assists| {
1121                &editor_assists.assist_ids
1122            });
1123
1124        for assist_id in assist_ids {
1125            if let Some(assist) = self.assists.get(assist_id) {
1126                let codegen = assist.codegen.read(cx);
1127                let buffer = codegen.buffer(cx).read(cx).read(cx);
1128                foreground_ranges.extend(codegen.last_equal_ranges(cx).iter().cloned());
1129
1130                let pending_range =
1131                    codegen.edit_position(cx).unwrap_or(assist.range.start)..assist.range.end;
1132                if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
1133                    gutter_pending_ranges.push(pending_range);
1134                }
1135
1136                if let Some(edit_position) = codegen.edit_position(cx) {
1137                    let edited_range = assist.range.start..edit_position;
1138                    if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
1139                        gutter_transformed_ranges.push(edited_range);
1140                    }
1141                }
1142
1143                if assist.decorations.is_some() {
1144                    inserted_row_ranges
1145                        .extend(codegen.diff(cx).inserted_row_ranges.iter().cloned());
1146                }
1147            }
1148        }
1149
1150        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
1151        merge_ranges(&mut foreground_ranges, &snapshot);
1152        merge_ranges(&mut gutter_pending_ranges, &snapshot);
1153        merge_ranges(&mut gutter_transformed_ranges, &snapshot);
1154        editor.update(cx, |editor, cx| {
1155            enum GutterPendingRange {}
1156            if gutter_pending_ranges.is_empty() {
1157                editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1158            } else {
1159                editor.highlight_gutter::<GutterPendingRange>(
1160                    &gutter_pending_ranges,
1161                    |cx| cx.theme().status().info_background,
1162                    cx,
1163                )
1164            }
1165
1166            enum GutterTransformedRange {}
1167            if gutter_transformed_ranges.is_empty() {
1168                editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1169            } else {
1170                editor.highlight_gutter::<GutterTransformedRange>(
1171                    &gutter_transformed_ranges,
1172                    |cx| cx.theme().status().info,
1173                    cx,
1174                )
1175            }
1176
1177            if foreground_ranges.is_empty() {
1178                editor.clear_highlights::<InlineAssist>(cx);
1179            } else {
1180                editor.highlight_text::<InlineAssist>(
1181                    foreground_ranges,
1182                    HighlightStyle {
1183                        fade_out: Some(0.6),
1184                        ..Default::default()
1185                    },
1186                    cx,
1187                );
1188            }
1189
1190            editor.clear_row_highlights::<InlineAssist>();
1191            for row_range in inserted_row_ranges {
1192                editor.highlight_rows::<InlineAssist>(
1193                    row_range,
1194                    cx.theme().status().info_background,
1195                    false,
1196                    cx,
1197                );
1198            }
1199        });
1200    }
1201
1202    fn update_editor_blocks(
1203        &mut self,
1204        editor: &View<Editor>,
1205        assist_id: InlineAssistId,
1206        cx: &mut WindowContext,
1207    ) {
1208        let Some(assist) = self.assists.get_mut(&assist_id) else {
1209            return;
1210        };
1211        let Some(decorations) = assist.decorations.as_mut() else {
1212            return;
1213        };
1214
1215        let codegen = assist.codegen.read(cx);
1216        let old_snapshot = codegen.snapshot(cx);
1217        let old_buffer = codegen.old_buffer(cx);
1218        let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
1219
1220        editor.update(cx, |editor, cx| {
1221            let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
1222            editor.remove_blocks(old_blocks, None, cx);
1223
1224            let mut new_blocks = Vec::new();
1225            for (new_row, old_row_range) in deleted_row_ranges {
1226                let (_, buffer_start) = old_snapshot
1227                    .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
1228                    .unwrap();
1229                let (_, buffer_end) = old_snapshot
1230                    .point_to_buffer_offset(Point::new(
1231                        *old_row_range.end(),
1232                        old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
1233                    ))
1234                    .unwrap();
1235
1236                let deleted_lines_editor = cx.new_view(|cx| {
1237                    let multi_buffer = cx.new_model(|_| {
1238                        MultiBuffer::without_headers(language::Capability::ReadOnly)
1239                    });
1240                    multi_buffer.update(cx, |multi_buffer, cx| {
1241                        multi_buffer.push_excerpts(
1242                            old_buffer.clone(),
1243                            Some(ExcerptRange {
1244                                context: buffer_start..buffer_end,
1245                                primary: None,
1246                            }),
1247                            cx,
1248                        );
1249                    });
1250
1251                    enum DeletedLines {}
1252                    let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
1253                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1254                    editor.set_show_wrap_guides(false, cx);
1255                    editor.set_show_gutter(false, cx);
1256                    editor.scroll_manager.set_forbid_vertical_scroll(true);
1257                    editor.set_read_only(true);
1258                    editor.set_show_inline_completions(Some(false), cx);
1259                    editor.highlight_rows::<DeletedLines>(
1260                        Anchor::min()..Anchor::max(),
1261                        cx.theme().status().deleted_background,
1262                        false,
1263                        cx,
1264                    );
1265                    editor
1266                });
1267
1268                let height =
1269                    deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
1270                new_blocks.push(BlockProperties {
1271                    placement: BlockPlacement::Above(new_row),
1272                    height,
1273                    style: BlockStyle::Flex,
1274                    render: Arc::new(move |cx| {
1275                        div()
1276                            .block_mouse_down()
1277                            .bg(cx.theme().status().deleted_background)
1278                            .size_full()
1279                            .h(height as f32 * cx.line_height())
1280                            .pl(cx.gutter_dimensions.full_width())
1281                            .child(deleted_lines_editor.clone())
1282                            .into_any_element()
1283                    }),
1284                    priority: 0,
1285                });
1286            }
1287
1288            decorations.removed_line_block_ids = editor
1289                .insert_blocks(new_blocks, None, cx)
1290                .into_iter()
1291                .collect();
1292        })
1293    }
1294
1295    fn resolve_inline_assist_target(
1296        workspace: &mut Workspace,
1297        cx: &mut WindowContext,
1298    ) -> Option<InlineAssistTarget> {
1299        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
1300            if terminal_panel
1301                .read(cx)
1302                .focus_handle(cx)
1303                .contains_focused(cx)
1304            {
1305                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
1306                    pane.read(cx)
1307                        .active_item()
1308                        .and_then(|t| t.downcast::<TerminalView>())
1309                }) {
1310                    return Some(InlineAssistTarget::Terminal(terminal_view));
1311                }
1312            }
1313        }
1314
1315        if let Some(workspace_editor) = workspace
1316            .active_item(cx)
1317            .and_then(|item| item.act_as::<Editor>(cx))
1318        {
1319            Some(InlineAssistTarget::Editor(workspace_editor))
1320        } else if let Some(terminal_view) = workspace
1321            .active_item(cx)
1322            .and_then(|item| item.act_as::<TerminalView>(cx))
1323        {
1324            Some(InlineAssistTarget::Terminal(terminal_view))
1325        } else {
1326            None
1327        }
1328    }
1329}
1330
1331struct EditorInlineAssists {
1332    assist_ids: Vec<InlineAssistId>,
1333    scroll_lock: Option<InlineAssistScrollLock>,
1334    highlight_updates: async_watch::Sender<()>,
1335    _update_highlights: Task<Result<()>>,
1336    _subscriptions: Vec<gpui::Subscription>,
1337}
1338
1339struct InlineAssistScrollLock {
1340    assist_id: InlineAssistId,
1341    distance_from_top: f32,
1342}
1343
1344impl EditorInlineAssists {
1345    #[allow(clippy::too_many_arguments)]
1346    fn new(editor: &View<Editor>, cx: &mut WindowContext) -> Self {
1347        let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
1348        Self {
1349            assist_ids: Vec::new(),
1350            scroll_lock: None,
1351            highlight_updates: highlight_updates_tx,
1352            _update_highlights: cx.spawn(|mut cx| {
1353                let editor = editor.downgrade();
1354                async move {
1355                    while let Ok(()) = highlight_updates_rx.changed().await {
1356                        let editor = editor.upgrade().context("editor was dropped")?;
1357                        cx.update_global(|assistant: &mut InlineAssistant, cx| {
1358                            assistant.update_editor_highlights(&editor, cx);
1359                        })?;
1360                    }
1361                    Ok(())
1362                }
1363            }),
1364            _subscriptions: vec![
1365                cx.observe_release(editor, {
1366                    let editor = editor.downgrade();
1367                    |_, cx| {
1368                        InlineAssistant::update_global(cx, |this, cx| {
1369                            this.handle_editor_release(editor, cx);
1370                        })
1371                    }
1372                }),
1373                cx.observe(editor, move |editor, cx| {
1374                    InlineAssistant::update_global(cx, |this, cx| {
1375                        this.handle_editor_change(editor, cx)
1376                    })
1377                }),
1378                cx.subscribe(editor, move |editor, event, cx| {
1379                    InlineAssistant::update_global(cx, |this, cx| {
1380                        this.handle_editor_event(editor, event, cx)
1381                    })
1382                }),
1383                editor.update(cx, |editor, cx| {
1384                    let editor_handle = cx.view().downgrade();
1385                    editor.register_action(
1386                        move |_: &editor::actions::Newline, cx: &mut WindowContext| {
1387                            InlineAssistant::update_global(cx, |this, cx| {
1388                                if let Some(editor) = editor_handle.upgrade() {
1389                                    this.handle_editor_newline(editor, cx)
1390                                }
1391                            })
1392                        },
1393                    )
1394                }),
1395                editor.update(cx, |editor, cx| {
1396                    let editor_handle = cx.view().downgrade();
1397                    editor.register_action(
1398                        move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
1399                            InlineAssistant::update_global(cx, |this, cx| {
1400                                if let Some(editor) = editor_handle.upgrade() {
1401                                    this.handle_editor_cancel(editor, cx)
1402                                }
1403                            })
1404                        },
1405                    )
1406                }),
1407            ],
1408        }
1409    }
1410}
1411
1412struct InlineAssistGroup {
1413    assist_ids: Vec<InlineAssistId>,
1414    linked: bool,
1415    active_assist_id: Option<InlineAssistId>,
1416}
1417
1418impl InlineAssistGroup {
1419    fn new() -> Self {
1420        Self {
1421            assist_ids: Vec::new(),
1422            linked: true,
1423            active_assist_id: None,
1424        }
1425    }
1426}
1427
1428fn build_assist_editor_renderer(editor: &View<PromptEditor<BufferCodegen>>) -> RenderBlock {
1429    let editor = editor.clone();
1430
1431    Arc::new(move |cx: &mut BlockContext| {
1432        let gutter_dimensions = editor.read(cx).gutter_dimensions();
1433
1434        *gutter_dimensions.lock() = *cx.gutter_dimensions;
1435        editor.clone().into_any_element()
1436    })
1437}
1438
1439#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1440struct InlineAssistGroupId(usize);
1441
1442impl InlineAssistGroupId {
1443    fn post_inc(&mut self) -> InlineAssistGroupId {
1444        let id = *self;
1445        self.0 += 1;
1446        id
1447    }
1448}
1449
1450pub struct InlineAssist {
1451    group_id: InlineAssistGroupId,
1452    range: Range<Anchor>,
1453    editor: WeakView<Editor>,
1454    decorations: Option<InlineAssistDecorations>,
1455    codegen: Model<BufferCodegen>,
1456    _subscriptions: Vec<Subscription>,
1457    workspace: WeakView<Workspace>,
1458}
1459
1460impl InlineAssist {
1461    #[allow(clippy::too_many_arguments)]
1462    fn new(
1463        assist_id: InlineAssistId,
1464        group_id: InlineAssistGroupId,
1465        editor: &View<Editor>,
1466        prompt_editor: &View<PromptEditor<BufferCodegen>>,
1467        prompt_block_id: CustomBlockId,
1468        end_block_id: CustomBlockId,
1469        range: Range<Anchor>,
1470        codegen: Model<BufferCodegen>,
1471        workspace: WeakView<Workspace>,
1472        cx: &mut WindowContext,
1473    ) -> Self {
1474        let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
1475        InlineAssist {
1476            group_id,
1477            editor: editor.downgrade(),
1478            decorations: Some(InlineAssistDecorations {
1479                prompt_block_id,
1480                prompt_editor: prompt_editor.clone(),
1481                removed_line_block_ids: HashSet::default(),
1482                end_block_id,
1483            }),
1484            range,
1485            codegen: codegen.clone(),
1486            workspace: workspace.clone(),
1487            _subscriptions: vec![
1488                cx.on_focus_in(&prompt_editor_focus_handle, move |cx| {
1489                    InlineAssistant::update_global(cx, |this, cx| {
1490                        this.handle_prompt_editor_focus_in(assist_id, cx)
1491                    })
1492                }),
1493                cx.on_focus_out(&prompt_editor_focus_handle, move |_, cx| {
1494                    InlineAssistant::update_global(cx, |this, cx| {
1495                        this.handle_prompt_editor_focus_out(assist_id, cx)
1496                    })
1497                }),
1498                cx.subscribe(prompt_editor, |prompt_editor, event, cx| {
1499                    InlineAssistant::update_global(cx, |this, cx| {
1500                        this.handle_prompt_editor_event(prompt_editor, event, cx)
1501                    })
1502                }),
1503                cx.observe(&codegen, {
1504                    let editor = editor.downgrade();
1505                    move |_, cx| {
1506                        if let Some(editor) = editor.upgrade() {
1507                            InlineAssistant::update_global(cx, |this, cx| {
1508                                if let Some(editor_assists) =
1509                                    this.assists_by_editor.get(&editor.downgrade())
1510                                {
1511                                    editor_assists.highlight_updates.send(()).ok();
1512                                }
1513
1514                                this.update_editor_blocks(&editor, assist_id, cx);
1515                            })
1516                        }
1517                    }
1518                }),
1519                cx.subscribe(&codegen, move |codegen, event, cx| {
1520                    InlineAssistant::update_global(cx, |this, cx| match event {
1521                        CodegenEvent::Undone => this.finish_assist(assist_id, false, cx),
1522                        CodegenEvent::Finished => {
1523                            let assist = if let Some(assist) = this.assists.get(&assist_id) {
1524                                assist
1525                            } else {
1526                                return;
1527                            };
1528
1529                            if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
1530                                if assist.decorations.is_none() {
1531                                    if let Some(workspace) = assist.workspace.upgrade() {
1532                                        let error = format!("Inline assistant error: {}", error);
1533                                        workspace.update(cx, |workspace, cx| {
1534                                            struct InlineAssistantError;
1535
1536                                            let id =
1537                                                NotificationId::composite::<InlineAssistantError>(
1538                                                    assist_id.0,
1539                                                );
1540
1541                                            workspace.show_toast(Toast::new(id, error), cx);
1542                                        })
1543                                    }
1544                                }
1545                            }
1546
1547                            if assist.decorations.is_none() {
1548                                this.finish_assist(assist_id, false, cx);
1549                            }
1550                        }
1551                    })
1552                }),
1553            ],
1554        }
1555    }
1556
1557    fn user_prompt(&self, cx: &AppContext) -> Option<String> {
1558        let decorations = self.decorations.as_ref()?;
1559        Some(decorations.prompt_editor.read(cx).prompt(cx))
1560    }
1561}
1562
1563struct InlineAssistDecorations {
1564    prompt_block_id: CustomBlockId,
1565    prompt_editor: View<PromptEditor<BufferCodegen>>,
1566    removed_line_block_ids: HashSet<CustomBlockId>,
1567    end_block_id: CustomBlockId,
1568}
1569
1570struct AssistantCodeActionProvider {
1571    editor: WeakView<Editor>,
1572    workspace: WeakView<Workspace>,
1573    thread_store: Option<WeakModel<ThreadStore>>,
1574}
1575
1576impl CodeActionProvider for AssistantCodeActionProvider {
1577    fn code_actions(
1578        &self,
1579        buffer: &Model<Buffer>,
1580        range: Range<text::Anchor>,
1581        cx: &mut WindowContext,
1582    ) -> Task<Result<Vec<CodeAction>>> {
1583        if !AssistantSettings::get_global(cx).enabled {
1584            return Task::ready(Ok(Vec::new()));
1585        }
1586
1587        let snapshot = buffer.read(cx).snapshot();
1588        let mut range = range.to_point(&snapshot);
1589
1590        // Expand the range to line boundaries.
1591        range.start.column = 0;
1592        range.end.column = snapshot.line_len(range.end.row);
1593
1594        let mut has_diagnostics = false;
1595        for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
1596            range.start = cmp::min(range.start, diagnostic.range.start);
1597            range.end = cmp::max(range.end, diagnostic.range.end);
1598            has_diagnostics = true;
1599        }
1600        if has_diagnostics {
1601            if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
1602                if let Some(symbol) = symbols_containing_start.last() {
1603                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1604                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1605                }
1606            }
1607
1608            if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
1609                if let Some(symbol) = symbols_containing_end.last() {
1610                    range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1611                    range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1612                }
1613            }
1614
1615            Task::ready(Ok(vec![CodeAction {
1616                server_id: language::LanguageServerId(0),
1617                range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
1618                lsp_action: lsp::CodeAction {
1619                    title: "Fix with Assistant".into(),
1620                    ..Default::default()
1621                },
1622            }]))
1623        } else {
1624            Task::ready(Ok(Vec::new()))
1625        }
1626    }
1627
1628    fn apply_code_action(
1629        &self,
1630        buffer: Model<Buffer>,
1631        action: CodeAction,
1632        excerpt_id: ExcerptId,
1633        _push_to_history: bool,
1634        cx: &mut WindowContext,
1635    ) -> Task<Result<ProjectTransaction>> {
1636        let editor = self.editor.clone();
1637        let workspace = self.workspace.clone();
1638        let thread_store = self.thread_store.clone();
1639        cx.spawn(|mut cx| async move {
1640            let editor = editor.upgrade().context("editor was released")?;
1641            let range = editor
1642                .update(&mut cx, |editor, cx| {
1643                    editor.buffer().update(cx, |multibuffer, cx| {
1644                        let buffer = buffer.read(cx);
1645                        let multibuffer_snapshot = multibuffer.read(cx);
1646
1647                        let old_context_range =
1648                            multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
1649                        let mut new_context_range = old_context_range.clone();
1650                        if action
1651                            .range
1652                            .start
1653                            .cmp(&old_context_range.start, buffer)
1654                            .is_lt()
1655                        {
1656                            new_context_range.start = action.range.start;
1657                        }
1658                        if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
1659                            new_context_range.end = action.range.end;
1660                        }
1661                        drop(multibuffer_snapshot);
1662
1663                        if new_context_range != old_context_range {
1664                            multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
1665                        }
1666
1667                        let multibuffer_snapshot = multibuffer.read(cx);
1668                        Some(
1669                            multibuffer_snapshot
1670                                .anchor_in_excerpt(excerpt_id, action.range.start)?
1671                                ..multibuffer_snapshot
1672                                    .anchor_in_excerpt(excerpt_id, action.range.end)?,
1673                        )
1674                    })
1675                })?
1676                .context("invalid range")?;
1677
1678            cx.update_global(|assistant: &mut InlineAssistant, cx| {
1679                let assist_id = assistant.suggest_assist(
1680                    &editor,
1681                    range,
1682                    "Fix Diagnostics".into(),
1683                    None,
1684                    true,
1685                    workspace,
1686                    thread_store,
1687                    cx,
1688                );
1689                assistant.start_assist(assist_id, cx);
1690            })?;
1691
1692            Ok(ProjectTransaction::default())
1693        })
1694    }
1695}
1696
1697fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1698    ranges.sort_unstable_by(|a, b| {
1699        a.start
1700            .cmp(&b.start, buffer)
1701            .then_with(|| b.end.cmp(&a.end, buffer))
1702    });
1703
1704    let mut ix = 0;
1705    while ix + 1 < ranges.len() {
1706        let b = ranges[ix + 1].clone();
1707        let a = &mut ranges[ix];
1708        if a.end.cmp(&b.start, buffer).is_gt() {
1709            if a.end.cmp(&b.end, buffer).is_lt() {
1710                a.end = b.end;
1711            }
1712            ranges.remove(ix + 1);
1713        } else {
1714            ix += 1;
1715        }
1716    }
1717}