Merge branch 'main' into settings-file

Max Brunsfeld created

Change summary

Cargo.lock                                |   1 
crates/client/src/client.rs               |   3 
crates/editor/src/display_map.rs          |   8 
crates/editor/src/display_map/fold_map.rs |  50 +-
crates/editor/src/display_map/tab_map.rs  |  10 
crates/editor/src/editor.rs               |  56 ++
crates/editor/src/multi_buffer.rs         |   2 
crates/gpui/src/elements/event_handler.rs |  42 ++
crates/gpui/src/gpui.rs                   |   2 
crates/gpui/src/platform.rs               |   2 
crates/gpui/src/platform/event.rs         |  30 +
crates/gpui/src/platform/mac/event.rs     |  64 +++
crates/gpui/src/platform/mac/window.rs    |  16 
crates/language/src/buffer.rs             |  34 +
crates/lsp/src/lsp.rs                     |  22 
crates/project/src/project.rs             | 456 +++++++++++++++++++-----
crates/rpc/proto/zed.proto                |  43 ++
crates/rpc/src/peer.rs                    |  10 
crates/rpc/src/proto.rs                   |   8 
crates/server/src/rpc.rs                  |  27 
crates/server/src/rpc/store.rs            |  20 +
crates/workspace/Cargo.toml               |   1 
crates/workspace/src/lsp_status.rs        |  63 +++
crates/workspace/src/pane.rs              |  74 +++
crates/zed/src/zed.rs                     |  41 +
25 files changed, 858 insertions(+), 227 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5853,6 +5853,7 @@ dependencies = [
  "project",
  "serde",
  "serde_json",
+ "smallvec",
  "theme",
  "util",
 ]

crates/client/src/client.rs 🔗

@@ -631,6 +631,9 @@ impl Client {
                         } else {
                             log::info!("unhandled message {}", type_name);
                         }
+
+                        // Don't starve the main thread when receiving lots of messages at once.
+                        smol::future::yield_now().await;
                     }
                 }
             })

crates/editor/src/display_map.rs 🔗

@@ -6,7 +6,7 @@ mod wrap_map;
 use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
-use fold_map::{FoldMap, ToFoldPoint as _};
+use fold_map::FoldMap;
 use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
 use language::{Point, Subscription as BufferSubscription};
 use std::ops::Range;
@@ -200,7 +200,7 @@ impl DisplaySnapshot {
 
     pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Left);
+            let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
             *fold_point.column_mut() = 0;
             point = fold_point.to_buffer_point(&self.folds_snapshot);
 
@@ -216,7 +216,7 @@ impl DisplaySnapshot {
 
     pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Right);
+            let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
             *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
             point = fold_point.to_buffer_point(&self.folds_snapshot);
 
@@ -231,7 +231,7 @@ impl DisplaySnapshot {
     }
 
     fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
-        let fold_point = point.to_fold_point(&self.folds_snapshot, bias);
+        let fold_point = self.folds_snapshot.to_fold_point(point, bias);
         let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
         let wrap_point = self.wraps_snapshot.from_tab_point(tab_point);
         let block_point = self.blocks_snapshot.to_block_point(wrap_point);

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

@@ -12,10 +12,6 @@ use std::{
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 
-pub trait ToFoldPoint {
-    fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint;
-}
-
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct FoldPoint(pub super::Point);
 
@@ -75,26 +71,6 @@ impl FoldPoint {
     }
 }
 
-impl ToFoldPoint for Point {
-    fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint {
-        let mut cursor = snapshot.transforms.cursor::<(Point, FoldPoint)>();
-        cursor.seek(self, Bias::Right, &());
-        if cursor.item().map_or(false, |t| t.is_fold()) {
-            if bias == Bias::Left || *self == cursor.start().0 {
-                cursor.start().1
-            } else {
-                cursor.end(&()).1
-            }
-        } else {
-            let overshoot = *self - cursor.start().0;
-            FoldPoint(cmp::min(
-                cursor.start().1 .0 + overshoot,
-                cursor.end(&()).1 .0,
-            ))
-        }
-    }
-}
-
 pub struct FoldMapWriter<'a>(&'a mut FoldMap);
 
 impl<'a> FoldMapWriter<'a> {
@@ -554,6 +530,24 @@ impl FoldSnapshot {
         summary
     }
 
+    pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
+        let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
+        cursor.seek(&point, Bias::Right, &());
+        if cursor.item().map_or(false, |t| t.is_fold()) {
+            if bias == Bias::Left || point == cursor.start().0 {
+                cursor.start().1
+            } else {
+                cursor.end(&()).1
+            }
+        } else {
+            let overshoot = point - cursor.start().0;
+            FoldPoint(cmp::min(
+                cursor.start().1 .0 + overshoot,
+                cursor.end(&()).1 .0,
+            ))
+        }
+    }
+
     pub fn len(&self) -> FoldOffset {
         FoldOffset(self.transforms.summary().output.bytes)
     }
@@ -1356,7 +1350,7 @@ mod tests {
                 let buffer_point = fold_point.to_buffer_point(&snapshot);
                 let buffer_offset = buffer_point.to_offset(&buffer_snapshot);
                 assert_eq!(
-                    buffer_point.to_fold_point(&snapshot, Right),
+                    snapshot.to_fold_point(buffer_point, Right),
                     fold_point,
                     "{:?} -> fold point",
                     buffer_point,
@@ -1428,10 +1422,8 @@ mod tests {
             }
 
             for fold_range in map.merged_fold_ranges() {
-                let fold_point = fold_range
-                    .start
-                    .to_point(&buffer_snapshot)
-                    .to_fold_point(&snapshot, Right);
+                let fold_point =
+                    snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right);
                 assert!(snapshot.is_line_folded(fold_point.row()));
             }
 

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

@@ -1,4 +1,4 @@
-use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint};
+use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot};
 use crate::MultiBufferSnapshot;
 use language::{rope, Chunk};
 use parking_lot::Mutex;
@@ -201,10 +201,6 @@ impl TabSnapshot {
         TabPoint::new(input.row(), expanded as u32)
     }
 
-    pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
-        self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias))
-    }
-
     pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
         let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
         let expanded = output.column() as usize;
@@ -217,6 +213,10 @@ impl TabSnapshot {
         )
     }
 
+    pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
+        self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias))
+    }
+
     pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
         self.to_fold_point(point, bias)
             .0

crates/editor/src/editor.rs 🔗

