Merge pull request #2316 from zed-industries/copilot

Mikayla Maki created

🚨 WIP 🚨 Copilot

Change summary

Cargo.lock                                        |  69 +
Cargo.toml                                        |   3 
assets/icons/copilot_16.svg                       |   4 
assets/icons/copilot_disabled_16.svg              |   2 
assets/icons/copilot_error_16.svg                 |   2 
assets/icons/copilot_init_16.svg                  |   1 
assets/icons/github-copilot-dummy.svg             |   0 
assets/icons/link_out_12.svg                      |   5 
assets/icons/zed_plus_copilot_32.svg              |   3 
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 
crates/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 
crates/node_runtime/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 
crates/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 
styles/src/styleTree/simpleMessageNotification.ts |  16 
styles/src/styleTree/welcome.ts                   |  19 
styles/src/styleTree/workspace.ts                 |  31 
113 files changed, 3,377 insertions(+), 695 deletions(-)

Detailed changes

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",

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",

assets/icons/copilot_16.svg 🔗

@@ -0,0 +1,12 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4.2926 3.48996C3.79162 3.79616 3.44871 4.26316 3.44871 4.93872C3.44871 5.75753 3.65302 6.19648 3.88658 6.43349C4.11948 6.66983 4.47018 6.79529 4.95638 6.79529C5.64158 6.79529 6.23176 6.65786 6.64548 6.37099C7.03216 6.10286 7.32149 5.66636 7.35698 4.91278C7.38386 4.34213 7.36863 3.96084 7.21748 3.68905C7.09721 3.47279 6.81682 3.2089 5.96976 3.11109C5.4731 3.05374 4.81346 3.17162 4.2926 3.48996ZM3.72539 2.5525C4.46348 2.10138 5.36842 1.93724 6.09436 2.02107C7.1336 2.14107 7.8142 2.51324 8.17039 3.15373C8.49569 3.73867 8.47238 4.43479 8.44743 4.96466C8.39736 6.02772 7.95809 6.7938 7.26541 7.27411C6.59976 7.73566 5.75982 7.89249 4.95638 7.89249C4.2936 7.89249 3.61755 7.71967 3.11095 7.20558C2.605 6.69216 2.35705 5.92853 2.35705 4.93872C2.35705 3.80566 2.96744 3.01576 3.72539 2.5525Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69546 8.97734C7.02432 8.97734 7.29091 9.24528 7.29091 9.57581V10.8725C7.29091 11.203 7.02432 11.471 6.69546 11.471C6.3666 11.471 6.1 11.203 6.1 10.8725V9.57581C6.1 9.24528 6.3666 8.97734 6.69546 8.97734Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.45301 7.32072C2.56382 6.90477 2.81104 6.35118 3.40175 6.17048L3.74851 7.31556C3.74509 7.31822 3.73425 7.32798 3.71842 7.35038C3.68409 7.39897 3.64151 7.48723 3.6034 7.6303C3.52629 7.91973 3.49839 8.31081 3.4984 8.73318V10.8761C3.5122 10.9688 3.52011 11.0083 3.53501 11.0478C3.5474 11.0807 3.57295 11.1339 3.6523 11.2153C3.83266 11.4004 4.24428 11.6866 5.21016 12.1174C5.99398 12.467 6.35125 12.6243 6.68361 12.7078C6.99799 12.7869 7.30564 12.8031 7.99999 12.8031V14C7.31311 14 6.86876 13.9882 6.3946 13.869C5.95125 13.7575 5.49691 13.5549 4.78914 13.2391C4.76868 13.23 4.74801 13.2208 4.72712 13.2115C3.73729 12.77 3.14865 12.4092 2.80139 12.0527C2.61692 11.8634 2.49682 11.6721 2.42136 11.4719C2.35507 11.2961 2.33141 11.1302 2.31663 11.0266C2.31561 11.0194 2.31463 11.0126 2.31369 11.0061L2.30749 10.9632V8.73321C2.30748 8.28334 2.33457 7.76532 2.45301 7.32072Z" fill="white"/>
@@ -0,0 +1,5 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
+<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
+<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
+</svg>

assets/icons/zed_plus_copilot_32.svg 🔗

