Merge pull request #2407 from zed-industries/lsp-log-view

Max Brunsfeld created

Add a simple language server log view

Change summary

Cargo.lock                           |  23 +
Cargo.toml                           |   2 
crates/activity_indicator/Cargo.toml |   2 
crates/call/Cargo.toml               |   2 
crates/client/Cargo.toml             |   2 
crates/collab/Cargo.toml             |   2 
crates/collab_ui/Cargo.toml          |   2 
crates/copilot/Cargo.toml            |   2 
crates/copilot_button/Cargo.toml     |   2 
crates/editor/Cargo.toml             |   2 
crates/editor/src/editor.rs          |  21 +
crates/feedback/Cargo.toml           |   2 
crates/fs/Cargo.toml                 |   2 
crates/git/Cargo.toml                |   2 
crates/gpui/Cargo.toml               |   2 
crates/language/Cargo.toml           |   2 
crates/live_kit_client/Cargo.toml    |   4 
crates/live_kit_server/Cargo.toml    |   2 
crates/lsp/Cargo.toml                |   2 
crates/lsp/src/lsp.rs                | 122 +++++-
crates/lsp_log/Cargo.toml            |  29 +
crates/lsp_log/src/lsp_log.rs        | 523 ++++++++++++++++++++++++++++++
crates/node_runtime/Cargo.toml       |   2 
crates/project/Cargo.toml            |   2 
crates/project/src/project.rs        |  36 +
crates/project_panel/Cargo.toml      |   2 
crates/project_symbols/Cargo.toml    |   2 
crates/rpc/Cargo.toml                |   2 
crates/search/Cargo.toml             |   2 
crates/settings/Cargo.toml           |   2 
crates/sqlez/Cargo.toml              |   4 
crates/terminal/Cargo.toml           |   2 
crates/terminal_view/Cargo.toml      |   2 
crates/theme/src/ui.rs               |  34 +
crates/util/Cargo.toml               |   2 
crates/welcome/src/welcome.rs        |  10 
crates/workspace/Cargo.toml          |   2 
crates/zed/Cargo.toml                |   5 
crates/zed/src/zed.rs                |   7 
39 files changed, 780 insertions(+), 92 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3612,6 +3612,26 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "lsp_log"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "editor",
+ "futures 0.3.25",
+ "gpui",
+ "language",
+ "lsp",
+ "project",
+ "serde",
+ "settings",
+ "theme",
+ "unindent",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "mach"
 version = "0.3.2"
