Allow editor blocks to replace ranges of text (#19531)

This PR adds the ability for editor blocks to replace lines of text, but
does not yet use that feature anywhere. We'll update assistant patches
to use replace blocks on another branch:
https://github.com/zed-industries/zed/tree/assistant-patch-replace-blocks

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Richard Feldman <richard@zed.dev>
Co-authored-by: Marshall Bowers <marshall@zed.dev>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-10-25 03:29:25 -07:00 committed by GitHub
parent 3617873431
commit 08a3c54bac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1118 additions and 599 deletions

View file

@ -26,8 +26,8 @@ use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease,
CreaseMetadata, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
@ -2009,13 +2009,12 @@ impl ContextEditor {
})
.map(|(command, error_message)| BlockProperties {
style: BlockStyle::Fixed,
position: Anchor {
height: 1,
placement: BlockPlacement::Below(Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: command.source_range.start,
},
height: 1,
disposition: BlockDisposition::Below,
}),
render: slash_command_error_block_renderer(error_message),
priority: 0,
}),
@ -2242,11 +2241,10 @@ impl ContextEditor {
} else {
let block_ids = editor.insert_blocks(
[BlockProperties {
position: patch_start,
height: path_count as u32 + 1,
style: BlockStyle::Flex,
render: render_block,
disposition: BlockDisposition::Below,
placement: BlockPlacement::Below(patch_start),
priority: 0,
}],
None,
@ -2731,12 +2729,13 @@ impl ContextEditor {
})
};
let create_block_properties = |message: &Message| BlockProperties {
position: buffer
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
.unwrap(),
height: 2,
style: BlockStyle::Sticky,
disposition: BlockDisposition::Above,
placement: BlockPlacement::Above(
buffer
.anchor_in_excerpt(excerpt_id, message.anchor_range.start)
.unwrap(),
),
priority: usize::MAX,
render: render_block(MessageMetadata::from(message)),
};
@ -3372,7 +3371,7 @@ impl ContextEditor {
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
let image = render_image.clone();
anchor.is_valid(&buffer).then(|| BlockProperties {
position: anchor,
placement: BlockPlacement::Above(anchor),
height: MAX_HEIGHT_IN_LINES,
style: BlockStyle::Sticky,
render: Box::new(move |cx| {
@ -3393,8 +3392,6 @@ impl ContextEditor {
)
.into_any_element()
}),
disposition: BlockDisposition::Above,
priority: 0,
})
})

View file

