From d0aff65b1cb9fe841aed9df8673e50f3deb96dc1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Jun 2023 10:55:19 +0200 Subject: [PATCH] Allow moving the assistant panel to other docks --- Cargo.lock | 3 + assets/settings/default.json | 10 +++ crates/ai/Cargo.toml | 3 + crates/ai/src/ai.rs | 1 + crates/ai/src/assistant.rs | 98 ++++++++++++++++++++++------- crates/ai/src/assistant_settings.rs | 42 +++++++++++++ crates/terminal_view/Cargo.toml | 2 - 7 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 crates/ai/src/assistant_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 9634cd2c8e..4e3ded5bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,13 +104,16 @@ dependencies = [ "chrono", "collections", "editor", + "fs", "futures 0.3.28", "gpui", "isahc", "language", + "schemars", "search", "serde", "serde_json", + "settings", "theme", "util", "workspace", diff --git a/assets/settings/default.json b/assets/settings/default.json index 23599c8dfb..695061a0aa 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -81,6 +81,16 @@ // 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": 480, + // Default height when the assistant is docked to the bottom. + "default_height": 320, + // OpenAI API key. + "openai_api_key": null + }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, // Whether to use language servers to provide code intelligence. diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 861a9e4785..ce2a3338eb 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -12,9 +12,11 @@ doctest = false assets = { path = "../assets"} collections = { path = "../collections"} editor = { path = "../editor" } +fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } search = { path = "../search" } +settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } @@ -23,6 +25,7 @@ anyhow.workspace = true chrono = "0.4" futures.workspace = true isahc.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index fe1125bb98..a5d5666a72 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,4 +1,5 @@ pub mod assistant; +mod assistant_settings; pub use assistant::AssistantPanel; use gpui::{actions, AppContext}; diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 6f5cbd6416..ecc27538ed 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,8 +1,12 @@ -use crate::{OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role}; +use crate::{ + assistant_settings::{AssistantDockPosition, AssistantSettings}, + OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, +}; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; use collections::HashMap; use editor::{Editor, ExcerptId, ExcerptRange, MultiBuffer}; +use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; use gpui::{ actions, elements::*, executor::Background, Action, AppContext, AsyncAppContext, Entity, @@ -11,6 +15,7 @@ use gpui::{ }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; +use settings::SettingsStore; use std::{io, sync::Arc}; use util::{post_inc, ResultExt, TryFutureExt}; use workspace::{ @@ -22,6 +27,7 @@ use workspace::{ actions!(assistant, [NewContext, Assist, QuoteSelection, ToggleFocus]); pub fn init(cx: &mut AppContext) { + settings::register::(cx); cx.add_action( |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { if let Some(this) = workspace.panel::(cx) { @@ -41,12 +47,15 @@ pub enum AssistantPanelEvent { ZoomOut, Focus, Close, + DockPositionChanged, } pub struct AssistantPanel { width: Option, + height: Option, pane: ViewHandle, languages: Arc, + fs: Arc, _subscriptions: Vec, } @@ -113,17 +122,40 @@ impl AssistantPanel { .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); pane }); - let subscriptions = vec![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - - Self { + let mut this = Self { pane, languages: workspace.app_state().languages.clone(), + fs: workspace.app_state().fs.clone(), width: None, - _subscriptions: subscriptions, - } + height: None, + _subscriptions: Default::default(), + }; + + let mut old_dock_position = this.position(cx); + let mut old_openai_api_key = settings::get::(cx) + .openai_api_key + .clone(); + this._subscriptions = vec![ + cx.observe(&this.pane, |_, _, cx| cx.notify()), + cx.subscribe(&this.pane, Self::handle_pane_event), + 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); + } + + let new_openai_api_key = settings::get::(cx) + .openai_api_key + .clone(); + if old_openai_api_key != new_openai_api_key { + old_openai_api_key = new_openai_api_key; + cx.notify(); + } + }), + ]; + + this }) }) }) @@ -174,24 +206,44 @@ impl View for AssistantPanel { } impl Panel for AssistantPanel { - fn position(&self, _: &WindowContext) -> DockPosition { - DockPosition::Right + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } } - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Right) + fn position_is_valid(&self, _: DockPosition) -> bool { + true } - fn set_position(&mut self, _: DockPosition, _: &mut ViewContext) { - // TODO! + 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, _: &WindowContext) -> f32 { - self.width.unwrap_or(480.) + fn size(&self, cx: &WindowContext) -> f32 { + let settings = settings::get::(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: f32, cx: &mut ViewContext) { - self.width = Some(size); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = Some(size), + DockPosition::Bottom => self.height = Some(size), + } cx.notify(); } @@ -225,9 +277,8 @@ impl Panel for AssistantPanel { ("Assistant Panel".into(), Some(Box::new(ToggleFocus))) } - fn should_change_position_on_event(_: &Self::Event) -> bool { - // TODO! - false + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, AssistantPanelEvent::DockPositionChanged) } fn should_activate_on_event(_: &Self::Event) -> bool { @@ -289,7 +340,10 @@ impl Assistant { stream: true, }; - if let Some(api_key) = std::env::var("OPENAI_API_KEY").log_err() { + if let Some(api_key) = settings::get::(cx) + .openai_api_key + .clone() + { let stream = stream_completion(api_key, cx.background().clone(), request); let response = self.push_message(Role::Assistant, cx); self.push_message(Role::User, cx); diff --git a/crates/ai/src/assistant_settings.rs b/crates/ai/src/assistant_settings.rs new file mode 100644 index 0000000000..c2652ec0cb --- /dev/null +++ b/crates/ai/src/assistant_settings.rs @@ -0,0 +1,42 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Setting; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AssistantDockPosition { + Left, + Right, + Bottom, +} + +#[derive(Deserialize, Debug)] +pub struct AssistantSettings { + pub dock: AssistantDockPosition, + pub default_width: f32, + pub default_height: f32, + pub openai_api_key: Option, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AssistantSettingsContent { + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub openai_api_key: Option, +} + +impl Setting for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 6fa920d739..85de173604 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -37,8 +37,6 @@ lazy_static.workspace = true serde.workspace = true serde_derive.workspace = true - - [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }