diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8d16a59bc12852ec9f06f3efa7ff43e0d333c038..67a7ed31c9b5a4cfbf3ab6881c627b1a9db126c5 100644 --- a/.github/pull_request_template.md +++ b/.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) ... ([#](https://github.com/zed-industries/community/issues/)). +* ... + +If the release notes are only intended for a specific release channel only, add `(-only)` to the end of the release note line. +These will be removed by the person making the release. diff --git a/Cargo.lock b/Cargo.lock index b8b57e8b8dde611aae3689ae1d30dd0a9ec1567a..35ea2eefd8b41c1208df3014aa3ec389c4e5c7a3 100644 --- a/Cargo.lock +++ b/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", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 02f6a2b0fa29814d85caf5dc65420376ba96a134..987c6cf1054fd58c3cb40086669df8b2d3fd1e50 100644 --- a/assets/keymaps/default.json +++ b/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", diff --git a/assets/settings/default.json b/assets/settings/default.json index 3e4c59e806784d5dc31cbb808046093983fa0022..246e28cc8e20c57c60179f0bfb2ce572d6c27c42 100644 --- a/assets/settings/default.json +++ b/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: diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 311d9a2b8872cd3d63ad861cc986849d93d1e240..c9b83d805a4a87a65eda95f5b765c35841627d30 100644 --- a/crates/client/src/client.rs +++ b/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, pub metrics: Option, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 807510d70555ed446d9ecf9d9bc40a6aa652f162..439ee0786a1b850bb68ffef539be1651f8ca0480 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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) }); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a202a6082c0522901a349c9537e49a2fe7e2c6b8..182efdfdd6c520e30d285ed822f66f9a3f98a368 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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) -> AnyElement { 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 diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7207e3c91c6da9f1e1ecfb37b94a560c875127d3..ce67a59bd39238f819fa38d3ceb807842e664a9b 100644 --- a/crates/editor/src/editor.rs +++ b/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]>, @@ -4762,6 +4761,80 @@ impl Editor { }); } + pub fn move_to_start_of_paragraph( + &mut self, + _: &MoveToStartOfParagraph, + cx: &mut ViewContext, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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) { 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) { 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() { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 5108d2740875bd38dabea3c4ca06ca78784d1f35..7f01834b161b8f1db75a40145694f1c80e473755 100644 --- a/crates/editor/src/editor_settings.rs +++ b/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, pub hover_popover_enabled: Option, pub show_completions_on_input: Option, - pub show_scrollbars: Option, + pub scrollbar: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ScrollbarContent { + pub show: Option, + pub git_diff: Option, } impl Setting for EditorSettings { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9a21429301b6906e025f87c14659a8594279c11d..180de155e998fff1492c22311502d87ebcf744bf 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57dc3293f6f9dc322c1a7744f97b21000fb34c0c..4e5863407f208ae0d4b0b84030d1b21521164ec6 100644 --- a/crates/editor/src/element.rs +++ b/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::(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 for EditorElement { )); } - let show_scrollbars = match settings::get::(cx).show_scrollbars { - ShowScrollbars::Auto => { - snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible() + let scrollbar_settings = &settings::get::(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 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)>, scrollbar_row_range: Range, show_scrollbars: bool, + is_singleton: bool, max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option<(u32, AnyElement)>, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6c9bd6cb4fe1554d9a04a840db78ff8edc88b4c9..523a0af9640aa98b3f3e3d7b9fd980768f1e4f89 100644 --- a/crates/editor/src/movement.rs +++ b/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 diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 1423473e1ad8c3d952d6bb47ef95d20286e0a46f..6a617756974ee5a2021c1377ba2edb00468aa304 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/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, diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 8704f850055aa4edb5f03109ecd1fa5d18d4aa5c..8260dfc98d9834f8cefff6dfc274a4d7a9e237d7 100644 --- a/crates/git/src/diff.rs +++ b/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, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3a977024873baa82d39e50bae06a3e3e43b7f254..5539d1d941726148fc543a59652d57190e1d400d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1644,10 +1644,17 @@ impl Buffer { cx: &mut ModelContext, ) { 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); diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 948a7ee39471579a4cc9a8553b700ccf26c2b9ae..f269fce88d694c8efe2f255e302bbef7aed865ea 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/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> { self.diagnostics.iter() } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index d877304f1d991eeb22a58022f6c9723be71ba1cd..c98297c03648f7db9c307a592b4f7bf2dcfe279d 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -49,7 +49,7 @@ pub struct CopilotSettings { pub disabled_globs: Vec, } -#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct AllLanguageSettingsContent { #[serde(default)] pub features: Option, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f91cd999f9dd0068f98ae9f30fda3067469481d2..dd53c30d140534c3b15ea5a562b6ebc3c92fe2d6 100644 --- a/crates/project/src/project.rs +++ b/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::>() + .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::>()) + .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, Arc>>>, +) -> Result, Arc> { + 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, ModelHandle, &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(); + } + } +} diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 92e8cfcca79fc59f17a879c5c7ade0264a6b40bf..c542d1d13fd42c3cd2721c92981e74129556a554 100644 --- a/crates/project/src/project_settings.rs +++ b/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, LspSettings>, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 69bcea8ce0426f7fe812618542336f3a018262f3..e48ce6258b34f38f438dd6035a533a99a0be5d0a 100644 --- a/crates/project/src/project_tests.rs +++ b/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::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::>(), + ["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::::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); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 4f898aa91d2558c606b39dda29b83ea6ff24a3d8..d2c035f91675a779b84a4facfc6331091b481910 100644 --- a/crates/project/src/worktree.rs +++ b/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>, /// 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, + ) { + 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) { 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; } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index aab5b8a41c2d00a5d445576c07cc952c0d020b73..55efc09deb179437d837e7f50e1acbdb6613a2eb 100644 --- a/crates/project_panel/Cargo.toml +++ b/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] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9159e8fa98eb85006726d8e737161aea628afc35..32a8148cbb7562d36f4a3bb2c7a530f72b728363 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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::load_via_json_merge(default_value, user_values) - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct ProjectPanelSettingsContent { - dock: Option, - default_width: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelDockPosition { - Left, - Right, -} - pub struct ProjectPanel { project: ModelHandle, fs: Arc, @@ -156,8 +125,12 @@ actions!( ] ); -pub fn init(cx: &mut AppContext) { +pub fn init_settings(cx: &mut AppContext) { settings::register::(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::(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); diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..1d6c590710b5b783b6acbfe27e89903de5ad3f69 --- /dev/null +++ b/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, + pub dock: Option, + pub default_width: Option, +} + +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::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d96d77eb005aa099642f01f6499db60363caaf6d..915957401d440c40dd13d4b3364b0be76e09f3ef 100644 --- a/crates/search/src/project_search.rs +++ b/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::(SearchOption::CaseSensitive, cx); @@ -794,18 +794,16 @@ impl ProjectSearchBar { } } - fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { + fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { if let Some(search_view) = pane .active_item() .and_then(|item| item.downcast::()) { 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 { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index dd81b05434af71680f85f18b644839d7a6b47875..71b3cc635f4e03465d94cb498567c21bd36bd76a 100644 --- a/crates/settings/src/settings_store.rs +++ b/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 AnySettingValue for SettingValue { fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { 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::(cx); store.register_setting::(cx); store.register_setting::(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, age: Option, @@ -1170,7 +1143,7 @@ mod tests { key2: String, } - #[derive(Clone, Serialize, Deserialize, JsonSchema)] + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] struct MultiKeySettingsJson { key1: Option, key2: Option, @@ -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, pub hour_format: Option, @@ -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, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a62694ea350561a0e43ef16ac25b86a84b5eca15..31a9f5cc03ed840af905f3652cf1ecc70830ddfc 100644 --- a/crates/theme/src/theme.rs +++ b/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)] diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index e4df24c89fcf92842e663e8a50792709a19b7981..b86bfca8c42ae05900d76d86e19544531c245899 100644 --- a/crates/theme/src/ui.rs +++ b/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, - 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, style: FileNameStyle) -> Self { - FileName { - filename, - git_status, - style, - } - } - - pub fn style>(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 gpui::elements::Component for FileName { - fn render(&self, _: &mut V, _: &mut ViewContext) -> AnyElement { - // 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() - } -} diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index b21c687050ef65865cb56fb7a880421f7468746c..4202c00a8d7a9fb6384280977493896ab5906fbc 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/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, pub confirm_quit: Option, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4c4137b7f37979d4c12e2b89b71fae1f5804ea22..f9c0a1855ee099ca7c4575ec4ca63c8462aabca9 100644 --- a/crates/zed/src/zed.rs +++ b/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); diff --git a/script/get-preview-channel-changes b/script/get-preview-channel-changes index 47623125f9c28fa12898a21d1a629c69fcf6158e..5a0be3ed6669233bb50e001320259f09c626c288 100755 --- a/script/get-preview-channel-changes +++ b/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() diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 7caa8b1c674996822d91a0a3b120b44be6e44cbc..deeff855ff0f61dc8e51c9a8b8065d8a29b9c437 100644 --- a/styles/src/styleTree/editor.ts +++ b/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: { diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index 3d06a683abebf71d55311273f8e93f08876ea509..08117bf6b01d5fe7b14f2e0ace22dc680c5cefac 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/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,