Merge remote-tracking branch 'origin/main' into panels

Antonio Scandurra created

Change summary

.github/pull_request_template.md                   |  10 +
Cargo.lock                                         |   2 
assets/keymaps/default.json                        |   4 
assets/settings/default.json                       |  46 +++--
crates/client/src/client.rs                        |   2 
crates/collab/src/tests/integration_tests.rs       |   1 
crates/diagnostics/src/diagnostics.rs              |  25 +
crates/editor/src/editor.rs                        |  93 +++++++++-
crates/editor/src/editor_settings.rs               |  23 +
crates/editor/src/editor_tests.rs                  | 112 +++++++++++++
crates/editor/src/element.rs                       | 108 ++++++-----
crates/editor/src/movement.rs                      |  38 ++++
crates/editor/src/multi_buffer.rs                  |   9 +
crates/git/src/diff.rs                             |   4 
crates/language/src/buffer.rs                      |  15 +
crates/language/src/diagnostic_set.rs              |   4 
crates/language/src/language_settings.rs           |   2 
crates/project/src/project.rs                      | 137 ++++++++++++---
crates/project/src/project_settings.rs             |   2 
crates/project/src/project_tests.rs                |  89 ++++++++++
crates/project/src/worktree.rs                     |  52 +++++
crates/project_panel/Cargo.toml                    |   4 
crates/project_panel/src/project_panel.rs          |  81 +++-----
crates/project_panel/src/project_panel_settings.rs |  39 ++++
crates/search/src/project_search.rs                |  14 
crates/settings/src/settings_store.rs              |  73 ++-----
crates/theme/src/theme.rs                          |  21 ++
crates/theme/src/ui.rs                             |  57 ------
crates/workspace/src/workspace_settings.rs         |   2 
crates/zed/src/zed.rs                              |   1 
script/get-preview-channel-changes                 |   7 
styles/src/styleTree/editor.ts                     |  41 +++-
styles/src/styleTree/projectPanel.ts               |  18 ++
33 files changed, 827 insertions(+), 309 deletions(-)

Detailed changes

.github/pull_request_template.md πŸ”—

@@ -2,4 +2,12 @@
 
 Release Notes:
 
-* [[Added foo / Fixed bar / No notes]]
+Use `N/A` in this section if this item should be skipped in the release notes.
+
+Add release note lines here:
+
+* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
+* ...
+
+If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
+These will be removed by the person making the release.

Cargo.lock πŸ”—

@@ -4886,6 +4886,7 @@ dependencies = [
 name = "project_panel"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "client",
  "context_menu",
  "db",
@@ -4899,6 +4900,7 @@ dependencies = [
  "project",
  "schemars",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "theme",

assets/keymaps/default.json πŸ”—

@@ -68,10 +68,12 @@
       "cmd-z": "editor::Undo",
       "cmd-shift-z": "editor::Redo",
       "up": "editor::MoveUp",
+      "ctrl-up": "editor::MoveToStartOfParagraph",
       "pageup": "editor::PageUp",
       "shift-pageup": "editor::MovePageUp",
       "home": "editor::MoveToBeginningOfLine",
       "down": "editor::MoveDown",
+      "ctrl-down": "editor::MoveToEndOfParagraph",
       "pagedown": "editor::PageDown",
       "shift-pagedown": "editor::MovePageDown",
       "end": "editor::MoveToEndOfLine",
@@ -104,6 +106,8 @@
       "alt-shift-b": "editor::SelectToPreviousWordStart",
       "alt-shift-right": "editor::SelectToNextWordEnd",
       "alt-shift-f": "editor::SelectToNextWordEnd",
+      "ctrl-shift-up": "editor::SelectToStartOfParagraph",
+      "ctrl-shift-down": "editor::SelectToEndOfParagraph",
       "cmd-shift-up": "editor::SelectToBeginning",
       "cmd-shift-down": "editor::SelectToEnd",
       "cmd-a": "editor::SelectAll",

assets/settings/default.json πŸ”—

@@ -52,19 +52,32 @@
   // 3. Draw all invisible symbols:
   //   "all"
   "show_whitespaces": "selection",
-  // Whether to show the scrollbar in the editor.
-  // This setting can take four values:
-  //
-  // 1. Show the scrollbar if there's important information or
-  //    follow the system's configured behavior (default):
-  //   "auto"
-  // 2. Match the system's configured behavior:
-  //    "system"
-  // 3. Always show the scrollbar:
-  //    "always"
-  // 4. Never show the scrollbar:
-  //    "never"
-  "show_scrollbars": "auto",
+  // Scrollbar related settings
+  "scrollbar": {
+      // When to show the scrollbar in the editor.
+      // This setting can take four values:
+      //
+      // 1. Show the scrollbar if there's important information or
+      //    follow the system's configured behavior (default):
+      //   "auto"
+      // 2. Match the system's configured behavior:
+      //    "system"
+      // 3. Always show the scrollbar:
+      //    "always"
+      // 4. Never show the scrollbar:
+      //    "never"
+      "show": "auto",
+      // Whether to show git diff indicators in the scrollbar.
+      "git_diff": true
+  },
+  "project_panel": {
+      // Whether to show the git status in the project panel.
+      "git_status": true,
+      // Where to dock project panel. Can be 'left' or 'right'.
+      "dock": "left",
+      // Default width of the project panel.
+      "default_width": 240
+  },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,
   // Whether to use language servers to provide code intelligence.
@@ -128,13 +141,6 @@
   },
   // Automatically update Zed
   "auto_update": true,
-  // Settings specific to the project panel
-  "project_panel": {
-    // Where to dock project panel. Can be 'left' or 'right'.
-    "dock": "left",
-    // Default width of the project panel.
-    "default_width": 240
-  },
   // Git gutter behavior configuration.
   "git": {
     // Control whether the git gutter is shown. May take 2 values:

crates/client/src/client.rs πŸ”—

@@ -339,7 +339,7 @@ pub struct TelemetrySettings {
     pub metrics: bool,
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct TelemetrySettingsContent {
     pub diagnostics: Option<bool>,
     pub metrics: Option<bool>,

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -2688,6 +2688,7 @@ async fn test_git_branch_name(
     });
 
     let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
+    deterministic.run_until_parked();
     project_remote_c.read_with(cx_c, |project, cx| {
         assert_branch(Some("branch-2"), project, cx)
     });

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -33,7 +33,7 @@ use theme::ThemeSettings;
 use util::TryFutureExt;
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
-    ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
+    ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
 };
 
 actions!(diagnostics, [Deploy]);
@@ -90,11 +90,15 @@ impl View for ProjectDiagnosticsEditor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if self.path_states.is_empty() {
             let theme = &theme::current(cx).project_diagnostics;
-            Label::new("No problems in workspace", theme.empty_message.clone())
-                .aligned()
-                .contained()
-                .with_style(theme.container)
-                .into_any()
+            PaneBackdrop::new(
+                cx.view_id(),
+                Label::new("No problems in workspace", theme.empty_message.clone())
+                    .aligned()
+                    .contained()
+                    .with_style(theme.container)
+                    .into_any(),
+            )
+            .into_any()
         } else {
             ChildView::new(&self.editor, cx).into_any()
         }
@@ -161,8 +165,13 @@ impl ProjectDiagnosticsEditor {
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
-        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
-            .detach();
+        cx.subscribe(&editor, |this, _, event, cx| {
+            cx.emit(event.clone());
+            if event == &editor::Event::Focused && this.path_states.is_empty() {
+                cx.focus_self()
+            }
+        })
+        .detach();
 
         let project = project_handle.read(cx);
         let paths_to_update = project

crates/editor/src/editor.rs πŸ”—

@@ -216,6 +216,8 @@ actions!(
         MoveToNextSubwordEnd,
         MoveToBeginningOfLine,
         MoveToEndOfLine,
+        MoveToStartOfParagraph,
+        MoveToEndOfParagraph,
         MoveToBeginning,
         MoveToEnd,
         SelectUp,
@@ -226,6 +228,8 @@ actions!(
         SelectToPreviousSubwordStart,
         SelectToNextWordEnd,
         SelectToNextSubwordEnd,
+        SelectToStartOfParagraph,
+        SelectToEndOfParagraph,
         SelectToBeginning,
         SelectToEnd,
         SelectAll,
@@ -337,6 +341,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::move_to_next_subword_end);
     cx.add_action(Editor::move_to_beginning_of_line);
     cx.add_action(Editor::move_to_end_of_line);
+    cx.add_action(Editor::move_to_start_of_paragraph);
+    cx.add_action(Editor::move_to_end_of_paragraph);
     cx.add_action(Editor::move_to_beginning);
     cx.add_action(Editor::move_to_end);
     cx.add_action(Editor::select_up);
@@ -349,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::select_to_next_subword_end);
     cx.add_action(Editor::select_to_beginning_of_line);
     cx.add_action(Editor::select_to_end_of_line);
+    cx.add_action(Editor::select_to_start_of_paragraph);
+    cx.add_action(Editor::select_to_end_of_paragraph);
     cx.add_action(Editor::select_to_beginning);
     cx.add_action(Editor::select_to_end);
     cx.add_action(Editor::select_all);
@@ -525,15 +533,6 @@ pub struct EditorSnapshot {
     ongoing_scroll: OngoingScroll,
 }
 
-impl EditorSnapshot {
-    fn has_scrollbar_info(&self) -> bool {
-        self.buffer_snapshot
-            .git_diff_hunks_in_range(0..self.max_point().row())
-            .next()
-            .is_some()
-    }
-}
-
 #[derive(Clone, Debug)]
 struct SelectionHistoryEntry {
     selections: Arc<[Selection<Anchor>]>,
@@ -4762,6 +4761,80 @@ impl Editor {
         });
     }
 
+    pub fn move_to_start_of_paragraph(
+        &mut self,
+        _: &MoveToStartOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_with(|map, selection| {
+                selection.collapse_to(
+                    movement::start_of_paragraph(map, selection.head()),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn move_to_end_of_paragraph(
+        &mut self,
+        _: &MoveToEndOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_with(|map, selection| {
+                selection.collapse_to(
+                    movement::end_of_paragraph(map, selection.head()),
+                    SelectionGoal::None,
+                )
+            });
+        })
+    }
+
+    pub fn select_to_start_of_paragraph(
+        &mut self,
+        _: &SelectToStartOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (movement::start_of_paragraph(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
+    pub fn select_to_end_of_paragraph(
+        &mut self,
+        _: &SelectToEndOfParagraph,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+            s.move_heads_with(|map, head, _| {
+                (movement::end_of_paragraph(map, head), SelectionGoal::None)
+            });
+        })
+    }
+
     pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
         if matches!(self.mode, EditorMode::SingleLine) {
             cx.propagate_action();
@@ -7128,6 +7201,7 @@ pub enum Event {
     BufferEdited,
     Edited,
     Reparsed,
+    Focused,
     Blurred,
     DirtyChanged,
     Saved,
@@ -7181,6 +7255,7 @@ impl View for Editor {
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         if cx.is_self_focused() {
             let focused_event = EditorFocused(cx.handle());
+            cx.emit(Event::Focused);
             cx.emit_global(focused_event);
         }
         if let Some(rename) = self.pending_rename.as_ref() {

crates/editor/src/editor_settings.rs πŸ”—

@@ -7,25 +7,36 @@ pub struct EditorSettings {
     pub cursor_blink: bool,
     pub hover_popover_enabled: bool,
     pub show_completions_on_input: bool,
-    pub show_scrollbars: ShowScrollbars,
+    pub scrollbar: Scrollbar,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct Scrollbar {
+    pub show: ShowScrollbar,
+    pub git_diff: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(rename_all = "snake_case")]
-pub enum ShowScrollbars {
-    #[default]
+pub enum ShowScrollbar {
     Auto,
     System,
     Always,
     Never,
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct EditorSettingsContent {
     pub cursor_blink: Option<bool>,
     pub hover_popover_enabled: Option<bool>,
     pub show_completions_on_input: Option<bool>,
-    pub show_scrollbars: Option<ShowScrollbars>,
+    pub scrollbar: Option<ScrollbarContent>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarContent {
+    pub show: Option<ShowScrollbar>,
+    pub git_diff: Option<bool>,
 }
 
 impl Setting for EditorSettings {

crates/editor/src/editor_tests.rs πŸ”—

@@ -1243,6 +1243,118 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx);
+
+    let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+    cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
+
+    cx.set_state(
+        &r#"Λ‡one
+        two
+
+        three
+        fourˇ
+        five
+
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+        Λ‡
+        three
+        four
+        five
+        Λ‡
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+
+        three
+        four
+        five
+        Λ‡
+        sixˇ"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"Λ‡one
+        two
+
+        three
+        four
+        five
+
+        sixˇ"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"Λ‡one
+        two
+        Λ‡
+        three
+        four
+        five
+
+        six"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"Λ‡one
+        two
+
+        three
+        four
+        five
+
+        sixˇ"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+
+        three
+        four
+        five
+        Λ‡
+        sixˇ"#
+            .unindent(),
+    );
+
+    cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+    cx.assert_editor_state(
+        &r#"one
+        two
+        Λ‡
+        three
+        four
+        five
+        Λ‡
+        six"#
+            .unindent(),
+    );
+}
+
 #[gpui::test]
 async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/element.rs πŸ”—

@@ -5,7 +5,7 @@ use super::{
 };
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
-    editor_settings::ShowScrollbars,
+    editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
         hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@@ -1052,51 +1052,53 @@ impl EditorElement {
                 ..Default::default()
             });
 
-            let diff_style = theme::current(cx).editor.diff.clone();
-            for hunk in layout
-                .position_map
-                .snapshot
-                .buffer_snapshot
-                .git_diff_hunks_in_range(0..(max_row.floor() as u32))
-            {
-                let start_display = Point::new(hunk.buffer_range.start, 0)
-                    .to_display_point(&layout.position_map.snapshot.display_snapshot);
-                let end_display = Point::new(hunk.buffer_range.end, 0)
-                    .to_display_point(&layout.position_map.snapshot.display_snapshot);
-                let start_y = y_for_row(start_display.row() as f32);
-                let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
-                    y_for_row((end_display.row() + 1) as f32)
-                } else {
-                    y_for_row((end_display.row()) as f32)
-                };
+            if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
+                let diff_style = theme::current(cx).editor.scrollbar.git.clone();
+                for hunk in layout
+                    .position_map
+                    .snapshot
+                    .buffer_snapshot
+                    .git_diff_hunks_in_range(0..(max_row.floor() as u32))
+                {
+                    let start_display = Point::new(hunk.buffer_range.start, 0)
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let end_display = Point::new(hunk.buffer_range.end, 0)
+                        .to_display_point(&layout.position_map.snapshot.display_snapshot);
+                    let start_y = y_for_row(start_display.row() as f32);
+                    let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
+                        y_for_row((end_display.row() + 1) as f32)
+                    } else {
+                        y_for_row((end_display.row()) as f32)
+                    };
 
-                if end_y - start_y < 1. {
-                    end_y = start_y + 1.;
-                }
-                let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
-
-                let color = match hunk.status() {
-                    DiffHunkStatus::Added => diff_style.inserted,
-                    DiffHunkStatus::Modified => diff_style.modified,
-                    DiffHunkStatus::Removed => diff_style.deleted,
-                };
-
-                let border = Border {
-                    width: 1.,
-                    color: style.thumb.border.color,
-                    overlay: false,
-                    top: false,
-                    right: true,
-                    bottom: false,
-                    left: true,
-                };
+                    if end_y - start_y < 1. {
+                        end_y = start_y + 1.;
+                    }
+                    let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
 
-                scene.push_quad(Quad {
-                    bounds,
-                    background: Some(color),
-                    border,
-                    corner_radius: style.thumb.corner_radius,
-                })
+                    let color = match hunk.status() {
+                        DiffHunkStatus::Added => diff_style.inserted,
+                        DiffHunkStatus::Modified => diff_style.modified,
+                        DiffHunkStatus::Removed => diff_style.deleted,
+                    };
+
+                    let border = Border {
+                        width: 1.,
+                        color: style.thumb.border.color,
+                        overlay: false,
+                        top: false,
+                        right: true,
+                        bottom: false,
+                        left: true,
+                    };
+
+                    scene.push_quad(Quad {
+                        bounds,
+                        background: Some(color),
+                        border,
+                        corner_radius: style.thumb.corner_radius,
+                    })
+                }
             }
 
             scene.push_quad(Quad {
@@ -2065,13 +2067,17 @@ impl Element<Editor> for EditorElement {
             ));
         }
 
-        let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
-            ShowScrollbars::Auto => {
-                snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
+        let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
+        let show_scrollbars = match scrollbar_settings.show {
+            ShowScrollbar::Auto => {
+                // Git
+                (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+                // Scrollmanager
+                || editor.scroll_manager.scrollbars_visible()
             }
-            ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
-            ShowScrollbars::Always => true,
-            ShowScrollbars::Never => false,
+            ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
+            ShowScrollbar::Always => true,
+            ShowScrollbar::Never => false,
         };
 
         let include_root = editor
@@ -2290,6 +2296,7 @@ impl Element<Editor> for EditorElement {
                 text_size,
                 scrollbar_row_range,
                 show_scrollbars,
+                is_singleton,
                 max_row,
                 gutter_margin,
                 active_rows,
@@ -2445,6 +2452,7 @@ pub struct LayoutState {
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
+    is_singleton: bool,
     max_row: u32,
     context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
     code_actions_indicator: Option<(u32, AnyElement<Editor>)>,

crates/editor/src/movement.rs πŸ”—

@@ -193,6 +193,44 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
     })
 }
 
+pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    if point.row == 0 {
+        return map.max_point();
+    }
+
+    let mut found_non_blank_line = false;
+    for row in (0..point.row + 1).rev() {
+        let blank = map.buffer_snapshot.is_line_blank(row);
+        if found_non_blank_line && blank {
+            return Point::new(row, 0).to_display_point(map);
+        }
+
+        found_non_blank_line |= !blank;
+    }
+
+    DisplayPoint::zero()
+}
+
+pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+    let point = display_point.to_point(map);
+    if point.row == map.max_buffer_row() {
+        return DisplayPoint::zero();
+    }
+
+    let mut found_non_blank_line = false;
+    for row in point.row..map.max_buffer_row() + 1 {
+        let blank = map.buffer_snapshot.is_line_blank(row);
+        if found_non_blank_line && blank {
+            return Point::new(row, 0).to_display_point(map);
+        }
+
+        found_non_blank_line |= !blank;
+    }
+
+    map.max_point()
+}
+
 /// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
 /// given predicate returning true. The predicate is called with the character to the left and right
 /// of the candidate boundary location, and will be called with `\n` characters indicating the start

crates/editor/src/multi_buffer.rs πŸ”—

@@ -2841,6 +2841,15 @@ impl MultiBufferSnapshot {
             })
     }
 
+    pub fn has_git_diffs(&self) -> bool {
+        for excerpt in self.excerpts.iter() {
+            if !excerpt.buffer.git_diff.is_empty() {
+                return true;
+            }
+        }
+        false
+    }
+
     pub fn git_diff_hunks_in_range_rev<'a>(
         &'a self,
         row_range: Range<u32>,

crates/git/src/diff.rs πŸ”—

@@ -71,6 +71,10 @@ impl BufferDiff {
         }
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.tree.is_empty()
+    }
+
     pub fn hunks_in_row_range<'a>(
         &'a self,
         range: Range<u32>,

crates/language/src/buffer.rs πŸ”—

@@ -1644,10 +1644,17 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         if lamport_timestamp > self.diagnostics_timestamp {
-            match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
-                Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
-                Ok(ix) => self.diagnostics[ix].1 = diagnostics,
-            };
+            let ix = self.diagnostics.binary_search_by_key(&server_id, |e| e.0);
+            if diagnostics.len() == 0 {
+                if let Ok(ix) = ix {
+                    self.diagnostics.remove(ix);
+                }
+            } else {
+                match ix {
+                    Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
+                    Ok(ix) => self.diagnostics[ix].1 = diagnostics,
+                };
+            }
             self.diagnostics_timestamp = lamport_timestamp;
             self.diagnostics_update_count += 1;
             self.text.lamport_clock.observe(lamport_timestamp);

crates/language/src/diagnostic_set.rs πŸ”—

@@ -80,6 +80,10 @@ impl DiagnosticSet {
         }
     }
 
+    pub fn len(&self) -> usize {
+        self.diagnostics.summary().count
+    }
+
     pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
         self.diagnostics.iter()
     }

crates/language/src/language_settings.rs πŸ”—

@@ -49,7 +49,7 @@ pub struct CopilotSettings {
     pub disabled_globs: Vec<GlobMatcher>,
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct AllLanguageSettingsContent {
     #[serde(default)]
     pub features: Option<FeaturesContent>,

crates/project/src/project.rs πŸ”—

@@ -16,6 +16,7 @@ use copilot::Copilot;
 use futures::{
     channel::mpsc::{self, UnboundedReceiver},
     future::{try_join_all, Shared},
+    stream::FuturesUnordered,
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
 use globset::{Glob, GlobSet, GlobSetBuilder};
@@ -1374,7 +1375,7 @@ impl Project {
             return Task::ready(Ok(existing_buffer));
         }
 
-        let mut loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
+        let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
             // If the given path is already being loaded, then wait for that existing
             // task to complete and return the same buffer.
             hash_map::Entry::Occupied(e) => e.get().clone(),
@@ -1405,15 +1406,9 @@ impl Project {
         };
 
         cx.foreground().spawn(async move {
-            loop {
-                if let Some(result) = loading_watch.borrow().as_ref() {
-                    match result {
-                        Ok(buffer) => return Ok(buffer.clone()),
-                        Err(error) => return Err(anyhow!("{}", error)),
-                    }
-                }
-                loading_watch.next().await;
-            }
+            pump_loading_buffer_reciever(loading_watch)
+                .await
+                .map_err(|error| anyhow!("{}", error))
         })
     }
 
@@ -2565,6 +2560,23 @@ impl Project {
                 }
             }
 
+            for buffer in self.opened_buffers.values() {
+                if let Some(buffer) = buffer.upgrade(cx) {
+                    buffer.update(cx, |buffer, cx| {
+                        buffer.update_diagnostics(server_id, Default::default(), cx);
+                    });
+                }
+            }
+            for worktree in &self.worktrees {
+                if let Some(worktree) = worktree.upgrade(cx) {
+                    worktree.update(cx, |worktree, cx| {
+                        if let Some(worktree) = worktree.as_local_mut() {
+                            worktree.clear_diagnostics_for_language_server(server_id, cx);
+                        }
+                    });
+                }
+            }
+
             self.language_server_statuses.remove(&server_id);
             cx.notify();
 
@@ -4805,6 +4817,51 @@ impl Project {
     ) {
         debug_assert!(worktree_handle.read(cx).is_local());
 
+        // Setup the pending buffers
+        let future_buffers = self
+            .loading_buffers_by_path
+            .iter()
+            .filter_map(|(path, receiver)| {
+                let path = &path.path;
+                let (work_directory, repo) = repos
+                    .iter()
+                    .find(|(work_directory, _)| path.starts_with(work_directory))?;
+
+                let repo_relative_path = path.strip_prefix(work_directory).log_err()?;
+
+                let receiver = receiver.clone();
+                let repo_ptr = repo.repo_ptr.clone();
+                let repo_relative_path = repo_relative_path.to_owned();
+                Some(async move {
+                    pump_loading_buffer_reciever(receiver)
+                        .await
+                        .ok()
+                        .map(|buffer| (buffer, repo_relative_path, repo_ptr))
+                })
+            })
+            .collect::<FuturesUnordered<_>>()
+            .filter_map(|result| async move {
+                let (buffer_handle, repo_relative_path, repo_ptr) = result?;
+
+                let lock = repo_ptr.lock();
+                lock.load_index_text(&repo_relative_path)
+                    .map(|diff_base| (diff_base, buffer_handle))
+            });
+
+        let update_diff_base_fn = update_diff_base(self);
+        cx.spawn(|_, mut cx| async move {
+            let diff_base_tasks = cx
+                .background()
+                .spawn(future_buffers.collect::<Vec<_>>())
+                .await;
+
+            for (diff_base, buffer) in diff_base_tasks.into_iter() {
+                update_diff_base_fn(Some(diff_base), buffer, &mut cx);
+            }
+        })
+        .detach();
+
+        // And the current buffers
         for (_, buffer) in &self.opened_buffers {
             if let Some(buffer) = buffer.upgrade(cx) {
                 let file = match File::from_dyn(buffer.read(cx).file()) {
@@ -4824,18 +4881,17 @@ impl Project {
                     .find(|(work_directory, _)| path.starts_with(work_directory))
                 {
                     Some(repo) => repo.clone(),
-                    None => return,
+                    None => continue,
                 };
 
                 let relative_repo = match path.strip_prefix(work_directory).log_err() {
                     Some(relative_repo) => relative_repo.to_owned(),
-                    None => return,
+                    None => continue,
                 };
 
                 drop(worktree);
 
-                let remote_id = self.remote_id();
-                let client = self.client.clone();
+                let update_diff_base_fn = update_diff_base(self);
                 let git_ptr = repo.repo_ptr.clone();
                 let diff_base_task = cx
                     .background()
@@ -4843,21 +4899,7 @@ impl Project {
 
                 cx.spawn(|_, mut cx| async move {
                     let diff_base = diff_base_task.await;
-
-                    let buffer_id = buffer.update(&mut cx, |buffer, cx| {
-                        buffer.set_diff_base(diff_base.clone(), cx);
-                        buffer.remote_id()
-                    });
-
-                    if let Some(project_id) = remote_id {
-                        client
-                            .send(proto::UpdateDiffBase {
-                                project_id,
-                                buffer_id: buffer_id as u64,
-                                diff_base,
-                            })
-                            .log_err();
-                    }
+                    update_diff_base_fn(diff_base, buffer, &mut cx);
                 })
                 .detach();
             }
@@ -6747,3 +6789,40 @@ impl Item for Buffer {
         })
     }
 }
+
+async fn pump_loading_buffer_reciever(
+    mut receiver: postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
+) -> Result<ModelHandle<Buffer>, Arc<anyhow::Error>> {
+    loop {
+        if let Some(result) = receiver.borrow().as_ref() {
+            match result {
+                Ok(buffer) => return Ok(buffer.to_owned()),
+                Err(e) => return Err(e.to_owned()),
+            }
+        }
+        receiver.next().await;
+    }
+}
+
+fn update_diff_base(
+    project: &Project,
+) -> impl Fn(Option<String>, ModelHandle<Buffer>, &mut AsyncAppContext) {
+    let remote_id = project.remote_id();
+    let client = project.client().clone();
+    move |diff_base, buffer, cx| {
+        let buffer_id = buffer.update(cx, |buffer, cx| {
+            buffer.set_diff_base(diff_base.clone(), cx);
+            buffer.remote_id()
+        });
+
+        if let Some(project_id) = remote_id {
+            client
+                .send(proto::UpdateDiffBase {
+                    project_id,
+                    buffer_id: buffer_id as u64,
+                    diff_base,
+                })
+                .log_err();
+        }
+    }
+}

crates/project/src/project_settings.rs πŸ”—

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 use settings::Setting;
 use std::sync::Arc;
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ProjectSettings {
     #[serde(default)]
     pub lsp: HashMap<Arc<str>, LspSettings>,

crates/project/src/project_tests.rs πŸ”—

@@ -926,6 +926,95 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
     });
 }
 
+#[gpui::test]
+async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        None,
+    );
+    let mut fake_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            ..Default::default()
+        }))
+        .await;
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
+
+    let project = Project::test(fs, ["/dir".as_ref()], cx).await;
+    project.update(cx, |project, _| project.languages.add(Arc::new(language)));
+
+    let buffer = project
+        .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
+        .await
+        .unwrap();
+
+    // Publish diagnostics
+    let fake_server = fake_servers.next().await.unwrap();
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
+        uri: Url::from_file_path("/dir/a.rs").unwrap(),
+        version: None,
+        diagnostics: vec![lsp::Diagnostic {
+            range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
+            severity: Some(lsp::DiagnosticSeverity::ERROR),
+            message: "the message".to_string(),
+            ..Default::default()
+        }],
+    });
+
+    cx.foreground().run_until_parked();
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..1, false)
+                .map(|entry| entry.diagnostic.message.clone())
+                .collect::<Vec<_>>(),
+            ["the message".to_string()]
+        );
+    });
+    project.read_with(cx, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 1,
+                warning_count: 0,
+            }
+        );
+    });
+
+    project.update(cx, |project, cx| {
+        project.restart_language_servers_for_buffers([buffer.clone()], cx);
+    });
+
+    // The diagnostics are cleared.
+    cx.foreground().run_until_parked();
+    buffer.read_with(cx, |buffer, _| {
+        assert_eq!(
+            buffer
+                .snapshot()
+                .diagnostics_in_range::<_, usize>(0..1, false)
+                .map(|entry| entry.diagnostic.message.clone())
+                .collect::<Vec<_>>(),
+            Vec::<String>::new(),
+        );
+    });
+    project.read_with(cx, |project, cx| {
+        assert_eq!(
+            project.diagnostic_summary(cx),
+            DiagnosticSummary {
+                error_count: 0,
+                warning_count: 0,
+            }
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/project/src/worktree.rs πŸ”—

@@ -329,7 +329,7 @@ pub struct LocalMutableSnapshot {
 #[derive(Debug, Clone)]
 pub struct LocalRepositoryEntry {
     pub(crate) scan_id: usize,
-    pub(crate) full_scan_id: usize,
+    pub(crate) git_dir_scan_id: usize,
     pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
     /// Path to the actual .git folder.
     /// Note: if .git is a file, this points to the folder indicated by the .git file
@@ -737,6 +737,45 @@ impl LocalWorktree {
         self.diagnostics.get(path).cloned().unwrap_or_default()
     }
 
+    pub fn clear_diagnostics_for_language_server(
+        &mut self,
+        server_id: LanguageServerId,
+        _: &mut ModelContext<Worktree>,
+    ) {
+        let worktree_id = self.id().to_proto();
+        self.diagnostic_summaries
+            .retain(|path, summaries_by_server_id| {
+                if summaries_by_server_id.remove(&server_id).is_some() {
+                    if let Some(share) = self.share.as_ref() {
+                        self.client
+                            .send(proto::UpdateDiagnosticSummary {
+                                project_id: share.project_id,
+                                worktree_id,
+                                summary: Some(proto::DiagnosticSummary {
+                                    path: path.to_string_lossy().to_string(),
+                                    language_server_id: server_id.0 as u64,
+                                    error_count: 0,
+                                    warning_count: 0,
+                                }),
+                            })
+                            .log_err();
+                    }
+                    !summaries_by_server_id.is_empty()
+                } else {
+                    true
+                }
+            });
+
+        self.diagnostics.retain(|_, diagnostics_by_server_id| {
+            if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
+                diagnostics_by_server_id.remove(ix);
+                !diagnostics_by_server_id.is_empty()
+            } else {
+                true
+            }
+        });
+    }
+
     pub fn update_diagnostics(
         &mut self,
         server_id: LanguageServerId,
@@ -800,6 +839,7 @@ impl LocalWorktree {
     fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
         let updated_repos =
             self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories);
+
         self.snapshot = new_snapshot;
 
         if let Some(share) = self.share.as_mut() {
@@ -830,7 +870,7 @@ impl LocalWorktree {
                             old_repos.next();
                         }
                         Ordering::Equal => {
-                            if old_repo.scan_id != new_repo.scan_id {
+                            if old_repo.git_dir_scan_id != new_repo.git_dir_scan_id {
                                 if let Some(entry) = self.entry_for_id(**new_entry_id) {
                                     diff.insert(entry.path.clone(), (*new_repo).clone());
                                 }
@@ -2006,7 +2046,7 @@ impl LocalSnapshot {
                 work_dir_id,
                 LocalRepositoryEntry {
                     scan_id,
-                    full_scan_id: scan_id,
+                    git_dir_scan_id: scan_id,
                     repo_ptr: repo,
                     git_dir_path: parent_path.clone(),
                 },
@@ -3166,7 +3206,7 @@ impl BackgroundScanner {
                     snapshot.build_repo(dot_git_dir.into(), fs);
                     return None;
                 };
-                if repo.full_scan_id == scan_id {
+                if repo.git_dir_scan_id == scan_id {
                     return None;
                 }
                 (*entry_id, repo.repo_ptr.to_owned())
@@ -3183,7 +3223,7 @@ impl BackgroundScanner {
 
             snapshot.git_repositories.update(&entry_id, |entry| {
                 entry.scan_id = scan_id;
-                entry.full_scan_id = scan_id;
+                entry.git_dir_scan_id = scan_id;
             });
 
             snapshot.repository_entries.update(&work_dir, |entry| {
@@ -3212,7 +3252,7 @@ impl BackgroundScanner {
             let local_repo = snapshot.get_local_repo(&repo)?.to_owned();
 
             // Short circuit if we've already scanned everything
-            if local_repo.full_scan_id == scan_id {
+            if local_repo.git_dir_scan_id == scan_id {
                 return None;
             }
 

crates/project_panel/Cargo.toml πŸ”—

@@ -22,9 +22,11 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 postage.workspace = true
 futures.workspace = true
-schemars.workspace = true
 serde.workspace = true
+serde_derive.workspace = true
 serde_json.workspace = true
+anyhow.workspace = true
+schemars.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]

crates/project_panel/src/project_panel.rs πŸ”—

@@ -1,3 +1,5 @@
+mod project_panel_settings;
+
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
 use drag_and_drop::{DragAndDrop, Draggable};
@@ -7,7 +9,7 @@ use gpui::{
     actions,
     anyhow::{self, anyhow, Result},
     elements::{
-        AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
+        AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
         ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
@@ -21,7 +23,7 @@ use project::{
     repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
     Worktree, WorktreeId,
 };
-use schemars::JsonSchema;
+use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
 use serde::{Deserialize, Serialize};
 use settings::SettingsStore;
 use std::{
@@ -32,7 +34,7 @@ use std::{
     path::Path,
     sync::Arc,
 };
-use theme::{ui::FileName, ProjectPanelEntry};
+use theme::ProjectPanelEntry;
 use unicase::UniCase;
 use util::{ResultExt, TryFutureExt};
 use workspace::{
@@ -43,39 +45,6 @@ use workspace::{
 const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
 const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
 
-#[derive(Deserialize)]
-pub struct ProjectPanelSettings {
-    dock: ProjectPanelDockPosition,
-    default_width: f32,
-}
-
-impl settings::Setting for ProjectPanelSettings {
-    const KEY: Option<&'static str> = Some("project_panel");
-
-    type FileContent = ProjectPanelSettingsContent;
-
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &AppContext,
-    ) -> Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct ProjectPanelSettingsContent {
-    dock: Option<ProjectPanelDockPosition>,
-    default_width: Option<f32>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ProjectPanelDockPosition {
-    Left,
-    Right,
-}
-
 pub struct ProjectPanel {
     project: ModelHandle<Project>,
     fs: Arc<dyn Fs>,
@@ -156,8 +125,12 @@ actions!(
     ]
 );
 
-pub fn init(cx: &mut AppContext) {
+pub fn init_settings(cx: &mut AppContext) {
     settings::register::<ProjectPanelSettings>(cx);
+}
+
+pub fn init(cx: &mut AppContext) {
+    init_settings(cx);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
     cx.add_action(ProjectPanel::select_prev);
@@ -1116,6 +1089,7 @@ impl ProjectPanel {
             }
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
+            let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
             if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
                 let snapshot = worktree.read(cx).snapshot();
                 let root_name = OsStr::new(snapshot.root_name());
@@ -1129,7 +1103,9 @@ impl ProjectPanel {
                 for (entry, repo) in
                     snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter())
                 {
-                    let status = (entry.path.parent().is_some() && !entry.is_ignored)
+                    let status = (git_status_setting
+                        && entry.path.parent().is_some()
+                        && !entry.is_ignored)
                         .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
                         .flatten();
 
@@ -1195,6 +1171,17 @@ impl ProjectPanel {
         let kind = details.kind;
         let show_editor = details.is_editing && !details.is_processing;
 
+        let mut filename_text_style = style.text.clone();
+        filename_text_style.color = details
+            .git_status
+            .as_ref()
+            .map(|status| match status {
+                GitFileStatus::Added => style.status.git.inserted,
+                GitFileStatus::Modified => style.status.git.modified,
+                GitFileStatus::Conflict => style.status.git.conflict,
+            })
+            .unwrap_or(style.text.color);
+
         Flex::row()
             .with_child(
                 if kind == EntryKind::Dir {
@@ -1222,16 +1209,12 @@ impl ProjectPanel {
                     .flex(1.0, true)
                     .into_any()
             } else {
-                ComponentHost::new(FileName::new(
-                    details.filename.clone(),
-                    details.git_status,
-                    FileName::style(style.text.clone(), &theme::current(cx)),
-                ))
-                .contained()
-                .with_margin_left(style.icon_spacing)
-                .aligned()
-                .left()
-                .into_any()
+                Label::new(details.filename.clone(), filename_text_style)
+                    .contained()
+                    .with_margin_left(style.icon_spacing)
+                    .aligned()
+                    .left()
+                    .into_any()
             })
             .constrained()
             .with_height(style.height)
@@ -2240,6 +2223,7 @@ mod tests {
         cx.foreground().forbid_parking();
         cx.update(|cx| {
             cx.set_global(SettingsStore::test(cx));
+            init_settings(cx);
             theme::init((), cx);
             language::init(cx);
             editor::init_settings(cx);
@@ -2253,6 +2237,7 @@ mod tests {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
             theme::init((), cx);
+            init_settings(cx);
             language::init(cx);
             editor::init(cx);
             pane::init(cx);

crates/project_panel/src/project_panel_settings.rs πŸ”—

@@ -0,0 +1,39 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ProjectPanelDockPosition {
+    Left,
+    Right,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ProjectPanelSettings {
+    pub git_status: bool,
+    pub dock: ProjectPanelDockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectPanelSettingsContent {
+    pub git_status: Option<bool>,
+    pub dock: Option<ProjectPanelDockPosition>,
+    pub default_width: Option<f32>,
+}
+
+impl Setting for ProjectPanelSettings {
+    const KEY: Option<&'static str> = Some("project_panel");
+
+    type FileContent = ProjectPanelSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/search/src/project_search.rs πŸ”—

@@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
-    cx.add_action(ProjectSearchBar::toggle_focus);
+    cx.add_action(ProjectSearchBar::move_focus_to_results);
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
@@ -794,18 +794,16 @@ impl ProjectSearchBar {
         }
     }
 
-    fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
+    fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
         if let Some(search_view) = pane
             .active_item()
             .and_then(|item| item.downcast::<ProjectSearchView>())
         {
             search_view.update(cx, |search_view, cx| {
-                if search_view.query_editor.is_focused(cx) {
-                    if !search_view.model.read(cx).match_ranges.is_empty() {
-                        search_view.focus_results_editor(cx);
-                    }
-                } else {
-                    search_view.focus_query_editor(cx);
+                if search_view.query_editor.is_focused(cx)
+                    && !search_view.model.read(cx).match_ranges.is_empty()
+                {
+                    search_view.focus_results_editor(cx);
                 }
             });
         } else {

crates/settings/src/settings_store.rs πŸ”—

@@ -25,7 +25,7 @@ pub trait Setting: 'static {
     const KEY: Option<&'static str>;
 
     /// The type that is stored in an individual JSON file.
-    type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
+    type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema;
 
     /// The logic for combining together values from one or more JSON files into the
     /// final value for this setting.
@@ -460,11 +460,12 @@ impl SettingsStore {
 
                 // If the global settings file changed, reload the global value for the field.
                 if changed_local_path.is_none() {
-                    setting_value.set_global_value(setting_value.load_setting(
-                        &default_settings,
-                        &user_settings_stack,
-                        cx,
-                    )?);
+                    if let Some(value) = setting_value
+                        .load_setting(&default_settings, &user_settings_stack, cx)
+                        .log_err()
+                    {
+                        setting_value.set_global_value(value);
+                    }
                 }
 
                 // Reload the local values for the setting.
@@ -495,14 +496,12 @@ impl SettingsStore {
                             continue;
                         }
 
-                        setting_value.set_local_value(
-                            path.clone(),
-                            setting_value.load_setting(
-                                &default_settings,
-                                &user_settings_stack,
-                                cx,
-                            )?,
-                        );
+                        if let Some(value) = setting_value
+                            .load_setting(&default_settings, &user_settings_stack, cx)
+                            .log_err()
+                        {
+                            setting_value.set_local_value(path.clone(), value);
+                        }
                     }
                 }
             }
@@ -536,7 +535,12 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
 
     fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
         if let Some(key) = T::KEY {
-            json = json.get(key).unwrap_or(&serde_json::Value::Null);
+            if let Some(value) = json.get(key) {
+                json = value;
+            } else {
+                let value = T::FileContent::default();
+                return Ok(DeserializedSetting(Box::new(value)));
+            }
         }
         let value = T::FileContent::deserialize(json)?;
         Ok(DeserializedSetting(Box::new(value)))
@@ -826,37 +830,6 @@ mod tests {
         store.register_setting::<UserSettings>(cx);
         store.register_setting::<TurboSetting>(cx);
         store.register_setting::<MultiKeySettings>(cx);
-
-        // error - missing required field in default settings
-        store
-            .set_default_settings(
-                r#"{
-                    "user": {
-                        "name": "John Doe",
-                        "age": 30,
-                        "staff": false
-                    }
-                }"#,
-                cx,
-            )
-            .unwrap_err();
-
-        // error - type error in default settings
-        store
-            .set_default_settings(
-                r#"{
-                    "turbo": "the-wrong-type",
-                    "user": {
-                        "name": "John Doe",
-                        "age": 30,
-                        "staff": false
-                    }
-                }"#,
-                cx,
-            )
-            .unwrap_err();
-
-        // valid default settings.
         store
             .set_default_settings(
                 r#"{
@@ -1126,7 +1099,7 @@ mod tests {
         staff: bool,
     }
 
-    #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+    #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
     struct UserSettingsJson {
         name: Option<String>,
         age: Option<u32>,
@@ -1170,7 +1143,7 @@ mod tests {
         key2: String,
     }
 
-    #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+    #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
     struct MultiKeySettingsJson {
         key1: Option<String>,
         key2: Option<String>,
@@ -1203,7 +1176,7 @@ mod tests {
         Hour24,
     }
 
-    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
     struct JournalSettingsJson {
         pub path: Option<String>,
         pub hour_format: Option<HourFormat>,
@@ -1223,7 +1196,7 @@ mod tests {
         }
     }
 
-    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
     struct LanguageSettings {
         #[serde(default)]
         languages: HashMap<String, LanguageSettingEntry>,

crates/theme/src/theme.rs πŸ”—

@@ -438,6 +438,19 @@ pub struct ProjectPanelEntry {
     pub icon_color: Color,
     pub icon_size: f32,
     pub icon_spacing: f32,
+    pub status: EntryStatus,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct EntryStatus {
+    pub git: GitProjectStatus,
+}
+
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct GitProjectStatus {
+    pub modified: Color,
+    pub inserted: Color,
+    pub conflict: Color,
 }
 
 #[derive(Clone, Debug, Deserialize, Default)]
@@ -662,6 +675,14 @@ pub struct Scrollbar {
     pub thumb: ContainerStyle,
     pub width: f32,
     pub min_height_factor: f32,
+    pub git: GitDiffColors,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct GitDiffColors {
+    pub inserted: Color,
+    pub modified: Color,
+    pub deleted: Color,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/theme/src/ui.rs πŸ”—

@@ -1,10 +1,9 @@
 use std::borrow::Cow;
 
-use fs::repository::GitFileStatus;
 use gpui::{
     color::Color,
     elements::{
-        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle,
+        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
         MouseEventHandler, ParentElement, Stack, Svg,
     },
     fonts::TextStyle,
@@ -12,11 +11,11 @@ use gpui::{
     platform,
     platform::MouseButton,
     scene::MouseClick,
-    Action, AnyElement, Element, EventContext, MouseState, View, ViewContext,
+    Action, Element, EventContext, MouseState, View, ViewContext,
 };
 use serde::Deserialize;
 
-use crate::{ContainedText, Interactive, Theme};
+use crate::{ContainedText, Interactive};
 
 #[derive(Clone, Deserialize, Default)]
 pub struct CheckboxStyle {
@@ -253,53 +252,3 @@ where
         .constrained()
         .with_height(style.dimensions().y())
 }
-
-pub struct FileName {
-    filename: String,
-    git_status: Option<GitFileStatus>,
-    style: FileNameStyle,
-}
-
-pub struct FileNameStyle {
-    template_style: LabelStyle,
-    git_inserted: Color,
-    git_modified: Color,
-    git_deleted: Color,
-}
-
-impl FileName {
-    pub fn new(filename: String, git_status: Option<GitFileStatus>, style: FileNameStyle) -> Self {
-        FileName {
-            filename,
-            git_status,
-            style,
-        }
-    }
-
-    pub fn style<I: Into<LabelStyle>>(style: I, theme: &Theme) -> FileNameStyle {
-        FileNameStyle {
-            template_style: style.into(),
-            git_inserted: theme.editor.diff.inserted,
-            git_modified: theme.editor.diff.modified,
-            git_deleted: theme.editor.diff.deleted,
-        }
-    }
-}
-
-impl<V: View> gpui::elements::Component<V> for FileName {
-    fn render(&self, _: &mut V, _: &mut ViewContext<V>) -> AnyElement<V> {
-        // Prepare colors for git statuses
-        let mut filename_text_style = self.style.template_style.text.clone();
-        filename_text_style.color = self
-            .git_status
-            .as_ref()
-            .map(|status| match status {
-                GitFileStatus::Added => self.style.git_inserted,
-                GitFileStatus::Modified => self.style.git_modified,
-                GitFileStatus::Conflict => self.style.git_deleted,
-            })
-            .unwrap_or(self.style.template_style.text.color);
-
-        Label::new(self.filename.clone(), filename_text_style).into_any()
-    }
-}

crates/workspace/src/workspace_settings.rs πŸ”—

@@ -11,7 +11,7 @@ pub struct WorkspaceSettings {
     pub git: GitSettings,
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct WorkspaceSettingsContent {
     pub active_pane_magnification: Option<f32>,
     pub confirm_quit: Option<bool>,

crates/zed/src/zed.rs πŸ”—

@@ -2087,6 +2087,7 @@ mod tests {
             workspace::init(app_state.clone(), cx);
             language::init(cx);
             editor::init(cx);
+            project_panel::init_settings(cx);
             pane::init(cx);
             project_panel::init(cx);
             terminal_view::init(cx);

script/get-preview-channel-changes πŸ”—

@@ -69,9 +69,12 @@ async function main() {
     let releaseNotes = (pullRequest.body || "").split("Release Notes:")[1];
 
     if (releaseNotes) {
-      releaseNotes = releaseNotes.trim();
+      releaseNotes = releaseNotes.trim().split("\n")
       console.log("  Release Notes:");
-      console.log(`    ${releaseNotes}`);
+
+      for (const line of releaseNotes) {
+        console.log(`    ${line}`);
+      }
     }
 
     console.log()

styles/src/styleTree/editor.ts πŸ”—

@@ -6,6 +6,8 @@ import hoverPopover from "./hoverPopover"
 import { SyntaxHighlightStyle, buildSyntax } from "../themes/common/syntax"
 
 export default function editor(colorScheme: ColorScheme) {
+    const { isLight } = colorScheme
+
     let layer = colorScheme.highest
 
     const autocompleteItem = {
@@ -97,12 +99,18 @@ export default function editor(colorScheme: ColorScheme) {
             foldBackground: foreground(layer, "variant"),
         },
         diff: {
-            deleted: foreground(layer, "negative"),
-            modified: foreground(layer, "warning"),
-            inserted: foreground(layer, "positive"),
+            deleted: isLight
+                ? colorScheme.ramps.red(0.5).hex()
+                : colorScheme.ramps.red(0.4).hex(),
+            modified: isLight
+                ? colorScheme.ramps.yellow(0.3).hex()
+                : colorScheme.ramps.yellow(0.5).hex(),
+            inserted: isLight
+                ? colorScheme.ramps.green(0.4).hex()
+                : colorScheme.ramps.green(0.5).hex(),
             removedWidthEm: 0.275,
-            widthEm: 0.22,
-            cornerRadius: 0.2,
+            widthEm: 0.15,
+            cornerRadius: 0.05,
         },
         /** Highlights matching occurences of what is under the cursor
          * as well as matched brackets
@@ -234,12 +242,27 @@ export default function editor(colorScheme: ColorScheme) {
                 border: border(layer, "variant", { left: true }),
             },
             thumb: {
-                background: withOpacity(background(layer, "inverted"), 0.4),
+                background: withOpacity(background(layer, "inverted"), 0.3),
                 border: {
-                    width: 1,
-                    color: borderColor(layer, "variant"),
-                },
+                        width: 1,
+                        color: borderColor(layer, "variant"),
+                        top: false,
+                        right: true,
+                        left: true,
+                        bottom: false,
+                }
             },
+            git: {
+                deleted: isLight
+                    ? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8)
+                    : withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8),
+                modified: isLight
+                    ? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8)
+                    : withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8),
+                inserted: isLight
+                    ? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8)
+                    : withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8),
+            }
         },
         compositionMark: {
             underline: {

styles/src/styleTree/projectPanel.ts πŸ”—

@@ -3,6 +3,8 @@ import { withOpacity } from "../utils/color"
 import { background, border, foreground, text } from "./components"
 
 export default function projectPanel(colorScheme: ColorScheme) {
+    const { isLight } = colorScheme
+
     let layer = colorScheme.middle
 
     let baseEntry = {
@@ -12,6 +14,20 @@ export default function projectPanel(colorScheme: ColorScheme) {
         iconSpacing: 8,
     }
 
+    let status = {
+        git: {
+            modified: isLight
+                ? colorScheme.ramps.yellow(0.6).hex()
+                : colorScheme.ramps.yellow(0.5).hex(),
+            inserted: isLight
+                ? colorScheme.ramps.green(0.45).hex()
+                : colorScheme.ramps.green(0.5).hex(),
+            conflict: isLight
+                ? colorScheme.ramps.red(0.6).hex()
+                : colorScheme.ramps.red(0.5).hex()
+        }
+    }
+
     let entry = {
         ...baseEntry,
         text: text(layer, "mono", "variant", { size: "sm" }),
@@ -28,6 +44,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
             background: background(layer, "active"),
             text: text(layer, "mono", "active", { size: "sm" }),
         },
+        status
     }
 
     return {
@@ -62,6 +79,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
             text: text(layer, "mono", "on", { size: "sm" }),
             background: withOpacity(background(layer, "on"), 0.9),
             border: border(layer),
+            status
         },
         ignoredEntry: {
             ...entry,