@@ -7239,7 +7259,7 @@ dependencies = [
 [[package]]
 name = "tree-sitter-json"
 version = "0.20.0"
-source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8#137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8"
+source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=40a81c01a40ac48744e0c8ccabbaba1920441199#40a81c01a40ac48744e0c8ccabbaba1920441199"
 dependencies = [
  "cc",
  "tree-sitter",
@@ -8571,6 +8591,7 @@ dependencies = [
  "libc",
  "log",
  "lsp",
+ "lsp_log",
  "node_runtime",
  "num_cpus",
  "outline",

Cargo.toml 🔗

@@ -35,6 +35,7 @@ members = [
     "crates/live_kit_client",
     "crates/live_kit_server",
     "crates/lsp",
+    "crates/lsp_log",
     "crates/media",
     "crates/menu",
     "crates/node_runtime",
@@ -77,6 +78,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 rand = { version = "0.8" }
 postage = { version = "0.5", features = ["futures-traits"] }
 smallvec = { version = "1.6", features = ["union"] }
+futures = { version = "0.3" }
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

crates/activity_indicator/Cargo.toml 🔗

@@ -17,5 +17,5 @@ project = { path = "../project" }
 settings = { path = "../settings" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
-futures = "0.3"
+futures = { workspace = true }
 smallvec = { workspace = true }

crates/call/Cargo.toml 🔗

@@ -33,7 +33,7 @@ util = { path = "../util" }
 
 anyhow = "1.0.38"
 async-broadcast = "0.4"
-futures = "0.3"
+futures = { workspace = true }
 postage = { workspace = true }
 
 [dev-dependencies]

crates/client/Cargo.toml 🔗

@@ -22,7 +22,7 @@ sum_tree = { path = "../sum_tree" }
 anyhow = "1.0.38"
 async-recursion = "0.3"
 async-tungstenite = { version = "0.16", features = ["async-tls"] }
-futures = "0.3"
+futures = { workspace = true }
 image = "0.23"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/collab/Cargo.toml 🔗

@@ -27,7 +27,7 @@ base64 = "0.13"
 clap = { version = "3.1", features = ["derive"], optional = true }
 dashmap = "5.4"
 envy = "0.4.2"
-futures = "0.3"
+futures = { workspace = true }
 hyper = "0.14"
 lazy_static = "1.4"
 lipsum = { version = "0.8", optional = true }

crates/collab_ui/Cargo.toml 🔗

@@ -40,7 +40,7 @@ theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 anyhow = "1.0"
-futures = "0.3"
+futures = { workspace = true }
 log = "0.4"
 postage = { workspace = true }
 serde = { workspace = true }

crates/copilot/Cargo.toml 🔗

@@ -35,7 +35,7 @@ log = "0.4"
 serde = { workspace = true }
 serde_derive = { workspace = true }
 smol = "1.2.5"
-futures = "0.3"
+futures = { workspace = true }
 
 [dev-dependencies]
 clock = { path = "../clock" }

crates/copilot_button/Cargo.toml 🔗

@@ -19,4 +19,4 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 anyhow = "1.0"
 smol = "1.2.5"
-futures = "0.3"
+futures = { workspace = true }

crates/editor/Cargo.toml 🔗

@@ -47,7 +47,7 @@ workspace = { path = "../workspace" }
 
 aho-corasick = "0.7"
 anyhow = "1.0"
-futures = "0.3"
+futures = { workspace = true }
 indoc = "1.0.4"
 itertools = "0.10"
 lazy_static = "1.4"

crates/editor/src/editor.rs 🔗

@@ -511,6 +511,7 @@ pub struct Editor {
     workspace_id: Option<WorkspaceId>,
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
+    read_only: bool,
     leader_replica_id: Option<u16>,
     remote_id: Option<ViewId>,
     hover_state: HoverState,
@@ -1283,6 +1284,7 @@ impl Editor {
             workspace_id: None,
             keymap_context_layers: Default::default(),
             input_enabled: true,
+            read_only: false,
             leader_replica_id: None,
             remote_id: None,
             hover_state: Default::default(),
@@ -1425,6 +1427,10 @@ impl Editor {
         self.input_enabled = input_enabled;
     }
 
+    pub fn set_read_only(&mut self, read_only: bool) {
+        self.read_only = read_only;
+    }
+
     fn selections_did_change(
         &mut self,
         local: bool,
@@ -1533,6 +1539,10 @@ impl Editor {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
+        if self.read_only {
+            return;
+        }
+
         self.buffer
             .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
     }
@@ -1543,6 +1553,10 @@ impl Editor {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
+        if self.read_only {
+            return;
+        }
+
         self.buffer.update(cx, |buffer, cx| {
             buffer.edit(edits, Some(AutoindentMode::EachLine), cx)
         });
@@ -1897,6 +1911,9 @@ impl Editor {
     pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
         let text: Arc<str> = text.into();
 
+        if self.read_only {
+            return;
+        }
         if !self.input_enabled {
             cx.emit(Event::InputIgnored { text });
             return;
@@ -2282,6 +2299,10 @@ impl Editor {
         autoindent_mode: Option<AutoindentMode>,
         cx: &mut ViewContext<Self>,
     ) {
+        if self.read_only {
+            return;
+        }
+
         let text: Arc<str> = text.into();
         self.transact(cx, |this, cx| {
             let old_selections = this.selections.all_adjusted(cx);

crates/feedback/Cargo.toml 🔗

@@ -16,7 +16,7 @@ client = { path = "../client" }
 editor = { path = "../editor" }
 language = { path = "../language" }
 log = "0.4"
-futures = "0.3"
+futures = { workspace = true }
 gpui = { path = "../gpui" }
 human_bytes = "0.4.1"
 isahc = "1.7"

crates/fs/Cargo.toml 🔗

@@ -15,7 +15,7 @@ rope = { path = "../rope" }
 util = { path = "../util" }
 anyhow = "1.0.57"
 async-trait = "0.1"
-futures = "0.3"
+futures = { workspace = true }
 tempfile = "3"
 fsevent = { path = "../fsevent" }
 lazy_static = "1.4.0"

crates/git/Cargo.toml 🔗

@@ -19,7 +19,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 smol = "1.2"
 parking_lot = "0.11.1"
 async-trait = "0.1"
-futures = "0.3"
+futures = { workspace = true }
 git2 = { version = "0.15", default-features = false }
 
 [dev-dependencies]

crates/gpui/Cargo.toml 🔗

@@ -25,7 +25,7 @@ ctor = "0.1"
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }
 etagere = "0.2"
-futures = "0.3"
+futures = { workspace = true }
 image = "0.23"
 itertools = "0.10"
 lazy_static = "1.4.0"

crates/language/Cargo.toml 🔗

@@ -39,7 +39,7 @@ util = { path = "../util" }
 anyhow = "1.0.38"
 async-broadcast = "0.4"
 async-trait = "0.1"
-futures = "0.3"
+futures = { workspace = true }
 lazy_static = "1.4"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"

crates/live_kit_client/Cargo.toml 🔗

@@ -32,7 +32,7 @@ anyhow = "1.0.38"
 async-broadcast = "0.4"
 core-foundation = "0.9.3"
 core-graphics = "0.22.3"
-futures = "0.3"
+futures = { workspace = true }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { workspace = true }
@@ -56,7 +56,7 @@ cocoa = "0.24"
 core-foundation = "0.9.3"
 core-graphics = "0.22.3"
 foreign-types = "0.3"
-futures = "0.3"
+futures = { workspace = true }
 hmac = "0.12"
 jwt = "0.16"
 lazy_static = "1.4"

crates/live_kit_server/Cargo.toml 🔗

@@ -12,7 +12,7 @@ doctest = false
 [dependencies]
 anyhow = "1.0.38"
 async-trait = "0.1"
-futures = "0.3"
+futures = { workspace = true }
 hmac = "0.12"
 log = "0.4"
 jwt = "0.16"

crates/lsp/Cargo.toml 🔗

@@ -17,7 +17,7 @@ gpui = { path = "../gpui" }
 util = { path = "../util" }
 anyhow = "1.0"
 async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
-futures = "0.3"
+futures = { workspace = true }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lsp-types = "0.91"
 parking_lot = "0.11"

crates/lsp/src/lsp.rs 🔗

@@ -20,10 +20,10 @@ use std::{
     future::Future,
     io::Write,
     path::PathBuf,
-    str::FromStr,
+    str::{self, FromStr as _},
     sync::{
         atomic::{AtomicUsize, Ordering::SeqCst},
-        Arc,
+        Arc, Weak,
     },
 };
 use std::{path::Path, process::Stdio};
@@ -34,16 +34,18 @@ const CONTENT_LEN_HEADER: &str = "Content-Length: ";
 
 type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
 type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
+type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
 
 pub struct LanguageServer {
     server_id: LanguageServerId,
     next_id: AtomicUsize,
-    outbound_tx: channel::Sender<Vec<u8>>,
+    outbound_tx: channel::Sender<String>,
     name: String,
     capabilities: ServerCapabilities,
     code_action_kinds: Option<Vec<CodeActionKind>>,
     notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+    io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
     executor: Arc<executor::Background>,
     #[allow(clippy::type_complexity)]
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
@@ -56,9 +58,15 @@ pub struct LanguageServer {
 #[repr(transparent)]
 pub struct LanguageServerId(pub usize);
 
-pub struct Subscription {
-    method: &'static str,
-    notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+pub enum Subscription {
+    Notification {
+        method: &'static str,
+        notification_handlers: Option<Arc<Mutex<HashMap<&'static str, NotificationHandler>>>>,
+    },
+    Io {
+        id: usize,
+        io_handlers: Option<Weak<Mutex<HashMap<usize, IoHandler>>>>,
+    },
 }
 
 #[derive(Serialize, Deserialize)]
@@ -177,33 +185,40 @@ impl LanguageServer {
         Stdout: AsyncRead + Unpin + Send + 'static,
         F: FnMut(AnyNotification) + 'static + Send,
     {
-        let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
+        let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
+        let (output_done_tx, output_done_rx) = barrier::channel();
         let notification_handlers =
             Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
         let response_handlers =
             Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
+        let io_handlers = Arc::new(Mutex::new(HashMap::default()));
         let input_task = cx.spawn(|cx| {
-            let notification_handlers = notification_handlers.clone();
-            let response_handlers = response_handlers.clone();
             Self::handle_input(
                 stdout,
                 on_unhandled_notification,
-                notification_handlers,
-                response_handlers,
+                notification_handlers.clone(),
+                response_handlers.clone(),
+                io_handlers.clone(),
                 cx,
             )
             .log_err()
         });
-        let (output_done_tx, output_done_rx) = barrier::channel();
         let output_task = cx.background().spawn({
-            let response_handlers = response_handlers.clone();
-            Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err()
+            Self::handle_output(
+                stdin,
+                outbound_rx,
+                output_done_tx,
+                response_handlers.clone(),
+                io_handlers.clone(),
+            )
+            .log_err()
         });
 
         Self {
             server_id,
             notification_handlers,
             response_handlers,
+            io_handlers,
             name: Default::default(),
             capabilities: Default::default(),
             code_action_kinds,
@@ -226,6 +241,7 @@ impl LanguageServer {
         mut on_unhandled_notification: F,
         notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
         response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+        io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
         cx: AsyncAppContext,
     ) -> anyhow::Result<()>
     where
@@ -252,7 +268,13 @@ impl LanguageServer {
 
             buffer.resize(message_len, 0);
             stdout.read_exact(&mut buffer).await?;
-            log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
+
+            if let Ok(message) = str::from_utf8(&buffer) {
+                log::trace!("incoming message:{}", message);
+                for handler in io_handlers.lock().values_mut() {
+                    handler(true, message);
+                }
+            }
 
             if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
                 if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
@@ -291,9 +313,10 @@ impl LanguageServer {
 
     async fn handle_output<Stdin>(
         stdin: Stdin,
-        outbound_rx: channel::Receiver<Vec<u8>>,
+        outbound_rx: channel::Receiver<String>,
         output_done_tx: barrier::Sender,
         response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
+        io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
     ) -> anyhow::Result<()>
     where
         Stdin: AsyncWrite + Unpin + Send + 'static,
@@ -307,13 +330,17 @@ impl LanguageServer {
         });
         let mut content_len_buffer = Vec::new();
         while let Ok(message) = outbound_rx.recv().await {
-            log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
+            log::trace!("outgoing message:{}", message);
+            for handler in io_handlers.lock().values_mut() {
+                handler(false, &message);
+            }
+
             content_len_buffer.clear();
             write!(content_len_buffer, "{}", message.len()).unwrap();
             stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
             stdin.write_all(&content_len_buffer).await?;
             stdin.write_all("\r\n\r\n".as_bytes()).await?;
-            stdin.write_all(&message).await?;
+            stdin.write_all(message.as_bytes()).await?;
             stdin.flush().await?;
         }
         drop(output_done_tx);
@@ -464,6 +491,19 @@ impl LanguageServer {
         self.on_custom_request(T::METHOD, f)
     }
 
+    #[must_use]
+    pub fn on_io<F>(&self, f: F) -> Subscription
+    where
+        F: 'static + Send + FnMut(bool, &str),
+    {
+        let id = self.next_id.fetch_add(1, SeqCst);
+        self.io_handlers.lock().insert(id, Box::new(f));
+        Subscription::Io {
+            id,
+            io_handlers: Some(Arc::downgrade(&self.io_handlers)),
+        }
+    }
+
     pub fn remove_request_handler<T: request::Request>(&self) {
         self.notification_handlers.lock().remove(T::METHOD);
     }
@@ -490,9 +530,9 @@ impl LanguageServer {
             prev_handler.is_none(),
             "registered multiple handlers for the same LSP method"
         );
-        Subscription {
+        Subscription::Notification {
             method,
-            notification_handlers: self.notification_handlers.clone(),
+            notification_handlers: Some(self.notification_handlers.clone()),
         }
     }
 
@@ -537,7 +577,7 @@ impl LanguageServer {
                                             },
                                         };
                                         if let Some(response) =
-                                            serde_json::to_vec(&response).log_err()
+                                            serde_json::to_string(&response).log_err()
                                         {
                                             outbound_tx.try_send(response).ok();
                                         }
@@ -560,7 +600,7 @@ impl LanguageServer {
                                     message: error.to_string(),
                                 }),
                             };
-                            if let Some(response) = serde_json::to_vec(&response).log_err() {
+                            if let Some(response) = serde_json::to_string(&response).log_err() {
                                 outbound_tx.try_send(response).ok();
                             }
                         }
@@ -572,9 +612,9 @@ impl LanguageServer {
             prev_handler.is_none(),
             "registered multiple handlers for the same LSP method"
         );
-        Subscription {
+        Subscription::Notification {
             method,
-            notification_handlers: self.notification_handlers.clone(),
+            notification_handlers: Some(self.notification_handlers.clone()),
         }
     }
 
@@ -612,14 +652,14 @@ impl LanguageServer {
     fn request_internal<T: request::Request>(
         next_id: &AtomicUsize,
         response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
-        outbound_tx: &channel::Sender<Vec<u8>>,
+        outbound_tx: &channel::Sender<String>,
         params: T::Params,
     ) -> impl 'static + Future<Output = Result<T::Result>>
     where
         T::Result: 'static + Send,
     {
         let id = next_id.fetch_add(1, SeqCst);
-        let message = serde_json::to_vec(&Request {
+        let message = serde_json::to_string(&Request {
             jsonrpc: JSON_RPC_VERSION,
             id,
             method: T::METHOD,
@@ -662,10 +702,10 @@ impl LanguageServer {
     }
 
     fn notify_internal<T: notification::Notification>(
-        outbound_tx: &channel::Sender<Vec<u8>>,
+        outbound_tx: &channel::Sender<String>,
         params: T::Params,
     ) -> Result<()> {
-        let message = serde_json::to_vec(&Notification {
+        let message = serde_json::to_string(&Notification {
             jsonrpc: JSON_RPC_VERSION,
             method: T::METHOD,
             params,
@@ -685,8 +725,14 @@ impl Drop for LanguageServer {
 }
 
 impl Subscription {
-    pub fn detach(mut self) {
-        self.method = "";
+    pub fn detach(&mut self) {
+        match self {
+            Subscription::Notification {
+                notification_handlers,
+                ..
+            } => *notification_handlers = None,
+            Subscription::Io { io_handlers, .. } => *io_handlers = None,
+        }
     }
 }
 
@@ -698,7 +744,21 @@ impl fmt::Display for LanguageServerId {
 
 impl Drop for Subscription {
     fn drop(&mut self) {
-        self.notification_handlers.lock().remove(self.method);
+        match self {
+            Subscription::Notification {
+                method,
+                notification_handlers,
+            } => {
+                if let Some(handlers) = notification_handlers {
+                    handlers.lock().remove(method);
+                }
+            }
+            Subscription::Io { id, io_handlers } => {
+                if let Some(io_handlers) = io_handlers.as_ref().and_then(|h| h.upgrade()) {
+                    io_handlers.lock().remove(id);
+                }
+            }
+        }
     }
 }
 

crates/lsp_log/Cargo.toml 🔗

@@ -0,0 +1,29 @@
+[package]
+name = "lsp_log"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/lsp_log.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { path = "../editor" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+language = { path = "../language" }
+project = { path = "../project" }
+workspace = { path = "../workspace" }
+gpui = { path = "../gpui" }
+util = { path = "../util" }
+lsp = { path = "../lsp" }
+futures = { workspace = true }
+serde = { workspace = true }
+anyhow = "1.0"
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+unindent = "0.1.7"

crates/lsp_log/src/lsp_log.rs 🔗

@@ -0,0 +1,523 @@
+use collections::{hash_map, HashMap};
+use editor::Editor;
+use futures::{channel::mpsc, StreamExt};
+use gpui::{
+    actions,
+    elements::{
+        AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
+        ParentElement, Stack,
+    },
+    platform::{CursorStyle, MouseButton},
+    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
+    ViewHandle, WeakModelHandle,
+};
+use language::{Buffer, LanguageServerId, LanguageServerName};
+use project::{Project, WorktreeId};
+use settings::Settings;
+use std::{borrow::Cow, sync::Arc};
+use theme::{ui, Theme};
+use workspace::{
+    item::{Item, ItemHandle},
+    ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+const SEND_LINE: &str = "// Send:\n";
+const RECEIVE_LINE: &str = "// Receive:\n";
+
+struct LogStore {
+    projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
+    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
+}
+
+struct LogStoreProject {
+    servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
+    _subscription: gpui::Subscription,
+}
+
+struct LogStoreLanguageServer {
+    buffer: ModelHandle<Buffer>,
+    last_message_kind: Option<MessageKind>,
+    _subscription: lsp::Subscription,
+}
+
+pub struct LspLogView {
+    log_store: ModelHandle<LogStore>,
+    current_server_id: Option<LanguageServerId>,
+    editor: Option<ViewHandle<Editor>>,
+    project: ModelHandle<Project>,
+}
+
+pub struct LspLogToolbarItemView {
+    log_view: Option<ViewHandle<LspLogView>>,
+    menu_open: bool,
+    project: ModelHandle<Project>,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+enum MessageKind {
+    Send,
+    Receive,
+}
+
+actions!(log, [OpenLanguageServerLogs]);
+
+pub fn init(cx: &mut AppContext) {
+    let log_set = cx.add_model(|cx| LogStore::new(cx));
+
+    cx.add_action(
+        move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
+            let project = workspace.project().read(cx);
+            if project.is_local() {
+                workspace.add_item(
+                    Box::new(cx.add_view(|cx| {
+                        LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
+                    })),
+                    cx,
+                );
+            }
+        },
+    );
+}
+
+impl LogStore {
+    fn new(cx: &mut ModelContext<Self>) -> Self {
+        let (io_tx, mut io_rx) = mpsc::unbounded();
+        let this = Self {
+            projects: HashMap::default(),
+            io_tx,
+        };
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        message.push('\n');
+                        this.on_io(project, server_id, is_output, &message, cx);
+                    });
+                }
+            }
+            anyhow::Ok(())
+        })
+        .detach();
+        this
+    }
+
+    pub fn has_enabled_logs_for_language_server(
+        &self,
+        project: &ModelHandle<Project>,
+        server_id: LanguageServerId,
+    ) -> bool {
+        self.projects
+            .get(&project.downgrade())
+            .map_or(false, |store| store.servers.contains_key(&server_id))
+    }
+
+    pub fn enable_logs_for_language_server(
+        &mut self,
+        project: &ModelHandle<Project>,
+        server_id: LanguageServerId,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<ModelHandle<Buffer>> {
+        let server = project.read(cx).language_server_for_id(server_id)?;
+        let weak_project = project.downgrade();
+        let project_logs = match self.projects.entry(weak_project) {
+            hash_map::Entry::Occupied(entry) => entry.into_mut(),
+            hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject {
+                servers: HashMap::default(),
+                _subscription: cx.observe_release(&project, move |this, _, _| {
+                    this.projects.remove(&weak_project);
+                }),
+            }),
+        };
+        let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
+            let io_tx = self.io_tx.clone();
+            let language = project.read(cx).languages().language_for_name("JSON");
+            let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
+            cx.spawn_weak({
+                let buffer = buffer.clone();
+                |_, mut cx| async move {
+                    let language = language.await.ok();
+                    buffer.update(&mut cx, |buffer, cx| {
+                        buffer.set_language(language, cx);
+                    });
+                }
+            })
+            .detach();
+
+            let project = project.downgrade();
+            LogStoreLanguageServer {
+                buffer,
+                last_message_kind: None,
+                _subscription: server.on_io(move |is_received, json| {
+                    io_tx
+                        .unbounded_send((project, server_id, is_received, json.to_string()))
+                        .ok();
+                }),
+            }
+        });
+        Some(server_log_state.buffer.clone())
+    }
+
+    pub fn disable_logs_for_language_server(
+        &mut self,
+        project: &ModelHandle<Project>,
+        server_id: LanguageServerId,
+        _: &mut ModelContext<Self>,
+    ) {
+        let project = project.downgrade();
+        if let Some(store) = self.projects.get_mut(&project) {
+            store.servers.remove(&server_id);
+            if store.servers.is_empty() {
+                self.projects.remove(&project);
+            }
+        }
+    }
+
+    fn on_io(
+        &mut self,
+        project: WeakModelHandle<Project>,
+        language_server_id: LanguageServerId,
+        is_received: bool,
+        message: &str,
+        cx: &mut AppContext,
+    ) -> Option<()> {
+        let state = self
+            .projects
+            .get_mut(&project)?
+            .servers
+            .get_mut(&language_server_id)?;
+        state.buffer.update(cx, |buffer, cx| {
+            let kind = if is_received {
+                MessageKind::Receive
+            } else {
+                MessageKind::Send
+            };
+            if state.last_message_kind != Some(kind) {
+                let len = buffer.len();
+                let line = match kind {
+                    MessageKind::Send => SEND_LINE,
+                    MessageKind::Receive => RECEIVE_LINE,
+                };
+                buffer.edit([(len..len, line)], None, cx);
+                state.last_message_kind = Some(kind);
+            }
+            let len = buffer.len();
+            buffer.edit([(len..len, message)], None, cx);
+        });
+        Some(())
+    }
+}
+
+impl LspLogView {
+    fn new(
+        project: ModelHandle<Project>,
+        log_set: ModelHandle<LogStore>,
+        _: &mut ViewContext<Self>,
+    ) -> Self {
+        Self {
+            project,
+            log_store: log_set,
+            editor: None,
+            current_server_id: None,
+        }
+    }
+
+    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
+        let buffer = self.log_store.update(cx, |log_set, cx| {
+            log_set.enable_logs_for_language_server(&self.project, server_id, cx)
+        });
+        if let Some(buffer) = buffer {
+            self.current_server_id = Some(server_id);
+            self.editor = Some(cx.add_view(|cx| {
+                let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
+                editor.set_read_only(true);
+                editor.move_to_end(&Default::default(), cx);
+                editor
+            }));
+            cx.notify();
+        }
+    }
+
+    fn toggle_logging_for_server(
+        &mut self,
+        server_id: LanguageServerId,
+        enabled: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.log_store.update(cx, |log_store, cx| {
+            if enabled {
+                log_store.enable_logs_for_language_server(&self.project, server_id, cx);
+            } else {
+                log_store.disable_logs_for_language_server(&self.project, server_id, cx);
+            }
+        });
+    }
+}
+
+impl View for LspLogView {
+    fn ui_name() -> &'static str {
+        "LspLogView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        if let Some(editor) = &self.editor {
+            ChildView::new(&editor, cx).into_any()
+        } else {
+            Empty::new().into_any()
+        }
+    }
+}
+
+impl Item for LspLogView {
+    fn tab_content<V: View>(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        _: &AppContext,
+    ) -> AnyElement<V> {
+        Label::new("LSP Logs", style.label.clone()).into_any()
+    }
+}
+
+impl ToolbarItemView for LspLogToolbarItemView {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        _: &mut ViewContext<Self>,
+    ) -> workspace::ToolbarItemLocation {
+        self.menu_open = false;
+        if let Some(item) = active_pane_item {
+            if let Some(log_view) = item.downcast::<LspLogView>() {
+                self.log_view = Some(log_view.clone());
+                return ToolbarItemLocation::PrimaryLeft {
+                    flex: Some((1., false)),
+                };
+            }
+        }
+        self.log_view = None;
+        ToolbarItemLocation::Hidden
+    }
+}
+
+impl View for LspLogToolbarItemView {
+    fn ui_name() -> &'static str {
+        "LspLogView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = cx.global::<Settings>().theme.clone();
+        let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
+        let project = self.project.read(cx);
+        let log_view = log_view.read(cx);
+        let log_store = log_view.log_store.read(cx);
+
+        let mut language_servers = project
+            .language_servers()
+            .map(|(id, name, worktree)| {
+                (
+                    id,
+                    name,
+                    worktree,
+                    log_store.has_enabled_logs_for_language_server(&self.project, id),
+                )
+            })
+            .collect::<Vec<_>>();
+        language_servers.sort_by_key(|a| (a.0, a.2));
+        language_servers.dedup_by_key(|a| a.0);
+
+        let current_server_id = log_view.current_server_id;
+        let current_server = current_server_id.and_then(|current_server_id| {
+            if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
+                Some(language_servers[ix].clone())
+            } else {
+                None
+            }
+        });
+
+        enum Menu {}
+
+        Stack::new()
+            .with_child(Self::render_language_server_menu_header(
+                current_server,
+                &self.project,
+                &theme,
+                cx,
+            ))
+            .with_children(if self.menu_open {
+                Some(
+                    Overlay::new(
+                        MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
+                            Flex::column()
+                                .with_children(language_servers.into_iter().filter_map(
+                                    |(id, name, worktree_id, logging_enabled)| {
+                                        Self::render_language_server_menu_item(
+                                            id,
+                                            name,
+                                            worktree_id,
+                                            logging_enabled,
+                                            Some(id) == current_server_id,
+                                            &self.project,
+                                            &theme,
+                                            cx,
+                                        )
+                                    },
+                                ))
+                                .contained()
+                                .with_style(theme.context_menu.container)
+                                .constrained()
+                                .with_width(400.)
+                                .with_height(400.)
+                        })
+                        .on_down_out(MouseButton::Left, |_, this, cx| {
+                            this.menu_open = false;
+                            cx.notify()
+                        }),
+                    )
+                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                    .with_anchor_corner(AnchorCorner::TopLeft)
+                    .with_z_index(999)
+                    .aligned()
+                    .bottom()
+                    .left(),
+                )
+            } else {
+                None
+            })
+            .aligned()
+            .left()
+            .clipped()
+            .into_any()
+    }
+}
+
+impl LspLogToolbarItemView {
+    pub fn new(project: ModelHandle<Project>) -> Self {
+        Self {
+            menu_open: false,
+            log_view: None,
+            project,
+        }
+    }
+
+    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
+        self.menu_open = !self.menu_open;
+        cx.notify();
+    }
+
+    fn toggle_logging_for_server(
+        &mut self,
+        id: LanguageServerId,
+        enabled: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(log_view) = &self.log_view {
+            log_view.update(cx, |log_view, cx| {
+                log_view.toggle_logging_for_server(id, enabled, cx);
+                if !enabled && Some(id) == log_view.current_server_id {
+                    log_view.current_server_id = None;
+                    log_view.editor = None;
+                    cx.notify();
+                }
+            });
+        }
+        cx.notify();
+    }
+
+    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
+        if let Some(log_view) = &self.log_view {
+            log_view.update(cx, |log_view, cx| {
+                log_view.show_logs_for_server(id, cx);
+            });
+            self.menu_open = false;
+        }
+        cx.notify();
+    }
+
+    fn render_language_server_menu_header(
+        current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
+        project: &ModelHandle<Project>,
+        theme: &Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element<Self> {
+        enum ToggleMenu {}
+        MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
+            let project = project.read(cx);
+            let label: Cow<str> = current_server
+                .and_then(|(_, server_name, worktree_id, _)| {
+                    let worktree = project.worktree_for_id(worktree_id, cx)?;
+                    let worktree = &worktree.read(cx);
+                    Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
+                })
+                .unwrap_or_else(|| "No server selected".into());
+            Label::new(
+                label,
+                theme
+                    .context_menu
+                    .item
+                    .style_for(state, false)
+                    .label
+                    .clone(),
+            )
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, view, cx| {
+            view.toggle_menu(cx);
+        })
+    }
+
+    fn render_language_server_menu_item(
+        id: LanguageServerId,
+        name: LanguageServerName,
+        worktree_id: WorktreeId,
+        logging_enabled: bool,
+        is_selected: bool,
+        project: &ModelHandle<Project>,
+        theme: &Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<impl Element<Self>> {
+        enum ActivateLog {}
+        let project = project.read(cx);
+        let worktree = project.worktree_for_id(worktree_id, cx)?;
+        let worktree = &worktree.read(cx);
+        if !worktree.is_visible() {
+            return None;
+        }
+        let label = format!("{} - ({})", name.0, worktree.root_name());
+
+        Some(
+            MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
+                let item_style = theme.context_menu.item.style_for(state, is_selected);
+                Flex::row()
+                    .with_child(ui::checkbox_with_label::<Self, _, Self, _>(
+                        Empty::new(),
+                        &theme.welcome.checkbox,
+                        logging_enabled,
+                        id.0,
+                        cx,
+                        move |this, enabled, cx| {
+                            this.toggle_logging_for_server(id, enabled, cx);
+                        },
+                    ))
+                    .with_child(Label::new(label, item_style.label.clone()).aligned().left())
+                    .align_children_center()
+                    .contained()
+                    .with_style(item_style.container)
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, view, cx| {
+                view.show_logs_for_server(id, cx);
+            }),
+        )
+    }
+}
+
+impl Entity for LogStore {
+    type Event = ();
+}
+
+impl Entity for LspLogView {
+    type Event = ();
+}
+
+impl Entity for LspLogToolbarItemView {
+    type Event = ();
+}

crates/node_runtime/Cargo.toml 🔗

@@ -13,7 +13,7 @@ gpui = { path = "../gpui" }
 util = { path = "../util" }
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-tar = "0.4.2"
-futures = "0.3"
+futures = { workspace = true }
 anyhow = "1.0.38"
 parking_lot = "0.11.1"
 serde = { workspace = true }

crates/project/Cargo.toml 🔗

@@ -41,7 +41,7 @@ aho-corasick = "0.7"
 anyhow = "1.0.57"
 async-trait = "0.1"
 backtrace = "0.3"
-futures = "0.3"
+futures = { workspace = true }
 ignore = "0.4"
 lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/project/src/project.rs 🔗

@@ -185,6 +185,8 @@ pub struct Collaborator {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
+    LanguageServerAdded(LanguageServerId),
+    LanguageServerRemoved(LanguageServerId),
     ActiveEntryChanged(Option<ProjectEntryId>),
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
@@ -1869,7 +1871,7 @@ impl Project {
                 let next_snapshot = buffer.text_snapshot();
 
                 let language_servers: Vec<_> = self
-                    .language_servers_iter_for_buffer(buffer, cx)
+                    .language_servers_for_buffer(buffer, cx)
                     .map(|i| i.1.clone())
                     .collect();
 
@@ -6279,7 +6281,25 @@ impl Project {
         }
     }
 
-    pub fn language_servers_iter_for_buffer(
+    pub fn language_servers(
+        &self,
+    ) -> impl '_ + Iterator<Item = (LanguageServerId, LanguageServerName, WorktreeId)> {
+        self.language_server_ids
+            .iter()
+            .map(|((worktree_id, server_name), server_id)| {
+                (*server_id, server_name.clone(), *worktree_id)
+            })
+    }
+
+    pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
+        if let LanguageServerState::Running { server, .. } = self.language_servers.get(&id)? {
+            Some(server.clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn language_servers_for_buffer(
         &self,
         buffer: &Buffer,
         cx: &AppContext,
@@ -6299,20 +6319,12 @@ impl Project {
             })
     }
 
-    fn language_servers_for_buffer(
-        &self,
-        buffer: &Buffer,
-        cx: &AppContext,
-    ) -> Vec<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
-        self.language_servers_iter_for_buffer(buffer, cx).collect()
-    }
-
     fn primary_language_servers_for_buffer(
         &self,
         buffer: &Buffer,
         cx: &AppContext,
     ) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
-        self.language_servers_iter_for_buffer(buffer, cx).next()
+        self.language_servers_for_buffer(buffer, cx).next()
     }
 
     fn language_server_for_buffer(
@@ -6321,7 +6333,7 @@ impl Project {
         server_id: LanguageServerId,
         cx: &AppContext,
     ) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
-        self.language_servers_iter_for_buffer(buffer, cx)
+        self.language_servers_for_buffer(buffer, cx)
             .find(|(_, s)| s.server_id() == server_id)
     }
 

crates/project_panel/Cargo.toml 🔗

@@ -20,7 +20,7 @@ theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 postage = { workspace = true }
-futures = "0.3"
+futures = { workspace = true }
 unicase = "2.6"
 
 [dev-dependencies]

crates/project_symbols/Cargo.toml 🔗

@@ -24,7 +24,7 @@ postage = { workspace = true }
 smol = "1.2"
 
 [dev-dependencies]
-futures = "0.3"
+futures = { workspace = true }
 settings = { path = "../settings", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }

crates/rpc/Cargo.toml 🔗

@@ -21,7 +21,7 @@ anyhow = "1.0"
 async-lock = "2.4"
 async-tungstenite = "0.16"
 base64 = "0.13"
-futures = "0.3"
+futures = { workspace = true }
 parking_lot = "0.11.1"
 prost = "0.8"
 rand = "0.8"

crates/search/Cargo.toml 🔗

@@ -20,7 +20,7 @@ theme = { path = "../theme" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 anyhow = "1.0"
-futures = "0.3"
+futures = { workspace = true }
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 postage = { workspace = true }
 serde = { workspace = true }

crates/settings/Cargo.toml 🔗

@@ -18,7 +18,7 @@ gpui = { path = "../gpui" }
 sqlez = { path = "../sqlez" }
 fs = { path = "../fs" }
 anyhow = "1.0.38"
-futures = "0.3"
+futures = { workspace = true }
 theme = { path = "../theme" }
 staff_mode = { path = "../staff_mode" }
 util = { path = "../util" }

crates/sqlez/Cargo.toml 🔗

@@ -14,5 +14,5 @@ smol = "1.2"
 thread_local = "1.1.4"
 lazy_static = "1.4"
 parking_lot = "0.11.1"
-futures = "0.3"
-uuid = { version = "1.1.2", features = ["v4"] }
+futures = { workspace = true }
+uuid = { version = "1.1.2", features = ["v4"] }

crates/terminal/Cargo.toml 🔗

@@ -20,7 +20,7 @@ procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f
 smallvec = { workspace = true }
 smol = "1.2.5"
 mio-extras = "2.0.6"
-futures = "0.3"
+futures = { workspace = true }
 ordered-float = "2.1.1"
 itertools = "0.10"
 dirs = "4.0.0"

crates/terminal_view/Cargo.toml 🔗

@@ -24,7 +24,7 @@ terminal = { path = "../terminal" }
 smallvec = { workspace = true }
 smol = "1.2.5"
 mio-extras = "2.0.6"
-futures = "0.3"
+futures = { workspace = true }
 ordered-float = "2.1.1"
 itertools = "0.10"
 dirs = "4.0.0"

crates/theme/src/ui.rs 🔗

@@ -27,28 +27,40 @@ pub struct CheckboxStyle {
     pub hovered_and_checked: ContainerStyle,
 }
 
-pub fn checkbox<Tag: 'static, V: View>(
+pub fn checkbox<Tag, V, F>(
     label: &'static str,
     style: &CheckboxStyle,
     checked: bool,
+    id: usize,
     cx: &mut ViewContext<V>,
-    change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
-) -> MouseEventHandler<Tag, V> {
+    change: F,
+) -> MouseEventHandler<Tag, V>
+where
+    Tag: 'static,
+    V: View,
+    F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
+{
     let label = Label::new(label, style.label.text.clone())
         .contained()
         .with_style(style.label.container);
-
-    checkbox_with_label(label, style, checked, cx, change)
+    checkbox_with_label(label, style, checked, id, cx, change)
 }
 
-pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>(
+pub fn checkbox_with_label<Tag, D, V, F>(
     label: D,
     style: &CheckboxStyle,
     checked: bool,
+    id: usize,
     cx: &mut ViewContext<V>,
-    change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
-) -> MouseEventHandler<Tag, V> {
-    MouseEventHandler::new(0, cx, |state, _| {
+    change: F,
+) -> MouseEventHandler<Tag, V>
+where
+    Tag: 'static,
+    D: Element<V>,
+    V: View,
+    F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
+{
+    MouseEventHandler::new(id, cx, |state, _| {
         let indicator = if checked {
             svg(&style.icon)
         } else {
@@ -75,8 +87,8 @@ pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>(
             .with_child(label)
             .align_children_center()
     })
-    .on_click(platform::MouseButton::Left, move |_, _, cx| {
-        change(!checked, cx)
+    .on_click(platform::MouseButton::Left, move |_, view, cx| {
+        change(view, !checked, cx)
     })
     .with_cursor_style(platform::CursorStyle::PointingHand)
 }

crates/util/Cargo.toml 🔗

@@ -16,7 +16,7 @@ anyhow = "1.0.38"
 backtrace = "0.3"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 lazy_static = "1.4.0"
-futures = "0.3"
+futures = { workspace = true }
 isahc = "1.7"
 smol = "1.2.5"
 url = "2.2"

crates/welcome/src/welcome.rs 🔗

@@ -126,7 +126,7 @@ impl View for WelcomePage {
                 .with_child(
                     Flex::column()
                         .with_child(
-                            theme::ui::checkbox_with_label::<Metrics, _, Self>(
+                            theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
                                 Flex::column()
                                     .with_child(
                                         Label::new(
@@ -146,8 +146,9 @@ impl View for WelcomePage {
                                     ),
                                 &theme.welcome.checkbox,
                                 metrics,
+                                0,
                                 cx,
-                                |checked, cx| {
+                                |_, checked, cx| {
                                     SettingsFile::update(cx, move |file| {
                                         file.telemetry.set_metrics(checked)
                                     })
@@ -157,12 +158,13 @@ impl View for WelcomePage {
                             .with_style(theme.welcome.checkbox_container),
                         )
                         .with_child(
-                            theme::ui::checkbox::<Diagnostics, Self>(
+                            theme::ui::checkbox::<Diagnostics, Self, _>(
                                 "Send crash reports",
                                 &theme.welcome.checkbox,
                                 diagnostics,
+                                0,
                                 cx,
-                                |checked, cx| {
+                                |_, checked, cx| {
                                     SettingsFile::update(cx, move |file| {
                                         file.telemetry.set_diagnostics(checked)
                                     })

crates/workspace/Cargo.toml 🔗

@@ -38,7 +38,7 @@ util = { path = "../util" }
 async-recursion = "1.0.0"
 bincode = "1.2.1"
 anyhow = "1.0.38"
-futures = "0.3"
+futures = { workspace = true }
 lazy_static = "1.4"
 env_logger = "0.9.1"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }

crates/zed/Cargo.toml 🔗

@@ -46,6 +46,7 @@ journal = { path = "../journal" }
 language = { path = "../language" }
 language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
+lsp_log = { path = "../lsp_log" }
 node_runtime = { path = "../node_runtime" }
 outline = { path = "../outline" }
 plugin_runtime = { path = "../plugin_runtime" }
@@ -76,7 +77,7 @@ chrono = "0.4"
 ctor = "0.1.20"
 easy-parallel = "3.1.0"
 env_logger = "0.9"
-futures = "0.3"
+futures = { workspace = true }
 ignore = "0.4"
 image = "0.23"
 indexmap = "1.6.2"
@@ -109,7 +110,7 @@ tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev
 tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
 tree-sitter-embedded-template = "0.20.0"
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
-tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
+tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
 tree-sitter-rust = "0.20.3"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 tree-sitter-python = "0.20.2"

crates/zed/src/zed.rs 🔗

@@ -262,6 +262,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
     );
     activity_indicator::init(cx);
     copilot_button::init(cx);
+    lsp_log::init(cx);
     call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
     settings::KeymapFileContent::load_defaults(cx);
 }
@@ -273,7 +274,7 @@ pub fn initialize_workspace(
 ) {
     let workspace_handle = cx.handle();
     cx.subscribe(&workspace_handle, {
-        move |_, _, event, cx| {
+        move |workspace, _, event, cx| {
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, cx| {
                     pane.toolbar().update(cx, |toolbar, cx| {
@@ -287,6 +288,10 @@ pub fn initialize_workspace(
                         toolbar.add_item(submit_feedback_button, cx);
                         let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
                         toolbar.add_item(feedback_info_text, cx);
+                        let lsp_log_item = cx.add_view(|_| {
+                            lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
+                        });
+                        toolbar.add_item(lsp_log_item, cx);
                     })
                 });
             }