@@ -2638,11 +2638,26 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         for selection in &mut selections {
             if selection.is_empty() {
-                let head = selection.head().to_display_point(&display_map);
-                let cursor = movement::left(&display_map, head)
-                    .unwrap()
-                    .to_point(&display_map);
-                selection.set_head(cursor);
+                let old_head = selection.head();
+                let mut new_head =
+                    movement::left(&display_map, old_head.to_display_point(&display_map))
+                        .unwrap()
+                        .to_point(&display_map);
+                if let Some((buffer, line_buffer_range)) = display_map
+                    .buffer_snapshot
+                    .buffer_line_for_row(old_head.row)
+                {
+                    let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
+                    if old_head.column <= indent_column && old_head.column > 0 {
+                        let indent = buffer.indent_size();
+                        new_head = cmp::min(
+                            new_head,
+                            Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
+                        );
+                    }
+                }
+
+                selection.set_head(new_head);
                 selection.goal = SelectionGoal::None;
             }
         }
@@ -7153,14 +7168,13 @@ mod tests {
 
     #[gpui::test]
     fn test_backspace(cx: &mut gpui::MutableAppContext) {
-        let buffer =
-            MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
         let settings = Settings::test(&cx);
         let (_, view) = cx.add_window(Default::default(), |cx| {
-            build_editor(buffer.clone(), settings, cx)
+            build_editor(MultiBuffer::build_simple("", cx), settings, cx)
         });
 
         view.update(cx, |view, cx| {
+            view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx);
             view.select_display_ranges(
                 &[
                     // an empty selection - the preceding character is deleted
@@ -7173,12 +7187,28 @@ mod tests {
                 cx,
             );
             view.backspace(&Backspace, cx);
-        });
+            assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n");
 
-        assert_eq!(
-            buffer.read(cx).read(cx).text(),
-            "oe two three\nfou five six\nseven ten\n"
-        );
+            view.set_text("    one\n        two\n        three\n   four", cx);
+            view.select_display_ranges(
+                &[
+                    // cursors at the the end of leading indent - last indent is deleted
+                    DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4),
+                    DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8),
+                    // cursors inside leading indent - overlapping indent deletions are coalesced
+                    DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+                    DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                    DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6),
+                    // cursor at the beginning of a line - preceding newline is deleted
+                    DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+                    // selection inside leading indent - only the selected character is deleted
+                    DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3),
+                ],
+                cx,
+            );
+            view.backspace(&Backspace, cx);
+            assert_eq!(view.text(cx), "one\n    two\n  three  four");
+        });
     }
 
     #[gpui::test]

crates/editor/src/multi_buffer.rs 🔗

@@ -1657,7 +1657,7 @@ impl MultiBufferSnapshot {
         }
     }
 
-    fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
+    pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
         let mut cursor = self.excerpts.cursor::<Point>();
         cursor.seek(&Point::new(row, 0), Bias::Right, &());
         if let Some(excerpt) = cursor.item() {

crates/gpui/src/elements/event_handler.rs 🔗

@@ -3,13 +3,15 @@ use serde_json::json;
 
 use crate::{
     geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
-    LayoutContext, PaintContext, SizeConstraint,
+    LayoutContext, NavigationDirection, PaintContext, SizeConstraint,
 };
 
 pub struct EventHandler {
     child: ElementBox,
     capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>,
     mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
+    right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
+    navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
 }
 
 impl EventHandler {
@@ -18,6 +20,8 @@ impl EventHandler {
             child,
             capture: None,
             mouse_down: None,
+            right_mouse_down: None,
+            navigate_mouse_down: None,
         }
     }
 
@@ -29,6 +33,22 @@ impl EventHandler {
         self
     }
 
+    pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
+    where
+        F: 'static + FnMut(&mut EventContext) -> bool,
+    {
+        self.right_mouse_down = Some(Box::new(callback));
+        self
+    }
+
+    pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
+    where
+        F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
+    {
+        self.navigate_mouse_down = Some(Box::new(callback));
+        self
+    }
+
     pub fn capture<F>(mut self, callback: F) -> Self
     where
         F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool,
@@ -87,6 +107,26 @@ impl Element for EventHandler {
                     }
                     false
                 }
+                Event::RightMouseDown { position, .. } => {
+                    if let Some(callback) = self.right_mouse_down.as_mut() {
+                        if bounds.contains_point(*position) {
+                            return callback(cx);
+                        }
+                    }
+                    false
+                }
+                Event::NavigateMouseDown {
+                    position,
+                    direction,
+                    ..
+                } => {
+                    if let Some(callback) = self.navigate_mouse_down.as_mut() {
+                        if bounds.contains_point(*position) {
+                            return callback(*direction, cx);
+                        }
+                    }
+                    false
+                }
                 _ => false,
             }
         }

crates/gpui/src/gpui.rs 🔗

@@ -29,7 +29,7 @@ pub mod keymap;
 pub mod platform;
 pub use gpui_macros::test;
 pub use platform::FontSystem;
-pub use platform::{Event, PathPromptOptions, Platform, PromptLevel};
+pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel};
 pub use presenter::{
     Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };

crates/gpui/src/platform.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
 };
 use anyhow::Result;
 use async_task::Runnable;
