From b8397a36c1272c7cc1ddeaa049278978bb634e49 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 30 Mar 2023 18:27:14 -0700 Subject: [PATCH] Merge pull request #2316 from zed-industries/copilot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 WIP 🚨 Copilot --- Cargo.lock | 69 +- Cargo.toml | 3 + assets/icons/copilot_16.svg | 12 + assets/icons/copilot_disabled_16.svg | 9 + assets/icons/copilot_error_16.svg | 7 + assets/icons/copilot_init_16.svg | 4 + assets/icons/github-copilot-dummy.svg | 1 + assets/icons/link_out_12.svg | 5 + assets/icons/zed_plus_copilot_32.svg | 14 + assets/keymaps/default.json | 5 +- assets/settings/default.json | 18 +- crates/auto_update/Cargo.toml | 6 +- crates/auto_update/src/auto_update.rs | 4 +- crates/cli/Cargo.toml | 4 +- crates/client/Cargo.toml | 5 +- crates/client/src/client.rs | 8 +- crates/client/src/http.rs | 57 -- crates/client/src/telemetry.rs | 18 +- crates/client/src/test.rs | 53 +- crates/client/src/user.rs | 3 +- crates/collab/Cargo.toml | 8 +- crates/collab/src/tests.rs | 4 +- crates/collab_ui/Cargo.toml | 4 +- crates/collab_ui/src/collab_titlebar_item.rs | 20 +- crates/collections/src/collections.rs | 7 + crates/command_palette/Cargo.toml | 2 +- crates/command_palette/src/command_palette.rs | 7 +- crates/context_menu/src/context_menu.rs | 128 +++- crates/copilot/Cargo.toml | 39 ++ crates/copilot/src/copilot.rs | 628 ++++++++++++++++++ crates/copilot/src/editor.rs | 3 + crates/copilot/src/request.rs | 142 ++++ crates/copilot/src/sign_in.rs | 344 ++++++++++ crates/copilot_button/Cargo.toml | 22 + crates/copilot_button/src/copilot_button.rs | 360 ++++++++++ crates/db/Cargo.toml | 4 +- crates/diagnostics/Cargo.toml | 2 +- crates/editor/Cargo.toml | 8 +- crates/editor/src/display_map.rs | 21 +- crates/editor/src/display_map/block_map.rs | 7 +- .../editor/src/display_map/suggestion_map.rs | 24 +- crates/editor/src/display_map/tab_map.rs | 20 +- crates/editor/src/display_map/wrap_map.rs | 12 +- crates/editor/src/editor.rs | 302 +++++++++ crates/editor/src/element.rs | 66 +- crates/feedback/Cargo.toml | 4 +- crates/file_finder/Cargo.toml | 2 +- crates/fs/Cargo.toml | 2 +- crates/gpui/Cargo.toml | 6 +- crates/gpui/src/app.rs | 6 + crates/gpui/src/elements.rs | 6 + crates/gpui/src/platform/mac/geometry.rs | 53 +- crates/gpui/src/platform/mac/window.rs | 31 +- crates/language/Cargo.toml | 6 +- crates/language/src/language.rs | 2 +- crates/live_kit_client/Cargo.toml | 10 +- crates/live_kit_server/Cargo.toml | 4 +- crates/lsp/Cargo.toml | 6 +- crates/node_runtime/Cargo.toml | 22 + .../src}/node_runtime.rs | 2 +- crates/picker/Cargo.toml | 2 +- crates/plugin/Cargo.toml | 4 +- crates/plugin_macros/Cargo.toml | 4 +- crates/plugin_runtime/Cargo.toml | 6 +- crates/project/Cargo.toml | 6 +- crates/project/src/project.rs | 2 +- crates/project/src/worktree.rs | 3 +- crates/project_panel/Cargo.toml | 2 +- crates/rpc/Cargo.toml | 4 +- crates/search/Cargo.toml | 6 +- crates/settings/Cargo.toml | 3 +- crates/settings/src/settings.rs | 182 ++++- crates/terminal/Cargo.toml | 4 +- crates/terminal_view/Cargo.toml | 4 +- crates/theme/Cargo.toml | 6 +- crates/theme/src/theme.rs | 76 ++- crates/theme/src/ui.rs | 159 ++++- crates/util/Cargo.toml | 8 +- crates/util/src/fs.rs | 28 + .../{zed/src/languages => util/src}/github.rs | 11 +- crates/util/src/http.rs | 117 ++++ crates/util/src/paths.rs | 1 + crates/util/src/util.rs | 12 + crates/vim/Cargo.toml | 6 +- crates/vim/src/vim.rs | 2 +- crates/welcome/src/welcome.rs | 101 +-- crates/workspace/Cargo.toml | 6 +- crates/workspace/src/notifications.rs | 50 +- crates/workspace/src/workspace.rs | 95 ++- crates/zed/Cargo.toml | 11 +- crates/zed/src/languages.rs | 9 +- crates/zed/src/languages/c.rs | 18 +- crates/zed/src/languages/elixir.rs | 24 +- crates/zed/src/languages/go.rs | 24 +- crates/zed/src/languages/html.rs | 25 +- crates/zed/src/languages/json.rs | 15 +- crates/zed/src/languages/language_plugin.rs | 2 +- crates/zed/src/languages/lua.rs | 8 +- crates/zed/src/languages/python.rs | 16 +- crates/zed/src/languages/ruby.rs | 2 +- crates/zed/src/languages/rust.rs | 16 +- crates/zed/src/languages/typescript.rs | 16 +- crates/zed/src/languages/yaml.rs | 15 +- crates/zed/src/main.rs | 18 +- crates/zed/src/zed.rs | 15 +- plugins/json_language/Cargo.toml | 2 +- styles/src/styleTree/app.ts | 2 + styles/src/styleTree/components.ts | 12 + styles/src/styleTree/copilot.ts | 226 +++++++ styles/src/styleTree/editor.ts | 4 + .../styleTree/simpleMessageNotification.ts | 16 +- styles/src/styleTree/welcome.ts | 19 +- styles/src/styleTree/workspace.ts | 31 +- 113 files changed, 3412 insertions(+), 695 deletions(-) create mode 100644 assets/icons/copilot_16.svg create mode 100644 assets/icons/copilot_disabled_16.svg create mode 100644 assets/icons/copilot_error_16.svg create mode 100644 assets/icons/copilot_init_16.svg create mode 100644 assets/icons/github-copilot-dummy.svg create mode 100644 assets/icons/link_out_12.svg create mode 100644 assets/icons/zed_plus_copilot_32.svg delete mode 100644 crates/client/src/http.rs create mode 100644 crates/copilot/Cargo.toml create mode 100644 crates/copilot/src/copilot.rs create mode 100644 crates/copilot/src/editor.rs create mode 100644 crates/copilot/src/request.rs create mode 100644 crates/copilot/src/sign_in.rs create mode 100644 crates/copilot_button/Cargo.toml create mode 100644 crates/copilot_button/src/copilot_button.rs create mode 100644 crates/node_runtime/Cargo.toml rename crates/{zed/src/languages => node_runtime/src}/node_runtime.rs (99%) create mode 100644 crates/util/src/fs.rs rename crates/{zed/src/languages => util/src}/github.rs (85%) create mode 100644 crates/util/src/http.rs create mode 100644 styles/src/styleTree/copilot.ts diff --git a/Cargo.lock b/Cargo.lock index 02b27566e42f29eccd5b4a0ca145567e885cf36b..fc9e73aa110eef6820581a547ca8d14c8b1207f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1113,7 +1113,6 @@ dependencies = [ "futures 0.3.25", "gpui", "image", - "isahc", "lazy_static", "log", "parking_lot 0.11.2", @@ -1332,6 +1331,48 @@ dependencies = [ "theme", ] +[[package]] +name = "copilot" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "async-tar", + "client", + "collections", + "context_menu", + "futures 0.3.25", + "gpui", + "language", + "log", + "lsp", + "node_runtime", + "serde", + "serde_derive", + "settings", + "smol", + "theme", + "util", + "workspace", +] + +[[package]] +name = "copilot_button" +version = "0.1.0" +dependencies = [ + "anyhow", + "context_menu", + "copilot", + "editor", + "futures 0.3.25", + "gpui", + "settings", + "smol", + "theme", + "util", + "workspace", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1932,6 +1973,7 @@ dependencies = [ "clock", "collections", "context_menu", + "copilot", "ctor", "db", "drag_and_drop", @@ -3910,6 +3952,23 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "node_runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-compression", + "async-tar", + "futures 0.3.25", + "gpui", + "parking_lot 0.11.2", + "serde", + "serde_derive", + "serde_json", + "smol", + "util", +] + [[package]] name = "nom" version = "7.1.1" @@ -5882,6 +5941,7 @@ dependencies = [ "gpui", "json_comments", "postage", + "pretty_assertions", "schemars", "serde", "serde_derive", @@ -7500,11 +7560,15 @@ dependencies = [ "dirs 3.0.2", "futures 0.3.25", "git2", + "isahc", "lazy_static", "log", "rand 0.8.5", + "serde", "serde_json", + "smol", "tempdir", + "url", ] [[package]] @@ -8460,6 +8524,8 @@ dependencies = [ "collections", "command_palette", "context_menu", + "copilot", + "copilot_button", "ctor", "db", "diagnostics", @@ -8486,6 +8552,7 @@ dependencies = [ "libc", "log", "lsp", + "node_runtime", "num_cpus", "outline", "parking_lot 0.11.2", diff --git a/Cargo.toml b/Cargo.toml index 9f795992d5888d24ef083abe231ed1a4edd1a950..8fad52c8f4d9f2906baf4a3aa72a16e1a848f959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ members = [ "crates/collections", "crates/command_palette", "crates/context_menu", + "crates/copilot", + "crates/copilot_button", "crates/db", "crates/diagnostics", "crates/drag_and_drop", @@ -35,6 +37,7 @@ members = [ "crates/lsp", "crates/media", "crates/menu", + "crates/node_runtime", "crates/outline", "crates/picker", "crates/plugin", diff --git a/assets/icons/copilot_16.svg b/assets/icons/copilot_16.svg new file mode 100644 index 0000000000000000000000000000000000000000..e14b61ce8bc73cc09242256706283e7e2831f8fb --- /dev/null +++ b/assets/icons/copilot_16.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/copilot_disabled_16.svg b/assets/icons/copilot_disabled_16.svg new file mode 100644 index 0000000000000000000000000000000000000000..eba36a2b692aca5841d5f3a8d131df980004fd9b --- /dev/null +++ b/assets/icons/copilot_disabled_16.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/copilot_error_16.svg b/assets/icons/copilot_error_16.svg new file mode 100644 index 0000000000000000000000000000000000000000..6069c554f1da71202b57a541a7b15195287723c9 --- /dev/null +++ b/assets/icons/copilot_error_16.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/copilot_init_16.svg b/assets/icons/copilot_init_16.svg new file mode 100644 index 0000000000000000000000000000000000000000..6cbf63fb49324409a096df8d4c60e5ba6bd7a87d --- /dev/null +++ b/assets/icons/copilot_init_16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/github-copilot-dummy.svg b/assets/icons/github-copilot-dummy.svg new file mode 100644 index 0000000000000000000000000000000000000000..4a7ded397623c25fa0c5dda08d639230cd1327b6 --- /dev/null +++ b/assets/icons/github-copilot-dummy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/link_out_12.svg b/assets/icons/link_out_12.svg new file mode 100644 index 0000000000000000000000000000000000000000..561f012452cd5a7a76ecbbc4cd608f6fe0912d06 --- /dev/null +++ b/assets/icons/link_out_12.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/zed_plus_copilot_32.svg b/assets/icons/zed_plus_copilot_32.svg new file mode 100644 index 0000000000000000000000000000000000000000..d024678c500640dc53eadeea0987e9c20070629b --- /dev/null +++ b/assets/icons/zed_plus_copilot_32.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index adda80d9edbd19ff4021d3ee91b8d3b2acc7778a..1a8350bb536a6fb5a09f977e36de831e7e8745a2 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -176,7 +176,10 @@ { "focus": false } - ] + ], + "alt-]": "copilot::NextSuggestion", + "alt-[": "copilot::PreviousSuggestion", + "alt-\\": "copilot::Toggle" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 7b775d6309d66b53784db02a428729c8aea25981..fbb52e00dca9f2ebc83e6eea42864a1dfb269d58 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -13,6 +13,11 @@ // The factor to grow the active pane by. Defaults to 1.0 // which gives the same size as all other panes. "active_pane_magnification": 1.0, + // Enable / disable copilot integration. + "enable_copilot_integration": true, + // Controls whether copilot provides suggestion immediately + // or waits for a `copilot::Toggle` + "copilot": "on", // Whether to enable vim modes and key bindings "vim_mode": false, // Whether to show the informational hover box when moving the mouse @@ -120,7 +125,7 @@ // Settings specific to the terminal "terminal": { // What shell to use when opening a terminal. May take 3 values: - // 1. Use the system's default terminal configuration (e.g. $TERM). + // 1. Use the system's default terminal configuration in /etc/passwd // "shell": "system" // 2. A program: // "shell": { @@ -200,7 +205,9 @@ // Different settings for specific languages. "languages": { "Plain Text": { - "soft_wrap": "preferred_line_length" + "soft_wrap": "preferred_line_length", + // Copilot can be a little strange on non-code files + "copilot": "off" }, "Elixir": { "tab_size": 2 @@ -210,7 +217,9 @@ "hard_tabs": true }, "Markdown": { - "soft_wrap": "preferred_line_length" + "soft_wrap": "preferred_line_length", + // Copilot can be a little strange on non-code files + "copilot": "off" }, "JavaScript": { "tab_size": 2 @@ -223,6 +232,9 @@ }, "YAML": { "tab_size": 2 + }, + "JSON": { + "copilot": "off" } }, // LSP Specific settings. diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 8edb1957afcf13b0e01c078d27ad75335ad09a3d..6b11f5ddbc19672f4c90fc6687b17a53425f1ae4 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -22,8 +22,8 @@ anyhow = "1.0.38" isahc = "1.7" lazy_static = "1.4" log = "0.4" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smol = "1.2.5" tempdir = "0.3.7" diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 4272d7b1afa82ea1449fbd7f55ed79f2e3585a26..3ad3380d2619f1b562af9b4594919f6ce6ec20c6 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,8 +1,7 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; -use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; -use client::{ZED_APP_PATH, ZED_APP_VERSION}; +use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -14,6 +13,7 @@ use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{ffi::OsString, sync::Arc, time::Duration}; use update_notification::UpdateNotification; use util::channel::ReleaseChannel; +use util::http::HttpClient; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bf2e583d2cd56a6f49b07b3cb1f84d68be03fdcc..6b814941b8a25ef061dfba58a0ad47816b759036 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,8 +17,8 @@ anyhow = "1.0" clap = { version = "3.1", features = ["derive"] } dirs = "3.0" ipc-channel = "0.16" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index cb6f29a42e5855d8d142c90fcadf5e5d5e5fe1c7..c75adf5bfa9857c06a80ad205cae22ee2603d634 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -23,7 +23,6 @@ async-recursion = "0.3" async-tungstenite = { version = "0.16", features = ["async-tls"] } futures = "0.3" image = "0.23" -isahc = "1.7" lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" @@ -35,8 +34,8 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] } tiny_http = "0.8" uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" -serde = { version = "*", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } settings = { path = "../settings" } tempfile = "3" diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 86d6bc9912c74e1e8f2efcaec0fc0b5532bcdaa9..bb39b0669989d55b121bc455d891f72a20dbd480 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,7 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -pub mod http; pub mod telemetry; pub mod user; @@ -18,7 +17,6 @@ use gpui::{ AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion, AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; -use http::HttpClient; use lazy_static::lazy_static; use parking_lot::RwLock; use postage::watch; @@ -41,6 +39,7 @@ use telemetry::Telemetry; use thiserror::Error; use url::Url; use util::channel::ReleaseChannel; +use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; pub use rpc::*; @@ -130,7 +129,7 @@ pub enum EstablishConnectionError { #[error("{0}")] Other(#[from] anyhow::Error), #[error("{0}")] - Http(#[from] http::Error), + Http(#[from] util::http::Error), #[error("{0}")] Io(#[from] std::io::Error), #[error("{0}")] @@ -1396,10 +1395,11 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> { #[cfg(test)] mod tests { use super::*; - use crate::test::{FakeHttpClient, FakeServer}; + use crate::test::FakeServer; use gpui::{executor::Deterministic, TestAppContext}; use parking_lot::Mutex; use std::future; + use util::http::FakeHttpClient; #[gpui::test(iterations = 10)] async fn test_reconnection(cx: &mut TestAppContext) { diff --git a/crates/client/src/http.rs b/crates/client/src/http.rs deleted file mode 100644 index 0757cebf3ad5d836edbd59f47a03cbab02c0c211..0000000000000000000000000000000000000000 --- a/crates/client/src/http.rs +++ /dev/null @@ -1,57 +0,0 @@ -pub use anyhow::{anyhow, Result}; -use futures::future::BoxFuture; -use isahc::{ - config::{Configurable, RedirectPolicy}, - AsyncBody, -}; -pub use isahc::{ - http::{Method, Uri}, - Error, -}; -use smol::future::FutureExt; -use std::{sync::Arc, time::Duration}; -pub use url::Url; - -pub type Request = isahc::Request; -pub type Response = isahc::Response; - -pub trait HttpClient: Send + Sync { - fn send(&self, req: Request) -> BoxFuture>; - - fn get<'a>( - &'a self, - uri: &str, - body: AsyncBody, - follow_redirects: bool, - ) -> BoxFuture<'a, Result> { - let request = isahc::Request::builder() - .redirect_policy(if follow_redirects { - RedirectPolicy::Follow - } else { - RedirectPolicy::None - }) - .method(Method::GET) - .uri(uri) - .body(body); - match request { - Ok(request) => self.send(request), - Err(error) => async move { Err(error.into()) }.boxed(), - } - } -} - -pub fn client() -> Arc { - Arc::new( - isahc::HttpClient::builder() - .connect_timeout(Duration::from_secs(5)) - .low_speed_timeout(100, Duration::from_secs(5)) - .build() - .unwrap(), - ) -} - -impl HttpClient for isahc::HttpClient { - fn send(&self, req: Request) -> BoxFuture> { - Box::pin(async move { self.send_async(req).await }) - } -} diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 9d486619d255dd7ab214a145f7f31162dd4a9c36..7ee099dfabf8f67256e7cd945ff7981c4cd5d3e7 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,11 +1,9 @@ -use crate::http::HttpClient; use db::kvp::KEY_VALUE_STORE; use gpui::{ executor::Background, serde_json::{self, value::Map, Value}, AppContext, Task, }; -use isahc::Request; use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; @@ -19,6 +17,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::NamedTempFile; +use util::http::HttpClient; use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -220,10 +219,10 @@ impl Telemetry { "App": true }), }])?; - let request = Request::post(MIXPANEL_ENGAGE_URL) - .header("Content-Type", "application/json") - .body(json_bytes.into())?; - this.http_client.send(request).await?; + + this.http_client + .post_json(MIXPANEL_ENGAGE_URL, json_bytes.into()) + .await?; anyhow::Ok(()) } .log_err(), @@ -316,10 +315,9 @@ impl Telemetry { json_bytes.clear(); serde_json::to_writer(&mut json_bytes, &events)?; - let request = Request::post(MIXPANEL_EVENTS_URL) - .header("Content-Type", "application/json") - .body(json_bytes.into())?; - this.http_client.send(request).await?; + this.http_client + .post_json(MIXPANEL_EVENTS_URL, json_bytes.into()) + .await?; anyhow::Ok(()) } .log_err(), diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index db9e0d8c487b27a7474373af9d2c25a29e04b9d7..4c12a205660f7932a6a7b412c6ee686a6199372c 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -1,16 +1,14 @@ -use crate::{ - http::{self, HttpClient, Request, Response}, - Client, Connection, Credentials, EstablishConnectionError, UserStore, -}; +use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore}; use anyhow::{anyhow, Result}; -use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt}; +use futures::{stream::BoxStream, StreamExt}; use gpui::{executor, ModelHandle, TestAppContext}; use parking_lot::Mutex; use rpc::{ proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse}, ConnectionId, Peer, Receipt, TypedEnvelope, }; -use std::{fmt, rc::Rc, sync::Arc}; +use std::{rc::Rc, sync::Arc}; +use util::http::FakeHttpClient; pub struct FakeServer { peer: Arc, @@ -219,46 +217,3 @@ impl Drop for FakeServer { self.disconnect(); } } - -pub struct FakeHttpClient { - handler: Box< - dyn 'static - + Send - + Sync - + Fn(Request) -> BoxFuture<'static, Result>, - >, -} - -impl FakeHttpClient { - pub fn create(handler: F) -> Arc - where - Fut: 'static + Send + Future>, - F: 'static + Send + Sync + Fn(Request) -> Fut, - { - Arc::new(Self { - handler: Box::new(move |req| Box::pin(handler(req))), - }) - } - - pub fn with_404_response() -> Arc { - Self::create(|_| async move { - Ok(isahc::Response::builder() - .status(404) - .body(Default::default()) - .unwrap()) - }) - } -} - -impl fmt::Debug for FakeHttpClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FakeHttpClient").finish() - } -} - -impl HttpClient for FakeHttpClient { - fn send(&self, req: Request) -> BoxFuture> { - let future = (self.handler)(req); - Box::pin(async move { future.await.map(Into::into) }) - } -} diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a0a730871d01e571ab1ce67e5966cefa03c763d5..8c6b1410017f311a73981ba8368a13a8edc865b7 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,4 +1,4 @@ -use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; +use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; @@ -7,6 +7,7 @@ use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use settings::Settings; use std::sync::{Arc, Weak}; +use util::http::HttpClient; use util::{StaffMode, TryFutureExt as _}; #[derive(Default, Debug)] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 2ee93f1a86b7fa5e5341c03ae32f5cfdbc1acf94..b85d9992986e94060eaf7ab14ae4d41dd60dc7f9 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -41,9 +41,9 @@ scrypt = "0.7" # Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released. sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] } sea-query = "0.27" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } sha-1 = "0.9" sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] } time = { version = "0.3", features = ["serde", "serde-well-known"] } @@ -79,7 +79,7 @@ env_logger = "0.9" util = { path = "../util" } lazy_static = "1.4" sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } sqlx = { version = "0.6", features = ["sqlite"] } unindent = "0.1" diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 91af40dc5a688077422d4e892aad59b5ad237c10..9c0f9f3bd896e599a1f7d459a5fd049f09e60977 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -7,8 +7,7 @@ use crate::{ use anyhow::anyhow; use call::ActiveCall; use client::{ - self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials, - EstablishConnectionError, UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -28,6 +27,7 @@ use std::{ }, }; use theme::ThemeRegistry; +use util::http::FakeHttpClient; use workspace::Workspace; mod integration_tests; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 516a1b4fe4a2ebaa2b5405bbb8a2ca8490039039..50f81c335ce670a44d90c11c99a0d7e0f0be13a4 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -43,8 +43,8 @@ anyhow = "1.0" futures = "0.3" log = "0.4" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [dev-dependencies] call = { path = "../call", features = ["test-support"] } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 3228f7d5a6f3a0b987e4c80694b9ff8777cfe630..b5e8696ec79f6d8661c82797ec78885d76c94f97 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -301,25 +301,13 @@ impl CollabTitlebarItem { .with_style(item_style.container) .boxed() })), - ContextMenuItem::Item { - label: "Sign out".into(), - action: Box::new(SignOut), - }, - ContextMenuItem::Item { - label: "Send Feedback".into(), - action: Box::new(feedback::feedback_editor::GiveFeedback), - }, + ContextMenuItem::item("Sign out", SignOut), + ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback), ] } else { vec![ - ContextMenuItem::Item { - label: "Sign in".into(), - action: Box::new(SignIn), - }, - ContextMenuItem::Item { - label: "Send Feedback".into(), - action: Box::new(feedback::feedback_editor::GiveFeedback), - }, + ContextMenuItem::item("Sign in", SignIn), + ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback), ] }; diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index acef74dbd64a326b82556a42326abef1df4f5da3..eb4e4d8462720c773d7e48b92f413cad2ca1970a 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -24,3 +24,10 @@ pub type HashMap = std::collections::HashMap; pub type HashSet = std::collections::HashSet; pub use std::collections::*; + +// NEW TYPES + +#[derive(Default)] +pub struct CommandPaletteFilter { + pub filtered_namespaces: HashSet<&'static str>, +} diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 555deff1ce6673fdf480eb7cc52c5033b1050a68..6965a3f1836e6972e9fcca3343607d89ea2f3005 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -24,7 +24,7 @@ workspace = { path = "../workspace" } gpui = { path = "../gpui", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 52a0e1cdc0b15f62ef2a66da56f39ac0e2169a03..229e4a04e5e52b5fbf60a9820288b8b0a61a9cc0 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,4 +1,4 @@ -use collections::HashSet; +use collections::CommandPaletteFilter; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, @@ -12,11 +12,6 @@ use settings::Settings; use std::cmp; use workspace::Workspace; -#[derive(Default)] -pub struct CommandPaletteFilter { - pub filtered_namespaces: HashSet<&'static str>, -} - pub fn init(cx: &mut MutableAppContext) { cx.add_action(CommandPalette::toggle); Picker::::init(cx); diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index e1b9f81c1a6342a1f23377bc16f8cf985d96f2a3..ffc121576edcce232d1b45d73e51bae321c8db7a 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,7 +1,7 @@ use gpui::{ elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext, platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, - MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext, + MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext, }; use menu::*; use settings::Settings; @@ -24,20 +24,71 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ContextMenu::cancel); } +type ContextMenuItemBuilder = Box ElementBox>; + +pub enum ContextMenuItemLabel { + String(Cow<'static, str>), + Element(ContextMenuItemBuilder), +} + +pub enum ContextMenuAction { + ParentAction { + action: Box, + }, + ViewAction { + action: Box, + for_view: usize, + }, +} + +impl ContextMenuAction { + fn id(&self) -> TypeId { + match self { + ContextMenuAction::ParentAction { action } => action.id(), + ContextMenuAction::ViewAction { action, .. } => action.id(), + } + } +} + pub enum ContextMenuItem { Item { - label: Cow<'static, str>, - action: Box, + label: ContextMenuItemLabel, + action: ContextMenuAction, }, Static(StaticItem), Separator, } impl ContextMenuItem { + pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self { + Self::Item { + label: ContextMenuItemLabel::Element(label), + action: ContextMenuAction::ParentAction { + action: Box::new(action), + }, + } + } + pub fn item(label: impl Into>, action: impl 'static + Action) -> Self { Self::Item { - label: label.into(), - action: Box::new(action), + label: ContextMenuItemLabel::String(label.into()), + action: ContextMenuAction::ParentAction { + action: Box::new(action), + }, + } + } + + pub fn item_for_view( + label: impl Into>, + view_id: usize, + action: impl 'static + Action, + ) -> Self { + Self::Item { + label: ContextMenuItemLabel::String(label.into()), + action: ContextMenuAction::ViewAction { + action: Box::new(action), + for_view: view_id, + }, } } @@ -168,7 +219,15 @@ impl ContextMenu { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(ix) = self.selected_index { if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { - cx.dispatch_any_action(action.boxed_clone()); + match action { + ContextMenuAction::ParentAction { action } => { + cx.dispatch_any_action(action.boxed_clone()) + } + ContextMenuAction::ViewAction { action, for_view } => { + let window_id = cx.window_id(); + cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone()) + } + }; self.reset(cx); } } @@ -278,10 +337,17 @@ impl ContextMenu { Some(ix) == self.selected_index, ); - Label::new(label.to_string(), style.label.clone()) - .contained() - .with_style(style.container) - .boxed() + match label { + ContextMenuItemLabel::String(label) => { + Label::new(label.to_string(), style.label.clone()) + .contained() + .with_style(style.container) + .boxed() + } + ContextMenuItemLabel::Element(element) => { + element(&mut Default::default(), style) + } + } } ContextMenuItem::Static(f) => f(cx), @@ -306,9 +372,18 @@ impl ContextMenu { &mut Default::default(), Some(ix) == self.selected_index, ); + let (action, view_id) = match action { + ContextMenuAction::ParentAction { action } => { + (action.boxed_clone(), self.parent_view_id) + } + ContextMenuAction::ViewAction { action, for_view } => { + (action.boxed_clone(), *for_view) + } + }; + KeystrokeLabel::new( window_id, - self.parent_view_id, + view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), @@ -347,22 +422,34 @@ impl ContextMenu { .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, action } => { - let action = action.boxed_clone(); + let (action, view_id) = match action { + ContextMenuAction::ParentAction { action } => { + (action.boxed_clone(), self.parent_view_id) + } + ContextMenuAction::ViewAction { action, for_view } => { + (action.boxed_clone(), *for_view) + } + }; MouseEventHandler::::new(ix, cx, |state, _| { let style = style.item.style_for(state, Some(ix) == self.selected_index); Flex::row() - .with_child( - Label::new(label.clone(), style.label.clone()) - .contained() - .boxed(), - ) + .with_child(match label { + ContextMenuItemLabel::String(label) => { + Label::new(label.clone(), style.label.clone()) + .contained() + .boxed() + } + ContextMenuItemLabel::Element(element) => { + element(state, style) + } + }) .with_child({ KeystrokeLabel::new( window_id, - self.parent_view_id, + view_id, action.boxed_clone(), style.keystroke.container, style.keystroke.text.clone(), @@ -375,9 +462,12 @@ impl ContextMenu { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) + .on_up(MouseButton::Left, |_, _| {}) // Capture these events + .on_down(MouseButton::Left, |_, _| {}) // Capture these events .on_click(MouseButton::Left, move |_, cx| { cx.dispatch_action(Clicked); - cx.dispatch_any_action(action.boxed_clone()); + let window_id = cx.window_id(); + cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone()); }) .on_drag(MouseButton::Left, |_, _| {}) .boxed() diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5c9ef2d7c4244041ba27bb25df41aec944a2bb9e --- /dev/null +++ b/crates/copilot/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "copilot" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/copilot.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +context_menu = { path = "../context_menu" } +gpui = { path = "../gpui" } +language = { path = "../language" } +settings = { path = "../settings" } +theme = { path = "../theme" } +lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime"} +util = { path = "../util" } +client = { path = "../client" } +workspace = { path = "../workspace" } +async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" +anyhow = "1.0" +log = "0.4" +serde = { workspace = true } +serde_derive = { workspace = true } +smol = "1.2.5" +futures = "0.3" + +[dev-dependencies] +gpui = { path = "../gpui", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +lsp = { path = "../lsp", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +client = { path = "../client", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b3b066ea8e27c2ca907f508174fd48bcf07b2d4 --- /dev/null +++ b/crates/copilot/src/copilot.rs @@ -0,0 +1,628 @@ +mod request; +mod sign_in; + +use anyhow::{anyhow, Context, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use client::Client; +use futures::{future::Shared, Future, FutureExt, TryFutureExt}; +use gpui::{ + actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, + Task, +}; +use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16}; +use lsp::LanguageServer; +use node_runtime::NodeRuntime; +use settings::Settings; +use smol::{fs, io::BufReader, stream::StreamExt}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::{ + fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, +}; + +const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; +actions!(copilot_auth, [SignIn, SignOut]); + +const COPILOT_NAMESPACE: &'static str = "copilot"; +actions!( + copilot, + [NextSuggestion, PreviousSuggestion, Toggle, Reinstall] +); + +pub fn init(client: Arc, node_runtime: Arc, cx: &mut MutableAppContext) { + let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx)); + cx.set_global(copilot.clone()); + cx.add_global_action(|_: &SignIn, cx| { + let copilot = Copilot::global(cx).unwrap(); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + }); + cx.add_global_action(|_: &SignOut, cx| { + let copilot = Copilot::global(cx).unwrap(); + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + }); + + cx.add_global_action(|_: &Reinstall, cx| { + let copilot = Copilot::global(cx).unwrap(); + copilot + .update(cx, |copilot, cx| copilot.reinstall(cx)) + .detach(); + }); + + cx.observe(&copilot, |handle, cx| { + let status = handle.read(cx).status(); + cx.update_global::( + move |filter, _cx| match status { + Status::Disabled => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); + } + Status::Authorized => { + filter.filtered_namespaces.remove(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } + _ => { + filter.filtered_namespaces.insert(COPILOT_NAMESPACE); + filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + } + }, + ); + }) + .detach(); + + sign_in::init(cx); +} + +enum CopilotServer { + Disabled, + Starting { + task: Shared>, + }, + Error(Arc), + Started { + server: Arc, + status: SignInStatus, + }, +} + +#[derive(Clone, Debug)] +enum SignInStatus { + Authorized { + _user: String, + }, + Unauthorized { + _user: String, + }, + SigningIn { + prompt: Option, + task: Shared>>>, + }, + SignedOut, +} + +#[derive(Debug, Clone)] +pub enum Status { + Starting { + task: Shared>, + }, + Error(Arc), + Disabled, + SignedOut, + SigningIn { + prompt: Option, + }, + Unauthorized, + Authorized, +} + +impl Status { + pub fn is_authorized(&self) -> bool { + matches!(self, Status::Authorized) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Completion { + pub position: Anchor, + pub text: String, +} + +pub struct Copilot { + http: Arc, + node_runtime: Arc, + server: CopilotServer, +} + +impl Entity for Copilot { + type Event = (); +} + +impl Copilot { + pub fn starting_task(&self) -> Option>> { + match self.server { + CopilotServer::Starting { ref task } => Some(task.clone()), + _ => None, + } + } + + pub fn global(cx: &AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) + } else { + None + } + } + + fn start( + http: Arc, + node_runtime: Arc, + cx: &mut ModelContext, + ) -> Self { + cx.observe_global::({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| { + if cx.global::().enable_copilot_integration { + if matches!(this.server, CopilotServer::Disabled) { + let start_task = cx + .spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| { + Self::start_language_server(http, node_runtime, this, cx) + } + }) + .shared(); + this.server = CopilotServer::Starting { task: start_task }; + cx.notify(); + } + } else { + this.server = CopilotServer::Disabled; + cx.notify(); + } + } + }) + .detach(); + + if cx.global::().enable_copilot_integration { + let start_task = cx + .spawn({ + let http = http.clone(); + let node_runtime = node_runtime.clone(); + move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + }) + .shared(); + + Self { + http, + node_runtime, + server: CopilotServer::Starting { task: start_task }, + } + } else { + Self { + http, + node_runtime, + server: CopilotServer::Disabled, + } + } + } + + fn start_language_server( + http: Arc, + node_runtime: Arc, + this: ModelHandle, + mut cx: AsyncAppContext, + ) -> impl Future { + async move { + let start_language_server = async { + let server_path = get_copilot_lsp(http).await?; + let node_path = node_runtime.binary_path().await?; + let arguments: &[OsString] = &[server_path.into(), "--stdio".into()]; + let server = LanguageServer::new( + 0, + &node_path, + arguments, + Path::new("/"), + None, + cx.clone(), + )?; + + let server = server.initialize(Default::default()).await?; + let status = server + .request::(request::CheckStatusParams { + local_checks_only: false, + }) + .await?; + anyhow::Ok((server, status)) + }; + + let server = start_language_server.await; + this.update(&mut cx, |this, cx| { + cx.notify(); + match server { + Ok((server, status)) => { + this.server = CopilotServer::Started { + server, + status: SignInStatus::SignedOut, + }; + this.update_sign_in_status(status, cx); + } + Err(error) => { + this.server = CopilotServer::Error(error.to_string().into()); + cx.notify() + } + } + }) + } + } + + fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { + if let CopilotServer::Started { server, status } = &mut self.server { + let task = match status { + SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { + Task::ready(Ok(())).shared() + } + SignInStatus::SigningIn { task, .. } => { + cx.notify(); + task.clone() + } + SignInStatus::SignedOut => { + let server = server.clone(); + let task = cx + .spawn(|this, mut cx| async move { + let sign_in = async { + let sign_in = server + .request::( + request::SignInInitiateParams {}, + ) + .await?; + match sign_in { + request::SignInInitiateResult::AlreadySignedIn { user } => { + Ok(request::SignInStatus::Ok { user }) + } + request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { + this.update(&mut cx, |this, cx| { + if let CopilotServer::Started { status, .. } = + &mut this.server + { + if let SignInStatus::SigningIn { + prompt: prompt_flow, + .. + } = status + { + *prompt_flow = Some(flow.clone()); + cx.notify(); + } + } + }); + let response = server + .request::( + request::SignInConfirmParams { + user_code: flow.user_code, + }, + ) + .await?; + Ok(response) + } + } + }; + + let sign_in = sign_in.await; + this.update(&mut cx, |this, cx| match sign_in { + Ok(status) => { + this.update_sign_in_status(status, cx); + Ok(()) + } + Err(error) => { + this.update_sign_in_status( + request::SignInStatus::NotSignedIn, + cx, + ); + Err(Arc::new(error)) + } + }) + }) + .shared(); + *status = SignInStatus::SigningIn { + prompt: None, + task: task.clone(), + }; + cx.notify(); + task + } + }; + + cx.foreground() + .spawn(task.map_err(|err| anyhow!("{:?}", err))) + } else { + // If we're downloading, wait until download is finished + // If we're in a stuck state, display to the user + Task::ready(Err(anyhow!("copilot hasn't started yet"))) + } + } + + fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { + if let CopilotServer::Started { server, status } = &mut self.server { + *status = SignInStatus::SignedOut; + cx.notify(); + + let server = server.clone(); + cx.background().spawn(async move { + server + .request::(request::SignOutParams {}) + .await?; + anyhow::Ok(()) + }) + } else { + Task::ready(Err(anyhow!("copilot hasn't started yet"))) + } + } + + fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { + let start_task = cx + .spawn({ + let http = self.http.clone(); + let node_runtime = self.node_runtime.clone(); + move |this, cx| async move { + clear_copilot_dir().await; + Self::start_language_server(http, node_runtime, this, cx).await + } + }) + .shared(); + + self.server = CopilotServer::Starting { + task: start_task.clone(), + }; + + cx.notify(); + + cx.foreground().spawn(start_task) + } + + pub fn completion( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> + where + T: ToPointUtf16, + { + let server = match self.authorized_server() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + + let buffer = buffer.read(cx).snapshot(); + let request = server + .request::(build_completion_params(&buffer, position, cx)); + cx.background().spawn(async move { + let result = request.await?; + let completion = result + .completions + .into_iter() + .next() + .map(|completion| completion_from_lsp(completion, &buffer)); + anyhow::Ok(completion) + }) + } + + pub fn completions_cycling( + &self, + buffer: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> + where + T: ToPointUtf16, + { + let server = match self.authorized_server() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + + let buffer = buffer.read(cx).snapshot(); + let request = server.request::(build_completion_params( + &buffer, position, cx, + )); + cx.background().spawn(async move { + let result = request.await?; + let completions = result + .completions + .into_iter() + .map(|completion| completion_from_lsp(completion, &buffer)) + .collect(); + anyhow::Ok(completions) + }) + } + + pub fn status(&self) -> Status { + match &self.server { + CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, + CopilotServer::Disabled => Status::Disabled, + CopilotServer::Error(error) => Status::Error(error.clone()), + CopilotServer::Started { status, .. } => match status { + SignInStatus::Authorized { .. } => Status::Authorized, + SignInStatus::Unauthorized { .. } => Status::Unauthorized, + SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { + prompt: prompt.clone(), + }, + SignInStatus::SignedOut => Status::SignedOut, + }, + } + } + + fn update_sign_in_status( + &mut self, + lsp_status: request::SignInStatus, + cx: &mut ModelContext, + ) { + if let CopilotServer::Started { status, .. } = &mut self.server { + *status = match lsp_status { + request::SignInStatus::Ok { user } + | request::SignInStatus::MaybeOk { user } + | request::SignInStatus::AlreadySignedIn { user } => { + SignInStatus::Authorized { _user: user } + } + request::SignInStatus::NotAuthorized { user } => { + SignInStatus::Unauthorized { _user: user } + } + request::SignInStatus::NotSignedIn => SignInStatus::SignedOut, + }; + cx.notify(); + } + } + + fn authorized_server(&self) -> Result> { + match &self.server { + CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")), + CopilotServer::Disabled => Err(anyhow!("copilot is disabled")), + CopilotServer::Error(error) => Err(anyhow!( + "copilot was not started because of an error: {}", + error + )), + CopilotServer::Started { server, status } => { + if matches!(status, SignInStatus::Authorized { .. }) { + Ok(server.clone()) + } else { + Err(anyhow!("must sign in before using copilot")) + } + } + } + } +} + +fn build_completion_params( + buffer: &BufferSnapshot, + position: T, + cx: &AppContext, +) -> request::GetCompletionsParams +where + T: ToPointUtf16, +{ + let position = position.to_point_utf16(&buffer); + let language_name = buffer.language_at(position).map(|language| language.name()); + let language_name = language_name.as_deref(); + + let path; + let relative_path; + if let Some(file) = buffer.file() { + if let Some(file) = file.as_local() { + path = file.abs_path(cx); + } else { + path = file.full_path(cx); + } + relative_path = file.path().to_path_buf(); + } else { + path = PathBuf::from("/untitled"); + relative_path = PathBuf::from("untitled"); + } + + let settings = cx.global::(); + let language_id = match language_name { + Some("Plain Text") => "plaintext".to_string(), + Some(language_name) => language_name.to_lowercase(), + None => "plaintext".to_string(), + }; + request::GetCompletionsParams { + doc: request::GetCompletionsDocument { + source: buffer.text(), + tab_size: settings.tab_size(language_name).into(), + indent_size: 1, + insert_spaces: !settings.hard_tabs(language_name), + uri: lsp::Url::from_file_path(&path).unwrap(), + path: path.to_string_lossy().into(), + relative_path: relative_path.to_string_lossy().into(), + language_id, + position: point_to_lsp(position), + version: 0, + }, + } +} + +fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) -> Completion { + let position = buffer.clip_point_utf16(point_from_lsp(completion.position), Bias::Left); + Completion { + position: buffer.anchor_before(position), + text: completion.display_text, + } +} + +async fn clear_copilot_dir() { + remove_matching(&paths::COPILOT_DIR, |_| true).await +} + +async fn get_copilot_lsp(http: Arc) -> anyhow::Result { + const SERVER_PATH: &'static str = "dist/agent.js"; + + ///Check for the latest copilot language server and download it if we haven't already + async fn fetch_latest(http: Arc) -> anyhow::Result { + let release = latest_github_release("zed-industries/copilot", http.clone()).await?; + + let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name)); + + fs::create_dir_all(version_dir).await?; + let server_path = version_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + // Copilot LSP looks for this dist dir specifcially, so lets add it in. + let dist_dir = version_dir.join("dist"); + fs::create_dir_all(dist_dir.as_path()).await?; + + let url = &release + .assets + .get(0) + .context("Github release for copilot contained no assets")? + .browser_download_url; + + let mut response = http + .get(&url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading copilot release: {}", err))?; + let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); + let archive = Archive::new(decompressed_bytes); + archive.unpack(dist_dir).await?; + + remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await; + } + + Ok(server_path) + } + + match fetch_latest(http).await { + ok @ Result::Ok(..) => ok, + e @ Err(..) => { + e.log_err(); + // Fetch a cached binary, if it exists + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = + last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(server_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + } + } +} diff --git a/crates/copilot/src/editor.rs b/crates/copilot/src/editor.rs new file mode 100644 index 0000000000000000000000000000000000000000..7fc4204449517551186cdb0424fd1ea9d8bc1176 --- /dev/null +++ b/crates/copilot/src/editor.rs @@ -0,0 +1,3 @@ +use gpui::MutableAppContext; + +fn init(cx: &mut MutableAppContext) {} diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea7f4577b6864fff2d4c52916eb3eefc99e0f975 --- /dev/null +++ b/crates/copilot/src/request.rs @@ -0,0 +1,142 @@ +use serde::{Deserialize, Serialize}; + +pub enum CheckStatus {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckStatusParams { + pub local_checks_only: bool, +} + +impl lsp::request::Request for CheckStatus { + type Params = CheckStatusParams; + type Result = SignInStatus; + const METHOD: &'static str = "checkStatus"; +} + +pub enum SignInInitiate {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SignInInitiateParams {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum SignInInitiateResult { + AlreadySignedIn { user: String }, + PromptUserDeviceFlow(PromptUserDeviceFlow), +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PromptUserDeviceFlow { + pub user_code: String, + pub verification_uri: String, +} + +impl lsp::request::Request for SignInInitiate { + type Params = SignInInitiateParams; + type Result = SignInInitiateResult; + const METHOD: &'static str = "signInInitiate"; +} + +pub enum SignInConfirm {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignInConfirmParams { + pub user_code: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum SignInStatus { + #[serde(rename = "OK")] + Ok { + user: String, + }, + MaybeOk { + user: String, + }, + AlreadySignedIn { + user: String, + }, + NotAuthorized { + user: String, + }, + NotSignedIn, +} + +impl lsp::request::Request for SignInConfirm { + type Params = SignInConfirmParams; + type Result = SignInStatus; + const METHOD: &'static str = "signInConfirm"; +} + +pub enum SignOut {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignOutParams {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignOutResult {} + +impl lsp::request::Request for SignOut { + type Params = SignOutParams; + type Result = SignOutResult; + const METHOD: &'static str = "signOut"; +} + +pub enum GetCompletions {} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsParams { + pub doc: GetCompletionsDocument, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsDocument { + pub source: String, + pub tab_size: u32, + pub indent_size: u32, + pub insert_spaces: bool, + pub uri: lsp::Url, + pub path: String, + pub relative_path: String, + pub language_id: String, + pub position: lsp::Position, + pub version: usize, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCompletionsResult { + pub completions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Completion { + pub text: String, + pub position: lsp::Position, + pub uuid: String, + pub range: lsp::Range, + pub display_text: String, +} + +impl lsp::request::Request for GetCompletions { + type Params = GetCompletionsParams; + type Result = GetCompletionsResult; + const METHOD: &'static str = "getCompletions"; +} + +pub enum GetCompletionsCycling {} + +impl lsp::request::Request for GetCompletionsCycling { + type Params = GetCompletionsParams; + type Result = GetCompletionsResult; + const METHOD: &'static str = "getCompletionsCycling"; +} diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs new file mode 100644 index 0000000000000000000000000000000000000000..2827aeac2d4fac726f4f6b70019d413784ade2a8 --- /dev/null +++ b/crates/copilot/src/sign_in.rs @@ -0,0 +1,344 @@ +use crate::{request::PromptUserDeviceFlow, Copilot, Status}; +use gpui::{ + elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View, + ViewContext, ViewHandle, WindowKind, WindowOptions, +}; +use settings::Settings; +use theme::ui::modal; + +#[derive(PartialEq, Eq, Debug, Clone)] +struct CopyUserCode; + +#[derive(PartialEq, Eq, Debug, Clone)] +struct OpenGithub; + +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; + +pub fn init(cx: &mut MutableAppContext) { + let copilot = Copilot::global(cx).unwrap(); + + let mut code_verification: Option> = None; + cx.observe(&copilot, move |copilot, cx| { + let status = copilot.read(cx).status(); + + match &status { + crate::Status::SigningIn { prompt } => { + if let Some(code_verification) = code_verification.as_ref() { + code_verification.update(cx, |code_verification, cx| { + code_verification.set_status(status, cx) + }); + cx.activate_window(code_verification.window_id()); + } else if let Some(_prompt) = prompt { + let window_size = cx.global::().theme.copilot.modal.dimensions(); + let window_options = WindowOptions { + bounds: gpui::WindowBounds::Fixed(RectF::new( + Default::default(), + window_size, + )), + titlebar: None, + center: true, + focus: true, + kind: WindowKind::Normal, + is_movable: true, + screen: None, + }; + let (_, view) = + cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status)); + code_verification = Some(view); + } + } + Status::Authorized | Status::Unauthorized => { + if let Some(code_verification) = code_verification.as_ref() { + code_verification.update(cx, |code_verification, cx| { + code_verification.set_status(status, cx) + }); + + cx.platform().activate(true); + cx.activate_window(code_verification.window_id()); + } + } + _ => { + if let Some(code_verification) = code_verification.take() { + cx.remove_window(code_verification.window_id()); + } + } + } + }) + .detach(); +} + +pub struct CopilotCodeVerification { + status: Status, +} + +impl CopilotCodeVerification { + pub fn new(status: Status) -> Self { + Self { status } + } + + pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + self.status = status; + cx.notify(); + } + + fn render_device_code( + data: &PromptUserDeviceFlow, + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &data.user_code) + .unwrap_or(false); + + let device_code_style = &style.auth.prompting.device_code; + + MouseEventHandler::::new(0, cx, |state, _cx| { + Flex::row() + .with_children([ + Label::new(data.user_code.clone(), device_code_style.text.clone()) + .aligned() + .contained() + .with_style(device_code_style.left_container) + .constrained() + .with_width(device_code_style.left) + .boxed(), + Label::new( + if copied { "Copied!" } else { "Copy" }, + device_code_style.cta.style_for(state, false).text.clone(), + ) + .aligned() + .contained() + .with_style(*device_code_style.right_container.style_for(state, false)) + .constrained() + .with_width(device_code_style.right) + .boxed(), + ]) + .contained() + .with_style(device_code_style.cta.style_for(state, false).container) + .boxed() + }) + .on_click(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + cx.platform() + .write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed() + } + + fn render_prompting_modal( + data: &PromptUserDeviceFlow, + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + style.auth.prompting.subheading.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "your existing license.", + style.auth.prompting.subheading.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.prompting.subheading.container) + .boxed(), + Self::render_device_code(data, &style, cx), + Flex::column() + .with_children([ + Label::new( + "Paste this code into GitHub after", + style.auth.prompting.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "clicking the button below.", + style.auth.prompting.hint.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(style.auth.prompting.hint.container.clone()) + .boxed(), + theme::ui::cta_button_with_click( + "Connect to GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + { + let verification_uri = data.verification_uri.clone(); + move |_, cx| cx.platform().open_url(&verification_uri) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() + } + fn render_enabled_modal( + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + let enabled_style = &style.auth.authorized; + Flex::column() + .with_children([ + Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) + .contained() + .with_style(enabled_style.subheading.container) + .aligned() + .boxed(), + Flex::column() + .with_children([ + Label::new( + "You can update your settings or", + enabled_style.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "sign out from the Copilot menu in", + enabled_style.hint.text.clone(), + ) + .aligned() + .boxed(), + Label::new("the status bar.", enabled_style.hint.text.clone()) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(enabled_style.hint.container) + .boxed(), + theme::ui::cta_button_with_click( + "Done", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() + } + fn render_unauthorized_modal( + style: &theme::Copilot, + cx: &mut gpui::RenderContext, + ) -> ElementBox { + let unauthorized_style = &style.auth.not_authorized; + + Flex::column() + .with_children([ + Flex::column() + .with_children([ + Label::new( + "Enable Copilot by connecting", + unauthorized_style.subheading.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "your existing license.", + unauthorized_style.subheading.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(unauthorized_style.subheading.container) + .boxed(), + Flex::column() + .with_children([ + Label::new( + "You must have an active copilot", + unauthorized_style.warning.text.clone(), + ) + .aligned() + .boxed(), + Label::new( + "license to use it in Zed.", + unauthorized_style.warning.text.clone(), + ) + .aligned() + .boxed(), + ]) + .align_children_center() + .contained() + .with_style(unauthorized_style.warning.container) + .boxed(), + theme::ui::cta_button_with_click( + "Subscribe on GitHub", + style.auth.content_width, + &style.auth.cta_button, + cx, + |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id); + cx.platform().open_url(COPILOT_SIGN_UP_URL) + }, + ) + .boxed(), + ]) + .align_children_center() + .boxed() + } +} + +impl Entity for CopilotCodeVerification { + type Event = (); +} + +impl View for CopilotCodeVerification { + fn ui_name() -> &'static str { + "CopilotCodeVerification" + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { + cx.notify() + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext) { + cx.notify() + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let style = cx.global::().theme.clone(); + + modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| { + Flex::column() + .with_children([ + theme::ui::icon(&style.copilot.auth.header).boxed(), + match &self.status { + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(&prompt, &style.copilot, cx), + Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), + Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), + _ => Empty::new().boxed(), + }, + ]) + .align_children_center() + .boxed() + }) + } +} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f44493b32394c8c64d533d451f1e201290f75716 --- /dev/null +++ b/crates/copilot_button/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "copilot_button" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/copilot_button.rs" +doctest = false + +[dependencies] +copilot = { path = "../copilot" } +editor = { path = "../editor" } +context_menu = { path = "../context_menu" } +gpui = { path = "../gpui" } +settings = { path = "../settings" } +theme = { path = "../theme" } +util = { path = "../util" } +workspace = { path = "../workspace" } +anyhow = "1.0" +smol = "1.2.5" +futures = "0.3" diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc6aee872134b9359e3611a0a341ab0590415cbf --- /dev/null +++ b/crates/copilot_button/src/copilot_button.rs @@ -0,0 +1,360 @@ +use std::sync::Arc; + +use context_menu::{ContextMenu, ContextMenuItem}; +use editor::Editor; +use gpui::{ + elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton, + MouseState, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, +}; +use settings::{settings_file::SettingsFile, Settings}; +use workspace::{ + item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast, + StatusItemView, +}; + +use copilot::{Copilot, Reinstall, SignIn, SignOut, Status}; + +const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; +const COPILOT_STARTING_TOAST_ID: usize = 1337; +const COPILOT_ERROR_TOAST_ID: usize = 1338; + +#[derive(Clone, PartialEq)] +pub struct DeployCopilotMenu; + +#[derive(Clone, PartialEq)] +pub struct ToggleCopilotForLanguage { + language: Arc, +} + +#[derive(Clone, PartialEq)] +pub struct ToggleCopilotGlobally; + +// TODO: Make the other code path use `get_or_insert` logic for this modal +#[derive(Clone, PartialEq)] +pub struct DeployCopilotModal; + +impl_internal_actions!( + copilot, + [ + DeployCopilotMenu, + DeployCopilotModal, + ToggleCopilotForLanguage, + ToggleCopilotGlobally, + ] +); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(CopilotButton::deploy_copilot_menu); + cx.add_action( + |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { + let language = action.language.to_owned(); + + let current_langauge = cx.global::().copilot_on(Some(&language)); + + SettingsFile::update(cx, move |file_contents| { + file_contents.languages.insert( + language.to_owned(), + settings::EditorSettings { + copilot: Some((!current_langauge).into()), + ..Default::default() + }, + ); + }) + }, + ); + + cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| { + let copilot_on = cx.global::().copilot_on(None); + + SettingsFile::update(cx, move |file_contents| { + file_contents.editor.copilot = Some((!copilot_on).into()) + }) + }); +} + +pub struct CopilotButton { + popup_menu: ViewHandle, + editor_subscription: Option<(Subscription, usize)>, + editor_enabled: Option, + language: Option>, +} + +impl Entity for CopilotButton { + type Event = (); +} + +impl View for CopilotButton { + fn ui_name() -> &'static str { + "CopilotButton" + } + + fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + + if !settings.enable_copilot_integration { + return Empty::new().boxed(); + } + + let theme = settings.theme.clone(); + let active = self.popup_menu.read(cx).visible(); + let Some(copilot) = Copilot::global(cx) else { + return Empty::new().boxed(); + }; + let status = copilot.read(cx).status(); + + let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + + let view_id = cx.view_id(); + + Stack::new() + .with_child( + MouseEventHandler::::new(0, cx, { + let theme = theme.clone(); + let status = status.clone(); + move |state, _cx| { + let style = theme + .workspace + .status_bar + .sidebar_buttons + .item + .style_for(state, active); + + Flex::row() + .with_child( + Svg::new({ + match status { + Status::Error(_) => "icons/copilot_error_16.svg", + Status::Authorized => { + if enabled { + "icons/copilot_16.svg" + } else { + "icons/copilot_disabled_16.svg" + } + } + _ => "icons/copilot_init_16.svg", + } + }) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned() + .named("copilot-icon"), + ) + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + .boxed() + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let status = status.clone(); + move |_, cx| match status { + Status::Authorized => cx.dispatch_action(DeployCopilotMenu), + Status::Starting { ref task } => { + cx.dispatch_action(workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot is starting...", + )); + let window_id = cx.window_id(); + let task = task.to_owned(); + cx.spawn(|mut cx| async move { + task.await; + cx.update(|cx| { + if let Some(copilot) = Copilot::global(cx) { + let status = copilot.read(cx).status(); + match status { + Status::Authorized => cx.dispatch_action_at( + window_id, + view_id, + workspace::Toast::new( + COPILOT_STARTING_TOAST_ID, + "Copilot has started!", + ), + ), + _ => { + cx.dispatch_action_at( + window_id, + view_id, + DismissToast::new(COPILOT_STARTING_TOAST_ID), + ); + cx.dispatch_global_action(SignIn) + } + } + } + }) + }) + .detach(); + } + Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action( + COPILOT_ERROR_TOAST_ID, + format!("Copilot can't be started: {}", e), + "Reinstall Copilot", + Reinstall, + )), + _ => cx.dispatch_action(SignIn), + } + }) + .with_tooltip::( + 0, + "GitHub Copilot".into(), + None, + theme.tooltip.clone(), + cx, + ) + .boxed(), + ) + .with_child( + ChildView::new(&self.popup_menu, cx) + .aligned() + .top() + .right() + .boxed(), + ) + .boxed() + } +} + +impl CopilotButton { + pub fn new(cx: &mut ViewContext) -> Self { + let menu = cx.add_view(|cx| { + let mut menu = ContextMenu::new(cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }); + + cx.observe(&menu, |_, _, cx| cx.notify()).detach(); + + Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); + + let this_handle = cx.handle().downgrade(); + cx.observe_global::(move |cx| { + if let Some(handle) = this_handle.upgrade(cx) { + handle.update(cx, |_, cx| cx.notify()) + } + }) + .detach(); + + Self { + popup_menu: menu, + editor_subscription: None, + editor_enabled: None, + language: None, + } + } + + pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext) { + let settings = cx.global::(); + + let mut menu_options = Vec::with_capacity(6); + + if let Some((_, view_id)) = self.editor_subscription.as_ref() { + let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); + menu_options.push(ContextMenuItem::item_for_view( + if locally_enabled { + "Pause Copilot for this file" + } else { + "Resume Copilot for this file" + }, + *view_id, + copilot::Toggle, + )); + } + + if let Some(language) = &self.language { + let language_enabled = settings.copilot_on(Some(language.as_ref())); + + menu_options.push(ContextMenuItem::item( + format!( + "{} Copilot for {}", + if language_enabled { + "Disable" + } else { + "Enable" + }, + language + ), + ToggleCopilotForLanguage { + language: language.to_owned(), + }, + )); + } + + let globally_enabled = cx.global::().copilot_on(None); + menu_options.push(ContextMenuItem::item( + if globally_enabled { + "Disable Copilot Globally" + } else { + "Enable Copilot Locally" + }, + ToggleCopilotGlobally, + )); + + menu_options.push(ContextMenuItem::Separator); + + let icon_style = settings.theme.copilot.out_link_icon.clone(); + menu_options.push(ContextMenuItem::element_item( + Box::new( + move |state: &mut MouseState, style: &theme::ContextMenuItem| { + Flex::row() + .with_children([ + Label::new("Copilot Settings", style.label.clone()).boxed(), + theme::ui::icon(icon_style.style_for(state, false)).boxed(), + ]) + .align_children_center() + .boxed() + }, + ), + OsOpen::new(COPILOT_SETTINGS_URL), + )); + + menu_options.push(ContextMenuItem::item("Sign Out", SignOut)); + + self.popup_menu.update(cx, |menu, cx| { + menu.show( + Default::default(), + AnchorCorner::BottomRight, + menu_options, + cx, + ); + }); + } + + pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + let editor = editor.read(cx); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + let settings = cx.global::(); + let suggestion_anchor = editor.selections.newest_anchor().start; + + let language_name = snapshot + .language_at(suggestion_anchor) + .map(|language| language.name()); + + self.language = language_name.clone(); + + if let Some(enabled) = editor.copilot_state.user_enabled { + self.editor_enabled = Some(enabled); + } else { + self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); + } + + cx.notify() + } +} + +impl StatusItemView for CopilotButton { + fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + if let Some(editor) = item.map(|item| item.act_as::(cx)).flatten() { + self.editor_subscription = + Some((cx.observe(&editor, Self::update_enabled), editor.id())); + self.update_enabled(editor, cx); + } else { + self.language = None; + self.editor_subscription = None; + self.editor_enabled = None; + } + cx.notify(); + } +} diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index 16ec37019a70007bca4c9528d578d8d7efd353cd..767bf57ba9439d9550baabe737bb0b5df22151ea 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -23,8 +23,8 @@ async-trait = "0.1" lazy_static = "1.4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smol = "1.2" [dev-dependencies] diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index e28a378a679952bfacdd81cb54738f4c23e458ec..8ef2546b5d4be8f0837d94eb19a6a20506e727cc 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -29,4 +29,4 @@ editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { workspace = true } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 44d0936808cedec50f64f545d3db007acbc30f3e..ef2489d7ec545e7a923c5de91d39132e2425ebb0 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -22,10 +22,10 @@ test-support = [ ] [dependencies] -drag_and_drop = { path = "../drag_and_drop" } -text = { path = "../text" } clock = { path = "../clock" } +copilot = { path = "../copilot" } db = { path = "../db" } +drag_and_drop = { path = "../drag_and_drop" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } fuzzy = { path = "../fuzzy" } @@ -38,10 +38,12 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } +text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } sqlez = { path = "../sqlez" } workspace = { path = "../workspace" } + aho-corasick = "0.7" anyhow = "1.0" futures = "0.3" @@ -54,7 +56,7 @@ parking_lot = "0.11" postage = { workspace = true } rand = { version = "0.8.3", optional = true } serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" tree-sitter-rust = { version = "*", optional = true } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4d8313520587a669919a0a540302162502bfa42e..c9869e7a9cc05ee480c0541df350a5e3d12dc3a6 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -382,7 +382,7 @@ impl DisplaySnapshot { /// Returns text chunks starting at the given display row until the end of the file pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.block_snapshot - .chunks(display_row..self.max_point().row() + 1, false, None) + .chunks(display_row..self.max_point().row() + 1, false, None, None) .map(|h| h.text) } @@ -390,7 +390,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None) + .chunks(row..row + 1, false, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -398,9 +398,18 @@ impl DisplaySnapshot { }) } - pub fn chunks(&self, display_rows: Range, language_aware: bool) -> DisplayChunks<'_> { - self.block_snapshot - .chunks(display_rows, language_aware, Some(&self.text_highlights)) + pub fn chunks( + &self, + display_rows: Range, + language_aware: bool, + suggestion_highlight: Option, + ) -> DisplayChunks<'_> { + self.block_snapshot.chunks( + display_rows, + language_aware, + Some(&self.text_highlights), + suggestion_highlight, + ) } pub fn chars_at( @@ -1687,7 +1696,7 @@ pub mod tests { ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option, Option)> = Vec::new(); - for chunk in snapshot.chunks(rows, true) { + for chunk in snapshot.chunks(rows, true, None) { let syntax_color = chunk .syntax_highlight_id .and_then(|id| id.style(theme)?.color); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index adea668179555db73e28cb99a45396a350aedf0d..c4af03e7037d5073ae0ae76d263a3e759f680bc4 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{ElementBox, RenderContext}; +use gpui::{fonts::HighlightStyle, ElementBox, RenderContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -572,7 +572,7 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows, false, None) + self.chunks(0..self.transforms.summary().output_rows, false, None, None) .map(|chunk| chunk.text) .collect() } @@ -582,6 +582,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -614,6 +615,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), input_chunk: Default::default(), transforms: cursor, @@ -1498,6 +1500,7 @@ mod tests { start_row as u32..blocks_snapshot.max_point().row + 1, false, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/suggestion_map.rs b/crates/editor/src/display_map/suggestion_map.rs index 5be4f6f0df9e2e5ebebeac2706199baa6245e719..0fc2a025febdfbee59fcdcc441ac47ee781d852f 100644 --- a/crates/editor/src/display_map/suggestion_map.rs +++ b/crates/editor/src/display_map/suggestion_map.rs @@ -60,7 +60,6 @@ impl SuggestionPoint { pub struct Suggestion { pub position: T, pub text: Rope, - pub highlight_style: HighlightStyle, } pub struct SuggestionMap(Mutex); @@ -93,7 +92,6 @@ impl SuggestionMap { Suggestion { position: fold_offset, text: new_suggestion.text, - highlight_style: new_suggestion.highlight_style, } }); @@ -395,7 +393,7 @@ impl SuggestionSnapshot { pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator { let start = self.to_offset(start); - self.chunks(start..self.len(), false, None) + self.chunks(start..self.len(), false, None, None) .flat_map(|chunk| chunk.text.chars()) } @@ -404,6 +402,7 @@ impl SuggestionSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> SuggestionChunks<'a> { if let Some(suggestion) = self.suggestion.as_ref() { let suggestion_range = @@ -447,7 +446,7 @@ impl SuggestionSnapshot { prefix_chunks, suggestion_chunks, suffix_chunks, - highlight_style: suggestion.highlight_style, + highlight_style: suggestion_highlight, } } else { SuggestionChunks { @@ -458,7 +457,7 @@ impl SuggestionSnapshot { )), suggestion_chunks: None, suffix_chunks: None, - highlight_style: Default::default(), + highlight_style: None, } } } @@ -493,7 +492,7 @@ impl SuggestionSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None) + self.chunks(Default::default()..self.len(), false, None, None) .map(|chunk| chunk.text) .collect() } @@ -503,7 +502,7 @@ pub struct SuggestionChunks<'a> { prefix_chunks: Option>, suggestion_chunks: Option>, suffix_chunks: Option>, - highlight_style: HighlightStyle, + highlight_style: Option, } impl<'a> Iterator for SuggestionChunks<'a> { @@ -523,7 +522,7 @@ impl<'a> Iterator for SuggestionChunks<'a> { return Some(Chunk { text: chunk, syntax_highlight_id: None, - highlight_style: Some(self.highlight_style), + highlight_style: self.highlight_style, diagnostic_severity: None, is_unnecessary: false, }); @@ -589,7 +588,6 @@ mod tests { Some(Suggestion { position: 3, text: "123\n456".into(), - highlight_style: Default::default(), }), fold_snapshot, Default::default(), @@ -718,7 +716,12 @@ mod tests { start = expected_text.clip_offset(start, Bias::Right); let actual_text = suggestion_snapshot - .chunks(SuggestionOffset(start)..SuggestionOffset(end), false, None) + .chunks( + SuggestionOffset(start)..SuggestionOffset(end), + false, + None, + None, + ) .map(|chunk| chunk.text) .collect::(); assert_eq!( @@ -842,7 +845,6 @@ mod tests { .collect::() .as_str() .into(), - highlight_style: Default::default(), }) }; diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index f5776270a0ff343b19558da2611256d715b41a5f..52290b773b4d0ce6f1b3c85fa2b85a9896e90931 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -3,6 +3,7 @@ use super::{ TextHighlights, }; use crate::MultiBufferSnapshot; +use gpui::fonts::HighlightStyle; use language::{Chunk, Point}; use parking_lot::Mutex; use std::{cmp, mem, num::NonZeroU32, ops::Range}; @@ -71,6 +72,7 @@ impl TabMap { suggestion_edit.old.end..old_max_offset, false, None, + None, ) { for (ix, mat) in chunk.text.match_indices(&['\t', '\n']) { let offset_from_edit = offset_from_edit + (ix as u32); @@ -200,7 +202,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None) + .chunks(range.start..line_end, false, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -214,7 +216,12 @@ impl TabSnapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None) + .chunks( + TabPoint::new(range.end.row(), 0)..range.end, + false, + None, + None, + ) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -235,6 +242,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_suggestion_point(range.start, Bias::Left); @@ -254,6 +262,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), input_column, column: expanded_char_column, @@ -275,7 +284,7 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None) + self.chunks(TabPoint::zero()..self.max_point(), false, None, None) .map(|chunk| chunk.text) .collect() } @@ -606,7 +615,8 @@ mod tests { .chunks( TabPoint::new(0, ix as u32)..tab_snapshot.max_point(), false, - None + None, + None, ) .map(|c| c.text) .collect::(), @@ -701,7 +711,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None) + .chunks(start..end, false, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 311233850d44a37643a2be8d7ca2d924f7c0745d..160b61650d55bd9aebbe7388c0c9861a18248f1a 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -5,8 +5,9 @@ use super::{ }; use crate::MultiBufferSnapshot; use gpui::{ - fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext, - Task, + fonts::{FontId, HighlightStyle}, + text_layout::LineWrapper, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; use language::{Chunk, Point}; use lazy_static::lazy_static; @@ -444,6 +445,7 @@ impl WrapSnapshot { TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), false, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -573,6 +575,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + suggestion_highlight: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -590,6 +593,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, + suggestion_highlight, ), input_chunk: Default::default(), output_position: output_start, @@ -1316,7 +1320,7 @@ mod tests { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false, None) + self.chunks(wrap_row..self.max_point().row() + 1, false, None, None) .map(|h| h.text) } @@ -1340,7 +1344,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None) + .chunks(start_row..end_row, true, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b9388dca7868e1be63059932d1e3599eec0b3efb..e0ab8d84b45777a11dc5860d5857dea758cb3bef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,6 +24,7 @@ use anyhow::Result; use blink_manager::BlinkManager; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; @@ -388,6 +389,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::rename); cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); + cx.add_action(Editor::next_copilot_suggestion); + cx.add_action(Editor::previous_copilot_suggestion); + cx.add_action(Editor::toggle_copilot_suggestions); hover_popover::init(cx); link_go_to_definition::init(cx); @@ -506,6 +510,7 @@ pub struct Editor { hover_state: HoverState, gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, + pub copilot_state: CopilotState, _subscriptions: Vec, } @@ -1003,6 +1008,65 @@ impl CodeActionsMenu { } } +pub struct CopilotState { + excerpt_id: Option, + pending_refresh: Task>, + completions: Vec, + active_completion_index: usize, + pub user_enabled: Option, +} + +impl Default for CopilotState { + fn default() -> Self { + Self { + excerpt_id: None, + pending_refresh: Task::ready(Some(())), + completions: Default::default(), + active_completion_index: 0, + user_enabled: None, + } + } +} + +impl CopilotState { + fn text_for_active_completion( + &self, + cursor: Anchor, + buffer: &MultiBufferSnapshot, + ) -> Option<&str> { + let cursor_offset = cursor.to_offset(buffer); + let completion = self.completions.get(self.active_completion_index)?; + if self.excerpt_id == Some(cursor.excerpt_id) { + let completion_offset: usize = buffer.summary_for_anchor(&Anchor { + excerpt_id: cursor.excerpt_id, + buffer_id: cursor.buffer_id, + text_anchor: completion.position, + }); + let prefix_len = cursor_offset.saturating_sub(completion_offset); + if completion_offset <= cursor_offset && prefix_len <= completion.text.len() { + let (prefix, suffix) = completion.text.split_at(prefix_len); + if buffer.contains_str_at(completion_offset, prefix) && !suffix.is_empty() { + return Some(suffix); + } + } + } + None + } + + fn push_completion( + &mut self, + new_completion: copilot::Completion, + ) -> Option<&copilot::Completion> { + for completion in &self.completions { + if *completion == new_completion { + return None; + } + } + self.completions.push(new_completion); + self.completions.last() + } +} + #[derive(Debug)] struct ActiveDiagnosticGroup { primary_range: Range, @@ -1176,6 +1240,7 @@ impl Editor { remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), + copilot_state: Default::default(), gutter_hovered: false, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -1385,6 +1450,7 @@ impl Editor { self.refresh_code_actions(cx); self.refresh_document_highlights(cx); refresh_matching_bracket_highlights(self, cx); + self.refresh_copilot_suggestions(cx); } self.blink_manager.update(cx, BlinkManager::pause_blinking); @@ -1758,6 +1824,10 @@ impl Editor { return; } + if self.clear_copilot_suggestions(cx) { + return; + } + if self.snippet_stack.pop().is_some() { return; } @@ -2677,6 +2747,234 @@ impl Editor { None } + fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext) -> Option<()> { + let copilot = Copilot::global(cx)?; + + if self.mode != EditorMode::Full { + return None; + } + + let settings = cx.global::(); + + if !self + .copilot_state + .user_enabled + .unwrap_or_else(|| settings.copilot_on(None)) + { + return None; + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest_anchor(); + + if !self.copilot_state.user_enabled.is_some() { + let language_name = snapshot + .language_at(selection.start) + .map(|language| language.name()); + + let copilot_enabled = settings.copilot_on(language_name.as_deref()); + + if !copilot_enabled { + return None; + } + } + + let cursor = if selection.start == selection.end { + selection.start.bias_left(&snapshot) + } else { + self.clear_copilot_suggestions(cx); + return None; + }; + + if let Some(new_text) = self + .copilot_state + .text_for_active_completion(cursor, &snapshot) + { + self.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position: cursor, + text: new_text.into(), + }), + cx, + ) + }); + self.copilot_state + .completions + .swap(0, self.copilot_state.active_completion_index); + self.copilot_state.completions.truncate(1); + self.copilot_state.active_completion_index = 0; + cx.notify(); + } else { + self.clear_copilot_suggestions(cx); + } + + if !copilot.read(cx).status().is_authorized() { + return None; + } + + let (buffer, buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { + let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { + ( + copilot.completion(&buffer, buffer_position, cx), + copilot.completions_cycling(&buffer, buffer_position, cx), + ) + }); + + let (completion, completions_cycling) = futures::join!(completion, completions_cycling); + let mut completions = Vec::new(); + completions.extend(completion.log_err().flatten()); + completions.extend(completions_cycling.log_err().into_iter().flatten()); + this.upgrade(&cx)?.update(&mut cx, |this, cx| { + this.copilot_state.completions.clear(); + this.copilot_state.active_completion_index = 0; + this.copilot_state.excerpt_id = Some(cursor.excerpt_id); + for completion in completions { + let was_empty = this.copilot_state.completions.is_empty(); + if let Some(completion) = this.copilot_state.push_completion(completion) { + if was_empty { + this.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position: cursor, + text: completion.text.as_str().into(), + }), + cx, + ) + }); + } + } + } + cx.notify(); + }); + + Some(()) + }); + + Some(()) + } + + fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + // Auto re-enable copilot if you're asking for a suggestion + if self.copilot_state.user_enabled == Some(false) { + cx.notify(); + self.copilot_state.user_enabled = Some(true); + } + + if self.copilot_state.completions.is_empty() { + self.refresh_copilot_suggestions(cx); + return; + } + + self.copilot_state.active_completion_index = + (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len(); + + self.sync_suggestion(cx); + } + + fn previous_copilot_suggestion( + &mut self, + _: &copilot::PreviousSuggestion, + cx: &mut ViewContext, + ) { + // Auto re-enable copilot if you're asking for a suggestion + if self.copilot_state.user_enabled == Some(false) { + cx.notify(); + self.copilot_state.user_enabled = Some(true); + } + + if self.copilot_state.completions.is_empty() { + self.refresh_copilot_suggestions(cx); + return; + } + + self.copilot_state.active_completion_index = + if self.copilot_state.active_completion_index == 0 { + self.copilot_state.completions.len() - 1 + } else { + self.copilot_state.active_completion_index - 1 + }; + + self.sync_suggestion(cx); + } + + fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext) { + self.copilot_state.user_enabled = match self.copilot_state.user_enabled { + Some(enabled) => Some(!enabled), + None => { + let selection = self.selections.newest_anchor().start; + + let language_name = self + .snapshot(cx) + .language_at(selection) + .map(|language| language.name()); + + let copilot_enabled = cx.global::().copilot_on(language_name.as_deref()); + + Some(!copilot_enabled) + } + }; + + // We know this can't be None, as we just set it to Some above + if self.copilot_state.user_enabled == Some(true) { + self.refresh_copilot_suggestions(cx); + } else { + self.clear_copilot_suggestions(cx); + } + + cx.notify(); + } + + fn sync_suggestion(&mut self, cx: &mut ViewContext) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor = self.selections.newest_anchor().head(); + + if let Some(text) = self + .copilot_state + .text_for_active_completion(cursor, &snapshot) + { + self.display_map.update(cx, |map, cx| { + map.replace_suggestion( + Some(Suggestion { + position: cursor, + text: text.into(), + }), + cx, + ) + }); + cx.notify(); + } + } + + fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor = self.selections.newest_anchor().head(); + if let Some(text) = self + .copilot_state + .text_for_active_completion(cursor, &snapshot) + { + self.insert(&text.to_string(), cx); + self.clear_copilot_suggestions(cx); + true + } else { + false + } + } + + fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext) -> bool { + self.display_map + .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + let was_empty = self.copilot_state.completions.is_empty(); + self.copilot_state.completions.clear(); + self.copilot_state.active_completion_index = 0; + self.copilot_state.pending_refresh = Task::ready(None); + self.copilot_state.excerpt_id = None; + cx.notify(); + !was_empty + } + pub fn render_code_actions_indicator( &self, style: &EditorStyle, @@ -2984,6 +3282,10 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if self.accept_copilot_suggestion(cx) { + return; + } + if self.move_to_next_snippet_tabstop(cx) { return; } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 780f8cd1d539bfff43d4c914f303a9e322efb71f..c349559d7dd09099368fc91eb1092c20a7fa5810 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1318,45 +1318,47 @@ impl EditorElement { .collect() } else { let style = &self.style; - let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| { - let mut highlight_style = chunk - .syntax_highlight_id - .and_then(|id| id.style(&style.syntax)); - - if let Some(chunk_highlight) = chunk.highlight_style { - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(chunk_highlight); - } else { - highlight_style = Some(chunk_highlight); + let chunks = snapshot + .chunks(rows.clone(), true, Some(style.theme.suggestion)) + .map(|chunk| { + let mut highlight_style = chunk + .syntax_highlight_id + .and_then(|id| id.style(&style.syntax)); + + if let Some(chunk_highlight) = chunk.highlight_style { + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(chunk_highlight); + } else { + highlight_style = Some(chunk_highlight); + } } - } - let mut diagnostic_highlight = HighlightStyle::default(); + let mut diagnostic_highlight = HighlightStyle::default(); - if chunk.is_unnecessary { - diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } + if chunk.is_unnecessary { + diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); + } - if let Some(severity) = chunk.diagnostic_severity { - // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. - if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } } - } - if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(diagnostic_highlight); - } else { - highlight_style = Some(diagnostic_highlight); - } + if let Some(highlight_style) = highlight_style.as_mut() { + highlight_style.highlight(diagnostic_highlight); + } else { + highlight_style = Some(diagnostic_highlight); + } - (chunk.text, highlight_style) - }); + (chunk.text, highlight_style) + }); layout_highlighted_chunks( chunks, &style.text, diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 1c0d0e93ea4549d052286b4161362acacf540b78..57b91876e388f9e2c60101c514daa7a661651b42 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -24,8 +24,8 @@ lazy_static = "1.4.0" postage = { workspace = true } project = { path = "../project" } search = { path = "../search" } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } settings = { path = "../settings" } sysinfo = "0.27.1" theme = { path = "../theme" } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index a1a3dbf71a3c8f8c5151b7729cb84bdac65b1f94..8c4d8532341dfe4ec98bff1204f56ba899be359f 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -23,7 +23,7 @@ postage = { workspace = true } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 66708943f94a2913c4bd394eea6a8c931cac94ed..f4981ac13a9744e82b08eff456e947e59db6ddc8 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -24,7 +24,7 @@ smol = "1.2.5" regex = "1.5" git2 = { version = "0.15", default-features = false } serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } serde_json = { workspace = true } log = { version = "0.4.16", features = ["kv_unstable_serde"] } libc = "0.2" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index bd994b5407ba6a3d5af787e50df9c77ebd432b2b..8715142dd30baa273a3fe98dfd8c410dbfdbf5f4 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -41,9 +41,9 @@ rand = "0.8.3" resvg = "0.14" schemars = "0.8" seahash = "4.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" time = { version = "0.3", features = ["serde", "serde-well-known"] } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0c8256fefb4b64e2b34d01c630ab21454552cd96..a744018e1feb9ae5d9d75c5b4ea6cbbd0b07cfce 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -765,6 +765,12 @@ impl MutableAppContext { }) } + pub fn has_window(&self, window_id: usize) -> bool { + self.window_ids() + .find(|window| window == &window_id) + .is_some() + } + pub fn window_ids(&self) -> impl Iterator + '_ { self.cx.windows.keys().copied() } diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index a42dc1cfa80ed2f7f9da459614092c41a44df2c0..bf3e17e1f1d938a0c65f1a0ef76925c8f1971db0 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -389,6 +389,12 @@ impl ElementBox { } } +impl Clone for ElementBox { + fn clone(&self) -> Self { + ElementBox(self.0.clone()) + } +} + impl From for ElementRc { fn from(val: ElementBox) -> Self { val.0 diff --git a/crates/gpui/src/platform/mac/geometry.rs b/crates/gpui/src/platform/mac/geometry.rs index 6a479681181e41ff454d2e7d50e554a9c7328598..3ff6c1d8cbb7065135282e6ae521cb1269e6a31c 100644 --- a/crates/gpui/src/platform/mac/geometry.rs +++ b/crates/gpui/src/platform/mac/geometry.rs @@ -1,7 +1,6 @@ use cocoa::{ - appkit::NSWindow, base::id, - foundation::{NSPoint, NSRect, NSSize}, + foundation::{NSPoint, NSRect}, }; use objc::{msg_send, sel, sel_impl}; use pathfinder_geometry::{ @@ -25,61 +24,15 @@ impl Vector2FExt for Vector2F { } } -pub trait RectFExt { - /// Converts self to an NSRect with y axis pointing up. - /// The resulting NSRect will have an origin at the bottom left of the rectangle. - /// Also takes care of converting from window scaled coordinates to screen coordinates - fn to_screen_ns_rect(&self, native_window: id) -> NSRect; - - /// Converts self to an NSRect with y axis point up. - /// The resulting NSRect will have an origin at the bottom left of the rectangle. - /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale - fn to_ns_rect(&self) -> NSRect; -} -impl RectFExt for RectF { - fn to_screen_ns_rect(&self, native_window: id) -> NSRect { - unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) } - } - - fn to_ns_rect(&self) -> NSRect { - NSRect::new( - NSPoint::new( - self.origin_x() as f64, - -(self.origin_y() + self.height()) as f64, - ), - NSSize::new(self.width() as f64, self.height() as f64), - ) - } -} - pub trait NSRectExt { - /// Converts self to a RectF with y axis pointing down. - /// The resulting RectF will have an origin at the top left of the rectangle. - /// Also takes care of converting from screen scale coordinates to window coordinates - fn to_window_rectf(&self, native_window: id) -> RectF; - - /// Converts self to a RectF with y axis pointing down. - /// The resulting RectF will have an origin at the top left of the rectangle. - /// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale fn to_rectf(&self) -> RectF; - fn intersects(&self, other: Self) -> bool; } -impl NSRectExt for NSRect { - fn to_window_rectf(&self, native_window: id) -> RectF { - unsafe { - self.origin.x; - let rect: NSRect = native_window.convertRectFromScreen_(*self); - rect.to_rectf() - } - } +impl NSRectExt for NSRect { fn to_rectf(&self) -> RectF { RectF::new( - vec2f( - self.origin.x as f32, - -(self.origin.y + self.size.height) as f32, - ), + vec2f(self.origin.x as f32, self.origin.y as f32), vec2f(self.size.width as f32, self.size.height as f32), ) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index a0c1820368c802b5453ed33c2d8f1e50322228d8..5d28397f8bbeb5b3f5a148e424bb17883ee8124d 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -8,7 +8,7 @@ use crate::{ mac::platform::NSViewLayerContentsRedrawDuringViewResize, platform::{ self, - mac::{geometry::RectFExt, renderer::Renderer, screen::Screen}, + mac::{renderer::Renderer, screen::Screen}, Event, WindowBounds, }, InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, @@ -372,7 +372,8 @@ impl WindowState { } let window_frame = self.frame(); - if window_frame == self.native_window.screen().visibleFrame().to_rectf() { + let screen_frame = self.native_window.screen().visibleFrame().to_rectf(); + if window_frame.size() == screen_frame.size() { WindowBounds::Maximized } else { WindowBounds::Fixed(window_frame) @@ -383,8 +384,19 @@ impl WindowState { // Returns the window bounds in window coordinates fn frame(&self) -> RectF { unsafe { - let ns_frame = NSWindow::frame(self.native_window); - ns_frame.to_rectf() + let screen_frame = self.native_window.screen().visibleFrame(); + let window_frame = NSWindow::frame(self.native_window); + RectF::new( + vec2f( + window_frame.origin.x as f32, + (screen_frame.size.height - window_frame.origin.y - window_frame.size.height) + as f32, + ), + vec2f( + window_frame.size.width as f32, + window_frame.size.height as f32, + ), + ) } } @@ -472,7 +484,16 @@ impl Window { } WindowBounds::Fixed(rect) => { let screen_frame = screen.visibleFrame(); - let ns_rect = rect.to_ns_rect(); + let ns_rect = NSRect::new( + NSPoint::new( + rect.origin_x() as f64, + screen_frame.size.height + - rect.origin_y() as f64 + - rect.height() as f64, + ), + NSSize::new(rect.width() as f64, rect.height() as f64), + ); + if ns_rect.intersects(screen_frame) { native_window.setFrame_display_(ns_rect, YES); } else { diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index ac4badbe2a35db850793ed569876d6c5070ba433..4311f043915eb0f7d4e1db62e96c9ffcc78de6c9 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -46,9 +46,9 @@ parking_lot = "0.11.1" postage = { workspace = true } rand = { version = "0.8.3", optional = true } regex = "1.5" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } similar = "1.3" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2a9ba95b3372f2e38f3a45286bcb7f20553739e8..beda6aa557e46dd18620d13c945847b66e148b46 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -10,7 +10,6 @@ mod buffer_tests; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::{ channel::oneshot, @@ -46,6 +45,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; +use util::http::HttpClient; use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 71a6235b95e28d1a1178d9a5c6253b7d190fbf79..70032d83aaa3bb930c5b93336b9fcf8d6d3fec2c 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -62,12 +62,12 @@ jwt = "0.16" lazy_static = "1.4" objc = "0.2" parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } sha2 = "0.10" simplelog = "0.9" [build-dependencies] -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/live_kit_server/Cargo.toml b/crates/live_kit_server/Cargo.toml index 319a026456b0a2b3cd500c567b01152d30a63542..8cced6d08951bdecabb22c8921c30262acf97ad4 100644 --- a/crates/live_kit_server/Cargo.toml +++ b/crates/live_kit_server/Cargo.toml @@ -19,8 +19,8 @@ jwt = "0.16" prost = "0.8" prost-types = "0.8" reqwest = "0.11" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } sha2 = "0.10" [build-dependencies] diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index aa1f49977c9f7761ecd713e8258bde7c0eaa8b1c..4370aaab06870ed8f232271a51eced39562164ad 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -22,9 +22,9 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } lsp-types = "0.91" parking_lot = "0.11" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["raw_value"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smol = "1.2" [dev-dependencies] diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..32ab6abbb3c1b022495d286859b4826881de1f19 --- /dev/null +++ b/crates/node_runtime/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "node_runtime" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/node_runtime.rs" +doctest = false + +[dependencies] +gpui = { path = "../gpui" } +util = { path = "../util" } +async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-tar = "0.4.2" +futures = "0.3" +anyhow = "1.0.38" +parking_lot = "0.11.1" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +smol = "1.2.5" diff --git a/crates/zed/src/languages/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs similarity index 99% rename from crates/zed/src/languages/node_runtime.rs rename to crates/node_runtime/src/node_runtime.rs index 41cbefbb732fc14fd70397213dde450582209716..079b6a5e45a97b920925777b5d6be96d1bca49e0 100644 --- a/crates/zed/src/languages/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use client::http::HttpClient; use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; use parking_lot::Mutex; @@ -12,6 +11,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::http::HttpClient; const VERSION: &str = "v18.15.0"; diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml index e7a8079caa944633560bc245a41082197c1e9cb4..2371cfa9fd8c9ba0feffd18bd32032847d125ba7 100644 --- a/crates/picker/Cargo.toml +++ b/crates/picker/Cargo.toml @@ -21,7 +21,7 @@ parking_lot = "0.11.1" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" diff --git a/crates/plugin/Cargo.toml b/crates/plugin/Cargo.toml index 6b86b19fc859ee2e40b2d3877a674bb617def09c..8c3a2fb83fecad5ed209dfd8e33cf99465de2f2e 100644 --- a/crates/plugin/Cargo.toml +++ b/crates/plugin/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } bincode = "1.3" plugin_macros = { path = "../plugin_macros" } diff --git a/crates/plugin_macros/Cargo.toml b/crates/plugin_macros/Cargo.toml index e661485373f87940bdc38b3ae7b1e69ada36c321..51cb78c7a1370caad39dd884a20ea50d14908596 100644 --- a/crates/plugin_macros/Cargo.toml +++ b/crates/plugin_macros/Cargo.toml @@ -11,6 +11,6 @@ proc-macro = true syn = { version = "1.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro2 = "1.0" -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } bincode = "1.3" diff --git a/crates/plugin_runtime/Cargo.toml b/crates/plugin_runtime/Cargo.toml index 13efa10dc223162a1034eeb212fc73a0943904e2..cf509a20d34d3195caefdf58164a57229b0de5e3 100644 --- a/crates/plugin_runtime/Cargo.toml +++ b/crates/plugin_runtime/Cargo.toml @@ -9,9 +9,9 @@ wasmtime = "0.38" wasmtime-wasi = "0.38" wasi-common = "0.38" anyhow = { version = "1.0", features = ["std"] } -serde = "1.0" -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = "1.0" +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } bincode = "1.3" pollster = "0.2.5" smol = "1.2.5" diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index b42a6fc674d230adeed763a4de8f10310d0b19e8..f5c144a3adb915c61ae52af38fa78cc961b00639 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -49,9 +49,9 @@ postage = { workspace = true } pulldown-cmark = { version = "0.9.1", default-features = false } rand = "0.8.3" regex = "1.5" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } sha2 = "0.10" similar = "1.3" smol = "1.2.5" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b49460bd4bd5f8516baa6e6451b1c2847d7e9bd7..4d6f42cd0e7ce4402bc9701deb808a21bc11f53c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -568,7 +568,7 @@ impl Project { let mut languages = LanguageRegistry::test(); languages.set_executor(cx.background()); - let http_client = client::test::FakeHttpClient::with_404_response(); + let http_client = util::http::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let project = diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1d40dad86480a8180c85fac262f9050c04e07c91..2357052d2cb13ad819caa5eecce4f2f210675ea3 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3114,13 +3114,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { #[cfg(test)] mod tests { use super::*; - use client::test::FakeHttpClient; use fs::repository::FakeGitRepository; use fs::{FakeFs, RealFs}; use gpui::{executor::Deterministic, TestAppContext}; use rand::prelude::*; use serde_json::json; use std::{env, fmt::Write}; + use util::http::FakeHttpClient; + use util::test::temp_tree; #[gpui::test] diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d245700d58ec1af022d12ef209f78f4690c4c04c..2b72959e25e76bfe35ea8c8ddbd921db123fc06e 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -27,4 +27,4 @@ unicase = "2.6" editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index ff71a2493e8d7c60ae35559faa6d887103f5f110..2773dd2f3b60ba27e59fa85d69b61dc684c60fc8 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -26,8 +26,8 @@ parking_lot = "0.11.1" prost = "0.8" rand = "0.8" rsa = "0.4" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smol-timeout = "0.6" tracing = { version = "0.1.34", features = ["log"] } zstd = "0.11" diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index e8c03a1a5e46db578d1f4dd961acb123a5235ce0..f786d4abc6497b19c66a3ffc334afb20daf2a28e 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -23,14 +23,14 @@ anyhow = "1.0" futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } workspace = { path = "../workspace", features = ["test-support"] } unindent = "0.1" diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 566fcfe3557649bacffb4e32a5f489b9d5d9e1cd..59728083966898da1f0be4e966b9de0df00ca7ca 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -25,7 +25,7 @@ json_comments = "0.2" postage = { workspace = true } schemars = "0.8" serde = { workspace = true } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde_derive = { workspace = true } serde_json = { workspace = true } serde_path_to_error = "0.1.4" toml = "0.5" @@ -36,3 +36,4 @@ tree-sitter-json = "*" unindent = "0.1" gpui = { path = "../gpui", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } +pretty_assertions = "1.3.0" diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 4566776a342381c5bc01b79bee46b53dc302e461..1c796ad5c3fdf6a7b9af165ea227c94bccfc6457 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -21,7 +21,7 @@ use sqlez::{ use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; -use util::ResultExt as _; +use util::{RangeExt, ResultExt as _}; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; pub use watched_json::watch_files; @@ -32,6 +32,7 @@ pub struct Settings { pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, + pub enable_copilot_integration: bool, pub buffer_font_size: f32, pub active_pane_magnification: f32, pub cursor_blink: bool, @@ -60,6 +61,29 @@ pub struct Settings { pub base_keymap: BaseKeymap, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum CopilotSettings { + #[default] + On, + Off, +} + +impl From for bool { + fn from(value: CopilotSettings) -> Self { + match value { + CopilotSettings::On => true, + CopilotSettings::Off => false, + } + } +} + +impl CopilotSettings { + pub fn is_on(&self) -> bool { + >::into(*self) + } +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { #[default] @@ -153,6 +177,42 @@ pub struct EditorSettings { pub ensure_final_newline_on_save: Option, pub formatter: Option, pub enable_language_server: Option, + pub copilot: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OnOff { + On, + Off, +} + +impl OnOff { + pub fn as_bool(&self) -> bool { + match self { + OnOff::On => true, + OnOff::Off => false, + } + } + + pub fn from_bool(value: bool) -> OnOff { + match value { + true => OnOff::On, + false => OnOff::Off, + } + } +} + +impl From for bool { + fn from(value: OnOff) -> bool { + value.as_bool() + } +} + +impl From for OnOff { + fn from(value: bool) -> OnOff { + OnOff::from_bool(value) + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -375,6 +435,8 @@ pub struct SettingsFileContent { pub auto_update: Option, #[serde(default)] pub base_keymap: Option, + #[serde(default)] + pub enable_copilot_integration: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -436,6 +498,7 @@ impl Settings { format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), + copilot: required(defaults.editor.copilot), }, editor_overrides: Default::default(), git: defaults.git.unwrap(), @@ -452,6 +515,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), base_keymap: Default::default(), + enable_copilot_integration: defaults.enable_copilot_integration.unwrap(), } } @@ -503,6 +567,10 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); + merge( + &mut self.enable_copilot_integration, + data.enable_copilot_integration, + ); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); @@ -526,6 +594,14 @@ impl Settings { self } + pub fn copilot_on(&self, language: Option<&str>) -> bool { + if self.enable_copilot_integration { + self.language_setting(language, |settings| settings.copilot.map(Into::into)) + } else { + false + } + } + pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { self.language_setting(language, |settings| settings.tab_size) } @@ -662,6 +738,7 @@ impl Settings { format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), + copilot: Some(OnOff::On), }, editor_overrides: Default::default(), journal_defaults: Default::default(), @@ -681,6 +758,7 @@ impl Settings { telemetry_overrides: Default::default(), auto_update: true, base_keymap: Default::default(), + enable_copilot_integration: true, } } @@ -787,6 +865,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu .unwrap(); let mut depth = 0; + let mut last_value_range = 0..0; let mut first_key_start = None; let mut existing_value_range = 0..settings_content.len(); let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes()); @@ -798,6 +877,14 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu let key_range = mat.captures[0].node.byte_range(); let value_range = mat.captures[1].node.byte_range(); + // Don't enter sub objects until we find an exact + // match for the current keypath + if last_value_range.contains_inclusive(&value_range) { + continue; + } + + last_value_range = value_range.clone(); + if key_range.start > existing_value_range.end { break; } @@ -811,6 +898,8 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu if found_key { existing_value_range = value_range; + // Reset last value range when increasing in depth + last_value_range = existing_value_range.start..existing_value_range.start; depth += 1; if depth == key_path.len() { @@ -852,7 +941,8 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu } if row > 0 { - let new_val = to_pretty_json(&new_value, column, column); + // depth is 0 based, but division needs to be 1 based. + let new_val = to_pretty_json(&new_value, column / (depth + 1), column); let content = format!(r#""{new_key}": {new_val},"#); settings_content.insert_str(first_key_start, &content); @@ -912,13 +1002,28 @@ fn to_pretty_json( pub fn update_settings_file( mut text: String, - old_file_content: SettingsFileContent, + mut old_file_content: SettingsFileContent, update: impl FnOnce(&mut SettingsFileContent), ) -> String { let mut new_file_content = old_file_content.clone(); update(&mut new_file_content); + if new_file_content.languages.len() != old_file_content.languages.len() { + for language in new_file_content.languages.keys() { + old_file_content + .languages + .entry(language.clone()) + .or_default(); + } + for language in old_file_content.languages.keys() { + new_file_content + .languages + .entry(language.clone()) + .or_default(); + } + } + let old_object = to_json_object(old_file_content); let new_object = to_json_object(new_file_content); @@ -931,6 +1036,7 @@ pub fn update_settings_file( for (key, old_value) in old_object.iter() { // We know that these two are from the same shape of object, so we can just unwrap let new_value = new_object.get(key).unwrap(); + if old_value != new_value { match new_value { Value::Bool(_) | Value::Number(_) | Value::String(_) => { @@ -986,7 +1092,75 @@ mod tests { let old_json = old_json.into(); let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); let new_json = update_settings_file(old_json, old_content, update); - assert_eq!(new_json, expected_new_json.into()); + pretty_assertions::assert_eq!(new_json, expected_new_json.into()); + } + + #[test] + fn test_update_copilot() { + assert_new_settings( + r#" + { + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + |settings| { + settings.editor.copilot = Some(OnOff::On); + }, + r#" + { + "copilot": "on", + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + ); + } + + #[test] + fn test_update_langauge_copilot() { + assert_new_settings( + r#" + { + "languages": { + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + |settings| { + settings.languages.insert( + "Rust".into(), + EditorSettings { + copilot: Some(OnOff::On), + ..Default::default() + }, + ); + }, + r#" + { + "languages": { + "Rust": { + "copilot": "on" + }, + "JSON": { + "copilot": "off" + } + } + } + "# + .unindent(), + ); } #[test] diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 56a8a3c45222c8d14a774e169ef91d1169135106..56796fca592b2aba5fd1ef4f5f712f3d4dd124e5 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -29,8 +29,8 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } [dev-dependencies] rand = "0.8.5" diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 1e5b9d6070b1ed5286ea1a0a11788c210f42c57e..726a1a674fd57255f6e5772ef9c60547d64154b9 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -33,8 +33,8 @@ libc = "0.2" anyhow = "1" thiserror = "1.0" lazy_static = "1.4.0" -serde = { version = "1.0", features = ["derive"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index a0ef4ad9f8c378f206c1fe60ec2fa83a4f82cbd3..cf8ff1db14fcb3202b21c26dcc872e90f417af21 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -13,8 +13,8 @@ gpui = { path = "../gpui" } anyhow = "1.0.38" indexmap = "1.6.2" parking_lot = "0.11.1" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } serde_path_to_error = "0.1.4" toml = "0.5" diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d64a1d2499a690e245e16e94b1d76dcdc7327132..7a63c5ae5f1976962c6c70e12daaa47659a49af5 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -9,7 +9,7 @@ use gpui::{ use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; -use ui::{CheckboxStyle, IconStyle}; +use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle}; pub mod ui; @@ -23,6 +23,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub contacts_popover: ContactsPopover, pub contact_list: ContactList, + pub copilot: Copilot, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, @@ -75,8 +76,8 @@ pub struct Workspace { #[derive(Clone, Deserialize, Default)] pub struct BlankPaneStyle { - pub logo: IconStyle, - pub logo_shadow: IconStyle, + pub logo: SvgStyle, + pub logo_shadow: SvgStyle, pub logo_container: ContainerStyle, pub keyboard_hints: ContainerStyle, pub keyboard_hint: Interactive, @@ -115,6 +116,52 @@ pub struct AvatarStyle { pub outer_corner_radius: f32, } +#[derive(Deserialize, Default, Clone)] +pub struct Copilot { + pub out_link_icon: Interactive, + pub modal: ModalStyle, + pub auth: CopilotAuth, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuth { + pub content_width: f32, + pub prompting: CopilotAuthPrompting, + pub not_authorized: CopilotAuthNotAuthorized, + pub authorized: CopilotAuthAuthorized, + pub cta_button: ButtonStyle, + pub header: IconStyle, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthPrompting { + pub subheading: ContainedText, + pub hint: ContainedText, + pub device_code: DeviceCode, +} + +#[derive(Deserialize, Default, Clone)] +pub struct DeviceCode { + pub text: TextStyle, + pub cta: ButtonStyle, + pub left: f32, + pub left_container: ContainerStyle, + pub right: f32, + pub right_container: Interactive, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthNotAuthorized { + pub subheading: ContainedText, + pub warning: ContainedText, +} + +#[derive(Deserialize, Default, Clone)] +pub struct CopilotAuthAuthorized { + pub subheading: ContainedText, + pub hint: ContainedText, +} + #[derive(Deserialize, Default)] pub struct ContactsPopover { #[serde(flatten)] @@ -566,6 +613,7 @@ pub struct Editor { pub line_number_active: Color, pub guest_selections: Vec, pub syntax: Arc, + pub suggestion: HighlightStyle, pub diagnostic_path_header: DiagnosticPathHeader, pub diagnostic_header: DiagnosticHeader, pub error_diagnostic: DiagnosticStyle, @@ -691,7 +739,9 @@ pub struct DiffStyle { pub struct Interactive { pub default: T, pub hover: Option, + pub hover_and_active: Option, pub clicked: Option, + pub click_and_active: Option, pub active: Option, pub disabled: Option, } @@ -699,7 +749,17 @@ pub struct Interactive { impl Interactive { pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T { if active { - self.active.as_ref().unwrap_or(&self.default) + if state.hovered() { + self.hover_and_active + .as_ref() + .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) + } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() { + self.click_and_active + .as_ref() + .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) + } else { + self.active.as_ref().unwrap_or(&self.default) + } } else if state.clicked() == Some(gpui::MouseButton::Left) && self.clicked.is_some() { self.clicked.as_ref().unwrap() } else if state.hovered() { @@ -724,7 +784,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { #[serde(flatten)] default: Value, hover: Option, + hover_and_active: Option, clicked: Option, + click_and_active: Option, active: Option, disabled: Option, } @@ -751,7 +813,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { }; let hover = deserialize_state(json.hover)?; + let hover_and_active = deserialize_state(json.hover_and_active)?; let clicked = deserialize_state(json.clicked)?; + let click_and_active = deserialize_state(json.click_and_active)?; let active = deserialize_state(json.active)?; let disabled = deserialize_state(json.disabled)?; let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?; @@ -759,7 +823,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { Ok(Interactive { default, hover, + hover_and_active, clicked, + click_and_active, active, disabled, }) @@ -868,7 +934,7 @@ pub struct FeedbackStyle { #[derive(Clone, Deserialize, Default)] pub struct WelcomeStyle { pub page_width: f32, - pub logo: IconStyle, + pub logo: SvgStyle, pub logo_subheading: ContainedText, pub usage_note: ContainedText, pub checkbox: CheckboxStyle, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 5441e711685970d3c33c9db94f9ac03815626531..30ccef28defd36b9b17c02d23a66eeac208a3147 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,18 +1,23 @@ +use std::borrow::Cow; + use gpui::{ color::Color, elements::{ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, - MouseEventHandler, ParentElement, Svg, + MouseEventHandler, ParentElement, Stack, Svg, }, - Action, Element, ElementBox, EventContext, RenderContext, View, + fonts::TextStyle, + geometry::vector::{vec2f, Vector2F}, + scene::MouseClick, + Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View, }; use serde::Deserialize; -use crate::ContainedText; +use crate::{ContainedText, Interactive}; #[derive(Clone, Deserialize, Default)] pub struct CheckboxStyle { - pub icon: IconStyle, + pub icon: SvgStyle, pub label: ContainedText, pub default: ContainerStyle, pub checked: ContainerStyle, @@ -44,7 +49,7 @@ pub fn checkbox_with_label( ) -> MouseEventHandler { MouseEventHandler::::new(0, cx, |state, _| { let indicator = if checked { - icon(&style.icon) + svg(&style.icon) } else { Empty::new() .constrained() @@ -80,9 +85,9 @@ pub fn checkbox_with_label( } #[derive(Clone, Deserialize, Default)] -pub struct IconStyle { +pub struct SvgStyle { pub color: Color, - pub icon: String, + pub asset: String, pub dimensions: Dimensions, } @@ -92,14 +97,30 @@ pub struct Dimensions { pub height: f32, } -pub fn icon(style: &IconStyle) -> ConstrainedBox { - Svg::new(style.icon.clone()) +impl Dimensions { + pub fn to_vec(&self) -> Vector2F { + vec2f(self.width, self.height) + } +} + +pub fn svg(style: &SvgStyle) -> ConstrainedBox { + Svg::new(style.asset.clone()) .with_color(style.color) .constrained() .with_width(style.dimensions.width) .with_height(style.dimensions.height) } +#[derive(Clone, Deserialize, Default)] +pub struct IconStyle { + icon: SvgStyle, + container: ContainerStyle, +} + +pub fn icon(style: &IconStyle) -> Container { + svg(&style.icon).contained().with_style(style.container) +} + pub fn keystroke_label( label_text: &'static str, label_style: &ContainedText, @@ -147,3 +168,123 @@ pub fn keystroke_label_for( .contained() .with_style(label_style.container) } + +pub type ButtonStyle = Interactive; + +pub fn cta_button( + label: L, + action: A, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, +) -> ElementBox +where + L: Into>, + A: 'static + Action + Clone, + V: View, +{ + cta_button_with_click(label, max_width, style, cx, move |_, cx| { + cx.dispatch_action(action.clone()) + }) + .boxed() +} + +pub fn cta_button_with_click( + label: L, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, + f: F, +) -> MouseEventHandler +where + L: Into>, + V: View, + F: Fn(MouseClick, &mut EventContext) + 'static, +{ + MouseEventHandler::::new(0, cx, |state, _| { + let style = style.style_for(state, false); + Label::new(label, style.text.to_owned()) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_max_width(max_width) + .boxed() + }) + .on_click(MouseButton::Left, f) + .with_cursor_style(gpui::CursorStyle::PointingHand) +} + +#[derive(Clone, Deserialize, Default)] +pub struct ModalStyle { + close_icon: Interactive, + container: ContainerStyle, + titlebar: ContainerStyle, + title_text: Interactive, + dimensions: Dimensions, +} + +impl ModalStyle { + pub fn dimensions(&self) -> Vector2F { + self.dimensions.to_vec() + } +} + +pub fn modal( + title: I, + style: &ModalStyle, + cx: &mut RenderContext, + build_modal: F, +) -> ElementBox +where + V: View, + I: Into>, + F: FnOnce(&mut gpui::RenderContext) -> ElementBox, +{ + const TITLEBAR_HEIGHT: f32 = 28.; + // let active = cx.window_is_active(cx.window_id()); + + Flex::column() + .with_child( + Stack::new() + .with_children([ + Label::new( + title, + style + .title_text + .style_for(&mut MouseState::default(), false) + .clone(), + ) + .boxed(), + // FIXME: Get a better tag type + MouseEventHandler::::new(999999, cx, |state, _cx| { + let style = style.close_icon.style_for(state, false); + icon(style).boxed() + }) + .on_click(gpui::MouseButton::Left, move |_, cx| { + let window_id = cx.window_id(); + cx.remove_window(window_id); + }) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .aligned() + .right() + .boxed(), + ]) + .contained() + .with_style(style.titlebar) + .constrained() + .with_height(TITLEBAR_HEIGHT) + .boxed(), + ) + .with_child( + Container::new(build_modal(cx)) + .with_style(style.container) + .constrained() + .with_width(style.dimensions().x()) + .with_height(style.dimensions().y() - TITLEBAR_HEIGHT) + .boxed(), + ) + .constrained() + .with_height(style.dimensions().y()) + .boxed() +} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index b13b8af956c0f174c1897a920d47555bde119543..0e3a8d96beadccea39f2aee80988c4ce351c9634 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -14,12 +14,16 @@ test-support = ["tempdir", "git2"] [dependencies] anyhow = "1.0.38" backtrace = "0.3" -futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } lazy_static = "1.4.0" +futures = "0.3" +isahc = "1.7" +smol = "1.2.5" +url = "2.2" rand = { workspace = true } tempdir = { version = "0.3.7", optional = true } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_json = { workspace = true } git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs new file mode 100644 index 0000000000000000000000000000000000000000..c6d562d15cc38531b7453d278dc8a349c9124035 --- /dev/null +++ b/crates/util/src/fs.rs @@ -0,0 +1,28 @@ +use std::path::Path; + +use smol::{fs, stream::StreamExt}; + +use crate::ResultExt; + +// Removes all files and directories matching the given predicate +pub async fn remove_matching(dir: &Path, predicate: F) +where + F: Fn(&Path) -> bool, +{ + if let Some(mut entries) = fs::read_dir(dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if predicate(entry_path.as_path()) { + if let Ok(metadata) = fs::metadata(&entry_path).await { + if metadata.is_file() { + fs::remove_file(&entry_path).await.log_err(); + } else { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + } +} diff --git a/crates/zed/src/languages/github.rs b/crates/util/src/github.rs similarity index 85% rename from crates/zed/src/languages/github.rs rename to crates/util/src/github.rs index 8fdef507908bbd1690a2f0e7b1ce735827e2a04b..5170bd6f4f397c77c505c606401e7f3299637b7f 100644 --- a/crates/zed/src/languages/github.rs +++ b/crates/util/src/github.rs @@ -1,7 +1,7 @@ +use crate::http::HttpClient; use anyhow::{Context, Result}; -use client::http::HttpClient; +use futures::AsyncReadExt; use serde::Deserialize; -use smol::io::AsyncReadExt; use std::sync::Arc; pub struct GitHubLspBinaryVersion { @@ -10,18 +10,18 @@ pub struct GitHubLspBinaryVersion { } #[derive(Deserialize)] -pub(crate) struct GithubRelease { +pub struct GithubRelease { pub name: String, pub assets: Vec, } #[derive(Deserialize)] -pub(crate) struct GithubReleaseAsset { +pub struct GithubReleaseAsset { pub name: String, pub browser_download_url: String, } -pub(crate) async fn latest_github_release( +pub async fn latest_github_release( repo_name_with_owner: &str, http: Arc, ) -> Result { @@ -39,6 +39,7 @@ pub(crate) async fn latest_github_release( .read_to_end(&mut body) .await .context("error reading latest release")?; + let release: GithubRelease = serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?; Ok(release) diff --git a/crates/util/src/http.rs b/crates/util/src/http.rs new file mode 100644 index 0000000000000000000000000000000000000000..e29768a53e891e165dd859fd27be5325f35cdfa5 --- /dev/null +++ b/crates/util/src/http.rs @@ -0,0 +1,117 @@ +pub use anyhow::{anyhow, Result}; +use futures::future::BoxFuture; +use isahc::config::{Configurable, RedirectPolicy}; +pub use isahc::{ + http::{Method, Uri}, + Error, +}; +pub use isahc::{AsyncBody, Request, Response}; +use smol::future::FutureExt; +#[cfg(feature = "test-support")] +use std::fmt; +use std::{sync::Arc, time::Duration}; +pub use url::Url; + +pub trait HttpClient: Send + Sync { + fn send(&self, req: Request) -> BoxFuture, Error>>; + + fn get<'a>( + &'a self, + uri: &str, + body: AsyncBody, + follow_redirects: bool, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .redirect_policy(if follow_redirects { + RedirectPolicy::Follow + } else { + RedirectPolicy::None + }) + .method(Method::GET) + .uri(uri) + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } + + fn post_json<'a>( + &'a self, + uri: &str, + body: AsyncBody, + ) -> BoxFuture<'a, Result, Error>> { + let request = isahc::Request::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .body(body); + match request { + Ok(request) => self.send(request), + Err(error) => async move { Err(error.into()) }.boxed(), + } + } +} + +pub fn client() -> Arc { + Arc::new( + isahc::HttpClient::builder() + .connect_timeout(Duration::from_secs(5)) + .low_speed_timeout(100, Duration::from_secs(5)) + .build() + .unwrap(), + ) +} + +impl HttpClient for isahc::HttpClient { + fn send(&self, req: Request) -> BoxFuture, Error>> { + Box::pin(async move { self.send_async(req).await }) + } +} + +#[cfg(feature = "test-support")] +pub struct FakeHttpClient { + handler: Box< + dyn 'static + + Send + + Sync + + Fn(Request) -> BoxFuture<'static, Result, Error>>, + >, +} + +#[cfg(feature = "test-support")] +impl FakeHttpClient { + pub fn create(handler: F) -> Arc + where + Fut: 'static + Send + futures::Future, Error>>, + F: 'static + Send + Sync + Fn(Request) -> Fut, + { + Arc::new(Self { + handler: Box::new(move |req| Box::pin(handler(req))), + }) + } + + pub fn with_404_response() -> Arc { + Self::create(|_| async move { + Ok(Response::builder() + .status(404) + .body(Default::default()) + .unwrap()) + }) + } +} + +#[cfg(feature = "test-support")] +impl fmt::Debug for FakeHttpClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FakeHttpClient").finish() + } +} + +#[cfg(feature = "test-support")] +impl HttpClient for FakeHttpClient { + fn send(&self, req: Request) -> BoxFuture, Error>> { + let future = (self.handler)(req); + Box::pin(async move { future.await.map(Into::into) }) + } +} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 63c3c6d884a4e91fdeaff3766adc97a99072ec24..e38f76d8a6e294cb52555cf28d9f164a5264c7e3 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -6,6 +6,7 @@ lazy_static::lazy_static! { pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); + pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 6a5ccb8bd52e95b4aa54aa88c5b07d0f71b1906f..d9db47c2bb1476a9dbdcb512bf5427cfb550bc77 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,4 +1,7 @@ pub mod channel; +pub mod fs; +pub mod github; +pub mod http; pub mod paths; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -298,6 +301,7 @@ pub trait RangeExt { fn sorted(&self) -> Self; fn to_inclusive(&self) -> RangeInclusive; fn overlaps(&self, other: &Range) -> bool; + fn contains_inclusive(&self, other: &Range) -> bool; } impl RangeExt for Range { @@ -312,6 +316,10 @@ impl RangeExt for Range { fn overlaps(&self, other: &Range) -> bool { self.start < other.end && other.start < self.end } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start <= other.start && other.end <= self.end + } } impl RangeExt for RangeInclusive { @@ -326,6 +334,10 @@ impl RangeExt for RangeInclusive { fn overlaps(&self, other: &Range) -> bool { self.start() < &other.end && &other.start <= self.end() } + + fn contains_inclusive(&self, other: &Range) -> bool { + self.start() <= &other.start && &other.end <= self.end() + } } #[cfg(test)] diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index dd79d56d8fd24ab32c8959682476a6c01c088165..bafa2c7a557442405f8ed5d95e3cc366e35663ca 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -12,8 +12,8 @@ doctest = false neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] [dependencies] -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } +serde = { workspace = true } +serde_derive = { workspace = true } itertools = "0.10" log = { version = "0.4.16", features = ["kv_unstable_serde"] } @@ -21,7 +21,7 @@ async-compat = { version = "0.2.1", "optional" = true } async-trait = { version = "0.1", "optional" = true } nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true } tokio = { version = "1.15", "optional" = true } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } assets = { path = "../assets" } collections = { path = "../collections" } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 33f142c21e692294a498d951c39b3ee05a0b1cf8..34afcb5f843d4f39f0ae673ee51c0bb613f64546 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ mod visual; use std::sync::Arc; -use command_palette::CommandPaletteFilter; +use collections::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode}; use gpui::{ actions, impl_actions, MutableAppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 3a35920b88922bb2b7bf13e5bd849acd77e4be3a..fb55c79a51e09a37d02c1ffa1e26f0522f5e282c 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,12 +1,11 @@ mod base_keymap_picker; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use db::kvp::KEY_VALUE_STORE; use gpui::{ - elements::{Flex, Label, MouseEventHandler, ParentElement}, - Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, + elements::{Flex, Label, ParentElement}, + Element, ElementBox, Entity, MutableAppContext, Subscription, View, ViewContext, }; use settings::{settings_file::SettingsFile, Settings}; @@ -77,7 +76,7 @@ impl View for WelcomePage { .with_children([ Flex::column() .with_children([ - theme::ui::icon(&theme.welcome.logo) + theme::ui::svg(&theme.welcome.logo) .aligned() .contained() .aligned() @@ -98,22 +97,25 @@ impl View for WelcomePage { .boxed(), Flex::column() .with_children([ - self.render_cta_button( + theme::ui::cta_button( "Choose a theme", theme_selector::Toggle, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Choose a keymap", ToggleBaseKeymapSelector, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Install the CLI", install_cli::Install, width, + &theme.welcome.button, cx, ), ]) @@ -201,89 +203,6 @@ impl WelcomePage { _settings_subscription: settings_subscription, } } - - fn render_cta_button( - &self, - label: L, - action: A, - width: f32, - cx: &mut RenderContext, - ) -> ElementBox - where - L: Into>, - A: 'static + Action + Clone, - { - let theme = cx.global::().theme.clone(); - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.welcome.button.style_for(state, false); - Label::new(label, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_max_width(width) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(action.clone()) - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed() - } - - // fn render_settings_checkbox( - // &self, - // label: &'static str, - // style: &CheckboxStyle, - // checked: bool, - // cx: &mut RenderContext, - // set_value: fn(&mut SettingsFileContent, checked: bool) -> (), - // ) -> ElementBox { - // MouseEventHandler::::new(0, cx, |state, _| { - // let indicator = if checked { - // Svg::new(style.check_icon.clone()) - // .with_color(style.check_icon_color) - // .constrained() - // } else { - // Empty::new().constrained() - // }; - - // Flex::row() - // .with_children([ - // indicator - // .with_width(style.width) - // .with_height(style.height) - // .contained() - // .with_style(if checked { - // if state.hovered() { - // style.hovered_and_checked - // } else { - // style.checked - // } - // } else { - // if state.hovered() { - // style.hovered - // } else { - // style.default - // } - // }) - // .boxed(), - // Label::new(label, style.label.text.clone()) - // .contained() - // .with_style(style.label.container) - // .boxed(), - // ]) - // .align_children_center() - // .boxed() - // }) - // .on_click(gpui::MouseButton::Left, move |_, cx| { - // SettingsFile::update(cx, move |content| set_value(content, !checked)) - // }) - // .with_cursor_style(gpui::CursorStyle::PointingHand) - // .contained() - // .with_style(style.container) - // .boxed() - // } } impl Item for WelcomePage { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 9a6813f6271756e06da33ab2275f4ba80a5b0e4d..5a2380de3f4097a943d8cfc8e7f19c8926457894 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -44,9 +44,9 @@ env_logger = "0.9.1" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { workspace = true } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } smallvec = { version = "1.6", features = ["union"] } indoc = "1.0.4" uuid = { version = "1.1.2", features = ["v4"] } diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 76f46f83c5f87c88b8dd5c68e2e2d98e39798a79..1cb5d3f50d7539885e0b4db317238e3263c3169c 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -97,7 +97,7 @@ impl Workspace { let notification = build_notification(cx); cx.subscribe(¬ification, move |this, handle, event, cx| { if handle.read(cx).should_dismiss_notification_on_event(event) { - this.dismiss_notification(type_id, id, cx); + this.dismiss_notification_internal(type_id, id, cx); } }) .detach(); @@ -107,7 +107,18 @@ impl Workspace { } } - fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext) { + pub fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext) { + let type_id = TypeId::of::(); + + self.dismiss_notification_internal(type_id, id, cx) + } + + fn dismiss_notification_internal( + &mut self, + type_id: TypeId, + id: usize, + cx: &mut ViewContext, + ) { self.notifications .retain(|(existing_type_id, existing_id, _)| { if (*existing_type_id, *existing_id) == (type_id, id) { @@ -141,7 +152,13 @@ pub mod simple_message_notification { actions!(message_notifications, [CancelMessageNotification]); #[derive(Clone, Default, Deserialize, PartialEq)] - pub struct OsOpen(pub String); + pub struct OsOpen(pub Cow<'static, str>); + + impl OsOpen { + pub fn new>>(url: I) -> Self { + OsOpen(url.into()) + } + } impl_actions!(message_notifications, [OsOpen]); @@ -149,7 +166,7 @@ pub mod simple_message_notification { cx.add_action(MessageNotification::dismiss); cx.add_action( |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext| { - cx.platform().open_url(open_action.0.as_str()); + cx.platform().open_url(open_action.0.as_ref()); }, ) } @@ -177,6 +194,18 @@ pub mod simple_message_notification { } } + pub fn new_boxed_action>, S2: Into>>( + message: S1, + click_action: Box, + click_message: S2, + ) -> Self { + Self { + message: message.into(), + click_action: Some(click_action), + click_message: Some(click_message.into()), + } + } + pub fn new>, A: Action, S2: Into>>( message: S1, click_action: A, @@ -264,9 +293,13 @@ pub mod simple_message_notification { let style = theme.action_message.style_for(state, false); if let Some(click_message) = click_message { Some( - Text::new(click_message, style.text.clone()) - .contained() - .with_style(style.container) + Flex::row() + .with_child( + Text::new(click_message, style.text.clone()) + .contained() + .with_style(style.container) + .boxed(), + ) .boxed(), ) } else { @@ -282,7 +315,8 @@ pub mod simple_message_notification { .on_up(MouseButton::Left, |_, _| {}) .on_click(MouseButton::Left, move |_, cx| { if let Some(click_action) = click_action.as_ref() { - cx.dispatch_any_action(click_action.boxed_clone()) + cx.dispatch_any_action(click_action.boxed_clone()); + cx.dispatch_action(CancelMessageNotification) } }) .with_cursor_style(if has_click_action { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fe60065486bff9c71b00ccf477df09b1c0d55208..3fffe57e3e8f494d61b85a0152944ce9d2c97f06 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -41,10 +41,10 @@ use gpui::{ impl_actions, impl_internal_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, - MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext, - SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, - WindowBounds, + Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, + ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, + RenderContext, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, + WeakViewHandle, WindowBounds, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use language::LanguageRegistry; @@ -165,6 +165,67 @@ pub struct OpenProjectEntryInPane { project_entry: ProjectEntryId, } +pub struct Toast { + id: usize, + msg: Cow<'static, str>, + click: Option<(Cow<'static, str>, Box)>, +} + +impl Toast { + pub fn new>>(id: usize, msg: I) -> Self { + Toast { + id, + msg: msg.into(), + click: None, + } + } + + pub fn new_action>, I2: Into>>( + id: usize, + msg: I1, + click_msg: I2, + action: impl Action, + ) -> Self { + Toast { + id, + msg: msg.into(), + click: Some((click_msg.into(), Box::new(action))), + } + } +} + +impl PartialEq for Toast { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.msg == other.msg + && self.click.is_some() == other.click.is_some() + } +} + +impl Clone for Toast { + fn clone(&self) -> Self { + Toast { + id: self.id, + msg: self.msg.to_owned(), + click: self + .click + .as_ref() + .map(|(msg, click)| (msg.to_owned(), click.boxed_clone())), + } + } +} + +#[derive(Clone, PartialEq)] +pub struct DismissToast { + id: usize, +} + +impl DismissToast { + pub fn new(id: usize) -> Self { + DismissToast { id } + } +} + pub type WorkspaceId = i64; impl_internal_actions!( @@ -178,6 +239,8 @@ impl_internal_actions!( SplitWithItem, SplitWithProjectEntry, OpenProjectEntryInPane, + Toast, + DismissToast ] ); impl_actions!(workspace, [ActivatePane]); @@ -353,6 +416,24 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { .detach(); }); + cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| { + workspace.dismiss_notification::(alert.id, cx); + workspace.show_notification(alert.id, cx, |cx| { + cx.add_view(|_cx| match &alert.click { + Some((click_msg, action)) => MessageNotification::new_boxed_action( + alert.msg.clone(), + action.boxed_clone(), + click_msg.clone(), + ), + None => MessageNotification::new_message(alert.msg.clone()), + }) + }) + }); + + cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| { + workspace.dismiss_notification::(alert.id, cx); + }); + let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); @@ -449,7 +530,7 @@ impl AppState { let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); - let http_client = client::test::FakeHttpClient::with_404_response(); + let http_client = util::http::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); @@ -2690,7 +2771,7 @@ fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAp indoc::indoc! {" Failed to load any database file :( "}, - OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), + OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()), "Click to let us know about this error" ) }) @@ -2712,7 +2793,7 @@ fn notify_if_database_failed(workspace: &ViewHandle, cx: &mut AsyncAp "}, backup_path ), - OsOpen(backup_path.to_string()), + OsOpen::new(backup_path.to_string()), "Click to show old database in finder", ) }) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 4d7ce828d69635a4e8f43fafb7b05e0089c0aa5e..b8680ebf02956e0a3cacea2d94de0fa31a54f853 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -28,6 +28,8 @@ command_palette = { path = "../command_palette" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } +copilot = { path = "../copilot" } +copilot_button = { path = "../copilot_button" } diagnostics = { path = "../diagnostics" } db = { path = "../db" } editor = { path = "../editor" } @@ -44,6 +46,7 @@ journal = { path = "../journal" } language = { path = "../language" } language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } +node_runtime = { path = "../node_runtime" } outline = { path = "../outline" } plugin_runtime = { path = "../plugin_runtime" } project = { path = "../project" } @@ -87,9 +90,9 @@ rand = "0.8.3" regex = "1.5" rsa = "0.4" rust-embed = { version = "6.3", features = ["include-exclude"] } -serde = { version = "1.0", features = ["derive", "rc"] } -serde_derive = { version = "1.0", features = ["deserialize_in_place"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } serde_path_to_error = "0.1.4" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } @@ -137,7 +140,7 @@ util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } env_logger = "0.9" -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = { workspace = true } unindent = "0.1.7" [package.metadata.bundle-dev] diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index c49c77f076d73d344345926fb3129fe74f6cf8e7..12e6c1b1f2eb3f389c156686c9a7aabb0d1045ae 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,6 +1,4 @@ use anyhow::Context; -use client::http::HttpClient; -use gpui::executor::Background; pub use language::*; use node_runtime::NodeRuntime; use rust_embed::RustEmbed; @@ -9,13 +7,11 @@ use theme::ThemeRegistry; mod c; mod elixir; -mod github; mod go; mod html; mod json; mod language_plugin; mod lua; -mod node_runtime; mod python; mod ruby; mod rust; @@ -37,13 +33,10 @@ mod yaml; struct LanguageDir; pub fn init( - http: Arc, - background: Arc, languages: Arc, themes: Arc, + node_runtime: Arc, ) { - let node_runtime = NodeRuntime::new(http, background); - for (name, grammar, lsp_adapter) in [ ( "c", diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 906592fc2d4b92f947b3328caa5182e32614fb1e..e142028196deb8c5af3a19f32d5e5b3c1716c9af 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -1,13 +1,16 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; +use util::github::GitHubLspBinaryVersion; + pub struct CLspAdapter; #[async_trait] @@ -69,16 +72,7 @@ impl super::LspAdapter for CLspAdapter { Err(anyhow!("failed to unzip clangd archive"))?; } - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 9f921a0c402c1d87f21e96aa3ecded5de55b1899..a2debcdb2d5a3f6c08a476dd6a05bd36e9fe8315 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -1,14 +1,17 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use lsp::{CompletionItemKind, SymbolKind}; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; +use util::github::GitHubLspBinaryVersion; + pub struct ElixirLspAdapter; #[async_trait] @@ -76,22 +79,7 @@ impl LspAdapter for ElixirLspAdapter { Err(anyhow!("failed to unzip clangd archive"))?; } - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - if let Ok(metadata) = fs::metadata(&entry_path).await { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 9af309839fedb17a47b98b2d45ffe7fa79b0d940..760c5f353d0d98c6f58d85d5472a8c519a2c37da 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -1,13 +1,15 @@ -use super::github::latest_github_release; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::{fs, process}; -use std::{any::Any, ffi::OsString, ops::Range, path::PathBuf, str, sync::Arc}; +use std::ffi::{OsStr, OsString}; +use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc}; +use util::fs::remove_matching; +use util::github::latest_github_release; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments() -> Vec { @@ -55,18 +57,10 @@ impl super::LspAdapter for GoLspAdapter { let binary_path = container_dir.join(&format!("gopls_{version}")); if let Ok(metadata) = fs::metadata(&binary_path).await { if metadata.is_file() { - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != binary_path - && entry.file_name() != "gobin" - { - fs::remove_file(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| { + entry != binary_path && entry.file_name() != Some(OsStr::new("gobin")) + }) + .await; return Ok(LanguageServerBinary { path: binary_path.to_path_buf(), diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index a2cfbac96b4ff47b7cf06e49551185f1fa1892cd..20f097ba7f8b173cdd3c74dec0ae5f711806111e 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,17 +1,15 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; -use std::{ - any::Any, - ffi::OsString, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::ffi::OsString; +use std::path::Path; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -69,16 +67,7 @@ impl LspAdapter for HtmlLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(container_dir.as_path(), |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 479308f370e6268d25d2b7bfae04ed383432e4d2..7919f7510d4ee77fce75f59e9a65a88df58c941a 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,11 +1,10 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::json; use settings::{keymap_file_json_schema, settings_file_json_schema}; use smol::fs; @@ -17,6 +16,7 @@ use std::{ sync::Arc, }; use theme::ThemeRegistry; +use util::{fs::remove_matching, http::HttpClient}; use util::{paths, ResultExt, StaffMode}; const SERVER_PATH: &'static str = @@ -84,16 +84,7 @@ impl LspAdapter for JsonLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != server_path).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index 38f50d2d88710d66d43cdb7cafe932f92c1be57d..9b82713d082d5d12a0243ebaf674b9a16550c3d4 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -1,12 +1,12 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use collections::HashMap; use futures::lock::Mutex; use gpui::executor::Background; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::http::HttpClient; use util::ResultExt; #[allow(dead_code)] diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7ffdac5218cb92a6a50622ae91e30eb0d8a07ead..2a18138cb71193c9f07e2c02f3335959e85da123 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -1,16 +1,14 @@ -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; - use anyhow::{anyhow, bail, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; -use client::http::HttpClient; use futures::{io::BufReader, StreamExt}; use language::{LanguageServerBinary, LanguageServerName}; use smol::fs; -use util::{async_iife, ResultExt}; +use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; +use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt}; -use super::github::{latest_github_release, GitHubLspBinaryVersion}; +use util::github::GitHubLspBinaryVersion; #[derive(Copy, Clone)] pub struct LuaLspAdapter; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 9a09c63bb6ca2447667ba41e484a0fb990412522..d5fd865221b2c37953a768ca01ca8230bc577981 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,9 +1,8 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use smol::fs; use std::{ any::Any, @@ -11,6 +10,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -60,16 +61,7 @@ impl LspAdapter for PythonLspAdapter { .npm_install_packages([("pyright", version.as_str())], &version_dir) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index 662c1f464d1fd1f216b0af37925950658e083ba4..d387f815f0cd345c4173f522370ab17495989592 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use client::http::HttpClient; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use std::{any::Any, path::PathBuf, sync::Arc}; +use util::http::HttpClient; pub struct RubyLanguageServer; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 0f8e90d7b268a71712375f0fda0406f0745110d6..d5a67731292612bad3c70509d8cd086a2172ef49 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,14 +1,15 @@ -use super::github::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; -use client::http::HttpClient; use futures::{io::BufReader, StreamExt}; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use util::fs::remove_matching; +use util::github::{latest_github_release, GitHubLspBinaryVersion}; +use util::http::HttpClient; use util::ResultExt; pub struct RustLspAdapter; @@ -60,16 +61,7 @@ impl LspAdapter for RustLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != destination_path { - fs::remove_file(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != destination_path).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 4fc2e0978c7a8f77f68038805ba3ac2215f5f557..0c6e7e3c0903cb1cd13304b28f47183503c28c4d 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,10 +1,9 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::StreamExt; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use lsp::CodeActionKind; +use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; use std::{ @@ -13,6 +12,8 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use util::fs::remove_matching; +use util::http::HttpClient; use util::ResultExt; fn server_binary_arguments(server_path: &Path) -> Vec { @@ -91,16 +92,7 @@ impl LspAdapter for TypeScriptLspAdapter { ) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index b6e82842dea80c777c1885e33fedfaf8fda439d5..7339512f1a31233a94962b0aad00a497f2566013 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -1,10 +1,9 @@ -use super::node_runtime::NodeRuntime; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; -use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::MutableAppContext; use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use node_runtime::NodeRuntime; use serde_json::Value; use settings::Settings; use smol::fs; @@ -16,6 +15,7 @@ use std::{ sync::Arc, }; use util::ResultExt; +use util::{fs::remove_matching, http::HttpClient}; fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -68,16 +68,7 @@ impl LspAdapter for YamlLspAdapter { .npm_install_packages([("yaml-language-server", version.as_str())], &version_dir) .await?; - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } + remove_matching(&container_dir, |entry| entry != version_dir).await; } Ok(LanguageServerBinary { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fb6c6227c35af0bf350580e873f591629532c781..8f7b858dfd716f4b77c7c6799d36e56632ac2967 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,11 +8,7 @@ use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, }; -use client::{ - self, - http::{self, HttpClient}, - UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, -}; +use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use futures::{ channel::{mpsc, oneshot}, @@ -22,6 +18,7 @@ use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, V use isahc::{config::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; +use node_runtime::NodeRuntime; use parking_lot::Mutex; use project::Fs; use serde_json::json; @@ -36,6 +33,7 @@ use std::{ path::PathBuf, sync::Arc, thread, time::Duration, }; use terminal_view::{get_working_directory, TerminalView}; +use util::http::{self, HttpClient}; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; @@ -139,12 +137,9 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - languages::init( - http.clone(), - cx.background().clone(), - languages.clone(), - themes.clone(), - ); + let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned()); + + languages::init(languages.clone(), themes.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); cx.set_global(client.clone()); @@ -165,6 +160,7 @@ fn main() { terminal_view::init(cx); theme_testbench::init(cx); recent_projects::init(cx); + copilot::init(client.clone(), node_runtime, cx); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 788be77e7587b46f191d344867a96ca9af7539f8..01b493bf7dc59b05fa319ba070df690778b2d5d1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -261,6 +261,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { }, ); activity_indicator::init(cx); + copilot_button::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); settings::KeymapFileContent::load_defaults(cx); } @@ -311,6 +312,7 @@ pub fn initialize_workspace( }); let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); + let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx)); let diagnostic_summary = cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx)); let activity_indicator = @@ -324,6 +326,7 @@ pub fn initialize_workspace( status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(toggle_terminal, cx); status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(copilot, cx); status_bar.add_right_item(active_buffer_language, cx); status_bar.add_right_item(cursor_position, cx); }); @@ -652,12 +655,12 @@ fn open_bundled_file( mod tests { use super::*; use assets::Assets; - use client::test::FakeHttpClient; use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, }; use language::LanguageRegistry; + use node_runtime::NodeRuntime; use project::{Project, ProjectPath}; use serde_json::json; use std::{ @@ -665,6 +668,7 @@ mod tests { path::{Path, PathBuf}, }; use theme::ThemeRegistry; + use util::http::FakeHttpClient; use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle, @@ -1851,12 +1855,9 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let themes = ThemeRegistry::new((), cx.font_cache().clone()); - languages::init( - FakeHttpClient::with_404_response(), - cx.background().clone(), - languages.clone(), - themes, - ); + let http = FakeHttpClient::with_404_response(); + let node_runtime = NodeRuntime::new(http, cx.background().to_owned()); + languages::init(languages.clone(), themes, node_runtime); for name in languages.language_names() { languages.language_for_name(&name); } diff --git a/plugins/json_language/Cargo.toml b/plugins/json_language/Cargo.toml index effbf2ed8abbfa0f6bf35156227f2a8e4e21eeba..5a5072995fdb3468f0ff3cb244bd8a89698b450f 100644 --- a/plugins/json_language/Cargo.toml +++ b/plugins/json_language/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] plugin = { path = "../../crates/plugin" } -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = "1.0" diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 423ce37d481e8f47bf9118ad6134b5c123a4ed31..f3315aa7cd9183c3560aef9388fdcbc62e3809cc 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -21,6 +21,7 @@ import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" import welcome from "./welcome" +import copilot from "./copilot" export default function app(colorScheme: ColorScheme): Object { return { @@ -34,6 +35,7 @@ export default function app(colorScheme: ColorScheme): Object { incomingCallNotification: incomingCallNotification(colorScheme), picker: picker(colorScheme), workspace: workspace(colorScheme), + copilot: copilot(colorScheme), welcome: welcome(colorScheme), contextMenu: contextMenu(colorScheme), editor: editor(colorScheme), diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 33546c997866ef5c9c79d94d17e67f8d21f7bb5f..6b21eec405a8f76caf3ee6f952e0937a0fd20b50 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -280,3 +280,15 @@ export function border( ...properties, } } + + +export function svg(color: string, asset: String, width: Number, height: Number) { + return { + color, + asset, + dimensions: { + width, + height, + } + } +} diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2df2e5d405917606831b93b649d72ccec3ab2af --- /dev/null +++ b/styles/src/styleTree/copilot.ts @@ -0,0 +1,226 @@ +import { ColorScheme } from "../themes/common/colorScheme" +import { background, border, foreground, svg, text } from "./components"; + + +export default function copilot(colorScheme: ColorScheme) { + let layer = colorScheme.middle; + + let content_width = 264; + + let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component + background: background(layer), + border: border(layer, "default"), + cornerRadius: 4, + margin: { + top: 4, + bottom: 4, + left: 8, + right: 8 + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(layer, "sans", "default", { size: "sm" }), + hover: { + ...text(layer, "sans", "default", { size: "sm" }), + background: background(layer, "hovered"), + border: border(layer, "active"), + }, + }; + + return { + outLinkIcon: { + icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12), + container: { + cornerRadius: 6, + padding: { left: 6 }, + }, + hover: { + icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12) + }, + }, + modal: { + titleText: { + ...text(layer, "sans", { size: "xs", "weight": "bold" }) + }, + titlebar: { + background: background(colorScheme.lowest), + border: border(layer, "active"), + padding: { + top: 4, + bottom: 4, + left: 8, + right: 8, + } + }, + container: { + background: background(colorScheme.lowest), + padding: { + top: 0, + left: 0, + right: 0, + bottom: 8, + } + }, + closeIcon: { + icon: svg(foreground(layer, "variant"), "icons/x_mark_8.svg", 8, 8), + container: { + cornerRadius: 2, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + right: 0 + } + }, + hover: { + icon: svg(foreground(layer, "on"), "icons/x_mark_8.svg", 8, 8), + }, + clicked: { + icon: svg(foreground(layer, "base"), "icons/x_mark_8.svg", 8, 8), + } + }, + dimensions: { + width: 280, + height: 280, + }, + }, + + auth: { + content_width, + + ctaButton, + + header: { + icon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 92, 32), + container: { + margin: { + top: 35, + bottom: 5, + left: 0, + right: 0 + } + }, + }, + + prompting: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + margin: { + top: 6, + bottom: 12, + left: 0, + right: 0 + } + }, + + hint: { + ...text(layer, "sans", { size: "xs", color: "#838994" }), + margin: { + top: 6, + bottom: 2 + } + }, + + deviceCode: { + text: + text(layer, "mono", { size: "sm" }), + cta: { + ...ctaButton, + background: background(colorScheme.lowest), + border: border(colorScheme.lowest, "inverted"), + padding: { + top: 0, + bottom: 0, + left: 16, + right: 16, + }, + margin: { + left: 16, + right: 16, + } + }, + left: content_width / 2, + leftContainer: { + padding: { + top: 3, + bottom: 3, + left: 0, + right: 6, + }, + }, + right: content_width * 1 / 3, + rightContainer: { + border: border(colorScheme.lowest, "inverted", { bottom: false, right: false, top: false, left: true }), + padding: { + top: 3, + bottom: 5, + left: 8, + right: 0, + }, + hover: { + border: border(layer, "active", { bottom: false, right: false, top: false, left: true }), + }, + } + }, + }, + + notAuthorized: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + + margin: { + top: 16, + bottom: 16, + left: 0, + right: 0 + } + }, + + warning: { + ...text(layer, "sans", { size: "xs", color: foreground(layer, "warning") }), + border: border(layer, "warning"), + background: background(layer, "warning"), + cornerRadius: 2, + padding: { + top: 4, + left: 4, + bottom: 4, + right: 4, + }, + margin: { + bottom: 16, + left: 8, + right: 8 + } + }, + }, + + authorized: { + subheading: { + ...text(layer, "sans", { size: "xs" }), + + margin: { + top: 16, + bottom: 16 + } + }, + + hint: { + ...text(layer, "sans", { size: "xs", color: "#838994" }), + margin: { + top: 24, + bottom: 4 + } + }, + + }, + } + } +} diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 4a7aae4c1bf0936da11034c34629ad01256ec727..a1b791d00da6eb80366d08c02bfc4814893e9eb3 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -43,6 +43,10 @@ export default function editor(colorScheme: ColorScheme) { background: background(layer), activeLineBackground: withOpacity(background(layer, "on"), 0.75), highlightedLineBackground: background(layer, "on"), + // Inline autocomplete suggestions, Co-pilot suggestions, etc. + suggestion: { + color: syntax.predictive.color, + }, codeActions: { indicator: { color: foreground(layer, "variant"), diff --git a/styles/src/styleTree/simpleMessageNotification.ts b/styles/src/styleTree/simpleMessageNotification.ts index 36b295c640241ce161fecff3450a85421aca75e3..dde689e9bd0563145f0b91ac4109a55eb5911d6c 100644 --- a/styles/src/styleTree/simpleMessageNotification.ts +++ b/styles/src/styleTree/simpleMessageNotification.ts @@ -1,5 +1,5 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { foreground, text } from "./components" +import { background, border, foreground, text } from "./components" const headerPadding = 8 @@ -14,9 +14,21 @@ export default function simpleMessageNotification( }, actionMessage: { ...text(layer, "sans", { size: "xs" }), + border: border(layer, "active"), + cornerRadius: 4, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + + margin: { left: headerPadding, top: 6, bottom: 6 }, hover: { - color: foreground(layer, "hovered"), + ...text(layer, "sans", "default", { size: "xs" }), + background: background(layer, "hovered"), + border: border(layer, "active"), }, }, dismissButton: { diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts index 252489ef1bdd1618693b1180e30f2c61591701e9..23e29c4a4049a5704703f8d28e874e186b83fcf7 100644 --- a/styles/src/styleTree/welcome.ts +++ b/styles/src/styleTree/welcome.ts @@ -6,6 +6,7 @@ import { foreground, text, TextProperties, + svg, } from "./components" export default function welcome(colorScheme: ColorScheme) { @@ -32,14 +33,7 @@ export default function welcome(colorScheme: ColorScheme) { return { pageWidth: 320, - logo: { - color: foreground(layer, "default"), - icon: "icons/logo_96.svg", - dimensions: { - width: 64, - height: 64, - }, - }, + logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64), logoSubheading: { ...text(layer, "sans", "variant", { size: "md" }), margin: { @@ -109,14 +103,7 @@ export default function welcome(colorScheme: ColorScheme) { ...text(layer, "sans", interactive_text_size), // Also supports margin, container, border, etc. }, - icon: { - color: foreground(layer, "on"), - icon: "icons/check_12.svg", - dimensions: { - width: 12, - height: 12, - }, - }, + icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12), default: { ...checkboxBase, background: background(layer, "default"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 1de2fe95020527765eae2c4b892642b447d11a0f..11f6561bd3f4ed37cd24a53ae941c8754b09b512 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,6 +1,6 @@ import { ColorScheme } from "../themes/common/colorScheme" import { withOpacity } from "../utils/color" -import { background, border, borderColor, foreground, text } from "./components" +import { background, border, borderColor, foreground, svg, text } from "./components" import statusBar from "./statusBar" import tabBar from "./tabBar" @@ -46,27 +46,14 @@ export default function workspace(colorScheme: ColorScheme) { width: 256, height: 256, }, - logo: { - color: withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, - logoShadow: { - color: withOpacity( - colorScheme.isLight - ? "#FFFFFF" - : colorScheme.lowest.base.default.background, - colorScheme.isLight ? 1 : 0.6 - ), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, + logo: svg(withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), "icons/logo_96.svg", 256, 256), + + logoShadow: svg(withOpacity( + colorScheme.isLight + ? "#FFFFFF" + : colorScheme.lowest.base.default.background, + colorScheme.isLight ? 1 : 0.6 + ), "icons/logo_96.svg", 256, 256), keyboardHints: { margin: { top: 96,