@@ -0,0 +1,14 @@
+<svg width="93" height="32" viewBox="0 0 93 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.03996 7.04962C8.00936 7.67635 7.30396 8.63219 7.30396 10.0149C7.30396 11.6908 7.72425 12.5893 8.2047 13.0744C8.68381 13.5581 9.40526 13.8149 10.4054 13.8149C11.815 13.8149 13.0291 13.5336 13.8802 12.9464C14.6756 12.3977 15.2708 11.5042 15.3438 9.96182C15.3991 8.79382 15.3678 8.01341 15.0568 7.45711C14.8094 7.01449 14.2326 6.47436 12.4901 6.27416C11.4684 6.15678 10.1114 6.39804 9.03996 7.04962ZM7.87312 5.13084C9.39147 4.2075 11.2531 3.87155 12.7464 4.04312C14.8843 4.28874 16.2844 5.05049 17.0171 6.36142C17.6863 7.55867 17.6384 8.98348 17.587 10.068C17.484 12.2439 16.5804 13.8118 15.1554 14.7949C13.7861 15.7396 12.0582 16.0606 10.4054 16.0606C9.04201 16.0606 7.65128 15.7069 6.60913 14.6547C5.56832 13.6038 5.05825 12.0408 5.05825 10.0149C5.05825 7.6958 6.3139 6.07903 7.87312 5.13084Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.983 18.2811C14.6595 18.2811 15.2079 18.8295 15.2079 19.506V22.16C15.2079 22.8365 14.6595 23.385 13.983 23.385C13.3065 23.385 12.758 22.8365 12.758 22.16V19.506C12.758 18.8295 13.3065 18.2811 13.983 18.2811Z" fill="white"/>

assets/keymaps/default.json 🔗

@@ -176,7 +176,10 @@
                 {
                     "focus": false
                 }
-            ]
+            ],
+            "alt-]": "copilot::NextSuggestion",
+            "alt-[": "copilot::PreviousSuggestion",
+            "alt-\\": "copilot::Toggle"
         }
     },
     {

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.

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"

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";

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"

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"
 

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) {

crates/client/src/http.rs 🔗

@@ -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<AsyncBody>;
-pub type Response = isahc::Response<AsyncBody>;
-
-pub trait HttpClient: Send + Sync {
-    fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>>;
-
-    fn get<'a>(
-        &'a self,
-        uri: &str,
-        body: AsyncBody,
-        follow_redirects: bool,
-    ) -> BoxFuture<'a, Result<Response, 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(),
-        }
-    }
-}
-
-pub fn client() -> Arc<dyn HttpClient> {
-    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<Result<Response, Error>> {
-        Box::pin(async move { self.send_async(req).await })
-    }
-}

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(),

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<Peer>,
@@ -219,46 +217,3 @@ impl Drop for FakeServer {
         self.disconnect();
     }
 }
-
-pub struct FakeHttpClient {
-    handler: Box<
-        dyn 'static
-            + Send
-            + Sync
-            + Fn(Request) -> BoxFuture<'static, Result<Response, http::Error>>,
-    >,
-}
-
-impl FakeHttpClient {
-    pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
-    where
-        Fut: 'static + Send + Future<Output = Result<Response, http::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<dyn HttpClient> {
-        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<Result<Response, crate::http::Error>> {
-        let future = (self.handler)(req);
-        Box::pin(async move { future.await.map(Into::into) })
-    }
-}

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)]

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"
 

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;

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"] }

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),
                 ]
             };
 

crates/collections/src/collections.rs 🔗

@@ -24,3 +24,10 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
 pub type HashSet<T> = std::collections::HashSet<T>;
 
 pub use std::collections::*;
