Merge branch 'main' into git-project-panel-and-scrollbar-styling
This commit is contained in:
@@ -2,4 +2,12 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
* [[Added foo / Fixed bar / No notes]]
|
||||
Use `N/A` in this section if this item should be skipped in the release notes.
|
||||
|
||||
Add release note lines here:
|
||||
|
||||
* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
* ...
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
||||
@@ -67,10 +67,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",
|
||||
@@ -103,6 +105,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",
|
||||
|
||||
@@ -52,19 +52,24 @@
|
||||
// 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
|
||||
},
|
||||
// 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.
|
||||
|
||||
@@ -339,7 +339,7 @@ pub struct TelemetrySettings {
|
||||
pub metrics: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TelemetrySettingsContent {
|
||||
pub diagnostics: Option<bool>,
|
||||
pub metrics: Option<bool>,
|
||||
|
||||
@@ -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,19 +90,24 @@ impl View for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &theme::current(cx).project_diagnostics;
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.into_any()
|
||||
PaneBackdrop::new(
|
||||
cx.view_id(),
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.into_any(),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() && !self.path_states.is_empty() {
|
||||
cx.focus(&self.editor);
|
||||
dbg!("Focus in");
|
||||
if dbg!(cx.is_self_focused()) && dbg!(!self.path_states.is_empty()) {
|
||||
dbg!(cx.focus(&self.editor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +166,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
|
||||
|
||||
@@ -216,6 +216,8 @@ actions!(
|
||||
MoveToNextSubwordEnd,
|
||||
MoveToBeginningOfLine,
|
||||
MoveToEndOfLine,
|
||||
MoveToStartOfParagraph,
|
||||
MoveToEndOfParagraph,
|
||||
MoveToBeginning,
|
||||
MoveToEnd,
|
||||
SelectUp,
|
||||
@@ -226,6 +228,8 @@ actions!(
|
||||
SelectToPreviousSubwordStart,
|
||||
SelectToNextWordEnd,
|
||||
SelectToNextSubwordEnd,
|
||||
SelectToStartOfParagraph,
|
||||
SelectToEndOfParagraph,
|
||||
SelectToBeginning,
|
||||
SelectToEnd,
|
||||
SelectAll,
|
||||
@@ -337,6 +341,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::move_to_next_subword_end);
|
||||
cx.add_action(Editor::move_to_beginning_of_line);
|
||||
cx.add_action(Editor::move_to_end_of_line);
|
||||
cx.add_action(Editor::move_to_start_of_paragraph);
|
||||
cx.add_action(Editor::move_to_end_of_paragraph);
|
||||
cx.add_action(Editor::move_to_beginning);
|
||||
cx.add_action(Editor::move_to_end);
|
||||
cx.add_action(Editor::select_up);
|
||||
@@ -349,6 +355,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::select_to_next_subword_end);
|
||||
cx.add_action(Editor::select_to_beginning_of_line);
|
||||
cx.add_action(Editor::select_to_end_of_line);
|
||||
cx.add_action(Editor::select_to_start_of_paragraph);
|
||||
cx.add_action(Editor::select_to_end_of_paragraph);
|
||||
cx.add_action(Editor::select_to_beginning);
|
||||
cx.add_action(Editor::select_to_end);
|
||||
cx.add_action(Editor::select_all);
|
||||
@@ -525,15 +533,6 @@ pub struct EditorSnapshot {
|
||||
ongoing_scroll: OngoingScroll,
|
||||
}
|
||||
|
||||
impl EditorSnapshot {
|
||||
fn has_scrollbar_info(&self) -> bool {
|
||||
self.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..self.max_point().row())
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SelectionHistoryEntry {
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
@@ -4762,6 +4761,80 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn move_to_start_of_paragraph(
|
||||
&mut self,
|
||||
_: &MoveToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::start_of_paragraph(map, selection.head()),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_paragraph(
|
||||
&mut self,
|
||||
_: &MoveToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::end_of_paragraph(map, selection.head()),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_start_of_paragraph(
|
||||
&mut self,
|
||||
_: &SelectToStartOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::start_of_paragraph(map, head), SelectionGoal::None)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_to_end_of_paragraph(
|
||||
&mut self,
|
||||
_: &SelectToEndOfParagraph,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::end_of_paragraph(map, head), SelectionGoal::None)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
@@ -7128,6 +7201,7 @@ pub enum Event {
|
||||
BufferEdited,
|
||||
Edited,
|
||||
Reparsed,
|
||||
Focused,
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
@@ -7179,8 +7253,10 @@ impl View for Editor {
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
dbg!("Editor Focus in");
|
||||
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() {
|
||||
|
||||
@@ -7,25 +7,36 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub show_scrollbars: ShowScrollbars,
|
||||
pub scrollbar: Scrollbar,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Scrollbar {
|
||||
pub show: ShowScrollbar,
|
||||
pub git_diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShowScrollbars {
|
||||
#[default]
|
||||
pub enum ShowScrollbar {
|
||||
Auto,
|
||||
System,
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub show_scrollbars: Option<ShowScrollbars>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarContent {
|
||||
pub show: Option<ShowScrollbar>,
|
||||
pub git_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for EditorSettings {
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
@@ -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,54 @@ impl EditorElement {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let diff_style = &theme::current(cx).editor.scrollbar.git;
|
||||
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.;
|
||||
if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
|
||||
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 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,
|
||||
};
|
||||
|
||||
scene.push_quad(Quad {
|
||||
bounds,
|
||||
background: Some(color),
|
||||
border,
|
||||
corner_radius: style.thumb.corner_radius,
|
||||
})
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
||||
scene.push_quad(Quad {
|
||||
bounds,
|
||||
background: Some(color),
|
||||
border,
|
||||
corner_radius: style.thumb.corner_radius,
|
||||
})
|
||||
}
|
||||
|
||||
scene.push_quad(Quad {
|
||||
@@ -2065,13 +2068,17 @@ impl Element<Editor> for EditorElement {
|
||||
));
|
||||
}
|
||||
|
||||
let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
|
||||
ShowScrollbars::Auto => {
|
||||
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
|
||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||
let show_scrollbars = match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
|
||||
// Scrollmanager
|
||||
|| editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbars::Always => true,
|
||||
ShowScrollbars::Never => false,
|
||||
ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let include_root = editor
|
||||
@@ -2290,6 +2297,7 @@ impl Element<Editor> for EditorElement {
|
||||
text_size,
|
||||
scrollbar_row_range,
|
||||
show_scrollbars,
|
||||
is_singleton,
|
||||
max_row,
|
||||
gutter_margin,
|
||||
active_rows,
|
||||
@@ -2445,6 +2453,7 @@ pub struct LayoutState {
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
is_singleton: bool,
|
||||
max_row: u32,
|
||||
context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
|
||||
code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2841,6 +2841,15 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_git_diffs(&self) -> bool {
|
||||
for excerpt in self.excerpts.iter() {
|
||||
if !excerpt.buffer.git_diff.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range_rev<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
|
||||
@@ -71,6 +71,10 @@ impl BufferDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn hunks_in_row_range<'a>(
|
||||
&'a self,
|
||||
range: Range<u32>,
|
||||
|
||||
@@ -1644,10 +1644,17 @@ impl Buffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if lamport_timestamp > self.diagnostics_timestamp {
|
||||
match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
|
||||
Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
|
||||
Ok(ix) => self.diagnostics[ix].1 = diagnostics,
|
||||
};
|
||||
let ix = self.diagnostics.binary_search_by_key(&server_id, |e| e.0);
|
||||
if diagnostics.len() == 0 {
|
||||
if let Ok(ix) = ix {
|
||||
self.diagnostics.remove(ix);
|
||||
}
|
||||
} else {
|
||||
match ix {
|
||||
Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
|
||||
Ok(ix) => self.diagnostics[ix].1 = diagnostics,
|
||||
};
|
||||
}
|
||||
self.diagnostics_timestamp = lamport_timestamp;
|
||||
self.diagnostics_update_count += 1;
|
||||
self.text.lamport_clock.observe(lamport_timestamp);
|
||||
|
||||
@@ -80,6 +80,10 @@ impl DiagnosticSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.diagnostics.summary().count
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry<Anchor>> {
|
||||
self.diagnostics.iter()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ pub struct CopilotSettings {
|
||||
pub disabled_globs: Vec<GlobMatcher>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AllLanguageSettingsContent {
|
||||
#[serde(default)]
|
||||
pub features: Option<FeaturesContent>,
|
||||
|
||||
@@ -2565,6 +2565,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();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ProjectSettings {
|
||||
#[serde(default)]
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
|
||||
@@ -926,6 +926,95 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Publish diagnostics
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
|
||||
uri: Url::from_file_path("/dir/a.rs").unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "the message".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.snapshot()
|
||||
.diagnostics_in_range::<_, usize>(0..1, false)
|
||||
.map(|entry| entry.diagnostic.message.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
["the message".to_string()]
|
||||
);
|
||||
});
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summary(cx),
|
||||
DiagnosticSummary {
|
||||
error_count: 1,
|
||||
warning_count: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.restart_language_servers_for_buffers([buffer.clone()], cx);
|
||||
});
|
||||
|
||||
// The diagnostics are cleared.
|
||||
cx.foreground().run_until_parked();
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.snapshot()
|
||||
.diagnostics_in_range::<_, usize>(0..1, false)
|
||||
.map(|entry| entry.diagnostic.message.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::<String>::new(),
|
||||
);
|
||||
});
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summary(cx),
|
||||
DiagnosticSummary {
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -329,7 +329,7 @@ pub struct LocalMutableSnapshot {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalRepositoryEntry {
|
||||
pub(crate) scan_id: usize,
|
||||
pub(crate) full_scan_id: usize,
|
||||
pub(crate) git_dir_scan_id: usize,
|
||||
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
|
||||
/// Path to the actual .git folder.
|
||||
/// Note: if .git is a file, this points to the folder indicated by the .git file
|
||||
@@ -737,6 +737,45 @@ impl LocalWorktree {
|
||||
self.diagnostics.get(path).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn clear_diagnostics_for_language_server(
|
||||
&mut self,
|
||||
server_id: LanguageServerId,
|
||||
_: &mut ModelContext<Worktree>,
|
||||
) {
|
||||
let worktree_id = self.id().to_proto();
|
||||
self.diagnostic_summaries
|
||||
.retain(|path, summaries_by_server_id| {
|
||||
if summaries_by_server_id.remove(&server_id).is_some() {
|
||||
if let Some(share) = self.share.as_ref() {
|
||||
self.client
|
||||
.send(proto::UpdateDiagnosticSummary {
|
||||
project_id: share.project_id,
|
||||
worktree_id,
|
||||
summary: Some(proto::DiagnosticSummary {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
}),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
!summaries_by_server_id.is_empty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
self.diagnostics.retain(|_, diagnostics_by_server_id| {
|
||||
if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
|
||||
diagnostics_by_server_id.remove(ix);
|
||||
!diagnostics_by_server_id.is_empty()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_diagnostics(
|
||||
&mut self,
|
||||
server_id: LanguageServerId,
|
||||
@@ -830,7 +869,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 +2045,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 +3205,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 +3222,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 +3251,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ProjectSearchBar::search_in_new);
|
||||
cx.add_action(ProjectSearchBar::select_next_match);
|
||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||
cx.add_action(ProjectSearchBar::toggle_focus);
|
||||
cx.add_action(ProjectSearchBar::move_focus_to_results);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
@@ -794,18 +794,16 @@ impl ProjectSearchBar {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
|
||||
if let Some(search_view) = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<ProjectSearchView>())
|
||||
{
|
||||
search_view.update(cx, |search_view, cx| {
|
||||
if search_view.query_editor.is_focused(cx) {
|
||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
search_view.focus_query_editor(cx);
|
||||
if search_view.query_editor.is_focused(cx)
|
||||
&& !search_view.model.read(cx).match_ranges.is_empty()
|
||||
{
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait Setting: 'static {
|
||||
const KEY: Option<&'static str>;
|
||||
|
||||
/// The type that is stored in an individual JSON file.
|
||||
type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
|
||||
type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema;
|
||||
|
||||
/// The logic for combining together values from one or more JSON files into the
|
||||
/// final value for this setting.
|
||||
@@ -460,11 +460,12 @@ impl SettingsStore {
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
setting_value.set_global_value(setting_value.load_setting(
|
||||
&default_settings,
|
||||
&user_settings_stack,
|
||||
cx,
|
||||
)?);
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
@@ -495,14 +496,12 @@ impl SettingsStore {
|
||||
continue;
|
||||
}
|
||||
|
||||
setting_value.set_local_value(
|
||||
path.clone(),
|
||||
setting_value.load_setting(
|
||||
&default_settings,
|
||||
&user_settings_stack,
|
||||
cx,
|
||||
)?,
|
||||
);
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(path.clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,7 +535,12 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
|
||||
fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
|
||||
if let Some(key) = T::KEY {
|
||||
json = json.get(key).unwrap_or(&serde_json::Value::Null);
|
||||
if let Some(value) = json.get(key) {
|
||||
json = value;
|
||||
} else {
|
||||
let value = T::FileContent::default();
|
||||
return Ok(DeserializedSetting(Box::new(value)));
|
||||
}
|
||||
}
|
||||
let value = T::FileContent::deserialize(json)?;
|
||||
Ok(DeserializedSetting(Box::new(value)))
|
||||
@@ -826,37 +830,6 @@ mod tests {
|
||||
store.register_setting::<UserSettings>(cx);
|
||||
store.register_setting::<TurboSetting>(cx);
|
||||
store.register_setting::<MultiKeySettings>(cx);
|
||||
|
||||
// error - missing required field in default settings
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
"user": {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"staff": false
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// error - type error in default settings
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
"turbo": "the-wrong-type",
|
||||
"user": {
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"staff": false
|
||||
}
|
||||
}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// valid default settings.
|
||||
store
|
||||
.set_default_settings(
|
||||
r#"{
|
||||
@@ -1126,7 +1099,7 @@ mod tests {
|
||||
staff: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
struct UserSettingsJson {
|
||||
name: Option<String>,
|
||||
age: Option<u32>,
|
||||
@@ -1170,7 +1143,7 @@ mod tests {
|
||||
key2: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct MultiKeySettingsJson {
|
||||
key1: Option<String>,
|
||||
key2: Option<String>,
|
||||
@@ -1203,7 +1176,7 @@ mod tests {
|
||||
Hour24,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct JournalSettingsJson {
|
||||
pub path: Option<String>,
|
||||
pub hour_format: Option<HourFormat>,
|
||||
@@ -1223,7 +1196,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct LanguageSettings {
|
||||
#[serde(default)]
|
||||
languages: HashMap<String, LanguageSettingEntry>,
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct WorkspaceSettings {
|
||||
pub git: GitSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WorkspaceSettingsContent {
|
||||
pub active_pane_magnification: Option<f32>,
|
||||
pub confirm_quit: Option<bool>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user