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

This commit is contained in:
Antonio Scandurra 2023-05-23 08:24:28 +02:00
commit 208ff2fba7
33 changed files with 827 additions and 309 deletions

View file

@ -2,4 +2,12 @@
Release Notes: 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.

2
Cargo.lock generated
View file

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

View file

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

View file

@ -52,7 +52,9 @@
// 3. Draw all invisible symbols: // 3. Draw all invisible symbols:
// "all" // "all"
"show_whitespaces": "selection", "show_whitespaces": "selection",
// Whether to show the scrollbar in the editor. // Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
// This setting can take four values: // This setting can take four values:
// //
// 1. Show the scrollbar if there's important information or // 1. Show the scrollbar if there's important information or
@ -64,7 +66,18 @@
// "always" // "always"
// 4. Never show the scrollbar: // 4. Never show the scrollbar:
// "never" // "never"
"show_scrollbars": "auto", "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. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
// Whether to use language servers to provide code intelligence. // Whether to use language servers to provide code intelligence.
@ -128,13 +141,6 @@
}, },
// Automatically update Zed // Automatically update Zed
"auto_update": true, "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 gutter behavior configuration.
"git": { "git": {
// Control whether the git gutter is shown. May take 2 values: // Control whether the git gutter is shown. May take 2 values:

View file

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

View file

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

View file

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

View file

@ -216,6 +216,8 @@ actions!(
MoveToNextSubwordEnd, MoveToNextSubwordEnd,
MoveToBeginningOfLine, MoveToBeginningOfLine,
MoveToEndOfLine, MoveToEndOfLine,
MoveToStartOfParagraph,
MoveToEndOfParagraph,
MoveToBeginning, MoveToBeginning,
MoveToEnd, MoveToEnd,
SelectUp, SelectUp,
@ -226,6 +228,8 @@ actions!(
SelectToPreviousSubwordStart, SelectToPreviousSubwordStart,
SelectToNextWordEnd, SelectToNextWordEnd,
SelectToNextSubwordEnd, SelectToNextSubwordEnd,
SelectToStartOfParagraph,
SelectToEndOfParagraph,
SelectToBeginning, SelectToBeginning,
SelectToEnd, SelectToEnd,
SelectAll, 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_next_subword_end);
cx.add_action(Editor::move_to_beginning_of_line); cx.add_action(Editor::move_to_beginning_of_line);
cx.add_action(Editor::move_to_end_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_beginning);
cx.add_action(Editor::move_to_end); cx.add_action(Editor::move_to_end);
cx.add_action(Editor::select_up); 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_next_subword_end);
cx.add_action(Editor::select_to_beginning_of_line); cx.add_action(Editor::select_to_beginning_of_line);
cx.add_action(Editor::select_to_end_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_beginning);
cx.add_action(Editor::select_to_end); cx.add_action(Editor::select_to_end);
cx.add_action(Editor::select_all); cx.add_action(Editor::select_all);
@ -525,15 +533,6 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll, 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)] #[derive(Clone, Debug)]
struct SelectionHistoryEntry { struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>, 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>) { pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) { if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action(); cx.propagate_action();
@ -7128,6 +7201,7 @@ pub enum Event {
BufferEdited, BufferEdited,
Edited, Edited,
Reparsed, Reparsed,
Focused,
Blurred, Blurred,
DirtyChanged, DirtyChanged,
Saved, Saved,
@ -7181,6 +7255,7 @@ impl View for Editor {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() { if cx.is_self_focused() {
let focused_event = EditorFocused(cx.handle()); let focused_event = EditorFocused(cx.handle());
cx.emit(Event::Focused);
cx.emit_global(focused_event); cx.emit_global(focused_event);
} }
if let Some(rename) = self.pending_rename.as_ref() { if let Some(rename) = self.pending_rename.as_ref() {

View file

@ -7,25 +7,36 @@ pub struct EditorSettings {
pub cursor_blink: bool, pub cursor_blink: bool,
pub hover_popover_enabled: bool, pub hover_popover_enabled: bool,
pub show_completions_on_input: 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")] #[serde(rename_all = "snake_case")]
pub enum ShowScrollbars { pub enum ShowScrollbar {
#[default]
Auto, Auto,
System, System,
Always, Always,
Never, Never,
} }
#[derive(Clone, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent { pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>, pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>, pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: 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 { impl Setting for EditorSettings {

View file

@ -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] #[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});

View file

@ -5,7 +5,7 @@ use super::{
}; };
use crate::{ use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
editor_settings::ShowScrollbars, editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk}, git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{ hover_popover::{
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@ -1052,7 +1052,8 @@ impl EditorElement {
..Default::default() ..Default::default()
}); });
let diff_style = theme::current(cx).editor.diff.clone(); 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 for hunk in layout
.position_map .position_map
.snapshot .snapshot
@ -1098,6 +1099,7 @@ impl EditorElement {
corner_radius: style.thumb.corner_radius, corner_radius: style.thumb.corner_radius,
}) })
} }
}
scene.push_quad(Quad { scene.push_quad(Quad {
bounds: thumb_bounds, bounds: thumb_bounds,
@ -2065,13 +2067,17 @@ impl Element<Editor> for EditorElement {
)); ));
} }
let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars { let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
ShowScrollbars::Auto => { let show_scrollbars = match scrollbar_settings.show {
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible() 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(), ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
ShowScrollbars::Always => true, ShowScrollbar::Always => true,
ShowScrollbars::Never => false, ShowScrollbar::Never => false,
}; };
let include_root = editor let include_root = editor
@ -2290,6 +2296,7 @@ impl Element<Editor> for EditorElement {
text_size, text_size,
scrollbar_row_range, scrollbar_row_range,
show_scrollbars, show_scrollbars,
is_singleton,
max_row, max_row,
gutter_margin, gutter_margin,
active_rows, active_rows,
@ -2445,6 +2452,7 @@ pub struct LayoutState {
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>, selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>, scrollbar_row_range: Range<f32>,
show_scrollbars: bool, show_scrollbars: bool,
is_singleton: bool,
max_row: u32, max_row: u32,
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>, context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
code_actions_indicator: Option<(u32, AnyElement<Editor>)>, code_actions_indicator: Option<(u32, AnyElement<Editor>)>,

View file

@ -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 /// 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 /// 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 /// of the candidate boundary location, and will be called with `\n` characters indicating the start

View file

@ -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>( pub fn git_diff_hunks_in_range_rev<'a>(
&'a self, &'a self,
row_range: Range<u32>, row_range: Range<u32>,

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@ use copilot::Copilot;
use futures::{ use futures::{
channel::mpsc::{self, UnboundedReceiver}, channel::mpsc::{self, UnboundedReceiver},
future::{try_join_all, Shared}, future::{try_join_all, Shared},
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
}; };
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
@ -1374,7 +1375,7 @@ impl Project {
return Task::ready(Ok(existing_buffer)); 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 // If the given path is already being loaded, then wait for that existing
// task to complete and return the same buffer. // task to complete and return the same buffer.
hash_map::Entry::Occupied(e) => e.get().clone(), hash_map::Entry::Occupied(e) => e.get().clone(),
@ -1405,15 +1406,9 @@ impl Project {
}; };
cx.foreground().spawn(async move { cx.foreground().spawn(async move {
loop { pump_loading_buffer_reciever(loading_watch)
if let Some(result) = loading_watch.borrow().as_ref() { .await
match result { .map_err(|error| anyhow!("{}", error))
Ok(buffer) => return Ok(buffer.clone()),
Err(error) => return Err(anyhow!("{}", error)),
}
}
loading_watch.next().await;
}
}) })
} }
@ -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); self.language_server_statuses.remove(&server_id);
cx.notify(); cx.notify();
@ -4805,6 +4817,51 @@ impl Project {
) { ) {
debug_assert!(worktree_handle.read(cx).is_local()); 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 { for (_, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) { if let Some(buffer) = buffer.upgrade(cx) {
let file = match File::from_dyn(buffer.read(cx).file()) { let file = match File::from_dyn(buffer.read(cx).file()) {
@ -4824,18 +4881,17 @@ impl Project {
.find(|(work_directory, _)| path.starts_with(work_directory)) .find(|(work_directory, _)| path.starts_with(work_directory))
{ {
Some(repo) => repo.clone(), Some(repo) => repo.clone(),
None => return, None => continue,
}; };
let relative_repo = match path.strip_prefix(work_directory).log_err() { let relative_repo = match path.strip_prefix(work_directory).log_err() {
Some(relative_repo) => relative_repo.to_owned(), Some(relative_repo) => relative_repo.to_owned(),
None => return, None => continue,
}; };
drop(worktree); drop(worktree);
let remote_id = self.remote_id(); let update_diff_base_fn = update_diff_base(self);
let client = self.client.clone();
let git_ptr = repo.repo_ptr.clone(); let git_ptr = repo.repo_ptr.clone();
let diff_base_task = cx let diff_base_task = cx
.background() .background()
@ -4843,21 +4899,7 @@ impl Project {
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let diff_base = diff_base_task.await; let diff_base = diff_base_task.await;
update_diff_base_fn(diff_base, buffer, &mut cx);
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();
}
}) })
.detach(); .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();
}
}
}

View file

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

View file

@ -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] #[gpui::test]
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);