+
+// NEW TYPES
+
+#[derive(Default)]
+pub struct CommandPaletteFilter {
+    pub filtered_namespaces: HashSet<&'static str>,
+}

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"

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::<CommandPalette>::init(cx);

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<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
+
+pub enum ContextMenuItemLabel {
+    String(Cow<'static, str>),
+    Element(ContextMenuItemBuilder),
+}
+
+pub enum ContextMenuAction {
+    ParentAction {
+        action: Box<dyn Action>,
+    },
+    ViewAction {
+        action: Box<dyn Action>,
+        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<dyn Action>,
+        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<Cow<'static, str>>, 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<Cow<'static, str>>,
+        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<Self>) {
         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::<MenuItem>::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()

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"] }

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<Client>, node_runtime: Arc<NodeRuntime>, 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::<collections::CommandPaletteFilter, _, _>(
+            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<Task<()>>,
+    },
+    Error(Arc<str>),
+    Started {
+        server: Arc<LanguageServer>,
+        status: SignInStatus,
+    },
+}
+
+#[derive(Clone, Debug)]
+enum SignInStatus {
+    Authorized {
+        _user: String,
+    },
+    Unauthorized {
+        _user: String,
+    },
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+        task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
+    },
+    SignedOut,
+}
+
+#[derive(Debug, Clone)]
+pub enum Status {
+    Starting {
+        task: Shared<Task<()>>,
+    },
+    Error(Arc<str>),
+    Disabled,
+    SignedOut,
+    SigningIn {
+        prompt: Option<request::PromptUserDeviceFlow>,
+    },
+    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<dyn HttpClient>,
+    node_runtime: Arc<NodeRuntime>,
+    server: CopilotServer,
+}
+
+impl Entity for Copilot {
+    type Event = ();
+}
+
+impl Copilot {
+    pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
+        match self.server {
+            CopilotServer::Starting { ref task } => Some(task.clone()),
+            _ => None,
+        }
+    }
+
+    pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
+        if cx.has_global::<ModelHandle<Self>>() {
+            Some(cx.global::<ModelHandle<Self>>().clone())
+        } else {
+            None
+        }
+    }
+
+    fn start(
+        http: Arc<dyn HttpClient>,
+        node_runtime: Arc<NodeRuntime>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        cx.observe_global::<Settings, _>({
+            let http = http.clone();
+            let node_runtime = node_runtime.clone();
+            move |this, cx| {
+                if cx.global::<Settings>().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::<Settings>().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<dyn HttpClient>,
+        node_runtime: Arc<NodeRuntime>,
+        this: ModelHandle<Self>,
+        mut cx: AsyncAppContext,
+    ) -> impl Future<Output = ()> {
+        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::CheckStatus>(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<Self>) -> Task<Result<()>> {
+        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::SignInInitiate>(
+                                        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::SignInConfirm>(
+                                                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<Self>) -> Task<Result<()>> {
+        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::SignOut>(request::SignOutParams {})
+                    .await?;
+                anyhow::Ok(())
+            })
+        } else {
+            Task::ready(Err(anyhow!("copilot hasn't started yet")))
+        }
+    }
+
+    fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> 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<T>(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Completion>>>
+    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::<request::GetCompletions>(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<T>(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Completion>>>
+    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::<request::GetCompletionsCycling>(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<Self>,
+    ) {
+        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<Arc<LanguageServer>> {
+        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<T>(
+    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::<Settings>();
+    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<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+    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<dyn HttpClient>) -> anyhow::Result<PathBuf> {
+        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
+        }
+    }
+}

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<Completion>,
+}
+
+#[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";
+}

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<ViewHandle<CopilotCodeVerification>> = 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::<Settings>().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>) {
+        self.status = status;
+        cx.notify();
+    }
+
+    fn render_device_code(
+        data: &PromptUserDeviceFlow,
+        style: &theme::Copilot,
+        cx: &mut gpui::RenderContext<Self>,
+    ) -> 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::<Self>::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<Self>,
+    ) -> 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<Self>,
+    ) -> 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<Self>,
+    ) -> 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<Self>) {
+        cx.notify()
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
+        let style = cx.global::<Settings>().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()
+        })
+    }
+}

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"

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<str>,
+}
+
+#[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::<Settings>().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::<Settings>().copilot_on(None);
+
+        SettingsFile::update(cx, move |file_contents| {
+            file_contents.editor.copilot = Some((!copilot_on).into())
+        })
+    });
+}
+
+pub struct CopilotButton {
+    popup_menu: ViewHandle<ContextMenu>,
+    editor_subscription: Option<(Subscription, usize)>,
+    editor_enabled: Option<bool>,
+    language: Option<Arc<str>>,
+}
+
+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::<Settings>();
+
+        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::<Self>::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::<Self, _>(
+                    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>) -> 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::<Settings, _>(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<Self>) {
+        let settings = cx.global::<Settings>();
+
+        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::<Settings>().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<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+        let settings = cx.global::<Settings>();
+        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<Self>) {
+        if let Some(editor) = item.map(|item| item.act_as::<Editor>(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();
+    }
+}

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]

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 }

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 }

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<Item = &str> {
         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<Item = &str> {
         (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::<Vec<_>>()
                 .into_iter()
@@ -398,9 +398,18 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
-        self.block_snapshot
-            .chunks(display_rows, language_aware, Some(&self.text_highlights))
+    pub fn chunks(
+        &self,
+        display_rows: Range<u32>,
+        language_aware: bool,
+        suggestion_highlight: Option<HighlightStyle>,
+    ) -> 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<Color>, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = 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);

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<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> 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::<String>();

crates/editor/src/display_map/suggestion_map.rs 🔗

@@ -60,7 +60,6 @@ impl SuggestionPoint {
 pub struct Suggestion<T> {
     pub position: T,
     pub text: Rope,
-    pub highlight_style: HighlightStyle,
 }
 
 pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
@@ -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<Item = char> {
         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<SuggestionOffset>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> 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<FoldChunks<'a>>,
     suggestion_chunks: Option<text::Chunks<'a>>,
     suffix_chunks: Option<FoldChunks<'a>>,
-    highlight_style: HighlightStyle,
+    highlight_style: Option<HighlightStyle>,
 }
 
 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::<String>();
                 assert_eq!(
@@ -842,7 +845,6 @@ mod tests {
                         .collect::<String>()
                         .as_str()
                         .into(),
-                    highlight_style: Default::default(),
                 })
             };
 

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<TabPoint>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> 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::<String>(),
@@ -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::<String>(),
                 expected_text,

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::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -573,6 +575,7 @@ impl WrapSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        suggestion_highlight: Option<HighlightStyle>,
     ) -> 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<Item = &str> {
-            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::<String>();
                 assert_eq!(

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<Subscription>,
 }
 
@@ -1003,6 +1008,65 @@ impl CodeActionsMenu {
     }
 }
 
+pub struct CopilotState {
+    excerpt_id: Option<ExcerptId>,
+    pending_refresh: Task<Option<()>>,
+    completions: Vec<copilot::Completion>,
+    active_completion_index: usize,
+    pub user_enabled: Option<bool>,
+}
+
+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<Anchor>,
@@ -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<Self>) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+
+        if self.mode != EditorMode::Full {
+            return None;
+        }
+
+        let settings = cx.global::<Settings>();
+
+        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<Self>) {
+        // 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<Self>,
+    ) {
+        // 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>) {
+        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::<Settings>().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<Self>) {
+        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<Self>) -> 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<Self>) -> bool {
+        self.display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(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<Self>) {
+        if self.accept_copilot_suggestion(cx) {
+            return;
+        }
+
         if self.move_to_next_snippet_tabstop(cx) {
             return;
         }

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,

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" }

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"

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"

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"] }

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<Item = usize> + '_ {
         self.cx.windows.keys().copied()
     }

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<ElementBox> for ElementRc {
     fn from(val: ElementBox) -> Self {
         val.0

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),
         )
     }

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 {

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"

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"))]

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 }

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]

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]

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"

