From 23bc11f8b3d6c7c7ec1448cf801071258a66e812 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 20 Jun 2023 18:51:37 -0600 Subject: [PATCH] Remove the nested Pane from the assistant Since we don't want tabs, I think it would be better to render the toolbar for ourselves directly and handle switching between conversations. Co-Authored-By: Julia Risley --- Cargo.lock | 1 + assets/keymaps/default.json | 6 +- assets/settings/default.json | 54 ++-- crates/ai/Cargo.toml | 1 + crates/ai/src/assistant.rs | 289 ++++++++++----------- crates/project_panel/src/project_panel.rs | 1 + crates/terminal_view/src/terminal_panel.rs | 1 + crates/workspace/src/dock.rs | 3 +- crates/workspace/src/workspace.rs | 10 +- 9 files changed, 189 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdabeb26ec..3ba305b2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,7 @@ dependencies = [ "isahc", "language", "menu", + "project", "regex", "schemars", "search", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cce8b07d17..3d51c07856 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -40,7 +40,8 @@ "cmd-o": "workspace::Open", "alt-cmd-o": "projects::OpenRecent", "ctrl-~": "workspace::NewTerminal", - "ctrl-`": "terminal_panel::ToggleFocus" + "ctrl-`": "terminal_panel::ToggleFocus", + "shift-escape": "workspace::ToggleZoom" } }, { @@ -235,8 +236,7 @@ "cmd-shift-g": "search::SelectPrevMatch", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex", - "shift-escape": "workspace::ToggleZoom" + "alt-cmd-r": "search::ToggleRegex" } }, // Bindings from VS Code diff --git a/assets/settings/default.json b/assets/settings/default.json index bd73bcbf08..c570660f38 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -57,37 +57,37 @@ "show_whitespaces": "selection", // Scrollbar related settings "scrollbar": { - // When to show the scrollbar in the editor. - // This setting can take four values: - // - // 1. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 2. Match the system's configured behavior: - // "system" - // 3. Always show the scrollbar: - // "always" - // 4. Never show the scrollbar: - // "never" - "show": "auto", - // Whether to show git diff indicators in the scrollbar. - "git_diff": true + // When to show the scrollbar in the editor. + // This setting can take four values: + // + // 1. Show the scrollbar if there's important information or + // follow the system's configured behavior (default): + // "auto" + // 2. Match the system's configured behavior: + // "system" + // 3. Always show the scrollbar: + // "always" + // 4. Never show the scrollbar: + // "never" + "show": "auto", + // Whether to show git diff indicators in the scrollbar. + "git_diff": true }, "project_panel": { - // Whether to show the git status in the project panel. - "git_status": true, - // Where to dock project panel. Can be 'left' or 'right'. - "dock": "left", - // Default width of the project panel. - "default_width": 240 + // Whether to show the git status in the project panel. + "git_status": true, + // Where to dock project panel. Can be 'left' or 'right'. + "dock": "left", + // Default width of the project panel. + "default_width": 240 }, "assistant": { - // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. - "dock": "right", - // Default width when the assistant is docked to the left or right. - "default_width": 450, - // Default height when the assistant is docked to the bottom. - "default_height": 320 + // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. + "dock": "right", + // Default width when the assistant is docked to the left or right. + "default_width": 450, + // Default height when the assistant is docked to the bottom. + "default_height": 320 }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 76aaf41017..785bc657cf 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -34,3 +34,4 @@ tiktoken-rs = "0.4" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index f7929006c6..eff3b7b5b4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -37,7 +37,7 @@ use util::{ use workspace::{ dock::{DockPosition, Panel}, item::Item, - pane, Pane, Save, Workspace, + Save, Workspace, }; const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; @@ -66,21 +66,24 @@ pub fn init(cx: &mut AppContext) { cx.add_action( |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { if let Some(this) = workspace.panel::(cx) { - this.update(cx, |this, cx| this.add_context(cx)) + this.update(cx, |this, cx| { + this.add_conversation(cx); + }) } workspace.focus_panel::(cx); }, ); - cx.add_action(AssistantEditor::assist); - cx.capture_action(AssistantEditor::cancel_last_assist); - cx.capture_action(AssistantEditor::save); - cx.add_action(AssistantEditor::quote_selection); - cx.capture_action(AssistantEditor::copy); - cx.capture_action(AssistantEditor::split); - cx.capture_action(AssistantEditor::cycle_message_role); + 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.capture_action(ConversationEditor::split); + cx.capture_action(ConversationEditor::cycle_message_role); cx.add_action(AssistantPanel::save_api_key); cx.add_action(AssistantPanel::reset_api_key); + cx.add_action(AssistantPanel::toggle_zoom); cx.add_action( |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { workspace.toggle_panel_focus::(cx); @@ -88,6 +91,7 @@ pub fn init(cx: &mut AppContext) { ); } +#[derive(Debug)] pub enum AssistantPanelEvent { ZoomIn, ZoomOut, @@ -99,14 +103,17 @@ pub enum AssistantPanelEvent { pub struct AssistantPanel { width: Option, height: Option, - pane: ViewHandle, + active_conversation_index: usize, + conversation_editors: Vec>, + saved_conversations: Vec, + zoomed: bool, + has_focus: bool, api_key: Rc>>, api_key_editor: Option>, has_read_credentials: bool, languages: Arc, fs: Arc, subscriptions: Vec, - saved_conversations: Vec, _watch_saved_conversations: Task>, } @@ -125,61 +132,6 @@ impl AssistantPanel { // TODO: deserialize state. workspace.update(&mut cx, |workspace, cx| { cx.add_view::(|cx| { - let weak_self = cx.weak_handle(); - let pane = cx.add_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - workspace.app_state().background_actions, - Default::default(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.on_can_drop(move |_, _| false); - pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - let weak_self = weak_self.clone(); - Flex::row() - .with_child(Pane::render_tab_bar_button( - 0, - "icons/plus_12.svg", - false, - Some(("New Context".into(), Some(Box::new(NewContext)))), - cx, - move |_, cx| { - let weak_self = weak_self.clone(); - cx.window_context().defer(move |cx| { - if let Some(this) = weak_self.upgrade(cx) { - this.update(cx, |this, cx| this.add_context(cx)); - } - }) - }, - None, - )) - .with_child(Pane::render_tab_bar_button( - 1, - if pane.is_zoomed() { - "icons/minimize_8.svg" - } else { - "icons/maximize_8.svg" - }, - pane.is_zoomed(), - Some(( - "Toggle Zoom".into(), - Some(Box::new(workspace::ToggleZoom)), - )), - cx, - move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - None, - )) - .into_any() - }); - let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); - pane - }); - 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 @@ -200,7 +152,11 @@ impl AssistantPanel { }); let mut this = Self { - pane, + active_conversation_index: 0, + conversation_editors: Default::default(), + saved_conversations, + zoomed: false, + has_focus: false, api_key: Rc::new(RefCell::new(None)), api_key_editor: None, has_read_credentials: false, @@ -209,22 +165,18 @@ impl AssistantPanel { width: None, height: None, subscriptions: Default::default(), - saved_conversations, _watch_saved_conversations, }; let mut old_dock_position = this.position(cx); - this.subscriptions = vec![ - cx.observe(&this.pane, |_, _, cx| cx.notify()), - cx.subscribe(&this.pane, Self::handle_pane_event), - cx.observe_global::(move |this, 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(AssistantPanelEvent::DockPositionChanged); } - }), - ]; + })]; this }) @@ -232,25 +184,14 @@ impl AssistantPanel { }) } - fn handle_pane_event( - &mut self, - _pane: ViewHandle, - event: &pane::Event, - cx: &mut ViewContext, - ) { - match event { - pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut), - pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus), - pane::Event::Remove => cx.emit(AssistantPanelEvent::Close), - _ => {} - } - } - - fn add_context(&mut self, cx: &mut ViewContext) { + fn add_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let focus = self.has_focus(cx); let editor = cx.add_view(|cx| { - AssistantEditor::new( + if focus { + cx.focus_self(); + } + + ConversationEditor::new( self.api_key.clone(), self.languages.clone(), self.fs.clone(), @@ -258,20 +199,23 @@ impl AssistantPanel { ) }); self.subscriptions - .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); - self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(editor), true, focus, None, cx) - }); + .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); + + self.active_conversation_index = self.conversation_editors.len(); + self.conversation_editors.push(editor.clone()); + + cx.notify(); + editor } - fn handle_assistant_editor_event( + fn handle_conversation_editor_event( &mut self, - _: ViewHandle, + _: ViewHandle, event: &AssistantEditorEvent, cx: &mut ViewContext, ) { match event { - AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()), + AssistantEditorEvent::TabContentChanged => cx.notify(), } } @@ -302,6 +246,19 @@ impl AssistantPanel { cx.focus_self(); cx.notify(); } + + fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { + if self.zoomed { + cx.emit(AssistantPanelEvent::ZoomOut) + } else { + cx.emit(AssistantPanelEvent::ZoomIn) + } + } + + fn active_conversation_editor(&self) -> Option<&ViewHandle> { + self.conversation_editors + .get(self.active_conversation_index) + } } fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { @@ -345,20 +302,27 @@ impl View for AssistantPanel { .with_style(style.api_key_prompt.container) .aligned() .into_any() + } else if let Some(editor) = self.active_conversation_editor() { + ChildView::new(editor, cx).into_any() } else { - ChildView::new(&self.pane, cx).into_any() + Empty::new().into_any() } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; if cx.is_self_focused() { - if let Some(api_key_editor) = self.api_key_editor.as_ref() { + if let Some(editor) = self.active_conversation_editor() { + cx.focus(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { cx.focus(api_key_editor); - } else { - cx.focus(&self.pane); } } } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } } impl Panel for AssistantPanel { @@ -411,12 +375,13 @@ impl Panel for AssistantPanel { matches!(event, AssistantPanelEvent::ZoomOut) } - fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + self.zoomed = zoomed; + cx.notify(); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { @@ -443,8 +408,8 @@ impl Panel for AssistantPanel { } } - if self.pane.read(cx).items_len() == 0 { - self.add_context(cx); + if self.conversation_editors.is_empty() { + self.add_conversation(cx); } } } @@ -469,12 +434,8 @@ impl Panel for AssistantPanel { matches!(event, AssistantPanelEvent::Close) } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() - || self - .api_key_editor - .as_ref() - .map_or(false, |editor| editor.is_focused(cx)) + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus } fn is_focus_event(event: &Self::Event) -> bool { @@ -494,7 +455,7 @@ struct Summary { done: bool, } -struct Assistant { +struct Conversation { buffer: ModelHandle, message_anchors: Vec, messages_metadata: HashMap, @@ -513,11 +474,11 @@ struct Assistant { _subscriptions: Vec, } -impl Entity for Assistant { +impl Entity for Conversation { type Event = AssistantEvent; } -impl Assistant { +impl Conversation { fn new( api_key: Rc>>, language_registry: Arc, @@ -1080,7 +1041,7 @@ impl Assistant { &mut self, debounce: Option, fs: Arc, - cx: &mut ModelContext, + cx: &mut ModelContext, ) { self.pending_save = cx.spawn(|this, mut cx| async move { if let Some(debounce) = debounce { @@ -1158,8 +1119,8 @@ struct ScrollPosition { cursor: Anchor, } -struct AssistantEditor { - assistant: ModelHandle, +struct ConversationEditor { + assistant: ModelHandle, fs: Arc, editor: ViewHandle, blocks: HashSet, @@ -1167,14 +1128,14 @@ struct AssistantEditor { _subscriptions: Vec, } -impl AssistantEditor { +impl ConversationEditor { fn new( api_key: Rc>>, language_registry: Arc, fs: Arc, cx: &mut ViewContext, ) -> Self { - let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx)); + let assistant = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx)); let editor = cx.add_view(|cx| { let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); @@ -1262,7 +1223,7 @@ impl AssistantEditor { fn handle_assistant_event( &mut self, - _: ModelHandle, + _: ModelHandle, event: &AssistantEvent, cx: &mut ViewContext, ) { @@ -1501,20 +1462,15 @@ impl AssistantEditor { if let Some(text) = text { panel.update(cx, |panel, cx| { - if let Some(assistant) = panel - .pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .ok_or_else(|| anyhow!("no active context")) - .log_err() - { - assistant.update(cx, |assistant, cx| { - assistant - .editor - .update(cx, |editor, cx| editor.insert(&text, cx)) - }); - } + let editor = panel + .active_conversation_editor() + .cloned() + .unwrap_or_else(|| panel.add_conversation(cx)); + editor.update(cx, |assistant, cx| { + assistant + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); }); } } @@ -1592,11 +1548,11 @@ impl AssistantEditor { } } -impl Entity for AssistantEditor { +impl Entity for ConversationEditor { type Event = AssistantEditorEvent; } -impl View for AssistantEditor { +impl View for ConversationEditor { fn ui_name() -> &'static str { "AssistantEditor" } @@ -1655,7 +1611,7 @@ impl View for AssistantEditor { } } -impl Item for AssistantEditor { +impl Item for ConversationEditor { fn tab_content( &self, _: Option, @@ -1812,12 +1768,55 @@ async fn stream_completion( #[cfg(test)] mod tests { use super::*; - use gpui::AppContext; + use fs::FakeFs; + use gpui::{AppContext, TestAppContext}; + use project::Project; + + fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + language::init(cx); + editor::init_settings(cx); + crate::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + }); + } + + #[gpui::test] + async fn test_panel(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + let project = Project::test(fs, [], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let weak_workspace = workspace.downgrade(); + + let panel = cx + .spawn(|cx| async move { AssistantPanel::load(weak_workspace, cx).await }) + .await + .unwrap(); + + workspace.update(cx, |workspace, cx| { + workspace.add_panel(panel.clone(), cx); + workspace.toggle_dock(DockPosition::Right, cx); + assert!(workspace.right_dock().read(cx).is_open()); + cx.focus(&panel); + }); + + cx.dispatch_action(window_id, workspace::ToggleZoom); + + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed_view(cx).unwrap(), panel); + }) + } #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { let registry = Arc::new(LanguageRegistry::test()); - let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx)); + let assistant = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = assistant.read(cx).buffer.clone(); let message_1 = assistant.read(cx).message_anchors[0].clone(); @@ -1943,7 +1942,7 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { let registry = Arc::new(LanguageRegistry::test()); - let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx)); + let assistant = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = assistant.read(cx).buffer.clone(); let message_1 = assistant.read(cx).message_anchors[0].clone(); @@ -2036,7 +2035,7 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { let registry = Arc::new(LanguageRegistry::test()); - let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx)); + let assistant = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); let buffer = assistant.read(cx).buffer.clone(); let message_1 = assistant.read(cx).message_anchors[0].clone(); @@ -2080,7 +2079,7 @@ mod tests { ); fn message_ids_for_offsets( - assistant: &ModelHandle, + assistant: &ModelHandle, offsets: &[usize], cx: &AppContext, ) -> Vec { @@ -2094,7 +2093,7 @@ mod tests { } fn messages( - assistant: &ModelHandle, + assistant: &ModelHandle, cx: &AppContext, ) -> Vec<(MessageId, Role, Range)> { assistant diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9563d54be8..c8781c195c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -153,6 +153,7 @@ pub fn init(cx: &mut AppContext) { ); } +#[derive(Debug)] pub enum Event { OpenedEntry { entry_id: ProjectEntryId, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ac3875af9e..6de6527a26 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -25,6 +25,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(TerminalPanel::new_terminal); } +#[derive(Debug)] pub enum Event { Close, DockPositionChanged, diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index c174b8d3a5..8ced21a809 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -249,7 +249,7 @@ impl Dock { } } - pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + pub(crate) fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { let subscriptions = [ cx.observe(&panel, |_, _, cx| cx.notify()), cx.subscribe(&panel, |this, panel, event, cx| { @@ -603,6 +603,7 @@ pub mod test { use super::*; use gpui::{ViewContext, WindowContext}; + #[derive(Debug)] pub enum TestPanelEvent { PositionChanged, Activated, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 43ca41ab1d..d93dae2d13 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -859,7 +859,10 @@ impl Workspace { &self.right_dock } - pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) + where + T::Event: std::fmt::Debug, + { let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, DockPosition::Bottom => &self.bottom_dock, @@ -1700,6 +1703,11 @@ impl Workspace { cx.notify(); } + #[cfg(any(test, feature = "test-support"))] + pub fn zoomed_view(&self, cx: &AppContext) -> Option { + self.zoomed.and_then(|view| view.upgrade(cx)) + } + fn dismiss_zoomed_items_to_reveal( &mut self, dock_to_reveal: Option,