View file

@ -329,7 +329,7 @@ pub struct LocalMutableSnapshot {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LocalRepositoryEntry { pub struct LocalRepositoryEntry {
pub(crate) scan_id: usize, 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>>, pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
/// Path to the actual .git folder. /// Path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file /// 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() 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( pub fn update_diagnostics(
&mut self, &mut self,
server_id: LanguageServerId, server_id: LanguageServerId,
@ -800,6 +839,7 @@ impl LocalWorktree {
fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) { fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
let updated_repos = let updated_repos =
self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories); self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories);
self.snapshot = new_snapshot; self.snapshot = new_snapshot;
if let Some(share) = self.share.as_mut() { if let Some(share) = self.share.as_mut() {
@ -830,7 +870,7 @@ impl LocalWorktree {
old_repos.next(); old_repos.next();
} }
Ordering::Equal => { 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) { if let Some(entry) = self.entry_for_id(**new_entry_id) {
diff.insert(entry.path.clone(), (*new_repo).clone()); diff.insert(entry.path.clone(), (*new_repo).clone());
} }
@ -2006,7 +2046,7 @@ impl LocalSnapshot {
work_dir_id, work_dir_id,
LocalRepositoryEntry { LocalRepositoryEntry {
scan_id, scan_id,
full_scan_id: scan_id, git_dir_scan_id: scan_id,
repo_ptr: repo, repo_ptr: repo,
git_dir_path: parent_path.clone(), git_dir_path: parent_path.clone(),
}, },
@ -3166,7 +3206,7 @@ impl BackgroundScanner {
snapshot.build_repo(dot_git_dir.into(), fs); snapshot.build_repo(dot_git_dir.into(), fs);
return None; return None;
}; };
if repo.full_scan_id == scan_id { if repo.git_dir_scan_id == scan_id {
return None; return None;
} }
(*entry_id, repo.repo_ptr.to_owned()) (*entry_id, repo.repo_ptr.to_owned())
@ -3183,7 +3223,7 @@ impl BackgroundScanner {
snapshot.git_repositories.update(&entry_id, |entry| { snapshot.git_repositories.update(&entry_id, |entry| {
entry.scan_id = scan_id; 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| { snapshot.repository_entries.update(&work_dir, |entry| {
@ -3212,7 +3252,7 @@ impl BackgroundScanner {
let local_repo = snapshot.get_local_repo(&repo)?.to_owned(); let local_repo = snapshot.get_local_repo(&repo)?.to_owned();
// Short circuit if we've already scanned everything // 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; return None;
} }

View file

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

View file

@ -1,3 +1,5 @@
mod project_panel_settings;
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use drag_and_drop::{DragAndDrop, Draggable}; use drag_and_drop::{DragAndDrop, Draggable};
@ -7,7 +9,7 @@ use gpui::{
actions, actions,
anyhow::{self, anyhow, Result}, anyhow::{self, anyhow, Result},
elements::{ elements::{
AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler, AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
}, },
geometry::vector::Vector2F, geometry::vector::Vector2F,
@ -21,7 +23,7 @@ use project::{
repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
Worktree, WorktreeId, Worktree, WorktreeId,
}; };
use schemars::JsonSchema; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::SettingsStore; use settings::SettingsStore;
use std::{ use std::{
@ -32,7 +34,7 @@ use std::{
path::Path, path::Path,
sync::Arc, sync::Arc,
}; };
use theme::{ui::FileName, ProjectPanelEntry}; use theme::ProjectPanelEntry;
use unicase::UniCase; use unicase::UniCase;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
@ -43,39 +45,6 @@ use workspace::{
const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; 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 { pub struct ProjectPanel {
project: ModelHandle<Project>, project: ModelHandle<Project>,
fs: Arc<dyn Fs>, 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); settings::register::<ProjectPanelSettings>(cx);
}
pub fn init(cx: &mut AppContext) {
init_settings(cx);
cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_prev);
@ -1116,6 +1089,7 @@ impl ProjectPanel {
} }
let end_ix = range.end.min(ix + visible_worktree_entries.len()); 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) { if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot(); let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name()); let root_name = OsStr::new(snapshot.root_name());
@ -1129,7 +1103,9 @@ impl ProjectPanel {
for (entry, repo) in for (entry, repo) in
snapshot.entries_with_repositories(visible_worktree_entries[entry_range].iter()) 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))) .then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
.flatten(); .flatten();
@ -1195,6 +1171,17 @@ impl ProjectPanel {
let kind = details.kind; let kind = details.kind;
let show_editor = details.is_editing && !details.is_processing; 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() Flex::row()
.with_child( .with_child(
if kind == EntryKind::Dir { if kind == EntryKind::Dir {
@ -1222,11 +1209,7 @@ impl ProjectPanel {
.flex(1.0, true) .flex(1.0, true)
.into_any() .into_any()
} else { } else {
ComponentHost::new(FileName::new( Label::new(details.filename.clone(), filename_text_style)
details.filename.clone(),
details.git_status,
FileName::style(style.text.clone(), &theme::current(cx)),
))
.contained() .contained()
.with_margin_left(style.icon_spacing) .with_margin_left(style.icon_spacing)
.aligned() .aligned()
@ -2240,6 +2223,7 @@ mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
cx.update(|cx| { cx.update(|cx| {
cx.set_global(SettingsStore::test(cx)); cx.set_global(SettingsStore::test(cx));
init_settings(cx);
theme::init((), cx); theme::init((), cx);
language::init(cx); language::init(cx);
editor::init_settings(cx); editor::init_settings(cx);
@ -2253,6 +2237,7 @@ mod tests {
cx.update(|cx| { cx.update(|cx| {
let app_state = AppState::test(cx); let app_state = AppState::test(cx);
theme::init((), cx); theme::init((), cx);
init_settings(cx);
language::init(cx); language::init(cx);
editor::init(cx); editor::init(cx);
pane::init(cx); pane::init(cx);

View file

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

View file

@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_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);
cx.capture_action(ProjectSearchBar::tab_previous); cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
@ -794,19 +794,17 @@ 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 if let Some(search_view) = pane
.active_item() .active_item()
.and_then(|item| item.downcast::<ProjectSearchView>()) .and_then(|item| item.downcast::<ProjectSearchView>())
{ {
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) { if search_view.query_editor.is_focused(cx)
if !search_view.model.read(cx).match_ranges.is_empty() { && !search_view.model.read(cx).match_ranges.is_empty()
{
search_view.focus_results_editor(cx); search_view.focus_results_editor(cx);
} }
} else {
search_view.focus_query_editor(cx);
}
}); });
} else { } else {
cx.propagate_action(); cx.propagate_action();

View file

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

View file

@ -438,6 +438,19 @@ pub struct ProjectPanelEntry {
pub icon_color: Color, pub icon_color: Color,
pub icon_size: f32, pub icon_size: f32,
pub icon_spacing: 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)] #[derive(Clone, Debug, Deserialize, Default)]
@ -662,6 +675,14 @@ pub struct Scrollbar {
pub thumb: ContainerStyle, pub thumb: ContainerStyle,
pub width: f32, pub width: f32,
pub min_height_factor: 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)] #[derive(Clone, Deserialize, Default)]

View file

@ -1,10 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use fs::repository::GitFileStatus;
use gpui::{ use gpui::{
color::Color, color::Color,
elements::{ elements::{
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, LabelStyle, ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
MouseEventHandler, ParentElement, Stack, Svg, MouseEventHandler, ParentElement, Stack, Svg,
}, },
fonts::TextStyle, fonts::TextStyle,
@ -12,11 +11,11 @@ use gpui::{
platform, platform,
platform::MouseButton, platform::MouseButton,
scene::MouseClick, scene::MouseClick,
Action, AnyElement, Element, EventContext, MouseState, View, ViewContext, Action, Element, EventContext, MouseState, View, ViewContext,
}; };
use serde::Deserialize; use serde::Deserialize;
use crate::{ContainedText, Interactive, Theme}; use crate::{ContainedText, Interactive};
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
pub struct CheckboxStyle { pub struct CheckboxStyle {
@ -253,53 +252,3 @@ where
.constrained() .constrained()
.with_height(style.dimensions().y()) .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()
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,8 @@ import { withOpacity } from "../utils/color"
import { background, border, foreground, text } from "./components" import { background, border, foreground, text } from "./components"
export default function projectPanel(colorScheme: ColorScheme) { export default function projectPanel(colorScheme: ColorScheme) {
const { isLight } = colorScheme
let layer = colorScheme.middle let layer = colorScheme.middle
let baseEntry = { let baseEntry = {
@ -12,6 +14,20 @@ export default function projectPanel(colorScheme: ColorScheme) {
iconSpacing: 8, 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 = { let entry = {
...baseEntry, ...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }), text: text(layer, "mono", "variant", { size: "sm" }),
@ -28,6 +44,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
background: background(layer, "active"), background: background(layer, "active"),
text: text(layer, "mono", "active", { size: "sm" }), text: text(layer, "mono", "active", { size: "sm" }),
}, },
status
} }
return { return {
@ -62,6 +79,7 @@ export default function projectPanel(colorScheme: ColorScheme) {
text: text(layer, "mono", "on", { size: "sm" }), text: text(layer, "mono", "on", { size: "sm" }),
background: withOpacity(background(layer, "on"), 0.9), background: withOpacity(background(layer, "on"), 0.9),
border: border(layer), border: border(layer),
status
}, },
ignoredEntry: { ignoredEntry: {
...entry, ...entry,