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