@ -9,7 +9,7 @@ use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
actions::{MoveDown, MoveUp, SelectAll},
display_map::{
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
ToDisplayPoint,
},
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
@ -446,15 +446,14 @@ impl InlineAssistant {
let assist_blocks = vec![
BlockProperties {
style: BlockStyle::Sticky,
position: range.start,
placement: BlockPlacement::Above(range.start),
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
position: range.end,
placement: BlockPlacement::Below(range.end),
height: 0,
render: Box::new(|cx| {
v_flex()
@ -464,7 +463,6 @@ impl InlineAssistant {
.border_color(cx.theme().status().info_border)
.into_any_element()
}),
disposition: BlockDisposition::Below,
priority: 0,
},
];
@ -1179,7 +1177,7 @@ impl InlineAssistant {
let height =
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
new_blocks.push(BlockProperties {
position: new_row,
placement: BlockPlacement::Above(new_row),
height,
style: BlockStyle::Flex,
render: Box::new(move |cx| {
@ -1191,7 +1189,6 @@ impl InlineAssistant {
.child(deleted_lines_editor.clone())
.into_any_element()
}),
disposition: BlockDisposition::Above,
priority: 0,
});
}

View file

@ -9,7 +9,7 @@ use anyhow::Result;
use collections::{BTreeSet, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
highlight_diagnostic_message,
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
@ -439,11 +439,10 @@ impl ProjectDiagnosticsEditor {
primary.message.split('\n').next().unwrap().to_string();
group_state.block_count += 1;
blocks_to_add.push(BlockProperties {
position: header_position,
placement: BlockPlacement::Above(header_position),
height: 2,
style: BlockStyle::Sticky,
render: diagnostic_header_renderer(primary),
disposition: BlockDisposition::Above,
priority: 0,
});
}
@ -459,13 +458,15 @@ impl ProjectDiagnosticsEditor {
if !diagnostic.message.is_empty() {
group_state.block_count += 1;
blocks_to_add.push(BlockProperties {
position: (excerpt_id, entry.range.start),
placement: BlockPlacement::Below((
excerpt_id,
entry.range.start,
)),
height: diagnostic.message.matches('\n').count() as u32 + 1,
style: BlockStyle::Fixed,
render: diagnostic_block_renderer(
diagnostic, None, true, true,
),
disposition: BlockDisposition::Below,
priority: 0,
});
}
@ -498,13 +499,24 @@ impl ProjectDiagnosticsEditor {
editor.remove_blocks(blocks_to_remove, None, cx);
let block_ids = editor.insert_blocks(
blocks_to_add.into_iter().flat_map(|block| {
let (excerpt_id, text_anchor) = block.position;
let placement = match block.placement {
BlockPlacement::Above((excerpt_id, text_anchor)) => BlockPlacement::Above(
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
),
BlockPlacement::Below((excerpt_id, text_anchor)) => BlockPlacement::Below(
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
),
BlockPlacement::Replace(_) => {
unreachable!(
"no Replace block should have been pushed to blocks_to_add"
)
}
};
Some(BlockProperties {
position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
placement,
height: block.height,
style: block.style,
render: block.render,
disposition: block.disposition,
priority: 0,
})
}),

View file

@ -29,8 +29,8 @@ use crate::{
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
};
pub use block_map::{
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use char_map::{CharMap, CharSnapshot};
@ -1180,6 +1180,7 @@ impl ToDisplayPoint for Anchor {
pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use block_map::BlockPlacement;
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
@ -1293,24 +1294,22 @@ pub mod tests {
Bias::Left,
));
let disposition = if rng.gen() {
BlockDisposition::Above
let placement = if rng.gen() {
BlockPlacement::Above(position)
} else {
BlockDisposition::Below
BlockPlacement::Below(position)
};
let height = rng.gen_range(1..5);
log::info!(
"inserting block {:?} {:?} with height {}",
disposition,
position.to_point(&buffer),
"inserting block {:?} with height {}",
placement.as_ref().map(|p| p.to_point(&buffer)),
height
);
let priority = rng.gen_range(1..100);
BlockProperties {
placement,
style: BlockStyle::Fixed,
position,
height,
disposition,
render: Box::new(|_| div().into_any()),
priority,
}

File diff suppressed because it is too large Load diff

View file

@ -252,6 +252,7 @@ impl CharSnapshot {
};
TabChunks {
snapshot: self,
fold_chunks: self.fold_snapshot.chunks(
input_start..input_end,
language_aware,
@ -492,6 +493,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
snapshot: &'a CharSnapshot,
fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
@ -503,6 +505,37 @@ pub struct TabChunks<'a> {
inside_leading_tab: bool,
}
impl<'a> TabChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<CharPoint>) {
let (input_start, expanded_char_column, to_next_stop) =
self.snapshot.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
let input_start = input_start.to_offset(&self.snapshot.fold_snapshot);
let input_end = self
.snapshot
.to_fold_point(range.end, Bias::Right)
.0
.to_offset(&self.snapshot.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
to_next_stop
};
self.fold_chunks.seek(input_start..input_end);
self.input_column = input_column;
self.column = expanded_char_column;
self.output_position = range.start.0;
self.max_output_position = range.end.0;
self.chunk = Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
..Default::default()
};
self.inside_leading_tab = to_next_stop > 0;
}
}
impl<'a> Iterator for TabChunks<'a> {
type Item = Chunk<'a>;

View file

@ -1100,6 +1100,17 @@ pub struct FoldBufferRows<'a> {
fold_point: FoldPoint,
}
impl<'a> FoldBufferRows<'a> {
pub(crate) fn seek(&mut self, row: u32) {
let fold_point = FoldPoint::new(row, 0);
self.cursor.seek(&fold_point, Bias::Left, &());
let overshoot = fold_point.0 - self.cursor.start().0 .0;
let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot);
self.input_buffer_rows.seek(inlay_point.row());
self.fold_point = fold_point;
}
}
impl<'a> Iterator for FoldBufferRows<'a> {
type Item = Option<u32>;
@ -1135,6 +1146,38 @@ pub struct FoldChunks<'a> {
max_output_offset: FoldOffset,
}
impl<'a> FoldChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<FoldOffset>) {
self.transform_cursor.seek(&range.start, Bias::Right, &());
let inlay_start = {
let overshoot = range.start.0 - self.transform_cursor.start().0 .0;
self.transform_cursor.start().1 + InlayOffset(overshoot)
};
let transform_end = self.transform_cursor.end(&());
let inlay_end = if self
.transform_cursor
.item()
.map_or(true, |transform| transform.is_fold())
{
inlay_start
} else if range.end < transform_end.0 {
let overshoot = range.end.0 - self.transform_cursor.start().0 .0;
self.transform_cursor.start().1 + InlayOffset(overshoot)
} else {
transform_end.1
};
self.inlay_chunks.seek(inlay_start..inlay_end);
self.inlay_chunk = None;
self.inlay_offset = inlay_start;
self.output_offset = range.start;
self.max_output_offset = range.end;
}
}
impl<'a> Iterator for FoldChunks<'a> {
type Item = Chunk<'a>;

View file

@ -56,6 +56,7 @@ pub struct WrapChunks<'a> {
output_position: WrapPoint,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
snapshot: &'a WrapSnapshot,
}
#[derive(Clone)]
@ -68,6 +69,21 @@ pub struct WrapBufferRows<'a> {
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
}
impl<'a> WrapBufferRows<'a> {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = self.transforms.start().1.row();
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_row += start_row - self.transforms.start().0.row();
}
self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic());
self.input_buffer_rows.seek(input_row);
self.input_buffer_row = self.input_buffer_rows.next().unwrap();
self.output_row = start_row;
}
}
impl WrapMap {
pub fn new(
char_snapshot: CharSnapshot,
@ -602,6 +618,7 @@ impl WrapSnapshot {
output_position: output_start,
max_output_row: rows.end,
transforms,
snapshot: self,
}
}
@ -629,6 +646,67 @@ impl WrapSnapshot {
}
}
pub fn text_summary_for_range(&self, rows: Range<u32>) -> TextSummary {
let mut summary = TextSummary::default();
let start = WrapPoint::new(rows.start, 0);
let end = WrapPoint::new(rows.end, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
cursor.seek(&start, Bias::Right, &());
if let Some(transform) = cursor.item() {
let start_in_transform = start.0 - cursor.start().0 .0;
let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = CharPoint(cursor.start().1 .0 + start_in_transform);
let char_end = CharPoint(cursor.start().1 .0 + end_in_transform);
summary += &self
.char_snapshot
.text_summary_for_range(char_start..char_end);
} else {
debug_assert_eq!(start_in_transform.row, end_in_transform.row);
let indent_len = end_in_transform.column - start_in_transform.column;
summary += &TextSummary {
lines: Point::new(0, indent_len),
first_line_chars: indent_len,
last_line_chars: indent_len,
longest_row: 0,
longest_row_chars: indent_len,
};
}
cursor.next(&());
}
if rows.end > cursor.start().0.row() {
summary += &cursor
.summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &())
.output;
if let Some(transform) = cursor.item() {
let end_in_transform = end.0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = cursor.start().1;
let char_end = CharPoint(char_start.0 + end_in_transform);
summary += &self
.char_snapshot
.text_summary_for_range(char_start..char_end);
} else {
debug_assert_eq!(end_in_transform, Point::new(1, 0));
summary += &TextSummary {
lines: Point::new(1, 0),
first_line_chars: 0,
last_line_chars: 0,
longest_row: 0,
longest_row_chars: 0,
};
}
}
}
summary
}
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
let mut cursor = self.transforms.cursor::<WrapPoint>(&());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
@ -745,6 +823,21 @@ impl WrapSnapshot {
None
}
#[cfg(test)]
pub fn text(&self) -> String {
self.text_chunks(0).collect()
}
#[cfg(test)]
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
self.chunks(
wrap_row..self.max_point().row() + 1,
false,
Highlights::default(),
)
.map(|h| h.text)
}
fn check_invariants(&self) {
#[cfg(test)]
{
@ -791,6 +884,26 @@ impl WrapSnapshot {
}
}
impl<'a> WrapChunks<'a> {
pub(crate) fn seek(&mut self, rows: Range<u32>) {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right, &());
let mut input_start = CharPoint(self.transforms.start().1 .0);
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - self.transforms.start().0 .0;
}
let input_end = self
.snapshot
.to_char_point(output_end)
.min(self.snapshot.char_snapshot.max_point());
self.input_chunks.seek(input_start..input_end);
self.input_chunk = Chunk::default();
self.output_position = output_start;
self.max_output_row = rows.end;
}
}
impl<'a> Iterator for WrapChunks<'a> {
type Item = Chunk<'a>;
@ -1336,19 +1449,6 @@ mod tests {
}
impl WrapSnapshot {
pub fn text(&self) -> String {
self.text_chunks(0).collect()
}
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
self.chunks(
wrap_row..self.max_point().row() + 1,
false,
Highlights::default(),
)
.map(|h| h.text)
}
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
let mut end_row = rng.gen_range(0..=self.max_point().row());

View file

@ -10210,7 +10210,7 @@ impl Editor {
let block_id = this.insert_blocks(
[BlockProperties {
style: BlockStyle::Flex,
position: range.start,
placement: BlockPlacement::Below(range.start),
height: 1,
render: Box::new({
let rename_editor = rename_editor.clone();
@ -10246,7 +10246,6 @@ impl Editor {
.into_any_element()
}
}),
disposition: BlockDisposition::Below,
priority: 0,
}],
Some(Autoscroll::fit()),
@ -10531,10 +10530,11 @@ impl Editor {
let message_height = diagnostic.message.matches('\n').count() as u32 + 1;
BlockProperties {
style: BlockStyle::Fixed,
position: buffer.anchor_after(entry.range.start),
placement: BlockPlacement::Below(
buffer.anchor_after(entry.range.start),
),
height: message_height,
render: diagnostic_block_renderer(diagnostic, None, true, true),
disposition: BlockDisposition::Below,
priority: 0,
}
}),

View file

@ -3868,8 +3868,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
editor.insert_blocks(
[BlockProperties {
style: BlockStyle::Fixed,
position: snapshot.anchor_after(Point::new(2, 0)),
disposition: BlockDisposition::Below,
placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
height: 1,
render: Box::new(|_| div().into_any()),
priority: 0,

View file

@ -2071,7 +2071,7 @@ impl EditorElement {
let mut element = match block {
Block::Custom(block) => {
let align_to = block
.position()
.start()
.to_point(&snapshot.buffer_snapshot)
.to_display_point(snapshot);
let anchor_x = text_x
@ -6294,7 +6294,7 @@ fn compute_auto_height_layout(
mod tests {
use super::*;
use crate::{
display_map::{BlockDisposition, BlockProperties},
display_map::{BlockPlacement, BlockProperties},
editor_tests::{init_test, update_test_language_settings},
Editor, MultiBuffer,
};
@ -6550,9 +6550,8 @@ mod tests {
editor.insert_blocks(
[BlockProperties {
style: BlockStyle::Fixed,
disposition: BlockDisposition::Above,
placement: BlockPlacement::Above(Anchor::min()),
height: 3,
position: Anchor::min(),
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
priority: 0,
}],

View file

@ -17,7 +17,7 @@ use workspace::Item;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
@ -417,10 +417,9 @@ impl Editor {
};
BlockProperties {
position: hunk.multi_buffer_range.start,
placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
height: 1,
style: BlockStyle::Sticky,
disposition: BlockDisposition::Above,
priority: 0,
render: Box::new({
let editor = cx.view().clone();
@ -700,10 +699,9 @@ impl Editor {
let hunk = hunk.clone();
let height = editor_height.max(deleted_text_height);
BlockProperties {
position: hunk.multi_buffer_range.start,
placement: BlockPlacement::Above(hunk.multi_buffer_range.start),
height,
style: BlockStyle::Flex,
disposition: BlockDisposition::Above,
priority: 0,
render: Box::new(move |cx| {
let width = EditorElement::diff_hunk_strip_width(cx.line_height());

View file

@ -8,7 +8,7 @@ use client::telemetry::Telemetry;
use collections::{HashMap, HashSet};
use editor::{
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId,
RenderBlock,
},
scroll::Autoscroll,
@ -90,12 +90,11 @@ impl EditorBlock {
let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
let block = BlockProperties {
position: code_range.end,
placement: BlockPlacement::Below(code_range.end),
// Take up at least one height for status, allow the editor to determine the real height based on the content from render
height: 1,
style: BlockStyle::Sticky,
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
disposition: BlockDisposition::Below,
priority: 0,
};