diff --git a/Cargo.lock b/Cargo.lock index 2a4c6c4f4394a7c3e13dd00b2178640528b8d034..e74068b2d2a0085dcea482788beac5de90fe2f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,7 +122,6 @@ dependencies = [ "serde", "serde_json", "settings", - "similar", "smol", "theme", "tiktoken-rs 0.4.5", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 3ec994335e3ce0d67c8ed115ae86ce5b60abe1ac..a81c18f7082b884c74de65d4be79fb12f236916a 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -528,7 +528,8 @@ "bindings": { "alt-enter": "editor::OpenExcerpts", "cmd-f8": "editor::GoToHunk", - "cmd-shift-f8": "editor::GoToPrevHunk" + "cmd-shift-f8": "editor::GoToPrevHunk", + "ctrl-enter": "assistant::InlineAssist" } }, { diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index b03405bb93fa72f6079a6ca32e661e7d37bf704d..4438f88108988e715d476a65fc2566d8a5f8e090 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -31,7 +31,6 @@ regex.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true -similar = "1.3" smol.workspace = true tiktoken-rs = "0.4" diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 48f490c9c0597a5e66261a6a77fcd268d808b768..0b56fedb110ae5696c2a4525f9e8405b9f9b9350 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,7 +1,5 @@ pub mod assistant; mod assistant_settings; -mod refactoring_assistant; -mod refactoring_modal; mod streaming_diff; use anyhow::{anyhow, Result}; @@ -196,7 +194,6 @@ struct OpenAIChoice { pub fn init(cx: &mut AppContext) { assistant::init(cx); - refactoring_modal::init(cx); } pub async fn stream_completion( diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f134eeeeb69ab1cfc291ffa29e0679436cff7219..58be0fe5849e2c5bf6c3b70465a1d591e70166af 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,18 +1,22 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, - Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, + stream_completion, + streaming_diff::{Hunk, StreamingDiff}, + MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, + SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::{HashMap, HashSet}; use editor::{ - display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, + display_map::{ + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, + }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset, + Anchor, Editor, ToOffset, ToPoint, }; use fs::Fs; -use futures::StreamExt; +use futures::{channel::mpsc, SinkExt, StreamExt}; use gpui::{ actions, elements::*, @@ -21,7 +25,10 @@ use gpui::{ Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use language::{ + language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, Selection, ToOffset as _, + TransactionId, +}; use search::BufferSearchBar; use settings::SettingsStore; use std::{ @@ -53,6 +60,7 @@ actions!( QuoteSelection, ToggleFocus, ResetKey, + InlineAssist ] ); @@ -84,6 +92,9 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action(AssistantPanel::inline_assist); + cx.add_action(InlineAssistant::confirm); + cx.add_action(InlineAssistant::cancel); } #[derive(Debug)] @@ -113,6 +124,9 @@ pub struct AssistantPanel { languages: Arc, fs: Arc, subscriptions: Vec, + next_inline_assist_id: usize, + pending_inline_assists: HashMap, + pending_inline_assist_ids_by_editor: HashMap, Vec>, _watch_saved_conversations: Task>, } @@ -176,6 +190,9 @@ impl AssistantPanel { width: None, height: None, subscriptions: Default::default(), + next_inline_assist_id: 0, + pending_inline_assists: Default::default(), + pending_inline_assist_ids_by_editor: Default::default(), _watch_saved_conversations, }; @@ -196,6 +213,425 @@ impl AssistantPanel { }) } + fn inline_assist(workspace: &mut Workspace, _: &InlineAssist, cx: &mut ViewContext) { + let assistant = if let Some(assistant) = workspace.panel::(cx) { + if assistant + .update(cx, |assistant, cx| assistant.load_api_key(cx)) + .is_some() + { + assistant + } else { + workspace.focus_panel::(cx); + return; + } + } else { + return; + }; + + let active_editor = if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor + } else { + return; + }; + + assistant.update(cx, |assistant, cx| { + assistant.new_inline_assist(&active_editor, cx) + }); + } + + fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + let id = post_inc(&mut self.next_inline_assist_id); + let (block_id, inline_assistant, selection) = editor.update(cx, |editor, cx| { + let selection = editor.selections.newest_anchor().clone(); + let prompt_editor = cx.add_view(|cx| { + Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ) + }); + let assist_kind = if editor.selections.newest::(cx).is_empty() { + InlineAssistKind::Insert + } else { + InlineAssistKind::Edit + }; + let assistant = cx.add_view(|_| InlineAssistant { + id, + prompt_editor, + confirmed: false, + has_focus: false, + assist_kind, + }); + cx.focus(&assistant); + + let block_id = editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: selection.head(), + height: 2, + render: Arc::new({ + let assistant = assistant.clone(); + move |cx: &mut BlockContext| { + ChildView::new(&assistant, cx) + .contained() + .with_padding_left(match assist_kind { + InlineAssistKind::Edit => cx.gutter_width, + InlineAssistKind::Insert => cx.anchor_x, + }) + .into_any() + } + }), + disposition: if selection.reversed { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0]; + editor.highlight_background::( + vec![selection.start..selection.end], + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + + (block_id, assistant, selection) + }); + + self.pending_inline_assists.insert( + id, + PendingInlineAssist { + editor: editor.downgrade(), + selection, + inline_assistant_block_id: Some(block_id), + code_generation: Task::ready(None), + transaction_id: None, + _subscriptions: vec![ + cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), + cx.subscribe(editor, { + let inline_assistant = inline_assistant.downgrade(); + move |_, editor, event, cx| { + if let Some(inline_assistant) = inline_assistant.upgrade(cx) { + if let editor::Event::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } + } + } + } + }), + ], + }, + ); + self.pending_inline_assist_ids_by_editor + .entry(editor.downgrade()) + .or_default() + .push(id); + } + + fn handle_inline_assistant_event( + &mut self, + inline_assistant: ViewHandle, + event: &InlineAssistantEvent, + cx: &mut ViewContext, + ) { + let assist_id = inline_assistant.read(cx).id; + match event { + InlineAssistantEvent::Confirmed { prompt } => { + self.generate_code(assist_id, prompt, cx); + } + InlineAssistantEvent::Canceled => { + self.complete_inline_assist(assist_id, true, cx); + } + InlineAssistantEvent::Dismissed => { + self.dismiss_inline_assist(assist_id, cx); + } + } + } + + fn complete_inline_assist( + &mut self, + assist_id: usize, + cancel: bool, + cx: &mut ViewContext, + ) { + self.dismiss_inline_assist(assist_id, cx); + + if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { + self.pending_inline_assist_ids_by_editor + .remove(&pending_assist.editor); + + if let Some(editor) = pending_assist.editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx); + editor.clear_text_highlights::(cx); + }); + + if cancel { + if let Some(transaction_id) = pending_assist.transaction_id { + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.undo_and_forget(transaction_id, cx) + }); + }); + } + } + } + } + } + + fn dismiss_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { + if let Some(editor) = pending_assist.editor.upgrade(cx) { + if let Some(block_id) = pending_assist.inline_assistant_block_id.take() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }); + } + } + } + } + + pub fn generate_code( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + cx: &mut ViewContext, + ) { + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { + api_key + } else { + return; + }; + + let pending_assist = + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { + pending_assist + } else { + return; + }; + + let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) { + editor + } else { + return; + }; + + let selection = pending_assist.selection.clone(); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let selected_text = snapshot + .text_for_range(selection.start..selection.end) + .collect::(); + + let mut normalized_selected_text = selected_text.clone(); + let mut base_indentation: Option = None; + let selection_start = selection.start.to_point(&snapshot); + let selection_end = selection.end.to_point(&snapshot); + if selection_start.row < selection_end.row { + for row in selection_start.row..=selection_end.row { + if snapshot.is_line_blank(row) { + continue; + } + + let line_indentation = snapshot.indent_size_for_line(row); + if let Some(base_indentation) = base_indentation.as_mut() { + if line_indentation.len < base_indentation.len { + *base_indentation = line_indentation; + } + } else { + base_indentation = Some(line_indentation); + } + } + } + + if let Some(base_indentation) = base_indentation { + for row in selection_start.row..=selection_end.row { + let selection_row = row - selection_start.row; + let line_start = + normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); + let indentation_len = if row == selection_start.row { + base_indentation.len.saturating_sub(selection_start.column) + } else { + let line_len = normalized_selected_text.line_len(selection_row); + cmp::min(line_len, base_indentation.len) + }; + let indentation_end = cmp::min( + line_start + indentation_len as usize, + normalized_selected_text.len(), + ); + normalized_selected_text.replace(line_start..indentation_end, ""); + } + } + + let language_name = snapshot + .language_at(selection.start) + .map(|language| language.name()); + let language_name = language_name.as_deref().unwrap_or(""); + + let mut prompt = String::new(); + writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); + writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "{user_prompt}.").unwrap(); + writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); + let request = OpenAIRequest { + model: "gpt-4".into(), + messages: vec![RequestMessage { + role: Role::User, + content: prompt, + }], + stream: true, + }; + let response = stream_completion(api_key, cx.background().clone(), request); + let editor = editor.downgrade(); + + pending_assist.code_generation = cx.spawn(|this, mut cx| { + async move { + let _cleanup = util::defer({ + let mut cx = cx.clone(); + let this = this.clone(); + move || { + let _ = this.update(&mut cx, |this, cx| { + this.complete_inline_assist(inline_assist_id, false, cx) + }); + } + }); + + let mut edit_start = selection.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let mut messages = response.await?; + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let indentation_len; + let indentation_text; + if let Some(base_indentation) = base_indentation { + indentation_len = base_indentation.len; + indentation_text = match base_indentation.kind { + language::IndentKind::Space => " ", + language::IndentKind::Tab => "\t", + }; + } else { + indentation_len = 0; + indentation_text = ""; + }; + + let mut new_text = indentation_text + .repeat(indentation_len.saturating_sub(selection_start.column) as usize); + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + if let Some(text) = choice.delta.content { + let mut lines = text.split('\n'); + if let Some(first_line) = lines.next() { + new_text.push_str(&first_line); + } + + for line in lines { + new_text.push('\n'); + new_text.push_str( + &indentation_text.repeat(indentation_len as usize), + ); + new_text.push_str(line); + } + } + } + + let hunks = diff.push_new(&new_text); + hunks_tx.send(hunks).await?; + new_text.clear(); + } + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("assistant was dropped"))?; + editor.update(&mut cx, |editor, cx| { + let mut highlights = Vec::new(); + + let transaction = editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start += len; + highlights.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + this.update(cx, |this, cx| { + if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + if let Some(first_transaction) = pending_assist.transaction_id { + // Group all assistant edits into the first transaction. + editor.buffer().update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + pending_assist.transaction_id = Some(transaction); + editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + }); + } + + editor.highlight_text::( + highlights, + gpui::fonts::HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + })?; + } + diff.await?; + + anyhow::Ok(()) + } + .log_err() + }); + } + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let editor = cx.add_view(|cx| { ConversationEditor::new( @@ -565,6 +1001,32 @@ impl AssistantPanel { .iter() .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) } + + pub fn load_api_key(&mut self, cx: &mut ViewContext) -> Option { + if self.api_key.borrow().is_none() && !self.has_read_credentials { + self.has_read_credentials = true; + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + if let Some(api_key) = api_key { + *self.api_key.borrow_mut() = Some(api_key); + } else if self.api_key_editor.is_none() { + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.notify(); + } + } + + self.api_key.borrow().clone() + } } fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { @@ -748,27 +1210,7 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active { - if self.api_key.borrow().is_none() && !self.has_read_credentials { - self.has_read_credentials = true; - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - if let Some(api_key) = api_key { - *self.api_key.borrow_mut() = Some(api_key); - } else if self.api_key_editor.is_none() { - self.api_key_editor = Some(build_api_key_editor(cx)); - cx.notify(); - } - } + self.load_api_key(cx); if self.editors.is_empty() { self.new_conversation(cx); @@ -2139,6 +2581,84 @@ impl Message { } } +enum InlineAssistantEvent { + Confirmed { prompt: String }, + Canceled, + Dismissed, +} + +#[derive(Copy, Clone)] +enum InlineAssistKind { + Edit, + Insert, +} + +struct InlineAssistant { + id: usize, + prompt_editor: ViewHandle, + confirmed: bool, + assist_kind: InlineAssistKind, + has_focus: bool, +} + +impl Entity for InlineAssistant { + type Event = InlineAssistantEvent; +} + +impl View for InlineAssistant { + fn ui_name() -> &'static str { + "InlineAssistant" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx); + let prompt_editor = ChildView::new(&self.prompt_editor, cx).aligned().left(); + match self.assist_kind { + InlineAssistKind::Edit => prompt_editor + .contained() + .with_style(theme.assistant.inline.container) + .into_any(), + InlineAssistKind::Insert => prompt_editor.into_any(), + } + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.focus(&self.prompt_editor); + self.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl InlineAssistant { + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(InlineAssistantEvent::Canceled); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if self.confirmed { + cx.emit(InlineAssistantEvent::Dismissed); + } else { + let prompt = self.prompt_editor.read(cx).text(cx); + self.prompt_editor + .update(cx, |editor, _| editor.set_read_only(true)); + cx.emit(InlineAssistantEvent::Confirmed { prompt }); + self.confirmed = true; + } + } +} + +struct PendingInlineAssist { + editor: WeakViewHandle, + selection: Selection, + inline_assistant_block_id: Option, + code_generation: Task>, + transaction_id: Option, + _subscriptions: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ai/src/refactoring_assistant.rs b/crates/ai/src/refactoring_assistant.rs deleted file mode 100644 index 5562cb46063cdb5018e4947dd0dc1ba9a3e821ee..0000000000000000000000000000000000000000 --- a/crates/ai/src/refactoring_assistant.rs +++ /dev/null @@ -1,252 +0,0 @@ -use collections::HashMap; -use editor::{Editor, ToOffset, ToPoint}; -use futures::{channel::mpsc, SinkExt, StreamExt}; -use gpui::{AppContext, Task, ViewHandle}; -use language::{Point, Rope}; -use std::{cmp, env, fmt::Write}; -use util::TryFutureExt; - -use crate::{ - stream_completion, - streaming_diff::{Hunk, StreamingDiff}, - OpenAIRequest, RequestMessage, Role, -}; - -pub struct RefactoringAssistant { - pending_edits_by_editor: HashMap>>, -} - -impl RefactoringAssistant { - fn new() -> Self { - Self { - pending_edits_by_editor: Default::default(), - } - } - - pub fn update(cx: &mut AppContext, f: F) -> T - where - F: FnOnce(&mut Self, &mut AppContext) -> T, - { - if !cx.has_global::() { - cx.set_global(Self::new()); - } - - cx.update_global(f) - } - - pub fn refactor( - &mut self, - editor: &ViewHandle, - user_prompt: &str, - cx: &mut AppContext, - ) { - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - api_key - } else { - // TODO: ensure the API key is present by going through the assistant panel's flow. - return; - }; - - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let selection = editor.read(cx).selections.newest_anchor().clone(); - let selected_text = snapshot - .text_for_range(selection.start..selection.end) - .collect::(); - - let mut normalized_selected_text = selected_text.clone(); - let mut base_indentation: Option = None; - let selection_start = selection.start.to_point(&snapshot); - let selection_end = selection.end.to_point(&snapshot); - if selection_start.row < selection_end.row { - for row in selection_start.row..=selection_end.row { - if snapshot.is_line_blank(row) { - continue; - } - - let line_indentation = snapshot.indent_size_for_line(row); - if let Some(base_indentation) = base_indentation.as_mut() { - if line_indentation.len < base_indentation.len { - *base_indentation = line_indentation; - } - } else { - base_indentation = Some(line_indentation); - } - } - } - - if let Some(base_indentation) = base_indentation { - for row in selection_start.row..=selection_end.row { - let selection_row = row - selection_start.row; - let line_start = - normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); - let indentation_len = if row == selection_start.row { - base_indentation.len.saturating_sub(selection_start.column) - } else { - let line_len = normalized_selected_text.line_len(selection_row); - cmp::min(line_len, base_indentation.len) - }; - let indentation_end = cmp::min( - line_start + indentation_len as usize, - normalized_selected_text.len(), - ); - normalized_selected_text.replace(line_start..indentation_end, ""); - } - } - - let language_name = snapshot - .language_at(selection.start) - .map(|language| language.name()); - let language_name = language_name.as_deref().unwrap_or(""); - - let mut prompt = String::new(); - writeln!(prompt, "Given the following {language_name} snippet:").unwrap(); - writeln!(prompt, "{normalized_selected_text}").unwrap(); - writeln!(prompt, "{user_prompt}.").unwrap(); - writeln!(prompt, "Never make remarks, reply only with the new code.").unwrap(); - let request = OpenAIRequest { - model: "gpt-4".into(), - messages: vec![RequestMessage { - role: Role::User, - content: prompt, - }], - stream: true, - }; - let response = stream_completion(api_key, cx.background().clone(), request); - let editor = editor.downgrade(); - self.pending_edits_by_editor.insert( - editor.id(), - cx.spawn(|mut cx| { - async move { - let _clear_highlights = util::defer({ - let mut cx = cx.clone(); - let editor = editor.clone(); - move || { - let _ = editor.update(&mut cx, |editor, cx| { - editor.clear_text_highlights::(cx); - }); - } - }); - - let mut edit_start = selection.start.to_offset(&snapshot); - - let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { - let mut messages = response.await?.ready_chunks(4); - let mut diff = StreamingDiff::new(selected_text.to_string()); - - let indentation_len; - let indentation_text; - if let Some(base_indentation) = base_indentation { - indentation_len = base_indentation.len; - indentation_text = match base_indentation.kind { - language::IndentKind::Space => " ", - language::IndentKind::Tab => "\t", - }; - } else { - indentation_len = 0; - indentation_text = ""; - }; - - let mut new_text = - indentation_text.repeat( - indentation_len.saturating_sub(selection_start.column) as usize, - ); - while let Some(messages) = messages.next().await { - for message in messages { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - if let Some(text) = choice.delta.content { - let mut lines = text.split('\n'); - if let Some(first_line) = lines.next() { - new_text.push_str(&first_line); - } - - for line in lines { - new_text.push('\n'); - new_text.push_str( - &indentation_text.repeat(indentation_len as usize), - ); - new_text.push_str(line); - } - } - } - } - - let hunks = diff.push_new(&new_text); - hunks_tx.send(hunks).await?; - new_text.clear(); - } - hunks_tx.send(diff.finish()).await?; - - anyhow::Ok(()) - }); - - let mut first_transaction = None; - while let Some(hunks) = hunks_rx.next().await { - editor.update(&mut cx, |editor, cx| { - let mut highlights = Vec::new(); - - editor.buffer().update(cx, |buffer, cx| { - // Avoid grouping assistant edits with user edits. - buffer.finalize_last_transaction(cx); - - buffer.start_transaction(cx); - buffer.edit( - hunks.into_iter().filter_map(|hunk| match hunk { - Hunk::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - Some((edit_start..edit_start, text)) - } - Hunk::Remove { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start = edit_end; - Some((edit_range, String::new())) - } - Hunk::Keep { len } => { - let edit_end = edit_start + len; - let edit_range = snapshot.anchor_after(edit_start) - ..snapshot.anchor_before(edit_end); - edit_start += len; - highlights.push(edit_range); - None - } - }), - None, - cx, - ); - if let Some(transaction) = buffer.end_transaction(cx) { - if let Some(first_transaction) = first_transaction { - // Group all assistant edits into the first transaction. - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ); - } else { - first_transaction = Some(transaction); - buffer.finalize_last_transaction(cx); - } - } - }); - - editor.highlight_text::( - highlights, - gpui::fonts::HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - })?; - } - diff.await?; - - anyhow::Ok(()) - } - .log_err() - }), - ); - } -} diff --git a/crates/ai/src/refactoring_modal.rs b/crates/ai/src/refactoring_modal.rs deleted file mode 100644 index 675e0fae99dc593f6d7dedf9b9090e0a7cabcf27..0000000000000000000000000000000000000000 --- a/crates/ai/src/refactoring_modal.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::refactoring_assistant::RefactoringAssistant; -use collections::HashSet; -use editor::{ - display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle}, - scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Editor, -}; -use gpui::{ - actions, elements::*, platform::MouseButton, AnyViewHandle, AppContext, Entity, View, - ViewContext, ViewHandle, WeakViewHandle, -}; -use std::sync::Arc; -use workspace::Workspace; - -actions!(assistant, [Refactor]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(RefactoringModal::deploy); - cx.add_action(RefactoringModal::confirm); - cx.add_action(RefactoringModal::cancel); -} - -enum Event { - Dismissed, -} - -struct RefactoringModal { - active_editor: WeakViewHandle, - prompt_editor: ViewHandle, - has_focus: bool, -} - -impl Entity for RefactoringModal { - type Event = Event; -} - -impl View for RefactoringModal { - fn ui_name() -> &'static str { - "RefactoringModal" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx); - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .contained() - .with_style(theme.assistant.modal.container) - .mouse::(0) - .on_click_out(MouseButton::Left, |_, _, cx| cx.emit(Event::Dismissed)) - .on_click_out(MouseButton::Right, |_, _, cx| cx.emit(Event::Dismissed)) - .into_any() - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - cx.focus(&self.prompt_editor); - } - - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - if !self.prompt_editor.is_focused(cx) { - self.has_focus = false; - cx.emit(Event::Dismissed); - } - } -} - -impl RefactoringModal { - fn deploy(workspace: &mut Workspace, _: &Refactor, cx: &mut ViewContext) { - if let Some(active_editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - active_editor.update(cx, |editor, cx| { - let position = editor.selections.newest_anchor().head(); - let prompt_editor = cx.add_view(|cx| { - Editor::single_line( - Some(Arc::new(|theme| theme.assistant.modal.editor.clone())), - cx, - ) - }); - let active_editor = cx.weak_handle(); - let refactoring = cx.add_view(|_| RefactoringModal { - active_editor, - prompt_editor, - has_focus: false, - }); - cx.focus(&refactoring); - - let block_id = editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - position, - height: 2, - render: Arc::new({ - let refactoring = refactoring.clone(); - move |cx: &mut BlockContext| { - ChildView::new(&refactoring, cx) - .contained() - .with_padding_left(cx.gutter_width) - .into_any() - } - }), - disposition: BlockDisposition::Below, - }], - Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), - cx, - )[0]; - cx.subscribe(&refactoring, move |_, refactoring, event, cx| { - let Event::Dismissed = event; - if let Some(active_editor) = refactoring.read(cx).active_editor.upgrade(cx) { - cx.window_context().defer(move |cx| { - active_editor.update(cx, |editor, cx| { - editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - }) - }); - } - }) - .detach(); - }); - } - } - - fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismissed); - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(editor) = self.active_editor.upgrade(cx) { - let prompt = self.prompt_editor.read(cx).text(cx); - RefactoringAssistant::update(cx, |assistant, cx| { - assistant.refactor(&editor, &prompt, cx); - }); - cx.emit(Event::Dismissed); - } - } -} diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs index 5425b75bbe8a62403dd490e2aa6d4a7ee119852c..7ea7f6dacdb813e22df1a33fbc8ec48a54cb8e3e 100644 --- a/crates/ai/src/streaming_diff.rs +++ b/crates/ai/src/streaming_diff.rs @@ -83,8 +83,8 @@ pub struct StreamingDiff { impl StreamingDiff { const INSERTION_SCORE: f64 = -1.; const DELETION_SCORE: f64 = -5.; - const EQUALITY_BASE: f64 = 1.4; - const MAX_EQUALITY_EXPONENT: i32 = 64; + const EQUALITY_BASE: f64 = 2.; + const MAX_EQUALITY_EXPONENT: i32 = 20; pub fn new(old: String) -> Self { let old = old.chars().collect::>(); @@ -117,12 +117,8 @@ impl StreamingDiff { equal_run += 1; self.equal_runs.insert((i, j), equal_run); - if self.old[i - 1] == ' ' { - self.scores.get(i - 1, j - 1) - } else { - let exponent = cmp::min(equal_run as i32, Self::MAX_EQUALITY_EXPONENT); - self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) - } + let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) } else { f64::NEG_INFINITY }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 904e77c9f0fdd3dccb84758b97e0005573dcaee2..0283b396f3f8f29d846f63a907b5554008d2cb8f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8209,7 +8209,7 @@ impl View for Editor { "Editor" } - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { let focused_event = EditorFocused(cx.handle()); cx.emit(Event::Focused); @@ -8217,7 +8217,7 @@ impl View for Editor { } if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); - } else { + } else if cx.is_self_focused() || !focused.is::() { if !self.focused { self.blink_manager.update(cx, BlinkManager::enable); } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 88c66d5200e4d74dacab8e2c3932826c76fbb69b..0990fdbcb75b41a20d606e6a67642d1c540c56c3 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -626,7 +626,7 @@ impl MultiBuffer { buffer.merge_transactions(transaction, destination) }); } else { - if let Some(transaction) = self.history.remove_transaction(transaction) { + if let Some(transaction) = self.history.forget(transaction) { if let Some(destination) = self.history.transaction_mut(destination) { for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { if let Some(destination_buffer_transaction_id) = @@ -822,6 +822,18 @@ impl MultiBuffer { None } + pub fn undo_and_forget(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); + } else if let Some(transaction) = self.history.forget(transaction_id) { + for (buffer_id, transaction_id) in transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(&buffer_id) { + buffer.update(cx, |buffer, cx| buffer.undo_and_forget(transaction_id, cx)); + } + } + } + } + pub fn stream_excerpts_with_context_lines( &mut self, excerpts: Vec<(ModelHandle, Vec>)>, @@ -3369,7 +3381,7 @@ impl History { } } - fn remove_transaction(&mut self, transaction_id: TransactionId) -> Option { + fn forget(&mut self, transaction_id: TransactionId) -> Option { if let Some(ix) = self .undo_stack .iter() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e2154f498eb9ed8a22fab83ab4b37c9b147c6edf..e8bbe29b47f75fb872fe6ba47d281c275c490cb3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1664,6 +1664,22 @@ impl Buffer { } } + pub fn undo_and_forget( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) -> bool { + let was_dirty = self.is_dirty(); + let old_version = self.version.clone(); + if let Some(operation) = self.text.undo_and_forget(transaction_id) { + self.send_operation(Operation::Buffer(operation), cx); + self.did_edit(&old_version, was_dirty, cx); + true + } else { + false + } + } + pub fn undo_to_transaction( &mut self, transaction_id: TransactionId, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8f15535ccf7aa56c749449fd417a9eda28fa0b5d..02f1be718f132b911b0b84312f3893d549e43e10 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -22,6 +22,7 @@ use postage::{oneshot, prelude::*}; pub use rope::*; pub use selection::*; +use util::ResultExt; use std::{ cmp::{self, Ordering, Reverse}, @@ -1206,6 +1207,14 @@ impl Buffer { } } + pub fn undo_and_forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(transaction) = self.history.forget(transaction_id) { + self.undo_or_redo(transaction).log_err() + } else { + None + } + } + #[allow(clippy::needless_collect)] pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { let transactions = self diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ebc959123933f9bb3f6e3ef8b7f6a9c1634f1c89..02d0de490513f68379a941fd6575c2c92643088f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1124,14 +1124,15 @@ pub struct AssistantStyle { pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, pub saved_conversation: SavedConversation, - pub modal: ModalAssistantStyle, + pub inline: InlineAssistantStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] -pub struct ModalAssistantStyle { +pub struct InlineAssistantStyle { #[serde(flatten)] pub container: ContainerStyle, pub editor: FieldEditor, + pub pending_edit_background: Color, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index ac91d1118de351d5dfda0ec7b3062d053cca3c5e..97bb3402b6b4600ad72ffbeaed049f431043fbfe 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -59,7 +59,7 @@ export default function assistant(): any { background: background(theme.highest), padding: { left: 12 }, }, - modal: { + inline: { border: border(theme.lowest, "on", { top: true, bottom: true, @@ -69,7 +69,8 @@ export default function assistant(): any { text: text(theme.lowest, "mono", "on", { size: "sm" }), placeholder_text: text(theme.lowest, "sans", "on", "disabled"), selection: theme.players[0], - } + }, + pending_edit_background: background(theme.highest, "positive"), }, message_header: { margin: { bottom: 4, top: 4 },