-pub use event::Event;
+pub use event::{Event, NavigationDirection};
 use postage::oneshot;
 use std::{
     any::Any,

crates/gpui/src/platform/event.rs 🔗

@@ -1,5 +1,11 @@
 use crate::{geometry::vector::Vector2F, keymap::Keystroke};
 
+#[derive(Copy, Clone, Debug)]
+pub enum NavigationDirection {
+    Back,
+    Forward,
+}
+
 #[derive(Clone, Debug)]
 pub enum Event {
     KeyDown {
@@ -26,6 +32,30 @@ pub enum Event {
     LeftMouseDragged {
         position: Vector2F,
     },
+    RightMouseDown {
+        position: Vector2F,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
+        click_count: usize,
+    },
+    RightMouseUp {
+        position: Vector2F,
+    },
+    NavigateMouseDown {
+        position: Vector2F,
+        direction: NavigationDirection,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
+        click_count: usize,
+    },
+    NavigateMouseUp {
+        position: Vector2F,
+        direction: NavigationDirection,
+    },
     MouseMoved {
         position: Vector2F,
         left_mouse_down: bool,

crates/gpui/src/platform/mac/event.rs 🔗

@@ -1,4 +1,8 @@
-use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
+use crate::{
+    geometry::vector::vec2f,
+    keymap::Keystroke,
+    platform::{Event, NavigationDirection},
+};
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventType},
     base::{id, nil, YES},
@@ -125,6 +129,64 @@ impl Event {
                     window_height - native_event.locationInWindow().y as f32,
                 ),
             }),
+            NSEventType::NSRightMouseDown => {
+                let modifiers = native_event.modifierFlags();
+                window_height.map(|window_height| Self::RightMouseDown {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                    click_count: native_event.clickCount() as usize,
+                })
+            }
+            NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp {
+                position: vec2f(
+                    native_event.locationInWindow().x as f32,
+                    window_height - native_event.locationInWindow().y as f32,
+                ),
+            }),
+            NSEventType::NSOtherMouseDown => {
+                let direction = match native_event.buttonNumber() {
+                    3 => NavigationDirection::Back,
+                    4 => NavigationDirection::Forward,
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                let modifiers = native_event.modifierFlags();
+                window_height.map(|window_height| Self::NavigateMouseDown {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    direction,
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                    click_count: native_event.clickCount() as usize,
+                })
+            }
+            NSEventType::NSOtherMouseUp => {
+                let direction = match native_event.buttonNumber() {
+                    3 => NavigationDirection::Back,
+                    4 => NavigationDirection::Forward,
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                window_height.map(|window_height| Self::NavigateMouseUp {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    direction,
+                })
+            }
             NSEventType::NSLeftMouseDragged => {
                 window_height.map(|window_height| Self::LeftMouseDragged {
                     position: vec2f(

crates/gpui/src/platform/mac/window.rs 🔗

@@ -95,6 +95,22 @@ unsafe fn build_classes() {
             sel!(mouseUp:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(rightMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(mouseMoved:),
             handle_view_event as extern "C" fn(&Object, Sel, id),

crates/language/src/buffer.rs 🔗

@@ -47,9 +47,6 @@ lazy_static! {
     static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
 }
 
-// TODO - Make this configurable
-const INDENT_SIZE: u32 = 4;
-
 pub struct Buffer {
     text: TextBuffer,
     file: Option<Box<dyn File>>,
@@ -70,6 +67,7 @@ pub struct Buffer {
     file_update_count: usize,
     completion_triggers: Vec<String>,
     deferred_ops: OperationQueue<Operation>,
+    indent_size: u32,
 }
 
 pub struct BufferSnapshot {
@@ -81,9 +79,9 @@ pub struct BufferSnapshot {
     file_update_count: usize,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
     selections_update_count: usize,
-    is_parsing: bool,
     language: Option<Arc<Language>>,
     parse_count: usize,
+    indent_size: u32,
 }
 
 #[derive(Clone, Debug)]
@@ -416,6 +414,8 @@ impl Buffer {
             file_update_count: 0,
             completion_triggers: Default::default(),
             deferred_ops: OperationQueue::new(),
+            // TODO: make this configurable
+            indent_size: 4,
         }
     }
 
@@ -428,10 +428,10 @@ impl Buffer {
             diagnostics: self.diagnostics.clone(),
             diagnostics_update_count: self.diagnostics_update_count,
             file_update_count: self.file_update_count,
-            is_parsing: self.parsing_in_background,
             language: self.language.clone(),
             parse_count: self.parse_count,
             selections_update_count: self.selections_update_count,
+            indent_size: self.indent_size,
         }
     }
 
@@ -768,7 +768,11 @@ impl Buffer {
                                     .before_edit
                                     .indent_column_for_line(suggestion.basis_row)
                             });
-                        let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                        let delta = if suggestion.indent {
+                            snapshot.indent_size
+                        } else {
+                            0
+                        };
                         old_suggestions.insert(
                             *old_to_new_rows.get(&old_row).unwrap(),
                             indentation_basis + delta,
@@ -787,7 +791,11 @@ impl Buffer {
                         .into_iter()
                         .flatten();
                     for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
-                        let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                        let delta = if suggestion.indent {
+                            snapshot.indent_size
+                        } else {
+                            0
+                        };
                         let new_indentation = indent_columns
                             .get(&suggestion.basis_row)
                             .copied()
@@ -819,7 +827,11 @@ impl Buffer {
                             .into_iter()
                             .flatten();
                         for (row, suggestion) in inserted_row_range.zip(suggestions) {
-                            let delta = if suggestion.indent { INDENT_SIZE } else { 0 };
+                            let delta = if suggestion.indent {
+                                snapshot.indent_size
+                            } else {
+                                0
+                            };
                             let new_indentation = indent_columns
                                 .get(&suggestion.basis_row)
                                 .copied()
@@ -1868,6 +1880,10 @@ impl BufferSnapshot {
     pub fn file_update_count(&self) -> usize {
         self.file_update_count
     }
+
+    pub fn indent_size(&self) -> u32 {
+        self.indent_size
+    }
 }
 
 impl Clone for BufferSnapshot {
@@ -1881,9 +1897,9 @@ impl Clone for BufferSnapshot {
             selections_update_count: self.selections_update_count,
             diagnostics_update_count: self.diagnostics_update_count,
             file_update_count: self.file_update_count,
-            is_parsing: self.is_parsing,
             language: self.language.clone(),
             parse_count: self.parse_count,
+            indent_size: self.indent_size,
         }
     }
 }

crates/lsp/src/lsp.rs 🔗

@@ -35,6 +35,7 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
 pub struct LanguageServer {
     next_id: AtomicUsize,
     outbound_tx: channel::Sender<Vec<u8>>,
+    name: String,
     capabilities: ServerCapabilities,
     notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
@@ -123,9 +124,11 @@ impl LanguageServer {
             .spawn()?;
         let stdin = server.stdin.take().unwrap();
         let stdout = server.stdout.take().unwrap();
-        Ok(Self::new_internal(
-            stdin, stdout, root_path, options, background,
-        ))
+        let mut server = Self::new_internal(stdin, stdout, root_path, options, background);
+        if let Some(name) = binary_path.file_name() {
+            server.name = name.to_string_lossy().to_string();
+        }
+        Ok(server)
     }
 
     fn new_internal<Stdin, Stdout>(
@@ -227,6 +230,7 @@ impl LanguageServer {
         Self {
             notification_handlers,
             response_handlers,
+            name: Default::default(),
             capabilities: Default::default(),
             next_id: Default::default(),
             outbound_tx,
@@ -297,7 +301,13 @@ impl LanguageServer {
         };
 
         let response = this.request::<request::Initialize>(params).await?;
-        Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities;
+        {
+            let this = Arc::get_mut(&mut this).unwrap();
+            if let Some(info) = response.server_info {
+                this.name = info.name;
+            }
+            this.capabilities = response.capabilities;
+        }
         this.notify::<notification::Initialized>(InitializedParams {})?;
         Ok(this)
     }
@@ -360,6 +370,10 @@ impl LanguageServer {
         }
     }
 
+    pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
+        &self.name
+    }
+
     pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
         &self.capabilities
     }

crates/project/src/project.rs 🔗

@@ -7,7 +7,7 @@ pub mod worktree;
 use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
-use collections::{hash_map, HashMap, HashSet};
+use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
@@ -28,7 +28,6 @@ use rand::prelude::*;
 use search::SearchQuery;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
-use smol::block_on;
 use std::{
     cell::RefCell,
     cmp::{self, Ordering},
@@ -52,6 +51,8 @@ pub struct Project {
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
     started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
+    language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
+    next_language_server_id: usize,
     client: Arc<client::Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
@@ -115,6 +116,33 @@ pub enum Event {
     DiagnosticsUpdated(ProjectPath),
 }
 
+enum LanguageServerEvent {
+    WorkStart {
+        token: String,
+    },
+    WorkProgress {
+        token: String,
+        progress: LanguageServerProgress,
+    },
+    WorkEnd {
+        token: String,
+    },
+    DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
+}
+
+pub struct LanguageServerStatus {
+    pub name: String,
+    pub pending_work: BTreeMap<String, LanguageServerProgress>,
+    pending_diagnostic_updates: isize,
+}
+
+#[derive(Clone, Debug)]
+pub struct LanguageServerProgress {
+    pub message: Option<String>,
+    pub percentage: Option<usize>,
+    pub last_update_at: Instant,
+}
+
 #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
 pub struct ProjectPath {
     pub worktree_id: WorktreeId,
@@ -203,8 +231,8 @@ impl Project {
         client.add_entity_message_handler(Self::handle_add_collaborator);
         client.add_entity_message_handler(Self::handle_buffer_reloaded);
         client.add_entity_message_handler(Self::handle_buffer_saved);
-        client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated);
-        client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating);
+        client.add_entity_message_handler(Self::handle_start_language_server);
+        client.add_entity_message_handler(Self::handle_update_language_server);
         client.add_entity_message_handler(Self::handle_remove_collaborator);
         client.add_entity_message_handler(Self::handle_register_worktree);
         client.add_entity_message_handler(Self::handle_unregister_worktree);
@@ -304,6 +332,8 @@ impl Project {
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
+                language_server_statuses: Default::default(),
+                next_language_server_id: 0,
                 nonce: StdRng::from_entropy().gen(),
             }
         })
@@ -373,6 +403,21 @@ impl Project {
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
+                language_server_statuses: response
+                    .language_servers
+                    .into_iter()
+                    .map(|server| {
+                        (
+                            server.id as usize,
+                            LanguageServerStatus {
+                                name: server.name,
+                                pending_work: Default::default(),
+                                pending_diagnostic_updates: 0,
+                            },
+                        )
+                    })
+                    .collect(),
+                next_language_server_id: 0,
                 opened_buffers: Default::default(),
                 buffer_snapshots: Default::default(),
                 nonce: StdRng::from_entropy().gen(),
@@ -1155,92 +1200,71 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        enum LspEvent {
-            DiagnosticsStart,
-            DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
-            DiagnosticsFinish,
-        }
-
         let key = (worktree_id, language.name());
         self.started_language_servers
             .entry(key.clone())
             .or_insert_with(|| {
+                let server_id = post_inc(&mut self.next_language_server_id);
                 let language_server = self.languages.start_language_server(
                     language.clone(),
                     worktree_path,
                     self.client.http_client(),
                     cx,
                 );
-                let rpc = self.client.clone();
                 cx.spawn_weak(|this, mut cx| async move {
                     let mut language_server = language_server?.await.log_err()?;
                     let this = this.upgrade(&cx)?;
+                    let (language_server_events_tx, language_server_events_rx) =
+                        smol::channel::unbounded();
 
-                    let disk_based_sources = language
-                        .disk_based_diagnostic_sources()
-                        .cloned()
-                        .unwrap_or_default();
-                    let disk_based_diagnostics_progress_token =
-                        language.disk_based_diagnostics_progress_token().cloned();
-                    let has_disk_based_diagnostic_progress_token =
-                        disk_based_diagnostics_progress_token.is_some();
-                    let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
-
-                    // Listen for `PublishDiagnostics` notifications.
                     language_server
                         .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                            let diagnostics_tx = diagnostics_tx.clone();
+                            let language_server_events_tx = language_server_events_tx.clone();
                             move |params| {
-                                if !has_disk_based_diagnostic_progress_token {
-                                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
-                                }
-                                block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params)))
+                                language_server_events_tx
+                                    .try_send(LanguageServerEvent::DiagnosticsUpdate(params))
                                     .ok();
-                                if !has_disk_based_diagnostic_progress_token {
-                                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
-                                }
                             }
                         })
                         .detach();
 
-                    // Listen for `Progress` notifications. Send an event when the language server
-                    // transitions between running jobs and not running any jobs.
-                    let mut running_jobs_for_this_server: i32 = 0;
                     language_server
                         .on_notification::<lsp::notification::Progress, _>(move |params| {
                             let token = match params.token {
-                                lsp::NumberOrString::Number(_) => None,
-                                lsp::NumberOrString::String(token) => Some(token),
+                                lsp::NumberOrString::String(token) => token,
+                                lsp::NumberOrString::Number(token) => {
+                                    log::info!("skipping numeric progress token {}", token);
+                                    return;
+                                }
                             };
 
-                            if token == disk_based_diagnostics_progress_token {
-                                match params.value {
-                                    lsp::ProgressParamsValue::WorkDone(progress) => {
-                                        match progress {
-                                            lsp::WorkDoneProgress::Begin(_) => {
-                                                running_jobs_for_this_server += 1;
-                                                if running_jobs_for_this_server == 1 {
-                                                    block_on(
-                                                        diagnostics_tx
-                                                            .send(LspEvent::DiagnosticsStart),
-                                                    )
-                                                    .ok();
-                                                }
-                                            }
-                                            lsp::WorkDoneProgress::End(_) => {
-                                                running_jobs_for_this_server -= 1;
-                                                if running_jobs_for_this_server == 0 {
-                                                    block_on(
-                                                        diagnostics_tx
-                                                            .send(LspEvent::DiagnosticsFinish),
-                                                    )
-                                                    .ok();
-                                                }
-                                            }
-                                            _ => {}
-                                        }
+                            match params.value {
+                                lsp::ProgressParamsValue::WorkDone(progress) => match progress {
+                                    lsp::WorkDoneProgress::Begin(_) => {
+                                        language_server_events_tx
+                                            .try_send(LanguageServerEvent::WorkStart { token })
+                                            .ok();
                                     }
-                                }
+                                    lsp::WorkDoneProgress::Report(report) => {
+                                        language_server_events_tx
+                                            .try_send(LanguageServerEvent::WorkProgress {
+                                                token,
+                                                progress: LanguageServerProgress {
+                                                    message: report.message,
+                                                    percentage: report
+                                                        .percentage
+                                                        .map(|p| p as usize),
+                                                    last_update_at: Instant::now(),
+                                                },
+                                            })
+                                            .ok();
+                                    }
+                                    lsp::WorkDoneProgress::End(_) => {
+                                        language_server_events_tx
+                                            .try_send(LanguageServerEvent::WorkEnd { token })
+                                            .ok();
+                                    }
+                                },
                             }
                         })
                         .detach();
@@ -1249,43 +1273,14 @@ impl Project {
                     cx.spawn(|mut cx| {
                         let this = this.downgrade();
                         async move {
-                            while let Ok(message) = diagnostics_rx.recv().await {
+                            while let Ok(event) = language_server_events_rx.recv().await {
                                 let this = this.upgrade(&cx)?;
-                                match message {
-                                    LspEvent::DiagnosticsStart => {
-                                        this.update(&mut cx, |this, cx| {
-                                            this.disk_based_diagnostics_started(cx);
-                                            if let Some(project_id) = this.remote_id() {
-                                                rpc.send(proto::DiskBasedDiagnosticsUpdating {
-                                                    project_id,
-                                                })
-                                                .log_err();
-                                            }
-                                        });
-                                    }
-                                    LspEvent::DiagnosticsUpdate(mut params) => {
-                                        language.process_diagnostics(&mut params);
-                                        this.update(&mut cx, |this, cx| {
-                                            this.update_diagnostics(
-                                                params,
-                                                &disk_based_sources,
-                                                cx,
-                                            )
-                                            .log_err();
-                                        });
-                                    }
-                                    LspEvent::DiagnosticsFinish => {
-                                        this.update(&mut cx, |this, cx| {
-                                            this.disk_based_diagnostics_finished(cx);
-                                            if let Some(project_id) = this.remote_id() {
-                                                rpc.send(proto::DiskBasedDiagnosticsUpdated {
-                                                    project_id,
-                                                })
-                                                .log_err();
-                                            }
-                                        });
-                                    }
-                                }
+                                this.update(&mut cx, |this, cx| {
+                                    this.on_lsp_event(server_id, event, &language, cx)
+                                });
+
+                                // Don't starve the main thread when lots of events arrive all at once.
+                                smol::future::yield_now().await;
                             }
                             Some(())
                         }
@@ -1296,6 +1291,26 @@ impl Project {
                     this.update(&mut cx, |this, cx| {
                         this.language_servers
                             .insert(key.clone(), language_server.clone());
+                        this.language_server_statuses.insert(
+                            server_id,
+                            LanguageServerStatus {
+                                name: language_server.name().to_string(),
+                                pending_work: Default::default(),
+                                pending_diagnostic_updates: 0,
+                            },
+                        );
+
+                        if let Some(project_id) = this.remote_id() {
+                            this.client
+                                .send(proto::StartLanguageServer {
+                                    project_id,
+                                    server: Some(proto::LanguageServer {
+                                        id: server_id as u64,
+                                        name: language_server.name().to_string(),
+                                    }),
+                                })
+                                .log_err();
+                        }
 
                         // Tell the language server about every open buffer in the worktree that matches the language.
                         for buffer in this.opened_buffers.values() {
@@ -1350,6 +1365,7 @@ impl Project {
                             }
                         }
 
+                        cx.notify();
                         Some(())
                     });
 
@@ -1358,6 +1374,185 @@ impl Project {
             });
     }
 
+    fn on_lsp_event(
+        &mut self,
+        language_server_id: usize,
+        event: LanguageServerEvent,
+        language: &Arc<Language>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let disk_diagnostics_token = language.disk_based_diagnostics_progress_token();
+        let language_server_status =
+            if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+                status
+            } else {
+                return;
+            };
+
+        match event {
+            LanguageServerEvent::WorkStart { token } => {
+                if Some(&token) == disk_diagnostics_token {
+                    language_server_status.pending_diagnostic_updates += 1;
+                    if language_server_status.pending_diagnostic_updates == 1 {
+                        self.disk_based_diagnostics_started(cx);
+                        self.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                                proto::LspDiskBasedDiagnosticsUpdating {},
+                            ),
+                        );
+                    }
+                } else {
+                    self.on_lsp_work_start(language_server_id, token.clone(), cx);
+                    self.broadcast_language_server_update(
+                        language_server_id,
+                        proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
+                            token,
+                        }),
+                    );
+                }
+            }
+            LanguageServerEvent::WorkProgress { token, progress } => {
+                if Some(&token) != disk_diagnostics_token {
+                    self.on_lsp_work_progress(
+                        language_server_id,
+                        token.clone(),
+                        progress.clone(),
+                        cx,
+                    );
+                    self.broadcast_language_server_update(
+                        language_server_id,
+                        proto::update_language_server::Variant::WorkProgress(
+                            proto::LspWorkProgress {
+                                token,
+                                message: progress.message,
+                                percentage: progress.percentage.map(|p| p as u32),
+                            },
+                        ),
+                    );
+                }
+            }
+            LanguageServerEvent::WorkEnd { token } => {
+                if Some(&token) == disk_diagnostics_token {
+                    language_server_status.pending_diagnostic_updates -= 1;
+                    if language_server_status.pending_diagnostic_updates == 0 {
+                        self.disk_based_diagnostics_finished(cx);
+                        self.broadcast_language_server_update(
+                            language_server_id,
+                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                                proto::LspDiskBasedDiagnosticsUpdated {},
+                            ),
+                        );
+                    }
+                } else {
+                    self.on_lsp_work_end(language_server_id, token.clone(), cx);
+                    self.broadcast_language_server_update(
+                        language_server_id,
+                        proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
+                            token,
+                        }),
+                    );
+                }
+            }
+            LanguageServerEvent::DiagnosticsUpdate(mut params) => {
+                language.process_diagnostics(&mut params);
+
+                if disk_diagnostics_token.is_none() {
+                    self.disk_based_diagnostics_started(cx);
+                    self.broadcast_language_server_update(
+                        language_server_id,
+                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                            proto::LspDiskBasedDiagnosticsUpdating {},
+                        ),
+                    );
+                }
+                self.update_diagnostics(
+                    params,
+                    language
+                        .disk_based_diagnostic_sources()
+                        .unwrap_or(&Default::default()),
+                    cx,
+                )
+                .log_err();
+                if disk_diagnostics_token.is_none() {
+                    self.disk_based_diagnostics_finished(cx);
+                    self.broadcast_language_server_update(
+                        language_server_id,
+                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                            proto::LspDiskBasedDiagnosticsUpdated {},
+                        ),
+                    );
+                }
+            }
+        }
+    }
+
+    fn on_lsp_work_start(
+        &mut self,
+        language_server_id: usize,
+        token: String,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            status.pending_work.insert(
+                token,
+                LanguageServerProgress {
+                    message: None,
+                    percentage: None,
+                    last_update_at: Instant::now(),
+                },
+            );
+            cx.notify();
+        }
+    }
+
+    fn on_lsp_work_progress(
+        &mut self,
+        language_server_id: usize,
+        token: String,
+        progress: LanguageServerProgress,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            status.pending_work.insert(token, progress);
+            cx.notify();
+        }
+    }
+
+    fn on_lsp_work_end(
+        &mut self,
+        language_server_id: usize,
+        token: String,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+            status.pending_work.remove(&token);
+            cx.notify();
+        }
+    }
+
+    fn broadcast_language_server_update(
+        &self,
+        language_server_id: usize,
+        event: proto::update_language_server::Variant,
+    ) {
+        if let Some(project_id) = self.remote_id() {
+            self.client
+                .send(proto::UpdateLanguageServer {
+                    project_id,
+                    language_server_id: language_server_id as u64,
+                    variant: Some(event),
+                })
+                .log_err();
+        }
+    }
+
+    pub fn language_server_statuses(
+        &self,
+    ) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {
+        self.language_server_statuses.values()
+    }
+
     pub fn update_diagnostics(
         &mut self,
         params: lsp::PublishDiagnosticsParams,
@@ -3096,23 +3291,76 @@ impl Project {
         })
     }
 
-    async fn handle_disk_based_diagnostics_updating(
+    async fn handle_start_language_server(
         this: ModelHandle<Self>,
-        _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+        envelope: TypedEnvelope<proto::StartLanguageServer>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
-        this.update(&mut cx, |this, cx| this.disk_based_diagnostics_started(cx));
+        let server = envelope
+            .payload
+            .server
+            .ok_or_else(|| anyhow!("invalid server"))?;
+        this.update(&mut cx, |this, cx| {
+            this.language_server_statuses.insert(
+                server.id as usize,
+                LanguageServerStatus {
+                    name: server.name,
+                    pending_work: Default::default(),
+                    pending_diagnostic_updates: 0,
+                },
+            );
+            cx.notify();
+        });
         Ok(())
     }
 
-    async fn handle_disk_based_diagnostics_updated(
+    async fn handle_update_language_server(
         this: ModelHandle<Self>,
-        _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+        envelope: TypedEnvelope<proto::UpdateLanguageServer>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
-        this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx));
+        let language_server_id = envelope.payload.language_server_id as usize;
+        match envelope
+            .payload
+            .variant
+            .ok_or_else(|| anyhow!("invalid variant"))?
+        {
+            proto::update_language_server::Variant::WorkStart(payload) => {
+                this.update(&mut cx, |this, cx| {
+                    this.on_lsp_work_start(language_server_id, payload.token, cx);
+                })
+            }
+            proto::update_language_server::Variant::WorkProgress(payload) => {
+                this.update(&mut cx, |this, cx| {
+                    this.on_lsp_work_progress(
+                        language_server_id,
+                        payload.token,
+                        LanguageServerProgress {
+                            message: payload.message,
+                            percentage: payload.percentage.map(|p| p as usize),
+                            last_update_at: Instant::now(),
+                        },
+                        cx,
+                    );
+                })
+            }
+            proto::update_language_server::Variant::WorkEnd(payload) => {
+                this.update(&mut cx, |this, cx| {
+                    this.on_lsp_work_end(language_server_id, payload.token, cx);
+                })
+            }
+            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => {
+                this.update(&mut cx, |this, cx| {
+                    this.disk_based_diagnostics_started(cx);
+                })
+            }
+            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => {
+                this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx));
+            }
+        }
+
         Ok(())
     }
 

crates/rpc/proto/zed.proto 🔗

@@ -37,8 +37,8 @@ message Envelope {
         UnregisterWorktree unregister_worktree = 29;
         UpdateWorktree update_worktree = 31;
         UpdateDiagnosticSummary update_diagnostic_summary = 32;
-        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33;
-        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34;
+        StartLanguageServer start_language_server = 33;
+        UpdateLanguageServer update_language_server = 34;
 
         OpenBuffer open_buffer = 35;
         OpenBufferResponse open_buffer_response = 36;
@@ -122,6 +122,7 @@ message JoinProjectResponse {
     uint32 replica_id = 1;
     repeated Worktree worktrees = 2;
     repeated Collaborator collaborators = 3;
+    repeated LanguageServer language_servers = 4;
 }
 
 message LeaveProject {
@@ -410,6 +411,16 @@ message LocalTimestamp {
     uint32 value = 2;
 }
 
+message LanguageServer {
+    uint64 id = 1;
+    string name = 2;
+}
+
+message StartLanguageServer {
+    uint64 project_id = 1;
+    LanguageServer server = 2;
+}
+
 message UpdateDiagnosticSummary {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
@@ -424,14 +435,36 @@ message DiagnosticSummary {
     uint32 hint_count = 5;
 }
 
-message DiskBasedDiagnosticsUpdating {
+message UpdateLanguageServer {
     uint64 project_id = 1;
+    uint64 language_server_id = 2;
+    oneof variant {
+        LspWorkStart work_start = 3;
+        LspWorkProgress work_progress = 4;
+        LspWorkEnd work_end = 5;
+        LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6;
+        LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7;
+    }
 }
 
-message DiskBasedDiagnosticsUpdated {
-    uint64 project_id = 1;
+message LspWorkStart {
+    string token = 1;
+}
+
+message LspWorkProgress {
+    string token = 1;
+    optional string message = 2;
+    optional uint32 percentage = 3;
 }
 
+message LspWorkEnd {
+    string token = 1;
+}
+
+message LspDiskBasedDiagnosticsUpdating {}
+
+message LspDiskBasedDiagnosticsUpdated {}
+
 message GetChannels {}
 
 message GetChannelsResponse {

crates/rpc/src/peer.rs 🔗

@@ -96,6 +96,7 @@ pub struct ConnectionState {
 
 const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
+const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30);
 
 impl Peer {
     pub fn new() -> Arc<Self> {
@@ -147,14 +148,14 @@ impl Peer {
             let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse();
             futures::pin_mut!(keepalive_timer);
 
+            // Disconnect if we don't receive messages at least this frequently.
+            let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse();
+            futures::pin_mut!(receive_timeout);
+
             loop {
                 let read_message = reader.read().fuse();
                 futures::pin_mut!(read_message);
 
-                // Disconnect if we don't receive messages at least this frequently.
-                let receive_timeout = create_timer(3 * KEEPALIVE_INTERVAL).fuse();
-                futures::pin_mut!(receive_timeout);
-
                 loop {
                     futures::select_biased! {
                         outgoing = outgoing_rx.next().fuse() => match outgoing {
@@ -170,6 +171,7 @@ impl Peer {
                         },
                         incoming = read_message => {
                             let incoming = incoming.context("received invalid RPC message")?;
+                            receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse());
                             if let proto::Message::Envelope(incoming) = incoming {
                                 if incoming_tx.send(incoming).await.is_err() {
                                     return Ok(());

crates/rpc/src/proto.rs 🔗

@@ -146,8 +146,6 @@ messages!(
     (BufferReloaded, Foreground),
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
-    (DiskBasedDiagnosticsUpdated, Background),
-    (DiskBasedDiagnosticsUpdating, Background),
     (Error, Foreground),
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
@@ -173,6 +171,8 @@ messages!(
     (JoinChannelResponse, Foreground),
     (JoinProject, Foreground),
     (JoinProjectResponse, Foreground),
+    (StartLanguageServer, Foreground),
+    (UpdateLanguageServer, Foreground),
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
     (OpenBuffer, Background),
@@ -246,8 +246,6 @@ entity_messages!(
     ApplyCompletionAdditionalEdits,
     BufferReloaded,
     BufferSaved,
-    DiskBasedDiagnosticsUpdated,
-    DiskBasedDiagnosticsUpdating,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -264,11 +262,13 @@ entity_messages!(
     RemoveProjectCollaborator,
     SaveBuffer,
     SearchProject,
+    StartLanguageServer,
     UnregisterWorktree,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
+    UpdateLanguageServer,
     RegisterWorktree,
     UpdateWorktree,
 );

crates/server/src/rpc.rs 🔗

@@ -83,9 +83,9 @@ impl Server {
             .add_request_handler(Server::register_worktree)
             .add_message_handler(Server::unregister_worktree)
             .add_request_handler(Server::update_worktree)
+            .add_message_handler(Server::start_language_server)
+            .add_message_handler(Server::update_language_server)
             .add_message_handler(Server::update_diagnostic_summary)
-            .add_message_handler(Server::disk_based_diagnostics_updating)
-            .add_message_handler(Server::disk_based_diagnostics_updated)
             .add_request_handler(Server::forward_project_request::<proto::GetDefinition>)
             .add_request_handler(Server::forward_project_request::<proto::GetReferences>)
             .add_request_handler(Server::forward_project_request::<proto::SearchProject>)
@@ -386,6 +386,7 @@ impl Server {
                     worktrees,
                     replica_id: joined.replica_id as u32,
                     collaborators,
+                    language_servers: joined.project.language_servers.clone(),
                 };
                 let connection_ids = joined.project.connection_ids();
                 let contact_user_ids = joined.project.authorized_user_ids();
@@ -535,13 +536,19 @@ impl Server {
         Ok(())
     }
 
-    async fn disk_based_diagnostics_updating(
-        self: Arc<Server>,
-        request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+    async fn start_language_server(
+        mut self: Arc<Server>,
+        request: TypedEnvelope<proto::StartLanguageServer>,
     ) -> tide::Result<()> {
-        let receiver_ids = self
-            .state()
-            .project_connection_ids(request.payload.project_id, request.sender_id)?;
+        let receiver_ids = self.state_mut().start_language_server(
+            request.payload.project_id,
+            request.sender_id,
+            request
+                .payload
+                .server
+                .clone()
+                .ok_or_else(|| anyhow!("invalid language server"))?,
+        )?;
         broadcast(request.sender_id, receiver_ids, |connection_id| {
             self.peer
                 .forward_send(request.sender_id, connection_id, request.payload.clone())
@@ -549,9 +556,9 @@ impl Server {
         Ok(())
     }
 
-    async fn disk_based_diagnostics_updated(
+    async fn update_language_server(
         self: Arc<Server>,
-        request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+        request: TypedEnvelope<proto::UpdateLanguageServer>,
     ) -> tide::Result<()> {
         let receiver_ids = self
             .state()

crates/server/src/rpc/store.rs 🔗

@@ -25,6 +25,7 @@ pub struct Project {
     pub host_user_id: UserId,
     pub share: Option<ProjectShare>,
     pub worktrees: HashMap<u64, Worktree>,
+    pub language_servers: Vec<proto::LanguageServer>,
 }
 
 pub struct Worktree {
@@ -240,6 +241,7 @@ impl Store {
                 host_user_id,
                 share: None,
                 worktrees: Default::default(),
+                language_servers: Default::default(),
             },
         );
         self.next_project_id += 1;
@@ -438,6 +440,24 @@ impl Store {
         Err(anyhow!("no such worktree"))?
     }
 
+    pub fn start_language_server(
+        &mut self,
+        project_id: u64,
+        connection_id: ConnectionId,
+        language_server: proto::LanguageServer,
+    ) -> tide::Result<Vec<ConnectionId>> {
+        let project = self
+            .projects
+            .get_mut(&project_id)
+            .ok_or_else(|| anyhow!("no such project"))?;
+        if project.host_connection_id == connection_id {
+            project.language_servers.push(language_server);
+            return Ok(project.connection_ids());
+        }
+
+        Err(anyhow!("no such project"))?
+    }
+
     pub fn join_project(
         &mut self,
         connection_id: ConnectionId,

crates/workspace/Cargo.toml 🔗

@@ -26,6 +26,7 @@ parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
 serde = { version = "1", features = ["derive", "rc"] }
 serde_json = { version = "1", features = ["preserve_order"] }
+smallvec = { version = "1.6", features = ["union"] }
 
 [dev-dependencies]
 client = { path = "../client", features = ["test-support"] }

crates/workspace/src/lsp_status.rs 🔗

@@ -1,11 +1,16 @@
 use crate::{ItemViewHandle, Settings, StatusItemView};
 use futures::StreamExt;
+use gpui::AppContext;
 use gpui::{
-    action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View,
-    ViewContext,
+    action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext,
+    RenderContext, View, ViewContext,
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use postage::watch;
+use project::{LanguageServerProgress, Project};
+use smallvec::SmallVec;
+use std::cmp::Reverse;
+use std::fmt::Write;
 use std::sync::Arc;
 
 action!(DismissErrorMessage);
@@ -15,6 +20,7 @@ pub struct LspStatus {
     checking_for_update: Vec<String>,
     downloading: Vec<String>,
     failed: Vec<String>,
+    project: ModelHandle<Project>,
 }
 
 pub fn init(cx: &mut MutableAppContext) {
@@ -23,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
 impl LspStatus {
     pub fn new(
+        project: &ModelHandle<Project>,
         languages: Arc<LanguageRegistry>,
         settings_rx: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self>,
@@ -62,11 +69,14 @@ impl LspStatus {
             }
         })
         .detach();
+        cx.observe(project, |_, _, cx| cx.notify()).detach();
+
         Self {
             settings_rx,
             checking_for_update: Default::default(),
             downloading: Default::default(),
             failed: Default::default(),
+            project: project.clone(),
         }
     }
 
@@ -74,6 +84,30 @@ impl LspStatus {
         self.failed.clear();
         cx.notify();
     }
+
+    fn pending_language_server_work<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
+        self.project
+            .read(cx)
+            .language_server_statuses()
+            .rev()
+            .filter_map(|status| {
+                if status.pending_work.is_empty() {
+                    None
+                } else {
+                    let mut pending_work = status
+                        .pending_work
+                        .iter()
+                        .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
+                        .collect::<SmallVec<[_; 4]>>();
+                    pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
+                    Some(pending_work)
+                }
+            })
+            .flatten()
+    }
 }
 
 impl Entity for LspStatus {
@@ -87,7 +121,29 @@ impl View for LspStatus {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = &self.settings_rx.borrow().theme;
-        if !self.downloading.is_empty() {
+
+        let mut pending_work = self.pending_language_server_work(cx);
+        if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
+            let mut message = lang_server_name.to_string();
+
+            message.push_str(": ");
+            if let Some(progress_message) = progress.message.as_ref() {
+                message.push_str(progress_message);
+            } else {
+                message.push_str(progress_token);
+            }
+
+            if let Some(percentage) = progress.percentage {
+                write!(&mut message, " ({}%)", percentage).unwrap();
+            }
+
+            let additional_work_count = pending_work.count();
+            if additional_work_count > 0 {
+                write!(&mut message, " + {} more", additional_work_count).unwrap();
+            }
+
+            Label::new(message, theme.workspace.status_bar.lsp_message.clone()).boxed()
+        } else if !self.downloading.is_empty() {
             Label::new(
                 format!(
                     "Downloading {} language server{}...",
@@ -112,6 +168,7 @@ impl View for LspStatus {
             )
             .boxed()
         } else if !self.failed.is_empty() {
+            drop(pending_work);
             MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
                 Label::new(
                     format!(

crates/workspace/src/pane.rs 🔗

@@ -6,9 +6,9 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
-    platform::CursorStyle,
+    platform::{CursorStyle, NavigationDirection},
     AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
-    ViewHandle,
+    ViewHandle, WeakViewHandle,
 };
 use postage::watch;
 use project::ProjectPath;
@@ -27,8 +27,8 @@ action!(ActivateNextItem);
 action!(CloseActiveItem);
 action!(CloseInactiveItems);
 action!(CloseItem, usize);
-action!(GoBack);
-action!(GoForward);
+action!(GoBack, Option<WeakViewHandle<Pane>>);
+action!(GoForward, Option<WeakViewHandle<Pane>>);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -54,11 +54,27 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &Split, cx| {
         pane.split(action.0, cx);
     });
-    cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
-        Pane::go_back(workspace, cx).detach();
+    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
+        Pane::go_back(
+            workspace,
+            action
+                .0
+                .as_ref()
+                .and_then(|weak_handle| weak_handle.upgrade(cx)),
+            cx,
+        )
+        .detach();
     });
-    cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
-        Pane::go_forward(workspace, cx).detach();
+    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
+        Pane::go_forward(
+            workspace,
+            action
+                .0
+                .as_ref()
+                .and_then(|weak_handle| weak_handle.upgrade(cx)),
+            cx,
+        )
+        .detach();
     });
 
     cx.add_bindings(vec![
@@ -70,8 +86,8 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
         Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
         Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
-        Binding::new("ctrl--", GoBack, Some("Pane")),
-        Binding::new("shift-ctrl-_", GoForward, Some("Pane")),
+        Binding::new("ctrl--", GoBack(None), Some("Pane")),
+        Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
     ]);
 }
 
@@ -163,19 +179,27 @@ impl Pane {
         cx.emit(Event::Activate);
     }
 
-    pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
+    pub fn go_back(
+        workspace: &mut Workspace,
+        pane: Option<ViewHandle<Pane>>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<()> {
         Self::navigate_history(
             workspace,
-            workspace.active_pane().clone(),
+            pane.unwrap_or_else(|| workspace.active_pane().clone()),
             NavigationMode::GoingBack,
             cx,
         )
     }
 
-    pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
+    pub fn go_forward(
+        workspace: &mut Workspace,
+        pane: Option<ViewHandle<Pane>>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<()> {
         Self::navigate_history(
             workspace,
-            workspace.active_pane().clone(),
+            pane.unwrap_or_else(|| workspace.active_pane().clone()),
             NavigationMode::GoingForward,
             cx,
         )
@@ -187,6 +211,8 @@ impl Pane {
         mode: NavigationMode,
         cx: &mut ViewContext<Workspace>,
     ) -> Task<()> {
+        workspace.activate_pane(pane.clone(), cx);
+
         let to_load = pane.update(cx, |pane, cx| {
             // Retrieve the weak item handle from the history.
             let entry = pane.nav_history.borrow_mut().pop(mode)?;
@@ -634,7 +660,9 @@ impl View for Pane {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        if let Some(active_item) = self.active_item() {
+        let this = cx.handle();
+
+        EventHandler::new(if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
                 .with_children(
@@ -643,10 +671,20 @@ impl View for Pane {
                         .map(|view| ChildView::new(view).boxed()),
                 )
                 .with_child(ChildView::new(active_item).flexible(1., true).boxed())
-                .named("pane")
+                .boxed()
         } else {
-            Empty::new().named("pane")
-        }
+            Empty::new().boxed()
+        })
+        .on_navigate_mouse_down(move |direction, cx| {
+            let this = this.clone();
+            match direction {
+                NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
+                NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
+            }
+
+            true
+        })
+        .named("pane")
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {

crates/zed/src/zed.rs 🔗

@@ -129,6 +129,7 @@ pub fn build_workspace(
     });
     let lsp_status = cx.add_view(|cx| {
         workspace::lsp_status::LspStatus::new(
+            workspace.project(),
             app_state.languages.clone(),
             app_state.settings.clone(),
             cx,
@@ -775,44 +776,58 @@ mod tests {
             (file3.clone(), DisplayPoint::new(15, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file2.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(0, 0))
         );
 
         // Go back one more time and ensure we don't navigate past the first item in the history.
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file2.clone(), DisplayPoint::new(0, 0))
@@ -826,7 +841,9 @@ mod tests {
                 .update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
             drop(editor3);
         });
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))
@@ -846,12 +863,16 @@ mod tests {
             })
             .await
             .unwrap();
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))