mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 19:25:41 +03:00
Indent guides (#11503)
Builds on top of existing work from #2249, but here's a showcase: https://github.com/zed-industries/zed/assets/53836821/4b346965-6654-496c-b379-75425d9b493f TODO: - [x] handle line wrapping - [x] implement handling in multibuffer (crashes currently) - [x] add configuration option - [x] new theme properties? What colors to use? - [x] Possibly support indents with different colors or background colors - [x] investigate edge cases (e.g. indent guides and folds continue on empty lines even if the next indent is different) - [x] add more tests (also test `find_active_indent_index`) - [x] docs (will do in a follow up PR) - [x] benchmark performance impact Release Notes: - Added indent guides ([#5373](https://github.com/zed-industries/zed/issues/5373)) --------- Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com> Co-authored-by: Remco <djsmits12@gmail.com>
This commit is contained in:
parent
3eb0418bda
commit
feea607bac
@ -216,6 +216,25 @@
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
},
|
||||
"indent_guides": {
|
||||
/// Whether to show indent guides in the editor.
|
||||
"enabled": true,
|
||||
/// The width of the indent guides in pixels, between 1 and 10.
|
||||
"line_width": 1,
|
||||
/// Determines how indent guides are colored.
|
||||
/// This setting can take the following three values:
|
||||
///
|
||||
/// 1. "disabled"
|
||||
/// 2. "fixed"
|
||||
/// 3. "indent_aware"
|
||||
"coloring": "fixed",
|
||||
/// Determines how indent guide backgrounds are colored.
|
||||
/// This setting can take the following two values:
|
||||
///
|
||||
/// 1. "disabled"
|
||||
/// 2. "indent_aware"
|
||||
"background_coloring": "disabled"
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
|
@ -5,6 +5,15 @@
|
||||
{
|
||||
"name": "Gruvbox Dark",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@ -379,6 +388,15 @@
|
||||
{
|
||||
"name": "Gruvbox Dark Hard",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@ -753,6 +771,15 @@
|
||||
{
|
||||
"name": "Gruvbox Dark Soft",
|
||||
"appearance": "dark",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#5b534dff",
|
||||
"border.variant": "#494340ff",
|
||||
@ -1127,6 +1154,15 @@
|
||||
{
|
||||
"name": "Gruvbox Light",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
@ -1501,6 +1537,15 @@
|
||||
{
|
||||
"name": "Gruvbox Light Hard",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
@ -1875,6 +1920,15 @@
|
||||
{
|
||||
"name": "Gruvbox Light Soft",
|
||||
"appearance": "light",
|
||||
"accents": [
|
||||
"#cc241dff",
|
||||
"#98971aff",
|
||||
"#d79921ff",
|
||||
"#458588ff",
|
||||
"#b16286ff",
|
||||
"#689d6aff",
|
||||
"#d65d0eff"
|
||||
],
|
||||
"style": {
|
||||
"border": "#c8b899ff",
|
||||
"border.variant": "#ddcca7ff",
|
||||
|
@ -2744,6 +2744,7 @@ impl ConversationEditor {
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Box::new(completion_provider));
|
||||
editor
|
||||
});
|
||||
|
@ -100,6 +100,9 @@ impl MessageEditor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
|
||||
editor.set_auto_replace_emoji_shortcode(
|
||||
MessageEditorSettings::get_global(cx)
|
||||
|
@ -845,20 +845,7 @@ impl DisplaySnapshot {
|
||||
.buffer_line_for_row(buffer_row)
|
||||
.unwrap();
|
||||
|
||||
let mut indent_size = 0;
|
||||
let mut is_blank = false;
|
||||
for c in buffer.chars_at(Point::new(range.start.row, 0)) {
|
||||
if c == ' ' || c == '\t' {
|
||||
indent_size += 1;
|
||||
} else {
|
||||
if c == '\n' {
|
||||
is_blank = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(indent_size, is_blank)
|
||||
buffer.line_indent_for_row(range.start.row)
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: DisplayRow) -> u32 {
|
||||
|
@ -26,6 +26,7 @@ mod git;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
mod hover_popover;
|
||||
mod indent_guides;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod mouse_context_menu;
|
||||
@ -76,6 +77,7 @@ use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
use hunk_diff::ExpandedHunks;
|
||||
pub(crate) use hunk_diff::HunkToExpand;
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
@ -453,11 +455,13 @@ pub struct Editor {
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_wrap_guides: Option<bool>,
|
||||
show_indent_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlight_order: usize,
|
||||
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
|
||||
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
|
||||
scrollbar_marker_state: ScrollbarMarkerState,
|
||||
active_indent_guides_state: ActiveIndentGuidesState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: RwLock<Option<ContextMenu>>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
@ -1656,11 +1660,13 @@ impl Editor {
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_wrap_guides: None,
|
||||
show_indent_guides: None,
|
||||
placeholder_text: None,
|
||||
highlight_order: 0,
|
||||
highlighted_rows: HashMap::default(),
|
||||
background_highlights: Default::default(),
|
||||
scrollbar_marker_state: ScrollbarMarkerState::default(),
|
||||
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
||||
nav_history: None,
|
||||
context_menu: RwLock::new(None),
|
||||
mouse_context_menu: None,
|
||||
@ -9440,6 +9446,7 @@ impl Editor {
|
||||
|
||||
cx.notify();
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9668,6 +9675,11 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_indent_guides = Some(show_indent_guides);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
@ -10303,6 +10315,7 @@ impl Editor {
|
||||
singleton_buffer_edited,
|
||||
} => {
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(cx);
|
||||
if self.has_active_inline_completion(cx) {
|
||||
|
@ -17,8 +17,10 @@ use language::{
|
||||
},
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
Point,
|
||||
};
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
@ -11448,6 +11450,505 @@ async fn test_multiple_expanded_hunks_merge(
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_indent_guides_editor(
|
||||
text: &str,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> (BufferId, EditorTestContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let buffer_id = cx.update_editor(|editor, cx| {
|
||||
editor.set_text(text, cx);
|
||||
let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
|
||||
let buffer_id = buffer_ids[0];
|
||||
buffer_id
|
||||
});
|
||||
|
||||
(buffer_id, cx)
|
||||
}
|
||||
|
||||
fn assert_indent_guides(
|
||||
range: Range<u32>,
|
||||
expected: Vec<IndentGuide>,
|
||||
active_indices: Option<Vec<usize>>,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
let indent_guides = cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
|
||||
MultiBufferRow(range.start)..MultiBufferRow(range.end),
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
indent_guides.sort_by(|a, b| {
|
||||
a.depth.cmp(&b.depth).then(
|
||||
a.start_row
|
||||
.cmp(&b.start_row)
|
||||
.then(a.end_row.cmp(&b.end_row)),
|
||||
)
|
||||
});
|
||||
indent_guides
|
||||
});
|
||||
|
||||
if let Some(expected) = active_indices {
|
||||
let active_indices = cx.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
editor.find_active_indent_guide_indices(&indent_guides, &snapshot, cx)
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
active_indices.unwrap().into_iter().collect::<Vec<_>>(),
|
||||
expected,
|
||||
"Active indent guide indices do not match"
|
||||
);
|
||||
}
|
||||
|
||||
let expected: Vec<_> = expected
|
||||
.into_iter()
|
||||
.map(|guide| MultiBufferIndentGuide {
|
||||
multibuffer_row_range: MultiBufferRow(guide.start_row)..MultiBufferRow(guide.end_row),
|
||||
buffer: guide,
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(indent_guides, expected, "Indent guides do not match");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_single_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_simple_block(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_nested(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..8,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 6, 0, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 5, 5, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_tab(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
|
||||
let c = 3;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_complex(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
|
||||
let c = 3;
|
||||
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_starts_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
|
||||
let c = 3;
|
||||
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
1..11,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_ends_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
|
||||
let c = 3;
|
||||
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_without_brackets(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
block1
|
||||
block2
|
||||
block3
|
||||
block4
|
||||
block2
|
||||
block1
|
||||
block1"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 4, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 2, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
block1
|
||||
block2
|
||||
block3
|
||||
|
||||
block1
|
||||
block1"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 2, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guides_continuing_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
block1
|
||||
|
||||
|
||||
|
||||
block2
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..1,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_active_indent_guides_single_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_active_indent_guides_respect_indented_range(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
if 1 == 2 {
|
||||
let a = 1;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_active_indent_guides_empty_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
|
||||
let b = 2;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_active_indent_guides_non_matching_indent(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
def m:
|
||||
a = 1
|
||||
pass"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
|
||||
});
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -38,7 +38,9 @@ use gpui::{
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
use language::language_settings::{
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||
use project::{
|
||||
@ -1460,6 +1462,118 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_indent_guides(
|
||||
&self,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
text_origin: gpui::Point<Pixels>,
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Vec<IndentGuideLayout>> {
|
||||
let indent_guides =
|
||||
self.editor
|
||||
.read(cx)
|
||||
.indent_guides(visible_buffer_range, snapshot, cx)?;
|
||||
|
||||
let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.find_active_indent_guide_indices(&indent_guides, snapshot, cx)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
Some(
|
||||
indent_guides
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, indent_guide)| {
|
||||
let indent_size = self.column_pixels(indent_guide.indent_size as usize, cx);
|
||||
let total_width = indent_size * px(indent_guide.depth as f32);
|
||||
|
||||
let start_x = content_origin.x + total_width - scroll_pixel_position.x;
|
||||
if start_x >= text_origin.x {
|
||||
let (offset_y, length) = Self::calculate_indent_guide_bounds(
|
||||
indent_guide.multibuffer_row_range.clone(),
|
||||
line_height,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
let start_y = content_origin.y + offset_y - scroll_pixel_position.y;
|
||||
|
||||
Some(IndentGuideLayout {
|
||||
origin: point(start_x, start_y),
|
||||
length,
|
||||
indent_size,
|
||||
depth: indent_guide.depth,
|
||||
active: active_indent_guide_indices.contains(&i),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn calculate_indent_guide_bounds(
|
||||
row_range: Range<MultiBufferRow>,
|
||||
line_height: Pixels,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> (gpui::Pixels, gpui::Pixels) {
|
||||
let start_point = Point::new(row_range.start.0, 0);
|
||||
let end_point = Point::new(row_range.end.0, 0);
|
||||
|
||||
let row_range = start_point.to_display_point(snapshot).row()
|
||||
..end_point.to_display_point(snapshot).row();
|
||||
|
||||
let mut prev_line = start_point;
|
||||
prev_line.row = prev_line.row.saturating_sub(1);
|
||||
let prev_line = prev_line.to_display_point(snapshot).row();
|
||||
|
||||
let mut cons_line = end_point;
|
||||
cons_line.row += 1;
|
||||
let cons_line = cons_line.to_display_point(snapshot).row();
|
||||
|
||||
let mut offset_y = row_range.start.0 as f32 * line_height;
|
||||
let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
|
||||
|
||||
// If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
|
||||
// we want to extend the indent guide to the start of the block.
|
||||
let mut block_height = 0;
|
||||
let mut block_offset = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
|
||||
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
|
||||
found_excerpt_header = true;
|
||||
break;
|
||||
}
|
||||
block_offset += block.height();
|
||||
block_height += block.height();
|
||||
}
|
||||
if !found_excerpt_header {
|
||||
offset_y -= block_offset as f32 * line_height;
|
||||
length += block_height as f32 * line_height;
|
||||
}
|
||||
|
||||
// If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
|
||||
// we want to ensure that the indent guide stops before the excerpt header.
|
||||
let mut block_height = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
|
||||
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
|
||||
found_excerpt_header = true;
|
||||
}
|
||||
block_height += block.height();
|
||||
}
|
||||
if found_excerpt_header {
|
||||
length -= block_height as f32 * line_height;
|
||||
}
|
||||
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@ -2500,6 +2614,91 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint_indent_guides(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
let Some(indent_guides) = &layout.indent_guides else {
|
||||
return;
|
||||
};
|
||||
|
||||
let settings = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides;
|
||||
|
||||
let faded_color = |color: Hsla, alpha: f32| {
|
||||
let mut faded = color;
|
||||
faded.a = alpha;
|
||||
faded
|
||||
};
|
||||
|
||||
for indent_guide in indent_guides {
|
||||
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
|
||||
|
||||
// TODO fixed for now, expose them through themes later
|
||||
const INDENT_AWARE_ALPHA: f32 = 0.2;
|
||||
const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
|
||||
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
|
||||
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
|
||||
|
||||
let line_color = match (&settings.coloring, indent_guide.active) {
|
||||
(IndentGuideColoring::Disabled, _) => None,
|
||||
(IndentGuideColoring::Fixed, false) => {
|
||||
Some(cx.theme().colors().editor_indent_guide)
|
||||
}
|
||||
(IndentGuideColoring::Fixed, true) => {
|
||||
Some(cx.theme().colors().editor_indent_guide_active)
|
||||
}
|
||||
(IndentGuideColoring::IndentAware, false) => {
|
||||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
|
||||
}
|
||||
(IndentGuideColoring::IndentAware, true) => {
|
||||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
|
||||
}
|
||||
};
|
||||
|
||||
let background_color = match (&settings.background_coloring, indent_guide.active) {
|
||||
(IndentGuideBackgroundColoring::Disabled, _) => None,
|
||||
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
INDENT_AWARE_BACKGROUND_ALPHA,
|
||||
)),
|
||||
(IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
|
||||
)),
|
||||
};
|
||||
|
||||
let requested_line_width = settings.line_width.clamp(1, 10);
|
||||
let mut line_indicator_width = 0.;
|
||||
if let Some(color) = line_color {
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: indent_guide.origin,
|
||||
size: size(px(requested_line_width as f32), indent_guide.length),
|
||||
},
|
||||
color,
|
||||
));
|
||||
line_indicator_width = requested_line_width as f32;
|
||||
}
|
||||
|
||||
if let Some(color) = background_color {
|
||||
let width = indent_guide.indent_size - px(line_indicator_width);
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: point(
|
||||
indent_guide.origin.x + px(line_indicator_width),
|
||||
indent_guide.origin.y,
|
||||
),
|
||||
size: size(width, indent_guide.length),
|
||||
},
|
||||
color,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
@ -4146,6 +4345,21 @@ impl Element for EditorElement {
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let start_buffer_row =
|
||||
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
let end_buffer_row =
|
||||
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
|
||||
let indent_guides = self.layout_indent_guides(
|
||||
content_origin,
|
||||
text_hitbox.origin,
|
||||
start_buffer_row..end_buffer_row,
|
||||
scroll_pixel_position,
|
||||
line_height,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
|
||||
self.prepaint_flap_trailers(
|
||||
flap_trailers,
|
||||
@ -4403,6 +4617,7 @@ impl Element for EditorElement {
|
||||
}),
|
||||
visible_display_row_range: start_row..end_row,
|
||||
wrap_guides,
|
||||
indent_guides,
|
||||
hitbox,
|
||||
text_hitbox,
|
||||
gutter_hitbox,
|
||||
@ -4492,6 +4707,7 @@ impl Element for EditorElement {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
self.paint_mouse_listeners(layout, hovered_hunk, cx);
|
||||
self.paint_background(layout, cx);
|
||||
self.paint_indent_guides(layout, cx);
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter(layout, cx)
|
||||
}
|
||||
@ -4530,6 +4746,7 @@ pub struct EditorLayout {
|
||||
scrollbar_layout: Option<ScrollbarLayout>,
|
||||
mode: EditorMode,
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, Hsla>,
|
||||
@ -4795,6 +5012,15 @@ fn layout_line(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndentGuideLayout {
|
||||
origin: gpui::Point<Pixels>,
|
||||
length: Pixels,
|
||||
indent_size: Pixels,
|
||||
depth: u32,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
pub struct CursorLayout {
|
||||
origin: gpui::Point<Pixels>,
|
||||
block_width: Pixels,
|
||||
|
164
crates/editor/src/indent_guides.rs
Normal file
164
crates/editor/src/indent_guides.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{AppContext, Task};
|
||||
use language::BufferRow;
|
||||
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
|
||||
use text::{BufferId, Point};
|
||||
use ui::ViewContext;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{DisplaySnapshot, Editor};
|
||||
|
||||
struct ActiveIndentedRange {
|
||||
buffer_id: BufferId,
|
||||
row_range: Range<BufferRow>,
|
||||
indent: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ActiveIndentGuidesState {
|
||||
pub dirty: bool,
|
||||
cursor_row: MultiBufferRow,
|
||||
pending_refresh: Option<Task<()>>,
|
||||
active_indent_range: Option<ActiveIndentedRange>,
|
||||
}
|
||||
|
||||
impl ActiveIndentGuidesState {
|
||||
pub fn should_refresh(&self, cursor_row: MultiBufferRow) -> bool {
|
||||
self.pending_refresh.is_none() && (self.cursor_row != cursor_row || self.dirty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn indent_guides(
|
||||
&self,
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Option<Vec<MultiBufferIndentGuide>> {
|
||||
if self.show_indent_guides == Some(false) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let settings = self.buffer.read(cx).settings_at(0, cx);
|
||||
if settings.indent_guides.enabled {
|
||||
Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_active_indent_guide_indices(
|
||||
&mut self,
|
||||
indent_guides: &[MultiBufferIndentGuide],
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<HashSet<usize>> {
|
||||
let selection = self.selections.newest::<Point>(cx);
|
||||
let cursor_row = MultiBufferRow(selection.head().row);
|
||||
|
||||
let state = &mut self.active_indent_guides_state;
|
||||
if state.cursor_row != cursor_row {
|
||||
state.cursor_row = cursor_row;
|
||||
state.dirty = true;
|
||||
}
|
||||
|
||||
if state.should_refresh(cursor_row) {
|
||||
let snapshot = snapshot.clone();
|
||||
state.dirty = false;
|
||||
|
||||
let task = cx
|
||||
.background_executor()
|
||||
.spawn(resolve_indented_range(snapshot, cursor_row));
|
||||
|
||||
// Try to resolve the indent in a short amount of time, otherwise move it to a background task.
|
||||
match cx
|
||||
.background_executor()
|
||||
.block_with_timeout(Duration::from_micros(200), task)
|
||||
{
|
||||
Ok(result) => state.active_indent_range = result,
|
||||
Err(future) => {
|
||||
state.pending_refresh = Some(cx.spawn(|editor, mut cx| async move {
|
||||
let result = cx.background_executor().spawn(future).await;
|
||||
editor
|
||||
.update(&mut cx, |editor, _| {
|
||||
editor.active_indent_guides_state.active_indent_range = result;
|
||||
editor.active_indent_guides_state.pending_refresh = None;
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let active_indent_range = state.active_indent_range.as_ref()?;
|
||||
|
||||
let candidates = indent_guides
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, indent_guide)| {
|
||||
indent_guide.buffer_id == active_indent_range.buffer_id
|
||||
&& indent_guide.indent_width() == active_indent_range.indent
|
||||
});
|
||||
|
||||
let mut matches = HashSet::default();
|
||||
for (i, indent) in candidates {
|
||||
// Find matches that are either an exact match, partially on screen, or inside the enclosing indent
|
||||
if active_indent_range.row_range.start <= indent.end_row
|
||||
&& indent.start_row <= active_indent_range.row_range.end
|
||||
{
|
||||
matches.insert(i);
|
||||
}
|
||||
}
|
||||
Some(matches)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indent_guides_in_range(
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Vec<MultiBufferIndentGuide> {
|
||||
let start_anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
|
||||
let end_anchor = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
|
||||
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.indent_guides_in_range(start_anchor..end_anchor, cx)
|
||||
.into_iter()
|
||||
.filter(|indent_guide| {
|
||||
// Filter out indent guides that are inside a fold
|
||||
!snapshot.is_line_folded(indent_guide.multibuffer_row_range.start)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn resolve_indented_range(
|
||||
snapshot: DisplaySnapshot,
|
||||
buffer_row: MultiBufferRow,
|
||||
) -> Option<ActiveIndentedRange> {
|
||||
let (buffer_row, buffer_snapshot, buffer_id) =
|
||||
if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
|
||||
(buffer_row.0, snapshot, buffer_id)
|
||||
} else {
|
||||
let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
|
||||
|
||||
let buffer_id = snapshot.remote_id();
|
||||
(point.start.row, snapshot, buffer_id)
|
||||
};
|
||||
|
||||
buffer_snapshot
|
||||
.enclosing_indent(buffer_row)
|
||||
.await
|
||||
.map(|(row_range, indent)| ActiveIndentedRange {
|
||||
row_range,
|
||||
indent,
|
||||
buffer_id,
|
||||
})
|
||||
}
|
@ -185,6 +185,7 @@ impl FeedbackModal {
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_inline_completions(false);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
|
@ -512,6 +512,37 @@ pub struct Runnable {
|
||||
pub buffer: BufferId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct IndentGuide {
|
||||
pub buffer_id: BufferId,
|
||||
pub start_row: BufferRow,
|
||||
pub end_row: BufferRow,
|
||||
pub depth: u32,
|
||||
pub indent_size: u32,
|
||||
}
|
||||
|
||||
impl IndentGuide {
|
||||
pub fn new(
|
||||
buffer_id: BufferId,
|
||||
start_row: BufferRow,
|
||||
end_row: BufferRow,
|
||||
depth: u32,
|
||||
indent_size: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
buffer_id,
|
||||
start_row,
|
||||
end_row,
|
||||
depth,
|
||||
indent_size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indent_width(&self) -> u32 {
|
||||
self.indent_size * self.depth
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Create a new buffer with the given base text.
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
|
||||
@ -3059,6 +3090,236 @@ impl BufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn indent_guides_in_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<IndentGuide> {
|
||||
fn indent_size_for_row(this: &BufferSnapshot, row: BufferRow, cx: &AppContext) -> u32 {
|
||||
let language = this.language_at(Point::new(row, 0));
|
||||
language_settings(language, None, cx).tab_size.get() as u32
|
||||
}
|
||||
|
||||
let start_row = range.start.to_point(self).row;
|
||||
let end_row = range.end.to_point(self).row;
|
||||
let row_range = start_row..end_row + 1;
|
||||
|
||||
let mut row_indents = self.line_indents_in_row_range(row_range.clone());
|
||||
|
||||
let mut result_vec = Vec::new();
|
||||
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
|
||||
|
||||
// TODO: This should be calculated for every row but it is pretty expensive
|
||||
let indent_size = indent_size_for_row(self, start_row, cx);
|
||||
|
||||
while let Some((first_row, mut line_indent, empty)) = row_indents.next() {
|
||||
let current_depth = indent_stack.len() as u32;
|
||||
|
||||
// When encountering empty, continue until found useful line indent
|
||||
// then add to the indent stack with the depth found
|
||||
let mut found_indent = false;
|
||||
let mut last_row = first_row;
|
||||
if empty {
|
||||
let mut trailing_row = end_row;
|
||||
while !found_indent {
|
||||
let (target_row, new_line_indent, empty) =
|
||||
if let Some(display_row) = row_indents.next() {
|
||||
display_row
|
||||
} else {
|
||||
// This means we reached the end of the given range and found empty lines at the end.
|
||||
// We need to traverse further until we find a non-empty line to know if we need to add
|
||||
// an indent guide for the last visible indent.
|
||||
trailing_row += 1;
|
||||
|
||||
const TRAILING_ROW_SEARCH_LIMIT: u32 = 25;
|
||||
if trailing_row > self.max_point().row
|
||||
|| trailing_row > end_row + TRAILING_ROW_SEARCH_LIMIT
|
||||
{
|
||||
break;
|
||||
}
|
||||
let (new_line_indent, empty) = self.line_indent_for_row(trailing_row);
|
||||
(trailing_row, new_line_indent, empty)
|
||||
};
|
||||
|
||||
if empty {
|
||||
continue;
|
||||
}
|
||||
last_row = target_row.min(end_row);
|
||||
line_indent = new_line_indent;
|
||||
found_indent = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
found_indent = true
|
||||
}
|
||||
|
||||
let depth = if found_indent {
|
||||
line_indent / indent_size + ((line_indent % indent_size) > 0) as u32
|
||||
} else {
|
||||
current_depth
|
||||
};
|
||||
|
||||
if depth < current_depth {
|
||||
for _ in 0..(current_depth - depth) {
|
||||
let mut indent = indent_stack.pop().unwrap();
|
||||
if last_row != first_row {
|
||||
// In this case, we landed on an empty row, had to seek forward,
|
||||
// and discovered that the indent we where on is ending.
|
||||
// This means that the last display row must
|
||||
// be on line that ends this indent range, so we
|
||||
// should display the range up to the first non-empty line
|
||||
indent.end_row = first_row.saturating_sub(1);
|
||||
}
|
||||
|
||||
result_vec.push(indent)
|
||||
}
|
||||
} else if depth > current_depth {
|
||||
for next_depth in current_depth..depth {
|
||||
indent_stack.push(IndentGuide {
|
||||
buffer_id: self.remote_id(),
|
||||
start_row: first_row,
|
||||
end_row: last_row,
|
||||
depth: next_depth,
|
||||
indent_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for indent in indent_stack.iter_mut() {
|
||||
indent.end_row = last_row;
|
||||
}
|
||||
}
|
||||
|
||||
result_vec.extend(indent_stack);
|
||||
|
||||
result_vec
|
||||
}
|
||||
|
||||
pub async fn enclosing_indent(
|
||||
&self,
|
||||
mut buffer_row: BufferRow,
|
||||
) -> Option<(Range<BufferRow>, u32)> {
|
||||
let max_row = self.max_point().row;
|
||||
if buffer_row >= max_row {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (mut target_indent_size, is_blank) = self.line_indent_for_row(buffer_row);
|
||||
|
||||
// If the current row is at the start of an indented block, we want to return this
|
||||
// block as the enclosing indent.
|
||||
if !is_blank && buffer_row < max_row {
|
||||
let (next_line_indent, is_blank) = self.line_indent_for_row(buffer_row + 1);
|
||||
if !is_blank && target_indent_size < next_line_indent {
|
||||
target_indent_size = next_line_indent;
|
||||
buffer_row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const SEARCH_ROW_LIMIT: u32 = 25000;
|
||||
const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500;
|
||||
const YIELD_INTERVAL: u32 = 100;
|
||||
|
||||
let mut accessed_row_counter = 0;
|
||||
|
||||
// If there is a blank line at the current row, search for the next non indented lines
|
||||
if is_blank {
|
||||
let start = buffer_row.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT);
|
||||
let end = (max_row + 1).min(buffer_row + SEARCH_WHITESPACE_ROW_LIMIT);
|
||||
|
||||
let mut non_empty_line_above = None;
|
||||
for (row, indent_size, is_blank) in self
|
||||
.text
|
||||
.reversed_line_indents_in_row_range(start..buffer_row)
|
||||
{
|
||||
accessed_row_counter += 1;
|
||||
if accessed_row_counter == YIELD_INTERVAL {
|
||||
accessed_row_counter = 0;
|
||||
yield_now().await;
|
||||
}
|
||||
if !is_blank {
|
||||
non_empty_line_above = Some((row, indent_size));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut non_empty_line_below = None;
|
||||
for (row, indent_size, is_blank) in
|
||||
self.text.line_indents_in_row_range((buffer_row + 1)..end)
|
||||
{
|
||||
accessed_row_counter += 1;
|
||||
if accessed_row_counter == YIELD_INTERVAL {
|
||||
accessed_row_counter = 0;
|
||||
yield_now().await;
|
||||
}
|
||||
if !is_blank {
|
||||
non_empty_line_below = Some((row, indent_size));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (row, indent_size) = match (non_empty_line_above, non_empty_line_below) {
|
||||
(Some((above_row, above_indent)), Some((below_row, below_indent))) => {
|
||||
if above_indent >= below_indent {
|
||||
(above_row, above_indent)
|
||||
} else {
|
||||
(below_row, below_indent)
|
||||
}
|
||||
}
|
||||
(Some(above), None) => above,
|
||||
(None, Some(below)) => below,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
target_indent_size = indent_size;
|
||||
buffer_row = row;
|
||||
}
|
||||
|
||||
let start = buffer_row.saturating_sub(SEARCH_ROW_LIMIT);
|
||||
let end = (max_row + 1).min(buffer_row + SEARCH_ROW_LIMIT);
|
||||
|
||||
let mut start_indent = None;
|
||||
for (row, indent_size, is_blank) in self
|
||||
.text
|
||||
.reversed_line_indents_in_row_range(start..buffer_row)
|
||||
{
|
||||
accessed_row_counter += 1;
|
||||
if accessed_row_counter == YIELD_INTERVAL {
|
||||
accessed_row_counter = 0;
|
||||
yield_now().await;
|
||||
}
|
||||
if !is_blank && indent_size < target_indent_size {
|
||||
start_indent = Some((row, indent_size));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (start_row, start_indent_size) = start_indent?;
|
||||
|
||||
let mut end_indent = (end, None);
|
||||
for (row, indent_size, is_blank) in
|
||||
self.text.line_indents_in_row_range((buffer_row + 1)..end)
|
||||
{
|
||||
accessed_row_counter += 1;
|
||||
if accessed_row_counter == YIELD_INTERVAL {
|
||||
accessed_row_counter = 0;
|
||||
yield_now().await;
|
||||
}
|
||||
if !is_blank && indent_size < target_indent_size {
|
||||
end_indent = (row.saturating_sub(1), Some(indent_size));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (end_row, end_indent_size) = end_indent;
|
||||
|
||||
let indent_size = if let Some(end_indent_size) = end_indent_size {
|
||||
start_indent_size.max(end_indent_size)
|
||||
} else {
|
||||
start_indent_size
|
||||
};
|
||||
|
||||
Some((start_row..end_row, indent_size))
|
||||
}
|
||||
|
||||
/// Returns selections for remote peers intersecting the given range.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn remote_selections_in_range(
|
||||
|
@ -2052,6 +2052,71 @@ fn test_serialization(cx: &mut gpui::AppContext) {
|
||||
assert_eq!(buffer2.read(cx).text(), "abcDF");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_matching_indent(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| init_settings(cx, |_| {}));
|
||||
|
||||
async fn enclosing_indent(
|
||||
text: impl Into<String>,
|
||||
buffer_row: u32,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Option<(Range<u32>, u32)> {
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
|
||||
let snapshot = cx.read(|cx| buffer.read(cx).snapshot());
|
||||
snapshot.enclosing_indent(buffer_row).await
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
enclosing_indent(
|
||||
"
|
||||
fn b() {
|
||||
if c {
|
||||
let d = 2;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
1,
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some((1..2, 4))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
enclosing_indent(
|
||||
"
|
||||
fn b() {
|
||||
if c {
|
||||
let d = 2;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
2,
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some((1..2, 4))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
enclosing_indent(
|
||||
"
|
||||
fn b() {
|
||||
if c {
|
||||
let d = 2;
|
||||
|
||||
let e = 5;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
3,
|
||||
cx,
|
||||
)
|
||||
.await,
|
||||
Some((1..4, 4))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||
let min_peers = env::var("MIN_PEERS")
|
||||
|
@ -78,6 +78,8 @@ pub struct LanguageSettings {
|
||||
pub show_wrap_guides: bool,
|
||||
/// Character counts at which to show wrap guides in the editor.
|
||||
pub wrap_guides: Vec<usize>,
|
||||
/// Indent guide related settings.
|
||||
pub indent_guides: IndentGuideSettings,
|
||||
/// Whether or not to perform a buffer format before saving.
|
||||
pub format_on_save: FormatOnSave,
|
||||
/// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
@ -242,6 +244,9 @@ pub struct LanguageSettingsContent {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
pub wrap_guides: Option<Vec<usize>>,
|
||||
/// Indent guide related settings.
|
||||
#[serde(default)]
|
||||
pub indent_guides: Option<IndentGuideSettings>,
|
||||
/// Whether or not to perform a buffer format before saving.
|
||||
///
|
||||
/// Default: on
|
||||
@ -411,6 +416,59 @@ pub enum Formatter {
|
||||
CodeActions(HashMap<String, bool>),
|
||||
}
|
||||
|
||||
/// The settings for indent guides.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct IndentGuideSettings {
|
||||
/// Whether to display indent guides in the editor.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// The width of the indent guides in pixels, between 1 and 10.
|
||||
///
|
||||
/// Default: 1
|
||||
#[serde(default = "line_width")]
|
||||
pub line_width: u32,
|
||||
/// Determines how indent guides are colored.
|
||||
///
|
||||
/// Default: Fixed
|
||||
#[serde(default)]
|
||||
pub coloring: IndentGuideColoring,
|
||||
/// Determines how indent guide backgrounds are colored.
|
||||
///
|
||||
/// Default: Disabled
|
||||
#[serde(default)]
|
||||
pub background_coloring: IndentGuideBackgroundColoring,
|
||||
}
|
||||
|
||||
fn line_width() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// Determines how indent guides are colored.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IndentGuideColoring {
|
||||
/// Do not render any lines for indent guides.
|
||||
Disabled,
|
||||
/// Use the same color for all indentation levels.
|
||||
#[default]
|
||||
Fixed,
|
||||
/// Use a different color for each indentation level.
|
||||
IndentAware,
|
||||
}
|
||||
|
||||
/// Determines how indent guide backgrounds are colored.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IndentGuideBackgroundColoring {
|
||||
/// Do not render any background for indent guides.
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Use a different color for each indentation level.
|
||||
IndentAware,
|
||||
}
|
||||
|
||||
/// The settings for inlay hints.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct InlayHintSettings {
|
||||
@ -715,6 +773,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
);
|
||||
merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
|
||||
merge(&mut settings.wrap_guides, src.wrap_guides.clone());
|
||||
merge(&mut settings.indent_guides, src.indent_guides);
|
||||
merge(
|
||||
&mut settings.code_actions_on_format,
|
||||
src.code_actions_on_format.clone(),
|
||||
|
@ -12,9 +12,9 @@ use language::{
|
||||
char_kind,
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharKind, Chunk,
|
||||
CursorShape, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt,
|
||||
OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
CursorShape, DiagnosticEntry, File, IndentGuide, IndentSize, Language, LanguageScope,
|
||||
OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension,
|
||||
ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@ -281,6 +281,20 @@ struct ExcerptBytes<'a> {
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MultiBufferIndentGuide {
|
||||
pub multibuffer_row_range: Range<MultiBufferRow>,
|
||||
pub buffer: IndentGuide,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for MultiBufferIndentGuide {
|
||||
type Target = IndentGuide;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiBuffer {
|
||||
pub fn new(replica_id: ReplicaId, capability: Capability) -> Self {
|
||||
Self {
|
||||
@ -1255,6 +1269,15 @@ impl MultiBuffer {
|
||||
excerpts
|
||||
}
|
||||
|
||||
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
|
||||
self.snapshot
|
||||
.borrow()
|
||||
.excerpts
|
||||
.iter()
|
||||
.map(|entry| entry.buffer_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
|
||||
self.snapshot
|
||||
.borrow()
|
||||
@ -3182,6 +3205,52 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn indent_guides_in_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<MultiBufferIndentGuide> {
|
||||
// Fast path for singleton buffers, we can skip the conversion between offsets.
|
||||
if let Some((_, _, snapshot)) = self.as_singleton() {
|
||||
return snapshot
|
||||
.indent_guides_in_range(range.start.text_anchor..range.end.text_anchor, cx)
|
||||
.into_iter()
|
||||
.map(|guide| MultiBufferIndentGuide {
|
||||
multibuffer_row_range: MultiBufferRow(guide.start_row)
|
||||
..MultiBufferRow(guide.end_row),
|
||||
buffer: guide,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
self.excerpts_for_range(range.clone())
|
||||
.flat_map(move |(excerpt, excerpt_offset)| {
|
||||
let excerpt_buffer_start_row =
|
||||
excerpt.range.context.start.to_point(&excerpt.buffer).row;
|
||||
let excerpt_offset_row = crate::ToPoint::to_point(&excerpt_offset, self).row;
|
||||
|
||||
excerpt
|
||||
.buffer
|
||||
.indent_guides_in_range(excerpt.range.context.clone(), cx)
|
||||
.into_iter()
|
||||
.map(move |indent_guide| {
|
||||
let start_row = excerpt_offset_row
|
||||
+ (indent_guide.start_row - excerpt_buffer_start_row);
|
||||
let end_row =
|
||||
excerpt_offset_row + (indent_guide.end_row - excerpt_buffer_start_row);
|
||||
|
||||
MultiBufferIndentGuide {
|
||||
multibuffer_row_range: MultiBufferRow(start_row)
|
||||
..MultiBufferRow(end_row),
|
||||
buffer: indent_guide,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn diagnostics_update_count(&self) -> usize {
|
||||
self.diagnostics_update_count
|
||||
}
|
||||
|
@ -619,10 +619,12 @@ impl<'a> Chunks<'a> {
|
||||
}
|
||||
|
||||
pub fn lines(self) -> Lines<'a> {
|
||||
let reversed = self.reversed;
|
||||
Lines {
|
||||
chunks: self,
|
||||
current_line: String::new(),
|
||||
done: false,
|
||||
reversed,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -726,6 +728,7 @@ pub struct Lines<'a> {
|
||||
chunks: Chunks<'a>,
|
||||
current_line: String,
|
||||
done: bool,
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl<'a> Lines<'a> {
|
||||
@ -737,7 +740,19 @@ impl<'a> Lines<'a> {
|
||||
self.current_line.clear();
|
||||
|
||||
while let Some(chunk) = self.chunks.peek() {
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
let lines = chunk.split('\n');
|
||||
if self.reversed {
|
||||
let mut lines = lines.rev().peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
self.current_line.insert_str(0, line);
|
||||
if lines.peek().is_some() {
|
||||
self.chunks
|
||||
.seek(self.chunks.offset() - line.len() - "\n".len());
|
||||
return Some(&self.current_line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut lines = lines.peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
self.current_line.push_str(line);
|
||||
if lines.peek().is_some() {
|
||||
@ -746,6 +761,7 @@ impl<'a> Lines<'a> {
|
||||
return Some(&self.current_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.chunks.next();
|
||||
}
|
||||
@ -1355,6 +1371,21 @@ mod tests {
|
||||
assert_eq!(lines.next(), Some("hi"));
|
||||
assert_eq!(lines.next(), Some(""));
|
||||
assert_eq!(lines.next(), None);
|
||||
|
||||
let rope = Rope::from("abc\ndefg\nhi");
|
||||
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
|
||||
assert_eq!(lines.next(), Some("hi"));
|
||||
assert_eq!(lines.next(), Some("defg"));
|
||||
assert_eq!(lines.next(), Some("abc"));
|
||||
assert_eq!(lines.next(), None);
|
||||
|
||||
let rope = Rope::from("abc\ndefg\nhi\n");
|
||||
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
|
||||
assert_eq!(lines.next(), Some(""));
|
||||
assert_eq!(lines.next(), Some("hi"));
|
||||
assert_eq!(lines.next(), Some("defg"));
|
||||
assert_eq!(lines.next(), Some("abc"));
|
||||
assert_eq!(lines.next(), None);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
|
@ -1865,6 +1865,87 @@ impl BufferSnapshot {
|
||||
(row_end_offset - row_start_offset) as u32
|
||||
}
|
||||
|
||||
pub fn line_indents_in_row_range(
|
||||
&self,
|
||||
row_range: Range<u32>,
|
||||
) -> impl Iterator<Item = (u32, u32, bool)> + '_ {
|
||||
let start = Point::new(row_range.start, 0).to_offset(self);
|
||||
let end = Point::new(row_range.end, 0).to_offset(self);
|
||||
|
||||
let mut lines = self.as_rope().chunks_in_range(start..end).lines();
|
||||
let mut row = row_range.start;
|
||||
std::iter::from_fn(move || {
|
||||
if let Some(line) = lines.next() {
|
||||
let mut indent_size = 0;
|
||||
let mut is_blank = true;
|
||||
|
||||
for c in line.chars() {
|
||||
is_blank = false;
|
||||
if c == ' ' || c == '\t' {
|
||||
indent_size += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row += 1;
|
||||
Some((row - 1, indent_size, is_blank))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reversed_line_indents_in_row_range(
|
||||
&self,
|
||||
row_range: Range<u32>,
|
||||
) -> impl Iterator<Item = (u32, u32, bool)> + '_ {
|
||||
let start = Point::new(row_range.start, 0).to_offset(self);
|
||||
let end = Point::new(row_range.end, 0)
|
||||
.to_offset(self)
|
||||
.saturating_sub(1);
|
||||
|
||||
let mut lines = self.as_rope().reversed_chunks_in_range(start..end).lines();
|
||||
let mut row = row_range.end;
|
||||
std::iter::from_fn(move || {
|
||||
if let Some(line) = lines.next() {
|
||||
let mut indent_size = 0;
|
||||
let mut is_blank = true;
|
||||
|
||||
for c in line.chars() {
|
||||
is_blank = false;
|
||||
if c == ' ' || c == '\t' {
|
||||
indent_size += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row = row.saturating_sub(1);
|
||||
Some((row, indent_size, is_blank))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn line_indent_for_row(&self, row: u32) -> (u32, bool) {
|
||||
let mut indent_size = 0;
|
||||
let mut is_blank = false;
|
||||
for c in self.chars_at(Point::new(row, 0)) {
|
||||
if c == ' ' || c == '\t' {
|
||||
indent_size += 1;
|
||||
} else {
|
||||
if c == '\n' {
|
||||
is_blank = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(indent_size, is_blank)
|
||||
}
|
||||
|
||||
pub fn is_line_blank(&self, row: u32) -> bool {
|
||||
self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row)))
|
||||
.all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none())
|
||||
|
@ -75,6 +75,8 @@ impl ThemeColors {
|
||||
editor_invisible: neutral().light().step_10(),
|
||||
editor_wrap_guide: neutral().light_alpha().step_7(),
|
||||
editor_active_wrap_guide: neutral().light_alpha().step_8(),
|
||||
editor_indent_guide: neutral().light_alpha().step_5(),
|
||||
editor_indent_guide_active: neutral().light_alpha().step_6(),
|
||||
editor_document_highlight_read_background: neutral().light_alpha().step_3(),
|
||||
editor_document_highlight_write_background: neutral().light_alpha().step_4(),
|
||||
terminal_background: neutral().light().step_1(),
|
||||
@ -170,6 +172,8 @@ impl ThemeColors {
|
||||
editor_invisible: neutral().dark_alpha().step_4(),
|
||||
editor_wrap_guide: neutral().dark_alpha().step_4(),
|
||||
editor_active_wrap_guide: neutral().dark_alpha().step_4(),
|
||||
editor_indent_guide: neutral().dark_alpha().step_4(),
|
||||
editor_indent_guide_active: neutral().dark_alpha().step_6(),
|
||||
editor_document_highlight_read_background: neutral().dark_alpha().step_4(),
|
||||
editor_document_highlight_write_background: neutral().dark_alpha().step_4(),
|
||||
terminal_background: neutral().dark().step_1(),
|
||||
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::WindowBackgroundAppearance;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::AccentColors;
|
||||
|
||||
use crate::{
|
||||
default_color_scales,
|
||||
@ -23,21 +23,7 @@ fn zed_pro_daylight() -> Theme {
|
||||
status: StatusColors::light(),
|
||||
player: PlayerColors::light(),
|
||||
syntax: Arc::new(SyntaxTheme::default()),
|
||||
accents: vec![
|
||||
blue().light().step_9(),
|
||||
orange().light().step_9(),
|
||||
pink().light().step_9(),
|
||||
lime().light().step_9(),
|
||||
purple().light().step_9(),
|
||||
amber().light().step_9(),
|
||||
jade().light().step_9(),
|
||||
tomato().light().step_9(),
|
||||
cyan().light().step_9(),
|
||||
gold().light().step_9(),
|
||||
grass().light().step_9(),
|
||||
indigo().light().step_9(),
|
||||
iris().light().step_9(),
|
||||
],
|
||||
accents: AccentColors::light(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -54,21 +40,7 @@ pub(crate) fn zed_pro_moonlight() -> Theme {
|
||||
status: StatusColors::dark(),
|
||||
player: PlayerColors::dark(),
|
||||
syntax: Arc::new(SyntaxTheme::default()),
|
||||
accents: vec![
|
||||
blue().dark().step_9(),
|
||||
orange().dark().step_9(),
|
||||
pink().dark().step_9(),
|
||||
lime().dark().step_9(),
|
||||
purple().dark().step_9(),
|
||||
amber().dark().step_9(),
|
||||
jade().dark().step_9(),
|
||||
tomato().dark().step_9(),
|
||||
cyan().dark().step_9(),
|
||||
gold().dark().step_9(),
|
||||
grass().dark().step_9(),
|
||||
indigo().dark().step_9(),
|
||||
iris().dark().step_9(),
|
||||
],
|
||||
accents: AccentColors::dark(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, WindowBackgroundAppearance};
|
||||
|
||||
use crate::{
|
||||
default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme,
|
||||
ThemeColors, ThemeFamily, ThemeStyles,
|
||||
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors, SyntaxTheme,
|
||||
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles,
|
||||
};
|
||||
|
||||
// Note: This theme family is not the one you see in Zed at the moment.
|
||||
@ -42,6 +42,7 @@ pub(crate) fn one_dark() -> Theme {
|
||||
styles: ThemeStyles {
|
||||
window_background_appearance: WindowBackgroundAppearance::Opaque,
|
||||
system: SystemColors::default(),
|
||||
accents: AccentColors(vec![blue, orange, purple, teal, red, green, yellow]),
|
||||
colors: ThemeColors {
|
||||
border: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
|
||||
border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
|
||||
@ -91,6 +92,8 @@ pub(crate) fn one_dark() -> Theme {
|
||||
editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
|
||||
editor_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
|
||||
editor_active_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
|
||||
editor_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
|
||||
editor_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
|
||||
editor_document_highlight_read_background: hsla(
|
||||
207.8 / 360.,
|
||||
81. / 100.,
|
||||
@ -249,7 +252,6 @@ pub(crate) fn one_dark() -> Theme {
|
||||
("variant".into(), HighlightStyle::default()),
|
||||
],
|
||||
}),
|
||||
accents: vec![blue, orange, purple, teal],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,9 @@ use refineable::Refineable;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
try_parse_color, Appearance, AppearanceContent, PlayerColors, StatusColors, SyntaxTheme,
|
||||
SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent, ThemeStyles,
|
||||
try_parse_color, AccentColors, Appearance, AppearanceContent, PlayerColors, StatusColors,
|
||||
SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
|
||||
ThemeStyles,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -118,6 +119,12 @@ impl ThemeRegistry {
|
||||
};
|
||||
player_colors.merge(&user_theme.style.players);
|
||||
|
||||
let mut accent_colors = match user_theme.appearance {
|
||||
AppearanceContent::Light => AccentColors::light(),
|
||||
AppearanceContent::Dark => AccentColors::dark(),
|
||||
};
|
||||
accent_colors.merge(&user_theme.style.accents);
|
||||
|
||||
let syntax_highlights = user_theme
|
||||
.style
|
||||
.syntax
|
||||
@ -156,11 +163,11 @@ impl ThemeRegistry {
|
||||
styles: ThemeStyles {
|
||||
system: SystemColors::default(),
|
||||
window_background_appearance,
|
||||
accents: accent_colors,
|
||||
colors: theme_colors,
|
||||
status: status_colors,
|
||||
player: player_colors,
|
||||
syntax: syntax_theme,
|
||||
accents: Vec::new(),
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
@ -75,6 +75,9 @@ pub struct ThemeStyleContent {
|
||||
#[serde(default, rename = "background.appearance")]
|
||||
pub window_background_appearance: Option<WindowBackgroundContent>,
|
||||
|
||||
#[serde(default)]
|
||||
pub accents: Vec<AccentContent>,
|
||||
|
||||
#[serde(flatten, default)]
|
||||
pub colors: ThemeColorsContent,
|
||||
|
||||
@ -381,6 +384,12 @@ pub struct ThemeColorsContent {
|
||||
#[serde(rename = "editor.active_wrap_guide")]
|
||||
pub editor_active_wrap_guide: Option<String>,
|
||||
|
||||
#[serde(rename = "editor.indent_guide")]
|
||||
pub editor_indent_guide: Option<String>,
|
||||
|
||||
#[serde(rename = "editor.indent_guide_active")]
|
||||
pub editor_indent_guide_active: Option<String>,
|
||||
|
||||
/// Read-access of a symbol, like reading a variable.
|
||||
///
|
||||
/// A document highlight is a range inside a text document which deserves
|
||||
@ -747,6 +756,14 @@ impl ThemeColorsContent {
|
||||
.editor_active_wrap_guide
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
editor_indent_guide: self
|
||||
.editor_indent_guide
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
editor_indent_guide_active: self
|
||||
.editor_indent_guide_active
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
editor_document_highlight_read_background: self
|
||||
.editor_document_highlight_read_background
|
||||
.as_ref()
|
||||
@ -1196,6 +1213,9 @@ impl StatusColorsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AccentContent(pub Option<String>);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PlayerColorContent {
|
||||
pub cursor: Option<String>,
|
||||
|
@ -325,6 +325,7 @@ impl ThemeSettings {
|
||||
.status
|
||||
.refine(&theme_overrides.status_colors_refinement());
|
||||
base_theme.styles.player.merge(&theme_overrides.players);
|
||||
base_theme.styles.accents.merge(&theme_overrides.accents);
|
||||
base_theme.styles.syntax =
|
||||
SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod accents;
|
||||
mod colors;
|
||||
mod players;
|
||||
mod status;
|
||||
@ -7,6 +8,7 @@ mod system;
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
pub use accents::*;
|
||||
pub use colors::*;
|
||||
pub use players::*;
|
||||
pub use status::*;
|
||||
|
85
crates/theme/src/styles/accents.rs
Normal file
85
crates/theme/src/styles/accents.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use gpui::Hsla;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use crate::{
|
||||
amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato,
|
||||
try_parse_color, AccentContent,
|
||||
};
|
||||
|
||||
/// A collection of colors that are used to color indent aware lines in the editor.
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct AccentColors(pub Vec<Hsla>);
|
||||
|
||||
impl Default for AccentColors {
|
||||
/// Don't use this!
|
||||
/// We have to have a default to be `[refineable::Refinable]`.
|
||||
/// TODO "Find a way to not need this for Refinable"
|
||||
fn default() -> Self {
|
||||
Self::dark()
|
||||
}
|
||||
}
|
||||
|
||||
impl AccentColors {
|
||||
pub fn dark() -> Self {
|
||||
Self(vec![
|
||||
blue().dark().step_9(),
|
||||
orange().dark().step_9(),
|
||||
pink().dark().step_9(),
|
||||
lime().dark().step_9(),
|
||||
purple().dark().step_9(),
|
||||
amber().dark().step_9(),
|
||||
jade().dark().step_9(),
|
||||
tomato().dark().step_9(),
|
||||
cyan().dark().step_9(),
|
||||
gold().dark().step_9(),
|
||||
grass().dark().step_9(),
|
||||
indigo().dark().step_9(),
|
||||
iris().dark().step_9(),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn light() -> Self {
|
||||
Self(vec![
|
||||
blue().light().step_9(),
|
||||
orange().light().step_9(),
|
||||
pink().light().step_9(),
|
||||
lime().light().step_9(),
|
||||
purple().light().step_9(),
|
||||
amber().light().step_9(),
|
||||
jade().light().step_9(),
|
||||
tomato().light().step_9(),
|
||||
cyan().light().step_9(),
|
||||
gold().light().step_9(),
|
||||
grass().light().step_9(),
|
||||
indigo().light().step_9(),
|
||||
iris().light().step_9(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl AccentColors {
|
||||
pub fn color_for_index(&self, index: u32) -> Hsla {
|
||||
self.0[index as usize % self.0.len()]
|
||||
}
|
||||
|
||||
/// Merges the given accent colors into this [`AccentColors`] instance.
|
||||
pub fn merge(&mut self, accent_colors: &[AccentContent]) {
|
||||
if accent_colors.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let colors = accent_colors
|
||||
.iter()
|
||||
.filter_map(|accent_color| {
|
||||
accent_color
|
||||
.0
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !colors.is_empty() {
|
||||
self.0 = colors;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,9 @@ use gpui::{Hsla, WindowBackgroundAppearance};
|
||||
use refineable::Refineable;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors};
|
||||
use crate::{
|
||||
AccentColors, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors,
|
||||
};
|
||||
|
||||
#[derive(Refineable, Clone, Debug)]
|
||||
#[refineable(Debug, serde::Deserialize)]
|
||||
@ -154,6 +156,8 @@ pub struct ThemeColors {
|
||||
pub editor_invisible: Hsla,
|
||||
pub editor_wrap_guide: Hsla,
|
||||
pub editor_active_wrap_guide: Hsla,
|
||||
pub editor_indent_guide: Hsla,
|
||||
pub editor_indent_guide_active: Hsla,
|
||||
/// Read-access of a symbol, like reading a variable.
|
||||
///
|
||||
/// A document highlight is a range inside a text document which deserves
|
||||
@ -242,7 +246,7 @@ pub struct ThemeStyles {
|
||||
/// An array of colors used for theme elements that iterate through a series of colors.
|
||||
///
|
||||
/// Example: Player colors, rainbow brackets and indent guides, etc.
|
||||
pub accents: Vec<Hsla>,
|
||||
pub accents: AccentColors,
|
||||
|
||||
#[refineable]
|
||||
pub colors: ThemeColors,
|
||||
@ -251,6 +255,7 @@ pub struct ThemeStyles {
|
||||
pub status: StatusColors,
|
||||
|
||||
pub player: PlayerColors,
|
||||
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
}
|
||||
|
||||
|
@ -125,6 +125,12 @@ impl Theme {
|
||||
&self.styles.system
|
||||
}
|
||||
|
||||
/// Returns the [`AccentColors`] for the theme.
|
||||
#[inline(always)]
|
||||
pub fn accents(&self) -> &AccentColors {
|
||||
&self.styles.accents
|
||||
}
|
||||
|
||||
/// Returns the [`PlayerColors`] for the theme.
|
||||
#[inline(always)]
|
||||
pub fn players(&self) -> &PlayerColors {
|
||||
|
@ -57,6 +57,7 @@ impl VsCodeThemeConverter {
|
||||
appearance,
|
||||
style: ThemeStyleContent {
|
||||
window_background_appearance: Some(theme::WindowBackgroundContent::Opaque),
|
||||
accents: Vec::new(), //TODO can we read this from the theme?
|
||||
colors: theme_colors,
|
||||
status: status_colors,
|
||||
players: Vec::new(),
|
||||
|
Loading…
Reference in New Issue
Block a user