crates/zed/src/languages/node_runtime.rs → 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";
 

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"

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" }

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"

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"

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"

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 =

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]

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 }

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"

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"

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"

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<CopilotSettings> for bool {
+    fn from(value: CopilotSettings) -> Self {
+        match value {
+            CopilotSettings::On => true,
+            CopilotSettings::Off => false,
+        }
+    }
+}
+
+impl CopilotSettings {
+    pub fn is_on(&self) -> bool {
+        <CopilotSettings as Into<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<bool>,
     pub formatter: Option<Formatter>,
     pub enable_language_server: Option<bool>,
+    pub copilot: Option<OnOff>,
+}
+
+#[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<OnOff> for bool {
+    fn from(value: OnOff) -> bool {
+        value.as_bool()
+    }
+}
+
+impl From<bool> 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<bool>,
     #[serde(default)]
     pub base_keymap: Option<BaseKeymap>,
+    #[serde(default)]
+    pub enable_copilot_integration: Option<bool>,
 }
 
 #[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]

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"

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 }
 
 
 

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"

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<ContainedText>,
@@ -115,6 +116,52 @@ pub struct AvatarStyle {
     pub outer_corner_radius: f32,
 }
 
+#[derive(Deserialize, Default, Clone)]
+pub struct Copilot {
+    pub out_link_icon: Interactive<IconStyle>,
+    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<ContainerStyle>,
+}
+
+#[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<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
+    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<T> {
     pub default: T,
     pub hover: Option<T>,
+    pub hover_and_active: Option<T>,
     pub clicked: Option<T>,
+    pub click_and_active: Option<T>,
     pub active: Option<T>,
     pub disabled: Option<T>,
 }
