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
27 changed files with 1705 additions and 65 deletions
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue