diff --git a/Cargo.lock b/Cargo.lock index a0b41534db..28608194f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,46 +328,6 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "assistant" version = "0.1.0" -dependencies = [ - "ai", - "anyhow", - "chrono", - "client", - "collections", - "ctor", - "editor", - "env_logger", - "fs", - "futures 0.3.28", - "gpui", - "indoc", - "isahc", - "language", - "log", - "menu", - "multi_buffer", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "project", - "rand 0.8.5", - "regex", - "schemars", - "search", - "semantic_index", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "tiktoken-rs", - "util", - "uuid 1.4.1", - "workspace", -] - -[[package]] -name = "assistant2" -version = "0.1.0" dependencies = [ "ai2", "anyhow", @@ -4193,24 +4153,6 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "journal" version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "dirs 4.0.0", - "editor", - "gpui", - "log", - "schemars", - "serde", - "settings", - "shellexpand", - "util", - "workspace", -] - -[[package]] -name = "journal2" -version = "0.1.0" dependencies = [ "anyhow", "chrono", @@ -6516,7 +6458,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ - "assistant2", + "assistant", "editor", "gpui2", "search", @@ -10862,7 +10804,7 @@ dependencies = [ "activity_indicator", "ai2", "anyhow", - "assistant2", + "assistant", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -10899,7 +10841,7 @@ dependencies = [ "indexmap 1.9.3", "install_cli2", "isahc", - "journal2", + "journal", "language2", "language_selector", "language_tools", diff --git a/Cargo.toml b/Cargo.toml index 2620f2495a..b55d064e52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "crates/activity_indicator", "crates/ai", "crates/assistant", - "crates/assistant2", "crates/audio", "crates/audio2", "crates/auto_update", @@ -50,7 +49,7 @@ members = [ "crates/install_cli", "crates/install_cli2", "crates/journal", - "crates/journal2", + "crates/journal", "crates/language", "crates/language2", "crates/language_selector", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 876e5e0b76..b03fef4515 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -9,22 +9,23 @@ path = "src/assistant.rs" doctest = false [dependencies] -ai = { path = "../ai" } -client = { path = "../client" } +ai = { package = "ai2", path = "../ai2" } +client = { package = "client2", path = "../client2" } collections = { path = "../collections"} editor = { path = "../editor" } -fs = { path = "../fs" } -gpui = { path = "../gpui" } -language = { path = "../language" } -menu = { path = "../menu" } +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } multi_buffer = { path = "../multi_buffer" } +project = { package = "project2", path = "../project2" } search = { path = "../search" } -settings = { path = "../settings" } -theme = { path = "../theme" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } workspace = { path = "../workspace" } -semantic_index = { path = "../semantic_index" } -project = { path = "../project" } uuid.workspace = true log.workspace = true @@ -43,9 +44,9 @@ smol.workspace = true tiktoken-rs.workspace = true [dev-dependencies] +ai = { package = "ai2", path = "../ai2", features = ["test-support"]} editor = { path = "../editor", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } -ai = { path = "../ai", features = ["test-support"]} +project = { package = "project2", path = "../project2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 91d61a19f9..743c8b22e6 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -12,12 +12,28 @@ use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; use futures::StreamExt; -use gpui::AppContext; +use gpui::{actions, AppContext, SharedString}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use util::paths::CONVERSATIONS_DIR; +actions!( + assistant, + [ + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, + ] +); + #[derive( Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] @@ -34,7 +50,7 @@ struct MessageMetadata { enum MessageStatus { Pending, Done, - Error(Arc), + Error(SharedString), } #[derive(Serialize, Deserialize)] diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e472e8c8df..7b19ad130c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2,8 +2,9 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, prompts::generate_content_prompt, - MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, + Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, + NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, }; use ai::{ @@ -22,28 +23,24 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, + ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; use gpui::{ - actions, - elements::{ - ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, - Stack, Svg, Text, UniformList, UniformListState, - }, - fonts::HighlightStyle, - geometry::vector::{vec2f, Vector2F}, - platform::{CursorStyle, MouseButton, PromptLevel}, - Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, - ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, - WeakModelHandle, WeakViewHandle, WindowContext, + canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, + AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle, + FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, + StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, + View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; use search::BufferSearchBar; use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use settings::SettingsStore; +use settings::{Settings, SettingsStore}; use std::{ cell::Cell, cmp, @@ -55,105 +52,60 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use theme::{ - components::{action_button::Button, ComponentExt}, - AssistantStyle, +use theme::ThemeSettings; +use ui::{ + prelude::*, + utils::{DateTimeType, FormatDistance}, + ButtonLike, Tab, TabBar, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ - dock::{DockPosition, Panel}, + dock::{DockPosition, Panel, PanelEvent}, searchable::Direction, Save, Toast, ToggleZoom, Toolbar, Workspace, }; -actions!( - assistant, - [ - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, - ] -); - pub fn init(cx: &mut AppContext) { - settings::register::(cx); - cx.add_action( - |this: &mut AssistantPanel, - _: &workspace::NewFile, - cx: &mut ViewContext| { - this.new_conversation(cx); + AssistantSettings::register(cx); + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace + .register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }) + .register_action(AssistantPanel::inline_assist) + .register_action(AssistantPanel::cancel_last_inline_assist) + .register_action(ConversationEditor::quote_selection); }, - ); - cx.add_action(ConversationEditor::assist); - cx.capture_action(ConversationEditor::cancel_last_assist); - cx.capture_action(ConversationEditor::save); - cx.add_action(ConversationEditor::quote_selection); - cx.capture_action(ConversationEditor::copy); - cx.add_action(ConversationEditor::split); - cx.capture_action(ConversationEditor::cycle_message_role); - cx.add_action(AssistantPanel::save_credentials); - cx.add_action(AssistantPanel::reset_credentials); - cx.add_action(AssistantPanel::toggle_zoom); - cx.add_action(AssistantPanel::deploy); - cx.add_action(AssistantPanel::select_next_match); - cx.add_action(AssistantPanel::select_prev_match); - cx.add_action(AssistantPanel::handle_editor_cancel); - cx.add_action( - |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); - }, - ); - cx.add_action(AssistantPanel::inline_assist); - cx.add_action(AssistantPanel::cancel_last_inline_assist); - cx.add_action(InlineAssistant::confirm); - cx.add_action(InlineAssistant::cancel); - cx.add_action(InlineAssistant::toggle_include_conversation); - cx.add_action(InlineAssistant::toggle_retrieve_context); - cx.add_action(InlineAssistant::move_up); - cx.add_action(InlineAssistant::move_down); -} - -#[derive(Debug)] -pub enum AssistantPanelEvent { - ZoomIn, - ZoomOut, - Focus, - Close, - DockPositionChanged, + ) + .detach(); } pub struct AssistantPanel { - workspace: WeakViewHandle, - width: Option, - height: Option, + workspace: WeakView, + width: Option, + height: Option, active_editor_index: Option, prev_active_editor_index: Option, - editors: Vec>, + editors: Vec>, saved_conversations: Vec, - saved_conversations_list_state: UniformListState, + saved_conversations_scroll_handle: UniformListScrollHandle, zoomed: bool, - has_focus: bool, - toolbar: ViewHandle, + focus_handle: FocusHandle, + toolbar: View, completion_provider: Arc, - api_key_editor: Option>, + api_key_editor: Option>, languages: Arc, fs: Arc, subscriptions: Vec, next_inline_assist_id: usize, pending_inline_assists: HashMap, - pending_inline_assist_ids_by_editor: HashMap, Vec>, + pending_inline_assist_ids_by_editor: HashMap, Vec>, include_conversation_in_next_inline_assist: bool, inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, - semantic_index: Option>, + semantic_index: Option>, retrieve_context_in_next_inline_assist: bool, } @@ -161,11 +113,11 @@ impl AssistantPanel { const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { cx.spawn(|mut cx| async move { - let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; let saved_conversations = SavedConversationMetadata::list(fs.clone()) .await .log_err() @@ -174,7 +126,7 @@ impl AssistantPanel { // TODO: deserialize state. let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { - cx.add_view::(|cx| { + cx.new_view::(|cx| { const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { let mut events = fs @@ -195,10 +147,10 @@ impl AssistantPanel { anyhow::Ok(()) }); - let toolbar = cx.add_view(|cx| { + let toolbar = cx.new_view(|cx| { let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar.add_item(cx.new_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); @@ -206,18 +158,22 @@ impl AssistantPanel { // Defaulting currently to GPT4, allow for this to be set via config. let completion_provider = Arc::new(OpenAICompletionProvider::new( "gpt-4", - cx.background().clone(), + cx.background_executor().clone(), )); + let focus_handle = cx.focus_handle(); + cx.on_focus_in(&focus_handle, Self::focus_in).detach(); + cx.on_focus_out(&focus_handle, Self::focus_out).detach(); + let mut this = Self { workspace: workspace_handle, active_editor_index: Default::default(), prev_active_editor_index: Default::default(), editors: Default::default(), saved_conversations, - saved_conversations_list_state: Default::default(), + saved_conversations_scroll_handle: Default::default(), zoomed: false, - has_focus: false, + focus_handle, toolbar, completion_provider, api_key_editor: None, @@ -238,11 +194,11 @@ impl AssistantPanel { let mut old_dock_position = this.position(cx); this.subscriptions = - vec![cx.observe_global::(move |this, cx| { + vec![cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(AssistantPanelEvent::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } cx.notify(); })]; @@ -253,6 +209,25 @@ impl AssistantPanel { }) } + fn focus_in(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); + cx.notify(); + if self.focus_handle.is_focused(cx) { + if let Some(editor) = self.active_editor() { + cx.focus_view(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus_view(api_key_editor); + } + } + } + + fn focus_out(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); + cx.notify(); + } + pub fn inline_assist( workspace: &mut Workspace, _: &InlineAssist, @@ -293,9 +268,9 @@ impl AssistantPanel { fn new_inline_assist( &mut self, - editor: &ViewHandle, + editor: &View, cx: &mut ViewContext, - project: &ModelHandle, + project: &Model, ) { let selection = editor.read(cx).selections.newest_anchor().clone(); if selection.start.excerpt_id != selection.end.excerpt_id { @@ -331,7 +306,7 @@ impl AssistantPanel { // Retrieve Credentials Authenticates the Provider provider.retrieve_credentials(cx); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -341,14 +316,14 @@ impl AssistantPanel { let previously_indexed = semantic_index .update(&mut cx, |index, cx| { index.project_previously_indexed(&project, cx) - }) + })? .await .unwrap_or(false); if previously_indexed { let _ = semantic_index .update(&mut cx, |index, cx| { index.index_project(project.clone(), cx) - }) + })? .await; } anyhow::Ok(()) @@ -357,8 +332,8 @@ impl AssistantPanel { } let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.add_view(|cx| { - let assistant = InlineAssistant::new( + let inline_assistant = cx.new_view(|cx| { + InlineAssistant::new( inline_assist_id, measurements.clone(), self.include_conversation_in_next_inline_assist, @@ -369,9 +344,7 @@ impl AssistantPanel { self.retrieve_context_in_next_inline_assist, self.semantic_index.clone(), project.clone(), - ); - cx.focus_self(); - assistant + ) }); let block_id = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |selections| { @@ -389,7 +362,7 @@ impl AssistantPanel { anchor_x: cx.anchor_x, gutter_width: cx.gutter_width, }); - ChildView::new(&inline_assistant, cx).into_any() + inline_assistant.clone().into_any_element() } }), disposition: if selection.reversed { @@ -415,10 +388,12 @@ impl AssistantPanel { 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); + if let Some(inline_assistant) = inline_assistant.upgrade() { + if let EditorEvent::SelectionsChanged { local } = event { + if *local + && inline_assistant.focus_handle(cx).contains_focused(cx) + { + cx.focus_view(&editor); } } } @@ -427,7 +402,7 @@ impl AssistantPanel { cx.observe(&codegen, { let editor = editor.downgrade(); move |this, _, cx| { - if let Some(editor) = editor.upgrade(cx) { + if let Some(editor) = editor.upgrade() { this.update_highlights_for_editor(&editor, cx); } } @@ -451,7 +426,7 @@ impl AssistantPanel { .map(|error| format!("Inline assistant error: {}", error)); if let Some(error) = error { if pending_assist.inline_assistant.is_none() { - if let Some(workspace) = this.workspace.upgrade(cx) { + if let Some(workspace) = this.workspace.upgrade() { workspace.update(cx, |workspace, cx| { workspace.show_toast( Toast::new(inline_assist_id, error), @@ -479,7 +454,7 @@ impl AssistantPanel { fn handle_inline_assistant_event( &mut self, - inline_assistant: ViewHandle, + inline_assistant: View, event: &InlineAssistantEvent, cx: &mut ViewContext, ) { @@ -543,7 +518,7 @@ impl AssistantPanel { } } - cx.propagate_action(); + cx.propagate(); } fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { @@ -552,7 +527,7 @@ impl AssistantPanel { if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor) + .entry(pending_assist.editor.clone()) { entry.get_mut().retain(|id| *id != assist_id); if entry.get().is_empty() { @@ -560,7 +535,7 @@ impl AssistantPanel { } } - if let Some(editor) = pending_assist.editor.upgrade(cx) { + if let Some(editor) = pending_assist.editor.upgrade() { self.update_highlights_for_editor(&editor, cx); if undo { @@ -574,10 +549,13 @@ impl AssistantPanel { fn hide_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.take() { + if let Some(editor) = pending_assist.editor.upgrade() { + if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() { editor.update(cx, |editor, cx| { editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + if inline_assistant.focus_handle(cx).contains_focused(cx) { + editor.focus(cx); + } }); } } @@ -606,7 +584,7 @@ impl AssistantPanel { return; }; - let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) { + let editor = if let Some(editor) = pending_assist.editor.upgrade() { editor } else { return; @@ -614,7 +592,7 @@ impl AssistantPanel { let project = pending_assist.project.clone(); - let project_name = if let Some(project) = project.upgrade(cx) { + let project_name = if let Some(project) = project.upgrade() { Some( project .read(cx) @@ -679,7 +657,7 @@ impl AssistantPanel { let user_prompt = user_prompt.to_string(); let snippets = if retrieve_context { - let Some(project) = project.upgrade(cx) else { + let Some(project) = project.upgrade() else { return; }; @@ -688,31 +666,35 @@ impl AssistantPanel { this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) }); - cx.background() + cx.background_executor() .spawn(async move { search_results.await.unwrap_or_default() }) } else { Task::ready(Vec::new()) }; - let snippets = cx.spawn(|_, cx| async move { + let snippets = cx.spawn(|_, mut cx| async move { let mut snippets = Vec::new(); for result in search_results.await { - snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &cx)); + snippets.push(PromptCodeSnippet::new( + result.buffer, + result.range, + &mut cx, + )?); } - snippets + anyhow::Ok(snippets) }); snippets } else { - Task::ready(Vec::new()) + Task::ready(Ok(Vec::new())) }; - let mut model = settings::get::(cx) + let mut model = AssistantSettings::get_global(cx) .default_open_ai_model .clone(); let model_name = model.full_name(); - let prompt = cx.background().spawn(async move { - let snippets = snippets.await; + let prompt = cx.background_executor().spawn(async move { + let snippets = snippets.await?; let language_name = language_name.as_deref(); generate_content_prompt( @@ -755,17 +737,13 @@ impl AssistantPanel { temperature, }); - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; anyhow::Ok(()) }) .detach(); } - fn update_highlights_for_editor( - &self, - editor: &ViewHandle, - cx: &mut ViewContext, - ) { + fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { let mut background_ranges = Vec::new(); let mut foreground_ranges = Vec::new(); let empty_inline_assist_ids = Vec::new(); @@ -791,7 +769,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| theme.assistant.inline.pending_edit_background, + |theme| theme.editor_active_line_background, // todo!("use the appropriate color") cx, ); } @@ -811,8 +789,8 @@ impl AssistantPanel { }); } - fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { - let editor = cx.add_view(|cx| { + fn new_conversation(&mut self, cx: &mut ViewContext) -> View { + let editor = cx.new_view(|cx| { ConversationEditor::new( self.completion_provider.clone(), self.languages.clone(), @@ -825,11 +803,7 @@ impl AssistantPanel { editor } - fn add_conversation( - &mut self, - editor: ViewHandle, - cx: &mut ViewContext, - ) { + fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { self.subscriptions .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); @@ -850,8 +824,8 @@ impl AssistantPanel { self.toolbar.update(cx, |toolbar, cx| { toolbar.set_active_item(Some(&editor), cx); }); - if self.has_focus(cx) { - cx.focus(&editor); + if self.focus_handle.contains_focused(cx) { + cx.focus_view(&editor); } } else { self.toolbar.update(cx, |toolbar, cx| { @@ -864,7 +838,7 @@ impl AssistantPanel { fn handle_conversation_editor_event( &mut self, - _: ViewHandle, + _: View, event: &ConversationEditorEvent, cx: &mut ViewContext, ) { @@ -887,45 +861,46 @@ impl AssistantPanel { self.completion_provider.save_credentials(cx, credential); self.api_key_editor.take(); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } } else { - cx.propagate_action(); + cx.propagate(); } } fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { self.completion_provider.delete_credentials(cx); self.api_key_editor = Some(build_api_key_editor(cx)); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { if self.zoomed { - cx.emit(AssistantPanelEvent::ZoomOut) + cx.emit(PanelEvent::ZoomOut) } else { - cx.emit(AssistantPanelEvent::ZoomIn) + cx.emit(PanelEvent::ZoomIn) } } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; + let mut propagate = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { if search_bar.show(cx) { search_bar.search_suggested(cx); if action.focus { + let focus_handle = search_bar.focus_handle(cx); search_bar.select_query(cx); - cx.focus_self(); + cx.focus(&focus_handle); } - propagate_action = false + propagate = false } }); } - if propagate_action { - cx.propagate_action(); + if propagate { + cx.propagate(); } } @@ -938,7 +913,7 @@ impl AssistantPanel { return; } } - cx.propagate_action(); + cx.propagate(); } fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { @@ -953,201 +928,125 @@ impl AssistantPanel { } } - fn active_editor(&self) -> Option<&ViewHandle> { + fn active_editor(&self) -> Option<&View> { self.editors.get(self.active_editor_index?) } - fn render_hamburger_button(cx: &mut ViewContext) -> impl Element { - enum History {} - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.hamburger_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if this.active_editor().is_some() { - this.set_active_editor_index(None, cx); - } else { - this.set_active_editor_index(this.prev_active_editor_index, cx); - } - }) - .with_tooltip::(1, "History", None, tooltip_style, cx) + fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("hamburger_button", Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if this.active_editor().is_some() { + this.set_active_editor_index(None, cx); + } else { + this.set_active_editor_index(this.prev_active_editor_index, cx); + } + })) + .tooltip(|cx| Tooltip::text("Conversation History", cx)) } - fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec> { + fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { if self.active_editor().is_some() { vec![ - Self::render_split_button(cx).into_any(), - Self::render_quote_button(cx).into_any(), - Self::render_assist_button(cx).into_any(), + Self::render_split_button(cx).into_any_element(), + Self::render_quote_button(cx).into_any_element(), + Self::render_assist_button(cx).into_any_element(), ] } else { Default::default() } } - fn render_split_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.split_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); - } - }) - .with_tooltip::( - 1, - "Split Message", - Some(Box::new(Split)), - tooltip_style, - cx, - ) + fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("split_button", Icon::Snip) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) } - fn render_assist_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.assist_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); - } - }) - .with_tooltip::(1, "Assist", Some(Box::new(Assist)), tooltip_style, cx) + fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("assist_button", Icon::MagicWand) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) } - fn render_quote_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.quote_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - ConversationEditor::quote_selection(workspace, &Default::default(), cx) + fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("quote_button", Icon::Quote) + .on_click(cx.listener(|this, _event, cx| { + if let Some(workspace) = this.workspace.upgrade() { + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + ConversationEditor::quote_selection(workspace, &Default::default(), cx) + }); }); - }); - } - }) - .with_tooltip::( - 1, - "Quote Selection", - Some(Box::new(QuoteSelection)), - tooltip_style, - cx, - ) + } + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("Quote Selection", &QuoteSelection, cx)) } - fn render_plus_button(cx: &mut ViewContext) -> impl Element { - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.assistant.plus_button.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this: &mut Self, cx| { - this.new_conversation(cx); - }) - .with_tooltip::( - 1, - "New Conversation", - Some(Box::new(NewConversation)), - tooltip_style, - cx, - ) + fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("plus_button", Icon::Plus) + .on_click(cx.listener(|this, _event, cx| { + this.new_conversation(cx); + })) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) } - fn render_zoom_button(&self, cx: &mut ViewContext) -> impl Element { - enum ToggleZoomButton {} - - let theme = theme::current(cx); - let tooltip_style = theme::current(cx).tooltip.clone(); - let style = if self.zoomed { - &theme.assistant.zoom_out_button - } else { - &theme.assistant.zoom_in_button - }; - - MouseEventHandler::new::(0, cx, |state, _| { - let style = style.style_for(state); - Svg::for_style(style.icon.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.toggle_zoom(&ToggleZoom, cx); - }) - .with_tooltip::( - 0, - if self.zoomed { "Zoom Out" } else { "Zoom In" }, - Some(Box::new(ToggleZoom)), - tooltip_style, - cx, - ) + fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let zoomed = self.zoomed; + IconButton::new("zoom_button", Icon::Maximize) + .on_click(cx.listener(|this, _event, cx| { + this.toggle_zoom(&ToggleZoom, cx); + })) + .selected(zoomed) + .selected_icon(Icon::Minimize) + .icon_size(IconSize::Small) + .tooltip(move |cx| { + Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) + }) } fn render_saved_conversation( &mut self, index: usize, cx: &mut ViewContext, - ) -> impl Element { + ) -> impl IntoElement { let conversation = &self.saved_conversations[index]; let path = conversation.path.clone(); - MouseEventHandler::new::(index, cx, move |state, cx| { - let style = &theme::current(cx).assistant.saved_conversation; - Flex::row() - .with_child( - Label::new( - conversation.mtime.format("%F %I:%M%p").to_string(), - style.saved_at.text.clone(), + + ButtonLike::new(index) + .on_click(cx.listener(move |this, _, cx| { + this.open_conversation(path.clone(), cx) + .detach_and_log_err(cx) + })) + .full_width() + .child( + div() + .flex() + .w_full() + .gap_2() + .child( + Label::new(conversation.mtime.format("%F %I:%M%p").to_string()) + .color(Color::Muted) + .size(LabelSize::Small), ) - .aligned() - .contained() - .with_style(style.saved_at.container), - ) - .with_child( - Label::new(conversation.title.clone(), style.title.text.clone()) - .aligned() - .contained() - .with_style(style.title.container), - ) - .contained() - .with_style(*style.container.style_for(state)) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.open_conversation(path.clone(), cx) - .detach_and_log_err(cx) - }) + .child(Label::new(conversation.title.clone()).size(LabelSize::Small)), + ) } fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { + cx.focus(&self.focus_handle); + if let Some(ix) = self.editor_index_for_path(&path, cx) { self.set_active_editor_index(Some(ix), cx); return Task::ready(Ok(())); @@ -1159,16 +1058,16 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let saved_conversation = fs.load(&path).await?; let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.add_model(|cx| { + let conversation = cx.new_model(|cx| { Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - }); + })?; this.update(&mut cx, |this, cx| { // If, by the time we've loaded the conversation, the user has already opened // the same conversation, we don't want to open it again. if let Some(ix) = this.editor_index_for_path(&path, cx) { this.set_active_editor_index(Some(ix), cx); } else { - let editor = cx.add_view(|cx| { + let editor = cx.new_view(|cx| { ConversationEditor::for_conversation(conversation, fs, workspace, cx) }); this.add_conversation(editor, cx); @@ -1193,168 +1092,133 @@ impl AssistantPanel { } } -fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { - cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())), - cx, - ); +fn build_api_key_editor(cx: &mut ViewContext) -> View { + cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); editor }) } -impl Entity for AssistantPanel { - type Event = AssistantPanelEvent; -} - -impl View for AssistantPanel { - fn ui_name() -> &'static str { - "AssistantPanel" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx); - let style = &theme.assistant; - if let Some(api_key_editor) = self.api_key_editor.as_ref() { - Flex::column() - .with_child( - Text::new( - "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " - Having a subscription for another service like GitHub Copilot won't work.", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " - You can create a api key at: platform.openai.com/api-keys", - style.api_key_prompt.text.clone(), - ), - ) - .with_child( - Text::new( - " ", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - Text::new( - "Paste your OpenAI API key and press Enter to use the assistant", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - ChildView::new(api_key_editor, cx) - .contained() - .with_style(style.api_key_editor.container) - .aligned(), - ) - .with_child( - Text::new( - " ", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .with_child( - Text::new( - "Click on the Z button in the status bar to close this panel.", - style.api_key_prompt.text.clone(), - ) - .aligned(), - ) - .contained() - .with_style(style.api_key_prompt.container) - .aligned() - .into_any() +impl Render for AssistantPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + if let Some(api_key_editor) = self.api_key_editor.clone() { + v_stack() + .on_action(cx.listener(AssistantPanel::save_credentials)) + .track_focus(&self.focus_handle) + .child(Label::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + )) + .child(Label::new( + " - Having a subscription for another service like GitHub Copilot won't work." + )) + .child(Label::new( + " - You can create a api key at: platform.openai.com/api-keys" + )) + .child(Label::new( + " " + )) + .child(Label::new( + "Paste your OpenAI API key and press Enter to use the assistant" + )) + .child(api_key_editor) + .child(Label::new( + "Click on the Z button in the status bar to close this panel." + )) + .border() + .border_color(gpui::red()) } else { - let title = self.active_editor().map(|editor| { - Label::new(editor.read(cx).title(cx), style.title.text.clone()) - .contained() - .with_style(style.title.container) - .aligned() - .left() - .flex(1., false) - }); - let mut header = Flex::row() - .with_child(Self::render_hamburger_button(cx).aligned()) - .with_children(title); - if self.has_focus { - header.add_children( - self.render_editor_tools(cx) - .into_iter() - .map(|tool| tool.aligned().flex_float()), - ); - header.add_child(Self::render_plus_button(cx).aligned().flex_float()); - header.add_child(self.render_zoom_button(cx).aligned()); - } - - Flex::column() - .with_child( - header - .contained() - .with_style(theme.workspace.tab_bar.container) - .expanded() - .constrained() - .with_height(theme.workspace.tab_bar.height), + let header = TabBar::new("assistant_header") + .start_child( + h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), ) - .with_children(if self.toolbar.read(cx).hidden() { + .children(self.active_editor().map(|editor| { + h_stack() + .h(rems(Tab::HEIGHT_IN_REMS)) + .flex_1() + .px_2() + .child(Label::new(editor.read(cx).title(cx)).into_element()) + })) + .end_child(if self.focus_handle.contains_focused(cx) { + h_stack() + .gap_2() + .child(h_stack().gap_1().children(self.render_editor_tools(cx))) + .child( + ui::Divider::vertical() + .inset() + .color(ui::DividerColor::Border), + ) + .child( + h_stack() + .gap_1() + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)), + ) + } else { + div() + }); + + v_stack() + .size_full() + .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { + this.new_conversation(cx); + })) + .on_action(cx.listener(AssistantPanel::reset_credentials)) + .on_action(cx.listener(AssistantPanel::toggle_zoom)) + .on_action(cx.listener(AssistantPanel::deploy)) + .on_action(cx.listener(AssistantPanel::select_next_match)) + .on_action(cx.listener(AssistantPanel::select_prev_match)) + .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) + .track_focus(&self.focus_handle) + .child(header) + .children(if self.toolbar.read(cx).hidden() { None } else { - Some(ChildView::new(&self.toolbar, cx).expanded()) + Some(self.toolbar.clone()) }) - .with_child(if let Some(editor) = self.active_editor() { - ChildView::new(editor, cx).flex(1., true).into_any() - } else { - UniformList::new( - self.saved_conversations_list_state.clone(), - self.saved_conversations.len(), - cx, - |this, range, items, cx| { - for ix in range { - items.push(this.render_saved_conversation(ix, cx).into_any()); - } - }, - ) - .flex(1., true) - .into_any() - }) - .into_any() + .child( + div() + .flex_1() + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + let view = cx.view().clone(); + let scroll_handle = self.saved_conversations_scroll_handle.clone(); + let conversation_count = self.saved_conversations.len(); + canvas(move |bounds, cx| { + uniform_list( + view, + "saved_conversations", + conversation_count, + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx)) + .collect() + }, + ) + .track_scroll(scroll_handle) + .into_any_element() + .draw( + bounds.origin, + bounds.size.map(AvailableSpace::Definite), + cx, + ); + }) + .size_full() + .into_any_element() + }), + ) } } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); - cx.notify(); - if cx.is_self_focused() { - if let Some(editor) = self.active_editor() { - cx.focus(editor); - } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { - cx.focus(api_key_editor); - } - } - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = false; - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); - cx.notify(); - } } impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel" + } + fn position(&self, cx: &WindowContext) -> DockPosition { - match settings::get::(cx).dock { + match AssistantSettings::get_global(cx).dock { AssistantDockPosition::Left => DockPosition::Left, AssistantDockPosition::Bottom => DockPosition::Bottom, AssistantDockPosition::Right => DockPosition::Right, @@ -1376,8 +1240,8 @@ impl Panel for AssistantPanel { }); } - fn size(&self, cx: &WindowContext) -> f32 { - let settings = settings::get::(cx); + fn size(&self, cx: &WindowContext) -> Pixels { + let settings = AssistantSettings::get_global(cx); match self.position(cx) { DockPosition::Left | DockPosition::Right => { self.width.unwrap_or_else(|| settings.default_width) @@ -1386,7 +1250,7 @@ impl Panel for AssistantPanel { } } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { match self.position(cx) { DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Bottom => self.height = size, @@ -1394,14 +1258,6 @@ impl Panel for AssistantPanel { cx.notify(); } - fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::ZoomIn) - } - - fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::ZoomOut) - } - fn is_zoomed(&self, _: &WindowContext) -> bool { self.zoomed } @@ -1421,34 +1277,24 @@ impl Panel for AssistantPanel { } } - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { - settings::get::(cx) - .button - .then(|| "icons/ai.svg") + fn icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::Ai) } - fn icon_tooltip(&self) -> (String, Option>) { - ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Assistant Panel") } - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, AssistantPanelEvent::DockPositionChanged) + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) } +} - fn should_activate_on_event(_: &Self::Event) -> bool { - false - } +impl EventEmitter for AssistantPanel {} - fn should_close_on_event(event: &AssistantPanelEvent) -> bool { - matches!(event, AssistantPanelEvent::Close) - } - - fn has_focus(&self, _: &WindowContext) -> bool { - self.has_focus - } - - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, AssistantPanelEvent::Focus) +impl FocusableView for AssistantPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } @@ -1466,7 +1312,7 @@ struct Summary { struct Conversation { id: Option, - buffer: ModelHandle, + buffer: Model, message_anchors: Vec, messages_metadata: HashMap, next_message_id: MessageId, @@ -1484,9 +1330,7 @@ struct Conversation { completion_provider: Arc, } -impl Entity for Conversation { - type Event = ConversationEvent; -} +impl EventEmitter for Conversation {} impl Conversation { fn new( @@ -1495,24 +1339,21 @@ impl Conversation { completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); buffer }); - let settings = settings::get::(cx); + let settings = AssistantSettings::get_global(cx); let model = settings.default_open_ai_model.clone(); let mut this = Self { @@ -1587,14 +1428,14 @@ impl Conversation { }; let model = saved_conversation.model; let completion_provider: Arc = Arc::new( - OpenAICompletionProvider::new(model.full_name(), cx.background().clone()), + OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), ); completion_provider.retrieve_credentials(cx); let markdown = language_registry.language_for_name("Markdown"); let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -1603,14 +1444,11 @@ impl Conversation { next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); } buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1645,7 +1483,7 @@ impl Conversation { fn handle_buffer_event( &mut self, - _: ModelHandle, + _: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -1681,24 +1519,24 @@ impl Conversation { }) .collect::>(); let model = self.model.clone(); - self.pending_token_count = cx.spawn_weak(|this, mut cx| { + self.pending_token_count = cx.spawn(|this, mut cx| { async move { - cx.background().timer(Duration::from_millis(200)).await; + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; let token_count = cx - .background() + .background_executor() .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) }) .await?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - }); + this.update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + })?; anyhow::Ok(()) } .log_err() @@ -1785,7 +1623,7 @@ impl Conversation { .unwrap(); user_messages.push(user_message); - let task = cx.spawn_weak({ + let task = cx.spawn({ |this, mut cx| async move { let assistant_message_id = assistant_message.id; let stream_completion = async { @@ -1794,59 +1632,56 @@ impl Conversation { while let Some(message) = messages.next().await { let text = message?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors + this.update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + })?; smol::future::yield_now().await; } - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); + this.update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + })?; anyhow::Ok(()) }; let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = - MessageStatus::Error(error.to_string().trim().into()); - } + + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = MessageStatus::Error(SharedString::from( + error.to_string().trim().to_string(), + )); } - cx.notify(); } - }); - } + cx.notify(); + } + }) + .ok(); } }); @@ -2069,7 +1904,7 @@ impl Conversation { .text .push_str(&text); cx.emit(ConversationEvent::SummaryChanged); - }); + })?; } this.update(&mut cx, |this, cx| { @@ -2077,7 +1912,7 @@ impl Conversation { summary.done = true; cx.emit(ConversationEvent::SummaryChanged); } - }); + })?; anyhow::Ok(()) } @@ -2166,7 +2001,7 @@ impl Conversation { ) { self.pending_save = cx.spawn(|this, mut cx| async move { if let Some(debounce) = debounce { - cx.background().timer(debounce).await; + cx.background_executor().timer(debounce).await; } let (old_path, summary) = this.read_with(&cx, |this, _| { @@ -2181,10 +2016,10 @@ impl Conversation { None }; (path, summary) - }); + })?; if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; let path = if let Some(old_path) = old_path { old_path } else { @@ -2208,7 +2043,7 @@ impl Conversation { fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) .await?; - this.update(&mut cx, |this, _| this.path = Some(path)); + this.update(&mut cx, |this, _| this.path = Some(path))?; } Ok(()) @@ -2227,15 +2062,15 @@ enum ConversationEditorEvent { #[derive(Copy, Clone, Debug, PartialEq)] struct ScrollPosition { - offset_before_cursor: Vector2F, + offset_before_cursor: gpui::Point, cursor: Anchor, } struct ConversationEditor { - conversation: ModelHandle, + conversation: Model, fs: Arc, - workspace: WeakViewHandle, - editor: ViewHandle, + workspace: WeakView, + editor: View, blocks: HashSet, scroll_position: Option, _subscriptions: Vec, @@ -2246,21 +2081,21 @@ impl ConversationEditor { completion_provider: Arc, language_registry: Arc, fs: Arc, - workspace: WeakViewHandle, + workspace: WeakView, cx: &mut ViewContext, ) -> Self { let conversation = - cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + cx.new_model(|cx| Conversation::new(language_registry, cx, completion_provider)); Self::for_conversation(conversation, fs, workspace, cx) } fn for_conversation( - conversation: ModelHandle, + conversation: Model, fs: Arc, - workspace: WeakViewHandle, + workspace: WeakView, cx: &mut ViewContext, ) -> Self { - let editor = cx.add_view(|cx| { + let editor = cx.new_view(|cx| { let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); @@ -2332,7 +2167,7 @@ impl ConversationEditor { .conversation .update(cx, |conversation, _| conversation.cancel_last_assist()) { - cx.propagate_action(); + cx.propagate(); } } @@ -2358,7 +2193,7 @@ impl ConversationEditor { fn handle_conversation_event( &mut self, - _: ModelHandle, + _: Model, event: &ConversationEvent, cx: &mut ViewContext, ) { @@ -2381,9 +2216,9 @@ impl ConversationEditor { let snapshot = editor.snapshot(cx); let cursor_point = scroll_position.cursor.to_display_point(&snapshot); let scroll_top = - cursor_point.row() as f32 - scroll_position.offset_before_cursor.y(); + cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; editor.set_scroll_position( - vec2f(scroll_position.offset_before_cursor.x(), scroll_top), + point(scroll_position.offset_before_cursor.x, scroll_top), cx, ); } @@ -2394,12 +2229,12 @@ impl ConversationEditor { fn handle_editor_event( &mut self, - _: ViewHandle, - event: &editor::Event, + _: View, + event: &EditorEvent, cx: &mut ViewContext, ) { match event { - editor::Event::ScrollPositionChanged { autoscroll, .. } => { + EditorEvent::ScrollPositionChanged { autoscroll, .. } => { let cursor_scroll_position = self.cursor_scroll_position(cx); if *autoscroll { self.scroll_position = cursor_scroll_position; @@ -2407,7 +2242,7 @@ impl ConversationEditor { self.scroll_position = None; } } - editor::Event::SelectionsChanged { .. } => { + EditorEvent::SelectionsChanged { .. } => { self.scroll_position = self.cursor_scroll_position(cx); } _ => {} @@ -2424,14 +2259,11 @@ impl ConversationEditor { .anchor() .scroll_position(&snapshot.display_snapshot); - let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y..scroll_bottom).contains(&cursor_row) { Some(ScrollPosition { cursor, - offset_before_cursor: vec2f( - scroll_position.x(), - cursor_row - scroll_position.y(), - ), + offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), }) } else { None @@ -2454,90 +2286,67 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - // let metadata = message.metadata.clone(); - // let message = message.clone(); - move |cx| { - enum Sender {} - enum ErrorTooltip {} - - let theme = theme::current(cx); - let style = &theme.assistant; + move |_cx| { let message_id = message.id; - let sender = MouseEventHandler::new::( - message_id.0, - cx, - |state, _| match message.role { - Role::User => { - let style = style.user_sender.style_for(state); - Label::new("You", style.text.clone()) - .contained() - .with_style(style.container) - } + let sender = ButtonLike::new("role") + .child(match message.role { + Role::User => Label::new("You").color(Color::Default), Role::Assistant => { - let style = style.assistant_sender.style_for(state); - Label::new("Assistant", style.text.clone()) - .contained() - .with_style(style.container) + Label::new("Assistant").color(Color::Modified) } - Role::System => { - let style = style.system_sender.style_for(state); - Label::new("System", style.text.clone()) - .contained() - .with_style(style.container) - } - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, { - let conversation = conversation.clone(); - move |_, _, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); - - Flex::row() - .with_child(sender.aligned()) - .with_child( - Label::new( - message.sent_at.format("%I:%M%P").to_string(), - style.sent_at.text.clone(), + Role::System => Label::new("System").color(Color::Warning), + }) + .tooltip(|cx| { + Tooltip::with_meta( + "Toggle message role", + None, + "Available roles: You (User), Assistant, System", + cx, ) - .contained() - .with_style(style.sent_at.container) - .aligned(), + }) + .on_click({ + let conversation = conversation.clone(); + move |_, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) + } + }); + + h_stack() + .id(("message_header", message_id.0)) + .h_11() + .gap_1() + .p_1() + .child(sender) + // TODO: Only show this if the message if the message has been sent + .child( + Label::new( + FormatDistance::from_now(DateTimeType::Local( + message.sent_at, + )) + .hide_prefix(true) + .add_suffix(true) + .to_string(), + ) + .color(Color::Muted), ) - .with_children( - if let MessageStatus::Error(error) = &message.status { + .children( + if let MessageStatus::Error(error) = message.status.clone() { Some( - Svg::new("icons/error.svg") - .with_color(style.error_icon.color) - .constrained() - .with_width(style.error_icon.width) - .contained() - .with_style(style.error_icon.container) - .with_tooltip::( - message_id.0, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), + div() + .id("error") + .tooltip(move |cx| Tooltip::text(error.clone(), cx)) + .child(IconElement::new(Icon::XCircle)), ) } else { None }, ) - .aligned() - .left() - .contained() - .with_style(style.message_header) - .into_any() + .into_any_element() } }), disposition: BlockDisposition::Above, @@ -2565,36 +2374,35 @@ impl ConversationEditor { return; }; - let text = editor.read_with(cx, |editor, cx| { - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + let editor = editor.read(cx); + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - let selected_text = buffer.text_for_range(range).collect::(); - if selected_text.is_empty() { - None + let selected_text = buffer.text_for_range(range).collect::(); + let text = if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - } - }); + format!("```{language_name}\n{selected_text}\n```") + }) + }; // Activate the panel - if !panel.read(cx).has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { workspace.toggle_panel_focus::(cx); } @@ -2638,13 +2446,12 @@ impl ConversationEditor { } if spanned_messages > 1 { - cx.platform() - .write_to_clipboard(ClipboardItem::new(copied_text)); + cx.write_to_clipboard(ClipboardItem::new(copied_text)); return; } } - cx.propagate_action(); + cx.propagate(); } fn split(&mut self, _: &Split, cx: &mut ViewContext) { @@ -2682,80 +2489,65 @@ impl ConversationEditor { .unwrap_or_else(|| "New Conversation".into()) } - fn render_current_model( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> impl Element { - enum Model {} - - MouseEventHandler::new::(0, cx, |state, cx| { - let style = style.model.style_for(state); - let model_display_name = self.conversation.read(cx).model.short_name(); - Label::new(model_display_name, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) - } - - fn render_remaining_tokens( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { - let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_style = if remaining_tokens <= 0 { - &style.no_remaining_tokens - } else if remaining_tokens <= 500 { - &style.low_remaining_tokens - } else { - &style.remaining_tokens - }; - Some( - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container), + fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { + Button::new( + "current_model", + self.conversation.read(cx).model.short_name(), ) + .style(ButtonStyle::Filled) + .tooltip(move |cx| Tooltip::text("Change Model", cx)) + .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) + } + + fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_color = if remaining_tokens <= 0 { + Color::Error + } else if remaining_tokens <= 500 { + Color::Warning + } else { + Color::Default + }; + Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)) } } -impl Entity for ConversationEditor { - type Event = ConversationEditorEvent; +impl EventEmitter for ConversationEditor {} + +impl Render for ConversationEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + div() + .key_context("ConversationEditor") + .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) + .capture_action(cx.listener(ConversationEditor::save)) + .capture_action(cx.listener(ConversationEditor::copy)) + .capture_action(cx.listener(ConversationEditor::cycle_message_role)) + .on_action(cx.listener(ConversationEditor::assist)) + .on_action(cx.listener(ConversationEditor::split)) + .size_full() + .relative() + .child( + div() + .size_full() + .pl_2() + .bg(cx.theme().colors().editor_background) + .child(self.editor.clone()), + ) + .child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) + } } -impl View for ConversationEditor { - fn ui_name() -> &'static str { - "ConversationEditor" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).assistant; - Stack::new() - .with_child( - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container), - ) - .with_child( - Flex::row() - .with_child(self.render_current_model(theme, cx)) - .with_children(self.render_remaining_tokens(theme, cx)) - .aligned() - .top() - .right(), - ) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } +impl FocusableView for ConversationEditor { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) } } @@ -2806,142 +2598,104 @@ enum InlineAssistantEvent { struct InlineAssistant { id: usize, - prompt_editor: ViewHandle, - workspace: WeakViewHandle, + prompt_editor: View, + workspace: WeakView, confirmed: bool, - has_focus: bool, include_conversation: bool, measurements: Rc>, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, - codegen: ModelHandle, + codegen: Model, _subscriptions: Vec, retrieve_context: bool, - semantic_index: Option>, + semantic_index: Option>, semantic_permissioned: Option, - project: WeakModelHandle, + project: WeakModel, maintain_rate_limit: Option>, } -impl Entity for InlineAssistant { - type Event = InlineAssistantEvent; -} +impl EventEmitter for InlineAssistant {} -impl View for InlineAssistant { - fn ui_name() -> &'static str { - "InlineAssistant" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum ErrorIcon {} - let theme = theme::current(cx); - - Flex::row() - .with_children([Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() - .aligned(), - ) - .with_children(if SemanticIndex::enabled(cx) { - Some( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), +impl Render for InlineAssistant { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let measurements = self.measurements.get(); + h_stack() + .w_full() + .py_2() + .border_y_1() + .border_color(cx.theme().colors().border) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::toggle_include_conversation)) + .on_action(cx.listener(Self::toggle_retrieve_context)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) + .child( + h_stack() + .justify_center() + .w(measurements.gutter_width) + .child( + IconButton::new("include_conversation", Icon::Ai) + .on_click(cx.listener(|this, _, cx| { + this.toggle_include_conversation(&ToggleIncludeConversation, cx) + })) + .selected(self.include_conversation) + .tooltip(|cx| { + Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, + cx, + ) + }), ) - } else { - None - }) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), - ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } - } - })]) - .with_child(Empty::new().constrained().dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.min.y(), - ), - max: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.max.y(), - ), - } - } - })) - .with_child( - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .flex(1., true), + .children(if SemanticIndex::enabled(cx) { + Some( + IconButton::new("retrieve_context", Icon::MagnifyingGlass) + .on_click(cx.listener(|this, _, cx| { + this.toggle_retrieve_context(&ToggleRetrieveContext, cx) + })) + .selected(self.retrieve_context) + .tooltip(|cx| { + Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + ) + }), + ) + } else { + None + }) + .children(if let Some(error) = self.codegen.read(cx).error() { + let error_message = SharedString::from(error.to_string()); + Some( + div() + .id("error") + .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) + .child(IconElement::new(Icon::XCircle).color(Color::Error)), + ) + } else { + None + }), ) - .with_children(if self.retrieve_context { - Some( - Flex::row() - .with_children(self.retrieve_context_status(cx)) - .flex(1., true) - .aligned(), - ) + .child( + h_stack() + .w_full() + .ml(measurements.anchor_x - measurements.gutter_width) + .child(self.render_prompt_editor(cx)), + ) + .children(if self.retrieve_context { + self.retrieve_context_status(cx) } else { None }) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() - .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 FocusableView for InlineAssistant { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.prompt_editor.focus_handle(cx) } } @@ -2951,18 +2705,15 @@ impl InlineAssistant { measurements: Rc>, include_conversation: bool, prompt_history: VecDeque, - codegen: ModelHandle, - workspace: WeakViewHandle, + codegen: Model, + workspace: WeakView, cx: &mut ViewContext, retrieve_context: bool, - semantic_index: Option>, - project: ModelHandle, + semantic_index: Option>, + project: Model, ) -> Self { - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); + let prompt_editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); let placeholder = match codegen.read(cx).kind() { CodegenKind::Transform { .. } => "Enter transformation prompt…", CodegenKind::Generate { .. } => "Enter generation prompt…", @@ -2970,6 +2721,8 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); + cx.focus_view(&prompt_editor); + let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), @@ -2984,7 +2737,6 @@ impl InlineAssistant { prompt_editor, workspace, confirmed: false, - has_focus: false, include_conversation, measurements, prompt_history, @@ -3009,7 +2761,7 @@ impl InlineAssistant { return Task::ready(Ok(value)); } - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return Task::ready(Err(anyhow!("project was dropped"))); }; @@ -3023,11 +2775,11 @@ impl InlineAssistant { fn handle_prompt_editor_events( &mut self, - _: ViewHandle, - event: &editor::Event, + _: View, + event: &EditorEvent, cx: &mut ViewContext, ) { - if let editor::Event::Edited = event { + if let EditorEvent::Edited = event { self.pending_prompt = self.prompt_editor.read(cx).text(cx); cx.notify(); } @@ -3035,10 +2787,10 @@ impl InlineAssistant { fn semantic_index_changed( &mut self, - semantic_index: ModelHandle, + semantic_index: Model, cx: &mut ViewContext, ) { - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return; }; @@ -3051,7 +2803,7 @@ impl InlineAssistant { if self.maintain_rate_limit.is_none() { self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { loop { - cx.background().timer(Duration::from_secs(1)).await; + cx.background_executor().timer(Duration::from_secs(1)).await; this.update(&mut cx, |_, cx| cx.notify()).log_err(); } })); @@ -3064,26 +2816,16 @@ impl InlineAssistant { } } - fn handle_codegen_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); - self.prompt_editor.update(cx, |editor, cx| { + self.prompt_editor.update(cx, |editor, _cx| { let was_read_only = editor.read_only(); if was_read_only != is_read_only { if is_read_only { editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); } else { self.confirmed = false; editor.set_read_only(false); - editor.set_field_editor_style( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); } } }); @@ -3101,15 +2843,8 @@ impl InlineAssistant { report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor.update(cx, |editor, cx| { - editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); - }); + self.prompt_editor + .update(cx, |editor, _cx| editor.set_read_only(true)); cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, @@ -3123,7 +2858,7 @@ impl InlineAssistant { fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { let semantic_permissioned = self.semantic_permissioned(cx); - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return; }; @@ -3141,7 +2876,7 @@ impl InlineAssistant { cx.spawn(|this, mut cx| async move { // If Necessary prompt user if !semantic_permissioned.await.unwrap_or(false) { - let mut answer = this.update(&mut cx, |_, cx| { + let answer = this.update(&mut cx, |_, cx| { cx.prompt( PromptLevel::Info, prompt_text.as_str(), @@ -3149,7 +2884,7 @@ impl InlineAssistant { ) })?; - if answer.next().await == Some(0) { + if answer.await? == 0 { this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; @@ -3179,7 +2914,7 @@ impl InlineAssistant { } fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { - let Some(project) = self.project.upgrade(cx) else { + let Some(project) = self.project.upgrade() else { return Err(anyhow!("project was dropped!")); }; @@ -3189,7 +2924,7 @@ impl InlineAssistant { // This has to be updated to accomodate for semantic_permissions if semantic_permissioned.await.unwrap_or(false) { semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx)) + .update(&mut cx, |index, cx| index.index_project(project, cx))? .await } else { Err(anyhow!("project is not permissioned for semantic indexing")) @@ -3201,153 +2936,72 @@ impl InlineAssistant { anyhow::Ok(()) } - fn retrieve_context_status( - &self, - cx: &mut ViewContext, - ) -> Option> { - enum ContextStatusIcon {} - - let Some(project) = self.project.upgrade(cx) else { + fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { + let Some(project) = self.project.upgrade() else { return None; }; - if let Some(semantic_index) = SemanticIndex::global(cx) { - let status = semantic_index.update(cx, |index, _| index.status(&project)); - let theme = theme::current(cx); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), - SemanticIndexStatus::NotIndexed {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.inline.context_status.error_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.error_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.error_icon.container) - .with_tooltip::( - self.id, - "Not Indexed", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { + let semantic_index = SemanticIndex::global(cx)?; + let status = semantic_index.update(cx, |index, _| index.status(&project)); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; + SemanticIndexStatus::NotIndexed {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); } - Some( - Svg::new("icons/update.svg") - .with_color(theme.assistant.inline.context_status.in_progress_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.in_progress_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.in_progress_icon.container) - .with_tooltip::( - self.id, - status_text, - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ) } - SemanticIndexStatus::Indexed {} => Some( - Svg::new("icons/check.svg") - .with_color(theme.assistant.inline.context_status.complete_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.complete_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.complete_icon.container) - .with_tooltip::( - self.id, - "Index up to date", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), - ), + + let status_text = SharedString::from(status_text); + Some( + div() + .id("update") + .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) + .child(IconElement::new(Icon::Update).color(Color::Info)) + .into_any_element() + ) } - } else { - None + + SemanticIndexStatus::Indexed {} => Some( + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(IconElement::new(Icon::Check).color(Color::Success)) + .into_any_element() + ), } } - // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { - // let project = self.project.clone(); - // if let Some(semantic_index) = self.semantic_index.clone() { - // let status = semantic_index.update(cx, |index, cx| index.status(&project)); - // return match status { - // // This theoretically shouldnt be a valid code path - // // As the inline assistant cant be launched without an API key - // // We keep it here for safety - // semantic_index::SemanticIndexStatus::NotAuthenticated => { - // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexed => { - // "Indexing Complete!".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { - - // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); - - // if let Some(rate_limit_expiry) = rate_limit_expiry { - // let remaining_seconds = - // rate_limit_expiry.duration_since(Instant::now()); - // if remaining_seconds > Duration::from_secs(0) { - // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); - // } - // } - // status - // } - // semantic_index::SemanticIndexStatus::NotIndexed => { - // "Not Indexed for Context Retrieval".to_string() - // } - // }; - // } - - // "".to_string() - // } - fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, @@ -3396,21 +3050,50 @@ impl InlineAssistant { }); }); } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt_editor.read(cx).read_only() { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + EditorElement::new( + &self.prompt_editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } } // This wouldn't need to exist if we could pass parameters when rendering child views. #[derive(Copy, Clone, Default)] struct BlockMeasurements { - anchor_x: f32, - gutter_width: f32, + anchor_x: Pixels, + gutter_width: Pixels, } struct PendingInlineAssist { - editor: WeakViewHandle, - inline_assistant: Option<(BlockId, ViewHandle)>, - codegen: ModelHandle, + editor: WeakView, + inline_assistant: Option<(BlockId, View)>, + codegen: Model, _subscriptions: Vec, - project: WeakModelHandle, + project: WeakModel, } fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { @@ -3444,12 +3127,13 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3574,12 +3258,13 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3672,11 +3357,12 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3740,7 +3426,7 @@ mod tests { ); fn message_ids_for_offsets( - conversation: &ModelHandle, + conversation: &Model, offsets: &[usize], cx: &AppContext, ) -> Vec { @@ -3755,12 +3441,13 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = - cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_0 = conversation.read(cx).message_anchors[0].id; let message_1 = conversation.update(cx, |conversation, cx| { @@ -3793,7 +3480,7 @@ mod tests { ] ); - let deserialized_conversation = cx.add_model(|cx| { + let deserialized_conversation = cx.new_model(|cx| { Conversation::deserialize( conversation.read(cx).serialize(cx), Default::default(), @@ -3814,7 +3501,7 @@ mod tests { } fn messages( - conversation: &ModelHandle, + conversation: &Model, cx: &AppContext, ) -> Vec<(MessageId, Role, Range)> { conversation @@ -3826,23 +3513,23 @@ mod tests { } fn report_assistant_event( - workspace: WeakViewHandle, + workspace: WeakView, conversation_id: Option, assistant_kind: AssistantKind, cx: &AppContext, ) { - let Some(workspace) = workspace.upgrade(cx) else { + let Some(workspace) = workspace.upgrade() else { return; }; let client = workspace.read(cx).project().read(cx).client(); let telemetry = client.telemetry(); - let model = settings::get::(cx) + let model = AssistantSettings::get_global(cx) .default_open_ai_model .clone(); - let telemetry_settings = *settings::get::(cx); + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); telemetry.report_assistant_event( telemetry_settings, diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 65dd588b3c..c0fbc74e9a 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -1,7 +1,8 @@ use anyhow; +use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub enum OpenAIModel { @@ -51,8 +52,8 @@ pub enum AssistantDockPosition { pub struct AssistantSettings { pub button: bool, pub dock: AssistantDockPosition, - pub default_width: f32, - pub default_height: f32, + pub default_width: Pixels, + pub default_height: Pixels, pub default_open_ai_model: OpenAIModel, } @@ -65,7 +66,7 @@ pub struct AssistantSettingsContent { pub default_open_ai_model: Option, } -impl Setting for AssistantSettings { +impl Settings for AssistantSettings { const KEY: Option<&'static str> = Some("assistant"); type FileContent = AssistantSettingsContent; @@ -73,7 +74,7 @@ impl Setting for AssistantSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, + _: &mut gpui::AppContext, ) -> anyhow::Result { Self::load_via_json_merge(default_value, user_values) } diff --git a/crates/assistant/src/codegen.rs b/crates/assistant/src/codegen.rs index 25c9deef7f..8af3010f40 100644 --- a/crates/assistant/src/codegen.rs +++ b/crates/assistant/src/codegen.rs @@ -3,7 +3,7 @@ use ai::completion::{CompletionProvider, CompletionRequest}; use anyhow::Result; use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; -use gpui::{Entity, ModelContext, ModelHandle, Task}; +use gpui::{EventEmitter, Model, ModelContext, Task}; use language::{Rope, TransactionId}; use multi_buffer; use std::{cmp, future, ops::Range, sync::Arc}; @@ -21,7 +21,7 @@ pub enum CodegenKind { pub struct Codegen { provider: Arc, - buffer: ModelHandle, + buffer: Model, snapshot: MultiBufferSnapshot, kind: CodegenKind, last_equal_ranges: Vec>, @@ -32,13 +32,11 @@ pub struct Codegen { _subscription: gpui::Subscription, } -impl Entity for Codegen { - type Event = Event; -} +impl EventEmitter for Codegen {} impl Codegen { pub fn new( - buffer: ModelHandle, + buffer: Model, kind: CodegenKind, provider: Arc, cx: &mut ModelContext, @@ -60,7 +58,7 @@ impl Codegen { fn handle_buffer_event( &mut self, - _buffer: ModelHandle, + _buffer: Model, event: &multi_buffer::Event, cx: &mut ModelContext, ) { @@ -111,13 +109,13 @@ impl Codegen { .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); let response = self.provider.complete(prompt); - self.generation = cx.spawn_weak(|this, mut cx| { + self.generation = cx.spawn(|this, mut cx| { async move { let generate = async { let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { + let diff = cx.background_executor().spawn(async move { let chunks = strip_invalid_spans_from_codeblock(response.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -183,12 +181,6 @@ impl Codegen { }); while let Some(hunks) = hunks_rx.next().await { - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; - this.update(&mut cx, |this, cx| { this.last_equal_ranges.clear(); @@ -245,7 +237,7 @@ impl Codegen { } cx.notify(); - }); + })?; } diff.await?; @@ -253,17 +245,16 @@ impl Codegen { }; let result = generate.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.last_equal_ranges.clear(); - this.idle = true; - if let Err(error) = result { - this.error = Some(error); - } - cx.emit(Event::Finished); - cx.notify(); - }); - } + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }) + .ok(); } }); self.error.take(); @@ -372,7 +363,7 @@ mod tests { use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; - use gpui::{executor::Deterministic, TestAppContext}; + use gpui::{Context, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use rand::prelude::*; @@ -391,12 +382,8 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_transform_autoindent( - cx: &mut TestAppContext, - mut rng: StdRng, - deterministic: Arc, - ) { - cx.set_global(cx.read(SettingsStore::test)); + async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -408,14 +395,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Transform { range }, @@ -442,10 +429,10 @@ mod tests { println!("CHUNK: {:?}", &chunk); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), @@ -464,9 +451,8 @@ mod tests { async fn test_autoindent_when_generating_past_indentation( cx: &mut TestAppContext, mut rng: StdRng, - deterministic: Arc, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -475,14 +461,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -508,10 +494,10 @@ mod tests { let (chunk, suffix) = new_text.split_at(len); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), @@ -530,9 +516,8 @@ mod tests { async fn test_autoindent_when_generating_before_indentation( cx: &mut TestAppContext, mut rng: StdRng, - deterministic: Arc, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = concat!( @@ -541,14 +526,14 @@ mod tests { "}\n" // ); let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.new_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -575,10 +560,10 @@ mod tests { println!("{:?}", &chunk); provider.send_completion(chunk); new_text = suffix; - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); } provider.finish_completion(); - deterministic.run_until_parked(); + cx.background_executor.run_until_parked(); assert_eq!( buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index b678c6fe3b..c88e257295 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -176,7 +176,7 @@ pub(crate) mod tests { use super::*; use std::sync::Arc; - use gpui::AppContext; + use gpui::{AppContext, Context}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use settings::SettingsStore; @@ -227,7 +227,8 @@ pub(crate) mod tests { #[gpui::test] fn test_outline_for_prompt(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); language_settings::init(cx); let text = indoc! {" struct X { @@ -253,7 +254,7 @@ pub(crate) mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); assert_eq!( diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml deleted file mode 100644 index fbe6a41b77..0000000000 --- a/crates/assistant2/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "assistant2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/assistant.rs" -doctest = false - -[dependencies] -ai = { package = "ai2", path = "../ai2" } -client = { package = "client2", path = "../client2" } -collections = { path = "../collections"} -editor = { path = "../editor" } -fs = { package = "fs2", path = "../fs2" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -menu = { package = "menu2", path = "../menu2" } -multi_buffer = { path = "../multi_buffer" } -project = { package = "project2", path = "../project2" } -search = { path = "../search" } -semantic_index = { package = "semantic_index2", path = "../semantic_index2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -ui = { package = "ui2", path = "../ui2" } -util = { path = "../util" } -workspace = { path = "../workspace" } - -uuid.workspace = true -log.workspace = true -anyhow.workspace = true -chrono = { version = "0.4", features = ["serde"] } -futures.workspace = true -indoc.workspace = true -isahc.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -regex.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -smol.workspace = true -tiktoken-rs.workspace = true - -[dev-dependencies] -ai = { package = "ai2", path = "../ai2", features = ["test-support"]} -editor = { path = "../editor", features = ["test-support"] } -project = { package = "project2", path = "../project2", features = ["test-support"] } - -ctor.workspace = true -env_logger.workspace = true -log.workspace = true -rand.workspace = true diff --git a/crates/assistant2/README.zmd b/crates/assistant2/README.zmd deleted file mode 100644 index af8702b609..0000000000 --- a/crates/assistant2/README.zmd +++ /dev/null @@ -1,63 +0,0 @@ -> Hello, I'm Nathan Sobo. -I'm currently your sole creator and the leader of Zed Industries. -Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation. - -< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development. - -As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world. - -> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish. - -We've decided that our next milestone will be open-sourcing Zed on the Zed platform. - -< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further. - -Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation. - -Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future! - -@nathan> #zed, can you describe the conversation UI as you currently understand it. - -< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary. - -Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered. - -@nathan> What details around the conversation view are worth exploring? - -< @nathan, there are several aspects of the conversation view that are worth exploring and refining: - -1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context. -2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history. -3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials. -4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants. -5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view. - -Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed. - -@nathan> @nate, which of these would you like to explore? - -@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5. - -#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots). - -@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try... - -#zed describe a hypothetical conversation UI that meets the description. - -< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements: - -1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space. - -2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions. - -3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view. - -4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly. - -5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow. - -6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view. - -7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context. - -This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs. diff --git a/crates/assistant2/features.zmd b/crates/assistant2/features.zmd deleted file mode 100644 index f3183d68bc..0000000000 --- a/crates/assistant2/features.zmd +++ /dev/null @@ -1,3 +0,0 @@ -Push content to a deeper layer. -A context can have multiple sublayers. -You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document. diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs deleted file mode 100644 index 743c8b22e6..0000000000 --- a/crates/assistant2/src/assistant.rs +++ /dev/null @@ -1,129 +0,0 @@ -pub mod assistant_panel; -mod assistant_settings; -mod codegen; -mod prompts; -mod streaming_diff; - -use ai::providers::open_ai::Role; -use anyhow::Result; -pub use assistant_panel::AssistantPanel; -use assistant_settings::OpenAIModel; -use chrono::{DateTime, Local}; -use collections::HashMap; -use fs::Fs; -use futures::StreamExt; -use gpui::{actions, AppContext, SharedString}; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; -use util::paths::CONVERSATIONS_DIR; - -actions!( - assistant, - [ - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, - ] -); - -#[derive( - Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, -)] -struct MessageId(usize); - -#[derive(Clone, Debug, Serialize, Deserialize)] -struct MessageMetadata { - role: Role, - sent_at: DateTime, - status: MessageStatus, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -enum MessageStatus { - Pending, - Done, - Error(SharedString), -} - -#[derive(Serialize, Deserialize)] -struct SavedMessage { - id: MessageId, - start: usize, -} - -#[derive(Serialize, Deserialize)] -struct SavedConversation { - id: Option, - zed: String, - version: String, - text: String, - messages: Vec, - message_metadata: HashMap, - summary: String, - model: OpenAIModel, -} - -impl SavedConversation { - const VERSION: &'static str = "0.1.0"; -} - -struct SavedConversationMetadata { - title: String, - path: PathBuf, - mtime: chrono::DateTime, -} - -impl SavedConversationMetadata { - pub async fn list(fs: Arc) -> Result> { - fs.create_dir(&CONVERSATIONS_DIR).await?; - - let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; - let mut conversations = Vec::::new(); - while let Some(path) = paths.next().await { - let path = path?; - if path.extension() != Some(OsStr::new("json")) { - continue; - } - - let pattern = r" - \d+.zed.json$"; - let re = Regex::new(pattern).unwrap(); - - let metadata = fs.metadata(&path).await?; - if let Some((file_name, metadata)) = path - .file_name() - .and_then(|name| name.to_str()) - .zip(metadata) - { - let title = re.replace(file_name, ""); - conversations.push(Self { - title: title.into_owned(), - path, - mtime: metadata.mtime.into(), - }); - } - } - conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); - - Ok(conversations) - } -} - -pub fn init(cx: &mut AppContext) { - assistant_panel::init(cx); -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs deleted file mode 100644 index 7b19ad130c..0000000000 --- a/crates/assistant2/src/assistant_panel.rs +++ /dev/null @@ -1,3540 +0,0 @@ -use crate::{ - assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - codegen::{self, Codegen, CodegenKind}, - prompts::generate_content_prompt, - Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, - NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, -}; - -use ai::{ - auth::ProviderCredential, - completion::{CompletionProvider, CompletionRequest}, - providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage}, -}; - -use ai::prompts::repository_context::PromptCodeSnippet; -use anyhow::{anyhow, Result}; -use chrono::{DateTime, Local}; -use client::{telemetry::AssistantKind, TelemetrySettings}; -use collections::{hash_map, HashMap, HashSet, VecDeque}; -use editor::{ - display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, - }, - scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, - ToOffset, ToPoint, -}; -use fs::Fs; -use futures::StreamExt; -use gpui::{ - canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, - AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter, FocusHandle, - FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, - ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, - StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, - View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, -}; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; -use project::Project; -use search::BufferSearchBar; -use semantic_index::{SemanticIndex, SemanticIndexStatus}; -use settings::{Settings, SettingsStore}; -use std::{ - cell::Cell, - cmp, - fmt::Write, - iter, - ops::Range, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, - time::{Duration, Instant}, -}; -use theme::ThemeSettings; -use ui::{ - prelude::*, - utils::{DateTimeType, FormatDistance}, - ButtonLike, Tab, TabBar, Tooltip, -}; -use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; -use uuid::Uuid; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - searchable::Direction, - Save, Toast, ToggleZoom, Toolbar, Workspace, -}; - -pub fn init(cx: &mut AppContext) { - AssistantSettings::register(cx); - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace - .register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }) - .register_action(AssistantPanel::inline_assist) - .register_action(AssistantPanel::cancel_last_inline_assist) - .register_action(ConversationEditor::quote_selection); - }, - ) - .detach(); -} - -pub struct AssistantPanel { - workspace: WeakView, - width: Option, - height: Option, - active_editor_index: Option, - prev_active_editor_index: Option, - editors: Vec>, - saved_conversations: Vec, - saved_conversations_scroll_handle: UniformListScrollHandle, - zoomed: bool, - focus_handle: FocusHandle, - toolbar: View, - completion_provider: Arc, - api_key_editor: Option>, - languages: Arc, - fs: Arc, - subscriptions: Vec, - next_inline_assist_id: usize, - pending_inline_assists: HashMap, - pending_inline_assist_ids_by_editor: HashMap, Vec>, - include_conversation_in_next_inline_assist: bool, - inline_prompt_history: VecDeque, - _watch_saved_conversations: Task>, - semantic_index: Option>, - retrieve_context_in_next_inline_assist: bool, -} - -impl AssistantPanel { - const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; - - pub fn load( - workspace: WeakView, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; - let saved_conversations = SavedConversationMetadata::list(fs.clone()) - .await - .log_err() - .unwrap_or_default(); - - // TODO: deserialize state. - let workspace_handle = workspace.clone(); - workspace.update(&mut cx, |workspace, cx| { - cx.new_view::(|cx| { - const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); - let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { - let mut events = fs - .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION) - .await; - while events.next().await.is_some() { - let saved_conversations = SavedConversationMetadata::list(fs.clone()) - .await - .log_err() - .unwrap_or_default(); - this.update(&mut cx, |this, cx| { - this.saved_conversations = saved_conversations; - cx.notify(); - }) - .ok(); - } - - anyhow::Ok(()) - }); - - let toolbar = cx.new_view(|cx| { - let mut toolbar = Toolbar::new(); - toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.new_view(|cx| BufferSearchBar::new(cx)), cx); - toolbar - }); - - let semantic_index = SemanticIndex::global(cx); - // Defaulting currently to GPT4, allow for this to be set via config. - let completion_provider = Arc::new(OpenAICompletionProvider::new( - "gpt-4", - cx.background_executor().clone(), - )); - - let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, Self::focus_out).detach(); - - let mut this = Self { - workspace: workspace_handle, - active_editor_index: Default::default(), - prev_active_editor_index: Default::default(), - editors: Default::default(), - saved_conversations, - saved_conversations_scroll_handle: Default::default(), - zoomed: false, - focus_handle, - toolbar, - completion_provider, - api_key_editor: None, - languages: workspace.app_state().languages.clone(), - fs: workspace.app_state().fs.clone(), - 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(), - include_conversation_in_next_inline_assist: false, - inline_prompt_history: Default::default(), - _watch_saved_conversations, - semantic_index, - retrieve_context_in_next_inline_assist: false, - }; - - let mut old_dock_position = this.position(cx); - this.subscriptions = - vec![cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - cx.notify(); - })]; - - this - }) - }) - }) - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); - cx.notify(); - if self.focus_handle.is_focused(cx) { - if let Some(editor) = self.active_editor() { - cx.focus_view(editor); - } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { - cx.focus_view(api_key_editor); - } - } - } - - fn focus_out(&mut self, cx: &mut ViewContext) { - self.toolbar - .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); - cx.notify(); - } - - pub fn inline_assist( - workspace: &mut Workspace, - _: &InlineAssist, - cx: &mut ViewContext, - ) { - let this = if let Some(this) = workspace.panel::(cx) { - if this.update(cx, |assistant, cx| { - if !assistant.has_credentials() { - assistant.load_credentials(cx); - }; - - assistant.has_credentials() - }) { - this - } 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; - }; - - let project = workspace.project(); - - this.update(cx, |assistant, cx| { - assistant.new_inline_assist(&active_editor, cx, project) - }); - } - - fn new_inline_assist( - &mut self, - editor: &View, - cx: &mut ViewContext, - project: &Model, - ) { - let selection = editor.read(cx).selections.newest_anchor().clone(); - if selection.start.excerpt_id != selection.end.excerpt_id { - return; - } - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - - // Extend the selection to the start and the end of the line. - let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); - if point_selection.end > point_selection.start { - point_selection.start.column = 0; - // If the selection ends at the start of the line, we don't want to include it. - if point_selection.end.column == 0 { - point_selection.end.row -= 1; - } - point_selection.end.column = snapshot.line_len(point_selection.end.row); - } - - let codegen_kind = if point_selection.start == point_selection.end { - CodegenKind::Generate { - position: snapshot.anchor_after(point_selection.start), - } - } else { - CodegenKind::Transform { - range: snapshot.anchor_before(point_selection.start) - ..snapshot.anchor_after(point_selection.end), - } - }; - - let inline_assist_id = post_inc(&mut self.next_inline_assist_id); - let provider = self.completion_provider.clone(); - - // Retrieve Credentials Authenticates the Provider - provider.retrieve_credentials(cx); - - let codegen = cx.new_model(|cx| { - Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) - }); - - if let Some(semantic_index) = self.semantic_index.clone() { - let project = project.clone(); - cx.spawn(|_, mut cx| async move { - let previously_indexed = semantic_index - .update(&mut cx, |index, cx| { - index.project_previously_indexed(&project, cx) - })? - .await - .unwrap_or(false); - if previously_indexed { - let _ = semantic_index - .update(&mut cx, |index, cx| { - index.index_project(project.clone(), cx) - })? - .await; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.new_view(|cx| { - InlineAssistant::new( - inline_assist_id, - measurements.clone(), - self.include_conversation_in_next_inline_assist, - self.inline_prompt_history.clone(), - codegen.clone(), - self.workspace.clone(), - cx, - self.retrieve_context_in_next_inline_assist, - self.semantic_index.clone(), - project.clone(), - ) - }); - let block_id = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { - selections.select_anchor_ranges([selection.head()..selection.head()]) - }); - editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - position: snapshot.anchor_before(point_selection.head()), - height: 2, - render: Arc::new({ - let inline_assistant = inline_assistant.clone(); - move |cx: &mut BlockContext| { - measurements.set(BlockMeasurements { - anchor_x: cx.anchor_x, - gutter_width: cx.gutter_width, - }); - inline_assistant.clone().into_any_element() - } - }), - disposition: if selection.reversed { - BlockDisposition::Above - } else { - BlockDisposition::Below - }, - }], - Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), - cx, - )[0] - }); - - self.pending_inline_assists.insert( - inline_assist_id, - PendingInlineAssist { - editor: editor.downgrade(), - inline_assistant: Some((block_id, inline_assistant.clone())), - codegen: codegen.clone(), - project: project.downgrade(), - _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() { - if let EditorEvent::SelectionsChanged { local } = event { - if *local - && inline_assistant.focus_handle(cx).contains_focused(cx) - { - cx.focus_view(&editor); - } - } - } - } - }), - cx.observe(&codegen, { - let editor = editor.downgrade(); - move |this, _, cx| { - if let Some(editor) = editor.upgrade() { - this.update_highlights_for_editor(&editor, cx); - } - } - }), - cx.subscribe(&codegen, move |this, codegen, event, cx| match event { - codegen::Event::Undone => { - this.finish_inline_assist(inline_assist_id, false, cx) - } - codegen::Event::Finished => { - let pending_assist = if let Some(pending_assist) = - this.pending_inline_assists.get(&inline_assist_id) - { - pending_assist - } else { - return; - }; - - let error = codegen - .read(cx) - .error() - .map(|error| format!("Inline assistant error: {}", error)); - if let Some(error) = error { - if pending_assist.inline_assistant.is_none() { - if let Some(workspace) = this.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new(inline_assist_id, error), - cx, - ); - }) - } - - this.finish_inline_assist(inline_assist_id, false, cx); - } - } else { - this.finish_inline_assist(inline_assist_id, false, cx); - } - } - }), - ], - }, - ); - self.pending_inline_assist_ids_by_editor - .entry(editor.downgrade()) - .or_default() - .push(inline_assist_id); - self.update_highlights_for_editor(&editor, cx); - } - - fn handle_inline_assistant_event( - &mut self, - inline_assistant: View, - event: &InlineAssistantEvent, - cx: &mut ViewContext, - ) { - let assist_id = inline_assistant.read(cx).id; - match event { - InlineAssistantEvent::Confirmed { - prompt, - include_conversation, - retrieve_context, - } => { - self.confirm_inline_assist( - assist_id, - prompt, - *include_conversation, - cx, - *retrieve_context, - ); - } - InlineAssistantEvent::Canceled => { - self.finish_inline_assist(assist_id, true, cx); - } - InlineAssistantEvent::Dismissed => { - self.hide_inline_assist(assist_id, cx); - } - InlineAssistantEvent::IncludeConversationToggled { - include_conversation, - } => { - self.include_conversation_in_next_inline_assist = *include_conversation; - } - InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { - self.retrieve_context_in_next_inline_assist = *retrieve_context - } - } - } - - fn cancel_last_inline_assist( - workspace: &mut Workspace, - _: &editor::Cancel, - cx: &mut ViewContext, - ) { - if let Some(panel) = workspace.panel::(cx) { - if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let handled = panel.update(cx, |panel, cx| { - if let Some(assist_id) = panel - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .and_then(|assist_ids| assist_ids.last().copied()) - { - panel.finish_inline_assist(assist_id, true, cx); - true - } else { - false - } - }); - if handled { - return; - } - } - } - - cx.propagate(); - } - - fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { - self.hide_inline_assist(assist_id, cx); - - if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { - if let hash_map::Entry::Occupied(mut entry) = self - .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor.clone()) - { - entry.get_mut().retain(|id| *id != assist_id); - if entry.get().is_empty() { - entry.remove(); - } - } - - if let Some(editor) = pending_assist.editor.upgrade() { - self.update_highlights_for_editor(&editor, cx); - - if undo { - pending_assist - .codegen - .update(cx, |codegen, cx| codegen.undo(cx)); - } - } - } - } - - fn hide_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() { - if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() { - editor.update(cx, |editor, cx| { - editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); - if inline_assistant.focus_handle(cx).contains_focused(cx) { - editor.focus(cx); - } - }); - } - } - } - } - - fn confirm_inline_assist( - &mut self, - inline_assist_id: usize, - user_prompt: &str, - include_conversation: bool, - cx: &mut ViewContext, - retrieve_context: bool, - ) { - let conversation = if include_conversation { - self.active_editor() - .map(|editor| editor.read(cx).conversation.clone()) - } else { - None - }; - - 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() { - editor - } else { - return; - }; - - let project = pending_assist.project.clone(); - - let project_name = if let Some(project) = project.upgrade() { - Some( - project - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join("/"), - ) - } else { - None - }; - - self.inline_prompt_history - .retain(|prompt| prompt != user_prompt); - self.inline_prompt_history.push_back(user_prompt.into()); - if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { - self.inline_prompt_history.pop_front(); - } - - let codegen = pending_assist.codegen.clone(); - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - let range = codegen.read(cx).range(); - let start = snapshot.point_to_buffer_offset(range.start); - let end = snapshot.point_to_buffer_offset(range.end); - let (buffer, range) = if let Some((start, end)) = start.zip(end) { - let (start_buffer, start_buffer_offset) = start; - let (end_buffer, end_buffer_offset) = end; - if start_buffer.remote_id() == end_buffer.remote_id() { - (start_buffer.clone(), start_buffer_offset..end_buffer_offset) - } else { - self.finish_inline_assist(inline_assist_id, false, cx); - return; - } - } else { - self.finish_inline_assist(inline_assist_id, false, cx); - return; - }; - - let language = buffer.language_at(range.start); - let language_name = if let Some(language) = language.as_ref() { - if Arc::ptr_eq(language, &language::PLAIN_TEXT) { - None - } else { - Some(language.name()) - } - } else { - None - }; - - // Higher Temperature increases the randomness of model outputs. - // If Markdown or No Language is Known, increase the randomness for more creative output - // If Code, decrease temperature to get more deterministic outputs - let temperature = if let Some(language) = language_name.clone() { - if language.to_string() != "Markdown".to_string() { - 0.5 - } else { - 1.0 - } - } else { - 1.0 - }; - - let user_prompt = user_prompt.to_string(); - - let snippets = if retrieve_context { - let Some(project) = project.upgrade() else { - return; - }; - - let search_results = if let Some(semantic_index) = self.semantic_index.clone() { - let search_results = semantic_index.update(cx, |this, cx| { - this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) - }); - - cx.background_executor() - .spawn(async move { search_results.await.unwrap_or_default() }) - } else { - Task::ready(Vec::new()) - }; - - let snippets = cx.spawn(|_, mut cx| async move { - let mut snippets = Vec::new(); - for result in search_results.await { - snippets.push(PromptCodeSnippet::new( - result.buffer, - result.range, - &mut cx, - )?); - } - anyhow::Ok(snippets) - }); - snippets - } else { - Task::ready(Ok(Vec::new())) - }; - - let mut model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); - let model_name = model.full_name(); - - let prompt = cx.background_executor().spawn(async move { - let snippets = snippets.await?; - - let language_name = language_name.as_deref(); - generate_content_prompt( - user_prompt, - language_name, - buffer, - range, - snippets, - model_name, - project_name, - ) - }); - - let mut messages = Vec::new(); - if let Some(conversation) = conversation { - let conversation = conversation.read(cx); - let buffer = conversation.buffer.read(cx); - messages.extend( - conversation - .messages(cx) - .map(|message| message.to_open_ai_message(buffer)), - ); - model = conversation.model.clone(); - } - - cx.spawn(|_, mut cx| async move { - // I Don't know if we want to return a ? here. - let prompt = prompt.await?; - - messages.push(RequestMessage { - role: Role::User, - content: prompt, - }); - - let request = Box::new(OpenAIRequest { - model: model.full_name().into(), - messages, - stream: true, - stop: vec!["|END|>".to_string()], - temperature, - }); - - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; - anyhow::Ok(()) - }) - .detach(); - } - - fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { - let mut background_ranges = Vec::new(); - let mut foreground_ranges = Vec::new(); - let empty_inline_assist_ids = Vec::new(); - let inline_assist_ids = self - .pending_inline_assist_ids_by_editor - .get(&editor.downgrade()) - .unwrap_or(&empty_inline_assist_ids); - - for inline_assist_id in inline_assist_ids { - if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { - let codegen = pending_assist.codegen.read(cx); - background_ranges.push(codegen.range()); - foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); - } - } - - let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); - merge_ranges(&mut background_ranges, &snapshot); - merge_ranges(&mut foreground_ranges, &snapshot); - editor.update(cx, |editor, cx| { - if background_ranges.is_empty() { - editor.clear_background_highlights::(cx); - } else { - editor.highlight_background::( - background_ranges, - |theme| theme.editor_active_line_background, // todo!("use the appropriate color") - cx, - ); - } - - if foreground_ranges.is_empty() { - editor.clear_highlights::(cx); - } else { - editor.highlight_text::( - foreground_ranges, - HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - } - }); - } - - fn new_conversation(&mut self, cx: &mut ViewContext) -> View { - let editor = cx.new_view(|cx| { - ConversationEditor::new( - self.completion_provider.clone(), - self.languages.clone(), - self.fs.clone(), - self.workspace.clone(), - cx, - ) - }); - self.add_conversation(editor.clone(), cx); - editor - } - - fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { - self.subscriptions - .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); - - let conversation = editor.read(cx).conversation.clone(); - self.subscriptions - .push(cx.observe(&conversation, |_, _, cx| cx.notify())); - - let index = self.editors.len(); - self.editors.push(editor); - self.set_active_editor_index(Some(index), cx); - } - - fn set_active_editor_index(&mut self, index: Option, cx: &mut ViewContext) { - self.prev_active_editor_index = self.active_editor_index; - self.active_editor_index = index; - if let Some(editor) = self.active_editor() { - let editor = editor.read(cx).editor.clone(); - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(Some(&editor), cx); - }); - if self.focus_handle.contains_focused(cx) { - cx.focus_view(&editor); - } - } else { - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(None, cx); - }); - } - - cx.notify(); - } - - fn handle_conversation_editor_event( - &mut self, - _: View, - event: &ConversationEditorEvent, - cx: &mut ViewContext, - ) { - match event { - ConversationEditorEvent::TabContentChanged => cx.notify(), - } - } - - fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(api_key) = self - .api_key_editor - .as_ref() - .map(|editor| editor.read(cx).text(cx)) - { - if !api_key.is_empty() { - let credential = ProviderCredential::Credentials { - api_key: api_key.clone(), - }; - - self.completion_provider.save_credentials(cx, credential); - - self.api_key_editor.take(); - self.focus_handle.focus(cx); - cx.notify(); - } - } else { - cx.propagate(); - } - } - - fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { - self.completion_provider.delete_credentials(cx); - self.api_key_editor = Some(build_api_key_editor(cx)); - self.focus_handle.focus(cx); - cx.notify(); - } - - fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { - if self.zoomed { - cx.emit(PanelEvent::ZoomOut) - } else { - cx.emit(PanelEvent::ZoomIn) - } - } - - fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate = true; - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(cx) { - search_bar.search_suggested(cx); - if action.focus { - let focus_handle = search_bar.focus_handle(cx); - search_bar.select_query(cx); - cx.focus(&focus_handle); - } - propagate = false - } - }); - } - if propagate { - cx.propagate(); - } - } - - fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - if !search_bar.read(cx).is_dismissed() { - search_bar.update(cx, |search_bar, cx| { - search_bar.dismiss(&Default::default(), cx) - }); - return; - } - } - cx.propagate(); - } - - fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); - } - } - - fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { - if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); - } - } - - fn active_editor(&self) -> Option<&View> { - self.editors.get(self.active_editor_index?) - } - - fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("hamburger_button", Icon::Menu) - .on_click(cx.listener(|this, _event, cx| { - if this.active_editor().is_some() { - this.set_active_editor_index(None, cx); - } else { - this.set_active_editor_index(this.prev_active_editor_index, cx); - } - })) - .tooltip(|cx| Tooltip::text("Conversation History", cx)) - } - - fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { - if self.active_editor().is_some() { - vec![ - Self::render_split_button(cx).into_any_element(), - Self::render_quote_button(cx).into_any_element(), - Self::render_assist_button(cx).into_any_element(), - ] - } else { - Default::default() - } - } - - fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", Icon::Snip) - .on_click(cx.listener(|this, _event, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); - } - })) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) - } - - fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("assist_button", Icon::MagicWand) - .on_click(cx.listener(|this, _event, cx| { - if let Some(active_editor) = this.active_editor() { - active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); - } - })) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) - } - - fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("quote_button", Icon::Quote) - .on_click(cx.listener(|this, _event, cx| { - if let Some(workspace) = this.workspace.upgrade() { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - ConversationEditor::quote_selection(workspace, &Default::default(), cx) - }); - }); - } - })) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::for_action("Quote Selection", &QuoteSelection, cx)) - } - - fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("plus_button", Icon::Plus) - .on_click(cx.listener(|this, _event, cx| { - this.new_conversation(cx); - })) - .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) - } - - fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { - let zoomed = self.zoomed; - IconButton::new("zoom_button", Icon::Maximize) - .on_click(cx.listener(|this, _event, cx| { - this.toggle_zoom(&ToggleZoom, cx); - })) - .selected(zoomed) - .selected_icon(Icon::Minimize) - .icon_size(IconSize::Small) - .tooltip(move |cx| { - Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) - }) - } - - fn render_saved_conversation( - &mut self, - index: usize, - cx: &mut ViewContext, - ) -> impl IntoElement { - let conversation = &self.saved_conversations[index]; - let path = conversation.path.clone(); - - ButtonLike::new(index) - .on_click(cx.listener(move |this, _, cx| { - this.open_conversation(path.clone(), cx) - .detach_and_log_err(cx) - })) - .full_width() - .child( - div() - .flex() - .w_full() - .gap_2() - .child( - Label::new(conversation.mtime.format("%F %I:%M%p").to_string()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .child(Label::new(conversation.title.clone()).size(LabelSize::Small)), - ) - } - - fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { - cx.focus(&self.focus_handle); - - if let Some(ix) = self.editor_index_for_path(&path, cx) { - self.set_active_editor_index(Some(ix), cx); - return Task::ready(Ok(())); - } - - let fs = self.fs.clone(); - let workspace = self.workspace.clone(); - let languages = self.languages.clone(); - cx.spawn(|this, mut cx| async move { - let saved_conversation = fs.load(&path).await?; - let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.new_model(|cx| { - Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - })?; - this.update(&mut cx, |this, cx| { - // If, by the time we've loaded the conversation, the user has already opened - // the same conversation, we don't want to open it again. - if let Some(ix) = this.editor_index_for_path(&path, cx) { - this.set_active_editor_index(Some(ix), cx); - } else { - let editor = cx.new_view(|cx| { - ConversationEditor::for_conversation(conversation, fs, workspace, cx) - }); - this.add_conversation(editor, cx); - } - })?; - Ok(()) - }) - } - - fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option { - self.editors - .iter() - .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) - } - - fn has_credentials(&mut self) -> bool { - self.completion_provider.has_credentials() - } - - fn load_credentials(&mut self, cx: &mut ViewContext) { - self.completion_provider.retrieve_credentials(cx); - } -} - -fn build_api_key_editor(cx: &mut ViewContext) -> View { - cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); - editor - }) -} - -impl Render for AssistantPanel { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if let Some(api_key_editor) = self.api_key_editor.clone() { - v_stack() - .on_action(cx.listener(AssistantPanel::save_credentials)) - .track_focus(&self.focus_handle) - .child(Label::new( - "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", - )) - .child(Label::new( - " - Having a subscription for another service like GitHub Copilot won't work." - )) - .child(Label::new( - " - You can create a api key at: platform.openai.com/api-keys" - )) - .child(Label::new( - " " - )) - .child(Label::new( - "Paste your OpenAI API key and press Enter to use the assistant" - )) - .child(api_key_editor) - .child(Label::new( - "Click on the Z button in the status bar to close this panel." - )) - .border() - .border_color(gpui::red()) - } else { - let header = TabBar::new("assistant_header") - .start_child( - h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title), - ) - .children(self.active_editor().map(|editor| { - h_stack() - .h(rems(Tab::HEIGHT_IN_REMS)) - .flex_1() - .px_2() - .child(Label::new(editor.read(cx).title(cx)).into_element()) - })) - .end_child(if self.focus_handle.contains_focused(cx) { - h_stack() - .gap_2() - .child(h_stack().gap_1().children(self.render_editor_tools(cx))) - .child( - ui::Divider::vertical() - .inset() - .color(ui::DividerColor::Border), - ) - .child( - h_stack() - .gap_1() - .child(Self::render_plus_button(cx)) - .child(self.render_zoom_button(cx)), - ) - } else { - div() - }); - - v_stack() - .size_full() - .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { - this.new_conversation(cx); - })) - .on_action(cx.listener(AssistantPanel::reset_credentials)) - .on_action(cx.listener(AssistantPanel::toggle_zoom)) - .on_action(cx.listener(AssistantPanel::deploy)) - .on_action(cx.listener(AssistantPanel::select_next_match)) - .on_action(cx.listener(AssistantPanel::select_prev_match)) - .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) - .track_focus(&self.focus_handle) - .child(header) - .children(if self.toolbar.read(cx).hidden() { - None - } else { - Some(self.toolbar.clone()) - }) - .child( - div() - .flex_1() - .child(if let Some(editor) = self.active_editor() { - editor.clone().into_any_element() - } else { - let view = cx.view().clone(); - let scroll_handle = self.saved_conversations_scroll_handle.clone(); - let conversation_count = self.saved_conversations.len(); - canvas(move |bounds, cx| { - uniform_list( - view, - "saved_conversations", - conversation_count, - |this, range, cx| { - range - .map(|ix| this.render_saved_conversation(ix, cx)) - .collect() - }, - ) - .track_scroll(scroll_handle) - .into_any_element() - .draw( - bounds.origin, - bounds.size.map(AvailableSpace::Definite), - cx, - ); - }) - .size_full() - .into_any_element() - }), - ) - } - } -} - -impl Panel for AssistantPanel { - fn persistent_name() -> &'static str { - "AssistantPanel" - } - - fn position(&self, cx: &WindowContext) -> DockPosition { - match AssistantSettings::get_global(cx).dock { - AssistantDockPosition::Left => DockPosition::Left, - AssistantDockPosition::Bottom => DockPosition::Bottom, - AssistantDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, _: DockPosition) -> bool { - true - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::(self.fs.clone(), cx, move |settings| { - let dock = match position { - DockPosition::Left => AssistantDockPosition::Left, - DockPosition::Bottom => AssistantDockPosition::Bottom, - DockPosition::Right => AssistantDockPosition::Right, - }; - settings.dock = Some(dock); - }); - } - - fn size(&self, cx: &WindowContext) -> Pixels { - let settings = AssistantSettings::get_global(cx); - match self.position(cx) { - DockPosition::Left | DockPosition::Right => { - self.width.unwrap_or_else(|| settings.default_width) - } - DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), - } - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - match self.position(cx) { - DockPosition::Left | DockPosition::Right => self.width = size, - DockPosition::Bottom => self.height = size, - } - cx.notify(); - } - - fn is_zoomed(&self, _: &WindowContext) -> bool { - self.zoomed - } - - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.zoomed = zoomed; - cx.notify(); - } - - fn set_active(&mut self, active: bool, cx: &mut ViewContext) { - if active { - self.load_credentials(cx); - - if self.editors.is_empty() { - self.new_conversation(cx); - } - } - } - - fn icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::Ai) - } - - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { - Some("Assistant Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } -} - -impl EventEmitter for AssistantPanel {} - -impl FocusableView for AssistantPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -enum ConversationEvent { - MessagesEdited, - SummaryChanged, - StreamedCompletion, -} - -#[derive(Default)] -struct Summary { - text: String, - done: bool, -} - -struct Conversation { - id: Option, - buffer: Model, - message_anchors: Vec, - messages_metadata: HashMap, - next_message_id: MessageId, - summary: Option, - pending_summary: Task>, - completion_count: usize, - pending_completions: Vec, - model: OpenAIModel, - token_count: Option, - max_token_count: usize, - pending_token_count: Task>, - pending_save: Task>, - path: Option, - _subscriptions: Vec, - completion_provider: Arc, -} - -impl EventEmitter for Conversation {} - -impl Conversation { - fn new( - language_registry: Arc, - cx: &mut ModelContext, - completion_provider: Arc, - ) -> Self { - let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.new_model(|cx| { - let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); - buffer.set_language_registry(language_registry); - cx.spawn(|buffer, mut cx| async move { - let markdown = markdown.await?; - buffer.update(&mut cx, |buffer: &mut Buffer, cx| { - buffer.set_language(Some(markdown), cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - buffer - }); - - let settings = AssistantSettings::get_global(cx); - let model = settings.default_open_ai_model.clone(); - - let mut this = Self { - id: Some(Uuid::new_v4().to_string()), - message_anchors: Default::default(), - messages_metadata: Default::default(), - next_message_id: Default::default(), - summary: None, - pending_summary: Task::ready(None), - completion_count: Default::default(), - pending_completions: Default::default(), - token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), - pending_token_count: Task::ready(None), - model: model.clone(), - _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], - pending_save: Task::ready(Ok(())), - path: None, - buffer, - completion_provider, - }; - let message = MessageAnchor { - id: MessageId(post_inc(&mut this.next_message_id.0)), - start: language::Anchor::MIN, - }; - this.message_anchors.push(message.clone()); - this.messages_metadata.insert( - message.id, - MessageMetadata { - role: Role::User, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - - this.count_remaining_tokens(cx); - this - } - - fn serialize(&self, cx: &AppContext) -> SavedConversation { - SavedConversation { - id: self.id.clone(), - zed: "conversation".into(), - version: SavedConversation::VERSION.into(), - text: self.buffer.read(cx).text(), - message_metadata: self.messages_metadata.clone(), - messages: self - .messages(cx) - .map(|message| SavedMessage { - id: message.id, - start: message.offset_range.start, - }) - .collect(), - summary: self - .summary - .as_ref() - .map(|summary| summary.text.clone()) - .unwrap_or_default(), - model: self.model.clone(), - } - } - - fn deserialize( - saved_conversation: SavedConversation, - path: PathBuf, - language_registry: Arc, - cx: &mut ModelContext, - ) -> Self { - let id = match saved_conversation.id { - Some(id) => Some(id), - None => Some(Uuid::new_v4().to_string()), - }; - let model = saved_conversation.model; - let completion_provider: Arc = Arc::new( - OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), - ); - completion_provider.retrieve_credentials(cx); - let markdown = language_registry.language_for_name("Markdown"); - let mut message_anchors = Vec::new(); - let mut next_message_id = MessageId(0); - let buffer = cx.new_model(|cx| { - let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); - for message in saved_conversation.messages { - message_anchors.push(MessageAnchor { - id: message.id, - start: buffer.anchor_before(message.start), - }); - next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); - } - buffer.set_language_registry(language_registry); - cx.spawn(|buffer, mut cx| async move { - let markdown = markdown.await?; - buffer.update(&mut cx, |buffer: &mut Buffer, cx| { - buffer.set_language(Some(markdown), cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - buffer - }); - - let mut this = Self { - id, - message_anchors, - messages_metadata: saved_conversation.message_metadata, - next_message_id, - summary: Some(Summary { - text: saved_conversation.summary, - done: true, - }), - pending_summary: Task::ready(None), - completion_count: Default::default(), - pending_completions: Default::default(), - token_count: None, - max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), - pending_token_count: Task::ready(None), - model, - _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], - pending_save: Task::ready(Ok(())), - path: Some(path), - buffer, - completion_provider, - }; - this.count_remaining_tokens(cx); - this - } - - fn handle_buffer_event( - &mut self, - _: Model, - event: &language::Event, - cx: &mut ModelContext, - ) { - match event { - language::Event::Edited => { - self.count_remaining_tokens(cx); - cx.emit(ConversationEvent::MessagesEdited); - } - _ => {} - } - } - - fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { - let messages = self - .messages(cx) - .into_iter() - .filter_map(|message| { - Some(tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: Some( - self.buffer - .read(cx) - .text_for_range(message.offset_range) - .collect(), - ), - name: None, - function_call: None, - }) - }) - .collect::>(); - let model = self.model.clone(); - self.pending_token_count = cx.spawn(|this, mut cx| { - async move { - cx.background_executor() - .timer(Duration::from_millis(200)) - .await; - let token_count = cx - .background_executor() - .spawn(async move { - tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) - }) - .await?; - - this.update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - })?; - anyhow::Ok(()) - } - .log_err() - }); - } - - fn remaining_tokens(&self) -> Option { - Some(self.max_token_count as isize - self.token_count? as isize) - } - - fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { - self.model = model; - self.count_remaining_tokens(cx); - cx.notify(); - } - - fn assist( - &mut self, - selected_messages: HashSet, - cx: &mut ModelContext, - ) -> Vec { - let mut user_messages = Vec::new(); - - let last_message_id = if let Some(last_message_id) = - self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }) { - last_message_id - } else { - return Default::default(); - }; - - let mut should_assist = false; - for selected_message_id in selected_messages { - let selected_message_role = - if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { - metadata.role - } else { - continue; - }; - - if selected_message_role == Role::Assistant { - if let Some(user_message) = self.insert_message_after( - selected_message_id, - Role::User, - MessageStatus::Done, - cx, - ) { - user_messages.push(user_message); - } - } else { - should_assist = true; - } - } - - if should_assist { - if !self.completion_provider.has_credentials() { - return Default::default(); - } - - let request: Box = Box::new(OpenAIRequest { - model: self.model.full_name().to_string(), - messages: self - .messages(cx) - .filter(|message| matches!(message.status, MessageStatus::Done)) - .map(|message| message.to_open_ai_message(self.buffer.read(cx))) - .collect(), - stream: true, - stop: vec![], - temperature: 1.0, - }); - - let stream = self.completion_provider.complete(request); - let assistant_message = self - .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) - .unwrap(); - - // Queue up the user's next reply. - let user_message = self - .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) - .unwrap(); - user_messages.push(user_message); - - let task = cx.spawn({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let text = message?; - - this.update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors - .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) - })?; - smol::future::yield_now().await; - } - - this.update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - })?; - - anyhow::Ok(()) - }; - - let result = stream_completion.await; - - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = MessageStatus::Error(SharedString::from( - error.to_string().trim().to_string(), - )); - } - } - cx.notify(); - } - }) - .ok(); - } - }); - - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - } - - user_messages - } - - fn cancel_last_assist(&mut self) -> bool { - self.pending_completions.pop().is_some() - } - - fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { - for id in ids { - if let Some(metadata) = self.messages_metadata.get_mut(&id) { - metadata.role.cycle(); - cx.emit(ConversationEvent::MessagesEdited); - cx.notify(); - } - } - } - - fn insert_message_after( - &mut self, - message_id: MessageId, - role: Role, - status: MessageStatus, - cx: &mut ModelContext, - ) -> Option { - if let Some(prev_message_ix) = self - .message_anchors - .iter() - .position(|message| message.id == message_id) - { - // Find the next valid message after the one we were given. - let mut next_message_ix = prev_message_ix + 1; - while let Some(next_message) = self.message_anchors.get(next_message_ix) { - if next_message.start.is_valid(self.buffer.read(cx)) { - break; - } - next_message_ix += 1; - } - - let start = self.buffer.update(cx, |buffer, cx| { - let offset = self - .message_anchors - .get(next_message_ix) - .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1); - buffer.edit([(offset..offset, "\n")], None, cx); - buffer.anchor_before(offset + 1) - }); - let message = MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start, - }; - self.message_anchors - .insert(next_message_ix, message.clone()); - self.messages_metadata.insert( - message.id, - MessageMetadata { - role, - sent_at: Local::now(), - status, - }, - ); - cx.emit(ConversationEvent::MessagesEdited); - Some(message) - } else { - None - } - } - - fn split_message( - &mut self, - range: Range, - cx: &mut ModelContext, - ) -> (Option, Option) { - let start_message = self.message_for_offset(range.start, cx); - let end_message = self.message_for_offset(range.end, cx); - if let Some((start_message, end_message)) = start_message.zip(end_message) { - // Prevent splitting when range spans multiple messages. - if start_message.id != end_message.id { - return (None, None); - } - - let message = start_message; - let role = message.role; - let mut edited_buffer = false; - - let mut suffix_start = None; - if range.start > message.offset_range.start && range.end < message.offset_range.end - 1 - { - if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') { - suffix_start = Some(range.end + 1); - } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') { - suffix_start = Some(range.end); - } - } - - let suffix = if let Some(suffix_start) = suffix_start { - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(suffix_start), - } - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit([(range.end..range.end, "\n")], None, cx); - }); - edited_buffer = true; - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(range.end + 1), - } - }; - - self.message_anchors - .insert(message.index_range.end + 1, suffix.clone()); - self.messages_metadata.insert( - suffix.id, - MessageMetadata { - role, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - - let new_messages = - if range.start == range.end || range.start == message.offset_range.start { - (None, Some(suffix)) - } else { - let mut prefix_end = None; - if range.start > message.offset_range.start - && range.end < message.offset_range.end - 1 - { - if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') { - prefix_end = Some(range.start + 1); - } else if self.buffer.read(cx).reversed_chars_at(range.start).next() - == Some('\n') - { - prefix_end = Some(range.start); - } - } - - let selection = if let Some(prefix_end) = prefix_end { - cx.emit(ConversationEvent::MessagesEdited); - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(prefix_end), - } - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit([(range.start..range.start, "\n")], None, cx) - }); - edited_buffer = true; - MessageAnchor { - id: MessageId(post_inc(&mut self.next_message_id.0)), - start: self.buffer.read(cx).anchor_before(range.end + 1), - } - }; - - self.message_anchors - .insert(message.index_range.end + 1, selection.clone()); - self.messages_metadata.insert( - selection.id, - MessageMetadata { - role, - sent_at: Local::now(), - status: MessageStatus::Done, - }, - ); - (Some(selection), Some(suffix)) - }; - - if !edited_buffer { - cx.emit(ConversationEvent::MessagesEdited); - } - new_messages - } else { - (None, None) - } - } - - fn summarize(&mut self, cx: &mut ModelContext) { - if self.message_anchors.len() >= 2 && self.summary.is_none() { - if !self.completion_provider.has_credentials() { - return; - } - - let messages = self - .messages(cx) - .take(2) - .map(|message| message.to_open_ai_message(self.buffer.read(cx))) - .chain(Some(RequestMessage { - role: Role::User, - content: "Summarize the conversation into a short title without punctuation" - .into(), - })); - let request: Box = Box::new(OpenAIRequest { - model: self.model.full_name().to_string(), - messages: messages.collect(), - stream: true, - stop: vec![], - temperature: 1.0, - }); - - let stream = self.completion_provider.complete(request); - self.pending_summary = cx.spawn(|this, mut cx| { - async move { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let text = message?; - this.update(&mut cx, |this, cx| { - this.summary - .get_or_insert(Default::default()) - .text - .push_str(&text); - cx.emit(ConversationEvent::SummaryChanged); - })?; - } - - this.update(&mut cx, |this, cx| { - if let Some(summary) = this.summary.as_mut() { - summary.done = true; - cx.emit(ConversationEvent::SummaryChanged); - } - })?; - - anyhow::Ok(()) - } - .log_err() - }); - } - } - - fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { - self.messages_for_offsets([offset], cx).pop() - } - - fn messages_for_offsets( - &self, - offsets: impl IntoIterator, - cx: &AppContext, - ) -> Vec { - let mut result = Vec::new(); - - let mut messages = self.messages(cx).peekable(); - let mut offsets = offsets.into_iter().peekable(); - let mut current_message = messages.next(); - while let Some(offset) = offsets.next() { - // Locate the message that contains the offset. - while current_message.as_ref().map_or(false, |message| { - !message.offset_range.contains(&offset) && messages.peek().is_some() - }) { - current_message = messages.next(); - } - let Some(message) = current_message.as_ref() else { - break; - }; - - // Skip offsets that are in the same message. - while offsets.peek().map_or(false, |offset| { - message.offset_range.contains(offset) || messages.peek().is_none() - }) { - offsets.next(); - } - - result.push(message.clone()); - } - result - } - - fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { - let buffer = self.buffer.read(cx); - let mut message_anchors = self.message_anchors.iter().enumerate().peekable(); - iter::from_fn(move || { - while let Some((start_ix, message_anchor)) = message_anchors.next() { - let metadata = self.messages_metadata.get(&message_anchor.id)?; - let message_start = message_anchor.start.to_offset(buffer); - let mut message_end = None; - let mut end_ix = start_ix; - while let Some((_, next_message)) = message_anchors.peek() { - if next_message.start.is_valid(buffer) { - message_end = Some(next_message.start); - break; - } else { - end_ix += 1; - message_anchors.next(); - } - } - let message_end = message_end - .unwrap_or(language::Anchor::MAX) - .to_offset(buffer); - return Some(Message { - index_range: start_ix..end_ix, - offset_range: message_start..message_end, - id: message_anchor.id, - anchor: message_anchor.start, - role: metadata.role, - sent_at: metadata.sent_at, - status: metadata.status.clone(), - }); - } - None - }) - } - - fn save( - &mut self, - debounce: Option, - fs: Arc, - cx: &mut ModelContext, - ) { - self.pending_save = cx.spawn(|this, mut cx| async move { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - - let (old_path, summary) = this.read_with(&cx, |this, _| { - let path = this.path.clone(); - let summary = if let Some(summary) = this.summary.as_ref() { - if summary.done { - Some(summary.text.clone()) - } else { - None - } - } else { - None - }; - (path, summary) - })?; - - if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; - let path = if let Some(old_path) = old_path { - old_path - } else { - let mut discriminant = 1; - let mut new_path; - loop { - new_path = CONVERSATIONS_DIR.join(&format!( - "{} - {}.zed.json", - summary.trim(), - discriminant - )); - if fs.is_file(&new_path).await { - discriminant += 1; - } else { - break; - } - } - new_path - }; - - fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; - fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) - .await?; - this.update(&mut cx, |this, _| this.path = Some(path))?; - } - - Ok(()) - }); - } -} - -struct PendingCompletion { - id: usize, - _task: Task<()>, -} - -enum ConversationEditorEvent { - TabContentChanged, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -struct ScrollPosition { - offset_before_cursor: gpui::Point, - cursor: Anchor, -} - -struct ConversationEditor { - conversation: Model, - fs: Arc, - workspace: WeakView, - editor: View, - blocks: HashSet, - scroll_position: Option, - _subscriptions: Vec, -} - -impl ConversationEditor { - fn new( - completion_provider: Arc, - language_registry: Arc, - fs: Arc, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let conversation = - cx.new_model(|cx| Conversation::new(language_registry, cx, completion_provider)); - Self::for_conversation(conversation, fs, workspace, cx) - } - - fn for_conversation( - conversation: Model, - fs: Arc, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let editor = cx.new_view(|cx| { - let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_gutter(false, cx); - editor.set_show_wrap_guides(false, cx); - editor - }); - - let _subscriptions = vec![ - cx.observe(&conversation, |_, _, cx| cx.notify()), - cx.subscribe(&conversation, Self::handle_conversation_event), - cx.subscribe(&editor, Self::handle_editor_event), - ]; - - let mut this = Self { - conversation, - editor, - blocks: Default::default(), - scroll_position: None, - fs, - workspace, - _subscriptions, - }; - this.update_message_headers(cx); - this - } - - fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - report_assistant_event( - self.workspace.clone(), - self.conversation.read(cx).id.clone(), - AssistantKind::Panel, - cx, - ); - - let cursors = self.cursors(cx); - - let user_messages = self.conversation.update(cx, |conversation, cx| { - let selected_messages = conversation - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - conversation.assist(selected_messages, cx) - }); - let new_selections = user_messages - .iter() - .map(|message| { - let cursor = message - .start - .to_offset(self.conversation.read(cx).buffer.read(cx)); - cursor..cursor - }) - .collect::>(); - if !new_selections.is_empty() { - self.editor.update(cx, |editor, cx| { - editor.change_selections( - Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), - cx, - |selections| selections.select_ranges(new_selections), - ); - }); - // Avoid scrolling to the new cursor position so the assistant's output is stable. - cx.defer(|this, _| this.scroll_position = None); - } - } - - fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { - if !self - .conversation - .update(cx, |conversation, _| conversation.cancel_last_assist()) - { - cx.propagate(); - } - } - - fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { - let cursors = self.cursors(cx); - self.conversation.update(cx, |conversation, cx| { - let messages = conversation - .messages_for_offsets(cursors, cx) - .into_iter() - .map(|message| message.id) - .collect(); - conversation.cycle_message_roles(messages, cx) - }); - } - - fn cursors(&self, cx: &AppContext) -> Vec { - let selections = self.editor.read(cx).selections.all::(cx); - selections - .into_iter() - .map(|selection| selection.head()) - .collect() - } - - fn handle_conversation_event( - &mut self, - _: Model, - event: &ConversationEvent, - cx: &mut ViewContext, - ) { - match event { - ConversationEvent::MessagesEdited => { - self.update_message_headers(cx); - self.conversation.update(cx, |conversation, cx| { - conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); - }); - } - ConversationEvent::SummaryChanged => { - cx.emit(ConversationEditorEvent::TabContentChanged); - self.conversation.update(cx, |conversation, cx| { - conversation.save(None, self.fs.clone(), cx); - }); - } - ConversationEvent::StreamedCompletion => { - self.editor.update(cx, |editor, cx| { - if let Some(scroll_position) = self.scroll_position { - let snapshot = editor.snapshot(cx); - let cursor_point = scroll_position.cursor.to_display_point(&snapshot); - let scroll_top = - cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; - editor.set_scroll_position( - point(scroll_position.offset_before_cursor.x, scroll_top), - cx, - ); - } - }); - } - } - } - - fn handle_editor_event( - &mut self, - _: View, - event: &EditorEvent, - cx: &mut ViewContext, - ) { - match event { - EditorEvent::ScrollPositionChanged { autoscroll, .. } => { - let cursor_scroll_position = self.cursor_scroll_position(cx); - if *autoscroll { - self.scroll_position = cursor_scroll_position; - } else if self.scroll_position != cursor_scroll_position { - self.scroll_position = None; - } - } - EditorEvent::SelectionsChanged { .. } => { - self.scroll_position = self.cursor_scroll_position(cx); - } - _ => {} - } - } - - fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { - self.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(cx); - let cursor = editor.selections.newest_anchor().head(); - let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32; - let scroll_position = editor - .scroll_manager - .anchor() - .scroll_position(&snapshot.display_snapshot); - - let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y..scroll_bottom).contains(&cursor_row) { - Some(ScrollPosition { - cursor, - offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), - }) - } else { - None - } - }) - } - - fn update_message_headers(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let excerpt_id = *buffer.as_singleton().unwrap().0; - let old_blocks = std::mem::take(&mut self.blocks); - let new_blocks = self - .conversation - .read(cx) - .messages(cx) - .map(|message| BlockProperties { - position: buffer.anchor_in_excerpt(excerpt_id, message.anchor), - height: 2, - style: BlockStyle::Sticky, - render: Arc::new({ - let conversation = self.conversation.clone(); - move |_cx| { - let message_id = message.id; - let sender = ButtonLike::new("role") - .child(match message.role { - Role::User => Label::new("You").color(Color::Default), - Role::Assistant => { - Label::new("Assistant").color(Color::Modified) - } - Role::System => Label::new("System").color(Color::Warning), - }) - .tooltip(|cx| { - Tooltip::with_meta( - "Toggle message role", - None, - "Available roles: You (User), Assistant, System", - cx, - ) - }) - .on_click({ - let conversation = conversation.clone(); - move |_, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); - - h_stack() - .id(("message_header", message_id.0)) - .h_11() - .gap_1() - .p_1() - .child(sender) - // TODO: Only show this if the message if the message has been sent - .child( - Label::new( - FormatDistance::from_now(DateTimeType::Local( - message.sent_at, - )) - .hide_prefix(true) - .add_suffix(true) - .to_string(), - ) - .color(Color::Muted), - ) - .children( - if let MessageStatus::Error(error) = message.status.clone() { - Some( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error.clone(), cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }, - ) - .into_any_element() - } - }), - disposition: BlockDisposition::Above, - }) - .collect::>(); - - editor.remove_blocks(old_blocks, None, cx); - let ids = editor.insert_blocks(new_blocks, None, cx); - self.blocks = HashSet::from_iter(ids); - }); - } - - fn quote_selection( - workspace: &mut Workspace, - _: &QuoteSelection, - cx: &mut ViewContext, - ) { - let Some(panel) = workspace.panel::(cx) else { - return; - }; - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - - let editor = editor.read(cx); - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - - let selected_text = buffer.text_for_range(range).collect::(); - let text = if selected_text.is_empty() { - None - } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - }; - - // Activate the panel - if !panel.focus_handle(cx).contains_focused(cx) { - workspace.toggle_panel_focus::(cx); - } - - if let Some(text) = text { - panel.update(cx, |panel, cx| { - let conversation = panel - .active_editor() - .cloned() - .unwrap_or_else(|| panel.new_conversation(cx)); - conversation.update(cx, |conversation, cx| { - conversation - .editor - .update(cx, |editor, cx| editor.insert(&text, cx)) - }); - }); - } - } - - fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { - let editor = self.editor.read(cx); - let conversation = self.conversation.read(cx); - if editor.selections.count() == 1 { - let selection = editor.selections.newest::(cx); - let mut copied_text = String::new(); - let mut spanned_messages = 0; - for message in conversation.messages(cx) { - if message.offset_range.start >= selection.range().end { - break; - } else if message.offset_range.end >= selection.range().start { - let range = cmp::max(message.offset_range.start, selection.range().start) - ..cmp::min(message.offset_range.end, selection.range().end); - if !range.is_empty() { - spanned_messages += 1; - write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); - for chunk in conversation.buffer.read(cx).text_for_range(range) { - copied_text.push_str(&chunk); - } - copied_text.push('\n'); - } - } - } - - if spanned_messages > 1 { - cx.write_to_clipboard(ClipboardItem::new(copied_text)); - return; - } - } - - cx.propagate(); - } - - fn split(&mut self, _: &Split, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - let selections = self.editor.read(cx).selections.disjoint_anchors(); - for selection in selections.into_iter() { - let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); - let range = selection - .map(|endpoint| endpoint.to_offset(&buffer)) - .range(); - conversation.split_message(range, cx); - } - }); - } - - fn save(&mut self, _: &Save, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - conversation.save(None, self.fs.clone(), cx) - }); - } - - fn cycle_model(&mut self, cx: &mut ViewContext) { - self.conversation.update(cx, |conversation, cx| { - let new_model = conversation.model.cycle(); - conversation.set_model(new_model, cx); - }); - } - - fn title(&self, cx: &AppContext) -> String { - self.conversation - .read(cx) - .summary - .as_ref() - .map(|summary| summary.text.clone()) - .unwrap_or_else(|| "New Conversation".into()) - } - - fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { - Button::new( - "current_model", - self.conversation.read(cx).model.short_name(), - ) - .style(ButtonStyle::Filled) - .tooltip(move |cx| Tooltip::text("Change Model", cx)) - .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) - } - - fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { - let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_color = if remaining_tokens <= 0 { - Color::Error - } else if remaining_tokens <= 500 { - Color::Warning - } else { - Color::Default - }; - Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)) - } -} - -impl EventEmitter for ConversationEditor {} - -impl Render for ConversationEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl Element { - div() - .key_context("ConversationEditor") - .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) - .capture_action(cx.listener(ConversationEditor::save)) - .capture_action(cx.listener(ConversationEditor::copy)) - .capture_action(cx.listener(ConversationEditor::cycle_message_role)) - .on_action(cx.listener(ConversationEditor::assist)) - .on_action(cx.listener(ConversationEditor::split)) - .size_full() - .relative() - .child( - div() - .size_full() - .pl_2() - .bg(cx.theme().colors().editor_background) - .child(self.editor.clone()), - ) - .child( - h_stack() - .absolute() - .gap_1() - .top_3() - .right_5() - .child(self.render_current_model(cx)) - .children(self.render_remaining_tokens(cx)), - ) - } -} - -impl FocusableView for ConversationEditor { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -#[derive(Clone, Debug)] -struct MessageAnchor { - id: MessageId, - start: language::Anchor, -} - -#[derive(Clone, Debug)] -pub struct Message { - offset_range: Range, - index_range: Range, - id: MessageId, - anchor: language::Anchor, - role: Role, - sent_at: DateTime, - status: MessageStatus, -} - -impl Message { - fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { - let content = buffer - .text_for_range(self.offset_range.clone()) - .collect::(); - RequestMessage { - role: self.role, - content: content.trim_end().into(), - } - } -} - -enum InlineAssistantEvent { - Confirmed { - prompt: String, - include_conversation: bool, - retrieve_context: bool, - }, - Canceled, - Dismissed, - IncludeConversationToggled { - include_conversation: bool, - }, - RetrieveContextToggled { - retrieve_context: bool, - }, -} - -struct InlineAssistant { - id: usize, - prompt_editor: View, - workspace: WeakView, - confirmed: bool, - include_conversation: bool, - measurements: Rc>, - prompt_history: VecDeque, - prompt_history_ix: Option, - pending_prompt: String, - codegen: Model, - _subscriptions: Vec, - retrieve_context: bool, - semantic_index: Option>, - semantic_permissioned: Option, - project: WeakModel, - maintain_rate_limit: Option>, -} - -impl EventEmitter for InlineAssistant {} - -impl Render for InlineAssistant { - fn render(&mut self, cx: &mut ViewContext) -> impl Element { - let measurements = self.measurements.get(); - h_stack() - .w_full() - .py_2() - .border_y_1() - .border_color(cx.theme().colors().border) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::toggle_include_conversation)) - .on_action(cx.listener(Self::toggle_retrieve_context)) - .on_action(cx.listener(Self::move_up)) - .on_action(cx.listener(Self::move_down)) - .child( - h_stack() - .justify_center() - .w(measurements.gutter_width) - .child( - IconButton::new("include_conversation", Icon::Ai) - .on_click(cx.listener(|this, _, cx| { - this.toggle_include_conversation(&ToggleIncludeConversation, cx) - })) - .selected(self.include_conversation) - .tooltip(|cx| { - Tooltip::for_action( - "Include Conversation", - &ToggleIncludeConversation, - cx, - ) - }), - ) - .children(if SemanticIndex::enabled(cx) { - Some( - IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .on_click(cx.listener(|this, _, cx| { - this.toggle_retrieve_context(&ToggleRetrieveContext, cx) - })) - .selected(self.retrieve_context) - .tooltip(|cx| { - Tooltip::for_action( - "Retrieve Context", - &ToggleRetrieveContext, - cx, - ) - }), - ) - } else { - None - }) - .children(if let Some(error) = self.codegen.read(cx).error() { - let error_message = SharedString::from(error.to_string()); - Some( - div() - .id("error") - .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) - .child(IconElement::new(Icon::XCircle).color(Color::Error)), - ) - } else { - None - }), - ) - .child( - h_stack() - .w_full() - .ml(measurements.anchor_x - measurements.gutter_width) - .child(self.render_prompt_editor(cx)), - ) - .children(if self.retrieve_context { - self.retrieve_context_status(cx) - } else { - None - }) - } -} - -impl FocusableView for InlineAssistant { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.prompt_editor.focus_handle(cx) - } -} - -impl InlineAssistant { - fn new( - id: usize, - measurements: Rc>, - include_conversation: bool, - prompt_history: VecDeque, - codegen: Model, - workspace: WeakView, - cx: &mut ViewContext, - retrieve_context: bool, - semantic_index: Option>, - project: Model, - ) -> Self { - let prompt_editor = cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - let placeholder = match codegen.read(cx).kind() { - CodegenKind::Transform { .. } => "Enter transformation prompt…", - CodegenKind::Generate { .. } => "Enter generation prompt…", - }; - editor.set_placeholder_text(placeholder, cx); - editor - }); - cx.focus_view(&prompt_editor); - - let mut subscriptions = vec![ - cx.observe(&codegen, Self::handle_codegen_changed), - cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), - ]; - - if let Some(semantic_index) = semantic_index.clone() { - subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); - } - - let assistant = Self { - id, - prompt_editor, - workspace, - confirmed: false, - include_conversation, - measurements, - prompt_history, - prompt_history_ix: None, - pending_prompt: String::new(), - codegen, - _subscriptions: subscriptions, - retrieve_context, - semantic_permissioned: None, - semantic_index, - project: project.downgrade(), - maintain_rate_limit: None, - }; - - assistant.index_project(cx).log_err(); - - assistant - } - - fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { - if let Some(value) = self.semantic_permissioned { - return Task::ready(Ok(value)); - } - - let Some(project) = self.project.upgrade() else { - return Task::ready(Err(anyhow!("project was dropped"))); - }; - - self.semantic_index - .as_ref() - .map(|semantic| { - semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) - }) - .unwrap_or(Task::ready(Ok(false))) - } - - fn handle_prompt_editor_events( - &mut self, - _: View, - event: &EditorEvent, - cx: &mut ViewContext, - ) { - if let EditorEvent::Edited = event { - self.pending_prompt = self.prompt_editor.read(cx).text(cx); - cx.notify(); - } - } - - fn semantic_index_changed( - &mut self, - semantic_index: Model, - cx: &mut ViewContext, - ) { - let Some(project) = self.project.upgrade() else { - return; - }; - - let status = semantic_index.read(cx).status(&project); - match status { - SemanticIndexStatus::Indexing { - rate_limit_expiry: Some(_), - .. - } => { - if self.maintain_rate_limit.is_none() { - self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - this.update(&mut cx, |_, cx| cx.notify()).log_err(); - } - })); - } - return; - } - _ => { - self.maintain_rate_limit = None; - } - } - } - - fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { - let is_read_only = !self.codegen.read(cx).idle(); - self.prompt_editor.update(cx, |editor, _cx| { - let was_read_only = editor.read_only(); - if was_read_only != is_read_only { - if is_read_only { - editor.set_read_only(true); - } else { - self.confirmed = false; - editor.set_read_only(false); - } - } - }); - cx.notify(); - } - - 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 { - report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); - - let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor - .update(cx, |editor, _cx| editor.set_read_only(true)); - cx.emit(InlineAssistantEvent::Confirmed { - prompt, - include_conversation: self.include_conversation, - retrieve_context: self.retrieve_context, - }); - self.confirmed = true; - cx.notify(); - } - } - - fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { - let semantic_permissioned = self.semantic_permissioned(cx); - - let Some(project) = self.project.upgrade() else { - return; - }; - - let project_name = project - .read(cx) - .worktree_root_names(cx) - .collect::>() - .join("/"); - let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; - let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, - if is_plural { - "s" - } else {""}); - - cx.spawn(|this, mut cx| async move { - // If Necessary prompt user - if !semantic_permissioned.await.unwrap_or(false) { - let answer = this.update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Info, - prompt_text.as_str(), - &["Continue", "Cancel"], - ) - })?; - - if answer.await? == 0 { - this.update(&mut cx, |this, _| { - this.semantic_permissioned = Some(true); - })?; - } else { - return anyhow::Ok(()); - } - } - - // If permissioned, update context appropriately - this.update(&mut cx, |this, cx| { - this.retrieve_context = !this.retrieve_context; - - cx.emit(InlineAssistantEvent::RetrieveContextToggled { - retrieve_context: this.retrieve_context, - }); - - if this.retrieve_context { - this.index_project(cx).log_err(); - } - - cx.notify(); - })?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { - let Some(project) = self.project.upgrade() else { - return Err(anyhow!("project was dropped!")); - }; - - let semantic_permissioned = self.semantic_permissioned(cx); - if let Some(semantic_index) = SemanticIndex::global(cx) { - cx.spawn(|_, mut cx| async move { - // This has to be updated to accomodate for semantic_permissions - if semantic_permissioned.await.unwrap_or(false) { - semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx))? - .await - } else { - Err(anyhow!("project is not permissioned for semantic indexing")) - } - }) - .detach_and_log_err(cx); - } - - anyhow::Ok(()) - } - - fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { - let Some(project) = self.project.upgrade() else { - return None; - }; - - let semantic_index = SemanticIndex::global(cx)?; - let status = semantic_index.update(cx, |index, _| index.status(&project)); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::NotIndexed {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Indexed", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; - - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } - } - - let status_text = SharedString::from(status_text); - Some( - div() - .id("update") - .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) - .child(IconElement::new(Icon::Update).color(Color::Info)) - .into_any_element() - ) - } - - SemanticIndexStatus::Indexed {} => Some( - div() - .id("check") - .tooltip(|cx| Tooltip::text("Index up to date", cx)) - .child(IconElement::new(Icon::Check).color(Color::Success)) - .into_any_element() - ), - } - } - - fn toggle_include_conversation( - &mut self, - _: &ToggleIncludeConversation, - cx: &mut ViewContext, - ) { - self.include_conversation = !self.include_conversation; - cx.emit(InlineAssistantEvent::IncludeConversationToggled { - include_conversation: self.include_conversation, - }); - cx.notify(); - } - - fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if let Some(ix) = self.prompt_history_ix { - if ix > 0 { - self.prompt_history_ix = Some(ix - 1); - let prompt = self.prompt_history[ix - 1].clone(); - self.set_prompt(&prompt, cx); - } - } else if !self.prompt_history.is_empty() { - self.prompt_history_ix = Some(self.prompt_history.len() - 1); - let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); - self.set_prompt(&prompt, cx); - } - } - - fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - if let Some(ix) = self.prompt_history_ix { - if ix < self.prompt_history.len() - 1 { - self.prompt_history_ix = Some(ix + 1); - let prompt = self.prompt_history[ix + 1].clone(); - self.set_prompt(&prompt, cx); - } else { - self.prompt_history_ix = None; - let pending_prompt = self.pending_prompt.clone(); - self.set_prompt(&pending_prompt, cx); - } - } - } - - fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { - self.prompt_editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - let len = buffer.len(cx); - buffer.edit([(0..len, prompt)], None, cx); - }); - }); - } - - fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if self.prompt_editor.read(cx).read_only() { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features, - font_size: rems(0.875).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.3).into(), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }; - EditorElement::new( - &self.prompt_editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -// This wouldn't need to exist if we could pass parameters when rendering child views. -#[derive(Copy, Clone, Default)] -struct BlockMeasurements { - anchor_x: Pixels, - gutter_width: Pixels, -} - -struct PendingInlineAssist { - editor: WeakView, - inline_assistant: Option<(BlockId, View)>, - codegen: Model, - _subscriptions: Vec, - project: WeakModel, -} - -fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { - ranges.sort_unstable_by(|a, b| { - a.start - .cmp(&b.start, buffer) - .then_with(|| b.end.cmp(&a.end, buffer)) - }); - - let mut ix = 0; - while ix + 1 < ranges.len() { - let b = ranges[ix + 1].clone(); - let a = &mut ranges[ix]; - if a.end.cmp(&b.start, buffer).is_gt() { - if a.end.cmp(&b.end, buffer).is_lt() { - a.end = b.end; - } - ranges.remove(ix + 1); - } else { - ix += 1; - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::MessageId; - use ai::test::FakeCompletionProvider; - use gpui::AppContext; - - #[gpui::test] - fn test_inserting_and_removing_messages(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - let message_2 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..1), - (message_2.id, Role::Assistant, 1..1) - ] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..3) - ] - ); - - let message_3 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_3.id, Role::User, 4..4) - ] - ); - - let message_4 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..5), - (message_3.id, Role::User, 5..5), - ] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..6), - (message_3.id, Role::User, 6..7), - ] - ); - - // Deleting across message boundaries merges the messages. - buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_3.id, Role::User, 3..4), - ] - ); - - // Undoing the deletion should also undo the merge. - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..2), - (message_2.id, Role::Assistant, 2..4), - (message_4.id, Role::User, 4..6), - (message_3.id, Role::User, 6..7), - ] - ); - - // Redoing the deletion should also redo the merge. - buffer.update(cx, |buffer, cx| buffer.redo(cx)); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_3.id, Role::User, 3..4), - ] - ); - - // Ensure we can still insert after a merged message. - let message_5 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..3), - (message_5.id, Role::System, 3..4), - (message_3.id, Role::User, 4..5) - ] - ); - } - - #[gpui::test] - fn test_message_splitting(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - - let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) - }); - - let (_, message_2) = - conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); - let message_2 = message_2.unwrap(); - - // We recycle newlines in the middle of a split message - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..16), - ] - ); - - let (_, message_3) = - conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); - let message_3 = message_3.unwrap(); - - // We don't recycle newlines at the end of a split message - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..17), - ] - ); - - let (_, message_4) = - conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); - let message_4 = message_4.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..17), - ] - ); - - let (_, message_5) = - conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); - let message_5 = message_5.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..10), - (message_5.id, Role::User, 10..18), - ] - ); - - let (message_6, message_7) = conversation.update(cx, |conversation, cx| { - conversation.split_message(14..16, cx) - }); - let message_6 = message_6.unwrap(); - let message_7 = message_7.unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_3.id, Role::User, 4..5), - (message_2.id, Role::User, 5..9), - (message_4.id, Role::User, 9..10), - (message_5.id, Role::User, 10..14), - (message_6.id, Role::User, 14..17), - (message_7.id, Role::User, 17..19), - ] - ); - } - - #[gpui::test] - fn test_messages_for_offsets(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - - let message_1 = conversation.read(cx).message_anchors[0].clone(); - assert_eq!( - messages(&conversation, cx), - vec![(message_1.id, Role::User, 0..0)] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); - let message_2 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); - - let message_3 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); - - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..8), - (message_3.id, Role::User, 8..11) - ] - ); - - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 4, 9], cx), - [message_1.id, message_2.id, message_3.id] - ); - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 1, 11], cx), - [message_1.id, message_3.id] - ); - - let message_4 = conversation - .update(cx, |conversation, cx| { - conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) - }) - .unwrap(); - assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); - assert_eq!( - messages(&conversation, cx), - vec![ - (message_1.id, Role::User, 0..4), - (message_2.id, Role::User, 4..8), - (message_3.id, Role::User, 8..12), - (message_4.id, Role::User, 12..12) - ] - ); - assert_eq!( - message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx), - [message_1.id, message_2.id, message_3.id, message_4.id] - ); - - fn message_ids_for_offsets( - conversation: &Model, - offsets: &[usize], - cx: &AppContext, - ) -> Vec { - conversation - .read(cx) - .messages_for_offsets(offsets.iter().copied(), cx) - .into_iter() - .map(|message| message.id) - .collect() - } - } - - #[gpui::test] - fn test_serialization(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init(cx); - let registry = Arc::new(LanguageRegistry::test()); - let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = - cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); - let buffer = conversation.read(cx).buffer.clone(); - let message_0 = conversation.read(cx).message_anchors[0].id; - let message_1 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) - .unwrap() - }); - let message_2 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); - buffer.finalize_last_transaction(); - }); - let _message_3 = conversation.update(cx, |conversation, cx| { - conversation - .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - assert_eq!(buffer.read(cx).text(), "a\nb\nc\n"); - assert_eq!( - messages(&conversation, cx), - [ - (message_0, Role::User, 0..2), - (message_1.id, Role::Assistant, 2..6), - (message_2.id, Role::System, 6..6), - ] - ); - - let deserialized_conversation = cx.new_model(|cx| { - Conversation::deserialize( - conversation.read(cx).serialize(cx), - Default::default(), - registry.clone(), - cx, - ) - }); - let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone(); - assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n"); - assert_eq!( - messages(&deserialized_conversation, cx), - [ - (message_0, Role::User, 0..2), - (message_1.id, Role::Assistant, 2..6), - (message_2.id, Role::System, 6..6), - ] - ); - } - - fn messages( - conversation: &Model, - cx: &AppContext, - ) -> Vec<(MessageId, Role, Range)> { - conversation - .read(cx) - .messages(cx) - .map(|message| (message.id, message.role, message.offset_range)) - .collect() - } -} - -fn report_assistant_event( - workspace: WeakView, - conversation_id: Option, - assistant_kind: AssistantKind, - cx: &AppContext, -) { - let Some(workspace) = workspace.upgrade() else { - return; - }; - - let client = workspace.read(cx).project().read(cx).client(); - let telemetry = client.telemetry(); - - let model = AssistantSettings::get_global(cx) - .default_open_ai_model - .clone(); - - let telemetry_settings = TelemetrySettings::get_global(cx).clone(); - - telemetry.report_assistant_event( - telemetry_settings, - conversation_id, - assistant_kind, - model.full_name(), - ) -} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs deleted file mode 100644 index c0fbc74e9a..0000000000 --- a/crates/assistant2/src/assistant_settings.rs +++ /dev/null @@ -1,81 +0,0 @@ -use anyhow; -use gpui::Pixels; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] -pub enum OpenAIModel { - #[serde(rename = "gpt-3.5-turbo-0613")] - ThreePointFiveTurbo, - #[serde(rename = "gpt-4-0613")] - Four, - #[serde(rename = "gpt-4-1106-preview")] - FourTurbo, -} - -impl OpenAIModel { - pub fn full_name(&self) -> &'static str { - match self { - OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", - OpenAIModel::Four => "gpt-4-0613", - OpenAIModel::FourTurbo => "gpt-4-1106-preview", - } - } - - pub fn short_name(&self) -> &'static str { - match self { - OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", - OpenAIModel::Four => "gpt-4", - OpenAIModel::FourTurbo => "gpt-4-turbo", - } - } - - pub fn cycle(&self) -> Self { - match self { - OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, - OpenAIModel::Four => OpenAIModel::FourTurbo, - OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AssistantDockPosition { - Left, - Right, - Bottom, -} - -#[derive(Deserialize, Debug)] -pub struct AssistantSettings { - pub button: bool, - pub dock: AssistantDockPosition, - pub default_width: Pixels, - pub default_height: Pixels, - pub default_open_ai_model: OpenAIModel, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct AssistantSettingsContent { - pub button: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, - pub default_open_ai_model: Option, -} - -impl Settings for AssistantSettings { - const KEY: Option<&'static str> = Some("assistant"); - - type FileContent = AssistantSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut gpui::AppContext, - ) -> anyhow::Result { - Self::load_via_json_merge(default_value, user_values) - } -} diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs deleted file mode 100644 index 8af3010f40..0000000000 --- a/crates/assistant2/src/codegen.rs +++ /dev/null @@ -1,688 +0,0 @@ -use crate::streaming_diff::{Hunk, StreamingDiff}; -use ai::completion::{CompletionProvider, CompletionRequest}; -use anyhow::Result; -use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; -use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; -use gpui::{EventEmitter, Model, ModelContext, Task}; -use language::{Rope, TransactionId}; -use multi_buffer; -use std::{cmp, future, ops::Range, sync::Arc}; - -pub enum Event { - Finished, - Undone, -} - -#[derive(Clone)] -pub enum CodegenKind { - Transform { range: Range }, - Generate { position: Anchor }, -} - -pub struct Codegen { - provider: Arc, - buffer: Model, - snapshot: MultiBufferSnapshot, - kind: CodegenKind, - last_equal_ranges: Vec>, - transaction_id: Option, - error: Option, - generation: Task<()>, - idle: bool, - _subscription: gpui::Subscription, -} - -impl EventEmitter for Codegen {} - -impl Codegen { - pub fn new( - buffer: Model, - kind: CodegenKind, - provider: Arc, - cx: &mut ModelContext, - ) -> Self { - let snapshot = buffer.read(cx).snapshot(cx); - Self { - provider, - buffer: buffer.clone(), - snapshot, - kind, - last_equal_ranges: Default::default(), - transaction_id: Default::default(), - error: Default::default(), - idle: true, - generation: Task::ready(()), - _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), - } - } - - fn handle_buffer_event( - &mut self, - _buffer: Model, - event: &multi_buffer::Event, - cx: &mut ModelContext, - ) { - if let multi_buffer::Event::TransactionUndone { transaction_id } = event { - if self.transaction_id == Some(*transaction_id) { - self.transaction_id = None; - self.generation = Task::ready(()); - cx.emit(Event::Undone); - } - } - } - - pub fn range(&self) -> Range { - match &self.kind { - CodegenKind::Transform { range } => range.clone(), - CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, - } - } - - pub fn kind(&self) -> &CodegenKind { - &self.kind - } - - pub fn last_equal_ranges(&self) -> &[Range] { - &self.last_equal_ranges - } - - pub fn idle(&self) -> bool { - self.idle - } - - pub fn error(&self) -> Option<&anyhow::Error> { - self.error.as_ref() - } - - pub fn start(&mut self, prompt: Box, cx: &mut ModelContext) { - let range = self.range(); - let snapshot = self.snapshot.clone(); - let selected_text = snapshot - .text_for_range(range.start..range.end) - .collect::(); - - let selection_start = range.start.to_point(&snapshot); - let suggested_line_indent = snapshot - .suggested_indents(selection_start.row..selection_start.row + 1, cx) - .into_values() - .next() - .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); - - let response = self.provider.complete(prompt); - self.generation = cx.spawn(|this, mut cx| { - async move { - let generate = async { - let mut edit_start = range.start.to_offset(&snapshot); - - let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background_executor().spawn(async move { - let chunks = strip_invalid_spans_from_codeblock(response.await?); - futures::pin_mut!(chunks); - let mut diff = StreamingDiff::new(selected_text.to_string()); - - let mut new_text = String::new(); - let mut base_indent = None; - let mut line_indent = None; - let mut first_line = true; - - while let Some(chunk) = chunks.next().await { - let chunk = chunk?; - - let mut lines = chunk.split('\n').peekable(); - while let Some(line) = lines.next() { - new_text.push_str(line); - if line_indent.is_none() { - if let Some(non_whitespace_ch_ix) = - new_text.find(|ch: char| !ch.is_whitespace()) - { - line_indent = Some(non_whitespace_ch_ix); - base_indent = base_indent.or(line_indent); - - let line_indent = line_indent.unwrap(); - let base_indent = base_indent.unwrap(); - let indent_delta = line_indent as i32 - base_indent as i32; - let mut corrected_indent_len = cmp::max( - 0, - suggested_line_indent.len as i32 + indent_delta, - ) - as usize; - if first_line { - corrected_indent_len = corrected_indent_len - .saturating_sub(selection_start.column as usize); - } - - let indent_char = suggested_line_indent.char(); - let mut indent_buffer = [0; 4]; - let indent_str = - indent_char.encode_utf8(&mut indent_buffer); - new_text.replace_range( - ..line_indent, - &indent_str.repeat(corrected_indent_len), - ); - } - } - - if line_indent.is_some() { - hunks_tx.send(diff.push_new(&new_text)).await?; - new_text.clear(); - } - - if lines.peek().is_some() { - hunks_tx.send(diff.push_new("\n")).await?; - line_indent = None; - first_line = false; - } - } - } - hunks_tx.send(diff.push_new(&new_text)).await?; - hunks_tx.send(diff.finish()).await?; - - anyhow::Ok(()) - }); - - while let Some(hunks) = hunks_rx.next().await { - this.update(&mut cx, |this, cx| { - this.last_equal_ranges.clear(); - - let transaction = this.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 = edit_end; - this.last_equal_ranges.push(edit_range); - None - } - }), - None, - cx, - ); - - buffer.end_transaction(cx) - }); - - if let Some(transaction) = transaction { - if let Some(first_transaction) = this.transaction_id { - // Group all assistant edits into the first transaction. - this.buffer.update(cx, |buffer, cx| { - buffer.merge_transactions( - transaction, - first_transaction, - cx, - ) - }); - } else { - this.transaction_id = Some(transaction); - this.buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx) - }); - } - } - - cx.notify(); - })?; - } - - diff.await?; - anyhow::Ok(()) - }; - - let result = generate.await; - this.update(&mut cx, |this, cx| { - this.last_equal_ranges.clear(); - this.idle = true; - if let Err(error) = result { - this.error = Some(error); - } - cx.emit(Event::Finished); - cx.notify(); - }) - .ok(); - } - }); - self.error.take(); - self.idle = false; - cx.notify(); - } - - pub fn undo(&mut self, cx: &mut ModelContext) { - if let Some(transaction_id) = self.transaction_id { - self.buffer - .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } - } -} - -fn strip_invalid_spans_from_codeblock( - stream: impl Stream>, -) -> impl Stream> { - let mut first_line = true; - let mut buffer = String::new(); - let mut starts_with_markdown_codeblock = false; - let mut includes_start_or_end_span = false; - stream.filter_map(move |chunk| { - let chunk = match chunk { - Ok(chunk) => chunk, - Err(err) => return future::ready(Some(Err(err))), - }; - buffer.push_str(&chunk); - - if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") { - includes_start_or_end_span = true; - - buffer = buffer - .strip_prefix("<|S|>") - .or_else(|| buffer.strip_prefix("<|S|")) - .unwrap_or(&buffer) - .to_string(); - } else if buffer.ends_with("|E|>") { - includes_start_or_end_span = true; - } else if buffer.starts_with("<|") - || buffer.starts_with("<|S") - || buffer.starts_with("<|S|") - || buffer.ends_with("|") - || buffer.ends_with("|E") - || buffer.ends_with("|E|") - { - return future::ready(None); - } - - if first_line { - if buffer == "" || buffer == "`" || buffer == "``" { - return future::ready(None); - } else if buffer.starts_with("```") { - starts_with_markdown_codeblock = true; - if let Some(newline_ix) = buffer.find('\n') { - buffer.replace_range(..newline_ix + 1, ""); - first_line = false; - } else { - return future::ready(None); - } - } - } - - let mut text = buffer.to_string(); - if starts_with_markdown_codeblock { - text = text - .strip_suffix("\n```\n") - .or_else(|| text.strip_suffix("\n```")) - .or_else(|| text.strip_suffix("\n``")) - .or_else(|| text.strip_suffix("\n`")) - .or_else(|| text.strip_suffix('\n')) - .unwrap_or(&text) - .to_string(); - } - - if includes_start_or_end_span { - text = text - .strip_suffix("|E|>") - .or_else(|| text.strip_suffix("E|>")) - .or_else(|| text.strip_prefix("|>")) - .or_else(|| text.strip_prefix(">")) - .unwrap_or(&text) - .to_string(); - }; - - if text.contains('\n') { - first_line = false; - } - - let remainder = buffer.split_off(text.len()); - let result = if buffer.is_empty() { - None - } else { - Some(Ok(buffer.clone())) - }; - - buffer = remainder; - future::ready(result) - }) -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use super::*; - use ai::test::FakeCompletionProvider; - use futures::stream::{self}; - use gpui::{Context, TestAppContext}; - use indoc::indoc; - use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; - use rand::prelude::*; - use serde::Serialize; - use settings::SettingsStore; - - #[derive(Serialize)] - pub struct DummyCompletionRequest { - pub name: String, - } - - impl CompletionRequest for DummyCompletionRequest { - fn data(&self) -> serde_json::Result { - serde_json::to_string(self) - } - } - - #[gpui::test(iterations = 10)] - async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = indoc! {" - fn main() { - let x = 0; - for _ in 0..10 { - x += 1; - } - } - "}; - let buffer = - cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let range = buffer.read_with(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) - }); - let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.new_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Transform { range }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); - - let mut new_text = concat!( - " let mut x = 0;\n", - " while x < 10 {\n", - " x += 1;\n", - " }", - ); - while !new_text.is_empty() { - let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); - let (chunk, suffix) = new_text.split_at(len); - println!("CHUNK: {:?}", &chunk); - provider.send_completion(chunk); - new_text = suffix; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.run_until_parked(); - - assert_eq!( - buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), - indoc! {" - fn main() { - let mut x = 0; - while x < 10 { - x += 1; - } - } - "} - ); - } - - #[gpui::test(iterations = 10)] - async fn test_autoindent_when_generating_past_indentation( - cx: &mut TestAppContext, - mut rng: StdRng, - ) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = indoc! {" - fn main() { - le - } - "}; - let buffer = - cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let position = buffer.read_with(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 6)) - }); - let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.new_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Generate { position }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); - - let mut new_text = concat!( - "t mut x = 0;\n", - "while x < 10 {\n", - " x += 1;\n", - "}", // - ); - while !new_text.is_empty() { - let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); - let (chunk, suffix) = new_text.split_at(len); - provider.send_completion(chunk); - new_text = suffix; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.run_until_parked(); - - assert_eq!( - buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), - indoc! {" - fn main() { - let mut x = 0; - while x < 10 { - x += 1; - } - } - "} - ); - } - - #[gpui::test(iterations = 10)] - async fn test_autoindent_when_generating_before_indentation( - cx: &mut TestAppContext, - mut rng: StdRng, - ) { - cx.set_global(cx.update(SettingsStore::test)); - cx.update(language_settings::init); - - let text = concat!( - "fn main() {\n", - " \n", - "}\n" // - ); - let buffer = - cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let position = buffer.read_with(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - snapshot.anchor_before(Point::new(1, 2)) - }); - let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.new_model(|cx| { - Codegen::new( - buffer.clone(), - CodegenKind::Generate { position }, - provider.clone(), - cx, - ) - }); - - let request = Box::new(DummyCompletionRequest { - name: "test".to_string(), - }); - codegen.update(cx, |codegen, cx| codegen.start(request, cx)); - - let mut new_text = concat!( - "let mut x = 0;\n", - "while x < 10 {\n", - " x += 1;\n", - "}", // - ); - while !new_text.is_empty() { - let max_len = cmp::min(new_text.len(), 10); - let len = rng.gen_range(1..=max_len); - let (chunk, suffix) = new_text.split_at(len); - println!("{:?}", &chunk); - provider.send_completion(chunk); - new_text = suffix; - cx.background_executor.run_until_parked(); - } - provider.finish_completion(); - cx.background_executor.run_until_parked(); - - assert_eq!( - buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), - indoc! {" - fn main() { - let mut x = 0; - while x < 10 { - x += 1; - } - } - "} - ); - } - - #[gpui::test] - async fn test_strip_invalid_spans_from_codeblock() { - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum dolor" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks( - "```html\n```js\nLorem ipsum dolor\n```\n```", - 2 - )) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "```js\nLorem ipsum dolor\n```" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "``\nLorem ipsum dolor\n```" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - assert_eq!( - strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2)) - .map(|chunk| chunk.unwrap()) - .collect::() - .await, - "Lorem ipsum" - ); - fn chunks(text: &str, size: usize) -> impl Stream> { - stream::iter( - text.chars() - .collect::>() - .chunks(size) - .map(|chunk| Ok(chunk.iter().collect::())) - .collect::>(), - ) - } - } - - fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query( - r#" - (call_expression) @indent - (field_expression) @indent - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - } -} diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs deleted file mode 100644 index c88e257295..0000000000 --- a/crates/assistant2/src/prompts.rs +++ /dev/null @@ -1,389 +0,0 @@ -use ai::models::LanguageModel; -use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate}; -use ai::prompts::file_context::FileContext; -use ai::prompts::generate::GenerateInlineContent; -use ai::prompts::preamble::EngineerPreamble; -use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext}; -use ai::providers::open_ai::OpenAILanguageModel; -use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; -use std::cmp::{self, Reverse}; -use std::ops::Range; -use std::sync::Arc; - -#[allow(dead_code)] -fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { - #[derive(Debug)] - struct Match { - collapse: Range, - keep: Vec>, - } - - let selected_range = selected_range.to_offset(buffer); - let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { - Some(&grammar.embedding_config.as_ref()?.query) - }); - let configs = ts_matches - .grammars() - .iter() - .map(|g| g.embedding_config.as_ref().unwrap()) - .collect::>(); - let mut matches = Vec::new(); - while let Some(mat) = ts_matches.peek() { - let config = &configs[mat.grammar_index]; - if let Some(collapse) = mat.captures.iter().find_map(|cap| { - if Some(cap.index) == config.collapse_capture_ix { - Some(cap.node.byte_range()) - } else { - None - } - }) { - let mut keep = Vec::new(); - for capture in mat.captures.iter() { - if Some(capture.index) == config.keep_capture_ix { - keep.push(capture.node.byte_range()); - } else { - continue; - } - } - ts_matches.advance(); - matches.push(Match { collapse, keep }); - } else { - ts_matches.advance(); - } - } - matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); - let mut matches = matches.into_iter().peekable(); - - let mut summary = String::new(); - let mut offset = 0; - let mut flushed_selection = false; - while let Some(mat) = matches.next() { - // Keep extending the collapsed range if the next match surrounds - // the current one. - while let Some(next_mat) = matches.peek() { - if mat.collapse.start <= next_mat.collapse.start - && mat.collapse.end >= next_mat.collapse.end - { - matches.next().unwrap(); - } else { - break; - } - } - - if offset > mat.collapse.start { - // Skip collapsed nodes that have already been summarized. - offset = cmp::max(offset, mat.collapse.end); - continue; - } - - if offset <= selected_range.start && selected_range.start <= mat.collapse.end { - if !flushed_selection { - // The collapsed node ends after the selection starts, so we'll flush the selection first. - summary.extend(buffer.text_for_range(offset..selected_range.start)); - summary.push_str("<|S|"); - if selected_range.end == selected_range.start { - summary.push_str(">"); - } else { - summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|E|>"); - } - offset = selected_range.end; - flushed_selection = true; - } - - // If the selection intersects the collapsed node, we won't collapse it. - if selected_range.end >= mat.collapse.start { - continue; - } - } - - summary.extend(buffer.text_for_range(offset..mat.collapse.start)); - for keep in mat.keep { - summary.extend(buffer.text_for_range(keep)); - } - offset = mat.collapse.end; - } - - // Flush selection if we haven't already done so. - if !flushed_selection && offset <= selected_range.start { - summary.extend(buffer.text_for_range(offset..selected_range.start)); - summary.push_str("<|S|"); - if selected_range.end == selected_range.start { - summary.push_str(">"); - } else { - summary.extend(buffer.text_for_range(selected_range.clone())); - summary.push_str("|E|>"); - } - offset = selected_range.end; - } - - summary.extend(buffer.text_for_range(offset..buffer.len())); - summary -} - -pub fn generate_content_prompt( - user_prompt: String, - language_name: Option<&str>, - buffer: BufferSnapshot, - range: Range, - search_results: Vec, - model: &str, - project_name: Option, -) -> anyhow::Result { - // Using new Prompt Templates - let openai_model: Arc = Arc::new(OpenAILanguageModel::load(model)); - let lang_name = if let Some(language_name) = language_name { - Some(language_name.to_string()) - } else { - None - }; - - let args = PromptArguments { - model: openai_model, - language_name: lang_name.clone(), - project_name, - snippets: search_results.clone(), - reserved_tokens: 1000, - buffer: Some(buffer), - selected_range: Some(range), - user_prompt: Some(user_prompt.clone()), - }; - - let templates: Vec<(PromptPriority, Box)> = vec![ - (PromptPriority::Mandatory, Box::new(EngineerPreamble {})), - ( - PromptPriority::Ordered { order: 1 }, - Box::new(RepositoryContext {}), - ), - ( - PromptPriority::Ordered { order: 0 }, - Box::new(FileContext {}), - ), - ( - PromptPriority::Mandatory, - Box::new(GenerateInlineContent {}), - ), - ]; - let chain = PromptChain::new(args, templates); - let (prompt, _) = chain.generate(true)?; - - anyhow::Ok(prompt) -} - -#[cfg(test)] -pub(crate) mod tests { - - use super::*; - use std::sync::Arc; - - use gpui::{AppContext, Context}; - use indoc::indoc; - use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; - use settings::SettingsStore; - - pub(crate) fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_embedding_query( - r#" - ( - [(line_comment) (attribute_item)]* @context - . - [ - (struct_item - name: (_) @name) - - (enum_item - name: (_) @name) - - (impl_item - trait: (_)? @name - "for"? @name - type: (_) @name) - - (trait_item - name: (_) @name) - - (function_item - name: (_) @name - body: (block - "{" @keep - "}" @keep) @collapse) - - (macro_definition - name: (_) @name) - ] @item - ) - "#, - ) - .unwrap() - } - - #[gpui::test] - fn test_outline_for_prompt(cx: &mut AppContext) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language_settings::init(cx); - let text = indoc! {" - struct X { - a: usize, - b: usize, - } - - impl X { - - fn new() -> Self { - let a = 1; - let b = 2; - Self { a, b } - } - - pub fn a(&self, param: bool) -> usize { - self.a - } - - pub fn b(&self) -> usize { - self.b - } - } - "}; - let buffer = - cx.new_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let snapshot = buffer.read(cx).snapshot(); - - assert_eq!( - summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), - indoc! {" - struct X { - <|S|>a: usize, - b: usize, - } - - impl X { - - fn new() -> Self {} - - pub fn a(&self, param: bool) -> usize {} - - pub fn b(&self) -> usize {} - } - "} - ); - - assert_eq!( - summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)), - indoc! {" - struct X { - a: usize, - b: usize, - } - - impl X { - - fn new() -> Self { - let <|S|a |E|>= 1; - let b = 2; - Self { a, b } - } - - pub fn a(&self, param: bool) -> usize {} - - pub fn b(&self) -> usize {} - } - "} - ); - - assert_eq!( - summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)), - indoc! {" - struct X { - a: usize, - b: usize, - } - - impl X { - <|S|> - fn new() -> Self {} - - pub fn a(&self, param: bool) -> usize {} - - pub fn b(&self) -> usize {} - } - "} - ); - - assert_eq!( - summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)), - indoc! {" - struct X { - a: usize, - b: usize, - } - - impl X { - - fn new() -> Self {} - - pub fn a(&self, param: bool) -> usize {} - - pub fn b(&self) -> usize {} - } - <|S|>"} - ); - - // Ensure nested functions get collapsed properly. - let text = indoc! {" - struct X { - a: usize, - b: usize, - } - - impl X { - - fn new() -> Self { - let a = 1; - let b = 2; - Self { a, b } - } - - pub fn a(&self, param: bool) -> usize { - let a = 30; - fn nested() -> usize { - 3 - } - self.a + nested() - } - - pub fn b(&self) -> usize { - self.b - } - } - "}; - buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); - let snapshot = buffer.read(cx).snapshot(); - assert_eq!( - summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), - indoc! {" - <|S|>struct X { - a: usize, - b: usize, - } - - impl X { - - fn new() -> Self {} - - pub fn a(&self, param: bool) -> usize {} - - pub fn b(&self) -> usize {} - } - "} - ); - } -} diff --git a/crates/assistant2/src/streaming_diff.rs b/crates/assistant2/src/streaming_diff.rs deleted file mode 100644 index 7399a7b4fa..0000000000 --- a/crates/assistant2/src/streaming_diff.rs +++ /dev/null @@ -1,293 +0,0 @@ -use collections::HashMap; -use ordered_float::OrderedFloat; -use std::{ - cmp, - fmt::{self, Debug}, - ops::Range, -}; - -struct Matrix { - cells: Vec, - rows: usize, - cols: usize, -} - -impl Matrix { - fn new() -> Self { - Self { - cells: Vec::new(), - rows: 0, - cols: 0, - } - } - - fn resize(&mut self, rows: usize, cols: usize) { - self.cells.resize(rows * cols, 0.); - self.rows = rows; - self.cols = cols; - } - - fn get(&self, row: usize, col: usize) -> f64 { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("col out of bounds") - } - self.cells[col * self.rows + row] - } - - fn set(&mut self, row: usize, col: usize, value: f64) { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("col out of bounds") - } - - self.cells[col * self.rows + row] = value; - } -} - -impl Debug for Matrix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - for i in 0..self.rows { - for j in 0..self.cols { - write!(f, "{:5}", self.get(i, j))?; - } - writeln!(f)?; - } - Ok(()) - } -} - -#[derive(Debug)] -pub enum Hunk { - Insert { text: String }, - Remove { len: usize }, - Keep { len: usize }, -} - -pub struct StreamingDiff { - old: Vec, - new: Vec, - scores: Matrix, - old_text_ix: usize, - new_text_ix: usize, - equal_runs: HashMap<(usize, usize), u32>, -} - -impl StreamingDiff { - const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -20.; - const EQUALITY_BASE: f64 = 1.8; - const MAX_EQUALITY_EXPONENT: i32 = 16; - - pub fn new(old: String) -> Self { - let old = old.chars().collect::>(); - let mut scores = Matrix::new(); - scores.resize(old.len() + 1, 1); - for i in 0..=old.len() { - scores.set(i, 0, i as f64 * Self::DELETION_SCORE); - } - Self { - old, - new: Vec::new(), - scores, - old_text_ix: 0, - new_text_ix: 0, - equal_runs: Default::default(), - } - } - - pub fn push_new(&mut self, text: &str) -> Vec { - self.new.extend(text.chars()); - self.scores.resize(self.old.len() + 1, self.new.len() + 1); - - for j in self.new_text_ix + 1..=self.new.len() { - self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE); - for i in 1..=self.old.len() { - let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE; - let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE; - let equality_score = if self.old[i - 1] == self.new[j - 1] { - let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); - equal_run += 1; - self.equal_runs.insert((i, j), equal_run); - - 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 - }; - - let score = insertion_score.max(deletion_score).max(equality_score); - self.scores.set(i, j, score); - } - } - - let mut max_score = f64::NEG_INFINITY; - let mut next_old_text_ix = self.old_text_ix; - let next_new_text_ix = self.new.len(); - for i in self.old_text_ix..=self.old.len() { - let score = self.scores.get(i, next_new_text_ix); - if score > max_score { - max_score = score; - next_old_text_ix = i; - } - } - - let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); - self.old_text_ix = next_old_text_ix; - self.new_text_ix = next_new_text_ix; - hunks - } - - fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { - let mut pending_insert: Option> = None; - let mut hunks = Vec::new(); - let mut i = old_text_ix; - let mut j = new_text_ix; - while (i, j) != (self.old_text_ix, self.new_text_ix) { - let insertion_score = if j > self.new_text_ix { - Some((i, j - 1)) - } else { - None - }; - let deletion_score = if i > self.old_text_ix { - Some((i - 1, j)) - } else { - None - }; - let equality_score = if i > self.old_text_ix && j > self.new_text_ix { - if self.old[i - 1] == self.new[j - 1] { - Some((i - 1, j - 1)) - } else { - None - } - } else { - None - }; - - let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] - .iter() - .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) - .unwrap() - .unwrap(); - - if prev_i == i && prev_j == j - 1 { - if let Some(pending_insert) = pending_insert.as_mut() { - pending_insert.start = prev_j; - } else { - pending_insert = Some(prev_j..j); - } - } else { - if let Some(range) = pending_insert.take() { - hunks.push(Hunk::Insert { - text: self.new[range].iter().collect(), - }); - } - - let char_len = self.old[i - 1].len_utf8(); - if prev_i == i - 1 && prev_j == j { - if let Some(Hunk::Remove { len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(Hunk::Remove { len: char_len }) - } - } else { - if let Some(Hunk::Keep { len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(Hunk::Keep { len: char_len }) - } - } - } - - i = prev_i; - j = prev_j; - } - - if let Some(range) = pending_insert.take() { - hunks.push(Hunk::Insert { - text: self.new[range].iter().collect(), - }); - } - - hunks.reverse(); - hunks - } - - pub fn finish(self) -> Vec { - self.backtrack(self.old.len(), self.new.len()) - } -} - -#[cfg(test)] -mod tests { - use std::env; - - use super::*; - use rand::prelude::*; - - #[gpui::test(iterations = 100)] - fn test_random_diffs(mut rng: StdRng) { - let old_text_len = env::var("OLD_TEXT_LEN") - .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) - .unwrap_or(10); - let new_text_len = env::var("NEW_TEXT_LEN") - .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) - .unwrap_or(10); - - let old = util::RandomCharIter::new(&mut rng) - .take(old_text_len) - .collect::(); - log::info!("old text: {:?}", old); - - let mut diff = StreamingDiff::new(old.clone()); - let mut hunks = Vec::new(); - let mut new_len = 0; - let mut new = String::new(); - while new_len < new_text_len { - let new_chunk_len = rng.gen_range(1..=new_text_len - new_len); - let new_chunk = util::RandomCharIter::new(&mut rng) - .take(new_len) - .collect::(); - log::info!("new chunk: {:?}", new_chunk); - new_len += new_chunk_len; - new.push_str(&new_chunk); - let new_hunks = diff.push_new(&new_chunk); - log::info!("hunks: {:?}", new_hunks); - hunks.extend(new_hunks); - } - let final_hunks = diff.finish(); - log::info!("final hunks: {:?}", final_hunks); - hunks.extend(final_hunks); - - log::info!("new text: {:?}", new); - let mut old_ix = 0; - let mut new_ix = 0; - let mut patched = String::new(); - for hunk in hunks { - match hunk { - Hunk::Keep { len } => { - assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]); - patched.push_str(&old[old_ix..old_ix + len]); - old_ix += len; - new_ix += len; - } - Hunk::Remove { len } => { - old_ix += len; - } - Hunk::Insert { text } => { - assert_eq!(text, &new[new_ix..new_ix + text.len()]); - patched.push_str(&text); - new_ix += text.len(); - } - } - } - assert_eq!(patched, new); - } -} diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index b7cbc62559..4491d552a8 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -10,10 +10,10 @@ doctest = false [dependencies] editor = { path = "../editor" } -gpui = { path = "../gpui" } +gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } workspace = { path = "../workspace" } -settings = { path = "../settings" } +settings2 = { path = "../settings2" } anyhow.workspace = true chrono = "0.4" diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 99fe997dc5..1db1846efb 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,15 +1,15 @@ use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use gpui::{actions, AppContext}; +use gpui::{actions, AppContext, ViewContext}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use settings2::Settings; use std::{ fs::OpenOptions, path::{Path, PathBuf}, sync::Arc, }; -use workspace::AppState; +use workspace::{AppState, Workspace}; actions!(journal, [NewJournalEntry]); @@ -36,24 +36,35 @@ pub enum HourFormat { Hour24, } -impl settings::Setting for JournalSettings { +impl settings2::Settings for JournalSettings { const KEY: Option<&'static str> = Some("journal"); type FileContent = Self; - fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { - Self::load_via_json_merge(default_value, user_values) + fn load( + defaults: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut AppContext, + ) -> Result { + Self::load_via_json_merge(defaults, user_values) } } -pub fn init(app_state: Arc, cx: &mut AppContext) { - settings::register::(cx); +pub fn init(_: Arc, cx: &mut AppContext) { + JournalSettings::register(cx); - cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx)); + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(|workspace, _: &NewJournalEntry, cx| { + new_journal_entry(workspace.app_state().clone(), cx); + }); + }, + ) + .detach(); } pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { - let settings = settings::get::(cx); + let settings = JournalSettings::get_global(cx); let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) { Some(journal_dir) => journal_dir, None => { @@ -68,9 +79,9 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { .join(format!("{:02}", now.month())); let entry_path = month_dir.join(format!("{:02}.md", now.day())); let now = now.time(); - let entry_heading = heading_entry(now, &settings.hour_format); + let _entry_heading = heading_entry(now, &settings.hour_format); - let create_entry = cx.background().spawn(async move { + let create_entry = cx.background_executor().spawn(async move { std::fs::create_dir_all(month_dir)?; OpenOptions::new() .create(true) @@ -82,30 +93,31 @@ pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { cx.spawn(|mut cx| async move { let (journal_dir, entry_path) = create_entry.await?; let (workspace, _) = cx - .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx)) + .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))? .await?; - let opened = workspace + let _opened = workspace .update(&mut cx, |workspace, cx| { workspace.open_paths(vec![entry_path], true, cx) })? .await; - if let Some(Some(Ok(item))) = opened.first() { - if let Some(editor) = item.downcast::().map(|editor| editor.downgrade()) { - editor.update(&mut cx, |editor, cx| { - let len = editor.buffer().read(cx).len(cx); - editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([len..len]) - }); - if len > 0 { - editor.insert("\n\n", cx); - } - editor.insert(&entry_heading, cx); - editor.insert("\n\n", cx); - })?; - } - } + // todo!("editor") + // if let Some(Some(Ok(item))) = opened.first() { + // if let Some(editor) = item.downcast::().map(|editor| editor.downgrade()) { + // editor.update(&mut cx, |editor, cx| { + // let len = editor.buffer().read(cx).len(cx); + // editor.change_selections(Some(Autoscroll::center()), cx, |s| { + // s.select_ranges([len..len]) + // }); + // if len > 0 { + // editor.insert("\n\n", cx); + // } + // editor.insert(&entry_heading, cx); + // editor.insert("\n\n", cx); + // })?; + // } + // } anyhow::Ok(()) }) diff --git a/crates/journal2/Cargo.toml b/crates/journal2/Cargo.toml deleted file mode 100644 index f43d90fc85..0000000000 --- a/crates/journal2/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "journal2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/journal2.rs" -doctest = false - -[dependencies] -editor = { path = "../editor" } -gpui = { package = "gpui2", path = "../gpui2" } -util = { path = "../util" } -workspace = { path = "../workspace" } -settings2 = { path = "../settings2" } - -anyhow.workspace = true -chrono = "0.4" -dirs = "4.0" -serde.workspace = true -schemars.workspace = true -log.workspace = true -shellexpand = "2.1.0" - -[dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/journal2/src/journal2.rs b/crates/journal2/src/journal2.rs deleted file mode 100644 index 1db1846efb..0000000000 --- a/crates/journal2/src/journal2.rs +++ /dev/null @@ -1,181 +0,0 @@ -use anyhow::Result; -use chrono::{Datelike, Local, NaiveTime, Timelike}; -use gpui::{actions, AppContext, ViewContext}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings2::Settings; -use std::{ - fs::OpenOptions, - path::{Path, PathBuf}, - sync::Arc, -}; -use workspace::{AppState, Workspace}; - -actions!(journal, [NewJournalEntry]); - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct JournalSettings { - pub path: Option, - pub hour_format: Option, -} - -impl Default for JournalSettings { - fn default() -> Self { - Self { - path: Some("~".into()), - hour_format: Some(Default::default()), - } - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HourFormat { - #[default] - Hour12, - Hour24, -} - -impl settings2::Settings for JournalSettings { - const KEY: Option<&'static str> = Some("journal"); - - type FileContent = Self; - - fn load( - defaults: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut AppContext, - ) -> Result { - Self::load_via_json_merge(defaults, user_values) - } -} - -pub fn init(_: Arc, cx: &mut AppContext) { - JournalSettings::register(cx); - - cx.observe_new_views( - |workspace: &mut Workspace, _cx: &mut ViewContext| { - workspace.register_action(|workspace, _: &NewJournalEntry, cx| { - new_journal_entry(workspace.app_state().clone(), cx); - }); - }, - ) - .detach(); -} - -pub fn new_journal_entry(app_state: Arc, cx: &mut AppContext) { - let settings = JournalSettings::get_global(cx); - let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) { - Some(journal_dir) => journal_dir, - None => { - log::error!("Can't determine journal directory"); - return; - } - }; - - let now = Local::now(); - let month_dir = journal_dir - .join(format!("{:02}", now.year())) - .join(format!("{:02}", now.month())); - let entry_path = month_dir.join(format!("{:02}.md", now.day())); - let now = now.time(); - let _entry_heading = heading_entry(now, &settings.hour_format); - - let create_entry = cx.background_executor().spawn(async move { - std::fs::create_dir_all(month_dir)?; - OpenOptions::new() - .create(true) - .write(true) - .open(&entry_path)?; - Ok::<_, std::io::Error>((journal_dir, entry_path)) - }); - - cx.spawn(|mut cx| async move { - let (journal_dir, entry_path) = create_entry.await?; - let (workspace, _) = cx - .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))? - .await?; - - let _opened = workspace - .update(&mut cx, |workspace, cx| { - workspace.open_paths(vec![entry_path], true, cx) - })? - .await; - - // todo!("editor") - // if let Some(Some(Ok(item))) = opened.first() { - // if let Some(editor) = item.downcast::().map(|editor| editor.downgrade()) { - // editor.update(&mut cx, |editor, cx| { - // let len = editor.buffer().read(cx).len(cx); - // editor.change_selections(Some(Autoscroll::center()), cx, |s| { - // s.select_ranges([len..len]) - // }); - // if len > 0 { - // editor.insert("\n\n", cx); - // } - // editor.insert(&entry_heading, cx); - // editor.insert("\n\n", cx); - // })?; - // } - // } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn journal_dir(path: &str) -> Option { - let expanded_journal_dir = shellexpand::full(path) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); - - return expanded_journal_dir; -} - -fn heading_entry(now: NaiveTime, hour_format: &Option) -> String { - match hour_format { - Some(HourFormat::Hour24) => { - let hour = now.hour(); - format!("# {}:{:02}", hour, now.minute()) - } - _ => { - let (pm, hour) = now.hour12(); - let am_or_pm = if pm { "PM" } else { "AM" }; - format!("# {}:{:02} {}", hour, now.minute(), am_or_pm) - } - } -} - -#[cfg(test)] -mod tests { - mod heading_entry_tests { - use super::super::*; - - #[test] - fn test_heading_entry_defaults_to_hour_12() { - let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &None); - let expected_heading_entry = "# 3:00 PM"; - - assert_eq!(actual_heading_entry, expected_heading_entry); - } - - #[test] - fn test_heading_entry_is_hour_12() { - let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12)); - let expected_heading_entry = "# 3:00 PM"; - - assert_eq!(actual_heading_entry, expected_heading_entry); - } - - #[test] - fn test_heading_entry_is_hour_24() { - let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap(); - let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24)); - let expected_heading_entry = "# 15:00"; - - assert_eq!(actual_heading_entry, expected_heading_entry); - } - } -} diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 8614cfd69f..d5ec49c0bb 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] -assistant = { package = "assistant2", path = "../assistant2" } +assistant = { path = "../assistant" } editor = { path = "../editor" } gpui = { package = "gpui2", path = "../gpui2" } search = { path = "../search" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 596141bf65..17212ce72e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -42,7 +42,7 @@ fsevent = { path = "../fsevent" } go_to_line = { path = "../go_to_line" } gpui = { package = "gpui2", path = "../gpui2" } install_cli = { package = "install_cli2", path = "../install_cli2" } -journal = { package = "journal2", path = "../journal2" } +journal = { path = "../journal" } language = { package = "language2", path = "../language2" } language_selector = { path = "../language_selector" } lsp = { package = "lsp2", path = "../lsp2" } @@ -50,7 +50,7 @@ menu = { package = "menu2", path = "../menu2" } language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } notifications = { package = "notifications2", path = "../notifications2" } -assistant = { package = "assistant2", path = "../assistant2" } +assistant = { path = "../assistant" } outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" }