@@ -699,7 +749,17 @@ pub struct Interactive<T> {
 impl<T> Interactive<T> {
     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<T> {
             #[serde(flatten)]
             default: Value,
             hover: Option<Value>,
+            hover_and_active: Option<Value>,
             clicked: Option<Value>,
+            click_and_active: Option<Value>,
             active: Option<Value>,
             disabled: Option<Value>,
         }
@@ -751,7 +813,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         };
 
         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<T> {
         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,

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<T: 'static, V: View>(
 ) -> MouseEventHandler<T> {
     MouseEventHandler::<T>::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<T: 'static, V: View>(
 }
 
 #[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<V: View>(
     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<ContainedText>;
+
+pub fn cta_button<L, A, V>(
+    label: L,
+    action: A,
+    max_width: f32,
+    style: &ButtonStyle,
+    cx: &mut RenderContext<V>,
+) -> ElementBox
+where
+    L: Into<Cow<'static, str>>,
+    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<L, V, F>(
+    label: L,
+    max_width: f32,
+    style: &ButtonStyle,
+    cx: &mut RenderContext<V>,
+    f: F,
+) -> MouseEventHandler<F>
+where
+    L: Into<Cow<'static, str>>,
+    V: View,
+    F: Fn(MouseClick, &mut EventContext) + 'static,
+{
+    MouseEventHandler::<F>::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<IconStyle>,
+    container: ContainerStyle,
+    titlebar: ContainerStyle,
+    title_text: Interactive<TextStyle>,
+    dimensions: Dimensions,
+}
+
+impl ModalStyle {
+    pub fn dimensions(&self) -> Vector2F {
+        self.dimensions.to_vec()
+    }
+}
+
+pub fn modal<V, I, F>(
+    title: I,
+    style: &ModalStyle,
+    cx: &mut RenderContext<V>,
+    build_modal: F,
+) -> ElementBox
+where
+    V: View,
+    I: Into<Cow<'static, str>>,
+    F: FnOnce(&mut gpui::RenderContext<V>) -> 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::<V>::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()
+}

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"
 

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<F>(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();
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

crates/zed/src/languages/github.rs → 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<GithubReleaseAsset>,
 }
 
 #[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<dyn HttpClient>,
 ) -> Result<GithubRelease, anyhow::Error> {
@@ -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)

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<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
+
+    fn get<'a>(
+        &'a self,
+        uri: &str,
+        body: AsyncBody,
+        follow_redirects: bool,
+    ) -> BoxFuture<'a, Result<Response<AsyncBody>, 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<Response<AsyncBody>, 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<dyn HttpClient> {
+    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<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, 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<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>,
+    >,
+}
+
+#[cfg(feature = "test-support")]
+impl FakeHttpClient {
+    pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
+    where
+        Fut: 'static + Send + futures::Future<Output = Result<Response<AsyncBody>, Error>>,
+        F: 'static + Send + Sync + Fn(Request<AsyncBody>) -> Fut,
+    {
+        Arc::new(Self {
+            handler: Box::new(move |req| Box::pin(handler(req))),
+        })
+    }
+
+    pub fn with_404_response() -> Arc<dyn HttpClient> {
+        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<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
+        let future = (self.handler)(req);
+        Box::pin(async move { future.await.map(Into::into) })
+    }
+}

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");

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<T> {
     fn sorted(&self) -> Self;
     fn to_inclusive(&self) -> RangeInclusive<T>;
     fn overlaps(&self, other: &Range<T>) -> bool;
+    fn contains_inclusive(&self, other: &Range<T>) -> bool;
 }
 
 impl<T: Ord + Clone> RangeExt<T> for Range<T> {
@@ -312,6 +316,10 @@ impl<T: Ord + Clone> RangeExt<T> for Range<T> {
     fn overlaps(&self, other: &Range<T>) -> bool {
         self.start < other.end && other.start < self.end
     }
+
+    fn contains_inclusive(&self, other: &Range<T>) -> bool {
+        self.start <= other.start && other.end <= self.end
+    }
 }
 
 impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
@@ -326,6 +334,10 @@ impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
     fn overlaps(&self, other: &Range<T>) -> bool {
         self.start() < &other.end && &other.start <= self.end()
     }
+
+    fn contains_inclusive(&self, other: &Range<T>) -> bool {
+        self.start() <= &other.start && &other.end <= self.end()
+    }
 }
 
 #[cfg(test)]

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" }

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,

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<L, A>(
-        &self,
-        label: L,
-        action: A,
-        width: f32,
-        cx: &mut RenderContext<Self>,
-    ) -> ElementBox
-    where
-        L: Into<Cow<'static, str>>,
-        A: 'static + Action + Clone,
-    {
-        let theme = cx.global::<Settings>().theme.clone();
-        MouseEventHandler::<A>::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<T: 'static>(
-    //     &self,
-    //     label: &'static str,
-    //     style: &CheckboxStyle,
-    //     checked: bool,
-    //     cx: &mut RenderContext<Self>,
-    //     set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
-    // ) -> ElementBox {
-    //     MouseEventHandler::<T>::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 {

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"] }

crates/workspace/src/notifications.rs 🔗

@@ -97,7 +97,7 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(&notification, 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<Self>) {
+    pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
+        let type_id = TypeId::of::<V>();
+
+        self.dismiss_notification_internal(type_id, id, cx)
+    }
+
+    fn dismiss_notification_internal(
+        &mut self,
+        type_id: TypeId,
+        id: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
         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<I: Into<Cow<'static, str>>>(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<Workspace>| {
-                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<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
+            message: S1,
+            click_action: Box<dyn Action>,
+            click_message: S2,
+        ) -> Self {
+            Self {
+                message: message.into(),
+                click_action: Some(click_action),
+                click_message: Some(click_message.into()),
+            }
+        }
+
         pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
             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 {

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<dyn Action>)>,
+}
+
+impl Toast {
+    pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
+        Toast {
+            id,
+            msg: msg.into(),
+            click: None,
+        }
+    }
+
+    pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
+        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<AppState>, cx: &mut MutableAppContext) {
         .detach();
     });
 
