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