+    cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
+        workspace.dismiss_notification::<MessageNotification>(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::<MessageNotification>(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<Workspace>, 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<Workspace>, cx: &mut AsyncAp
                                 "},
                                 backup_path
                             ),
-                            OsOpen(backup_path.to_string()),
+                            OsOpen::new(backup_path.to_string()),
                             "Click to show old database in finder",
                         )
                     })

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]

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<dyn HttpClient>,
-    background: Arc<Background>,
     languages: Arc<LanguageRegistry>,
     themes: Arc<ThemeRegistry>,
+    node_runtime: Arc<NodeRuntime>,
 ) {
-    let node_runtime = NodeRuntime::new(http, background);
-
     for (name, grammar, lsp_adapter) in [
         (
             "c",

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 {

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 {

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<OsString> {
@@ -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(),

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<OsString> {
@@ -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 {

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 {

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)]

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;

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<OsString> {
@@ -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 {

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;
 

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 {

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<OsString> {
@@ -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 {

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<OsString> {
     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 {

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();

crates/zed/src/zed.rs 🔗

@@ -261,6 +261,7 @@ pub fn init(app_state: &Arc<AppState>, 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);
         }

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"
 

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),

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,
+        }
+    }
+}

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
+                    }
+                },
+
+            },
+        }
+    }
+}

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"),

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: {

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"),

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,